Add basic TSP Model registration and materialization #29
Add basic TSP Model registration and materialization #29FionaBronwen wants to merge 8 commits intofeature/graphqlfrom
Conversation
packages/graphql/src/registry.ts
Outdated
| } | ||
|
|
||
| addModel(tspModel: Model): void { | ||
| const modelName = tspModel.name; |
There was a problem hiding this comment.
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.
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.
Can we come up with better types for this?
There was a problem hiding this comment.
+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 = {}; |
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.
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.
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.
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.
+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.
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.
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.
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.
Why are we adding fields to all these test models?
There was a problem hiding this comment.
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.
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.
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.
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.
+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.
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.
+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