Refactor builder to embed Object validation create/delete/update (#13934)

# Introduction
Moving validation directly in the builder that has the perfect
granularity to do it.
When importing we won't have to infer and dispatch on the operation
nature ( update delete create ) and validate accordingly

## Objects
Only migrated object validation for the moment even though create object
involves a validate flat field metadata creation call too

## TODO
- improve `otherFlatObjectMetadataMapsToValidate` naming too vague

## Next
- handle fields validation within fields actions build
- Unit test coverage validation issue on builder and validate
- integration test plugging with new feature flag

## Manual tested
- Update
- Delete
- Create TODO
This commit is contained in:
Paul Rastoin
2025-08-18 11:16:16 +02:00
committed by GitHub
parent 18b2a68b69
commit 6629092645
51 changed files with 3800 additions and 617 deletions
@@ -1,4 +1,4 @@
import { ALL_FLAT_OBJECT_METADATA_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/all-flat-object.mock';
import { ALL_FLAT_OBJECT_METADATA_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/all-flat-object-metadatas.mock';
import { fromFlatObjectMetadatasToFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadatas-to-flat-object-metadata-maps.util';
export const FLAT_OBJECT_METADATA_MAPS_MOCKS =
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatasOrThrow should throw when object metadata id is not found 1`] = `"getSubFlatObjectMetadataMapsOfSpecificFieldsOrThrow object metadata not found"`;
@@ -0,0 +1,119 @@
import { jestExpectToBeDefined } from 'test/utils/expect-to-be-defined.util.test';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { PET_FLAT_FIELDS_MOCK } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/pet-flat-fields.mock';
import { ROCKET_FLAT_FIELDS_MOCK } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/rocket-flat-fields.mock';
import { FLAT_OBJECT_METADATA_MAPS_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata-maps/mocks/flat-object-metadata-maps.mock';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import {
type GetSubFlatObjectMetadataMapsOfSpecificFieldsOrThrowArgs,
getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatasOrThrow,
} from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-out-of-flat-field-metadatas-or-throw.util';
import { PET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock';
import { ROCKET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/rocket-flat-object.mock';
import { fromFlatObjectMetadatasToFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadatas-to-flat-object-metadata-maps.util';
type GetSubObjectMetadataMapsOrThrowTestCase = {
input: GetSubFlatObjectMetadataMapsOfSpecificFieldsOrThrowArgs;
shouldThrow?: true;
expected?: FlatObjectMetadataMaps;
};
describe('getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatasOrThrow', () => {
const testCases: EachTestingContext<GetSubObjectMetadataMapsOrThrowTestCase>[] =
[
{
title: 'should throw when object metadata id is not found',
context: {
input: {
flatFieldMetadatas: [
getFlatFieldMetadataMock({
objectMetadataId: 'non-existent',
type: FieldMetadataType.TEXT,
uniqueIdentifier: 'unique-identifier-1',
}),
],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
shouldThrow: true,
},
},
{
title: 'should extract single object metadata from maps',
context: {
input: {
flatFieldMetadatas: [PET_FLAT_FIELDS_MOCK.species],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
expected: fromFlatObjectMetadatasToFlatObjectMetadataMaps([
{
...PET_FLAT_OBJECT_MOCK,
flatFieldMetadatas: [PET_FLAT_FIELDS_MOCK.species],
},
]),
},
},
{
title: 'should extract object metadata from maps with provided fields',
context: {
input: {
flatFieldMetadatas: [
PET_FLAT_FIELDS_MOCK.species,
PET_FLAT_FIELDS_MOCK.name,
ROCKET_FLAT_FIELDS_MOCK.attachments,
],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
expected: fromFlatObjectMetadatasToFlatObjectMetadataMaps([
{
...PET_FLAT_OBJECT_MOCK,
flatFieldMetadatas: [
PET_FLAT_FIELDS_MOCK.name,
PET_FLAT_FIELDS_MOCK.species,
],
},
{
...ROCKET_FLAT_OBJECT_MOCK,
flatFieldMetadatas: [ROCKET_FLAT_FIELDS_MOCK.attachments],
},
]),
},
},
];
const filteredTestCases = eachTestingContextFilter(testCases);
it.each(filteredTestCases)(
'$title',
({
context: {
input: { flatFieldMetadatas, flatObjectMetadataMaps },
shouldThrow = false,
expected,
},
}) => {
if (shouldThrow) {
expect(() =>
getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatasOrThrow({
flatFieldMetadatas,
flatObjectMetadataMaps,
}),
).toThrowErrorMatchingSnapshot();
} else {
jestExpectToBeDefined(expected);
const result =
getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatasOrThrow({
flatFieldMetadatas,
flatObjectMetadataMaps,
});
expect(result).toEqual(expected);
}
},
);
});
@@ -0,0 +1,53 @@
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { FLAT_OBJECT_METADATA_MAPS_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata-maps/mocks/flat-object-metadata-maps.mock';
import { type GetSubFlatObjectMetadataMapsOfSpecificFieldsOrThrowArgs } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-out-of-flat-field-metadatas-or-throw.util';
import { getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-out-of-flat-field-metadatas.util';
type GetSubFlatObjectMetadataMapsTestCase = {
input: GetSubFlatObjectMetadataMapsOfSpecificFieldsOrThrowArgs;
};
describe('getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatas', () => {
const testCases: EachTestingContext<GetSubFlatObjectMetadataMapsTestCase>[] =
[
{
title: 'return undefined when object metadata id is not found',
context: {
input: {
flatFieldMetadatas: [
getFlatFieldMetadataMock({
objectMetadataId: 'non-existent',
type: FieldMetadataType.TEXT,
uniqueIdentifier: 'unique-identifier-1',
}),
],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
},
},
];
const filteredTestCases = eachTestingContextFilter(testCases);
it.each(filteredTestCases)(
'$title',
({
context: {
input: { flatFieldMetadatas, flatObjectMetadataMaps },
},
}) => {
const result = getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatas({
flatFieldMetadatas,
flatObjectMetadataMaps,
});
expect(result).toBeUndefined();
},
);
});
@@ -0,0 +1,66 @@
import { fromArrayToValuesByKeyRecord, isDefined } from 'twenty-shared/utils';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { EMPTY_FLAT_OBJECT_METADATA_MAPS } from 'src/engine/metadata-modules/flat-object-metadata-maps/constant/empty-flat-object-metadata-maps.constant';
import {
FlatObjectMetadataMapsException,
FlatObjectMetadataMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-object-metadata-maps/flat-object-metadata-maps.exception';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { addFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-object-metadata-with-field-maps-to-flat-object-metadata-maps-or-throw.util';
import { deleteFieldFromFlatObjectMetadataWithFlatFieldMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-field-from-flat-object-metadata-with-flat-field-maps-or-throw.util';
export type GetSubFlatObjectMetadataMapsOfSpecificFieldsOrThrowArgs = {
flatFieldMetadatas: Pick<FlatFieldMetadata, 'objectMetadataId' | 'id'>[];
flatObjectMetadataMaps: FlatObjectMetadataMaps;
};
export const getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatasOrThrow = ({
flatObjectMetadataMaps: sourceFlatObjectMetadataMaps,
flatFieldMetadatas,
}: GetSubFlatObjectMetadataMapsOfSpecificFieldsOrThrowArgs): FlatObjectMetadataMaps => {
const fieldIdPerObjectId = fromArrayToValuesByKeyRecord({
array: flatFieldMetadatas,
key: 'objectMetadataId',
});
return Object.keys(fieldIdPerObjectId).reduce(
(flatObjectMetadataMaps, objectMetadataId) => {
let flatObjectMetadataWithFlatFieldMaps = structuredClone(
sourceFlatObjectMetadataMaps.byId[objectMetadataId],
);
if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) {
throw new FlatObjectMetadataMapsException(
'getSubFlatObjectMetadataMapsOfSpecificFieldsOrThrow object metadata not found',
FlatObjectMetadataMapsExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
const fieldIds =
fieldIdPerObjectId[objectMetadataId]?.map(({ id }) => id) ?? [];
flatObjectMetadataWithFlatFieldMaps =
flatObjectMetadataWithFlatFieldMaps.flatFieldMetadatas.reduce(
(flatObjectMetadataWithFlatFieldMaps, flatFieldMetadata) => {
if (fieldIds.includes(flatFieldMetadata.id)) {
return flatObjectMetadataWithFlatFieldMaps;
}
return deleteFieldFromFlatObjectMetadataWithFlatFieldMapsOrThrow({
fieldMetadataId: flatFieldMetadata.id,
flatObjectMetadataWithFlatFieldMaps,
});
},
flatObjectMetadataWithFlatFieldMaps,
);
return addFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadataMapsOrThrow(
{
flatObjectMetadataWithFlatFieldMaps,
flatObjectMetadataMaps,
},
);
},
EMPTY_FLAT_OBJECT_METADATA_MAPS,
);
};
@@ -0,0 +1,15 @@
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import {
type GetSubFlatObjectMetadataMapsOfSpecificFieldsOrThrowArgs,
getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatasOrThrow,
} from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/get-sub-flat-object-metadata-maps-out-of-flat-field-metadatas-or-throw.util';
export const getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatas = (
args: GetSubFlatObjectMetadataMapsOfSpecificFieldsOrThrowArgs,
): FlatObjectMetadataMaps | undefined => {
try {
return getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatasOrThrow(args);
} catch {
return undefined;
}
};