-
Notifications
You must be signed in to change notification settings - Fork 1
Add basic TSP Model registration and materialization #29
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?
Conversation
packages/graphql/src/registry.ts
Outdated
| } | ||
|
|
||
| addModel(tspModel: Model): void { | ||
| const modelName = tspModel.name; |
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.
I don't think we can assume that tspModel.name is the "GraphQL name". There are a number of transformations that might be applied to the name.
Some of them (like use of the @friendlyName decorator, or visibility) will be resolved by the compiler (though I think we have to e.g. call getFriendlyName), but others we will be computing in the GraphQL emitter (like changing names to be GraphQL-friendly, adding Input suffixes, etc).
(forgive me, I may have asked this before about enums)
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.
I might be wrong, but I don't think the TSPTypeContextRegistry is storing the GraphQL name. But, then I must ask what is the context registry storing and why?
packages/graphql/src/registry.ts
Outdated
| private computeModelFields(tspModel: Model): GraphQLFieldConfigMap<any, any> { | ||
| const registry = this; | ||
|
|
||
| const fields: GraphQLFieldConfigMap<any, any> = {}; |
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.
Can we come up with better types for this?
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.
+1. I had to refactor the entire prototype code just to get proper types for this.
packages/graphql/src/registry.ts
Outdated
|
|
||
| // Process each property of the model | ||
| for (const [propertyName, property] of tspModel.properties) { | ||
| const fieldConfig: any = {}; |
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.
Can we do better than the any type?
packages/graphql/src/registry.ts
Outdated
| // If the property type is a reference to another type, try to materialize it | ||
| if (property.type.kind === "Model") { | ||
| const referencedType = registry.materializeModel(property.type.name); | ||
| if (referencedType) { | ||
| fieldType = referencedType; | ||
| } | ||
| } else if (property.type.kind === "Enum") { | ||
| const referencedType = registry.materializeEnum(property.type.name); | ||
| if (referencedType) { | ||
| fieldType = referencedType; | ||
| } | ||
| } |
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.
I expect we'll want a dedicated method that is able to do this sort of routing, i.e. it won't just be limited to handling model fields.
packages/graphql/src/registry.ts
Outdated
| let fieldType: GraphQLOutputType = GraphQLString; | ||
|
|
||
| // If the property type is a reference to another type, try to materialize it | ||
| if (property.type.kind === "Model") { |
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.
This is JavaScript, and we have switch statements! 🎉
packages/graphql/src/registry.ts
Outdated
| fields: this.computeModelFields(tspModel), | ||
| }); | ||
|
|
||
| this.materializedGraphQLTypes.set(modelName, gqlObjectType); |
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.
I'd expect to see a similar pattern to addModel, but for materializedGraphQLTypes.
I am starting to think we want a class (a subclass of Map, perhaps) that can be used for TSPTypeContextRegistry and materializedGraphQLTypes, which maintains their internal consistency. e.g. it would:
- have a standard "get or add" action (like
addModel) - identify type conflicts (e.g. you're trying to add a model but the existing value is an enum)
- do type checking (i.e. I state that I am trying to get a model, but the value it has is not a model)
- has some kind of encapsulation of a "reset state" behavior
- can potentially track additional metadata about the types that isn't exposed externally (e.g. how many times were they accessed / set)?
I would also search the TSP code for something similar that already exists.
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.
+1 I think the prototype does this badly by having multiple top-level maps. We could have something like:
/**
* TypeSpec context for type mapping
* @template T - The TypeSpec type
*/
interface TSPContext<T = any> {
type: T; // The TypeSpec type
usageFlag: UsageFlags; // How the type is being used
name?: string; // Optional name override
metadata?: Record<string, any>; // Optional additional metadata
}
/**
* Base TypeMap for all GraphQL type mappings
* @template T - The TypeSpec type
* @template G - The GraphQL type
*/
abstract class TypeMap<T, G> {
// Map of materialized GraphQL types
protected materializedMap = new Map<string, G>();
// Map of registration contexts
protected registrationMap = new Map<string, TSPContext<T>>();
/**
* Register a TypeSpec type with context for later materialization
* @param context - The TypeSpec context
* @returns The name used for registration
*/
register(context: TSPContext<T>): string {
const name = this.getNameFromContext(context);
this.registrationMap.set(name, context);
return name;
}
/**
* Get the materialized GraphQL type
* @param name - The type name
* @returns The materialized GraphQL type or undefined
*/
get(name: string): G | undefined {
// Return already materialized type if available
if (this.materializedMap.has(name)) {
return this.materializedMap.get(name);
}
// Attempt to materialize if registered
const context = this.registrationMap.get(name);
if (context) {
const materializedType = this.materialize(context);
if (materializedType) {
this.materializedMap.set(name, materializedType);
return materializedType;
}
}
return undefined;
}
/**
* Check if a type is registered
*/
isRegistered(name: string): boolean {
return this.registrationMap.has(name);
}
/**
* Get a name from a context
*/
protected abstract getNameFromContext(context: TSPContext<T>): string;
/**
* Materialize a type from a context
*/
protected abstract materialize(context: TSPContext<T>): G | undefined;
}
/**
* Model field map to store thunk field configurations
*/
class ModelFieldMap {
private fieldMap = new Map<string, ThunkFieldConfig>();
/**
* Add a field with thunk configuration
*/
addField(
fieldName: string,
type: ThunkGraphQLType,
isOptional: boolean,
isList: boolean,
args?: ThunkGraphQLFieldConfigArgumentMap
): void {
this.fieldMap.set(fieldName, {
type,
isOptional,
isList,
args
});
}
/**
* Get all field thunk configurations
*/
getFieldThunks(): Map<string, ThunkFieldConfig> {
return this.fieldMap;
}
}
/**
* TypeMap for GraphQL Object types (output types)
*/
class ObjectTypeMap extends TypeMap<Model, GraphQLObjectType> {
// Maps for fields by model name
private modelFieldMaps = new Map<string, ModelFieldMap>();
// For handling interfaces
private interfacesMap = new Map<string, GraphQLInterfaceType[]>();
/**
* Get a name from a context
*/
protected override getNameFromContext(context: TSPContext<Model>): string {
return context.name || context.type.name || '';
}
/**
* Register a field for a model
*/
registerField(
modelName: string,
fieldName: string,
type: ThunkGraphQLType,
isOptional: boolean,
isList: boolean,
args?: ThunkGraphQLFieldConfigArgumentMap
): void {
if (!this.modelFieldMaps.has(modelName)) {
this.modelFieldMaps.set(modelName, new ModelFieldMap());
}
this.modelFieldMaps.get(modelName)!.addField(
fieldName,
type,
isOptional,
isList,
args
);
}
/**
* Add an interface to a model
*/
addInterface(modelName: string, interfaceType: GraphQLInterfaceType): void {
if (!this.interfacesMap.has(modelName)) {
this.interfacesMap.set(modelName, []);
}
this.interfacesMap.get(modelName)!.push(interfaceType);
}
/**
* Get interfaces for a model
*/
getInterfaces(modelName: string): GraphQLInterfaceType[] {
return this.interfacesMap.get(modelName) || [];
}
/**
* Materialize a GraphQL object type
*/
protected override materialize(context: TSPContext<Model>): GraphQLObjectType | undefined {
const modelName = this.getNameFromContext(context);
return new GraphQLObjectType({
name: modelName,
fields: () => this.materializeFields(modelName),
interfaces: () => this.getInterfaces(modelName)
});
}
/**
* Materialize fields for a model
*/
private materializeFields(modelName: string): GraphQLFieldConfigMap<any, any> {
const fieldMap = this.modelFieldMaps.get(modelName);
if (!fieldMap) {
return {};
}
const result: GraphQLFieldConfigMap<any, any> = {};
const fieldThunks = fieldMap.getFieldThunks();
fieldThunks.forEach((config, fieldName) => {
let fieldType = config.type() as GraphQLOutputType;
if (fieldType instanceof GraphQLInputObjectType) {
throw new Error(
`Model "${modelName}" has a field "${fieldName}" that is an input type. It should be an output type.`
);
}
if (!config.isOptional) {
fieldType = new GraphQLNonNull(fieldType);
}
if (config.isList) {
fieldType = new GraphQLNonNull(new GraphQLList(fieldType));
}
result[fieldName] = {
type: fieldType,
args: config.args ? config.args() : undefined
};
});
return result;
}
}
/**
* TypeMap for GraphQL Input types
*/
class InputTypeMap extends TypeMap<Model, GraphQLInputObjectType> {
// Maps for fields by model name
private modelFieldMaps = new Map<string, ModelFieldMap>();
/**
* Get a name from a context
*/
protected override getNameFromContext(context: TSPContext<Model>): string {
return context.name || `${context.type.name || ''}Input`;
}
/**
* Register a field for an input model
*/
registerField(
modelName: string,
fieldName: string,
type: ThunkGraphQLType,
isOptional: boolean,
isList: boolean
): void {
if (!this.modelFieldMaps.has(modelName)) {
this.modelFieldMaps.set(modelName, new ModelFieldMap());
}
this.modelFieldMaps.get(modelName)!.addField(
fieldName,
type,
isOptional,
isList
);
}
/**
* Materialize a GraphQL input type
*/
protected override materialize(context: TSPContext<Model>): GraphQLInputObjectType | undefined {
const modelName = this.getNameFromContext(context);
return new GraphQLInputObjectType({
name: modelName,
fields: () => this.materializeFields(modelName)
});
}
/**
* Materialize fields for an input model
*/
private materializeFields(modelName: string): GraphQLInputFieldConfigMap {
const fieldMap = this.modelFieldMaps.get(modelName);
if (!fieldMap) {
return {};
}
const result: GraphQLInputFieldConfigMap = {};
const fieldThunks = fieldMap.getFieldThunks();
fieldThunks.forEach((config, fieldName) => {
let fieldType = config.type() as GraphQLInputType;
if (!(fieldType instanceof GraphQLInputType)) {
throw new Error(
`Model "${modelName}" has a field "${fieldName}" that is not an input type.`
);
}
if (!config.isOptional) {
fieldType = new GraphQLNonNull(fieldType);
}
if (config.isList) {
fieldType = new GraphQLNonNull(new GraphQLList(fieldType));
}
result[fieldName] = {
type: fieldType
};
});
return result;
}
}
/**
* TypeMap for GraphQL Enum types
*/
class EnumTypeMap extends TypeMap<Enum, GraphQLEnumType> {
private sanitizeFn: (name: string) => string;
constructor(sanitizeFn: (name: string) => string) {
super();
this.sanitizeFn = sanitizeFn;
}
/**
* Get a name from a context
*/
protected override getNameFromContext(context: TSPContext<Enum>): string {
return context.name || context.type.name || '';
}
/**
* Materialize a GraphQL enum type
*/
protected override materialize(context: TSPContext<Enum>): GraphQLEnumType | undefined {
const enumType = context.type;
const name = this.getNameFromContext(context);
return new GraphQLEnumType({
name,
values: Array.from(enumType.members.values()).reduce<{
[key: string]: GraphQLEnumValueConfig;
}>((acc, member) => {
acc[this.sanitizeFn(member.name)] = {
value: member.name,
};
return acc;
}, {})
});
}
}
... and more ...
Then the registry itself can be made to handle/manage these maps.
export class GraphQLTypeRegistry {
// Type name registry
private modelTypeNames = new ModelTypeRegistry();
// Type maps for different GraphQL types
private objectTypes: ObjectTypeMap;
private inputTypes: InputTypeMap;
private interfaceTypes: InterfaceTypeMap;
private enumTypes: EnumTypeMap;
private unionTypes: UnionTypeMap;
constructor() {
// Initialize type maps with necessary dependencies
this.objectTypes = new ObjectTypeMap();
this.inputTypes = new InputTypeMap();
this.interfaceTypes = new InterfaceTypeMap(this.objectTypes);
this.enumTypes = new EnumTypeMap(this.sanitizeEnumMemberName.bind(this));
this.unionTypes = new UnionTypeMap(this.objectTypes);
}
/**
* Register a model with usage context
*/
addModelUsage(model: Model, usageFlag: UsageFlags): void {
const modelName = model.name;
if (!modelName) return;
// Register with the type name registry
const graphqlTypeName = this.modelTypeNames.registerTypeName(modelName, usageFlag);
// Create context for registration
const context: TSPContext<Model> = {
type: model,
usageFlag,
name: graphqlTypeName
};
// Register with the appropriate type map
if (usageFlag === UsageFlags.Output) {
this.objectTypes.register(context);
} else if (usageFlag === UsageFlags.Input) {
this.inputTypes.register(context);
}
}
/**
* Get all GraphQL type names for a model
*/
getModelTypeNames(modelName: string): ModelTypeNames {
return this.modelTypeNames.getModelTypeNames(modelName);
}
/**
* Register a model property
*/
addModelProperty(
parentModelName: string,
propName: string,
type: ThunkGraphQLType,
isOptional: boolean,
isList: boolean,
args?: ThunkGraphQLFieldConfigArgumentMap
): void {
// Get all GraphQL type names for the model
const typeNames = this.getModelTypeNames(parentModelName);
// Add to appropriate type maps based on usage
const outputTypeName = typeNames[UsageFlags.Output];
if (outputTypeName) {
this.objectTypes.registerField(
outputTypeName,
propName,
type,
isOptional,
isList,
args
);
}
const inputTypeName = typeNames[UsageFlags.Input];
if (inputTypeName) {
this.inputTypes.registerField(
inputTypeName,
propName,
type,
isOptional,
isList
);
}
}
... and more ...
You can look at TSP for more examples/patterns on how to do this as well, but that would be the general idea.
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.
Oh yeah, I like that approach better! I made updates in this PR buts it's getting too lengthy for my taste. Going to break all of this into smaller PRs starting with #30. Thanks!
| this.registry.addModel(node); | ||
| }, | ||
| 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); |
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.
Can you describe (possibly in a code comment or in the commit message) why we want to add the model on visit, but materialize it on exit?
packages/graphql/src/registry.ts
Outdated
| return gqlEnum; | ||
| } | ||
|
|
||
| private computeModelFields(tspModel: Model): GraphQLFieldConfigMap<any, any> { |
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.
From what I've seen elsewhere in the TypeSpec codebase (and it makes sense to me), we should use JavaScript's #private properties over TypeScript's (pseudo-)private properties.
| @test model TestModel { | ||
| name: string; | ||
| } |
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.
Why are we adding fields to all these test models?
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.
This is to fix a bunch of schema validation errors: error GraphQLSchemaValidationError: Type TestModel must define one or more fields.
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.
Gotcha. That is a real issue though, as TypeSpec has no problem with empty models.
So I think instead of changing the TSP schema, we need to handle empty TSP models in the GraphQL emitter.
packages/graphql/src/registry.ts
Outdated
| return gqlEnum; | ||
| } | ||
|
|
||
| private computeModelFields(tspModel: Model): GraphQLFieldConfigMap<any, any> { |
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.
I don't think this should be navigated from the model, instead this should be navigated from modelProperty on the navigateModel.
packages/graphql/src/registry.ts
Outdated
| } | ||
|
|
||
| addModel(tspModel: Model): void { | ||
| const modelName = tspModel.name; |
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.
I might be wrong, but I don't think the TSPTypeContextRegistry is storing the GraphQL name. But, then I must ask what is the context registry storing and why?
packages/graphql/src/registry.ts
Outdated
| private computeModelFields(tspModel: Model): GraphQLFieldConfigMap<any, any> { | ||
| const registry = this; | ||
|
|
||
| const fields: GraphQLFieldConfigMap<any, any> = {}; |
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.
+1. I had to refactor the entire prototype code just to get proper types for this.
packages/graphql/src/registry.ts
Outdated
|
|
||
| // If the property type is a reference to another type, try to materialize it | ||
| if (property.type.kind === "Model") { | ||
| const referencedType = registry.materializeModel(property.type.name); |
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.
You are creating a recursion within an existing recursion (navigateProgram) by doing this. You want to use the navigateProgram's modelProperty to collect the fields and store them to be materialized later in exitModel
packages/graphql/src/registry.ts
Outdated
| fields: this.computeModelFields(tspModel), | ||
| }); | ||
|
|
||
| this.materializedGraphQLTypes.set(modelName, gqlObjectType); |
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.
+1 I think the prototype does this badly by having multiple top-level maps. We could have something like:
/**
* TypeSpec context for type mapping
* @template T - The TypeSpec type
*/
interface TSPContext<T = any> {
type: T; // The TypeSpec type
usageFlag: UsageFlags; // How the type is being used
name?: string; // Optional name override
metadata?: Record<string, any>; // Optional additional metadata
}
/**
* Base TypeMap for all GraphQL type mappings
* @template T - The TypeSpec type
* @template G - The GraphQL type
*/
abstract class TypeMap<T, G> {
// Map of materialized GraphQL types
protected materializedMap = new Map<string, G>();
// Map of registration contexts
protected registrationMap = new Map<string, TSPContext<T>>();
/**
* Register a TypeSpec type with context for later materialization
* @param context - The TypeSpec context
* @returns The name used for registration
*/
register(context: TSPContext<T>): string {
const name = this.getNameFromContext(context);
this.registrationMap.set(name, context);
return name;
}
/**
* Get the materialized GraphQL type
* @param name - The type name
* @returns The materialized GraphQL type or undefined
*/
get(name: string): G | undefined {
// Return already materialized type if available
if (this.materializedMap.has(name)) {
return this.materializedMap.get(name);
}
// Attempt to materialize if registered
const context = this.registrationMap.get(name);
if (context) {
const materializedType = this.materialize(context);
if (materializedType) {
this.materializedMap.set(name, materializedType);
return materializedType;
}
}
return undefined;
}
/**
* Check if a type is registered
*/
isRegistered(name: string): boolean {
return this.registrationMap.has(name);
}
/**
* Get a name from a context
*/
protected abstract getNameFromContext(context: TSPContext<T>): string;
/**
* Materialize a type from a context
*/
protected abstract materialize(context: TSPContext<T>): G | undefined;
}
/**
* Model field map to store thunk field configurations
*/
class ModelFieldMap {
private fieldMap = new Map<string, ThunkFieldConfig>();
/**
* Add a field with thunk configuration
*/
addField(
fieldName: string,
type: ThunkGraphQLType,
isOptional: boolean,
isList: boolean,
args?: ThunkGraphQLFieldConfigArgumentMap
): void {
this.fieldMap.set(fieldName, {
type,
isOptional,
isList,
args
});
}
/**
* Get all field thunk configurations
*/
getFieldThunks(): Map<string, ThunkFieldConfig> {
return this.fieldMap;
}
}
/**
* TypeMap for GraphQL Object types (output types)
*/
class ObjectTypeMap extends TypeMap<Model, GraphQLObjectType> {
// Maps for fields by model name
private modelFieldMaps = new Map<string, ModelFieldMap>();
// For handling interfaces
private interfacesMap = new Map<string, GraphQLInterfaceType[]>();
/**
* Get a name from a context
*/
protected override getNameFromContext(context: TSPContext<Model>): string {
return context.name || context.type.name || '';
}
/**
* Register a field for a model
*/
registerField(
modelName: string,
fieldName: string,
type: ThunkGraphQLType,
isOptional: boolean,
isList: boolean,
args?: ThunkGraphQLFieldConfigArgumentMap
): void {
if (!this.modelFieldMaps.has(modelName)) {
this.modelFieldMaps.set(modelName, new ModelFieldMap());
}
this.modelFieldMaps.get(modelName)!.addField(
fieldName,
type,
isOptional,
isList,
args
);
}
/**
* Add an interface to a model
*/
addInterface(modelName: string, interfaceType: GraphQLInterfaceType): void {
if (!this.interfacesMap.has(modelName)) {
this.interfacesMap.set(modelName, []);
}
this.interfacesMap.get(modelName)!.push(interfaceType);
}
/**
* Get interfaces for a model
*/
getInterfaces(modelName: string): GraphQLInterfaceType[] {
return this.interfacesMap.get(modelName) || [];
}
/**
* Materialize a GraphQL object type
*/
protected override materialize(context: TSPContext<Model>): GraphQLObjectType | undefined {
const modelName = this.getNameFromContext(context);
return new GraphQLObjectType({
name: modelName,
fields: () => this.materializeFields(modelName),
interfaces: () => this.getInterfaces(modelName)
});
}
/**
* Materialize fields for a model
*/
private materializeFields(modelName: string): GraphQLFieldConfigMap<any, any> {
const fieldMap = this.modelFieldMaps.get(modelName);
if (!fieldMap) {
return {};
}
const result: GraphQLFieldConfigMap<any, any> = {};
const fieldThunks = fieldMap.getFieldThunks();
fieldThunks.forEach((config, fieldName) => {
let fieldType = config.type() as GraphQLOutputType;
if (fieldType instanceof GraphQLInputObjectType) {
throw new Error(
`Model "${modelName}" has a field "${fieldName}" that is an input type. It should be an output type.`
);
}
if (!config.isOptional) {
fieldType = new GraphQLNonNull(fieldType);
}
if (config.isList) {
fieldType = new GraphQLNonNull(new GraphQLList(fieldType));
}
result[fieldName] = {
type: fieldType,
args: config.args ? config.args() : undefined
};
});
return result;
}
}
/**
* TypeMap for GraphQL Input types
*/
class InputTypeMap extends TypeMap<Model, GraphQLInputObjectType> {
// Maps for fields by model name
private modelFieldMaps = new Map<string, ModelFieldMap>();
/**
* Get a name from a context
*/
protected override getNameFromContext(context: TSPContext<Model>): string {
return context.name || `${context.type.name || ''}Input`;
}
/**
* Register a field for an input model
*/
registerField(
modelName: string,
fieldName: string,
type: ThunkGraphQLType,
isOptional: boolean,
isList: boolean
): void {
if (!this.modelFieldMaps.has(modelName)) {
this.modelFieldMaps.set(modelName, new ModelFieldMap());
}
this.modelFieldMaps.get(modelName)!.addField(
fieldName,
type,
isOptional,
isList
);
}
/**
* Materialize a GraphQL input type
*/
protected override materialize(context: TSPContext<Model>): GraphQLInputObjectType | undefined {
const modelName = this.getNameFromContext(context);
return new GraphQLInputObjectType({
name: modelName,
fields: () => this.materializeFields(modelName)
});
}
/**
* Materialize fields for an input model
*/
private materializeFields(modelName: string): GraphQLInputFieldConfigMap {
const fieldMap = this.modelFieldMaps.get(modelName);
if (!fieldMap) {
return {};
}
const result: GraphQLInputFieldConfigMap = {};
const fieldThunks = fieldMap.getFieldThunks();
fieldThunks.forEach((config, fieldName) => {
let fieldType = config.type() as GraphQLInputType;
if (!(fieldType instanceof GraphQLInputType)) {
throw new Error(
`Model "${modelName}" has a field "${fieldName}" that is not an input type.`
);
}
if (!config.isOptional) {
fieldType = new GraphQLNonNull(fieldType);
}
if (config.isList) {
fieldType = new GraphQLNonNull(new GraphQLList(fieldType));
}
result[fieldName] = {
type: fieldType
};
});
return result;
}
}
/**
* TypeMap for GraphQL Enum types
*/
class EnumTypeMap extends TypeMap<Enum, GraphQLEnumType> {
private sanitizeFn: (name: string) => string;
constructor(sanitizeFn: (name: string) => string) {
super();
this.sanitizeFn = sanitizeFn;
}
/**
* Get a name from a context
*/
protected override getNameFromContext(context: TSPContext<Enum>): string {
return context.name || context.type.name || '';
}
/**
* Materialize a GraphQL enum type
*/
protected override materialize(context: TSPContext<Enum>): GraphQLEnumType | undefined {
const enumType = context.type;
const name = this.getNameFromContext(context);
return new GraphQLEnumType({
name,
values: Array.from(enumType.members.values()).reduce<{
[key: string]: GraphQLEnumValueConfig;
}>((acc, member) => {
acc[this.sanitizeFn(member.name)] = {
value: member.name,
};
return acc;
}, {})
});
}
}
... and more ...
Then the registry itself can be made to handle/manage these maps.
export class GraphQLTypeRegistry {
// Type name registry
private modelTypeNames = new ModelTypeRegistry();
// Type maps for different GraphQL types
private objectTypes: ObjectTypeMap;
private inputTypes: InputTypeMap;
private interfaceTypes: InterfaceTypeMap;
private enumTypes: EnumTypeMap;
private unionTypes: UnionTypeMap;
constructor() {
// Initialize type maps with necessary dependencies
this.objectTypes = new ObjectTypeMap();
this.inputTypes = new InputTypeMap();
this.interfaceTypes = new InterfaceTypeMap(this.objectTypes);
this.enumTypes = new EnumTypeMap(this.sanitizeEnumMemberName.bind(this));
this.unionTypes = new UnionTypeMap(this.objectTypes);
}
/**
* Register a model with usage context
*/
addModelUsage(model: Model, usageFlag: UsageFlags): void {
const modelName = model.name;
if (!modelName) return;
// Register with the type name registry
const graphqlTypeName = this.modelTypeNames.registerTypeName(modelName, usageFlag);
// Create context for registration
const context: TSPContext<Model> = {
type: model,
usageFlag,
name: graphqlTypeName
};
// Register with the appropriate type map
if (usageFlag === UsageFlags.Output) {
this.objectTypes.register(context);
} else if (usageFlag === UsageFlags.Input) {
this.inputTypes.register(context);
}
}
/**
* Get all GraphQL type names for a model
*/
getModelTypeNames(modelName: string): ModelTypeNames {
return this.modelTypeNames.getModelTypeNames(modelName);
}
/**
* Register a model property
*/
addModelProperty(
parentModelName: string,
propName: string,
type: ThunkGraphQLType,
isOptional: boolean,
isList: boolean,
args?: ThunkGraphQLFieldConfigArgumentMap
): void {
// Get all GraphQL type names for the model
const typeNames = this.getModelTypeNames(parentModelName);
// Add to appropriate type maps based on usage
const outputTypeName = typeNames[UsageFlags.Output];
if (outputTypeName) {
this.objectTypes.registerField(
outputTypeName,
propName,
type,
isOptional,
isList,
args
);
}
const inputTypeName = typeNames[UsageFlags.Input];
if (inputTypeName) {
this.inputTypes.registerField(
inputTypeName,
propName,
type,
isOptional,
isList
);
}
}
... and more ...
You can look at TSP for more examples/patterns on how to do this as well, but that would be the general idea.
Summary
This PR:
Stringas the default type for Scalars for nowComing Soon
UsageFlagsto generateGraphQLInputTypes andGraphQLOutputTypes