From b364acf5bbad930a90d7ce97bbdbe8b0efc64114 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Thu, 28 Aug 2025 18:05:58 +0200 Subject: [PATCH] Morph relation validation v2 (#14141) ## Introduction Binding morph relation validation to relation validation code flow Please note that ALL create field will go through basic atomic validation, which involve name availability and so on close https://github.com/twentyhq/core-team-issues/issues/1407 --- ...lat-field-metadatas-to-create.spec.ts.snap | 15 ++- ...t-field-metadata-type-validator.service.ts | 100 +++++++++++------- ...ield-input-to-flat-field-metadatas.util.ts | 3 +- ...m-validate-one-field-metadata-args.util.ts | 13 --- ...te-morph-relation-creation-payload.util.ts | 24 +++-- ...orph-relation-flat-field-metadata.util.ts} | 6 +- 6 files changed, 91 insertions(+), 70 deletions(-) delete mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-enum-validate-one-field-metadata-args.util.ts rename packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/{validate-relation-flat-field-metadata.util.ts => validate-relation-or-morph-relation-flat-field-metadata.util.ts} (90%) diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap index b670f504bc5..6123a63ed84 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap @@ -4,8 +4,8 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su { "error": { "code": "FIELD_METADATA_RELATION_MALFORMED", - "message": "Morph relation relations must have the same relation type", - "userFriendlyMessage": "Morph relation relations must have the same relation type", + "message": "Morph relation creation payloads must have the same relation type", + "userFriendlyMessage": "Morph relation creation payloads must have the same relation type", }, "status": "fail", } @@ -32,8 +32,8 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su { "error": { "code": "FIELD_METADATA_RELATION_MALFORMED", - "message": "Morph relation relations must have only relation to the same object metadata", - "userFriendlyMessage": "Morph relation relations must have only relation to the same object metadata", + "message": "Morph relation creation payloads must have only relation to the same object metadata", + "userFriendlyMessage": "Morph relation creation payloads must only contain relation to the same object metadata", }, "status": "fail", } @@ -42,10 +42,9 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test suite Failure cases should fail when morphRelationsCreationPayload is empty array 1`] = ` { "error": { - "code": "INVALID_FIELD_INPUT", - "message": "Relation creation payload is required", - "userFriendlyMessage": "Relation creation payload is required", - "value": [], + "code": "FIELD_METADATA_RELATION_MALFORMED", + "message": "Morph relation creation payloads are empty", + "userFriendlyMessage": "At least one relation is require", }, "status": "fail", } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts index 62335eca695..d45fcbac083 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts @@ -6,16 +6,12 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service'; import { type FlatFieldMetadataTypeValidator } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type'; import { FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; -import { isEnumValidateOneFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-validate-one-field-metadata-args.util'; import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util'; -import { validateRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util'; +import { validateRelationOrMorphRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-or-morph-relation-flat-field-metadata.util'; @Injectable() export class FlatFieldMetadataTypeValidatorService { constructor(private readonly featureFlagService: FeatureFlagService) {} @@ -52,7 +48,12 @@ export class FlatFieldMetadataTypeValidatorService { LINKS: async (_args) => { return []; }, - MORPH_RELATION: async ({ workspaceId }) => { + MORPH_RELATION: async ({ + workspaceId, + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + otherFlatObjectMetadataMapsToValidate, + }) => { const isMorphRelationEnabled = await this.featureFlagService.isFeatureEnabled( FeatureFlagKey.IS_MORPH_RELATION_ENABLED, @@ -69,18 +70,26 @@ export class FlatFieldMetadataTypeValidatorService { ]; } - return []; + return validateRelationOrMorphRelationFlatFieldMetadata({ + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }); }, - MULTI_SELECT: (args) => { - if (!isEnumValidateOneFieldMetadata(args)) { - throw new FieldMetadataException( - 'Should never occur, invalid enum field metadata type', - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } + MULTI_SELECT: ({ + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }) => + validateEnumSelectFlatFieldMetadata({ + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }), - return validateEnumSelectFlatFieldMetadata(args); - }, NUMBER: async (_args) => { return []; }, @@ -93,36 +102,51 @@ export class FlatFieldMetadataTypeValidatorService { POSITION: async (_args) => { return []; }, - RATING: (args) => { - if (!isEnumValidateOneFieldMetadata(args)) { - throw new FieldMetadataException( - 'Should never occur, invalid enum field metadata type', - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - return validateEnumSelectFlatFieldMetadata(args); - }, + RATING: ({ + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }) => + validateEnumSelectFlatFieldMetadata({ + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }), RAW_JSON: async (_args) => { return []; }, - RELATION: validateRelationFlatFieldMetadata, + RELATION: ({ + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }) => + validateRelationOrMorphRelationFlatFieldMetadata({ + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }), RICH_TEXT: async (_args) => { return []; }, RICH_TEXT_V2: async (_args) => { return []; }, - SELECT: (args) => { - if (!isEnumValidateOneFieldMetadata(args)) { - throw new FieldMetadataException( - 'Should never occur, invalid enum field metadata type', - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - return validateEnumSelectFlatFieldMetadata(args); - }, + SELECT: ({ + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }) => + validateEnumSelectFlatFieldMetadata({ + existingFlatObjectMetadataMaps, + flatFieldMetadataToValidate, + workspaceId, + otherFlatObjectMetadataMapsToValidate, + }), TEXT: async (_args) => { return []; }, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts index a0b46882060..4e5fc051918 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts @@ -35,8 +35,7 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({ if ( !isDefined(rawMorphCreationPayload) || - !Array.isArray(rawMorphCreationPayload) || - rawMorphCreationPayload.length === 0 + !Array.isArray(rawMorphCreationPayload) ) { return { status: 'fail', diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-enum-validate-one-field-metadata-args.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-enum-validate-one-field-metadata-args.util.ts deleted file mode 100644 index 8b30ba39e8b..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-enum-validate-one-field-metadata-args.util.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - type EnumFieldMetadataType, - type FieldMetadataType, -} from 'twenty-shared/types'; - -import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { type ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service'; - -export const isEnumValidateOneFieldMetadata = ( - args: ValidateOneFieldMetadataArgs, -): args is ValidateOneFieldMetadataArgs & - ValidateOneFieldMetadataArgs => - isEnumFieldMetadataType(args.flatFieldMetadataToValidate.type); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-creation-payload.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-creation-payload.util.ts index f3e788a602a..2d5d077ca2c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-creation-payload.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-creation-payload.util.ts @@ -1,12 +1,12 @@ -import { type RelationCreationPayload } from 'twenty-shared/types'; import { t } from '@lingui/core/macro'; import { isDefined } from 'class-validator'; +import { type RelationCreationPayload } from 'twenty-shared/types'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { type FailedFieldInputTranspilation, - type SuccessfulFieldInputTranspilation, type FieldInputTranspilationResult, + type SuccessfulFieldInputTranspilation, } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; import { validateRelationCreationPayload } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-creation-payload.util'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; @@ -27,6 +27,17 @@ export const validateMorphRelationCreationPayload = async ({ }: ValidateMorphRelationCreationPayloadUtilArgs): Promise< FieldInputTranspilationResult > => { + if (morphRelationCreationPayload.length === 0) { + return { + status: 'fail', + error: { + code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, + message: 'Morph relation creation payloads are empty', + userFriendlyMessage: t`At least one relation is require`, + }, + }; + } + const allRelationType = [ ...new Set( morphRelationCreationPayload.map( @@ -40,8 +51,9 @@ export const validateMorphRelationCreationPayload = async ({ status: 'fail', error: { code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, - message: 'Morph relation relations must have the same relation type', - userFriendlyMessage: t`Morph relation relations must have the same relation type`, + message: + 'Morph relation creation payloads must have the same relation type', + userFriendlyMessage: t`Morph relation creation payloads must have the same relation type`, }, }; } @@ -61,8 +73,8 @@ export const validateMorphRelationCreationPayload = async ({ error: { code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, message: - 'Morph relation relations must have only relation to the same object metadata', - userFriendlyMessage: t`Morph relation relations must have only relation to the same object metadata`, + 'Morph relation creation payloads must have only relation to the same object metadata', + userFriendlyMessage: t`Morph relation creation payloads must only contain relation to the same object metadata`, }, }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-or-morph-relation-flat-field-metadata.util.ts similarity index 90% rename from packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-or-morph-relation-flat-field-metadata.util.ts index ea55c1ffcc2..a8f7101cf68 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-or-morph-relation-flat-field-metadata.util.ts @@ -1,16 +1,16 @@ import { t } from '@lingui/core/macro'; -import { type FieldMetadataType } from 'twenty-shared/types'; import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type RelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/relation-field-metadata-type.type'; import { type ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service'; import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; -export const validateRelationFlatFieldMetadata = async ({ +export const validateRelationOrMorphRelationFlatFieldMetadata = async ({ existingFlatObjectMetadataMaps, flatFieldMetadataToValidate, otherFlatObjectMetadataMapsToValidate, -}: ValidateOneFieldMetadataArgs): Promise< +}: ValidateOneFieldMetadataArgs): Promise< FlatFieldMetadataValidationError[] > => { const { relationTargetFieldMetadataId, relationTargetObjectMetadataId } =