-
Notifications
You must be signed in to change notification settings - Fork 1
Add TSP Model -> GraphQLObject type translation #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/graphql
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import type { Scalar } from "@typespec/compiler"; | ||
| import { $ } from "@typespec/compiler/typekit"; | ||
| import { | ||
| GraphQLBoolean, | ||
| GraphQLFloat, | ||
| GraphQLInt, | ||
| GraphQLString, | ||
| type GraphQLScalarType, | ||
| } from "graphql"; | ||
|
|
||
| /** | ||
| * Map TypeSpec scalar types to GraphQL Built-in types | ||
| */ | ||
| export function mapScalarToGraphQL( | ||
| scalar: Scalar, | ||
| typekit: ReturnType<typeof $>, | ||
| ): GraphQLScalarType { | ||
| // Check for string type | ||
| if (typekit.scalar.isString(scalar)) { | ||
| return GraphQLString; | ||
| } | ||
|
|
||
| // Check for integer types | ||
| if ( | ||
| typekit.scalar.isInt8(scalar) || | ||
| typekit.scalar.isInt16(scalar) || | ||
| typekit.scalar.isInt32(scalar) || | ||
| typekit.scalar.isSafeint(scalar) || | ||
| typekit.scalar.isUint8(scalar) || | ||
| typekit.scalar.isUint16(scalar) || | ||
| typekit.scalar.isUint32(scalar) | ||
| ) { | ||
| return GraphQLInt; | ||
| } | ||
|
|
||
| // Check for float types | ||
| if ( | ||
| typekit.scalar.isFloat32(scalar) || | ||
| typekit.scalar.isFloat64(scalar) || | ||
| typekit.scalar.isFloat(scalar) | ||
| ) { | ||
| return GraphQLFloat; | ||
| } | ||
|
|
||
| // Check for boolean type | ||
| if (typekit.scalar.isBoolean(scalar)) { | ||
| return GraphQLBoolean; | ||
| } | ||
|
|
||
| // Default to string for unknown scalar types | ||
| return GraphQLString; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,22 @@ | ||
| import { UsageFlags, type Enum, type Model } from "@typespec/compiler"; | ||
| import { | ||
| UsageFlags, | ||
| type Enum, | ||
| type Model, | ||
| type ModelProperty, | ||
| type Program, | ||
| type Type, | ||
| } from "@typespec/compiler"; | ||
| import { $ } from "@typespec/compiler/typekit"; | ||
| import { | ||
| GraphQLBoolean, | ||
| GraphQLEnumType, | ||
| GraphQLObjectType, | ||
| type GraphQLNamedType, | ||
| GraphQLString, | ||
| type GraphQLInputType, | ||
| type GraphQLOutputType, | ||
| type GraphQLSchemaConfig, | ||
| } from "graphql"; | ||
| import { mapScalarToGraphQL } from "./lib/scalars.js"; | ||
| import { ObjectTypeMap, type TSPContext, type TypeKey } from "./type-maps.js"; | ||
|
|
||
| // The TSPTypeContext interface represents the intermediate TSP type information before materialization. | ||
| // It stores the raw TSP type and any extracted metadata relevant for GraphQL generation. | ||
|
|
@@ -16,19 +27,19 @@ interface TSPTypeContext { | |
| // TODO: Add any other TSP-specific metadata here. | ||
| } | ||
| /** | ||
| * GraphQLTypeRegistry manages the registration and materialization of TypeSpec (TSP) | ||
| * GraphQLTypeRegistry manages the registration and materialization of TypeSpec (TSP) | ||
| * types into their corresponding GraphQL type definitions. | ||
| * | ||
| * The registry operates in a two-stage process: | ||
| * 1. Registration: TSP types (like Enums, Models, etc.) are first registered | ||
| * along with relevant metadata (e.g., name, usage flags). This stores an | ||
| * intermediate representation (`TSPTypeContext`) without immediately creating | ||
| * GraphQL types. This stage is typically performed while traversing the TSP AST. | ||
| * GraphQL types. This stage is typically performed while traversing the TSP AST. | ||
| * Register type by calling the appropriate method (e.g., `addEnum`). | ||
| * | ||
| * | ||
| * 2. Materialization: When a GraphQL type is needed (e.g., to build the final | ||
| * schema or resolve a field type), the registry can materialize the TSP type | ||
| * into its GraphQL counterpart (e.g., `GraphQLEnumType`, `GraphQLObjectType`). | ||
| * into its GraphQL counterpart (e.g., `GraphQLEnumType`, `GraphQLObjectType`). | ||
| * Materialize types by calling the appropriate method (e.g., `materializeEnum`). | ||
| * | ||
| * This approach helps in: | ||
|
|
@@ -39,61 +50,148 @@ interface TSPTypeContext { | |
| * by using thunks for fields/arguments. | ||
| */ | ||
| export class GraphQLTypeRegistry { | ||
| // Stores intermediate TSP type information, keyed by TSP type name. | ||
| // TODO: make this more of a seen set | ||
| private TSPTypeContextRegistry: Map<string, TSPTypeContext> = new Map(); | ||
| // Global registry to prevent GraphQL type name collisions | ||
| static #globalNameRegistry = new Set<TypeKey>(); | ||
|
|
||
| // Stores materialized GraphQL types, keyed by their GraphQL name. | ||
| private materializedGraphQLTypes: Map<string, GraphQLNamedType> = new Map(); | ||
| // Type maps for different GraphQL types | ||
| #objectTypes: ObjectTypeMap; | ||
|
Comment on lines
+56
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we'll want a construct that can return the mapping for any type? #getTypeMap(tspType: Type): TypeMap<Type, Any> {}; |
||
|
|
||
| addEnum(tspEnum: Enum): void { | ||
| const enumName = tspEnum.name; | ||
| if (this.TSPTypeContextRegistry.has(enumName)) { | ||
| // Optionally, log a warning or update if new information is more complete. | ||
| return; | ||
| } | ||
| // Program reference for using TypeSpec utilities | ||
| #program: Program; | ||
|
|
||
| // TypeSpec typekit for easy access to TypeSpec utilities | ||
| #typekit: ReturnType<typeof $>; | ||
|
|
||
| constructor(program: Program) { | ||
| // Initialize type maps with necessary dependencies | ||
| this.#objectTypes = new ObjectTypeMap(); | ||
| this.#program = program; | ||
| this.#typekit = $(program); | ||
| } | ||
|
|
||
| this.TSPTypeContextRegistry.set(enumName, { | ||
| tspType: tspEnum, | ||
| name: enumName, | ||
| // TODO: Populate usageFlags based on TSP context and other decorator context. | ||
| }); | ||
| /** | ||
| * Reset the global name registry | ||
| */ | ||
| static resetGlobalRegistry(): void { | ||
| GraphQLTypeRegistry.#globalNameRegistry.clear(); | ||
| } | ||
|
|
||
| // Materializes a TSP Enum into a GraphQLEnumType. | ||
| materializeEnum(enumName: string): GraphQLEnumType | undefined { | ||
| // Check if the GraphQL type is already materialized. | ||
| if (this.materializedGraphQLTypes.has(enumName)) { | ||
| return this.materializedGraphQLTypes.get(enumName) as GraphQLEnumType; | ||
| /** | ||
| * Get GraphQL type names for a model based on usage flags | ||
| * Returns a mapping of usage flags to their corresponding GraphQL type names | ||
| */ | ||
| #getModelTypeNames(modelName: string): Record<UsageFlags, string | undefined> { | ||
| // For now, we only support output types | ||
| // TODO: Add support for input types when InputTypeMap is implemented | ||
| const outputTypeName = this.#objectTypes.isRegistered(modelName) ? modelName : undefined; | ||
|
|
||
| return { | ||
| [UsageFlags.None]: undefined, | ||
| [UsageFlags.Input]: undefined, // TODO: Implement when InputTypeMap is added | ||
| [UsageFlags.Output]: outputTypeName, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Register a TSP Model | ||
| */ | ||
| addModel(model: Model): void { | ||
| const model_context: TSPContext<Model> = { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the expected style is to use |
||
| type: model, | ||
| usageFlag: UsageFlags.Output, | ||
| graphqlName: model.name, | ||
| metadata: {}, | ||
| }; | ||
|
|
||
| // Check if the model name already exists in the global registry | ||
| const graphqlName = model_context.graphqlName as TypeKey; | ||
| if (GraphQLTypeRegistry.#globalNameRegistry.has(graphqlName)) { | ||
| throw new Error( | ||
| `GraphQL type name '${graphqlName}' is already registered. Type names must be unique across the entire schema.`, | ||
| ); | ||
| } | ||
|
|
||
| const context = this.TSPTypeContextRegistry.get(enumName); | ||
| if (!context || context.tspType.kind !== "Enum") { | ||
| // TODO: Handle error or warning for missing context. | ||
| return undefined; | ||
| this.#objectTypes.register(model_context); | ||
| GraphQLTypeRegistry.#globalNameRegistry.add(graphqlName); | ||
|
|
||
| // TODO: Register input types for models | ||
| } | ||
|
|
||
| /** | ||
| * Materializes a TSP Model into a GraphQLObjectType. | ||
| */ | ||
| materializeModel(modelName: string): GraphQLObjectType | undefined { | ||
| const model = this.#objectTypes.get(modelName as TypeKey); | ||
| return model; // This will be undefined for models with no fields, which is correct | ||
| } | ||
|
|
||
| /** | ||
| * Register a model property | ||
| */ | ||
| addModelProperty(property: ModelProperty): void { | ||
| // Only process properties that have a parent model | ||
| if (!property.model) { | ||
| return; | ||
| } | ||
|
|
||
| const tspEnum = context.tspType as Enum; | ||
| // Create a thunk for the property type that will be resolved later | ||
| const typeThunk = (): GraphQLOutputType | GraphQLInputType => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
| return this.#mapTypeSpecToGraphQL(property.type); | ||
| }; | ||
|
|
||
| // Check if this property represents a list/array type | ||
| const isListType = | ||
| this.#typekit.model.is(property.type) && this.#typekit.array.is(property.type); | ||
|
|
||
| const gqlEnum = new GraphQLEnumType({ | ||
| name: context.name, | ||
| values: Object.fromEntries( | ||
| Array.from(tspEnum.members.values()).map((member) => [ | ||
| member.name, | ||
| { | ||
| value: member.value ?? member.name, | ||
| }, | ||
| ]), | ||
| ), | ||
| }); | ||
| // Register the field with the object type map | ||
| this.#objectTypes.registerField( | ||
| property.model.name, // modelName | ||
| property.name, // fieldName | ||
| typeThunk, // type (thunk) | ||
| property.optional, // isOptional | ||
| isListType, // isList | ||
| undefined, // args | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Maps a TypeSpec type to a GraphQL type | ||
| */ | ||
| #mapTypeSpecToGraphQL(type: Type): GraphQLOutputType | GraphQLInputType { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| if (this.#typekit.scalar.is(type)) { | ||
| return mapScalarToGraphQL(type, this.#typekit); | ||
| } | ||
|
|
||
| if (this.#typekit.model.is(type)) { | ||
| if (this.#typekit.array.is(type)) { | ||
| const elementType = this.#typekit.array.getElementType(type); | ||
| const graphqlElementType = this.#mapTypeSpecToGraphQL(elementType); | ||
| // Return the array element type directly for now, the GraphQLList wrapper | ||
| // will be applied in the materializeFields method based on the isList flag | ||
| return graphqlElementType; | ||
| } | ||
| // For regular model types, get the materialized GraphQL object type | ||
| const modelType = this.#objectTypes.get(type.name as TypeKey); | ||
| if (!modelType) { | ||
| throw new Error( | ||
| `Referenced model ${type.name} not found. Make sure it's registered before being referenced.`, | ||
| ); | ||
| } | ||
| return modelType; | ||
| } | ||
|
|
||
| this.materializedGraphQLTypes.set(enumName, gqlEnum); | ||
| return gqlEnum; | ||
| // For unsupported types, log a warning and default to string | ||
| console.warn(`Unsupported TypeSpec type: ${type.kind}, defaulting to GraphQLString`); | ||
| return GraphQLString; | ||
| } | ||
|
|
||
| /** | ||
| * Materialize the schema configuration for GraphQL | ||
| */ | ||
| materializeSchemaConfig(): GraphQLSchemaConfig { | ||
| const allMaterializedGqlTypes = Array.from(this.materializedGraphQLTypes.values()); | ||
| let queryType = this.materializedGraphQLTypes.get("Query") as GraphQLObjectType | undefined; | ||
| // TODO: Add other types to allMaterializedGqlTypes | ||
| const allMaterializedGqlTypes = Array.from(this.#objectTypes.getAllMaterialized()); | ||
| let queryType = this.#objectTypes.get("Query" as TypeKey) as GraphQLObjectType | undefined; | ||
| if (!queryType) { | ||
| queryType = new GraphQLObjectType({ | ||
| name: "Query", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,17 +1,18 @@ | ||||||
| import { | ||||||
| createDiagnosticCollector, | ||||||
| ListenerFlow, | ||||||
| navigateTypesInNamespace, | ||||||
| type Diagnostic, | ||||||
| type DiagnosticCollector, | ||||||
| type EmitContext, | ||||||
| type Enum, | ||||||
| type Model, | ||||||
| type ModelProperty, | ||||||
| type Namespace, | ||||||
| } from "@typespec/compiler"; | ||||||
| import { GraphQLSchema, validateSchema } from "graphql"; | ||||||
| import { type GraphQLEmitterOptions } from "./lib.js"; | ||||||
| import type { Schema } from "./lib/schema.js"; | ||||||
| import { GraphQLTypeRegistry } from "./registry.js"; | ||||||
| import { exit } from "node:process"; | ||||||
|
|
||||||
| class GraphQLSchemaEmitter { | ||||||
| private tspSchema: Schema; | ||||||
|
|
@@ -29,7 +30,7 @@ class GraphQLSchemaEmitter { | |||||
| this.context = context; | ||||||
| this.options = options; | ||||||
| this.diagnostics = createDiagnosticCollector(); | ||||||
| this.registry = new GraphQLTypeRegistry(); | ||||||
| this.registry = new GraphQLTypeRegistry(context.program); | ||||||
| } | ||||||
|
|
||||||
| async emitSchema(): Promise<[GraphQLSchema, Readonly<Diagnostic[]>] | undefined> { | ||||||
|
|
@@ -54,17 +55,20 @@ class GraphQLSchemaEmitter { | |||||
| semanticNodeListener() { | ||||||
| // TODO: Add GraphQL types to registry as the TSP nodes are visited | ||||||
| return { | ||||||
| enum: (node: Enum) => { | ||||||
| this.registry.addEnum(node); | ||||||
| namespace: (namespace: Namespace) => { | ||||||
| if (namespace.name === "TypeSpec" || namespace.name === "Reflection") { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| return ListenerFlow.NoRecursion; | ||||||
| } | ||||||
| return; | ||||||
| }, | ||||||
| model: (node: Model) => { | ||||||
| // Add logic to handle the model node | ||||||
| }, | ||||||
| exitEnum: (node: Enum) => { | ||||||
| this.registry.materializeEnum(node.name); | ||||||
| this.registry.addModel(node); | ||||||
| }, | ||||||
| exitModel: (node: Model) => { | ||||||
| // Add logic to handle the exit of the model node | ||||||
| this.registry.materializeModel(node.name); | ||||||
| }, | ||||||
| modelProperty: (node: ModelProperty) => { | ||||||
| this.registry.addModelProperty(node); | ||||||
| }, | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like https://github.com/microsoft/typespec/blob/ff28640117f7f58166c9455c4812acd7fc044636/packages/http-client-js/src/components/transforms/scalar-transform.tsx establishes a good pattern to follow here.
Parts of that code (like
getScalarTests) should arguably part of a shared library — only thescalarTransformerMapis really emitter-specific. Might want to check with @joheredi on that.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think scalar-transform really belongs in
packages/emitter-framework. There is some type-transform in there but I think that is probably not used much and might want to replace with the transforms fromhttp-client-js.@maorleger - fyi