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

@ -1,14 +1,13 @@
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
@ -67,7 +66,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.noteTargets,
label: msg`Notes`,
type: RelationMetadataType.ONE_TO_MANY,
type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => {
const label = objectMetadata.labelSingular;
@ -83,7 +82,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.taskTargets,
label: msg`Tasks`,
type: RelationMetadataType.ONE_TO_MANY,
type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => {
const label = objectMetadata.labelSingular;
@ -99,7 +98,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.favorites,
label: msg`Favorites`,
type: RelationMetadataType.ONE_TO_MANY,
type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => {
const label = objectMetadata.labelSingular;
@ -116,7 +115,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.attachments,
label: msg`Attachments`,
type: RelationMetadataType.ONE_TO_MANY,
type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => {
const label = objectMetadata.labelSingular;
@ -132,7 +131,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities,
label: msg`Timeline Activities`,
type: RelationMetadataType.ONE_TO_MANY,
type: RelationType.ONE_TO_MANY,
description: (objectMetadata) => {
const label = objectMetadata.labelSingular;

View File

@ -1,16 +1,14 @@
import { ObjectType } from 'typeorm';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';
interface WorkspaceBaseDynamicRelationOptions<TClass> {
type: RelationMetadataType;
type: RelationType;
argsFactory: WorkspaceDynamicRelationMetadataArgsFactory;
inverseSideTarget: () => ObjectType<TClass>;
inverseSideFieldKey?: keyof TClass;

View File

@ -28,6 +28,15 @@ export function WorkspaceFieldIndex(
...additionalDefaultColumnsForIndex,
];
// TODO: Remove this when we are handling properly indexes for new relation metadata
if (
process.env.SYNC_METADATA_INDEX_ENABLED === 'false' ||
process.env.SYNC_METADATA_INDEX_ENABLED === '' ||
process.env.SYNC_METADATA_INDEX_ENABLED === undefined
) {
return;
}
metadataArgsStorage.addIndexes({
name: `IDX_${generateDeterministicIndexName([
convertClassNameToObjectMetadataName(target.constructor.name),

View File

@ -11,7 +11,11 @@ import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args
import { TypedReflect } from 'src/utils/typed-reflect';
export interface WorkspaceFieldOptions<
T extends FieldMetadataType = FieldMetadataType,
T extends FieldMetadataType = Exclude<
FieldMetadataType,
// Use @WorkspaceRelation or @WorkspaceDynamicRelation for relation fields
FieldMetadataType.RELATION
>,
> {
standardId: string;
type: T;

View File

@ -18,6 +18,15 @@ export function WorkspaceIndex(
throw new Error('Class level WorkspaceIndex should be used with columns');
}
// TODO: Remove this when we are handling properly indexes for new relation metadata
if (
process.env.SYNC_METADATA_INDEX_ENABLED === 'false' ||
process.env.SYNC_METADATA_INDEX_ENABLED === '' ||
process.env.SYNC_METADATA_INDEX_ENABLED === undefined
) {
return (_target: any) => {};
}
return (target: any) => {
const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args',

View File

@ -17,17 +17,20 @@ export function WorkspaceIsUnique(): PropertyDecorator {
const columns = [propertyKey.toString()];
metadataArgsStorage.addIndexes({
name: `IDX_UNIQUE_${generateDeterministicIndexName([
convertClassNameToObjectMetadataName(target.constructor.name),
...columns,
])}`,
columns,
target: target.constructor,
gate,
isUnique: true,
whereClause: null,
});
// TODO: Remove this when we are handling properly indexes for new relation metadata
if (process.env.SYNC_METADATA_INDEX_ENABLED === 'true') {
metadataArgsStorage.addIndexes({
name: `IDX_UNIQUE_${generateDeterministicIndexName([
convertClassNameToObjectMetadataName(target.constructor.name),
...columns,
])}`,
columns,
target: target.constructor,
gate,
isUnique: true,
whereClause: null,
});
}
return TypedReflect.defineMetadata(
'workspace:is-unique-metadata-args',

View File

@ -1,15 +1,14 @@
import { MessageDescriptor } from '@lingui/core';
import { ObjectType } from 'typeorm';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';
interface WorkspaceRelationOptions<TClass> {
interface WorkspaceRelationBaseOptions<TClass> {
standardId: string;
label:
| MessageDescriptor
@ -18,12 +17,26 @@ interface WorkspaceRelationOptions<TClass> {
| MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
icon?: string;
type: RelationMetadataType;
inverseSideTarget: () => ObjectType<TClass>;
inverseSideFieldKey?: keyof TClass;
onDelete?: RelationOnDeleteAction;
}
interface WorkspaceOtherRelationOptions<TClass>
extends WorkspaceRelationBaseOptions<TClass> {
type: RelationType.ONE_TO_MANY | RelationType.ONE_TO_ONE;
}
interface WorkspaceManyToOneRelationOptions<TClass extends object>
extends WorkspaceRelationBaseOptions<TClass> {
type: RelationType.MANY_TO_ONE;
inverseSideFieldKey: keyof TClass;
}
type WorkspaceRelationOptions<TClass extends object> =
| WorkspaceOtherRelationOptions<TClass>
| WorkspaceManyToOneRelationOptions<TClass>;
export function WorkspaceRelation<TClass extends object>(
options: WorkspaceRelationOptions<TClass>,
): PropertyDecorator {

View File

@ -14,4 +14,5 @@ export enum TwentyORMExceptionCode {
ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND',
FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND',
USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND = 'USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND',
MALFORMED_METADATA = 'MALFORMED_METADATA',
}

View File

@ -1,9 +1,11 @@
import { Injectable } from '@nestjs/common';
import { ColumnType, EntitySchemaColumnOptions } from 'typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ColumnType, EntitySchemaColumnOptions } from 'typeorm';
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 { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
@ -12,7 +14,11 @@ import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metad
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
type EntitySchemaColumnMap = {
[key: string]: EntitySchemaColumnOptions;
@ -20,7 +26,10 @@ type EntitySchemaColumnMap = {
@Injectable()
export class EntitySchemaColumnFactory {
create(fieldMetadataMapByName: FieldMetadataMap): EntitySchemaColumnMap {
create(
fieldMetadataMapByName: FieldMetadataMap,
isNewRelationEnabled: boolean,
): EntitySchemaColumnMap {
let entitySchemaColumnMap: EntitySchemaColumnMap = {};
const fieldMetadataCollection = Object.values(fieldMetadataMapByName);
@ -28,33 +37,63 @@ export class EntitySchemaColumnFactory {
for (const fieldMetadata of fieldMetadataCollection) {
const key = fieldMetadata.name;
if (isRelationFieldMetadataType(fieldMetadata.type)) {
const relationMetadata =
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata;
if (!relationMetadata) {
throw new Error(
`Relation metadata is missing for field ${fieldMetadata.name}`,
);
}
const joinColumnKey = fieldMetadata.name + 'Id';
const joinColumn = fieldMetadataCollection.find(
(field) => field.name === joinColumnKey,
if (
isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
)
? joinColumnKey
: null;
) {
if (!isNewRelationEnabled) {
const relationMetadata =
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata;
if (joinColumn) {
entitySchemaColumnMap[joinColumn] = {
name: joinColumn,
if (!relationMetadata) {
throw new Error(
`Relation metadata is missing for field ${fieldMetadata.name}`,
);
}
const joinColumnKey = fieldMetadata.name + 'Id';
const joinColumn = fieldMetadataCollection.find(
(field) => field.name === joinColumnKey,
)
? joinColumnKey
: null;
if (joinColumn) {
entitySchemaColumnMap[joinColumn] = {
name: joinColumn,
type: 'uuid',
nullable: fieldMetadata.isNullable,
};
}
continue;
} else {
const isManyToOneRelation =
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE;
const joinColumnName = fieldMetadata.settings?.joinColumnName;
if (!isManyToOneRelation) {
continue;
}
if (!isDefined(joinColumnName)) {
throw new TwentyORMException(
`Field ${fieldMetadata.id} of type ${fieldMetadata.type} is a many to one relation but does not have a join column name`,
TwentyORMExceptionCode.MALFORMED_METADATA,
);
}
entitySchemaColumnMap[joinColumnName] = {
name: joinColumnName,
type: 'uuid',
nullable: fieldMetadata.isNullable,
};
}
continue;
continue;
}
}
if (isCompositeFieldMetadataType(fieldMetadata.type)) {

View File

@ -1,11 +1,13 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { EntitySchemaRelationOptions } from 'typeorm';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
type EntitySchemaRelationMap = {
[key: string]: EntitySchemaRelationOptions;
@ -18,37 +20,64 @@ export class EntitySchemaRelationFactory {
async create(
fieldMetadataMapByName: FieldMetadataMap,
objectMetadataMaps: ObjectMetadataMaps,
isNewRelationEnabled: boolean,
): Promise<EntitySchemaRelationMap> {
const entitySchemaRelationMap: EntitySchemaRelationMap = {};
const fieldMetadataCollection = Object.values(fieldMetadataMapByName);
for (const fieldMetadata of fieldMetadataCollection) {
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
if (
!isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
)
) {
continue;
}
const relationMetadata =
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
if (!isNewRelationEnabled) {
const relationMetadata =
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata;
if (!relationMetadata) {
throw new Error(
`Relation metadata is missing for field ${fieldMetadata.name}`,
if (!relationMetadata) {
throw new Error(
`Relation metadata is missing for field ${fieldMetadata.name}`,
);
}
const relationDetails = await determineRelationDetails(
fieldMetadata,
relationMetadata,
objectMetadataMaps,
);
entitySchemaRelationMap[fieldMetadata.name] = {
type: relationDetails.relationType,
target: relationDetails.target,
inverseSide: relationDetails.inverseSide,
joinColumn: relationDetails.joinColumn,
} satisfies EntitySchemaRelationOptions;
} else {
if (!fieldMetadata.settings) {
throw new Error(
`Field metadata settings are missing for field ${fieldMetadata.name}`,
);
}
const schemaRelationDetails = await determineSchemaRelationDetails(
fieldMetadata,
objectMetadataMaps,
);
entitySchemaRelationMap[fieldMetadata.name] = {
type: schemaRelationDetails.relationType,
target: schemaRelationDetails.target,
inverseSide: schemaRelationDetails.inverseSide,
joinColumn: schemaRelationDetails.joinColumn,
} satisfies EntitySchemaRelationOptions;
}
const relationDetails = await determineRelationDetails(
fieldMetadata,
relationMetadata,
objectMetadataMaps,
);
entitySchemaRelationMap[fieldMetadata.name] = {
type: relationDetails.relationType,
target: relationDetails.target,
inverseSide: relationDetails.inverseSide,
joinColumn: relationDetails.joinColumn,
} satisfies EntitySchemaRelationOptions;
}
return entitySchemaRelationMap;

View File

@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common';
import { EntitySchema } from 'typeorm';
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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory';
@ -14,6 +16,7 @@ export class EntitySchemaFactory {
constructor(
private readonly entitySchemaColumnFactory: EntitySchemaColumnFactory,
private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory,
private readonly featureFlagService: FeatureFlagService,
) {}
async create(
@ -22,13 +25,20 @@ export class EntitySchemaFactory {
objectMetadata: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<EntitySchema> {
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
const columns = this.entitySchemaColumnFactory.create(
objectMetadata.fieldsByName,
isNewRelationEnabled,
);
const relations = await this.entitySchemaRelationFactory.create(
objectMetadata.fieldsByName,
objectMetadataMaps,
isNewRelationEnabled,
);
const entitySchema = new EntitySchema({

View File

@ -147,6 +147,7 @@ export class WorkspaceDatasourceFactory {
{
workspaceId,
objectMetadataMaps: cachedObjectMetadataMaps,
featureFlagsMap: cachedFeatureFlagMap,
},
{
url:

View File

@ -1,12 +1,10 @@
import { ObjectType } from 'typeorm';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
export type WorkspaceDynamicRelationMetadataArgsFactory = (
oppositeObjectMetadata: ObjectMetadataEntity,
@ -57,7 +55,7 @@ export interface WorkspaceDynamicRelationMetadataArgs {
/**
* Relation type.
*/
readonly type: RelationMetadataType;
readonly type: RelationType;
/**
* Relation inverse side target.

View File

@ -1,6 +1,8 @@
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 interface WorkspaceInternalContext {
workspaceId: string;
objectMetadataMaps: ObjectMetadataMaps;
featureFlagsMap: Record<FeatureFlagKey, boolean>;
}

View File

@ -1,12 +1,10 @@
import { ObjectType } from 'typeorm';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
export interface WorkspaceRelationMetadataArgs {
/**
@ -33,7 +31,7 @@ export interface WorkspaceRelationMetadataArgs {
/**
* Relation type.
*/
readonly type: RelationMetadataType;
readonly type: RelationType;
/**
* Relation description.

View File

@ -715,7 +715,14 @@ export class WorkspaceRepository<
objectMetadata ??= await this.getObjectMetadataFromTarget();
const objectMetadataMaps = this.internalContext.objectMetadataMaps;
const isNewRelationEnabled =
this.internalContext.featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled];
return formatResult(data, objectMetadata, objectMetadataMaps) as T;
return formatResult(
data,
objectMetadata,
objectMetadataMaps,
isNewRelationEnabled,
) as T;
}
}

View File

@ -0,0 +1,14 @@
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
export const converRelationTypeToTypeORMRelationType = (type: RelationType) => {
switch (type) {
case RelationType.ONE_TO_MANY:
return 'one-to-many';
case RelationType.MANY_TO_ONE:
return 'many-to-one';
case RelationType.ONE_TO_ONE:
return 'one-to-one';
default:
throw new Error(`Invalid relation type: ${type}`);
}
};

View File

@ -0,0 +1,62 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'typeorm/metadata/types/RelationTypes';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { converRelationTypeToTypeORMRelationType } from 'src/engine/twenty-orm/utils/convert-relation-type-to-typeorm-relation-type.util';
interface RelationDetails {
relationType: RelationType;
target: string;
inverseSide: string;
joinColumn: { name: string } | undefined;
}
export async function determineSchemaRelationDetails(
fieldMetadata: FieldMetadataInterface<FieldMetadataType.RELATION>,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<RelationDetails> {
if (!fieldMetadata.settings) {
throw new Error('Field metadata settings are missing');
}
const relationType = converRelationTypeToTypeORMRelationType(
fieldMetadata.settings.relationType,
);
if (!fieldMetadata.relationTargetObjectMetadataId) {
throw new Error('Relation target object metadata ID is missing');
}
const sourceObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.objectMetadataId];
const targetObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
if (!sourceObjectMetadata || !targetObjectMetadata) {
throw new Error('Object metadata not found');
}
if (!fieldMetadata.relationTargetFieldMetadataId) {
throw new Error('Relation target field metadata ID is missing');
}
const targetFieldMetadata =
targetObjectMetadata.fieldsById[
fieldMetadata.relationTargetFieldMetadataId
];
if (!targetFieldMetadata) {
throw new Error('Target field metadata not found');
}
return {
relationType,
target: targetObjectMetadata.nameSingular,
inverseSide: targetFieldMetadata.name,
joinColumn: fieldMetadata.settings.joinColumnName
? { name: fieldMetadata.settings.joinColumnName }
: undefined,
};
}

View File

@ -7,6 +7,7 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
export function formatData<T>(
data: T,
@ -23,9 +24,28 @@ export function formatData<T>(
}
const newData: Record<string, any> = {};
const fieldMetadataByJoinColumnName =
objectMetadataItemWithFieldMaps.fields.reduce((acc, fieldMetadata) => {
if (
isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
)
) {
const joinColumnName = fieldMetadata.settings?.joinColumnName;
if (joinColumnName) {
acc.set(joinColumnName, fieldMetadata);
}
}
return acc;
}, new Map<string, FieldMetadataInterface>());
for (const [key, value] of Object.entries(data)) {
const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsByName[key];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsByName[key] ||
fieldMetadataByJoinColumnName.get(key);
if (!fieldMetadata) {
throw new Error(

View File

@ -13,7 +13,7 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { isDate } from 'src/utils/date/isDate';
import { isValidDate } from 'src/utils/date/isValidDate';
@ -21,6 +21,7 @@ export function formatResult<T>(
data: any,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
isNewRelationEnabled: boolean,
): T {
if (!data) {
return data;
@ -28,7 +29,12 @@ export function formatResult<T>(
if (Array.isArray(data)) {
return data.map((item) =>
formatResult(item, objectMetadataItemWithFieldMaps, objectMetadataMaps),
formatResult(
item,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
isNewRelationEnabled,
),
) as T;
}
@ -44,38 +50,57 @@ export function formatResult<T>(
objectMetadataItemWithFieldMaps,
);
const relationMetadataMap = new Map(
Object.values(objectMetadataItemWithFieldMaps.fieldsById)
.filter(({ type }) => isRelationFieldMetadataType(type))
.map((fieldMetadata) => [
fieldMetadata.name,
{
relationMetadata:
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata,
relationType: computeRelationType(
fieldMetadata,
fieldMetadata.fromRelationMetadata ??
(fieldMetadata.toRelationMetadata as RelationMetadataEntity),
),
},
]),
);
const relationMetadataMap: Map<
string,
{
relationMetadata: RelationMetadataEntity | undefined;
relationType: string;
}
> = isNewRelationEnabled
? new Map()
: new Map(
Object.values(objectMetadataItemWithFieldMaps.fieldsById)
.filter((fieldMetadata) =>
isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
),
)
.map((fieldMetadata) => [
fieldMetadata.name,
{
relationMetadata:
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata,
relationType: computeRelationType(
fieldMetadata,
fieldMetadata.fromRelationMetadata ??
(fieldMetadata.toRelationMetadata as RelationMetadataEntity),
),
},
]),
);
const newData: object = {};
const objectMetadaItemFieldsByName =
objectMetadataMaps.byId[objectMetadataItemWithFieldMaps.id]?.fieldsByName;
for (const [key, value] of Object.entries(data)) {
const compositePropertyArgs = compositeFieldMetadataMap.get(key);
const { relationMetadata, relationType } =
relationMetadataMap.get(key) ?? {};
const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsById[key];
const isRelation = fieldMetadata
? isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
)
: false;
if (!compositePropertyArgs && !relationMetadata) {
if (!compositePropertyArgs && !isRelation) {
if (isPlainObject(value)) {
newData[key] = formatResult(
value,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
isNewRelationEnabled,
);
} else if (objectMetadaItemFieldsByName[key]) {
newData[key] = formatFieldMetadataValue(
@ -89,31 +114,63 @@ export function formatResult<T>(
continue;
}
if (relationMetadata) {
const toObjectMetadata =
objectMetadataMaps.byId[relationMetadata.toObjectMetadataId];
if (!isNewRelationEnabled) {
const { relationMetadata, relationType } =
relationMetadataMap.get(key) ?? {};
const fromObjectMetadata =
objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId];
if (relationMetadata) {
const toObjectMetadata =
objectMetadataMaps.byId[relationMetadata.toObjectMetadataId];
if (!toObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`,
const fromObjectMetadata =
objectMetadataMaps.byId[relationMetadata.fromObjectMetadataId];
if (!toObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${relationMetadata.toObjectMetadataId}" is missing`,
);
}
if (!fromObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`,
);
}
newData[key] = formatResult(
value,
relationType === 'one-to-many'
? toObjectMetadata
: fromObjectMetadata,
objectMetadataMaps,
isNewRelationEnabled,
);
continue;
}
} else {
if (isRelation) {
if (!fieldMetadata.relationTargetObjectMetadataId) {
throw new Error(
`Relation target object metadata ID is missing for field "${key}"`,
);
}
const targetObjectMetadata =
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
if (!targetObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${fieldMetadata.relationTargetObjectMetadataId}" is missing`,
);
}
newData[key] = formatResult(
value,
targetObjectMetadata,
objectMetadataMaps,
isNewRelationEnabled,
);
}
if (!fromObjectMetadata) {
throw new Error(
`Object metadata for object metadataId "${relationMetadata.fromObjectMetadataId}" is missing`,
);
}
newData[key] = formatResult(
value,
relationType === 'one-to-many' ? toObjectMetadata : fromObjectMetadata,
objectMetadataMaps,
);
continue;
}
if (!compositePropertyArgs) {

View File

@ -1,7 +1,7 @@
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface';
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
export const getJoinColumn = (
@ -9,10 +9,7 @@ export const getJoinColumn = (
relationMetadataArgs: WorkspaceRelationMetadataArgs,
opposite = false,
): string | null => {
if (
relationMetadataArgs.type === RelationMetadataType.ONE_TO_MANY ||
relationMetadataArgs.type === RelationMetadataType.MANY_TO_MANY
) {
if (relationMetadataArgs.type === RelationType.ONE_TO_MANY) {
return null;
}
@ -41,7 +38,7 @@ export const getJoinColumn = (
// If we're in a ONE_TO_ONE relation and there are no join columns, we need to find the join column on the inverse side
if (
relationMetadataArgs.type === RelationMetadataType.ONE_TO_ONE &&
relationMetadataArgs.type === RelationType.ONE_TO_ONE &&
filteredJoinColumnsMetadataArgsCollection.length === 0 &&
!opposite
) {