From bcfae7d3e8225b60d2b4ea3a78849f64c0dcf7ba Mon Sep 17 00:00:00 2001 From: Fiona Date: Mon, 5 Jan 2026 18:10:28 -0500 Subject: [PATCH 1/2] feat(graphql): integrate mutation engine and TypeMaps into emitter --- packages/graphql/src/schema-emitter.ts | 33 ++++++++++++++++---------- packages/graphql/test/emitter.test.ts | 21 +++++++++++++--- packages/graphql/test/main.tsp | 2 +- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/packages/graphql/src/schema-emitter.ts b/packages/graphql/src/schema-emitter.ts index fbb04e26dd8..2f86a727f24 100644 --- a/packages/graphql/src/schema-emitter.ts +++ b/packages/graphql/src/schema-emitter.ts @@ -11,6 +11,10 @@ import { 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 { @@ -19,26 +23,29 @@ class GraphQLSchemaEmitter { private options: GraphQLEmitterOptions; private diagnostics: DiagnosticCollector; private registry: GraphQLTypeRegistry; + private engine: GraphQLMutationEngine; + constructor( tspSchema: Schema, context: EmitContext, 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] | undefined> { - const schemaNamespace = this.tspSchema.type; - // Logic to emit the GraphQL schema - navigateTypesInNamespace(schemaNamespace, this.semanticNodeListener()); + // Navigate the original namespace, mutate on-demand via engine + navigateTypesInNamespace(this.tspSchema.type, this.semanticNodeListener()); + 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({ @@ -52,20 +59,23 @@ class GraphQLSchemaEmitter { } semanticNodeListener() { - // TODO: Add GraphQL types to registry as the TSP nodes are visited return { enum: (node: Enum) => { - this.registry.addEnum(node); + const mutation = this.engine.mutateEnum(node); + this.registry.addEnum(mutation.mutatedType); }, model: (node: Model) => { - // TODO: Determine usageFlag from mutation engine or usage tracking - this.registry.addModel(node, UsageFlags.Output); + const mutation = this.engine.mutateModel(node); + // TODO: Handle input/output variants + this.registry.addModel(mutation.mutatedType, UsageFlags.Output); }, exitEnum: (node: Enum) => { - this.registry.materializeEnum(node.name); + const mutation = this.engine.mutateEnum(node); + this.registry.materializeEnum(mutation.mutatedType.name); }, exitModel: (node: Model) => { - this.registry.materializeModel(node.name); + const mutation = this.engine.mutateModel(node); + this.registry.materializeModel(mutation.mutatedType.name); }, }; } @@ -76,7 +86,6 @@ export function createSchemaEmitter( context: EmitContext, options: GraphQLEmitterOptions, ): GraphQLSchemaEmitter { - // Placeholder for creating a GraphQL schema emitter return new GraphQLSchemaEmitter(schema, context, options); } diff --git a/packages/graphql/test/emitter.test.ts b/packages/graphql/test/emitter.test.ts index 19dafc6fa50..553b64fa500 100644 --- a/packages/graphql/test/emitter.test.ts +++ b/packages/graphql/test/emitter.test.ts @@ -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 @@ -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 { @@ -36,6 +45,12 @@ describe("name", () => { name: string; books: Book[]; } + enum Genre { + $Fiction$, + NonFiction, + Mystery, + Fantasy, + } op getBooks(): Book[]; op getAuthors(): Author[]; } diff --git a/packages/graphql/test/main.tsp b/packages/graphql/test/main.tsp index ea4ae208f92..cc61607b7fa 100644 --- a/packages/graphql/test/main.tsp +++ b/packages/graphql/test/main.tsp @@ -19,7 +19,7 @@ namespace MyLibrary { } enum Genre { - Fiction, + $Fiction$, NonFiction, Mystery, Fantasy, From 972e4046af8a642b65c0657be12ec03c9a5b660b Mon Sep 17 00:00:00 2001 From: Fiona Date: Wed, 28 Jan 2026 18:45:59 -0500 Subject: [PATCH 2/2] refactor mutator engine usage into a separate pass of the program --- packages/graphql/src/schema-emitter.ts | 57 +++++++++++++++++++++----- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/graphql/src/schema-emitter.ts b/packages/graphql/src/schema-emitter.ts index 2f86a727f24..dd090ce06f9 100644 --- a/packages/graphql/src/schema-emitter.ts +++ b/packages/graphql/src/schema-emitter.ts @@ -1,5 +1,8 @@ import { createDiagnosticCollector, + isArrayModelType, + isRecordModelType, + navigateType, navigateTypesInNamespace, UsageFlags, type Diagnostic, @@ -7,6 +10,7 @@ import { type EmitContext, type Enum, type Model, + type Type, } from "@typespec/compiler"; import { GraphQLSchema, validateSchema } from "graphql"; import { type GraphQLEmitterOptions } from "./lib.js"; @@ -39,8 +43,15 @@ class GraphQLSchemaEmitter { } async emitSchema(): Promise<[GraphQLSchema, Readonly] | undefined> { - // Navigate the original namespace, mutate on-demand via engine - navigateTypesInNamespace(this.tspSchema.type, 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); @@ -58,24 +69,50 @@ class GraphQLSchemaEmitter { return [schema, this.diagnostics.diagnostics]; } - semanticNodeListener() { + /** + * Pass 1: Mutation listeners - mutate types and collect them + */ + mutationListeners(mutatedTypes: Type[]) { return { enum: (node: Enum) => { const mutation = this.engine.mutateEnum(node); - this.registry.addEnum(mutation.mutatedType); + mutatedTypes.push(mutation.mutatedType); }, model: (node: Model) => { const mutation = this.engine.mutateModel(node); - // TODO: Handle input/output variants - this.registry.addModel(mutation.mutatedType, UsageFlags.Output); + mutatedTypes.push(mutation.mutatedType); + }, + }; + } + + /** + * Pass 2: Emission listeners - register and materialize mutated types + */ + emissionListeners() { + return { + enum: (node: Enum) => { + this.registry.addEnum(node); + }, + model: (node: Model) => { + if ( + isArrayModelType(this.context.program, node) || + isRecordModelType(this.context.program, node) + ) { + return; + } + this.registry.addModel(node, UsageFlags.Output); }, exitEnum: (node: Enum) => { - const mutation = this.engine.mutateEnum(node); - this.registry.materializeEnum(mutation.mutatedType.name); + this.registry.materializeEnum(node.name); }, exitModel: (node: Model) => { - const mutation = this.engine.mutateModel(node); - this.registry.materializeModel(mutation.mutatedType.name); + if ( + isArrayModelType(this.context.program, node) || + isRecordModelType(this.context.program, node) + ) { + return; + } + this.registry.materializeModel(node.name); }, }; }