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
This commit is contained in:
Paul Rastoin
2025-08-28 16:05:58 +00:00
committed by GitHub
parent af6fc1e9ef
commit b364acf5bb
6 changed files with 91 additions and 70 deletions
@@ -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",
}
@@ -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 [];
},
@@ -35,8 +35,7 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
if (
!isDefined(rawMorphCreationPayload) ||
!Array.isArray(rawMorphCreationPayload) ||
rawMorphCreationPayload.length === 0
!Array.isArray(rawMorphCreationPayload)
) {
return {
status: 'fail',
@@ -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 = <T extends FieldMetadataType>(
args: ValidateOneFieldMetadataArgs<T>,
): args is ValidateOneFieldMetadataArgs<T> &
ValidateOneFieldMetadataArgs<EnumFieldMetadataType> =>
isEnumFieldMetadataType(args.flatFieldMetadataToValidate.type);
@@ -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<RelationCreationPayloadAndObjectMetadata[]>
> => {
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`,
},
};
}
@@ -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<FieldMetadataType.RELATION>): Promise<
}: ValidateOneFieldMetadataArgs<RelationFieldMetadataType>): Promise<
FlatFieldMetadataValidationError[]
> => {
const { relationTargetFieldMetadataId, relationTargetObjectMetadataId } =