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
34 changes: 29 additions & 5 deletions packages/graphql/src/registry.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<string>();
Expand All @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions packages/graphql/src/schema-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
createDiagnosticCollector,
navigateTypesInNamespace,
UsageFlags,
type Diagnostic,
type DiagnosticCollector,
type EmitContext,
Expand Down Expand Up @@ -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);
},
};
}
Expand Down
1 change: 1 addition & 0 deletions packages/graphql/src/type-maps/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { TypeMap, type TSPContext, type TypeKey } from "../type-maps.js";
export { EnumTypeMap } from "./enum.js";
export { ModelTypeMap } from "./model.js";
74 changes: 74 additions & 0 deletions packages/graphql/src/type-maps/model.ts
Original file line number Diff line number Diff line change
@@ -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<Model, GraphQLObjectType | GraphQLInputObjectType> {
/**
* Derives the type key from the mutated model's name.
*/
protected getNameFromContext(context: TSPContext<Model>): TypeKey {
return context.type.name as TypeKey;
}

/**
* Materializes a TypeSpec Model into a GraphQL ObjectType or InputObjectType.
*/
protected materialize(context: TSPContext<Model>): 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<unknown, unknown> = {};

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;
}
}
16 changes: 13 additions & 3 deletions packages/graphql/test/emitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down