diff --git a/packages/graphql/src/registry.ts b/packages/graphql/src/registry.ts index e4ae4c57f3b..26a3980e9fc 100644 --- a/packages/graphql/src/registry.ts +++ b/packages/graphql/src/registry.ts @@ -1,13 +1,14 @@ -import { UsageFlags, type Enum } from "@typespec/compiler"; +import { UsageFlags, type Enum, type Model } from "@typespec/compiler"; import { GraphQLBoolean, GraphQLEnumType, + GraphQLInputObjectType, GraphQLObjectType, type GraphQLNamedType, type GraphQLSchemaConfig, } from "graphql"; import { type TypeKey } from "./type-maps.js"; -import { EnumTypeMap } from "./type-maps/index.js"; +import { EnumTypeMap, ModelTypeMap } from "./type-maps/index.js"; /** * GraphQLTypeRegistry manages the registration and materialization of TypeSpec (TSP) * types into their corresponding GraphQL type definitions. @@ -32,8 +33,9 @@ import { EnumTypeMap } from "./type-maps/index.js"; * by using thunks for fields/arguments. */ export class GraphQLTypeRegistry { - // TypeMap for enum types + // TypeMaps for each type kind private enumTypeMap = new EnumTypeMap(); + private modelTypeMap = new ModelTypeMap(); // Track all registered names to detect cross-TypeMap name collisions private allRegisteredNames = new Set(); @@ -55,14 +57,36 @@ export class GraphQLTypeRegistry { this.allRegisteredNames.add(enumName); } - // Materializes a TSP Enum into a GraphQLEnumType. + addModel(tspModel: Model, usageFlag: UsageFlags): void { + const modelName = tspModel.name; + + // Check for duplicate names across all type maps + if (this.allRegisteredNames.has(modelName)) { + // Already registered (could be same model or name collision) + return; + } + + this.modelTypeMap.register({ + type: tspModel, + usageFlag, + }); + this.allRegisteredNames.add(modelName); + } + materializeEnum(enumName: string): GraphQLEnumType | undefined { return this.enumTypeMap.get(enumName as TypeKey); } + materializeModel(modelName: string): GraphQLObjectType | GraphQLInputObjectType | undefined { + return this.modelTypeMap.get(modelName as TypeKey); + } + materializeSchemaConfig(): GraphQLSchemaConfig { // Collect all materialized types from all TypeMaps - const allMaterializedGqlTypes: GraphQLNamedType[] = [...this.enumTypeMap.getAllMaterialized()]; + const allMaterializedGqlTypes: GraphQLNamedType[] = [ + ...this.enumTypeMap.getAllMaterialized(), + ...this.modelTypeMap.getAllMaterialized(), + ]; // TODO: Query type will come from operations let queryType: GraphQLObjectType | undefined = undefined; if (!queryType) { diff --git a/packages/graphql/src/schema-emitter.ts b/packages/graphql/src/schema-emitter.ts index b922fe49b0a..fbb04e26dd8 100644 --- a/packages/graphql/src/schema-emitter.ts +++ b/packages/graphql/src/schema-emitter.ts @@ -1,6 +1,7 @@ import { createDiagnosticCollector, navigateTypesInNamespace, + UsageFlags, type Diagnostic, type DiagnosticCollector, type EmitContext, @@ -57,13 +58,14 @@ class GraphQLSchemaEmitter { this.registry.addEnum(node); }, model: (node: Model) => { - // Add logic to handle the model node + // TODO: Determine usageFlag from mutation engine or usage tracking + this.registry.addModel(node, UsageFlags.Output); }, exitEnum: (node: Enum) => { this.registry.materializeEnum(node.name); }, exitModel: (node: Model) => { - // Add logic to handle the exit of the model node + this.registry.materializeModel(node.name); }, }; } diff --git a/packages/graphql/src/type-maps/index.ts b/packages/graphql/src/type-maps/index.ts index 09d87be5490..0fc1cc16fc2 100644 --- a/packages/graphql/src/type-maps/index.ts +++ b/packages/graphql/src/type-maps/index.ts @@ -1,2 +1,3 @@ export { TypeMap, type TSPContext, type TypeKey } from "../type-maps.js"; export { EnumTypeMap } from "./enum.js"; +export { ModelTypeMap } from "./model.js"; diff --git a/packages/graphql/src/type-maps/model.ts b/packages/graphql/src/type-maps/model.ts new file mode 100644 index 00000000000..0b3b794c06d --- /dev/null +++ b/packages/graphql/src/type-maps/model.ts @@ -0,0 +1,74 @@ +import { UsageFlags, type Model } from "@typespec/compiler"; +import { + GraphQLInputObjectType, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, + type GraphQLInputFieldConfigMap, +} from "graphql"; +import { TypeMap, type TSPContext, type TypeKey } from "../type-maps.js"; + +/** + * TypeMap for converting TypeSpec Models to GraphQL ObjectTypes or InputObjectTypes. + * + * Handles registration of TSP models and lazy materialization into + * GraphQLObjectType (for output) or GraphQLInputObjectType (for input) instances. + * The usageFlag in TSPContext determines which type to create. + */ +export class ModelTypeMap extends TypeMap { + /** + * Derives the type key from the mutated model's name. + */ + protected getNameFromContext(context: TSPContext): TypeKey { + return context.type.name as TypeKey; + } + + /** + * Materializes a TypeSpec Model into a GraphQL ObjectType or InputObjectType. + */ + protected materialize(context: TSPContext): GraphQLObjectType | GraphQLInputObjectType { + const tspModel = context.type; + const name = tspModel.name; + + // Create InputObjectType for input usage, ObjectType for output + if (context.usageFlag === UsageFlags.Input) { + return this.materializeInputType(name, tspModel); + } + return this.materializeOutputType(name, tspModel); + } + + private materializeOutputType(name: string, tspModel: Model): GraphQLObjectType { + const fields: GraphQLFieldConfigMap = {}; + + for (const [propName, prop] of tspModel.properties) { + fields[propName] = { + type: this.mapPropertyType(prop.type), + // TODO: Add description from doc comments + }; + } + + return new GraphQLObjectType({ name, fields }); + } + + private materializeInputType(name: string, tspModel: Model): GraphQLInputObjectType { + const fields: GraphQLInputFieldConfigMap = {}; + + for (const [propName, prop] of tspModel.properties) { + fields[propName] = { + type: this.mapPropertyType(prop.type), + // TODO: Add description from doc comments + }; + } + + return new GraphQLInputObjectType({ name, fields }); + } + + /** + * Map a TypeSpec property type to a GraphQL String type. + * TODO: Implement full type mapping with references to other registered types. + */ + private mapPropertyType(_type: unknown): typeof GraphQLString { + // Placeholder - will need to resolve references to other types + return GraphQLString; + } +} diff --git a/packages/graphql/test/emitter.test.ts b/packages/graphql/test/emitter.test.ts index bd0b1a74566..19dafc6fa50 100644 --- a/packages/graphql/test/emitter.test.ts +++ b/packages/graphql/test/emitter.test.ts @@ -2,9 +2,19 @@ import { strictEqual } from "node:assert"; import { describe, it } from "vitest"; import { emitSingleSchema } from "./test-host.js"; -// For now, the expected output is a placeholder string. -// In the future, this should be replaced with the actual GraphQL schema output. -const expectedGraphQLSchema = `type Query { +const expectedGraphQLSchema = `type Book { + name: String + page_count: String + published: String + price: String +} + +type Author { + name: String + books: String +} + +type Query { """ A placeholder field. If you are seeing this, it means no operations were defined that could be emitted. """