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
This commit is contained in:
Charles Bochet
2025-04-25 01:02:49 +02:00
committed by GitHub
parent 9fb7ef5d47
commit 0c8eb149e6
7 changed files with 285 additions and 156 deletions

View File

@ -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,

View File

@ -72,6 +72,9 @@ export class AccessTokenService {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{
shouldFailIfMetadataNotFound: false,
},
);
const workspaceMember = await workspaceMemberRepository.findOne({

View File

@ -355,7 +355,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
});
if (fieldMetadata.type === FieldMetadataType.RELATION) {
const isManyToManyRelation =
const isManyToOneRelation =
(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>)
.settings?.relationType === RelationType.MANY_TO_ONE;
@ -385,7 +385,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: isManyToManyRelation
columnName: isManyToOneRelation
? `${(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`
: `${(targetFieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`,
} satisfies WorkspaceMigrationColumnDrop,

View File

@ -100,7 +100,6 @@ export class ObjectMetadataFieldRelationService {
...sourceFieldMetadata,
settings: {
relationType: RelationType.ONE_TO_MANY,
onDelete: RelationOnDeleteAction.CASCADE,
},
relationTargetObjectMetadataId: targetObjectMetadata.id,
relationTargetFieldMetadataId: targetFieldMetadata.id,

View File

@ -55,6 +55,12 @@ export class RelationColumnActionFactory extends ColumnActionAbstractFactory<Fie
return [];
}
if (
currentFieldMetadata.settings.relationType === RelationType.ONE_TO_MANY
) {
return [];
}
const currentJoinColumnName = currentFieldMetadata.settings.joinColumnName;
const alteredJoinColumnName = alteredFieldMetadata.settings.joinColumnName;

View File

@ -1,110 +1,232 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
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 { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
import { assert } from 'src/utils/assert';
interface CustomRelationFactory {
object: ObjectMetadataEntity;
metadata: typeof BaseWorkspaceEntity;
}
@Injectable()
export class StandardFieldRelationFactory {
createFieldRelationForCustomObject(
customObjectFactories: CustomRelationFactory[],
context: WorkspaceSyncContext,
computeRelationFieldsForCustomObject(
customObjectMetadata: ObjectMetadataEntity,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
): FieldMetadataEntity<FieldMetadataType.RELATION>[] {
return customObjectFactories.flatMap((customObjectFactory) =>
this.updateFieldRelationMetadata(
customObjectFactory,
context,
originalObjectMetadataMap,
),
);
}
createFieldRelationForStandardObject(
standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[],
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
): Map<string, FieldMetadataEntity<FieldMetadataType.RELATION>[]> {
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<string, FieldMetadataEntity<FieldMetadataType.RELATION>[]>(),
);
}
private updateFieldRelationMetadata(
workspaceEntityOrCustomRelationFactory:
| typeof BaseWorkspaceEntity
| CustomRelationFactory,
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
): FieldMetadataEntity<FieldMetadataType.RELATION>[] {
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<FieldMetadataType.RELATION>;
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<FieldMetadataType.RELATION>;
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<FieldMetadataType.RELATION>;
},
);
}
computeRelationFieldsForStandardObject(
standardObjectMetadataWorkspaceEntity: typeof BaseWorkspaceEntity,
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
): FieldMetadataEntity<FieldMetadataType.RELATION>[] {
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<FieldMetadataType.RELATION>;
const targetFieldMetadata = targetObjectMetadata?.fields.find(
(field) =>
field.name ===
workspaceDynamicRelationMetadataArgs.inverseSideFieldKey,
) as FieldMetadataEntity<FieldMetadataType.RELATION>;
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<FieldMetadataType.RELATION>;
});
});
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<FieldMetadataType.RELATION>;
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<FieldMetadataType.RELATION>;
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<FieldMetadataType.RELATION>;
});
return [...staticRelations, ...relationsFromDynamicRelations];
}
}

View File

@ -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<string, ObjectMetadataEntity>,
storage: WorkspaceSyncStorage,
): Promise<void> {
// 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<string, ObjectMetadataEntity>,
storage: WorkspaceSyncStorage,
): Promise<void> {
// 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<FieldMetadataType.RELATION>[];
const customFieldMetadataRelationCollection =
this.standardFieldRelationFactory.computeRelationFieldsForCustomObject(
customObjectMetadata,
originalObjectMetadataMapByName,
);
const fieldComparatorResults =
this.workspaceFieldRelationComparator.compare(
customObjectMetadata.fields as FieldMetadataEntity<FieldMetadataType.RELATION>[],
originalFieldRelationMetadataCollection,
customFieldMetadataRelationCollection,
);
@ -233,9 +234,6 @@ export class WorkspaceSyncFieldMetadataRelationService {
await objectMetadataRepository.find({
where: {
workspaceId: context.workspaceId,
fields: {
type: FieldMetadataType.RELATION,
},
},
relations: ['dataSource', 'fields'],
});