From 0c8eb149e65e83b3206df25c0eb7e33a8a44dd40 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 25 Apr 2025 01:02:49 +0200 Subject: [PATCH] Refactor new relation sync (#11711) In this PR: - this should fix the sync metadata for new relation system This goes with the recent PR: https://github.com/twentyhq/twenty/pull/11725 What we want: - ONE_TO_MANY relations should have no joinColumn and no onDelete - MANY_TO_ONE should have both --- ...ate-relations-to-field-metadata.command.ts | 10 +- .../token/services/access-token.service.ts | 3 + .../field-metadata/field-metadata.service.ts | 4 +- .../object-metadata-field-relation.service.ts | 1 - .../relation-column-action.factory.ts | 6 + .../standard-field-relation.factory.ts | 353 ++++++++++++------ ...ce-sync-field-metadata-relation.service.ts | 64 ++-- 7 files changed, 285 insertions(+), 156 deletions(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command.ts index 24c66125a..497dea4f3 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-52/0-52-migrate-relations-to-field-metadata.command.ts @@ -129,8 +129,14 @@ export class MigrateRelationsToFieldMetadataCommand extends ActiveOrSuspendedWor ...fieldMetadata, settings: { relationType, - onDelete: relationMetadata.onDeleteAction, - joinColumnName: joinColumnFieldMetadata?.name, + onDelete: + relationType === RelationType.MANY_TO_ONE + ? relationMetadata.onDeleteAction + : undefined, + joinColumnName: + relationType === RelationType.MANY_TO_ONE + ? joinColumnFieldMetadata?.name + : undefined, }, relationTargetFieldMetadataId, relationTargetObjectMetadataId, diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts index 17df32f37..0095cd776 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts @@ -72,6 +72,9 @@ export class AccessTokenService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'workspaceMember', + { + shouldFailIfMetadataNotFound: false, + }, ); const workspaceMember = await workspaceMemberRepository.findOne({ diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 9edca8aed..8aebca6a3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -355,7 +355,7 @@ export class FieldMetadataService extends TypeOrmQueryService) .settings?.relationType === RelationType.MANY_TO_ONE; @@ -385,7 +385,7 @@ export class FieldMetadataService extends TypeOrmQueryService).settings?.joinColumnName}` : `${(targetFieldMetadata as FieldMetadataEntity).settings?.joinColumnName}`, } satisfies WorkspaceMigrationColumnDrop, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts index 57f2f4f45..333115872 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts @@ -100,7 +100,6 @@ export class ObjectMetadataFieldRelationService { ...sourceFieldMetadata, settings: { relationType: RelationType.ONE_TO_MANY, - onDelete: RelationOnDeleteAction.CASCADE, }, relationTargetObjectMetadataId: targetObjectMetadata.id, relationTargetFieldMetadataId: targetFieldMetadata.id, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory.ts index 1cd31d70f..452bf2e71 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory.ts @@ -55,6 +55,12 @@ export class RelationColumnActionFactory extends ColumnActionAbstractFactory, ): FieldMetadataEntity[] { - return customObjectFactories.flatMap((customObjectFactory) => - this.updateFieldRelationMetadata( - customObjectFactory, - context, - originalObjectMetadataMap, - ), - ); - } - - createFieldRelationForStandardObject( - standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[], - context: WorkspaceSyncContext, - originalObjectMetadataMap: Record, - ): Map[]> { - return standardObjectMetadataDefinitions.reduce( - (acc, standardObjectMetadata) => { - const workspaceEntityMetadataArgs = metadataArgsStorage.filterEntities( - standardObjectMetadata, - ); - - if (!workspaceEntityMetadataArgs) { - return acc; - } - - if ( - isGatedAndNotEnabled( - workspaceEntityMetadataArgs.gate, - context.featureFlags, - ) - ) { - return acc; - } - - acc.set( - workspaceEntityMetadataArgs.standardId, - this.updateFieldRelationMetadata( - standardObjectMetadata, - context, - originalObjectMetadataMap, - ), - ); - - return acc; - }, - new Map[]>(), - ); - } - - private updateFieldRelationMetadata( - workspaceEntityOrCustomRelationFactory: - | typeof BaseWorkspaceEntity - | CustomRelationFactory, - context: WorkspaceSyncContext, - originalObjectMetadataMap: Record, - ): FieldMetadataEntity[] { - const target = - 'metadata' in workspaceEntityOrCustomRelationFactory - ? workspaceEntityOrCustomRelationFactory.metadata - : workspaceEntityOrCustomRelationFactory; - const workspaceEntity = - 'metadata' in workspaceEntityOrCustomRelationFactory - ? metadataArgsStorage.filterExtendedEntities(target) - : metadataArgsStorage.filterEntities(target); const workspaceRelationMetadataArgsCollection = - metadataArgsStorage.filterRelations(target); + metadataArgsStorage.filterRelations(CustomWorkspaceEntity); - if (!workspaceEntity) { + const joinColumnsMetadataArgsCollection = + metadataArgsStorage.filterJoinColumns(CustomWorkspaceEntity); + + const objectNameSingular = customObjectMetadata.nameSingular; + + const sourceObjectMetadata = originalObjectMetadataMap[objectNameSingular]; + + if (!isDefined(sourceObjectMetadata)) { throw new Error( - `Object metadata decorator not found, can't parse ${target.name}`, + `Source object ${objectNameSingular} not found in database while parsing ${objectNameSingular} relations`, ); } + return workspaceRelationMetadataArgsCollection.map( + (workspaceRelationMetadataArgs) => { + const inverseSideTarget = + workspaceRelationMetadataArgs.inverseSideTarget(); + const targetObjectNameSingular = convertClassNameToObjectMetadataName( + inverseSideTarget.name, + ); + const targetObjectMetadata = + originalObjectMetadataMap[targetObjectNameSingular]; + + if (!isDefined(targetObjectMetadata)) { + throw new Error( + `Target object ${targetObjectNameSingular} not found in database while parsing ${objectNameSingular} relations`, + ); + } + + const sourceFieldMetadataName = workspaceRelationMetadataArgs.name; + + const targetFieldMetadataName = + workspaceRelationMetadataArgs.inverseSideFieldKey ?? + objectNameSingular; + + const targetFieldMetadata = targetObjectMetadata.fields.find( + (field) => field.name === targetFieldMetadataName, + ) as FieldMetadataEntity; + + if (!isDefined(targetFieldMetadata)) { + throw new Error( + `Target field ${targetFieldMetadataName} not found in object ${targetObjectNameSingular} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, + ); + } + + const joinColumnName = + workspaceRelationMetadataArgs.type === RelationType.MANY_TO_ONE + ? getJoinColumn( + joinColumnsMetadataArgsCollection, + workspaceRelationMetadataArgs as WorkspaceRelationMetadataArgs, + ) + : undefined; + + const sourceFieldMetadata = sourceObjectMetadata.fields.find( + (field) => field.name === sourceFieldMetadataName, + ) as FieldMetadataEntity; + + if (!isDefined(sourceFieldMetadata)) { + throw new Error( + `Source field ${sourceFieldMetadataName} not found in object ${sourceFieldMetadata.name} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, + ); + } + + return { + ...sourceFieldMetadata, + type: FieldMetadataType.RELATION, + settings: { + relationType: workspaceRelationMetadataArgs.type, + onDelete: + workspaceRelationMetadataArgs.type === RelationType.MANY_TO_ONE + ? workspaceRelationMetadataArgs.onDelete + : undefined, + joinColumnName, + }, + relationTargetObjectMetadataId: targetObjectMetadata.id, + relationTargetFieldMetadataId: targetFieldMetadata.id, + isNullable: workspaceRelationMetadataArgs.isNullable, + } satisfies FieldMetadataEntity; + }, + ); + } + + computeRelationFieldsForStandardObject( + standardObjectMetadataWorkspaceEntity: typeof BaseWorkspaceEntity, + context: WorkspaceSyncContext, + originalObjectMetadataMap: Record, + ): FieldMetadataEntity[] { + const workspaceEntityMetadataArgs = metadataArgsStorage.filterEntities( + standardObjectMetadataWorkspaceEntity, + ); + + if (!workspaceEntityMetadataArgs) { + return []; + } + if ( - !workspaceRelationMetadataArgsCollection || - isGatedAndNotEnabled(workspaceEntity?.gate, context.featureFlags) + isGatedAndNotEnabled( + workspaceEntityMetadataArgs.gate, + context.featureFlags, + ) ) { return []; } - return workspaceRelationMetadataArgsCollection + const sourceObjectNameSingular = workspaceEntityMetadataArgs.nameSingular; + + const workspaceStaticRelationMetadataArgsCollection = + metadataArgsStorage.filterRelations( + standardObjectMetadataWorkspaceEntity, + ); + + const workspaceDynamicRelationMetadataArgsCollection = + metadataArgsStorage.filterDynamicRelations( + standardObjectMetadataWorkspaceEntity, + ); + + const joinColumnsMetadataArgsCollection = + metadataArgsStorage.filterJoinColumns( + standardObjectMetadataWorkspaceEntity, + ); + + if ( + !isDefined(workspaceStaticRelationMetadataArgsCollection) && + !isDefined(workspaceDynamicRelationMetadataArgsCollection) + ) { + throw new Error( + `No relations found for object ${sourceObjectNameSingular}`, + ); + } + + const sourceObjectMetadata = + originalObjectMetadataMap[sourceObjectNameSingular]; + + if (!isDefined(sourceObjectMetadata)) { + throw new Error( + `Source object ${sourceObjectNameSingular} not found in database while parsing relations`, + ); + } + + const relationsFromDynamicRelations = + workspaceDynamicRelationMetadataArgsCollection + .filter( + (workspaceDynamicRelationMetadataArgs) => + !isGatedAndNotEnabled( + workspaceDynamicRelationMetadataArgs.gate, + context.featureFlags, + ), + ) + .flatMap((workspaceDynamicRelationMetadataArgs) => { + const customObjectMetadataItems = Object.values( + originalObjectMetadataMap, + ).filter((objectMetadata) => objectMetadata.isCustom); + + // TODO: this is hacky and needs to be simplified + return customObjectMetadataItems.flatMap((targetObjectMetadata) => { + const relationMetadataArgs = + workspaceDynamicRelationMetadataArgs.argsFactory( + targetObjectMetadata, + ); + + const sourceFieldMetadata = sourceObjectMetadata?.fields.find( + (field) => field.name === relationMetadataArgs.name, + ) as FieldMetadataEntity; + + const targetFieldMetadata = targetObjectMetadata?.fields.find( + (field) => + field.name === + workspaceDynamicRelationMetadataArgs.inverseSideFieldKey, + ) as FieldMetadataEntity; + + if (!isDefined(sourceFieldMetadata)) { + throw new Error( + `Source field ${relationMetadataArgs.name} not found in object ${sourceObjectNameSingular} for relation ${relationMetadataArgs.name} of type ${relationMetadataArgs}`, + ); + } + + if (!isDefined(targetFieldMetadata)) { + throw new Error( + `Target field ${workspaceDynamicRelationMetadataArgs.inverseSideFieldKey} not found in object ${targetObjectMetadata.nameSingular} for relation ${relationMetadataArgs.name} of type ${workspaceDynamicRelationMetadataArgs.type}`, + ); + } + + return { + ...sourceFieldMetadata, + type: FieldMetadataType.RELATION, + settings: { + relationType: workspaceDynamicRelationMetadataArgs.type, + onDelete: + workspaceDynamicRelationMetadataArgs.type === + RelationType.MANY_TO_ONE + ? workspaceDynamicRelationMetadataArgs.onDelete + : undefined, + joinColumnName: relationMetadataArgs.joinColumn, + }, + relationTargetObjectMetadataId: targetObjectMetadata.id, + relationTargetFieldMetadataId: targetFieldMetadata.id, + isNullable: workspaceDynamicRelationMetadataArgs.isNullable, + } satisfies FieldMetadataEntity; + }); + }); + + const staticRelations = workspaceStaticRelationMetadataArgsCollection .filter( (workspaceRelationMetadataArgs) => !isGatedAndNotEnabled( @@ -113,13 +235,6 @@ export class StandardFieldRelationFactory { ), ) .map((workspaceRelationMetadataArgs) => { - // Compute reflect relation metadata - const sourceObjectNameSingular = - 'object' in workspaceEntityOrCustomRelationFactory - ? workspaceEntityOrCustomRelationFactory.object.nameSingular - : convertClassNameToObjectMetadataName( - workspaceRelationMetadataArgs.target.name, - ); const inverseSideTarget = workspaceRelationMetadataArgs.inverseSideTarget(); const targetObjectNameSingular = convertClassNameToObjectMetadataName( @@ -127,60 +242,62 @@ export class StandardFieldRelationFactory { ); const sourceFieldMetadataName = workspaceRelationMetadataArgs.name; const targetFieldMetadataName = - (workspaceRelationMetadataArgs.inverseSideFieldKey as - | string - | undefined) ?? sourceObjectNameSingular; - const sourceObjectMetadata = - originalObjectMetadataMap[sourceObjectNameSingular]; - const joinColumnsMetadataArgsCollection = - metadataArgsStorage.filterJoinColumns(target); - const joinColumnName = getJoinColumn( - joinColumnsMetadataArgsCollection, - workspaceRelationMetadataArgs, - ); + workspaceRelationMetadataArgs.inverseSideFieldKey ?? + sourceObjectNameSingular; - assert( - sourceObjectMetadata, - `Source object ${sourceObjectNameSingular} not found in databse for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, - ); - - const targetObjectMetadata = - originalObjectMetadataMap[targetObjectNameSingular]; - - assert( - targetObjectMetadata, - `Target object ${targetObjectNameSingular} not found in databse for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, - ); + const joinColumnName = + workspaceRelationMetadataArgs.type === RelationType.MANY_TO_ONE + ? getJoinColumn( + joinColumnsMetadataArgsCollection, + workspaceRelationMetadataArgs as WorkspaceRelationMetadataArgs, + ) + : undefined; const sourceFieldMetadata = sourceObjectMetadata?.fields.find( (field) => field.name === sourceFieldMetadataName, ) as FieldMetadataEntity; - assert( - sourceFieldMetadata, - `Source field ${sourceFieldMetadataName} not found in object ${sourceObjectNameSingular} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, - ); + if (!isDefined(sourceFieldMetadata)) { + throw new Error( + `Source field ${sourceFieldMetadataName} not found in object ${sourceObjectNameSingular} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, + ); + } - const targetFieldMetadata = targetObjectMetadata?.fields.find( + const targetObjectMetadata = + originalObjectMetadataMap[targetObjectNameSingular]; + + if (!isDefined(targetObjectMetadata)) { + throw new Error( + `Target object ${targetObjectNameSingular} not found in database for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, + ); + } + + const targetFieldMetadata = targetObjectMetadata.fields.find( (field) => field.name === targetFieldMetadataName, ) as FieldMetadataEntity; - assert( - targetFieldMetadata, - `Target field ${targetFieldMetadataName} not found in object ${targetObjectNameSingular} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, - ); + if (!isDefined(targetFieldMetadata)) { + throw new Error( + `Target field ${targetFieldMetadataName} not found in object ${targetObjectNameSingular} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`, + ); + } return { ...sourceFieldMetadata, type: FieldMetadataType.RELATION, settings: { relationType: workspaceRelationMetadataArgs.type, - onDelete: workspaceRelationMetadataArgs.onDelete, + onDelete: + workspaceRelationMetadataArgs.type === RelationType.MANY_TO_ONE + ? workspaceRelationMetadataArgs.onDelete + : undefined, joinColumnName, }, relationTargetObjectMetadataId: targetObjectMetadata.id, relationTargetFieldMetadataId: targetFieldMetadata.id, } satisfies FieldMetadataEntity; }); + + return [...staticRelations, ...relationsFromDynamicRelations]; } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service.ts index b440fc625..beaf3a24f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service.ts @@ -13,7 +13,7 @@ import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-syn import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; +import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { WorkspaceMigrationFieldRelationFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field-relation.factory'; import { FieldMetadataUpdate, @@ -150,26 +150,30 @@ export class WorkspaceSyncFieldMetadataRelationService { originalObjectMetadataMapByName: Record, storage: WorkspaceSyncStorage, ): Promise { - // Create standard field metadata map - const standardFieldMetadataRelationCollection = - this.standardFieldRelationFactory.createFieldRelationForStandardObject( - standardObjectMetadataDefinitions, - context, - originalObjectMetadataMapByName, - ); - // Create map of original and standard object metadata by standard ids const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier( originalObjectMetadataCollection, ); // Loop over all standard objects and compare them with the objects in DB - for (const [ - standardObjectId, - standardFieldMetadataCollection, - ] of standardFieldMetadataRelationCollection) { + for (const standardObjectMetadataDefinition of standardObjectMetadataDefinitions) { + const workspaceEntityMetadataArgs = metadataArgsStorage.filterEntities( + standardObjectMetadataDefinition, + ); + + if (!workspaceEntityMetadataArgs) { + continue; + } + + const standardFieldMetadataRelationCollection = + this.standardFieldRelationFactory.computeRelationFieldsForStandardObject( + standardObjectMetadataDefinition, + context, + originalObjectMetadataMapByName, + ); + const originalObjectMetadata = - originalObjectMetadataMap[standardObjectId]; + originalObjectMetadataMap[workspaceEntityMetadataArgs.standardId]; const originalFieldRelationMetadataCollection = (originalObjectMetadata?.fields.filter( @@ -183,7 +187,7 @@ export class WorkspaceSyncFieldMetadataRelationService { const fieldComparatorResults = this.workspaceFieldRelationComparator.compare( originalFieldRelationMetadataCollection, - standardFieldMetadataCollection, + standardFieldMetadataRelationCollection, ); this.storeComparatorResults(fieldComparatorResults, storage); @@ -196,25 +200,22 @@ export class WorkspaceSyncFieldMetadataRelationService { originalObjectMetadataMapByName: Record, storage: WorkspaceSyncStorage, ): Promise { - // Create standard field metadata collection - const customFieldMetadataRelationCollection = - this.standardFieldRelationFactory.createFieldRelationForCustomObject( - customObjectMetadataCollection.map((objectMetadata) => ({ - object: objectMetadata, - metadata: CustomWorkspaceEntity, - })), - context, - originalObjectMetadataMapByName, - ); - // Loop over all custom objects from the DB and compare their fields with standard fields for (const customObjectMetadata of customObjectMetadataCollection) { - /** - * COMPARE FIELD METADATA - */ + const originalFieldRelationMetadataCollection = + (customObjectMetadata.fields.filter( + (field) => field.type === FieldMetadataType.RELATION, + ) ?? []) as FieldMetadataEntity[]; + + const customFieldMetadataRelationCollection = + this.standardFieldRelationFactory.computeRelationFieldsForCustomObject( + customObjectMetadata, + originalObjectMetadataMapByName, + ); + const fieldComparatorResults = this.workspaceFieldRelationComparator.compare( - customObjectMetadata.fields as FieldMetadataEntity[], + originalFieldRelationMetadataCollection, customFieldMetadataRelationCollection, ); @@ -233,9 +234,6 @@ export class WorkspaceSyncFieldMetadataRelationService { await objectMetadataRepository.find({ where: { workspaceId: context.workspaceId, - fields: { - type: FieldMetadataType.RELATION, - }, }, relations: ['dataSource', 'fields'], });