feat: new relation sync-metadata, twenty-orm, create/update (#10217)

Fix
https://github.com/twentyhq/core-team-issues/issues/330#issue-2827026606
and
https://github.com/twentyhq/core-team-issues/issues/327#issue-2827001814

What this PR does when `isNewRelationEnabled` is set to `true`:
- [x] Drop the creation of the  foreign key as a `FieldMetadata`
- [x] Stop creating `RelationMetadata`
- [x] Properly fill `FieldMetadata` of type `RELATION` during the sync
command
- [x] Use new relation settings in TwentyORM
- [x] Properly create `FieldMetadata` relations when we create a new
object
- [x] Handle `database:reset` with new relations

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
Jérémy M
2025-04-22 19:01:39 +02:00
committed by GitHub
parent de1489aabb
commit cc29c25176
160 changed files with 3247 additions and 711 deletions

View File

@ -29,6 +29,7 @@ export const mockPersonObjectMetadata = (
fields: [],
indexMetadatas: [],
fieldsById: {},
fieldsByJoinColumnName: {},
fieldsByName: {
name: {
id: '',

View File

@ -14,15 +14,19 @@ import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.pars
export class GraphqlQueryFilterConditionParser {
private fieldMetadataMapByName: FieldMetadataMap;
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
private queryFilterFieldParser: GraphqlQueryFilterFieldParser;
constructor(
fieldMetadataMapByName: FieldMetadataMap,
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
featureFlagsMap: FeatureFlagMap,
) {
this.fieldMetadataMapByName = fieldMetadataMapByName;
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser(
this.fieldMetadataMapByName,
this.fieldMetadataMapByJoinColumnName,
featureFlagsMap,
);
}

View File

@ -1,5 +1,5 @@
import { WhereExpressionBuilder } from 'typeorm';
import { capitalize } from 'twenty-shared/utils';
import { WhereExpressionBuilder } from 'typeorm';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
@ -18,13 +18,16 @@ const ARRAY_OPERATORS = ['in', 'contains', 'notContains'];
export class GraphqlQueryFilterFieldParser {
private fieldMetadataMapByName: FieldMetadataMap;
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
private featureFlagsMap: FeatureFlagMap;
constructor(
fieldMetadataMapByName: FieldMetadataMap,
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
featureFlagsMap: FeatureFlagMap,
) {
this.fieldMetadataMapByName = fieldMetadataMapByName;
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
this.featureFlagsMap = featureFlagsMap;
}
@ -35,7 +38,9 @@ export class GraphqlQueryFilterFieldParser {
filterValue: any,
isFirst = false,
): void {
const fieldMetadata = this.fieldMetadataMapByName[`${key}`];
const fieldMetadata =
this.fieldMetadataMapByName[`${key}`] ||
this.fieldMetadataMapByJoinColumnName[`${key}`];
if (!fieldMetadata) {
throw new Error(`Field metadata not found for field: ${key}`);

View File

@ -24,6 +24,7 @@ import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modu
export class GraphqlQueryParser {
private fieldMetadataMapByName: FieldMetadataMap;
private fieldMetadataMapByJoinColumnName: FieldMetadataMap;
private objectMetadataMaps: ObjectMetadataMaps;
private filterConditionParser: GraphqlQueryFilterConditionParser;
private orderFieldParser: GraphqlQueryOrderFieldParser;
@ -31,14 +32,17 @@ export class GraphqlQueryParser {
constructor(
fieldMetadataMapByName: FieldMetadataMap,
fieldMetadataMapByJoinColumnName: FieldMetadataMap,
objectMetadataMaps: ObjectMetadataMaps,
featureFlagsMap: FeatureFlagMap,
) {
this.objectMetadataMaps = objectMetadataMaps;
this.fieldMetadataMapByName = fieldMetadataMapByName;
this.fieldMetadataMapByJoinColumnName = fieldMetadataMapByJoinColumnName;
this.featureFlagsMap = featureFlagsMap;
this.filterConditionParser = new GraphqlQueryFilterConditionParser(
this.fieldMetadataMapByName,
this.fieldMetadataMapByJoinColumnName,
featureFlagsMap,
);
this.orderFieldParser = new GraphqlQueryOrderFieldParser(

View File

@ -23,7 +23,7 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
@Injectable()
export class ProcessNestedRelationsV2Helper {
@ -103,7 +103,10 @@ export class ProcessNestedRelationsV2Helper {
parentObjectMetadataItem.fieldsByName[sourceFieldName];
if (
!isFieldMetadataOfType(sourceFieldMetadata, FieldMetadataType.RELATION)
!isFieldMetadataInterfaceOfType(
sourceFieldMetadata,
FieldMetadataType.RELATION,
)
) {
// TODO: Maybe we should throw an error here ?
return;
@ -149,7 +152,7 @@ export class ProcessNestedRelationsV2Helper {
? `"${targetRelationName}Id"`
: 'id',
ids: relationIds,
limit,
limit: limit * parentObjectRecords.length,
objectMetadataMaps,
targetObjectMetadata,
aggregate,
@ -313,6 +316,7 @@ export class ProcessNestedRelationsV2Helper {
result,
targetObjectMetadata,
objectMetadataMaps,
true,
);
return { relationResults, relationAggregatedFieldsResult };

View File

@ -453,6 +453,7 @@ export class ProcessNestedRelationsHelper {
result,
referenceObjectMetadata,
objectMetadataMaps,
false,
);
return { relationResults, relationAggregatedFieldsResult };

View File

@ -130,6 +130,7 @@ export abstract class GraphqlQueryBaseResolverService<
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps.fieldsByName,
objectMetadataItemWithFieldMaps.fieldsByJoinColumnName,
options.objectMetadataMaps,
featureFlagsMap,
);

View File

@ -44,6 +44,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
objectRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap,
);
this.apiEventEmitterService.emitCreateEvents(
@ -301,6 +302,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
objectRecords: InsertResult,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
featureFlagsMap: Record<FeatureFlagKey, boolean>,
): Promise<ObjectRecord[]> {
const queryBuilder = executionArgs.repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
@ -317,6 +319,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
nonFormattedUpsertedRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
}

View File

@ -53,6 +53,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
nonFormattedUpsertedRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
this.apiEventEmitterService.emitCreateEvents(

View File

@ -54,6 +54,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
this.apiEventEmitterService.emitDeletedEvents(

View File

@ -47,6 +47,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
this.apiEventEmitterService.emitDeletedEvents(

View File

@ -52,6 +52,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
this.apiEventEmitterService.emitDestroyEvents(

View File

@ -52,6 +52,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
nonFormattedDeletedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
this.apiEventEmitterService.emitDestroyEvents(

View File

@ -64,6 +64,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldsMaps?.fieldsByName,
objectMetadataItemWithFieldsMaps?.fieldsByJoinColumnName,
objectMetadataMaps,
featureFlagsMap,
);
@ -85,6 +86,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
nonFormattedObjectRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
} else if (executionArgs.args.data && !isEmpty(executionArgs.args.data)) {
objectRecords = formatData(
@ -131,6 +133,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
nonFormattedDuplicates,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
return typeORMObjectRecordsParser.createConnection({

View File

@ -130,6 +130,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
nonFormattedObjectRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
const { hasNextPage, hasPreviousPage } = getPaginationInfo(

View File

@ -59,6 +59,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
nonFormattedObjectRecord,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
if (!objectRecord) {

View File

@ -54,6 +54,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
nonFormattedRestoredObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
this.apiEventEmitterService.emitRestoreEvents(

View File

@ -47,6 +47,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
nonFormattedRestoredObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
this.apiEventEmitterService.emitRestoreEvents(

View File

@ -55,6 +55,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
existingRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
if (isEmpty(formattedExistingRecords)) {
@ -89,6 +90,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
nonFormattedUpdatedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
this.apiEventEmitterService.emitUpdateEvents(

View File

@ -55,6 +55,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
existingRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
if (isEmpty(formattedExistingRecords)) {
@ -74,6 +75,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
nonFormattedUpdatedObjectRecords.raw,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
);
this.apiEventEmitterService.emitUpdateEvents(