fieldmetadatatype + featurelfag creation (#13021)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Guillim
2025-07-08 12:23:28 +02:00
committed by GitHub
parent 56607c0449
commit a5deddaffd
47 changed files with 1447 additions and 793 deletions

View File

@ -0,0 +1,138 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import omit from 'lodash.omit';
import { FieldMetadataType } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service';
import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util';
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 { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
@Injectable()
export class FieldMetadataMorphRelationService {
constructor(
private readonly fieldMetadataRelationService: FieldMetadataRelationService,
) {}
async createMorphRelationFieldMetadataItems({
fieldMetadataForCreate,
morphRelationsCreationPayload,
objectMetadata,
fieldMetadataRepository,
objectMetadataMaps,
}: {
fieldMetadataForCreate: CreateFieldInput;
morphRelationsCreationPayload: CreateFieldInput['morphRelationsCreationPayload'];
objectMetadata: ObjectMetadataItemWithFieldMaps;
fieldMetadataRepository: Repository<FieldMetadataEntity>;
objectMetadataMaps: ObjectMetadataMaps;
}): Promise<FieldMetadataEntity[]> {
if (
!isDefined(morphRelationsCreationPayload) ||
!Array.isArray(morphRelationsCreationPayload)
) {
throw new FieldMetadataException(
'Morph relations creation payload is not defined',
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
);
}
if (morphRelationsCreationPayload.length < 1) {
throw new FieldMetadataException(
'Morph relations creation payload must not be empty',
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
);
}
const fieldsCreated: FieldMetadataEntity[] = [];
for (const relation of morphRelationsCreationPayload) {
const relationFieldMetadataForCreate =
await this.fieldMetadataRelationService.addCustomRelationFieldMetadataForCreation(
{
fieldMetadataInput: fieldMetadataForCreate,
relationCreationPayload: relation,
objectMetadata,
},
);
await this.fieldMetadataRelationService.validateFieldMetadataRelationSpecifics(
{
fieldMetadataInput: relationFieldMetadataForCreate,
fieldMetadataType: relationFieldMetadataForCreate.type,
objectMetadataMaps,
},
);
const createdFieldMetadataItem = await fieldMetadataRepository.save(
omit(relationFieldMetadataForCreate, 'id'),
);
const targetFieldMetadataName = computeMetadataNameFromLabel(
relation.targetFieldLabel,
);
const targetFieldMetadataToCreate = prepareCustomFieldMetadataForCreation(
{
objectMetadataId: relation.targetObjectMetadataId,
type: FieldMetadataType.RELATION,
name: targetFieldMetadataName,
label: relation.targetFieldLabel,
icon: relation.targetFieldIcon,
workspaceId: fieldMetadataForCreate.workspaceId,
settings: fieldMetadataForCreate.settings,
},
);
const targetFieldMetadataToCreateWithRelation =
await this.fieldMetadataRelationService.addCustomRelationFieldMetadataForCreation(
{
fieldMetadataInput: targetFieldMetadataToCreate,
relationCreationPayload: {
targetObjectMetadataId: objectMetadata.id,
targetFieldLabel: fieldMetadataForCreate.label,
targetFieldIcon: fieldMetadataForCreate.icon ?? 'Icon123',
type:
relation.type === RelationType.ONE_TO_MANY
? RelationType.MANY_TO_ONE
: RelationType.ONE_TO_MANY,
},
objectMetadata,
},
);
// todo better type
const targetFieldMetadataToCreateWithRelationWithId = {
id: v4(),
...targetFieldMetadataToCreateWithRelation,
};
const targetFieldMetadata = await fieldMetadataRepository.save({
...targetFieldMetadataToCreateWithRelationWithId,
relationTargetFieldMetadataId: createdFieldMetadataItem.id,
});
const createdFieldMetadataItemUpdated =
await fieldMetadataRepository.save({
...createdFieldMetadataItem,
relationTargetFieldMetadataId: targetFieldMetadata.id,
});
fieldsCreated.push(createdFieldMetadataItemUpdated, targetFieldMetadata);
}
return fieldsCreated;
}
}

View File

@ -1,19 +1,23 @@
import { Injectable } from '@nestjs/common';
import isEmpty from 'lodash.isempty';
import { MAX_OPTIONS_TO_DISPLAY } from 'twenty-shared/constants';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { settings } from 'src/engine/constants/settings';
import {
FieldMetadataComplexOption,
FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { isSelectFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util';
import { SelectOrMultiSelectFieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
@ -310,4 +314,74 @@ export class FieldMetadataRelatedRecordsService {
private getMaxPosition(viewGroups: ViewGroupWorkspaceEntity[]): number {
return viewGroups.reduce((max, group) => Math.max(max, group.position), 0);
}
async createViewAndViewFields(
createdFieldMetadatas: FieldMetadataEntity[],
workspaceId: string,
) {
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
await workspaceDataSource.transaction(
async (workspaceEntityManager: WorkspaceEntityManager) => {
const viewsRepository = workspaceEntityManager.getRepository('view', {
shouldBypassPermissionChecks: true,
});
const viewFieldsRepository = workspaceEntityManager.getRepository(
'viewField',
{
shouldBypassPermissionChecks: true,
},
);
for (const createdFieldMetadata of createdFieldMetadatas) {
const views = await viewsRepository.find({
where: {
objectMetadataId: createdFieldMetadata.objectMetadataId,
},
});
if (!isEmpty(views)) {
const view = views[0];
const existingViewFields = await viewFieldsRepository.find({
where: {
viewId: view.id,
},
});
const isVisible =
existingViewFields.length < settings.maxVisibleViewFields;
const createdFieldIsAlreadyInView = existingViewFields.some(
(existingViewField) =>
existingViewField.fieldMetadataId === createdFieldMetadata.id,
);
if (!createdFieldIsAlreadyInView) {
const lastPosition = existingViewFields
.map((viewField) => viewField.position)
.reduce((acc, position) => {
if (position > acc) {
return position;
}
return acc;
}, -1);
await viewFieldsRepository.insert({
fieldMetadataId: createdFieldMetadata.id,
position: lastPosition + 1,
isVisible,
size: 180,
viewId: view.id,
});
}
}
}
},
);
}
}

View File

@ -0,0 +1,330 @@
import { Injectable, ValidationError } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { IsEnum, IsString, IsUUID, validateOrReject } from 'class-validator';
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
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 { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
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 { getObjectMetadataFromObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps';
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
export class RelationCreationPayloadValidation {
@IsUUID()
targetObjectMetadataId?: string;
@IsString()
targetFieldLabel: string;
@IsString()
targetFieldIcon: string;
@IsEnum(RelationType)
type: RelationType;
}
type ValidateFieldMetadataArgs<T extends UpdateFieldInput | CreateFieldInput> =
{
fieldMetadataType: FieldMetadataType;
fieldMetadataInput: T;
objectMetadata: ObjectMetadataItemWithFieldMaps;
existingFieldMetadata?: FieldMetadataInterface;
objectMetadataMaps: ObjectMetadataMaps;
};
@Injectable()
export class FieldMetadataRelationService {
constructor(
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {}
async createRelationFieldMetadataItems({
fieldMetadataInput,
objectMetadata,
fieldMetadataRepository,
}: {
fieldMetadataInput: CreateFieldInput;
objectMetadata: ObjectMetadataItemWithFieldMaps;
fieldMetadataRepository: Repository<FieldMetadataEntity>;
}): Promise<FieldMetadataEntity[]> {
const createdFieldMetadataItem =
await fieldMetadataRepository.save(fieldMetadataInput);
const relationCreationPayload = fieldMetadataInput.relationCreationPayload;
if (!isDefined(relationCreationPayload)) {
throw new FieldMetadataException(
'Relation creation payload is not defined',
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
);
}
const targetFieldMetadataName = computeMetadataNameFromLabel(
relationCreationPayload.targetFieldLabel,
);
const targetFieldMetadataToCreate = prepareCustomFieldMetadataForCreation({
objectMetadataId: relationCreationPayload.targetObjectMetadataId,
type: fieldMetadataInput.type,
name: targetFieldMetadataName,
label: relationCreationPayload.targetFieldLabel,
icon: relationCreationPayload.targetFieldIcon,
workspaceId: fieldMetadataInput.workspaceId,
});
const targetFieldMetadataToCreateWithRelation =
await this.addCustomRelationFieldMetadataForCreation({
fieldMetadataInput: targetFieldMetadataToCreate,
relationCreationPayload: {
targetObjectMetadataId: objectMetadata.id,
targetFieldLabel: fieldMetadataInput.label,
targetFieldIcon: fieldMetadataInput.icon ?? 'Icon123',
type:
relationCreationPayload.type === RelationType.ONE_TO_MANY
? RelationType.MANY_TO_ONE
: RelationType.ONE_TO_MANY,
},
objectMetadata,
});
// todo better type
const targetFieldMetadataToCreateWithRelationWithId = {
id: v4(),
...targetFieldMetadataToCreateWithRelation,
};
const targetFieldMetadata = await fieldMetadataRepository.save({
...targetFieldMetadataToCreateWithRelationWithId,
relationTargetFieldMetadataId: createdFieldMetadataItem.id,
});
const createdFieldMetadataItemUpdated = await fieldMetadataRepository.save({
...createdFieldMetadataItem,
relationTargetFieldMetadataId: targetFieldMetadata.id,
});
return [createdFieldMetadataItemUpdated, targetFieldMetadata];
}
async validateFieldMetadataRelationSpecifics<
T extends UpdateFieldInput | CreateFieldInput,
>({
fieldMetadataInput,
fieldMetadataType,
objectMetadataMaps,
}: Pick<
ValidateFieldMetadataArgs<T>,
'fieldMetadataInput' | 'fieldMetadataType' | 'objectMetadataMaps'
>): Promise<T> {
// TODO: clean typings, we should try to validate both update and create inputs in the same function
const isRelation =
fieldMetadataType === FieldMetadataType.RELATION ||
fieldMetadataType === FieldMetadataType.MORPH_RELATION;
if (
isRelation &&
isDefined(
(fieldMetadataInput as unknown as CreateFieldInput)
.relationCreationPayload,
)
) {
const relationCreationPayload = (
fieldMetadataInput as unknown as CreateFieldInput
).relationCreationPayload;
if (isDefined(relationCreationPayload)) {
await this.validateRelationCreationPayloadOrThrow(
relationCreationPayload,
);
const computedMetadataNameFromLabel = computeMetadataNameFromLabel(
relationCreationPayload.targetFieldLabel,
);
validateMetadataNameOrThrow(computedMetadataNameFromLabel);
const objectMetadataTarget =
objectMetadataMaps.byId[
relationCreationPayload.targetObjectMetadataId
];
validateFieldNameAvailabilityOrThrow(
computedMetadataNameFromLabel,
objectMetadataTarget,
);
}
}
return fieldMetadataInput;
}
private async validateRelationCreationPayloadOrThrow(
relationCreationPayload: RelationCreationPayloadValidation,
) {
try {
const relationCreationPayloadInstance = plainToInstance(
RelationCreationPayloadValidation,
relationCreationPayload,
);
await validateOrReject(relationCreationPayloadInstance);
} catch (error) {
const errorMessages = Array.isArray(error)
? error
.map((err: ValidationError) => Object.values(err.constraints ?? {}))
.flat()
.join(', ')
: error.message;
throw new FieldMetadataException(
`Relation creation payload is invalid: ${errorMessages}`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
}
}
async findCachedFieldMetadataRelation(
fieldMetadataItems: Array<
Pick<
FieldMetadataInterface,
| 'id'
| 'type'
| 'objectMetadataId'
| 'relationTargetFieldMetadataId'
| 'relationTargetObjectMetadataId'
>
>,
workspaceId: string,
): Promise<
Array<{
sourceObjectMetadata: ObjectMetadataEntity;
sourceFieldMetadata: FieldMetadataEntity;
targetObjectMetadata: ObjectMetadataEntity;
targetFieldMetadata: FieldMetadataEntity;
}>
> {
const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
workspaceId,
);
return fieldMetadataItems.map((fieldMetadataItem) => {
const {
id,
objectMetadataId,
relationTargetFieldMetadataId,
relationTargetObjectMetadataId,
} = fieldMetadataItem;
if (!relationTargetObjectMetadataId || !relationTargetFieldMetadataId) {
throw new FieldMetadataException(
`Relation target object metadata id or relation target field metadata id not found for field metadata ${id}`,
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
);
}
const sourceObjectMetadata = objectMetadataMaps.byId[objectMetadataId];
const targetObjectMetadata =
objectMetadataMaps.byId[relationTargetObjectMetadataId];
const sourceFieldMetadata = sourceObjectMetadata?.fieldsById[id];
const targetFieldMetadata =
targetObjectMetadata?.fieldsById[relationTargetFieldMetadataId];
if (
!sourceObjectMetadata ||
!targetObjectMetadata ||
!sourceFieldMetadata ||
!targetFieldMetadata
) {
throw new FieldMetadataException(
`Field relation metadata not found for field metadata ${id}`,
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
);
}
return {
sourceObjectMetadata:
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
sourceObjectMetadata,
) as ObjectMetadataEntity,
sourceFieldMetadata: sourceFieldMetadata as FieldMetadataEntity,
targetObjectMetadata:
getObjectMetadataFromObjectMetadataItemWithFieldMaps(
targetObjectMetadata,
) as ObjectMetadataEntity,
targetFieldMetadata: targetFieldMetadata as FieldMetadataEntity,
};
});
}
addCustomRelationFieldMetadataForCreation({
fieldMetadataInput,
relationCreationPayload,
objectMetadata,
}: {
fieldMetadataInput: CreateFieldInput;
relationCreationPayload: CreateFieldInput['relationCreationPayload'];
objectMetadata: ObjectMetadataItemWithFieldMaps;
}) {
const isRelation =
isFieldMetadataInterfaceOfType(
fieldMetadataInput,
FieldMetadataType.RELATION,
) ||
isFieldMetadataInterfaceOfType(
fieldMetadataInput,
FieldMetadataType.MORPH_RELATION,
);
const isManyToOne =
isRelation && relationCreationPayload?.type === RelationType.MANY_TO_ONE;
const isOneToMany =
isRelation && relationCreationPayload?.type === RelationType.ONE_TO_MANY;
const defaultIcon = 'IconRelationOneToMany';
const joinColumnName = `${fieldMetadataInput.name}${capitalize(objectMetadata.nameSingular)}Id`;
return {
...fieldMetadataInput,
icon: fieldMetadataInput.icon ?? defaultIcon,
relationCreationPayload,
relationTargetObjectMetadataId:
relationCreationPayload?.targetObjectMetadataId,
settings: {
...fieldMetadataInput.settings,
...(isOneToMany
? {
relationType: RelationType.ONE_TO_MANY,
}
: {}),
...(isManyToOne
? {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName,
}
: {}),
},
};
}
}

View File

@ -0,0 +1,191 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { ClassConstructor, plainToInstance } from 'class-transformer';
import {
IsEnum,
IsInt,
IsOptional,
Max,
Min,
ValidationError,
isDefined,
validateOrReject,
} from 'class-validator';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { FieldMetadataEnumValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-enum-validation.service';
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
type ValidateFieldMetadataArgs = {
fieldMetadataType: FieldMetadataType;
fieldMetadataInput: CreateFieldInput | UpdateFieldInput;
objectMetadata: ObjectMetadataItemWithFieldMaps;
existingFieldMetadata?: FieldMetadataInterface;
};
enum ValueType {
PERCENTAGE = 'percentage',
NUMBER = 'number',
SHORT_NUMBER = 'shortNumber',
}
class NumberSettingsValidation {
@IsOptional()
@IsInt()
@Min(0)
decimals?: number;
@IsOptional()
@IsEnum(ValueType)
type?: 'percentage' | 'number' | 'shortNumber';
}
class TextSettingsValidation {
@IsOptional()
@IsInt()
@Min(0)
@Max(100)
displayedMaxRows?: number;
}
@Injectable()
export class FieldMetadataValidationService {
constructor(
private readonly fieldMetadataEnumValidationService: FieldMetadataEnumValidationService,
) {}
async validateSettingsOrThrow<T extends FieldMetadataType>({
fieldType,
settings,
}: {
fieldType: FieldMetadataType;
settings: FieldMetadataSettings<T>;
}) {
switch (fieldType) {
case FieldMetadataType.NUMBER:
await this.validateSettings<FieldMetadataType.NUMBER>(
NumberSettingsValidation,
settings,
);
break;
case FieldMetadataType.TEXT:
await this.validateSettings<FieldMetadataType.TEXT>(
TextSettingsValidation,
settings,
);
break;
default:
break;
}
}
private async validateSettings<Type extends FieldMetadataType>(
validator: ClassConstructor<
Type extends FieldMetadataType.NUMBER
? NumberSettingsValidation
: Type extends FieldMetadataType.TEXT
? TextSettingsValidation
: never
>,
settings: FieldMetadataSettings<Type>,
) {
try {
const settingsInstance = plainToInstance(validator, settings);
await validateOrReject(settingsInstance);
} catch (error) {
const errorMessages = Array.isArray(error)
? error
.map((err: ValidationError) => Object.values(err.constraints ?? {}))
.flat()
.join(', ')
: error.message;
throw new FieldMetadataException(
`Value for settings is invalid: ${errorMessages}`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
}
}
async validateFieldMetadata({
fieldMetadataInput,
fieldMetadataType,
objectMetadata,
existingFieldMetadata,
}: ValidateFieldMetadataArgs): Promise<void> {
if (fieldMetadataInput.name) {
try {
validateMetadataNameOrThrow(fieldMetadataInput.name);
} catch (error) {
if (error instanceof InvalidMetadataException) {
throw new FieldMetadataException(
error.message,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
}
throw error;
}
try {
validateFieldNameAvailabilityOrThrow(
fieldMetadataInput.name,
objectMetadata,
);
} catch (error) {
if (error instanceof InvalidMetadataException) {
throw new FieldMetadataException(
`Name "${fieldMetadataInput.name}" is not available, check that it is not duplicating another field's name.`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: t`Name is not available, it may be duplicating another field's name.`,
},
);
}
throw error;
}
}
if (fieldMetadataInput.isNullable === false) {
if (!isDefined(fieldMetadataInput.defaultValue)) {
throw new FieldMetadataException(
'Default value is required for non nullable fields',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
}
}
if (isEnumFieldMetadataType(fieldMetadataType)) {
await this.fieldMetadataEnumValidationService.validateEnumFieldMetadataInput(
{
fieldMetadataInput,
fieldMetadataType,
existingFieldMetadata,
},
);
}
if (fieldMetadataInput.settings) {
await this.validateSettingsOrThrow({
fieldType: fieldMetadataType,
settings: fieldMetadataInput.settings,
});
}
}
}

View File

@ -0,0 +1,743 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { t } from '@lingui/core/macro';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { DataSource, FindOneOptions, In, Repository } 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 { 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 { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service';
import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service';
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service';
import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service';
import { assertDoesNotNullifyDefaultValueForNonNullableField } from 'src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util';
import { buildUpdatableStandardFieldInput } from 'src/engine/metadata-modules/field-metadata/utils/build-updatable-standard-field-input.util';
import { checkCanDeactivateFieldOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw';
import {
computeColumnName,
computeCompositeColumnName,
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { generateRatingOptions } from 'src/engine/metadata-modules/field-metadata/utils/generate-rating-optionts.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util';
import { prepareCustomFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util';
import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
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 { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnDrop,
WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { ViewService } from 'src/modules/view/services/view.service';
@Injectable()
export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntity> {
constructor(
@InjectDataSource('core')
private readonly coreDataSource: DataSource,
@InjectRepository(FieldMetadataEntity, 'core')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly fieldMetadataRelatedRecordsService: FieldMetadataRelatedRecordsService,
private readonly viewService: ViewService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly featureFlagService: FeatureFlagService,
private readonly fieldMetadataValidationService: FieldMetadataValidationService,
private readonly fieldMetadataMorphRelationService: FieldMetadataMorphRelationService,
private readonly fieldMetadataRelationService: FieldMetadataRelationService,
) {
super(fieldMetadataRepository);
}
override async createOne(
fieldMetadataInput: CreateFieldInput,
): Promise<FieldMetadataEntity> {
const [createdFieldMetadata] = await this.createMany([fieldMetadataInput]);
if (!isDefined(createdFieldMetadata)) {
throw new FieldMetadataException(
'Failed to create field metadata',
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
return createdFieldMetadata;
}
override async updateOne(
id: string,
fieldMetadataInput: UpdateFieldInput,
): Promise<FieldMetadataEntity> {
const { objectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
{ workspaceId: fieldMetadataInput.workspaceId },
);
let existingFieldMetadata: FieldMetadataInterface | undefined;
for (const objectMetadataItem of Object.values(objectMetadataMaps.byId)) {
const fieldMetadata = objectMetadataItem.fieldsById[id];
if (fieldMetadata) {
existingFieldMetadata = fieldMetadata;
break;
}
}
if (!isDefined(existingFieldMetadata)) {
throw new FieldMetadataException(
'Field does not exist',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
);
}
const objectMetadataItemWithFieldMaps =
objectMetadataMaps.byId[existingFieldMetadata.objectMetadataId];
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
if (
!isDefined(
objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId,
)
) {
throw new FieldMetadataException(
'Label identifier field metadata id does not exist',
FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND,
);
}
assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps);
assertDoesNotNullifyDefaultValueForNonNullableField({
isNullable: existingFieldMetadata.isNullable,
defaultValueFromUpdate: fieldMetadataInput.defaultValue,
});
if (fieldMetadataInput.isActive === false) {
checkCanDeactivateFieldOrThrow({
labelIdentifierFieldMetadataId:
objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId,
existingFieldMetadata,
});
}
const updatableFieldInput =
existingFieldMetadata.isCustom === false
? buildUpdatableStandardFieldInput(
fieldMetadataInput,
existingFieldMetadata,
)
: fieldMetadataInput;
const optionsForUpdate = isDefined(fieldMetadataInput.options)
? prepareCustomFieldMetadataOptions(fieldMetadataInput.options)
: undefined;
const defaultValueForUpdate =
updatableFieldInput.defaultValue !== undefined
? updatableFieldInput.defaultValue
: existingFieldMetadata.defaultValue;
const fieldMetadataForUpdate = {
...updatableFieldInput,
defaultValue: defaultValueForUpdate,
...optionsForUpdate,
};
await this.fieldMetadataValidationService.validateFieldMetadata({
fieldMetadataType: existingFieldMetadata.type,
existingFieldMetadata,
fieldMetadataInput: fieldMetadataForUpdate,
objectMetadata: objectMetadataItemWithFieldMaps,
});
const isLabelSyncedWithName =
fieldMetadataForUpdate.isLabelSyncedWithName ??
existingFieldMetadata.isLabelSyncedWithName;
if (isLabelSyncedWithName) {
validateNameAndLabelAreSyncOrThrow(
fieldMetadataForUpdate.label ?? existingFieldMetadata.label,
fieldMetadataForUpdate.name ?? existingFieldMetadata.name,
);
}
await fieldMetadataRepository.update(id, fieldMetadataForUpdate);
const [updatedFieldMetadata] = await fieldMetadataRepository.find({
where: { id },
});
if (!isDefined(updatedFieldMetadata)) {
throw new FieldMetadataException(
'Field does not exist',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
);
}
if (
isDefined(fieldMetadataInput.name) ||
isDefined(updatableFieldInput.options) ||
isDefined(updatableFieldInput.defaultValue)
) {
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`update-${updatedFieldMetadata.name}`),
fieldMetadataInput.workspaceId,
[
{
name: computeObjectTargetTable(objectMetadataItemWithFieldMaps),
action: WorkspaceMigrationTableActionType.ALTER,
columns: this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.ALTER,
existingFieldMetadata,
updatedFieldMetadata,
),
} satisfies WorkspaceMigrationTableAction,
],
queryRunner,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
updatedFieldMetadata.workspaceId,
queryRunner,
);
}
await queryRunner.commitTransaction();
if (fieldMetadataInput.isActive === false) {
const viewsRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
fieldMetadataInput.workspaceId,
'view',
);
await viewsRepository.delete({
kanbanFieldMetadataId: id,
});
}
if (
updatedFieldMetadata.isActive &&
isSelectOrMultiSelectFieldMetadata(updatedFieldMetadata) &&
isSelectOrMultiSelectFieldMetadata(existingFieldMetadata)
) {
await this.fieldMetadataRelatedRecordsService.updateRelatedViewGroups(
existingFieldMetadata,
updatedFieldMetadata,
);
await this.fieldMetadataRelatedRecordsService.updateRelatedViewFilters(
existingFieldMetadata,
updatedFieldMetadata,
);
}
await this.workspaceMetadataVersionService.incrementMetadataVersion(
fieldMetadataInput.workspaceId,
);
return updatedFieldMetadata;
} catch (error) {
if (queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction();
}
throw error;
} finally {
await queryRunner.release();
}
}
public async deleteOneField(
input: DeleteOneFieldInput,
workspaceId: string,
): Promise<FieldMetadataEntity> {
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
const [fieldMetadata] = await fieldMetadataRepository.find({
where: {
id: input.id,
workspaceId: workspaceId,
},
relations: [
'object',
'relationTargetFieldMetadata',
'relationTargetObjectMetadata',
],
});
if (!isDefined(fieldMetadata)) {
throw new FieldMetadataException(
'Field does not exist',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
);
}
if (!isDefined(fieldMetadata.object)) {
throw new FieldMetadataException(
'Object metadata does not exist',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
if (
fieldMetadata.object.labelIdentifierFieldMetadataId === fieldMetadata.id
) {
throw new FieldMetadataException(
'Cannot delete, please update the label identifier field first',
FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED,
{
userFriendlyMessage: t`Cannot delete, please update the label identifier field first`,
},
);
}
if (fieldMetadata.type === FieldMetadataType.RELATION) {
const isManyToOneRelation =
(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>)
.settings?.relationType === RelationType.MANY_TO_ONE;
if (!isDefined(fieldMetadata.relationTargetFieldMetadata)) {
throw new FieldMetadataException(
'Target field metadata does not exist',
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
);
}
await fieldMetadataRepository.delete({
id: In([
fieldMetadata.id,
fieldMetadata.relationTargetFieldMetadata.id,
]),
});
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`delete-${fieldMetadata.name}`),
workspaceId,
[
{
name: isManyToOneRelation
? computeObjectTargetTable(fieldMetadata.object)
: computeObjectTargetTable(
fieldMetadata.relationTargetObjectMetadata,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: isManyToOneRelation
? `${(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`
: `${(fieldMetadata.relationTargetFieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`,
} satisfies WorkspaceMigrationColumnDrop,
],
} satisfies WorkspaceMigrationTableAction,
],
queryRunner,
);
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
await fieldMetadataRepository.delete(fieldMetadata.id);
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) {
throw new Error(
`Composite type not found for field metadata type: ${fieldMetadata.type}`,
);
}
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`delete-${fieldMetadata.name}-composite-columns`,
),
workspaceId,
[
{
name: computeObjectTargetTable(fieldMetadata.object),
action: WorkspaceMigrationTableActionType.ALTER,
columns: compositeType.properties.map((property) => {
return {
action: WorkspaceMigrationColumnActionType.DROP,
columnName: computeCompositeColumnName(
fieldMetadata.name,
property,
),
} satisfies WorkspaceMigrationColumnDrop;
}),
} satisfies WorkspaceMigrationTableAction,
],
queryRunner,
);
} else {
await fieldMetadataRepository.delete(fieldMetadata.id);
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`delete-${fieldMetadata.name}`),
workspaceId,
[
{
name: computeObjectTargetTable(fieldMetadata.object),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: computeColumnName(fieldMetadata),
} satisfies WorkspaceMigrationColumnDrop,
],
} satisfies WorkspaceMigrationTableAction,
],
queryRunner,
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
workspaceId,
queryRunner,
);
await queryRunner.commitTransaction();
await this.viewService.resetKanbanAggregateOperationByFieldMetadataId({
workspaceId,
fieldMetadataId: fieldMetadata.id,
});
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
return fieldMetadata;
} catch (error) {
if (queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction();
}
throw error;
} finally {
await queryRunner.release();
}
}
public async findOneWithinWorkspace(
workspaceId: string,
options: FindOneOptions<FieldMetadataEntity>,
) {
const [fieldMetadata] = await this.fieldMetadataRepository.find({
...options,
where: {
...options.where,
workspaceId,
},
});
return fieldMetadata;
}
private groupFieldInputsByObjectId(
fieldMetadataInputs: CreateFieldInput[],
): Record<string, CreateFieldInput[]> {
return fieldMetadataInputs.reduce(
(acc, input) => {
if (!acc[input.objectMetadataId]) {
acc[input.objectMetadataId] = [];
}
acc[input.objectMetadataId].push(input);
return acc;
},
{} as Record<string, CreateFieldInput[]>,
);
}
async createMany(
fieldMetadataInputs: CreateFieldInput[],
): Promise<FieldMetadataEntity[]> {
if (!fieldMetadataInputs.length) {
return [];
}
const { objectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
{ workspaceId: fieldMetadataInputs[0].workspaceId },
);
const workspaceId = fieldMetadataInputs[0].workspaceId;
const isMorphRelationEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_MORPH_RELATION_ENABLED,
workspaceId,
);
const isSomeFieldMetadatInputsMorph = fieldMetadataInputs.some(
(fieldMetadataInput) =>
fieldMetadataInput.type === FieldMetadataType.MORPH_RELATION,
);
if (isSomeFieldMetadatInputsMorph && !isMorphRelationEnabled) {
throw new FieldMetadataException(
'Morph Relation feature is not enabled for this workspace',
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const fieldMetadataRepository =
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
const inputsByObjectId =
this.groupFieldInputsByObjectId(fieldMetadataInputs);
const objectMetadataIds = Object.keys(inputsByObjectId);
const createdFieldMetadatas: FieldMetadataEntity[] = [];
const migrationActions: WorkspaceMigrationTableAction[] = [];
for (const objectMetadataId of objectMetadataIds) {
const objectMetadata = objectMetadataMaps.byId[objectMetadataId];
if (!isDefined(objectMetadata)) {
throw new FieldMetadataException(
'Object metadata does not exist',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
const inputs = inputsByObjectId[objectMetadataId];
for (const fieldMetadataInput of inputs) {
const createdFieldMetadataItems =
await this.validateAndCreateFieldMetadataItems(
fieldMetadataInput,
objectMetadata,
fieldMetadataRepository,
objectMetadataMaps,
);
createdFieldMetadatas.push(...createdFieldMetadataItems);
const fieldMigrationActions = await this.createMigrationActions({
createdFieldMetadataItems,
objectMetadataMap: objectMetadataMaps.byId,
isRemoteCreation: fieldMetadataInput.isRemoteCreation ?? false,
});
migrationActions.push(...fieldMigrationActions);
}
}
if (migrationActions.length > 0) {
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`create-multiple-fields`),
workspaceId,
migrationActions,
queryRunner,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
workspaceId,
queryRunner,
);
}
await queryRunner.commitTransaction();
await this.fieldMetadataRelatedRecordsService.createViewAndViewFields(
createdFieldMetadatas,
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
return createdFieldMetadatas;
} catch (error) {
if (queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction();
}
throw error;
} finally {
await queryRunner.release();
}
}
private async validateAndCreateFieldMetadataItems(
fieldMetadataInput: CreateFieldInput,
objectMetadata: ObjectMetadataItemWithFieldMaps,
fieldMetadataRepository: Repository<FieldMetadataEntity>,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<FieldMetadataEntity[]> {
if (!fieldMetadataInput.isRemoteCreation) {
assertMutationNotOnRemoteObject(objectMetadata);
}
if (fieldMetadataInput.type === FieldMetadataType.RATING) {
fieldMetadataInput.options = generateRatingOptions();
}
if (fieldMetadataInput.isLabelSyncedWithName === true) {
validateNameAndLabelAreSyncOrThrow(
fieldMetadataInput.label,
fieldMetadataInput.name,
);
}
const fieldMetadataForCreate =
prepareCustomFieldMetadataForCreation(fieldMetadataInput);
await this.fieldMetadataValidationService.validateFieldMetadata({
fieldMetadataType: fieldMetadataForCreate.type,
fieldMetadataInput: fieldMetadataForCreate,
objectMetadata,
});
const isRelation =
fieldMetadataInput.type === FieldMetadataType.RELATION ||
fieldMetadataInput.type === FieldMetadataType.MORPH_RELATION;
if (!isRelation) {
const createdFieldMetadataItem = await fieldMetadataRepository.save(
fieldMetadataForCreate,
);
return [createdFieldMetadataItem];
}
if (fieldMetadataInput.type === FieldMetadataType.RELATION) {
const relationFieldMetadataForCreate =
await this.fieldMetadataRelationService.addCustomRelationFieldMetadataForCreation(
{
fieldMetadataInput: fieldMetadataForCreate,
relationCreationPayload: fieldMetadataInput.relationCreationPayload,
objectMetadata,
},
);
await this.fieldMetadataRelationService.validateFieldMetadataRelationSpecifics(
{
fieldMetadataInput: relationFieldMetadataForCreate,
fieldMetadataType: fieldMetadataForCreate.type,
objectMetadataMaps,
},
);
return await this.fieldMetadataRelationService.createRelationFieldMetadataItems(
{
fieldMetadataInput: relationFieldMetadataForCreate,
objectMetadata,
fieldMetadataRepository,
},
);
}
return await this.fieldMetadataMorphRelationService.createMorphRelationFieldMetadataItems(
{
fieldMetadataForCreate,
morphRelationsCreationPayload:
fieldMetadataInput.morphRelationsCreationPayload,
objectMetadata,
fieldMetadataRepository,
objectMetadataMaps,
},
);
}
private async createMigrationActions({
createdFieldMetadataItems,
objectMetadataMap,
isRemoteCreation,
}: {
createdFieldMetadataItems: FieldMetadataEntity[];
objectMetadataMap: Record<string, ObjectMetadataItemWithFieldMaps>;
isRemoteCreation: boolean;
}): Promise<WorkspaceMigrationTableAction[]> {
if (isRemoteCreation) {
return [];
}
const migrationActions: WorkspaceMigrationTableAction[] = [];
for (const createdFieldMetadata of createdFieldMetadataItems) {
if (
isFieldMetadataEntityOfType(
createdFieldMetadata,
FieldMetadataType.RELATION,
)
) {
const relationType = createdFieldMetadata.settings?.relationType;
if (relationType === RelationType.ONE_TO_MANY) {
continue;
}
}
migrationActions.push({
name: computeObjectTargetTable(
objectMetadataMap[createdFieldMetadata.objectMetadataId],
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.CREATE,
createdFieldMetadata,
),
});
}
return migrationActions;
}
}