Skip to content

Commit ccc10f6

Browse files
committed
feat(graphql): integrate mutation engine and TypeMaps into emitter
1 parent f7a355e commit ccc10f6

File tree

3 files changed

+89
-80
lines changed

3 files changed

+89
-80
lines changed

packages/graphql/src/registry.ts

Lines changed: 51 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,84 @@
11
import { UsageFlags, type Enum, type Model } from "@typespec/compiler";
2-
import {
3-
GraphQLBoolean,
4-
GraphQLEnumType,
5-
GraphQLObjectType,
6-
type GraphQLNamedType,
7-
type GraphQLSchemaConfig,
8-
} from "graphql";
2+
import { GraphQLBoolean, GraphQLObjectType, type GraphQLSchemaConfig } from "graphql";
3+
import { EnumTypeMap, ModelTypeMap, type TypeKey } from "./type-maps/index.js";
94

10-
// The TSPTypeContext interface represents the intermediate TSP type information before materialization.
11-
// It stores the raw TSP type and any extracted metadata relevant for GraphQL generation.
12-
interface TSPTypeContext {
13-
tspType: Enum | Model; // Extend with other TSP types like Operation, Interface, TSP Union, etc.
14-
name: string;
15-
usageFlags?: Set<UsageFlags>;
16-
// TODO: Add any other TSP-specific metadata here.
17-
}
185
/**
196
* GraphQLTypeRegistry manages the registration and materialization of TypeSpec (TSP)
207
* types into their corresponding GraphQL type definitions.
218
*
229
* The registry operates in a two-stage process:
2310
* 1. Registration: TSP types (like Enums, Models, etc.) are first registered
2411
* along with relevant metadata (e.g., name, usage flags). This stores an
25-
* intermediate representation (`TSPTypeContext`) without immediately creating
26-
* GraphQL types. This stage is typically performed while traversing the TSP AST.
27-
* Register type by calling the appropriate method (e.g., `addEnum`).
12+
* intermediate representation without immediately creating GraphQL types.
2813
*
2914
* 2. Materialization: When a GraphQL type is needed (e.g., to build the final
30-
* schema or resolve a field type), the registry can materialize the TSP type
15+
* schema or resolve a field type), the registry materializes the TSP type
3116
* into its GraphQL counterpart (e.g., `GraphQLEnumType`, `GraphQLObjectType`).
32-
* Materialize types by calling the appropriate method (e.g., `materializeEnum`).
3317
*
3418
* This approach helps in:
3519
* - Decoupling TSP AST traversal from GraphQL object instantiation.
3620
* - Caching materialized GraphQL types to avoid redundant work and ensure object identity.
3721
* - Handling forward references and circular dependencies, as types can be
38-
* registered first and materialized later when all dependencies are known or
39-
* by using thunks for fields/arguments.
22+
* registered first and materialized later when all dependencies are known.
4023
*/
4124
export class GraphQLTypeRegistry {
42-
// Stores intermediate TSP type information, keyed by TSP type name.
43-
// TODO: make this more of a seen set
44-
private TSPTypeContextRegistry: Map<string, TSPTypeContext> = new Map();
45-
46-
// Stores materialized GraphQL types, keyed by their GraphQL name.
47-
private materializedGraphQLTypes: Map<string, GraphQLNamedType> = new Map();
25+
// TypeMaps for each GraphQL type kind
26+
private enumMap = new EnumTypeMap();
27+
private modelMap = new ModelTypeMap();
4828

29+
/**
30+
* Register a TypeSpec Enum for later materialization.
31+
*/
4932
addEnum(tspEnum: Enum): void {
50-
const enumName = tspEnum.name;
51-
if (this.TSPTypeContextRegistry.has(enumName)) {
52-
// Optionally, log a warning or update if new information is more complete.
53-
return;
33+
if (!this.enumMap.isRegistered(tspEnum.name)) {
34+
this.enumMap.register({
35+
type: tspEnum,
36+
usageFlag: UsageFlags.None,
37+
metadata: {},
38+
});
5439
}
55-
56-
this.TSPTypeContextRegistry.set(enumName, {
57-
tspType: tspEnum,
58-
name: enumName,
59-
// TODO: Populate usageFlags based on TSP context and other decorator context.
60-
});
6140
}
6241

63-
// Materializes a TSP Enum into a GraphQLEnumType.
64-
materializeEnum(enumName: string): GraphQLEnumType | undefined {
65-
// Check if the GraphQL type is already materialized.
66-
if (this.materializedGraphQLTypes.has(enumName)) {
67-
return this.materializedGraphQLTypes.get(enumName) as GraphQLEnumType;
68-
}
69-
70-
const context = this.TSPTypeContextRegistry.get(enumName);
71-
if (!context || context.tspType.kind !== "Enum") {
72-
// TODO: Handle error or warning for missing context.
73-
return undefined;
42+
/**
43+
* Register a TypeSpec Model for later materialization.
44+
*/
45+
addModel(tspModel: Model): void {
46+
if (!this.modelMap.isRegistered(tspModel.name)) {
47+
this.modelMap.register({
48+
type: tspModel,
49+
usageFlag: UsageFlags.None,
50+
metadata: {},
51+
});
7452
}
53+
}
7554

76-
const tspEnum = context.tspType as Enum;
77-
78-
const gqlEnum = new GraphQLEnumType({
79-
name: context.name,
80-
values: Object.fromEntries(
81-
Array.from(tspEnum.members.values()).map((member) => [
82-
member.name,
83-
{
84-
value: member.value ?? member.name,
85-
},
86-
]),
87-
),
88-
});
55+
/**
56+
* Materialize a registered enum into a GraphQLEnumType.
57+
*/
58+
materializeEnum(enumName: string): void {
59+
this.enumMap.get(enumName as TypeKey);
60+
}
8961

90-
this.materializedGraphQLTypes.set(enumName, gqlEnum);
91-
return gqlEnum;
62+
/**
63+
* Materialize a registered model into a GraphQLObjectType.
64+
*/
65+
materializeModel(modelName: string): void {
66+
this.modelMap.get(modelName as TypeKey);
9267
}
9368

69+
/**
70+
* Build the final GraphQL schema configuration.
71+
*/
9472
materializeSchemaConfig(): GraphQLSchemaConfig {
95-
const allMaterializedGqlTypes = Array.from(this.materializedGraphQLTypes.values());
96-
let queryType = this.materializedGraphQLTypes.get("Query") as GraphQLObjectType | undefined;
73+
// Collect all materialized types from all type maps
74+
const allMaterializedGqlTypes = [
75+
...this.enumMap.getAllMaterialized(),
76+
...this.modelMap.getAllMaterialized(),
77+
];
78+
79+
let queryType = undefined as GraphQLObjectType | undefined;
80+
// TODO: Get query type from operations when implemented
81+
9782
if (!queryType) {
9883
queryType = new GraphQLObjectType({
9984
name: "Query",

packages/graphql/src/schema-emitter.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { GraphQLSchema, validateSchema } from "graphql";
1111
import { type GraphQLEmitterOptions } from "./lib.js";
1212
import type { Schema } from "./lib/schema.js";
13+
import { createGraphQLMutationEngine, type GraphQLMutationEngine } from "./mutation-engine/index.js";
1314
import { GraphQLTypeRegistry } from "./registry.js";
1415

1516
class GraphQLSchemaEmitter {
@@ -18,12 +19,13 @@ class GraphQLSchemaEmitter {
1819
private options: GraphQLEmitterOptions;
1920
private diagnostics: DiagnosticCollector;
2021
private registry: GraphQLTypeRegistry;
22+
private engine: GraphQLMutationEngine | null = null;
23+
2124
constructor(
2225
tspSchema: Schema,
2326
context: EmitContext<GraphQLEmitterOptions>,
2427
options: GraphQLEmitterOptions,
2528
) {
26-
// Initialize any properties if needed, including the registry
2729
this.tspSchema = tspSchema;
2830
this.context = context;
2931
this.options = options;
@@ -32,12 +34,16 @@ class GraphQLSchemaEmitter {
3234
}
3335

3436
async emitSchema(): Promise<[GraphQLSchema, Readonly<Diagnostic[]>] | undefined> {
35-
const schemaNamespace = this.tspSchema.type;
36-
// Logic to emit the GraphQL schema
37-
navigateTypesInNamespace(schemaNamespace, this.semanticNodeListener());
37+
// Create mutation engine with the schema namespace
38+
this.engine = createGraphQLMutationEngine(this.context.program, this.tspSchema.type);
39+
40+
// Navigate the original namespace, mutate on-demand via engine
41+
navigateTypesInNamespace(this.tspSchema.type, this.semanticNodeListener());
42+
3843
const schemaConfig = this.registry.materializeSchemaConfig();
3944
const schema = new GraphQLSchema(schemaConfig);
40-
// validate the schema
45+
46+
// Validate the schema
4147
const validationErrors = validateSchema(schema);
4248
validationErrors.forEach((error) => {
4349
this.diagnostics.add({
@@ -51,19 +57,26 @@ class GraphQLSchemaEmitter {
5157
}
5258

5359
semanticNodeListener() {
54-
// TODO: Add GraphQL types to registry as the TSP nodes are visited
5560
return {
5661
enum: (node: Enum) => {
57-
this.registry.addEnum(node);
62+
// Mutate the enum (applies name sanitization)
63+
const mutation = this.engine!.mutateEnum(node);
64+
this.registry.addEnum(mutation.mutatedType);
5865
},
5966
model: (node: Model) => {
60-
// Add logic to handle the model node
67+
// Mutate the model (applies name sanitization)
68+
const mutation = this.engine!.mutateModel(node);
69+
this.registry.addModel(mutation.mutatedType);
6170
},
6271
exitEnum: (node: Enum) => {
63-
this.registry.materializeEnum(node.name);
72+
// Use mutated name for materialization
73+
const mutation = this.engine!.mutateEnum(node);
74+
this.registry.materializeEnum(mutation.mutatedType.name);
6475
},
6576
exitModel: (node: Model) => {
66-
// Add logic to handle the exit of the model node
77+
// Use mutated name for materialization
78+
const mutation = this.engine!.mutateModel(node);
79+
this.registry.materializeModel(mutation.mutatedType.name);
6780
},
6881
};
6982
}
@@ -74,7 +87,6 @@ export function createSchemaEmitter(
7487
context: EmitContext<GraphQLEmitterOptions>,
7588
options: GraphQLEmitterOptions,
7689
): GraphQLSchemaEmitter {
77-
// Placeholder for creating a GraphQL schema emitter
7890
return new GraphQLSchemaEmitter(schema, context, options);
7991
}
8092

packages/graphql/test/emitter.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,21 @@ import { strictEqual } from "node:assert";
22
import { describe, it } from "vitest";
33
import { emitSingleSchema } from "./test-host.js";
44

5-
// For now, the expected output is a placeholder string.
6-
// In the future, this should be replaced with the actual GraphQL schema output.
7-
const expectedGraphQLSchema = `type Query {
5+
// Expected output with models. Note: field types are placeholders (String) until
6+
// type resolution is fully implemented.
7+
const expectedGraphQLSchema = `type Book {
8+
name: String
9+
page_count: String
10+
published: String
11+
price: String
12+
}
13+
14+
type Author {
15+
name: String
16+
books: String
17+
}
18+
19+
type Query {
820
"""
921
A placeholder field. If you are seeing this, it means no operations were defined that could be emitted.
1022
"""

0 commit comments

Comments
 (0)