From 68b74d306516ecc1d3455bbf778f49b8746aa9cd Mon Sep 17 00:00:00 2001 From: hendrik-lemon Date: Wed, 5 Mar 2025 15:31:38 +0100 Subject: [PATCH] feat: update graphql type informer to handle circular dependencies between types --- .../services/graphql/graphql-type-informer.ts | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/packages/framework-core/src/services/graphql/graphql-type-informer.ts b/packages/framework-core/src/services/graphql/graphql-type-informer.ts index 1bd9fada1..5cfe61951 100644 --- a/packages/framework-core/src/services/graphql/graphql-type-informer.ts +++ b/packages/framework-core/src/services/graphql/graphql-type-informer.ts @@ -14,6 +14,8 @@ import { GraphQLType, GraphQLUnionType, } from 'graphql' +import { defineFieldMap } from 'graphql/type/definition' + import { GraphQLJSON } from 'graphql-scalars' import { ClassMetadata, ClassType, PropertyMetadata, TypeMetadata } from '@boostercloud/metadata-booster' import { DateScalar, isExternalType, nonExcludedFields } from './common' @@ -153,20 +155,37 @@ export class GraphQLTypeInformer { inputType: boolean, excludeProps?: Array ): GraphQLType { + const typeName = classMetadata.name + (inputType ? 'Input' : '') + + // Create a placeholder to store type before defining fields (for circular refs) + if (!this.graphQLTypes[typeName]) { + this.graphQLTypes[typeName] = inputType + ? new GraphQLInputObjectType({ name: typeName, fields: {} }) + : new GraphQLObjectType({ name: typeName, fields: {} }) + } + const finalFields: Array = nonExcludedFields(classMetadata.fields, excludeProps) + if (inputType) { - return new GraphQLInputObjectType({ - name: classMetadata.name + 'Input', - fields: finalFields?.reduce((obj, prop) => { - this.logger.debug(`Get or create GraphQL input type for property ${prop.name}`) - return { - ...obj, - [prop.name]: { type: this.getOrCreateGraphQLType(prop.typeInfo, inputType) }, - } - }, {}), + Object.assign(this.graphQLTypes[typeName], { + _fields: () => + defineFieldMap({ + name: typeName, + fields: () => + finalFields.reduce((obj, prop) => { + this.logger.debug(`Get or create GraphQL input type for property ${prop.name}`) + return { + ...obj, + [prop.name]: { type: this.getOrCreateGraphQLType(prop.typeInfo, inputType) }, + } + }, {}), + }), }) + } else { + this.createOutputObjectType(classMetadata, excludeProps) } - return this.createOutputObjectType(classMetadata, excludeProps) + + return this.graphQLTypes[typeName] } private getOrCreateObjectTypeForUnion(classMetadata: ClassMetadata, excludeProps?: Array): GraphQLType { @@ -178,16 +197,30 @@ export class GraphQLTypeInformer { } private createOutputObjectType(classMetadata: ClassMetadata, excludeProps?: Array): GraphQLObjectType { + const typeName = classMetadata.name + + // Placeholder for circular references + if (!this.graphQLTypes[typeName]) { + this.graphQLTypes[typeName] = new GraphQLObjectType({ name: typeName, fields: {} }) + } + const finalFields: Array = nonExcludedFields(classMetadata.fields, excludeProps) - return new GraphQLObjectType({ - name: classMetadata.name, - fields: finalFields?.reduce((obj, prop) => { - this.logger.debug(`Get or create GraphQL output type for property ${prop.name}`) - return { - ...obj, - [prop.name]: { type: this.getOrCreateGraphQLType(prop.typeInfo, false) }, - } - }, {}), + + Object.assign(this.graphQLTypes[typeName], { + _fields: () => + defineFieldMap({ + name: typeName, + fields: () => + finalFields.reduce((obj, prop) => { + this.logger.debug(`Get or create GraphQL output type for property ${prop.name}`) + return { + ...obj, + [prop.name]: { type: this.getOrCreateGraphQLType(prop.typeInfo, false) }, + } + }, {}), + }), }) + + return this.graphQLTypes[typeName] as GraphQLObjectType } }