From fbb67d74c8f06c348afeb54c0102cff99a43fd86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Wed, 29 Jan 2025 10:33:17 +0100 Subject: [PATCH] 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. --- .../__tests__/workspace.factory.spec.ts | 5 + .../api/graphql/core-graphql-api.module.ts | 2 + .../errors/graphql-query-runner.exception.ts | 1 + ...xtend-object-type-definition-v2.factory.ts | 150 ++++++++++++++++++ .../extend-object-type-definition.factory.ts | 8 +- .../factories/factories.ts | 4 + .../factories/relation-type-v2.factory.ts | 56 +++++++ .../factories/relation-type.factory.ts | 12 +- .../type-definitions.generator.ts | 47 +++++- .../workspace-schema-builder.module.ts | 5 +- .../api/graphql/workspace-schema.factory.ts | 50 ++++++ .../object-record-changed-values.spec.ts | 1 + .../interfaces/object-metadata.interface.ts | 1 + .../field-metadata-relation.service.ts | 6 +- .../object-metadata.service.ts | 2 - ...e-field-maps-from-object-metadata.util.ts} | 2 +- .../utils/is-relation-field-metadata.util.ts | 9 ++ .../workspace-cache-storage.service.ts | 24 ++- 18 files changed, 361 insertions(+), 24 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-type-v2.factory.ts rename packages/twenty-server/src/engine/metadata-modules/utils/{clean-object-metadata.util.ts => remove-field-maps-from-object-metadata.util.ts} (89%) create mode 100644 packages/twenty-server/src/engine/utils/is-relation-field-metadata.util.ts diff --git a/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts index 05d0cbaa9..8787bc320 100644 --- a/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts @@ -4,6 +4,7 @@ import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars- import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory'; import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; @@ -44,6 +45,10 @@ describe('WorkspaceSchemaFactory', () => { provide: WorkspaceMetadataCacheService, useValue: {}, }, + { + provide: FeatureFlagService, + useValue: {}, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts b/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts index 1b0603186..28fafb885 100644 --- a/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module'; import { WorkspaceSchemaBuilderModule } from 'src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { MetadataEngineModule } from 'src/engine/metadata-modules/metadata-engine.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -16,6 +17,7 @@ import { WorkspaceSchemaFactory } from './workspace-schema.factory'; WorkspaceResolverBuilderModule, WorkspaceCacheStorageModule, WorkspaceMetadataCacheModule, + FeatureFlagModule, ], providers: [WorkspaceSchemaFactory, ScalarsExplorerService], exports: [WorkspaceSchemaFactory], diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts index 55c70c5af..747a56308 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts @@ -20,4 +20,5 @@ export enum GraphqlQueryRunnerExceptionCode { INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST', INVALID_ARGS_LAST = 'INVALID_ARGS_LAST', METADATA_CACHE_VERSION_NOT_FOUND = 'METADATA_CACHE_VERSION_NOT_FOUND', + METADATA_CACHE_FEATURE_FLAG_RECOMPUTATION_REQUIRED = 'METADATA_CACHE_FEATURE_FLAG_RECOMPUTATION_REQUIRED', } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts new file mode 100644 index 000000000..036a3b338 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts @@ -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 { + const fields: GraphQLFieldConfigMap = {}; + + 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; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition.factory.ts index 1e2b9c563..6cf60ee26 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition.factory.ts @@ -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', diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts index 1e1219192..b1f229068 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/factories.ts @@ -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, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-type-v2.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-type-v2.factory.ts new file mode 100644 index 000000000..9a31f5f44 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-type-v2.factory.ts @@ -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, + ): 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; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-type.factory.ts index 2c5fbea0b..6017a3e15 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-type.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-type.factory.ts @@ -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; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts index 57e959dfc..caedc0ded 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts @@ -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); + } } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module.ts index a5bc54e85..969de9f55 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module.ts @@ -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, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index c80bb0c2e..de13052f8 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import chalk from 'chalk'; import { GraphQLSchema, printSchema } from 'graphql'; import { gql } from 'graphql-tag'; @@ -13,9 +14,12 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory'; import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; + @Injectable() export class WorkspaceSchemaFactory { constructor( @@ -25,6 +29,7 @@ export class WorkspaceSchemaFactory { private readonly workspaceResolverFactory: WorkspaceResolverFactory, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, + private readonly featureFlagService: FeatureFlagService, ) {} async createGraphQLSchema(authContext: AuthContext): Promise { @@ -32,6 +37,22 @@ export class WorkspaceSchemaFactory { return new GraphQLSchema({}); } + const cachedIsNewRelationEnabled = + await this.workspaceCacheStorageService.getIsNewRelationEnabled( + authContext.workspace.id, + ); + + const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + authContext.workspace.id, + ); + + if (isNewRelationEnabled) { + console.log( + chalk.yellow('🚧 New relation schema generation is enabled 🚧'), + ); + } + const dataSourcesMetadata = await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( authContext.workspace.id, @@ -50,12 +71,41 @@ export class WorkspaceSchemaFactory { await this.workspaceMetadataCacheService.recomputeMetadataCache({ workspaceId: authContext.workspace.id, }); + throw new GraphqlQueryRunnerException( 'Metadata cache version not found', GraphqlQueryRunnerExceptionCode.METADATA_CACHE_VERSION_NOT_FOUND, ); } + // TODO: remove this after the feature flag is droped + if ( + (isNewRelationEnabled && cachedIsNewRelationEnabled === undefined) || + (isNewRelationEnabled !== cachedIsNewRelationEnabled && + cachedIsNewRelationEnabled !== undefined) + ) { + console.log( + chalk.yellow('Recomputing due to new relation feature flag'), + { + isNewRelationEnabled, + }, + ); + + await this.workspaceCacheStorageService.setIsNewRelationEnabled( + authContext.workspace.id, + isNewRelationEnabled, + ); + + await this.workspaceMetadataCacheService.recomputeMetadataCache({ + workspaceId: authContext.workspace.id, + }); + + throw new GraphqlQueryRunnerException( + 'Metadata cache recomputation required due to relation feature flag change', + GraphqlQueryRunnerExceptionCode.METADATA_CACHE_FEATURE_FLAG_RECOMPUTATION_REQUIRED, + ); + } + const objectMetadataMaps = await this.workspaceCacheStorageService.getObjectMetadataMaps( authContext.workspace.id, diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts index 2352ef8d9..fbfc94cd0 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts @@ -10,6 +10,7 @@ const mockObjectMetadata: ObjectMetadataInterface = { labelPlural: 'Objects', description: 'Test object metadata', targetTableName: 'test_table', + workspaceId: '1', fromRelations: [], toRelations: [], fields: [], diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface.ts index cc903fba3..6fc29826b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface.ts @@ -6,6 +6,7 @@ import { RelationMetadataInterface } from './relation-metadata.interface'; export interface ObjectMetadataInterface { id: string; standardId?: string | null; + workspaceId: string; nameSingular: string; namePlural: string; labelSingular: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/relation/field-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/relation/field-metadata-relation.service.ts index 8d58fff2c..ce9a2789a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/relation/field-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/relation/field-metadata-relation.service.ts @@ -8,7 +8,7 @@ import { FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { cleanObjectMetadata } from 'src/engine/metadata-modules/utils/clean-object-metadata.util'; +import { removeFieldMapsFromObjectMetadata } from 'src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @Injectable() @@ -95,11 +95,11 @@ export class FieldMetadataRelationService { } return { - sourceObjectMetadata: cleanObjectMetadata( + sourceObjectMetadata: removeFieldMapsFromObjectMetadata( sourceObjectMetadata, ) as ObjectMetadataEntity, sourceFieldMetadata: sourceFieldMetadata as FieldMetadataEntity, - targetObjectMetadata: cleanObjectMetadata( + targetObjectMetadata: removeFieldMapsFromObjectMetadata( targetObjectMetadata, ) as ObjectMetadataEntity, targetFieldMetadata: targetFieldMetadata as FieldMetadataEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 329f85486..c77164163 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -1,8 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import console from 'console'; - import { i18n } from '@lingui/core'; import { Query, QueryOptions } from '@ptc-org/nestjs-query-core'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/clean-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts similarity index 89% rename from packages/twenty-server/src/engine/metadata-modules/utils/clean-object-metadata.util.ts rename to packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts index 1ea6c33a6..dcba421ec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/clean-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util.ts @@ -4,7 +4,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -export const cleanObjectMetadata = ( +export const removeFieldMapsFromObjectMetadata = ( objectMetadata: ObjectMetadataItemWithFieldMaps, ): ObjectMetadataInterface => omit(objectMetadata, ['fieldsById', 'fieldsByName']); diff --git a/packages/twenty-server/src/engine/utils/is-relation-field-metadata.util.ts b/packages/twenty-server/src/engine/utils/is-relation-field-metadata.util.ts new file mode 100644 index 000000000..9dada2530 --- /dev/null +++ b/packages/twenty-server/src/engine/utils/is-relation-field-metadata.util.ts @@ -0,0 +1,9 @@ +import { FieldMetadataType } from 'twenty-shared'; + +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +export const isRelationFieldMetadata = ( + fieldMetadata: FieldMetadataInterface<'default' | FieldMetadataType.RELATION>, +): fieldMetadata is FieldMetadataInterface => { + return fieldMetadata.type === FieldMetadataType.RELATION; +}; diff --git a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts index 6c4ddf976..3a8e16646 100644 --- a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts +++ b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts @@ -5,6 +5,7 @@ import { EntitySchemaOptions } from 'typeorm'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export enum WorkspaceCacheKeys { @@ -12,6 +13,7 @@ export enum WorkspaceCacheKeys { GraphQLUsedScalarNames = 'graphql:used-scalar-names', GraphQLOperations = 'graphql:operations', ORMEntitySchemas = 'orm:entity-schemas', + GraphQLFeatureFlag = 'graphql:feature-flag', MetadataObjectMetadataMaps = 'metadata:object-metadata-maps', MetadataObjectMetadataOngoingCachingLock = 'metadata:object-metadata-ongoing-caching-lock', MetadataVersion = 'metadata:workspace-metadata-version', @@ -156,6 +158,22 @@ export class WorkspaceCacheStorageService { ); } + // TODO: remove this after the feature flag is droped + setIsNewRelationEnabled(workspaceId: string, isNewRelationEnabled: boolean) { + return this.cacheStorageService.set( + `${WorkspaceCacheKeys.GraphQLFeatureFlag}:${workspaceId}:${FeatureFlagKey.IsNewRelationEnabled}`, + isNewRelationEnabled, + TTL_INFINITE, + ); + } + + // TODO: remove this after the feature flag is droped + getIsNewRelationEnabled(workspaceId: string): Promise { + return this.cacheStorageService.get( + `${WorkspaceCacheKeys.GraphQLFeatureFlag}:${workspaceId}:${FeatureFlagKey.IsNewRelationEnabled}`, + ); + } + async flush(workspaceId: string, metadataVersion: number): Promise { await this.cacheStorageService.del( `${WorkspaceCacheKeys.MetadataObjectMetadataMaps}:${workspaceId}:${metadataVersion}`, @@ -172,9 +190,13 @@ export class WorkspaceCacheStorageService { await this.cacheStorageService.del( `${WorkspaceCacheKeys.ORMEntitySchemas}:${workspaceId}:${metadataVersion}`, ); - await this.cacheStorageService.del( `${WorkspaceCacheKeys.MetadataObjectMetadataOngoingCachingLock}:${workspaceId}:${metadataVersion}`, ); + + // TODO: remove this after the feature flag is droped + await this.cacheStorageService.del( + `${FeatureFlagKey.IsNewRelationEnabled}:${workspaceId}`, + ); } }