Handle input transpilation morph relation v2 (#14124)

# Introduction
From `morphRelationCreationPayload` to `flatFieldMetadatas`

close https://github.com/twentyhq/core-team-issues/issues/1406

## Unit testing
I'm not a fan of covering things this way, where we could have some
strong integration testing tests in the first place
But I wanted to freeze the optimistic path computation, also covered the
exception because it was cheap, doesn't mean I won't cover them through
integration tests later anw
This commit is contained in:
Paul Rastoin
2025-08-28 17:22:03 +02:00
committed by GitHub
parent e264d7f32b
commit 7c481abf0c
14 changed files with 1581 additions and 298 deletions
@@ -0,0 +1,122 @@
import { type RelationCreationPayload } from 'twenty-shared/types';
import { t } from '@lingui/core/macro';
import { isDefined } from 'class-validator';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import {
type FailedFieldInputTranspilation,
type SuccessfulFieldInputTranspilation,
type FieldInputTranspilationResult,
} 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';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
type RelationCreationPayloadAndObjectMetadata = {
relationCreationPayload: RelationCreationPayload;
targetFlatObjectMetadata: FlatObjectMetadata;
};
type ValidateMorphRelationCreationPayloadUtilArgs = {
morphRelationCreationPayload: RelationCreationPayload[];
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
};
export const validateMorphRelationCreationPayload = async ({
existingFlatObjectMetadataMaps,
morphRelationCreationPayload,
}: ValidateMorphRelationCreationPayloadUtilArgs): Promise<
FieldInputTranspilationResult<RelationCreationPayloadAndObjectMetadata[]>
> => {
const allRelationType = [
...new Set(
morphRelationCreationPayload.map(
(relationCreationPayload) => relationCreationPayload.type,
),
),
];
if (allRelationType.length > 1) {
return {
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`,
},
};
}
const allRelatedObjectMetadataIds = morphRelationCreationPayload.map(
(relationCreationPayload) => relationCreationPayload.targetObjectMetadataId,
);
const allRelatedObjectMetadataIdsSet = [
...new Set(allRelatedObjectMetadataIds),
];
if (
allRelatedObjectMetadataIds.length !== allRelatedObjectMetadataIdsSet.length
) {
return {
status: 'fail',
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`,
},
};
}
const relationCreationPayloadReport: {
success: SuccessfulFieldInputTranspilation<RelationCreationPayloadAndObjectMetadata>[];
failed: FailedFieldInputTranspilation[];
} = {
success: [],
failed: [],
};
for (const rawRelationCreationPayload of morphRelationCreationPayload) {
const relationValidationResult = await validateRelationCreationPayload({
existingFlatObjectMetadataMaps,
relationCreationPayload: rawRelationCreationPayload,
});
if (relationValidationResult.status === 'fail') {
relationCreationPayloadReport.failed.push(relationValidationResult);
continue;
}
const { relationCreationPayload, targetFlatObjectMetadata } =
relationValidationResult.result;
relationCreationPayloadReport.success.push({
status: 'success',
result: {
relationCreationPayload,
targetFlatObjectMetadata,
},
});
}
if (relationCreationPayloadReport.failed.length > 0) {
return {
status: 'fail',
error: {
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message: 'Morph relation input transpilation failed',
userFriendlyMessage: t`Invalid morph relation input`,
value: relationCreationPayloadReport.failed
.map((failedTranspilation) => failedTranspilation.error.value)
.filter(isDefined),
},
};
}
return {
status: 'success',
result: relationCreationPayloadReport.success.map(
(relationCreationPayloadValidation) =>
relationCreationPayloadValidation.result,
),
};
};
@@ -0,0 +1,82 @@
import { t } from '@lingui/core/macro';
import { type RelationCreationPayload } from 'twenty-shared/types';
import {
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { validateRelationCreationPayloadOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-with-flat-field-maps-to-flat-object-metadatas.util';
type ValidateRelationCreationPayloadUtilArgs = {
relationCreationPayload: RelationCreationPayload;
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
};
export const validateRelationCreationPayload = async ({
existingFlatObjectMetadataMaps,
relationCreationPayload: rawRelationCreationPayload,
}: ValidateRelationCreationPayloadUtilArgs): Promise<
FieldInputTranspilationResult<{
relationCreationPayload: RelationCreationPayload;
targetFlatObjectMetadata: FlatObjectMetadata;
}>
> => {
const relationCreationPayload =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawRelationCreationPayload,
['targetFieldIcon', 'targetFieldLabel', 'targetObjectMetadataId', 'type'],
);
try {
await validateRelationCreationPayloadOrThrow(relationCreationPayload);
} catch (error) {
if (error instanceof FieldMetadataException) {
return {
status: 'fail',
error: {
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message: `Relation creation payload is invalid ${JSON.stringify(relationCreationPayload)}`,
userFriendlyMessage: t`Invalid relation creation payload`,
value: relationCreationPayload,
},
};
} else {
throw error;
}
}
const targetFlatObjectMetadataWithFlatFieldMaps =
existingFlatObjectMetadataMaps.byId[
relationCreationPayload.targetObjectMetadataId
];
if (!isDefined(targetFlatObjectMetadataWithFlatFieldMaps)) {
return {
status: 'fail',
error: {
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message: `Object metadata relation target not found for relation creation payload`,
userFriendlyMessage: t`Object targeted by field to create not found`,
value: relationCreationPayload,
},
};
}
return {
status: 'success',
result: {
relationCreationPayload,
targetFlatObjectMetadata:
fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata(
targetFlatObjectMetadataWithFlatFieldMaps,
),
},
};
};