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:
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -147,6 +147,7 @@ export class WorkspaceDatasourceFactory {
|
||||
{
|
||||
workspaceId,
|
||||
objectMetadataMaps: cachedObjectMetadataMaps,
|
||||
featureFlagsMap: cachedFeatureFlagMap,
|
||||
},
|
||||
{
|
||||
url:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user