Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 55 additions & 9 deletions packages/graphql/src/schema-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import {
createDiagnosticCollector,
isArrayModelType,
isRecordModelType,
navigateType,
navigateTypesInNamespace,
UsageFlags,
type Diagnostic,
type DiagnosticCollector,
type EmitContext,
type Enum,
type Model,
type Type,
} from "@typespec/compiler";
import { GraphQLSchema, validateSchema } from "graphql";
import { type GraphQLEmitterOptions } from "./lib.js";
import type { Schema } from "./lib/schema.js";
import {
createGraphQLMutationEngine,
type GraphQLMutationEngine,
} from "./mutation-engine/index.js";
import { GraphQLTypeRegistry } from "./registry.js";

class GraphQLSchemaEmitter {
Expand All @@ -19,26 +27,36 @@ class GraphQLSchemaEmitter {
private options: GraphQLEmitterOptions;
private diagnostics: DiagnosticCollector;
private registry: GraphQLTypeRegistry;
private engine: GraphQLMutationEngine;

constructor(
tspSchema: Schema,
context: EmitContext<GraphQLEmitterOptions>,
options: GraphQLEmitterOptions,
) {
// Initialize any properties if needed, including the registry
this.tspSchema = tspSchema;
this.context = context;
this.options = options;
this.diagnostics = createDiagnosticCollector();
this.registry = new GraphQLTypeRegistry();
this.engine = createGraphQLMutationEngine(context.program, tspSchema.type);
}

async emitSchema(): Promise<[GraphQLSchema, Readonly<Diagnostic[]>] | undefined> {
const schemaNamespace = this.tspSchema.type;
// Logic to emit the GraphQL schema
navigateTypesInNamespace(schemaNamespace, this.semanticNodeListener());
// Pass 1: Mutation - collect all mutated types
const mutatedTypes: Type[] = [];
navigateTypesInNamespace(this.tspSchema.type, this.mutationListeners(mutatedTypes));

// Pass 2: Emission - navigate mutated types to register and materialize
const emissionListeners = this.emissionListeners();
for (const type of mutatedTypes) {
navigateType(type, emissionListeners, {});
}

const schemaConfig = this.registry.materializeSchemaConfig();
const schema = new GraphQLSchema(schemaConfig);
// validate the schema

// Validate the schema
const validationErrors = validateSchema(schema);
validationErrors.forEach((error) => {
this.diagnostics.add({
Expand All @@ -51,20 +69,49 @@ class GraphQLSchemaEmitter {
return [schema, this.diagnostics.diagnostics];
}

semanticNodeListener() {
// TODO: Add GraphQL types to registry as the TSP nodes are visited
/**
* Pass 1: Mutation listeners - mutate types and collect them
*/
mutationListeners(mutatedTypes: Type[]) {
return {
enum: (node: Enum) => {
const mutation = this.engine.mutateEnum(node);
mutatedTypes.push(mutation.mutatedType);
},
model: (node: Model) => {
const mutation = this.engine.mutateModel(node);
mutatedTypes.push(mutation.mutatedType);
},
};
}
Comment on lines +72 to +86

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused as to what the point of the engine is if we have to enumerate its mutations explicitly. Can you explain?


/**
* Pass 2: Emission listeners - register and materialize mutated types
*/
emissionListeners() {
return {
enum: (node: Enum) => {
this.registry.addEnum(node);
},
model: (node: Model) => {
// TODO: Determine usageFlag from mutation engine or usage tracking
if (
isArrayModelType(this.context.program, node) ||
isRecordModelType(this.context.program, node)
) {
return;
}
this.registry.addModel(node, UsageFlags.Output);
},
exitEnum: (node: Enum) => {
this.registry.materializeEnum(node.name);
},
exitModel: (node: Model) => {
if (
isArrayModelType(this.context.program, node) ||
isRecordModelType(this.context.program, node)
) {
return;
}
this.registry.materializeModel(node.name);
},
};
Expand All @@ -76,7 +123,6 @@ export function createSchemaEmitter(
context: EmitContext<GraphQLEmitterOptions>,
options: GraphQLEmitterOptions,
): GraphQLSchemaEmitter {
// Placeholder for creating a GraphQL schema emitter
return new GraphQLSchemaEmitter(schema, context, options);
}

Expand Down
21 changes: 18 additions & 3 deletions packages/graphql/test/emitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import { strictEqual } from "node:assert";
import { describe, it } from "vitest";
import { emitSingleSchema } from "./test-host.js";

const expectedGraphQLSchema = `type Book {
// Expected output with models and enums. Note: field types are placeholders (String) until
// type resolution is fully implemented.
const expectedGraphQLSchema = `enum Genre {
_Fiction_
NonFiction
Mystery
Fantasy
}

type Book {
name: String
page_count: String
published: String
Expand All @@ -21,8 +30,8 @@ type Query {
_: Boolean
}`;

describe("name", () => {
it("Emits a schema.graphql file with placeholder text", async () => {
describe("emitter", () => {
it("emits models and enums with mutations applied", async () => {
const code = `
@schema
namespace TestNamespace {
Expand All @@ -36,6 +45,12 @@ describe("name", () => {
name: string;
books: Book[];
}
enum Genre {
$Fiction$,
NonFiction,
Mystery,
Fantasy,
}
op getBooks(): Book[];
op getAuthors(): Author[];
}
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/test/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace MyLibrary {
}

enum Genre {
Fiction,
$Fiction$,
NonFiction,
Mystery,
Fantasy,
Expand Down