diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts index 92c1b3e5b4..7270513a9d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts @@ -37,7 +37,7 @@ import { FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/type import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service'; import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service'; import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; -import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; +import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; @Injectable() @@ -264,7 +264,7 @@ export class ApplicationSyncService { for (const fieldToCreate of fieldsToCreate) { const createFieldInput: CreateFieldInput = { - name: computeMetadataNameFromLabel(fieldToCreate.label), + name: computeMetadataNameFromLabelOrThrow(fieldToCreate.label), type: fieldToCreate.type, label: fieldToCreate.label, description: fieldToCreate.description ?? undefined, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index 69a36eb98e..6ddb6fa2f7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -15,14 +15,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver'; -import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor'; -import { FieldMetadataEnumValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-enum-validation.service'; -import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.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 { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; -import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator'; -import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { FlatFieldMetadataModule } from 'src/engine/metadata-modules/flat-field-metadata/flat-field-metadata.module'; import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; @@ -38,6 +31,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace- import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; +import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor'; import { FieldMetadataEntity } from './field-metadata.entity'; @@ -72,14 +66,7 @@ import { UpdateFieldInput } from './dtos/update-field.input'; IndexMetadataModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, ], - services: [ - IsFieldMetadataDefaultValue, - FieldMetadataService, - FieldMetadataMorphRelationService, - FieldMetadataRelationService, - FieldMetadataValidationService, - FieldMetadataEnumValidationService, - ], + services: [FieldMetadataService], resolvers: [ { EntityClass: FieldMetadataEntity, @@ -104,22 +91,7 @@ import { UpdateFieldInput } from './dtos/update-field.input'; ], }), ], - providers: [ - IsFieldMetadataDefaultValue, - IsFieldMetadataOptions, - FieldMetadataService, - FieldMetadataRelationService, - FieldMetadataMorphRelationService, - FieldMetadataValidationService, - FieldMetadataEnumValidationService, - FieldMetadataResolver, - ], - exports: [ - FieldMetadataService, - FieldMetadataRelationService, - FieldMetadataMorphRelationService, - FieldMetadataEnumValidationService, - FieldMetadataValidationService, - ], + providers: [FieldMetadataService, FieldMetadataResolver], + exports: [FieldMetadataService], }) export class FieldMetadataModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface.ts deleted file mode 100644 index cd55f0ff8c..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - type FieldMetadataType, - type FieldMetadataOptions, -} from 'twenty-shared/types'; - -export interface CompositeProperty< - Type extends FieldMetadataType = FieldMetadataType, -> { - name: string; - description?: string; - type: Type; - hidden: 'input' | 'output' | true | false; - isRequired: boolean; - isIncludedInUniqueConstraint?: boolean; - isArray?: boolean; - options?: FieldMetadataOptions; -} - -export interface CompositeType { - type: FieldMetadataType; - properties: CompositeProperty[]; -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-enum-validation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-enum-validation.service.ts deleted file mode 100644 index 3b05c57d60..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-enum-validation.service.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type MessageDescriptor } from '@lingui/core'; -import { msg } from '@lingui/core/macro'; -import { isNonEmptyString } from '@sniptt/guards'; -import { - type EnumFieldMetadataType, - FieldMetadataType, - type FieldMetadataOptions, -} from 'twenty-shared/types'; -import { assertUnreachable, isDefined } from 'twenty-shared/utils'; -import { z } from 'zod'; - -import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { - type FieldMetadataComplexOption, - type FieldMetadataDefaultOption, -} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { type 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 { type EnumFieldMetadataUnionType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { - beneathDatabaseIdentifierMinimumLength, - exceedsDatabaseIdentifierMaximumLength, -} from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; -import { isSnakeCaseString } from 'src/utils/is-snake-case-string'; - -type Validator = { - validator: (str: T) => boolean; - message: MessageDescriptor; -}; - -type FieldMetadataUpdateCreateInput = CreateFieldInput | UpdateFieldInput; - -type ValidateEnumFieldMetadataArgs = { - existingFieldMetadata?: Pick< - FieldMetadataEntity, - 'type' | 'isNullable' | 'defaultValue' | 'options' - >; - fieldMetadataInput: FieldMetadataUpdateCreateInput; - fieldMetadataType: EnumFieldMetadataUnionType; -}; - -const QUOTED_STRING_REGEX = /^['"](.*)['"]$/; - -@Injectable() -export class FieldMetadataEnumValidationService { - constructor() {} - - private validatorRunner( - elementToValidate: T, - { message, validator }: Validator, - ) { - const shouldThrow = validator(elementToValidate); - - if (shouldThrow) { - throw new FieldMetadataException( - message.message ?? 'Invalid field input', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - { - userFriendlyMessage: message, - }, - ); - } - } - - private validateMetadataOptionId(sanitizedId?: string) { - const validators: Validator[] = [ - { - validator: (id) => !isDefined(id), - message: msg`Option id is required`, - }, - { - validator: (id) => !z.string().uuid().safeParse(id).success, - message: msg`Option id is invalid`, - }, - ]; - - validators.forEach((validator) => - this.validatorRunner(sanitizedId, validator), - ); - } - - private validateMetadataOptionLabel(sanitizedLabel: string) { - const validators: Validator[] = [ - { - validator: (label) => !isDefined(label), - message: msg`Option label is required`, - }, - { - validator: exceedsDatabaseIdentifierMaximumLength, - message: msg`Option label exceeds 63 characters`, - }, - { - validator: beneathDatabaseIdentifierMinimumLength, - message: msg`Option label "${sanitizedLabel}" is beneath 1 character`, - }, - { - validator: (label) => label.includes(','), - message: msg`Label must not contain a comma`, - }, - { - validator: (label) => !isNonEmptyString(label) || label === ' ', - message: msg`Label must not be empty`, - }, - ]; - - validators.forEach((validator) => - this.validatorRunner(sanitizedLabel, validator), - ); - } - - private validateMetadataOptionValue(sanitizedValue: string) { - const validators: Validator[] = [ - { - validator: (value) => !isDefined(value), - message: msg`Option value is required`, - }, - { - validator: exceedsDatabaseIdentifierMaximumLength, - message: msg`Option value exceeds 63 characters`, - }, - { - validator: beneathDatabaseIdentifierMinimumLength, - message: msg`Option value "${sanitizedValue}" is beneath 1 character`, - }, - { - validator: (value) => !isSnakeCaseString(value), - message: msg`Value must be in UPPER_CASE and follow snake_case "${sanitizedValue}"`, - }, - ]; - - validators.forEach((validator) => - this.validatorRunner(sanitizedValue, validator), - ); - } - - private validateDuplicates( - options: FieldMetadataOptions, - ) { - const fieldsToCheckForDuplicates = [ - 'position', - 'id', - 'value', - ] as const satisfies (keyof ( - | FieldMetadataDefaultOption[] - | FieldMetadataComplexOption[] - )[number])[]; - const duplicatedValidators = fieldsToCheckForDuplicates.map< - Validator - >((field) => ({ - message: msg`Duplicated option ${field}`, - validator: () => - new Set(options.map((option) => option[field])).size !== options.length, - })); - - duplicatedValidators.forEach((validator) => - this.validatorRunner(options, validator), - ); - } - - private validateFieldMetadataInputOptions( - fieldMetadataInput: FieldMetadataUpdateCreateInput, - ) { - const { options } = fieldMetadataInput; - - if (!isDefined(options) || options.length === 0) { - throw new FieldMetadataException( - 'Options are required for enum fields', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } - - for (const option of options) { - this.validateMetadataOptionId(option.id); - this.validateMetadataOptionValue(option.value); - this.validateMetadataOptionLabel(option.label); - } - - this.validateDuplicates(options); - } - - private validateSelectDefaultValue( - options: FieldMetadataOptions, - defaultValue: unknown, - ) { - if (typeof defaultValue !== 'string') { - throw new FieldMetadataException( - 'Default value for multi-select must be a stringified array', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } - - const validators: Validator[] = [ - { - validator: (value: string) => !QUOTED_STRING_REGEX.test(value), - message: msg`Default value should be as quoted string`, - }, - { - validator: (value: string) => - !options.some( - (option) => - option.value === value.replace(QUOTED_STRING_REGEX, '$1'), - ), - message: msg`Default value "${defaultValue}" must be one of the option values`, - }, - ]; - - validators.forEach((validator) => - this.validatorRunner(defaultValue, validator), - ); - } - - private validateMultiSelectDefaultValue( - options: FieldMetadataOptions, - defaultValue: unknown, - ) { - if (!Array.isArray(defaultValue)) { - throw new FieldMetadataException( - 'Default value for multi-select must be an array', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } - - const validators: Validator[] = [ - { - validator: (values) => values.length === 0, - message: msg`If defined default value must contain at least one value`, - }, - { - validator: (values) => new Set(values).size !== values.length, - message: msg`Default values must be unique`, - }, - ]; - - validators.forEach((validator) => - this.validatorRunner(defaultValue, validator), - ); - - defaultValue.forEach((value) => { - this.validateSelectDefaultValue(options, value); - }); - } - - private validateFieldMetadataDefaultValue( - fieldType: EnumFieldMetadataType, - options: FieldMetadataOptions, - defaultValue: unknown, - ) { - switch (fieldType) { - case FieldMetadataType.SELECT: - this.validateSelectDefaultValue(options, defaultValue); - break; - case FieldMetadataType.MULTI_SELECT: - this.validateMultiSelectDefaultValue(options, defaultValue); - break; - case FieldMetadataType.RATING: - // TODO: Determine if RATING should be handled here - break; - default: { - assertUnreachable( - fieldType, - 'Should never occur, unknown field metadata enum type', - ); - } - } - } - - async validateEnumFieldMetadataInput({ - fieldMetadataInput, - fieldMetadataType, - existingFieldMetadata, - }: ValidateEnumFieldMetadataArgs) { - const isUpdate = isDefined(existingFieldMetadata); - const shouldSkipFieldMetadataInputOptionsValidation = - isUpdate && fieldMetadataInput.options === undefined; - - if (!shouldSkipFieldMetadataInputOptionsValidation) { - this.validateFieldMetadataInputOptions(fieldMetadataInput); - } - - if (isDefined(fieldMetadataInput.defaultValue)) { - const options = - fieldMetadataInput.options ?? existingFieldMetadata?.options; - - if (!isDefined(options)) { - throw new FieldMetadataException( - 'Should never occur, could not retrieve any options to validate default value', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } - - this.validateFieldMetadataDefaultValue( - fieldMetadataType, - options, - fieldMetadataInput.defaultValue, - ); - } - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts deleted file mode 100644 index 34e2a585ed..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import omit from 'lodash.omit'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; -import { type Repository } from 'typeorm'; -import { v4 } from 'uuid'; -import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { type 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 { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; -import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; - -@Injectable() -export class FieldMetadataMorphRelationService { - constructor( - private readonly fieldMetadataRelationService: FieldMetadataRelationService, - ) {} - - async createMorphRelationFieldMetadataItems({ - fieldMetadataForCreate, - morphRelationsCreationPayload, - flatObjectMetadata, - fieldMetadataRepository, - flatObjectMetadataMaps, - flatFieldMetadataMaps, - }: { - fieldMetadataForCreate: CreateFieldInput; - morphRelationsCreationPayload: CreateFieldInput['morphRelationsCreationPayload']; - flatObjectMetadata: FlatObjectMetadata; - fieldMetadataRepository: Repository; - flatObjectMetadataMaps: FlatEntityMaps; - flatFieldMetadataMaps: FlatEntityMaps; - }): Promise { - 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[] = []; - const morphId = v4(); - - for (const relationCreationPayload of morphRelationsCreationPayload) { - const targetObjectMetadata = - flatObjectMetadataMaps.byId[ - relationCreationPayload.targetObjectMetadataId - ]; - - if (!isDefined(targetObjectMetadata)) { - throw new FieldMetadataException( - 'Target object metadata does not exist in the object metadata maps', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - const currentMorphRelationFieldName = computeMorphRelationFieldName({ - fieldName: fieldMetadataForCreate.name, - relationType: relationCreationPayload.type, - targetObjectMetadataNameSingular: targetObjectMetadata.nameSingular, - targetObjectMetadataNamePlural: targetObjectMetadata.namePlural, - }); - const relationFieldMetadataForCreate = - this.fieldMetadataRelationService.computeCustomRelationFieldMetadataForCreation( - { - fieldMetadataInput: { - ...fieldMetadataForCreate, - name: currentMorphRelationFieldName, - }, - relationCreationPayload: relationCreationPayload, - joinColumnName: computeMorphOrRelationFieldJoinColumnName({ - name: currentMorphRelationFieldName, - }), - }, - ); - - await this.fieldMetadataRelationService.validateFieldMetadataRelationSpecifics( - { - fieldMetadataInput: relationFieldMetadataForCreate, - fieldMetadataType: relationFieldMetadataForCreate.type, - flatObjectMetadataMaps, - flatFieldMetadataMaps, - flatObjectMetadata, - }, - ); - - const createdMorphFieldMetadataItemWithoutTargetField = - await fieldMetadataRepository.save( - omit({ ...relationFieldMetadataForCreate, morphId }, 'id'), - ); - - const targetFieldMetadataName = computeMetadataNameFromLabel( - relationCreationPayload.targetFieldLabel, - ); - - const targetFieldMetadataToCreate = prepareCustomFieldMetadataForCreation( - { - objectMetadataId: relationCreationPayload.targetObjectMetadataId, - type: FieldMetadataType.RELATION, - name: targetFieldMetadataName, - label: relationCreationPayload.targetFieldLabel, - icon: relationCreationPayload.targetFieldIcon, - workspaceId: fieldMetadataForCreate.workspaceId, - settings: fieldMetadataForCreate.settings, - }, - ); - - const targetFieldMetadataToCreateWithRelation = - this.fieldMetadataRelationService.computeCustomRelationFieldMetadataForCreation( - { - fieldMetadataInput: targetFieldMetadataToCreate, - relationCreationPayload: { - targetObjectMetadataId: flatObjectMetadata.id, - targetFieldLabel: fieldMetadataForCreate.label, - targetFieldIcon: fieldMetadataForCreate.icon ?? 'Icon123', - type: - relationCreationPayload.type === RelationType.ONE_TO_MANY - ? RelationType.MANY_TO_ONE - : RelationType.ONE_TO_MANY, - }, - joinColumnName: computeMorphOrRelationFieldJoinColumnName({ - name: targetFieldMetadataToCreate.name, - }), - }, - ); - - // todo better type - const targetFieldMetadataToCreateWithRelationWithId = { - id: v4(), - ...targetFieldMetadataToCreateWithRelation, - }; - - const targetFieldMetadata = await fieldMetadataRepository.save({ - ...targetFieldMetadataToCreateWithRelationWithId, - relationTargetFieldMetadataId: - createdMorphFieldMetadataItemWithoutTargetField.id, - }); - - const createdFieldMetadataItemUpdated = - await fieldMetadataRepository.save({ - ...createdMorphFieldMetadataItemWithoutTargetField, - relationTargetFieldMetadataId: targetFieldMetadata.id, - }); - - fieldsCreated.push(createdFieldMetadataItemUpdated, targetFieldMetadata); - } - - return fieldsCreated; - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts deleted file mode 100644 index eb7a9d196e..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { msg } from '@lingui/core/macro'; -import { IsEnum, IsString, IsUUID } from 'class-validator'; -import { - type FieldMetadataSettings, - FieldMetadataType, - RelationOnDeleteAction, -} from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { type Repository } from 'typeorm'; -import { v4 } from 'uuid'; -import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { type 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 { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; -import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; -import { validateRelationCreationPayloadOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util'; -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; -import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; - -export class RelationCreationPayloadValidation { - @IsUUID() - targetObjectMetadataId?: string; - - @IsString() - targetFieldLabel: string; - - @IsString() - targetFieldIcon: string; - - @IsEnum(RelationType) - type: RelationType; -} - -type ValidateFieldMetadataArgs = - { - fieldMetadataType: FieldMetadataType; - fieldMetadataInput: T; - flatObjectMetadata: FlatObjectMetadata; - flatObjectMetadataMaps: FlatEntityMaps; - flatFieldMetadataMaps: FlatEntityMaps; - existingFieldMetadata?: FieldMetadataEntity; - }; - -@Injectable() -export class FieldMetadataRelationService { - constructor() {} - - async createRelationFieldMetadataItems({ - fieldMetadataInput, - flatObjectMetadata, - fieldMetadataRepository, - }: { - fieldMetadataInput: CreateFieldInput; - flatObjectMetadata: FlatObjectMetadata; - fieldMetadataRepository: Repository; - }): Promise { - 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, - defaultValue: fieldMetadataInput.defaultValue, - }); - - const targetFieldMetadataToCreateWithRelation = - this.computeCustomRelationFieldMetadataForCreation({ - fieldMetadataInput: targetFieldMetadataToCreate, - relationCreationPayload: { - targetObjectMetadataId: flatObjectMetadata.id, - targetFieldLabel: fieldMetadataInput.label, - targetFieldIcon: fieldMetadataInput.icon ?? 'Icon123', - type: - relationCreationPayload.type === RelationType.ONE_TO_MANY - ? RelationType.MANY_TO_ONE - : RelationType.ONE_TO_MANY, - }, - joinColumnName: computeMorphOrRelationFieldJoinColumnName({ - name: targetFieldMetadataToCreate.name, - }), - }); - - // 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, - flatObjectMetadataMaps, - flatFieldMetadataMaps, - flatObjectMetadata, - }: Pick< - ValidateFieldMetadataArgs, - | 'fieldMetadataInput' - | 'fieldMetadataType' - | 'flatObjectMetadataMaps' - | 'flatFieldMetadataMaps' - | 'flatObjectMetadata' - >): Promise { - // 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, - ) - ) { - validateFieldNameAvailabilityOrThrow({ - name: `${fieldMetadataInput.name}Id`, - flatObjectMetadata, - flatFieldMetadataMaps, - }); - - const relationCreationPayload = ( - fieldMetadataInput as unknown as CreateFieldInput - ).relationCreationPayload; - - if (isDefined(relationCreationPayload)) { - await validateRelationCreationPayloadOrThrow(relationCreationPayload); - const computedMetadataNameFromLabel = computeMetadataNameFromLabel( - relationCreationPayload.targetFieldLabel, - ); - - validateMetadataNameOrThrow(computedMetadataNameFromLabel); - - const targetFlatObjectMetadata = - flatObjectMetadataMaps.byId[ - relationCreationPayload.targetObjectMetadataId - ]; - - if (!isDefined(targetFlatObjectMetadata)) { - throw new FieldMetadataException( - `Object metadata relation target not found for relation creation payload`, - FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - ); - } - - validateFieldNameAvailabilityOrThrow({ - name: computedMetadataNameFromLabel, - flatObjectMetadata: targetFlatObjectMetadata, - flatFieldMetadataMaps, - }); - - validateFieldNameAvailabilityOrThrow({ - name: `${computedMetadataNameFromLabel}Id`, - flatObjectMetadata: targetFlatObjectMetadata, - flatFieldMetadataMaps, - }); - - if ( - computedMetadataNameFromLabel === fieldMetadataInput.name && - flatObjectMetadata.id === targetFlatObjectMetadata.id - ) { - throw new FieldMetadataException( - `Name "${computedMetadataNameFromLabel}" cannot be the same on both side of the relation`, - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - { - userFriendlyMessage: msg`Name "${computedMetadataNameFromLabel}" cannot be the same on both side of the relation`, - }, - ); - } - } - } - - return fieldMetadataInput; - } - - // TODO refactor and strictly type - computeCustomRelationFieldMetadataForCreation({ - fieldMetadataInput, - relationCreationPayload, - joinColumnName, - }: { - fieldMetadataInput: CreateFieldInput; - relationCreationPayload: CreateFieldInput['relationCreationPayload']; - joinColumnName: string; - }) { - const isRelation = - isFieldMetadataEntityOfType( - fieldMetadataInput, - FieldMetadataType.RELATION, - ) || - isFieldMetadataEntityOfType( - fieldMetadataInput, - FieldMetadataType.MORPH_RELATION, - ); - - const defaultIcon = 'IconRelationOneToMany'; - - const isManyToOne = - isRelation && relationCreationPayload?.type === RelationType.MANY_TO_ONE; - - const settings = isManyToOne - ? { - relationType: RelationType.MANY_TO_ONE, - onDelete: RelationOnDeleteAction.SET_NULL, - joinColumnName, - } - : { - ...(fieldMetadataInput.settings as FieldMetadataSettings< - FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION - >), - relationType: RelationType.ONE_TO_MANY, - }; - - return { - ...fieldMetadataInput, - icon: fieldMetadataInput.icon ?? defaultIcon, - relationCreationPayload, - relationTargetObjectMetadataId: - relationCreationPayload?.targetObjectMetadataId, - settings, - }; - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts deleted file mode 100644 index d279c10143..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { msg } from '@lingui/core/macro'; -import { type ClassConstructor, plainToInstance } from 'class-transformer'; -import { - IsArray, - IsEnum, - IsInt, - IsOptional, - Max, - Min, - type ValidationError, - validateOrReject, -} from 'class-validator'; -import { MULTI_ITEM_FIELD_MIN_MAX_VALUES } from 'twenty-shared/constants'; -import { - ALLOWED_ADDRESS_SUBFIELDS, - type AllowedAddressSubField, - FieldMetadataType, - type FieldMetadataSettings, -} from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { type 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 { 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 { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -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-or-throw.utils'; - -type ValidateFieldMetadataArgs = { - fieldMetadataType: FieldMetadataType; - fieldMetadataInput: CreateFieldInput | UpdateFieldInput; - flatObjectMetadata: FlatObjectMetadata; - flatFieldMetadataMaps: FlatEntityMaps; - existingFieldMetadata?: FieldMetadataEntity; -}; - -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; -} -class AddressSettingsValidation { - @IsOptional() - @IsArray() - @IsEnum(ALLOWED_ADDRESS_SUBFIELDS, { each: true }) - subFields?: AllowedAddressSubField[]; -} - -class MultipleValuesSettingsValidation { - @IsOptional() - @IsInt() - @Min(MULTI_ITEM_FIELD_MIN_MAX_VALUES) - maxNumberOfValues?: number; -} -@Injectable() -export class FieldMetadataValidationService { - constructor( - private readonly fieldMetadataEnumValidationService: FieldMetadataEnumValidationService, - ) {} - - async validateSettingsOrThrow({ - fieldType, - settings, - }: { - fieldType: FieldMetadataType; - settings: FieldMetadataSettings; - }) { - switch (fieldType) { - case FieldMetadataType.NUMBER: - await this.validateSettings({ - type: FieldMetadataType.NUMBER, - validator: NumberSettingsValidation, - settings, - }); - break; - case FieldMetadataType.TEXT: - await this.validateSettings({ - type: FieldMetadataType.TEXT, - validator: TextSettingsValidation, - settings, - }); - break; - case FieldMetadataType.ADDRESS: - await this.validateSettings({ - type: FieldMetadataType.ADDRESS, - validator: AddressSettingsValidation, - settings, - }); - break; - case FieldMetadataType.PHONES: - case FieldMetadataType.EMAILS: - case FieldMetadataType.LINKS: - case FieldMetadataType.ARRAY: - await this.validateSettings({ - type: fieldType, - validator: MultipleValuesSettingsValidation, - settings, - }); - break; - default: - break; - } - } - - private async validateSettings< - Type extends FieldMetadataType, - TValidator extends ClassConstructor, - TFieldMetadataType extends FieldMetadataType, - >({ - type: _type, - settings, - validator, - }: { - validator: TValidator; - settings: FieldMetadataSettings; - type: TFieldMetadataType; - }) { - 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, - flatObjectMetadata, - flatFieldMetadataMaps, - existingFieldMetadata, - }: ValidateFieldMetadataArgs): Promise { - if (fieldMetadataInput.name) { - try { - validateMetadataNameOrThrow(fieldMetadataInput.name); - } catch (error) { - if (error instanceof InvalidMetadataException) { - throw new FieldMetadataException( - error.message, - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - { - userFriendlyMessage: error.userFriendlyMessage, - }, - ); - } - - throw error; - } - - try { - validateFieldNameAvailabilityOrThrow({ - name: fieldMetadataInput.name, - flatObjectMetadata, - flatFieldMetadataMaps, - }); - } 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: msg`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, - }); - } - - const isRelationField = - fieldMetadataType === FieldMetadataType.RELATION || - fieldMetadataType === FieldMetadataType.MORPH_RELATION; - - if ( - isRelationField && - isDefined(existingFieldMetadata) && - isDefined(fieldMetadataInput.name) && - fieldMetadataInput.name !== existingFieldMetadata.name - ) { - throw new FieldMetadataException( - 'Name cannot be changed for relation fields', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/__snapshots__/check-can-deactivate-field-or-throw.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/__snapshots__/check-can-deactivate-field-or-throw.spec.ts.snap deleted file mode 100644 index 4001975ee7..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/__snapshots__/check-can-deactivate-field-or-throw.spec.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate createdAt field 1`] = `"Cannot deactivate createdAt, updatedAt or deletedAt field"`; - -exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate deletedAt field 1`] = `"Cannot deactivate createdAt, updatedAt or deletedAt field"`; - -exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate label identifier field 1`] = `"Cannot deactivate label identifier field"`; - -exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate system field 1`] = `"Cannot deactivate system field"`; - -exports[`checkCanDeactivateFieldOrThrow throws if trying to deactivate updatedAt field 1`] = `"Cannot deactivate createdAt, updatedAt or deletedAt field"`; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/assert-does-not-nullify-default-value-for-non-nullable-field.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/assert-does-not-nullify-default-value-for-non-nullable-field.spec.ts deleted file mode 100644 index 04ecc10334..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/assert-does-not-nullify-default-value-for-non-nullable-field.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { assertDoesNotNullifyDefaultValueForNonNullableField } from 'src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util'; - -describe('assertDoesNotNullifyDefaultValueForNonNullableField', () => { - it('should not throw if default value is set to null and field is nullable', () => { - expect(() => - assertDoesNotNullifyDefaultValueForNonNullableField({ - isNullable: true, - defaultValueFromUpdate: null, - }), - ).not.toThrow(); - }); - - it('should not throw if default value is undefined and field is non nullable', () => { - expect(() => - assertDoesNotNullifyDefaultValueForNonNullableField({ - isNullable: false, - }), - ).not.toThrow(); - }); - - it('should not throw if default value is not set to null and field is non nullable', () => { - expect(() => - assertDoesNotNullifyDefaultValueForNonNullableField({ - isNullable: false, - defaultValueFromUpdate: 'new default value', - }), - ).not.toThrow(); - }); - - it('should throw if default value is set to null and field is non nullable', () => { - expect(() => - assertDoesNotNullifyDefaultValueForNonNullableField({ - isNullable: false, - defaultValueFromUpdate: null, - }), - ).toThrow(); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/check-can-deactivate-field-or-throw.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/check-can-deactivate-field-or-throw.spec.ts deleted file mode 100644 index 873da7250d..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/check-can-deactivate-field-or-throw.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { type EachTestingContext } from 'twenty-shared/testing'; - -import { checkCanDeactivateFieldOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw'; - -type CheckCanDeactivateFieldOrThrowTestContext = EachTestingContext<{ - input: Parameters[0]; - shouldNotThrow?: true; -}>; - -const checkCanDeactivateFieldOrThrowTestCases: CheckCanDeactivateFieldOrThrowTestContext[] = - [ - { - title: 'does not throw if nominal case', - context: { - input: { - labelIdentifierFieldMetadataId: 'fieldIdentifierId', - existingFieldMetadata: { - id: 'myFieldId', - isSystem: false, - name: 'myFieldName', - }, - }, - shouldNotThrow: true, - }, - }, - { - title: 'throws if trying to deactivate label identifier field', - context: { - input: { - labelIdentifierFieldMetadataId: 'fieldId', - existingFieldMetadata: { - id: 'fieldId', - isSystem: false, - name: 'name', - }, - }, - }, - }, - { - title: 'throws if trying to deactivate system field', - context: { - input: { - labelIdentifierFieldMetadataId: 'fieldIdentifierId', - existingFieldMetadata: { - id: 'systemFieldId', - isSystem: true, - name: 'systemField', - }, - }, - }, - }, - { - title: 'throws if trying to deactivate createdAt field', - context: { - input: { - labelIdentifierFieldMetadataId: 'fieldIdentifierId', - existingFieldMetadata: { - id: 'createdAtId', - isSystem: false, - name: 'createdAt', - }, - }, - }, - }, - { - title: 'throws if trying to deactivate updatedAt field', - context: { - input: { - labelIdentifierFieldMetadataId: 'fieldIdentifierId', - existingFieldMetadata: { - id: 'updatedAtId', - isSystem: false, - name: 'updatedAt', - }, - }, - }, - }, - { - title: 'throws if trying to deactivate deletedAt field', - context: { - input: { - labelIdentifierFieldMetadataId: 'fieldIdentifierId', - existingFieldMetadata: { - id: 'deletedAtId', - isSystem: false, - name: 'deletedAt', - }, - }, - }, - }, - ]; - -describe('checkCanDeactivateFieldOrThrow', () => { - it.each(checkCanDeactivateFieldOrThrowTestCases)( - '$title', - ({ context: { input, shouldNotThrow } }) => { - if (shouldNotThrow) { - expect(() => checkCanDeactivateFieldOrThrow(input)).not.toThrow(); - } else { - expect(() => - checkCanDeactivateFieldOrThrow(input), - ).toThrowErrorMatchingSnapshot(); - } - }, - ); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/field-metadata-validation.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/field-metadata-validation.service.spec.ts deleted file mode 100644 index ccb8d3a22c..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/field-metadata-validation.service.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; - -import { - type FieldMetadataSettings, - FieldMetadataType, -} from 'twenty-shared/types'; - -import { FieldMetadataException } 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 { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service'; - -describe('FieldMetadataValidationService', () => { - let service: FieldMetadataValidationService; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - FieldMetadataValidationService, - { - provide: FieldMetadataEnumValidationService, - useValue: {}, - }, - ], - }).compile(); - - service = module.get( - FieldMetadataValidationService, - ); - }); - - it('should validate NUMBER settings successfully', async () => { - const settings: FieldMetadataSettings = { - decimals: 2, - type: 'number', - }; - - await expect( - service.validateSettingsOrThrow({ - fieldType: FieldMetadataType.NUMBER, - settings, - }), - ).resolves.not.toThrow(); - }); - - it('should throw an error for invalid NUMBER settings', async () => { - const settings: FieldMetadataSettings = { - // @ts-expect-error expected invalid payload below - type: 'invalidType', - }; - - await expect( - service.validateSettingsOrThrow({ - fieldType: FieldMetadataType.NUMBER, - settings, - }), - ).rejects.toThrow(FieldMetadataException); - }); - - it('should validate TEXT settings successfully', async () => { - const settings: FieldMetadataSettings = { - displayedMaxRows: 10, - }; - - await expect( - service.validateSettingsOrThrow({ - fieldType: FieldMetadataType.TEXT, - settings, - }), - ).resolves.not.toThrow(); - }); - - it('should throw an error for invalid TEXT settings', async () => { - const settings: FieldMetadataSettings = { - // @ts-expect-error expected invalid payload below - displayedMaxRows: 'NotANumber', - }; - - await expect( - service.validateSettingsOrThrow({ - fieldType: FieldMetadataType.TEXT, - settings, - }), - ).rejects.toThrow(FieldMetadataException); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-of-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-of-type.util.ts deleted file mode 100644 index 2a68c509d9..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-of-type.util.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; - -export const areFieldMetadatasOfType = ( - fieldMetadatas: FieldMetadataEntity[], - fieldMetadataType: T, -): fieldMetadatas is Array> => - fieldMetadatas.every((fieldMetadata) => - isFlatFieldMetadataOfType(fieldMetadata, fieldMetadataType), - ); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util.ts deleted file mode 100644 index 809c669279..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; - -export const assertDoesNotNullifyDefaultValueForNonNullableField = ({ - isNullable, - defaultValueFromUpdate, -}: { - isNullable: boolean | null; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - defaultValueFromUpdate?: any; -}) => { - if (!isNullable && defaultValueFromUpdate === null) { - throw new FieldMetadataException( - 'Default value cannot be nullified for non-nullable field', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/build-updatable-standard-field-input.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/build-updatable-standard-field-input.util.ts deleted file mode 100644 index 8a849e6c81..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/build-updatable-standard-field-input.util.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto'; -import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; - -export const buildUpdatableStandardFieldInput = ( - fieldMetadataInput: UpdateFieldInput, - existingFieldMetadata: Pick< - FieldMetadataEntity, - 'type' | 'isNullable' | 'defaultValue' | 'options' - >, -) => { - const updatableStandardFieldInput: UpdateFieldInput & { - standardOverrides?: FieldStandardOverridesDTO; - } = { - id: fieldMetadataInput.id, - isActive: fieldMetadataInput.isActive, - workspaceId: fieldMetadataInput.workspaceId, - defaultValue: fieldMetadataInput.defaultValue, - settings: fieldMetadataInput.settings, - isLabelSyncedWithName: fieldMetadataInput.isLabelSyncedWithName, - isUnique: fieldMetadataInput.isUnique, - }; - - if ('standardOverrides' in fieldMetadataInput) { - updatableStandardFieldInput.standardOverrides = - fieldMetadataInput.standardOverrides as FieldStandardOverridesDTO; - } - - if ( - existingFieldMetadata.type === FieldMetadataType.SELECT || - existingFieldMetadata.type === FieldMetadataType.MULTI_SELECT - ) { - return { - ...updatableStandardFieldInput, - options: fieldMetadataInput.options, - }; - } - - return updatableStandardFieldInput; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw.ts deleted file mode 100644 index 35caea46cc..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; - -type CheckCanDeactivateFieldOptions = { - labelIdentifierFieldMetadataId: string; - existingFieldMetadata: Pick; -}; - -export const checkCanDeactivateFieldOrThrow = ({ - labelIdentifierFieldMetadataId, - existingFieldMetadata, -}: CheckCanDeactivateFieldOptions) => { - if (existingFieldMetadata.id === labelIdentifierFieldMetadataId) { - throw new FieldMetadataException( - 'Cannot deactivate label identifier field', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } - - if (existingFieldMetadata.isSystem === true) { - throw new FieldMetadataException( - 'Cannot deactivate system field', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } - - if ( - ['deletedAt', 'createdAt', 'updatedAt'].includes(existingFieldMetadata.name) - ) { - throw new FieldMetadataException( - 'Cannot deactivate createdAt, updatedAt or deletedAt field', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util.ts deleted file mode 100644 index fa8b00308f..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; - -export const isFieldMetadataTypeMorphRelation = ( - fieldMetadata: FieldMetadataEntity, -): fieldMetadata is FieldMetadataEntity & - FieldMetadataEntity => { - return fieldMetadata.type === FieldMetadataType.MORPH_RELATION; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util.ts deleted file mode 100644 index 181e98b921..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; -export const isSelectFieldMetadataType = ( - type: FieldMetadataType, -): type is FieldMetadataType.SELECT => { - return type === FieldMetadataType.SELECT; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts deleted file mode 100644 index a726943fc7..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; - -export type SelectOrMultiSelectFieldMetadataEntity = FieldMetadataEntity< - FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT ->; -export const isSelectOrMultiSelectFieldMetadata = ( - fieldMetadata: FieldMetadataEntity, -): fieldMetadata is FieldMetadataEntity & - SelectOrMultiSelectFieldMetadataEntity => { - return [FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT].includes( - fieldMetadata.type, - ); -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util.ts deleted file mode 100644 index e3c6748697..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { v4 } from 'uuid'; -import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils'; - -import { - type FieldMetadataComplexOption, - type FieldMetadataDefaultOption, -} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; - -export const prepareCustomFieldMetadataOptions = ( - options: FieldMetadataDefaultOption[] | FieldMetadataComplexOption[], -): undefined | Pick => { - return { - options: options.map((option) => ({ - id: v4(), - ...trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(option, [ - 'label', - 'value', - 'id', - ]), - })), - }; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util.ts deleted file mode 100644 index 45e0639ef4..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { v4 } from 'uuid'; - -import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value'; -import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable'; -import { prepareCustomFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util'; - -export const prepareCustomFieldMetadataForCreation = ( - fieldMetadataInput: CreateFieldInput, -) => { - const options = fieldMetadataInput.options - ? prepareCustomFieldMetadataOptions(fieldMetadataInput.options) - : undefined; - const defaultValue = - fieldMetadataInput.defaultValue ?? - generateDefaultValue(fieldMetadataInput.type); - - return { - id: v4(), - createdAt: new Date(), - updatedAt: new Date(), - name: fieldMetadataInput.name, - label: fieldMetadataInput.label, - icon: fieldMetadataInput.icon, - type: fieldMetadataInput.type, - isLabelSyncedWithName: fieldMetadataInput.isLabelSyncedWithName, - objectMetadataId: fieldMetadataInput.objectMetadataId, - workspaceId: fieldMetadataInput.workspaceId, - isNullable: generateNullable( - fieldMetadataInput.isNullable, - fieldMetadataInput.isRemoteCreation, - ), - relationTargetObjectMetadataId: - fieldMetadataInput?.relationCreationPayload?.targetObjectMetadataId, - defaultValue, - ...options, - isUnique: fieldMetadataInput.isUnique ?? false, - isActive: true, - isCustom: true, - settings: fieldMetadataInput.settings, - }; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util.ts index e032788da1..f6e1ccb143 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util.ts @@ -1,11 +1,31 @@ import { plainToInstance } from 'class-transformer'; -import { validateOrReject, type ValidationError } from 'class-validator'; +import { + IsEnum, + IsString, + IsUUID, + validateOrReject, + type ValidationError, +} from 'class-validator'; +import { RelationType } from 'twenty-shared/types'; import { FieldMetadataException, FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; -import { RelationCreationPayloadValidation } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; + +class RelationCreationPayloadValidation { + @IsUUID() + targetObjectMetadataId?: string; + + @IsString() + targetFieldLabel: string; + + @IsString() + targetFieldIcon: string; + + @IsEnum(RelationType) + type: RelationType; +} export const validateRelationCreationPayloadOrThrow = async ( relationCreationPayload: RelationCreationPayloadValidation, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts deleted file mode 100644 index 7ea4a42e58..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { - type ValidationArguments, - ValidatorConstraint, - type ValidatorConstraintInterface, -} from 'class-validator'; -import { type FieldMetadataType } from 'twenty-shared/types'; -import { Repository } from 'typeorm'; - -import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; - -import { LoggerService } from 'src/engine/core-modules/logger/logger.service'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util'; - -@Injectable() -@ValidatorConstraint({ name: 'isFieldMetadataDefaultValue', async: true }) -export class IsFieldMetadataDefaultValue - implements ValidatorConstraintInterface -{ - constructor( - @InjectRepository(FieldMetadataEntity) - private readonly fieldMetadataRepository: Repository, - private readonly loggerService: LoggerService, - ) {} - - async validate( - value: FieldMetadataDefaultValue, - args: ValidationArguments, - ): Promise { - // Try to extract type value from the object - // @ts-expect-error legacy noImplicitAny - let type: FieldMetadataType | null = args.object['type']; - - if (!type) { - // Extract id value from the instance, should happen only when updating - // @ts-expect-error legacy noImplicitAny - const id: string | undefined = args.instance?.['id']; - - if (!id) { - return false; - } - - let fieldMetadata: FieldMetadataEntity; - - try { - fieldMetadata = await this.fieldMetadataRepository.findOneOrFail({ - where: { - id, - }, - }); - } catch { - return false; - } - - type = fieldMetadata.type; - } - - const validationResult = validateDefaultValueForType(type, value); - - if (!validationResult.isValid) { - this.loggerService.error( - { - message: 'Error during field validation', - errors: validationResult.errors, - }, - 'Field Metadata Validation', - ); - } - - return validationResult.isValid; - } - - defaultMessage(): string { - return 'FieldMetadataDefaultValue is not valid'; - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator.ts deleted file mode 100644 index 6fcaf2bc2e..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { type ValidationArguments, ValidatorConstraint } from 'class-validator'; -import { - type FieldMetadataType, - type FieldMetadataOptions, -} from 'twenty-shared/types'; -import { Repository } from 'typeorm'; - -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { validateOptionsForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util'; - -@Injectable() -@ValidatorConstraint({ name: 'isFieldMetadataOptions', async: true }) -export class IsFieldMetadataOptions { - private validationErrors: string[] = []; - - constructor( - @InjectRepository(FieldMetadataEntity) - private readonly fieldMetadataRepository: Repository, - ) {} - - async validate( - value: FieldMetadataOptions, - args: ValidationArguments, - ): Promise { - // Try to extract type value from the object - // @ts-expect-error legacy noImplicitAny - let type: FieldMetadataType | null = args.object['type']; - - if (!type) { - // Extract id value from the instance, should happen only when updating - // @ts-expect-error legacy noImplicitAny - const id: string | undefined = args.instance?.['id']; - - if (!id) { - return false; - } - - let fieldMetadata: FieldMetadataEntity; - - try { - fieldMetadata = await this.fieldMetadataRepository.findOneOrFail({ - where: { id }, - }); - } catch { - return false; - } - - type = fieldMetadata.type; - } - - try { - return validateOptionsForType(type, value); - } catch (err) { - this.validationErrors.push(err.message); - - return false; - } - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts index 37939b4886..db087b92dd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts @@ -1,4 +1,5 @@ -import { t, msg } from '@lingui/core/macro'; +import { msg, t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type'; @@ -7,7 +8,30 @@ import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata- import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util'; import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util'; import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util'; + +type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = { + objectMetadataNameSingular: string; + objectMetadataNamePlural: string; + existingObjectMetadataId?: string; + objectMetadataMaps: FlatEntityMaps; +}; + +export const doesOtherObjectWithSameNameExists = ({ + objectMetadataMaps, + objectMetadataNamePlural, + objectMetadataNameSingular, + existingObjectMetadataId, +}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => + Object.values(objectMetadataMaps.byId) + .filter(isDefined) + .some( + (objectMetadata) => + (objectMetadata.nameSingular === objectMetadataNameSingular || + objectMetadata.namePlural === objectMetadataNamePlural || + objectMetadata.nameSingular === objectMetadataNamePlural || + objectMetadata.namePlural === objectMetadataNameSingular) && + objectMetadata.id !== existingObjectMetadataId, + ); export const validateFlatObjectMetadataNameAndLabels = ({ optimisticFlatObjectMetadataMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/__snapshots__/validate-object-metadata-input.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/__snapshots__/validate-object-metadata-input.util.spec.ts.snap deleted file mode 100644 index b78c1e3b55..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/__snapshots__/validate-object-metadata-input.util.spec.ts.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`validateObjectMetadataInputOrThrow should fail when name exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`; - -exports[`validateObjectMetadataInputOrThrow should fail when namePlural has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`; - -exports[`validateObjectMetadataInputOrThrow should fail when namePlural is a reserved keyword 1`] = `"The name "users" is not available"`; - -exports[`validateObjectMetadataInputOrThrow should fail when namePlural is an empty string 1`] = `"Input is too short: """`; - -exports[`validateObjectMetadataInputOrThrow should fail when namePlural is not camelCased 1`] = `"Not_Camel_Case should be in camelCase"`; - -exports[`validateObjectMetadataInputOrThrow should fail when nameSingular has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`; - -exports[`validateObjectMetadataInputOrThrow should fail when nameSingular is a reserved keyword 1`] = `"The name "user" is not available"`; - -exports[`validateObjectMetadataInputOrThrow should fail when nameSingular is an empty string 1`] = `"Input is too short: """`; - -exports[`validateObjectMetadataInputOrThrow should fail when nameSingular is not camelCased 1`] = `"Not_Camel_Case should be in camelCase"`; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/validate-object-metadata-input.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/validate-object-metadata-input.util.spec.ts deleted file mode 100644 index fe7369462b..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/validate-object-metadata-input.util.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; -import { type EachTestingContext } from 'twenty-shared/testing'; - -import { type UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; -import { validateObjectMetadataInputNamesOrThrow } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; - -type ValidateObjectNameTestingContext = EachTestingContext< - Partial ->; -const validateObjectMetadataTestCases: ValidateObjectNameTestingContext[] = [ - { - title: 'when nameSingular has invalid characters', - context: { nameSingular: 'μ' }, - }, - { - title: 'when namePlural has invalid characters', - context: { namePlural: 'μ' }, - }, - { - title: 'when nameSingular is a reserved keyword', - context: { nameSingular: 'user' }, - }, - { - title: 'when namePlural is a reserved keyword', - context: { namePlural: 'users' }, - }, - { - title: 'when nameSingular is not camelCased', - context: { nameSingular: 'Not_Camel_Case' }, - }, - { - title: 'when namePlural is not camelCased', - context: { namePlural: 'Not_Camel_Case' }, - }, - { - title: 'when namePlural is an empty string', - context: { namePlural: '' }, - }, - { - title: 'when nameSingular is an empty string', - context: { nameSingular: '' }, - }, - { - title: 'when name exceeds maximum length', - context: { nameSingular: 'a'.repeat(64) }, - }, -]; - -describe('validateObjectMetadataInputOrThrow should fail', () => { - it.each(validateObjectMetadataTestCases)('$title', ({ context }) => { - expect(() => - validateObjectMetadataInputNamesOrThrow( - getMockCreateObjectInput(context), - ), - ).toThrowErrorMatchingSnapshot(); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-name-label-and-description-for-foreign-key-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-name-label-and-description-for-foreign-key-field-metadata.util.ts deleted file mode 100644 index d6e4134a49..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-name-label-and-description-for-foreign-key-field-metadata.util.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const buildNameLabelAndDescriptionForForeignKeyFieldMetadata = ({ - targetObjectNameSingular, - targetObjectLabelSingular, - relatedObjectLabelSingular, -}: { - targetObjectNameSingular: string; - targetObjectLabelSingular: string; - relatedObjectLabelSingular: string; -}) => { - const name = `${targetObjectNameSingular}Id`; - const label = `${targetObjectLabelSingular} ID (foreign key)`; - const description = `${relatedObjectLabelSingular} ${targetObjectLabelSingular} id foreign key`; - - return { name, label, description }; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts deleted file mode 100644 index 04e08b0ae9..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { msg } from '@lingui/core/macro'; -import { isDefined } from 'twenty-shared/utils'; - -import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; -import { type UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; -import { validateMetadataNameIsNotTooLongOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils'; -import { validateMetadataNameIsNotTooShortOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils'; -import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils'; - -export const validateObjectMetadataInputNamesOrThrow = < - T extends UpdateObjectPayload | CreateObjectInput, ->({ - namePlural, - nameSingular, -}: T): void => - [namePlural, nameSingular].forEach((name) => { - if (!isDefined(name)) { - return; - } - validateObjectMetadataInputNameOrThrow(name); - }); - -export const validateObjectMetadataInputNameOrThrow = (name: string): void => { - try { - validateMetadataNameOrThrow(name); - } catch (error) { - if (error instanceof InvalidMetadataException) { - const errorMessage = error.message; - - throw new ObjectMetadataException( - errorMessage, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - { - userFriendlyMessage: msg`Invalid object metadata input`, - }, - ); - } - - throw error; - } -}; - -export const validateObjectMetadataInputLabelsOrThrow = < - T extends CreateObjectInput, ->({ - labelPlural, - labelSingular, -}: T): void => - [labelPlural, labelSingular].forEach((label) => - validateObjectMetadataInputLabelOrThrow(label), - ); - -const validateObjectMetadataInputLabelOrThrow = (name: string): void => { - const validators = [ - validateMetadataNameIsNotTooShortOrThrow, - validateMetadataNameIsNotTooLongOrThrow, - ]; - - try { - validators.forEach((validator) => validator(name.trim())); - } catch (error) { - if (error instanceof InvalidMetadataException) { - throw new ObjectMetadataException( - error.message, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - { - userFriendlyMessage: error.userFriendlyMessage, - }, - ); - } - - throw error; - } -}; - -type ValidateLowerCasedAndTrimmedStringAreDifferentOrThrowArgs = { - inputs: [string, string]; - message: string; -}; -export const validateLowerCasedAndTrimmedStringsAreDifferentOrThrow = ({ - message, - inputs: [firstString, secondString], -}: ValidateLowerCasedAndTrimmedStringAreDifferentOrThrowArgs) => { - if (firstString.trim().toLowerCase() === secondString.trim().toLowerCase()) { - throw new ObjectMetadataException( - message, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-field-name-availability.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-field-name-availability.spec.ts.snap deleted file mode 100644 index 53c01ab88b..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-field-name-availability.spec.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`validateFieldNameAvailabilityOrThrow throws error with ACTOR suffixes 1`] = `"Name "fieldActorName" is not available"`; - -exports[`validateFieldNameAvailabilityOrThrow throws error with ADDRESS suffixes 1`] = `"Name "fieldAddressAddressStreet1" is not available"`; - -exports[`validateFieldNameAvailabilityOrThrow throws error with CURRENCY suffixes 1`] = `"Name "fieldCurrencyAmountMicros" is not available"`; - -exports[`validateFieldNameAvailabilityOrThrow throws error with FULL_NAME suffixes 1`] = `"Name "fieldFullNameFirstName" is not available"`; - -exports[`validateFieldNameAvailabilityOrThrow throws error with LINKS suffixes 1`] = `"Name "fieldLinksPrimaryLinkLabel" is not available"`; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-metadata-name.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-metadata-name.spec.ts.snap deleted file mode 100644 index e9e8065f4b..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-metadata-name.spec.ts.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`validateMetadataNameOrThrow throw error when string is not in camel case 1`] = `"TestName should be in camelCase"`; - -exports[`validateMetadataNameOrThrow throws error when starts with digits 1`] = `"123string should be in camelCase"`; - -exports[`validateMetadataNameOrThrow throws error when string has non latin characters 1`] = `"String "בְרִבְרִ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`; - -exports[`validateMetadataNameOrThrow throws error when string has spaces 1`] = `"name with spaces should be in camelCase"`; - -exports[`validateMetadataNameOrThrow throws error when string is a reserved word 1`] = `"The name "role" is not available"`; - -exports[`validateMetadataNameOrThrow throws error when string is above 63 characters 1`] = `"Name is too long: it exceeds the 63 characters limit."`; - -exports[`validateMetadataNameOrThrow throws error when string is empty 1`] = `"Input is too short: """`; - -exports[`validateMetadataNameOrThrow throws error when string starts with capital letter 1`] = `"StringStartingWithCapitalLetter should be in camelCase"`; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/compute-metadata-name-from-label.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/compute-metadata-name-from-label.util.spec.ts index b932571c8d..142ee9991e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/compute-metadata-name-from-label.util.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/compute-metadata-name-from-label.util.spec.ts @@ -1,6 +1,6 @@ import { type EachTestingContext } from 'twenty-shared/testing'; -import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; +import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util'; import { InvalidMetadataException, InvalidMetadataExceptionCode, @@ -124,7 +124,7 @@ describe('computeMetadataNameFromLabel', () => { describe('successful cases', () => { it.each(successfulTestCases)('$title', ({ context }) => { - const result = computeMetadataNameFromLabel(context.input); + const result = computeMetadataNameFromLabelOrThrow(context.input); expect(result).toBe(context.expected); }); @@ -132,7 +132,7 @@ describe('computeMetadataNameFromLabel', () => { describe('failing cases', () => { it.each(failingTestCases)('$title', ({ context }) => { - expect(() => computeMetadataNameFromLabel(context.input)).toThrow( + expect(() => computeMetadataNameFromLabelOrThrow(context.input)).toThrow( context.expectToThrow?.error, ); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts deleted file mode 100644 index 99f43d24c7..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; -import { type EachTestingContext } from 'twenty-shared/testing'; - -import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; -import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; -import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; - -const FIELD_LINKS_MOCK_NAME = 'fieldLinks'; -const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency'; -const FIELD_ADDRESS_MOCK_NAME = 'fieldAddress'; -const FIELD_ACTOR_MOCK_NAME = 'fieldActor'; -const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName'; - -const objectMetadataId = '20202020-0000-0000-0000-000000000001'; -const workspaceId = '20202020-0000-0000-0000-000000000000'; - -const createFlatFieldMetadata = ( - id: string, - name: string, - type: FieldMetadataType, -): FlatFieldMetadata => { - return getFlatFieldMetadataMock({ - id, - name, - type, - objectMetadataId, - universalIdentifier: id, - workspaceId, - }); -}; - -const fieldLinksMock = createFlatFieldMetadata( - 'fieldLinksId', - FIELD_LINKS_MOCK_NAME, - FieldMetadataType.LINKS, -); -const fieldCurrencyMock = createFlatFieldMetadata( - 'fieldCurrencyId', - FIELD_CURRENCY_MOCK_NAME, - FieldMetadataType.CURRENCY, -); -const fieldFullNameMock = createFlatFieldMetadata( - 'fieldFullNameId', - FIELD_FULL_NAME_MOCK_NAME, - FieldMetadataType.FULL_NAME, -); -const fieldActorMock = createFlatFieldMetadata( - 'fieldActorId', - FIELD_ACTOR_MOCK_NAME, - FieldMetadataType.ACTOR, -); -const fieldAddressMock = createFlatFieldMetadata( - 'fieldAddressId', - FIELD_ADDRESS_MOCK_NAME, - FieldMetadataType.ADDRESS, -); - -const FIELDS_MOCK = [ - fieldLinksMock, - fieldCurrencyMock, - fieldFullNameMock, - fieldActorMock, - fieldAddressMock, -]; - -const flatFieldMetadataMaps: FlatEntityMaps = - FIELDS_MOCK.reduce( - (acc, field) => ({ - ...acc, - byId: { - ...acc.byId, - [field.id]: field, - }, - }), - createEmptyFlatEntityMaps() as FlatEntityMaps, - ); - -const flatObjectMetadata = getFlatObjectMetadataMock({ - id: objectMetadataId, - workspaceId, - nameSingular: 'objectName', - namePlural: 'objectNames', - labelSingular: 'Object Name', - labelPlural: 'Object Names', - description: 'Object description', - icon: 'Icon123', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - fieldMetadataIds: FIELDS_MOCK.map((f) => f.id), - indexMetadataIds: [], - viewIds: [], - universalIdentifier: objectMetadataId, - applicationId: null, - labelIdentifierFieldMetadataId: null, - imageIdentifierFieldMetadataId: null, - shortcut: null, - isLabelSyncedWithName: true, - standardId: null, - standardOverrides: null, - targetTableName: 'DEPRECATED', - duplicateCriteria: null, -}); - -type ValidateFieldNameAvailabilityTestContext = EachTestingContext<{ - input: string; - shouldNotThrow?: true; -}>; - -const validateFieldNameAvailabilityTestCases: ValidateFieldNameAvailabilityTestContext[] = - [ - { - title: 'does not throw if name is not reserved', - context: { - input: 'testName', - shouldNotThrow: true, - }, - }, - { - title: 'throws error with LINKS suffixes', - context: { - input: `${FIELD_LINKS_MOCK_NAME}PrimaryLinkLabel`, - }, - }, - { - title: 'throws error with CURRENCY suffixes', - context: { - input: `${FIELD_CURRENCY_MOCK_NAME}AmountMicros`, - }, - }, - { - title: 'throws error with FULL_NAME suffixes', - context: { - input: `${FIELD_FULL_NAME_MOCK_NAME}FirstName`, - }, - }, - { - title: 'throws error with ACTOR suffixes', - context: { - input: `${FIELD_ACTOR_MOCK_NAME}Name`, - }, - }, - { - title: 'throws error with ADDRESS suffixes', - context: { - input: `${FIELD_ADDRESS_MOCK_NAME}AddressStreet1`, - }, - }, - ]; - -describe('validateFieldNameAvailabilityOrThrow', () => { - it.each(validateFieldNameAvailabilityTestCases)( - '$title', - ({ context: { input, shouldNotThrow } }) => { - if (shouldNotThrow) { - expect(() => - validateFieldNameAvailabilityOrThrow({ - name: input, - flatObjectMetadata, - flatFieldMetadataMaps, - }), - ).not.toThrow(); - } else { - expect(() => - validateFieldNameAvailabilityOrThrow({ - name: input, - flatObjectMetadata, - flatFieldMetadataMaps, - }), - ).toThrowErrorMatchingSnapshot(); - } - }, - ); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-metadata-name.spec.ts b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-metadata-name.spec.ts deleted file mode 100644 index 6a969d7e32..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-metadata-name.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { type EachTestingContext } from 'twenty-shared/testing'; - -import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils'; - -type ValidateMetadataNameTestContext = EachTestingContext<{ - input: string; - shouldNotThrow?: true; -}>; - -const validateMetadataNameTestCases: ValidateMetadataNameTestContext[] = [ - { - title: 'validates when string is valid', - context: { - input: 'testName', - shouldNotThrow: true, - }, - }, - { - title: 'throw error when string is not in camel case', - context: { - input: 'TestName', - }, - }, - { - title: 'throws error when string has spaces', - context: { - input: 'name with spaces', - }, - }, - { - title: 'throws error when string is a reserved word', - context: { - input: 'role', - }, - }, - { - title: 'throws error when string starts with capital letter', - context: { - input: 'StringStartingWithCapitalLetter', - }, - }, - { - title: 'throws error when string has non latin characters', - context: { - input: 'בְרִבְרִ', - }, - }, - { - title: 'throws error when starts with digits', - context: { - input: '123string', - }, - }, - { - title: 'validates when string is less than 63 characters', - context: { - input: 'a'.repeat(63), - shouldNotThrow: true, - }, - }, - { - title: 'throws error when string is above 63 characters', - context: { - input: 'a'.repeat(64), - }, - }, - { - title: 'throws error when string is empty', - context: { - input: '', - }, - }, -]; - -describe('validateMetadataNameOrThrow', () => { - it.each(validateMetadataNameTestCases)( - '$title', - ({ context: { input, shouldNotThrow } }) => { - if (shouldNotThrow) { - expect(() => validateMetadataNameOrThrow(input)).not.toThrow(); - } else { - expect(() => - validateMetadataNameOrThrow(input), - ).toThrowErrorMatchingSnapshot(); - } - }, - ); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util.ts similarity index 65% rename from packages/twenty-server/src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util.ts rename to packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util.ts index 601ee48583..e4f5405e7d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util.ts @@ -8,7 +8,7 @@ import { // Server-specific wrapper that converts generic errors to InvalidMetadataException // This provides consistent error handling with proper exception codes for the server -export const computeMetadataNameFromLabel = (label: string): string => { +export const computeMetadataNameFromLabelOrThrow = (label: string): string => { if (!isDefined(label)) { throw new InvalidMetadataException( 'Label is required', @@ -28,20 +28,3 @@ export const computeMetadataNameFromLabel = (label: string): string => { throw error; } }; - -export const validateNameAndLabelAreSyncOrThrow = ({ - label, - name, -}: { - label: string; - name: string; -}) => { - const computedName = computeMetadataNameFromLabel(label); - - if (name !== computedName) { - throw new InvalidMetadataException( - `Name is not synced with label. Expected name: "${computedName}", got ${name}`, - InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants.ts b/packages/twenty-server/src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants.ts index 9351c4e426..0a0d5940a1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants.ts @@ -7,7 +7,9 @@ import { beneathDatabaseIdentifierMinimumLength, exceedsDatabaseIdentifierMaximumLength, } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; -import { STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX } from 'src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils'; + +const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX = + /^[a-z][a-zA-Z0-9]*$/; export const METADATA_NAME_VALIDATORS: FlatMetadataValidator[] = [ { diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util.ts deleted file mode 100644 index 7587fa1861..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { isDefined } from 'twenty-shared/utils'; -export const isArgDefinedIfProvidedOrThrow = ({ - input, - key, - value, -}: { - input: object; - key: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any; -}) => { - if (key in input && !isDefined(value)) { - throw new Error(`${key} must be defined when provided`); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts deleted file mode 100644 index b56c778388..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { msg } from '@lingui/core/macro'; -import { - FieldMetadataType, - compositeTypeDefinitions, -} from 'twenty-shared/types'; - -import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { - InvalidMetadataException, - InvalidMetadataExceptionCode, -} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; - -const getReservedCompositeFieldNames = ( - flatObjectMetadata: FlatObjectMetadata, - flatFieldMetadataMaps: FlatEntityMaps, -) => { - const reservedCompositeFieldsNames: string[] = []; - - for (const fieldId of flatObjectMetadata.fieldMetadataIds) { - const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatFieldMetadataMaps, - flatEntityId: fieldId, - }); - - if (isCompositeFieldMetadataType(field.type)) { - const base = field.name; - const compositeType = compositeTypeDefinitions.get(field.type); - - compositeType?.properties.map((property) => - reservedCompositeFieldsNames.push( - computeCompositeColumnName(base, property), - ), - ); - } - } - - return reservedCompositeFieldsNames; -}; - -type ValidateFieldNameAvailabilityOrThrowArgs = { - name: string; - flatObjectMetadata: FlatObjectMetadata; - flatFieldMetadataMaps: FlatEntityMaps; -}; -export const validateFieldNameAvailabilityOrThrow = ({ - name, - flatObjectMetadata, - flatFieldMetadataMaps, -}: ValidateFieldNameAvailabilityOrThrowArgs) => { - const reservedCompositeFieldsNames = getReservedCompositeFieldNames( - flatObjectMetadata, - flatFieldMetadataMaps, - ); - - for (const fieldId of flatObjectMetadata.fieldMetadataIds) { - const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatFieldMetadataMaps, - flatEntityId: fieldId, - }); - - if ( - field.name === name || - (field.type === FieldMetadataType.RELATION && `${field.name}Id` === name) - ) { - throw new InvalidMetadataException( - `Name "${name}" is not available as it is already used by another field`, - InvalidMetadataExceptionCode.NOT_AVAILABLE, - { - userFriendlyMessage: msg`This name is not available as it is already used by another field.`, - }, - ); - } - } - - if (reservedCompositeFieldsNames.includes(name)) { - throw new InvalidMetadataException( - `Name "${name}" is not available`, - InvalidMetadataExceptionCode.RESERVED_KEYWORD, - { - userFriendlyMessage: msg`This name is not available.`, - }, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-identifier-field-metadata-id.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-identifier-field-metadata-id.utils.ts deleted file mode 100644 index 6037637c67..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-identifier-field-metadata-id.utils.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - isDefined, - isLabelIdentifierFieldMetadataTypes, -} from 'twenty-shared/utils'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; - -type Validator = { - validator: (args: { - fieldMetadataId: string; - matchingFieldMetadata?: FieldMetadataEntity; - }) => boolean; - label: string; -}; - -type ValidateMetadataIdentifierFieldMetadataIdOrThrowArgs = { - fieldMetadataId: string; - fieldMetadataItems: FieldMetadataEntity[]; - validators: Validator[]; -}; -const validatorRunner = ({ - fieldMetadataId, - fieldMetadataItems, - validators, -}: ValidateMetadataIdentifierFieldMetadataIdOrThrowArgs): void => { - const matchingFieldMetadata = fieldMetadataItems.find( - (fieldMetadata) => fieldMetadata.id === fieldMetadataId, - ); - - validators.forEach(({ label, validator }) => { - if (validator({ fieldMetadataId, matchingFieldMetadata })) { - throw new ObjectMetadataException( - label, - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ); - } - }); -}; - -type ValidateMetadataIdentifierFieldMetadataIdsArgs = { - labelIdentifierFieldMetadataId: string | undefined; - imageIdentifierFieldMetadataId: string | undefined; - fieldMetadataItems: FieldMetadataEntity[]; -}; -export const validateMetadataIdentifierFieldMetadataIds = ({ - imageIdentifierFieldMetadataId, - labelIdentifierFieldMetadataId, - fieldMetadataItems, -}: ValidateMetadataIdentifierFieldMetadataIdsArgs) => { - const isMatchingFieldMetadataDefined: Validator['validator'] = ({ - matchingFieldMetadata, - }) => !isDefined(matchingFieldMetadata); - - if (isDefined(labelIdentifierFieldMetadataId)) { - validatorRunner({ - fieldMetadataId: labelIdentifierFieldMetadataId, - fieldMetadataItems, - validators: [ - { - validator: isMatchingFieldMetadataDefined, - label: - 'labelIdentifierFieldMetadataId validation failed: related field metadata not found', - }, - { - validator: ({ matchingFieldMetadata }) => - isDefined(matchingFieldMetadata) && - !isLabelIdentifierFieldMetadataTypes(matchingFieldMetadata.type), - label: - 'labelIdentifierFieldMetadataId validation failed: it must be a TEXT or FULL_NAME field metadata type id', - }, - ], - }); - } - - if (isDefined(imageIdentifierFieldMetadataId)) { - validatorRunner({ - fieldMetadataId: imageIdentifierFieldMetadataId, - fieldMetadataItems, - validators: [ - { - validator: isMatchingFieldMetadataDefined, - label: - 'imageIdentifierFieldMetadataId validation failed: related field metadata not found', - }, - ], - }); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-camel-case.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-camel-case.utils.ts deleted file mode 100644 index 6b2a442609..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-camel-case.utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { t } from '@lingui/core/macro'; -import camelCase from 'lodash.camelcase'; - -import { - InvalidMetadataException, - InvalidMetadataExceptionCode, -} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; - -export const validateMetadataNameIsCamelCaseOrThrow = (name: string) => { - if (name !== camelCase(name)) { - throw new InvalidMetadataException( - t`${name} should be in camelCase`, - InvalidMetadataExceptionCode.NOT_CAMEL_CASE, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts deleted file mode 100644 index c7e00df9b3..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { msg } from '@lingui/core/macro'; -import { RESERVED_METADATA_NAME_KEYWORDS } from 'twenty-shared/metadata'; - -import { - InvalidMetadataException, - InvalidMetadataExceptionCode, -} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; - -export const validateMetadataNameIsNotReservedKeywordOrThrow = ( - name: string, -) => { - if (RESERVED_METADATA_NAME_KEYWORDS.includes(name)) { - throw new InvalidMetadataException( - `The name "${name}" is not available`, - InvalidMetadataExceptionCode.RESERVED_KEYWORD, - { - userFriendlyMessage: msg`This name is not available.`, - }, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils.ts deleted file mode 100644 index d3486ca9f4..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { t } from '@lingui/core/macro'; - -import { - InvalidMetadataException, - InvalidMetadataExceptionCode, -} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; -import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; - -export const validateMetadataNameIsNotTooLongOrThrow = (name: string) => { - if (exceedsDatabaseIdentifierMaximumLength(name)) { - throw new InvalidMetadataException( - t`Name is too long: it exceeds the 63 characters limit.`, - InvalidMetadataExceptionCode.EXCEEDS_MAX_LENGTH, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils.ts deleted file mode 100644 index 5dd27edc48..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { t } from '@lingui/core/macro'; - -import { - InvalidMetadataException, - InvalidMetadataExceptionCode, -} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; -import { beneathDatabaseIdentifierMinimumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; - -export const validateMetadataNameIsNotTooShortOrThrow = (name: string) => { - if (beneathDatabaseIdentifierMinimumLength(name)) { - throw new InvalidMetadataException( - t`Input is too short: "${name}"`, - InvalidMetadataExceptionCode.INPUT_TOO_SHORT, - ); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils.ts deleted file mode 100644 index 663bb02911..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { validateMetadataNameIsCamelCaseOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-camel-case.utils'; -import { validateMetadataNameIsNotReservedKeywordOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword'; -import { validateMetadataNameIsNotTooLongOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils'; -import { validateMetadataNameIsNotTooShortOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils'; -import { validateMetadataNameStartWithLowercaseLetterAndContainDigitsNorLettersOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils'; - -export const validateMetadataNameOrThrow = (name: string): void => { - const validators = [ - validateMetadataNameIsNotTooLongOrThrow, - validateMetadataNameIsNotTooShortOrThrow, - validateMetadataNameIsCamelCaseOrThrow, - validateMetadataNameStartWithLowercaseLetterAndContainDigitsNorLettersOrThrow, - validateMetadataNameIsNotReservedKeywordOrThrow, - ]; - - validators.forEach((validator) => validator(name)); -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts deleted file mode 100644 index 9915605947..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { t } from '@lingui/core/macro'; - -import { - InvalidMetadataException, - InvalidMetadataExceptionCode, -} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; - -export const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX = - /^[a-z][a-zA-Z0-9]*$/; - -export const validateMetadataNameStartWithLowercaseLetterAndContainDigitsNorLettersOrThrow = - (name: string) => { - if ( - !name.match( - STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX, - ) - ) { - throw new InvalidMetadataException( - t`String "${name}" is not valid: must start with lowercase letter and contain only alphanumeric letters`, - InvalidMetadataExceptionCode.INVALID_STRING, - ); - } - }; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts deleted file mode 100644 index b222d2f291..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { msg } from '@lingui/core/macro'; -import { isDefined } from 'twenty-shared/utils'; - -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; - -type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = { - objectMetadataNameSingular: string; - objectMetadataNamePlural: string; - existingObjectMetadataId?: string; - objectMetadataMaps: FlatEntityMaps; -}; - -export const doesOtherObjectWithSameNameExists = ({ - objectMetadataMaps, - objectMetadataNamePlural, - objectMetadataNameSingular, - existingObjectMetadataId, -}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => - Object.values(objectMetadataMaps.byId) - .filter(isDefined) - .some( - (objectMetadata) => - (objectMetadata.nameSingular === objectMetadataNameSingular || - objectMetadata.namePlural === objectMetadataNamePlural || - objectMetadata.nameSingular === objectMetadataNamePlural || - objectMetadata.namePlural === objectMetadataNameSingular) && - objectMetadata.id !== existingObjectMetadataId, - ); - -export const validatesNoOtherObjectWithSameNameExistsOrThrows = ( - args: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams, -) => { - const objectAlreadyExists = doesOtherObjectWithSameNameExists(args); - - if (objectAlreadyExists) { - throw new ObjectMetadataException( - 'Object already exists', - ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, - { - userFriendlyMessage: msg`Object already exists`, - }, - ); - } -};