feat: new relation schema generation (#9882)

Fix https://github.com/twentyhq/core-team-issues/issues/295

Based on the feature-flag `IsNewRelationEnabled` the schema will be
marked as outdated and regenerated, this will cause an error on the
front-end on the first request on the following ones schema will be well
generated ans request will work.
This commit is contained in:
Jérémy M
2025-01-29 10:33:17 +01:00
committed by GitHub
parent f74bb5a60b
commit fbb67d74c8
18 changed files with 361 additions and 24 deletions

View File

@ -0,0 +1,150 @@
import { Injectable, Logger } from '@nestjs/common';
import {
GraphQLFieldConfigArgumentMap,
GraphQLFieldConfigMap,
GraphQLObjectType,
} from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { RelationTypeV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-type-v2.factory';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field';
import { isRelationFieldMetadata } from 'src/engine/utils/is-relation-field-metadata.util';
import { ArgsFactory } from './args.factory';
export enum ObjectTypeDefinitionKind {
Connection = 'Connection',
Edge = 'Edge',
Plain = '',
}
export interface ObjectTypeDefinition {
target: string;
kind: ObjectTypeDefinitionKind;
type: GraphQLObjectType;
}
@Injectable()
export class ExtendObjectTypeDefinitionV2Factory {
private readonly logger = new Logger(
ExtendObjectTypeDefinitionV2Factory.name,
);
constructor(
private readonly relationTypeV2Factory: RelationTypeV2Factory,
private readonly argsFactory: ArgsFactory,
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): ObjectTypeDefinition {
const kind = ObjectTypeDefinitionKind.Plain;
const gqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
objectMetadata.id,
kind,
);
const containsRelationField = objectContainsRelationField(objectMetadata);
if (!gqlType) {
this.logger.error(
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
{
objectMetadata,
options,
},
);
throw new Error(
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
);
}
// Security check to avoid extending an object that does not need to be extended
if (!containsRelationField) {
this.logger.error(
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
{
objectMetadata,
options,
},
);
throw new Error(
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
);
}
// Extract current object config to extend it
const config = gqlType.toConfig();
// Recreate the same object type with the new fields
return {
target: objectMetadata.id,
kind,
type: new GraphQLObjectType({
...config,
fields: () => ({
...config.fields,
...this.generateFields(objectMetadata, options),
}),
}),
};
}
private generateFields(
objectMetadata: ObjectMetadataInterface,
options: WorkspaceBuildSchemaOptions,
): GraphQLFieldConfigMap<any, any> {
const fields: GraphQLFieldConfigMap<any, any> = {};
for (const fieldMetadata of objectMetadata.fields) {
// Ignore non-relation fields as they are already defined
if (!isRelationFieldMetadata(fieldMetadata)) {
continue;
}
if (!fieldMetadata.settings) {
throw new Error(
`Field Metadata of type RELATION with id ${fieldMetadata.id} has no settings`,
);
}
if (!fieldMetadata.relationTargetObjectMetadataId) {
throw new Error(
`Field Metadata of type RELATION with id ${fieldMetadata.id} has no relation target object metadata id`,
);
}
const relationType = this.relationTypeV2Factory.create(fieldMetadata);
let argsType: GraphQLFieldConfigArgumentMap | undefined = undefined;
if (fieldMetadata.settings.relationType === RelationType.ONE_TO_MANY) {
const args = getResolverArgs('findMany');
argsType = this.argsFactory.create(
{
args,
objectMetadataId: fieldMetadata.relationTargetObjectMetadataId,
},
options,
);
}
fields[fieldMetadata.name] = {
type: relationType,
args: argsType,
description: fieldMetadata.description,
};
}
return fields;
}
}

View File

@ -10,17 +10,17 @@ import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-sc
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import {
RelationDirection,
deduceRelationDirection,
} from 'src/engine/utils/deduce-relation-direction.util';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { RelationTypeFactory } from './relation-type.factory';
import { ArgsFactory } from './args.factory';
import { RelationTypeFactory } from './relation-type.factory';
export enum ObjectTypeDefinitionKind {
Connection = 'Connection',

View File

@ -3,6 +3,8 @@ import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/works
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
import { ExtendObjectTypeDefinitionV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory';
import { RelationTypeV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-type-v2.factory';
import { ArgsFactory } from './args.factory';
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
@ -31,7 +33,9 @@ export const workspaceSchemaBuilderFactories = [
EnumTypeDefinitionFactory,
CompositeEnumTypeDefinitionFactory,
RelationTypeFactory,
RelationTypeV2Factory,
ExtendObjectTypeDefinitionFactory,
ExtendObjectTypeDefinitionV2Factory,
ConnectionTypeFactory,
ConnectionTypeDefinitionFactory,
EdgeTypeFactory,

View File

@ -0,0 +1,56 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLOutputType } from 'graphql';
import { FieldMetadataType } from 'twenty-shared';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
@Injectable()
export class RelationTypeV2Factory {
private readonly logger = new Logger(RelationTypeV2Factory.name);
constructor(
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
) {}
public create(
fieldMetadata: FieldMetadataInterface<FieldMetadataType.RELATION>,
): GraphQLOutputType {
if (!fieldMetadata.settings) {
throw new Error(
`Field Metadata of type RELATION with id ${fieldMetadata.id} has no settings`,
);
}
if (!fieldMetadata.relationTargetObjectMetadataId) {
throw new Error(
`Field Metadata of type RELATION with id ${fieldMetadata.id} has no relation target object metadata id`,
);
}
const relationGqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
fieldMetadata.relationTargetObjectMetadataId,
fieldMetadata.settings.relationType === RelationType.ONE_TO_MANY
? ObjectTypeDefinitionKind.Connection
: ObjectTypeDefinitionKind.Plain,
);
if (!relationGqlType) {
this.logger.error(
`Could not find a relation type for ${fieldMetadata.id}`,
{
fieldMetadata,
},
);
throw new Error(`Could not find a relation type for ${fieldMetadata.id}`);
}
return relationGqlType;
}
}

View File

@ -5,8 +5,8 @@ import { GraphQLOutputType } from 'graphql';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-metadata.interface';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { RelationDirection } from 'src/engine/utils/deduce-relation-direction.util';
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
@ -24,13 +24,13 @@ export class RelationTypeFactory {
relationMetadata: RelationMetadataInterface,
relationDirection: RelationDirection,
): GraphQLOutputType {
let relationQqlType: GraphQLOutputType | undefined = undefined;
let relationGqlType: GraphQLOutputType | undefined = undefined;
if (
relationDirection === RelationDirection.FROM &&
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY
) {
relationQqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
relationGqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
relationMetadata.toObjectMetadataId,
ObjectTypeDefinitionKind.Connection,
);
@ -40,13 +40,13 @@ export class RelationTypeFactory {
? relationMetadata.toObjectMetadataId
: relationMetadata.fromObjectMetadataId;
relationQqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
relationGqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
relationObjectId,
ObjectTypeDefinitionKind.Plain,
);
}
if (!relationQqlType) {
if (!relationGqlType) {
this.logger.error(
`Could not find a relation type for ${fieldMetadata.id}`,
{
@ -57,6 +57,6 @@ export class RelationTypeFactory {
throw new Error(`Could not find a relation type for ${fieldMetadata.id}`);
}
return relationQqlType;
return relationGqlType;
}
}

View File

@ -1,5 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import chalk from 'chalk';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
@ -7,6 +9,9 @@ import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/works
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
import { ExtendObjectTypeDefinitionV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory';
@ -39,6 +44,8 @@ export class TypeDefinitionsGenerator {
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory,
private readonly extendObjectTypeDefinitionFactory: ExtendObjectTypeDefinitionFactory,
private readonly extendObjectTypeDefinitionV2Factory: ExtendObjectTypeDefinitionV2Factory,
private readonly featureFlagService: FeatureFlagService,
) {}
generate(
@ -256,18 +263,48 @@ export class TypeDefinitionsGenerator {
this.typeDefinitionsStorage.addEnumTypes(enumTypeDefs);
}
private generateExtendedObjectTypeDefs(
private async generateExtendedObjectTypeDefs(
objectMetadataCollection: ObjectMetadataInterface[],
options: WorkspaceBuildSchemaOptions,
) {
// Generate extended object type defs only for objects that contain composite fields
const objectMetadataCollectionWithCompositeFields =
objectMetadataCollection.filter(objectContainsRelationField);
const objectTypeDefs = objectMetadataCollectionWithCompositeFields.map(
(objectMetadata) =>
this.extendObjectTypeDefinitionFactory.create(objectMetadata, options),
const workspaceId =
objectMetadataCollectionWithCompositeFields[0]?.workspaceId;
if (!workspaceId) {
throw new Error('Workspace ID not found');
}
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs);
if (!isNewRelationEnabled) {
const objectTypeDefs = objectMetadataCollectionWithCompositeFields.map(
(objectMetadata) =>
this.extendObjectTypeDefinitionFactory.create(
objectMetadata,
options,
),
);
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs);
} else {
this.logger.log(
chalk.green('Extend object type definition with new relation fields'),
);
const objectTypeDefsV2 = objectMetadataCollectionWithCompositeFields.map(
(objectMetadata) =>
this.extendObjectTypeDefinitionV2Factory.create(
objectMetadata,
options,
),
);
this.typeDefinitionsStorage.addObjectTypes(objectTypeDefsV2);
}
}
}

View File

@ -1,16 +1,17 @@
import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { TypeDefinitionsGenerator } from './type-definitions.generator';
import { WorkspaceGraphQLSchemaFactory } from './workspace-graphql-schema.factory';
import { workspaceSchemaBuilderFactories } from './factories/factories';
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
import { TypeMapperService } from './services/type-mapper.service';
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
@Module({
imports: [ObjectMetadataModule],
imports: [ObjectMetadataModule, FeatureFlagModule],
providers: [
TypeDefinitionsStorage,
TypeMapperService,