Refactor FieldMetadataService and validation to consume FlatObjectMetadataMaps (#13719)

# Introduction
Following recent `FlatObjectMetadataMaps` manipulation, transpilers
utils introduction in https://github.com/twentyhq/twenty/pull/13620
Refactored the field metadata service and validator to iterate over
`FlatObjectMetadataMaps` instead of `FlatObjectMetadata[]`.

The object metadata service v2 is shadow coding
This commit is contained in:
Paul Rastoin
2025-08-07 17:19:07 +02:00
committed by GitHub
parent 05c6805f0a
commit 83a560c936
20 changed files with 557 additions and 165 deletions
@@ -15,12 +15,15 @@ import {
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { FlatFieldMetadataValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service';
import { FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { fromCreateFieldInputToFlatFieldAndItsFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-and-its-flat-object-metadata.util';
import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { mergeFlatFieldMetadatasInFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/merge-flat-field-metadatas-in-flat-object-metadata.util';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { dispatchAndMergeFlatFieldMetadatasInFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/dispatch-and-merge-flat-field-metadatas-in-flat-object-metadatas.util';
import { fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-with-flat-field-maps-to-flat-object-metadatas.util';
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { addFlatFieldMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps.util';
import { extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/extract-flat-object-metadata-maps-out-of-flat-object-metadata-maps-or-throw.util';
import { extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/extract-flat-object-metadata-maps-out-of-flat-object-metadata-maps.util';
import { fromFlatObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-maps-to-flat-object-metadatas.util';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service';
@@ -53,6 +56,57 @@ export class FieldMetadataServiceV2 extends TypeOrmQueryService<FieldMetadataEnt
return createdFieldMetadata;
}
private computeOtherFlatObjectMetadataMapsToValidate({
flatFieldMetadataToCreate,
flatFieldMetadatasToCreate,
flatObjectMetadataMaps,
}: {
flatObjectMetadataMaps: FlatObjectMetadataMaps;
flatFieldMetadatasToCreate: FlatFieldMetadata[];
flatFieldMetadataToCreate: FlatFieldMetadata;
}): FlatObjectMetadataMaps | undefined {
if (
!isFlatFieldMetadataEntityOfType(
flatFieldMetadataToCreate,
FieldMetadataType.RELATION,
) &&
!isFlatFieldMetadataEntityOfType(
flatFieldMetadataToCreate,
FieldMetadataType.MORPH_RELATION,
)
) {
return undefined;
}
const relatedFlatFieldMetadataToCreate = flatFieldMetadatasToCreate.find(
(relatedFlatFieldMetadata) =>
isFlatFieldMetadataEntityOfType(
relatedFlatFieldMetadata,
FieldMetadataType.RELATION,
) &&
relatedFlatFieldMetadata.id ===
flatFieldMetadataToCreate.relationTargetFieldMetadataId,
);
if (!isDefined(relatedFlatFieldMetadataToCreate)) {
return undefined;
}
const flatObjectMetadataMapsWithRelatedObjectMetadata =
extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMaps({
flatObjectMetadataMaps,
objectMetadataIds: [relatedFlatFieldMetadataToCreate.objectMetadataId],
});
if (!isDefined(flatObjectMetadataMapsWithRelatedObjectMetadata)) {
return undefined;
}
return addFlatFieldMetadataInFlatObjectMetadataMaps({
flatFieldMetadata: relatedFlatFieldMetadataToCreate,
flatObjectMetadataMaps: flatObjectMetadataMapsWithRelatedObjectMetadata,
});
}
async createMany(
fieldMetadataInputs: CreateFieldInput[],
): Promise<FieldMetadataEntity[]> {
@@ -79,86 +133,27 @@ export class FieldMetadataServiceV2 extends TypeOrmQueryService<FieldMetadataEnt
)
).flat();
const existingFlatObjectMetadatas = Object.values(
existingFlatObjectMetadataMaps.byId,
).map(fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata);
const impactedObjectMetadataIds = Array.from(
new Set(
flatFieldMetadatasToCreate.map(
(flatFieldMetadata) => flatFieldMetadata.objectMetadataId,
),
),
);
const filterFlatObjectMetadatasByImpactedIds = (
flatObjectMetadatas: FlatObjectMetadata[],
) =>
flatObjectMetadatas.filter((flatObjectMetadata) =>
impactedObjectMetadataIds.includes(flatObjectMetadata.id),
);
const impactedExistingFlatObjectMetadatas =
filterFlatObjectMetadatasByImpactedIds(existingFlatObjectMetadatas);
const allValidationErrors: FailedFlatFieldMetadataValidationExceptions[] =
[];
let sequentiallyOptimisticallyRenderedFlatObjectMetadatas = structuredClone(
existingFlatObjectMetadatas,
let optimisticFlatObjectMetadataMaps = structuredClone(
existingFlatObjectMetadataMaps,
);
for (const flatFieldMetadataToCreate of flatFieldMetadatasToCreate) {
let otherFlatObjectMetadataToValidate: FlatObjectMetadata | undefined =
undefined;
if (
isFlatFieldMetadataEntityOfType(
const otherFlatObjectMetadataMapsToValidate =
this.computeOtherFlatObjectMetadataMapsToValidate({
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
flatFieldMetadatasToCreate,
flatFieldMetadataToCreate,
FieldMetadataType.RELATION,
) ||
isFlatFieldMetadataEntityOfType(
flatFieldMetadataToCreate,
FieldMetadataType.MORPH_RELATION,
)
) {
const relatedFlatFieldMetadataToCreate =
flatFieldMetadatasToCreate.find(
(relatedFlatFieldMetadata) =>
isFlatFieldMetadataEntityOfType(
relatedFlatFieldMetadata,
FieldMetadataType.RELATION,
) &&
relatedFlatFieldMetadata.id ===
flatFieldMetadataToCreate.relationTargetFieldMetadataId,
);
const relatedFlatObjectMetadata = isDefined(
relatedFlatFieldMetadataToCreate,
)
? existingFlatObjectMetadataMaps.byId[
relatedFlatFieldMetadataToCreate.objectMetadataId
]
: undefined;
otherFlatObjectMetadataToValidate =
isDefined(relatedFlatObjectMetadata) &&
isDefined(relatedFlatFieldMetadataToCreate)
? mergeFlatFieldMetadatasInFlatObjectMetadata({
flatFieldMetadatas: [relatedFlatFieldMetadataToCreate],
flatObjectMetadata: relatedFlatObjectMetadata,
})
: undefined;
}
});
const validationErrors =
await this.flatFieldMetadataValidatorService.validateOneFlatFieldMetadata(
{
existingFlatObjectMetadatas:
sequentiallyOptimisticallyRenderedFlatObjectMetadatas,
existingFlatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
flatFieldMetadataToValidate: flatFieldMetadataToCreate,
workspaceId,
othersFlatObjectMetadataToValidate: isDefined(
otherFlatObjectMetadataToValidate,
)
? [otherFlatObjectMetadataToValidate]
: undefined,
otherFlatObjectMetadataMapsToValidate,
},
);
@@ -167,12 +162,18 @@ export class FieldMetadataServiceV2 extends TypeOrmQueryService<FieldMetadataEnt
continue;
}
sequentiallyOptimisticallyRenderedFlatObjectMetadatas =
dispatchAndMergeFlatFieldMetadatasInFlatObjectMetadatas({
flatFieldMetadatas: [flatFieldMetadataToCreate],
flatObjectMetadatas:
sequentiallyOptimisticallyRenderedFlatObjectMetadatas,
});
try {
optimisticFlatObjectMetadataMaps =
addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
flatFieldMetadata: flatFieldMetadataToCreate,
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
});
} catch (e) {
throw new FieldMetadataException(
'Optimistic cache manipulation failed, should never occur',
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
}
if (allValidationErrors.length > 0) {
@@ -182,20 +183,48 @@ export class FieldMetadataServiceV2 extends TypeOrmQueryService<FieldMetadataEnt
);
}
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
objectMetadataFromToInputs: {
from: impactedExistingFlatObjectMetadatas,
to: filterFlatObjectMetadatasByImpactedIds(
sequentiallyOptimisticallyRenderedFlatObjectMetadatas,
const impactedObjectMetadataIds = Array.from(
new Set(
flatFieldMetadatasToCreate.map(
(flatFieldMetadata) => flatFieldMetadata.objectMetadataId,
),
},
inferDeletionFromMissingObjectFieldIndex: false,
workspaceId,
});
),
);
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
try {
const fromImpactedFlatObjectMetadataMaps =
extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
objectMetadataIds: impactedObjectMetadataIds,
});
const toImpactedFlatObjectMetadataMaps =
extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow({
flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps,
objectMetadataIds: impactedObjectMetadataIds,
});
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
objectMetadataFromToInputs: {
from: fromFlatObjectMetadataMapsToFlatObjectMetadatas(
fromImpactedFlatObjectMetadataMaps,
),
to: fromFlatObjectMetadataMapsToFlatObjectMetadatas(
toImpactedFlatObjectMetadataMaps,
),
},
inferDeletionFromMissingObjectFieldIndex: false,
workspaceId,
});
// TODO refactor once the runner has been refactored to return created entities
return [];
await this.workspaceMigrationRunnerV2Service.run(workspaceMigration);
// TODO refactor once the runner has been refactored to return created entities
return [];
} catch {
// TODO prastoin We should pass the internal error here
throw new FieldMetadataException(
'Workspace migration failed to run',
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
}
}
@@ -11,8 +11,7 @@ import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modul
import { FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/validate-flat-field-metadata-name-availability.validator';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { mergeTwoFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/merge-two-flat-object-metadatas.util';
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
@@ -27,8 +26,8 @@ import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/
export type ValidateOneFieldMetadataArgs<
T extends FieldMetadataType = FieldMetadataType,
> = {
existingFlatObjectMetadatas: FlatObjectMetadata[];
othersFlatObjectMetadataToValidate?: FlatObjectMetadata[];
existingFlatObjectMetadataMaps: FlatObjectMetadataMaps;
otherFlatObjectMetadataMapsToValidate?: FlatObjectMetadataMaps;
flatFieldMetadataToValidate: FlatFieldMetadata<T>;
workspaceId: string;
};
@@ -42,32 +41,27 @@ export class FlatFieldMetadataValidatorService {
async validateOneFlatFieldMetadata<
T extends FieldMetadataType = FieldMetadataType,
>({
existingFlatObjectMetadatas,
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate,
othersFlatObjectMetadataToValidate,
otherFlatObjectMetadataMapsToValidate,
workspaceId,
}: ValidateOneFieldMetadataArgs<T>): Promise<
FailedFlatFieldMetadataValidationExceptions[]
> {
const errors: FailedFlatFieldMetadataValidationExceptions[] = [];
const allFlatObjectMetadata = isDefined(othersFlatObjectMetadataToValidate)
? mergeTwoFlatObjectMetadatas({
destFlatObjectMetadatas: existingFlatObjectMetadatas,
toMergeFlatObjectMetadatas: othersFlatObjectMetadataToValidate,
})
: existingFlatObjectMetadatas;
const parentFlatObjectMetadata = allFlatObjectMetadata.find(
(existingFlatObjectMetadata) =>
existingFlatObjectMetadata.id ===
flatFieldMetadataToValidate.objectMetadataId,
);
const parentFlatObjectMetadata =
otherFlatObjectMetadataMapsToValidate?.byId[
flatFieldMetadataToValidate.objectMetadataId
] ??
existingFlatObjectMetadataMaps.byId[
flatFieldMetadataToValidate.objectMetadataId
];
if (!isDefined(parentFlatObjectMetadata)) {
errors.push(
new FieldMetadataException(
isDefined(othersFlatObjectMetadataToValidate)
isDefined(otherFlatObjectMetadataMapsToValidate)
? 'Object metadata not found in both existing and about to be created object metadatas'
: 'Object metadata not found',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
@@ -139,10 +133,10 @@ export class FlatFieldMetadataValidatorService {
} else {
const fieldMetadataTypeValidatorExceptions =
await fieldMetadataTypeValidator({
existingFlatObjectMetadatas,
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate,
workspaceId,
othersFlatObjectMetadataToValidate,
otherFlatObjectMetadataMapsToValidate,
});
errors.push(...fieldMetadataTypeValidatorExceptions);
@@ -7,17 +7,16 @@ import {
} 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 { FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { mergeTwoFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/merge-two-flat-object-metadatas.util';
// Remark: This is duplicated with api metadata transpilers exceptions handlers
// We might wanna have an NotValidatedFlatFieldMetadata and NotValidatedFlatObjectMetadata, in order to avoid
export const validateRelationFlatFieldMetadata = async ({
existingFlatObjectMetadatas,
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate: {
relationTargetFieldMetadataId,
relationTargetObjectMetadataId,
},
othersFlatObjectMetadataToValidate,
otherFlatObjectMetadataMapsToValidate,
}: ValidateOneFieldMetadataArgs<FieldMetadataType.RELATION>): Promise<
FailedFlatFieldMetadataValidationExceptions[]
> => {
@@ -38,22 +37,16 @@ export const validateRelationFlatFieldMetadata = async ({
}
const errors: FailedFlatFieldMetadataValidationExceptions[] = [];
const allFlatObjectMetadata = isDefined(othersFlatObjectMetadataToValidate)
? mergeTwoFlatObjectMetadatas({
destFlatObjectMetadatas: existingFlatObjectMetadatas,
toMergeFlatObjectMetadatas: othersFlatObjectMetadataToValidate,
})
: existingFlatObjectMetadatas;
const targetRelationFlatObjectMetadata = allFlatObjectMetadata.find(
(flatObjectMetadata) =>
flatObjectMetadata.id === relationTargetObjectMetadataId,
);
const targetRelationFlatObjectMetadata =
otherFlatObjectMetadataMapsToValidate?.byId[
relationTargetObjectMetadataId
] ?? existingFlatObjectMetadataMaps.byId[relationTargetObjectMetadataId];
if (!isDefined(targetRelationFlatObjectMetadata)) {
errors.push(
new FieldMetadataException(
isDefined(othersFlatObjectMetadataToValidate)
isDefined(otherFlatObjectMetadataMapsToValidate)
? 'Relation target object metadata not found in both existing and about to be created object metadatas'
: 'Relation target object metadata not found',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
@@ -62,15 +55,15 @@ export const validateRelationFlatFieldMetadata = async ({
}
const targetRelationFlatFieldMetadata =
targetRelationFlatObjectMetadata?.flatFieldMetadatas.find(
(flatFieldMetadata) =>
flatFieldMetadata.id === relationTargetFieldMetadataId,
);
targetRelationFlatObjectMetadata?.fieldsById[relationTargetFieldMetadataId];
if (!isDefined(targetRelationFlatFieldMetadata)) {
if (
isDefined(targetRelationFlatObjectMetadata) &&
!isDefined(targetRelationFlatFieldMetadata)
) {
errors.push(
new FieldMetadataException(
isDefined(othersFlatObjectMetadataToValidate)
isDefined(otherFlatObjectMetadataMapsToValidate)
? 'Relation field target metadata not found in both existing and about to be created field metadatas'
: 'Relation field target metadata not found',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
@@ -15,9 +15,10 @@ export const expectFlatFieldMetadataToBeInFlatObjectMetadataMaps = ({
flatObjectMetadataMaps,
}: ExpectFlatFieldMetadataToBeInFlatObjectMetadataMapsArgs) => {
const { objectMetadataId, id: flatFieldMetadataId } = flatFieldMetadata;
const petObject = flatObjectMetadataMaps.byId[objectMetadataId];
const matchingFlatObjectMetadata =
flatObjectMetadataMaps.byId[objectMetadataId];
jestExpectToBeDefined(petObject);
jestExpectToBeDefined(matchingFlatObjectMetadata);
if (
isFlatFieldMetadataEntityOfType(
@@ -27,15 +28,22 @@ export const expectFlatFieldMetadataToBeInFlatObjectMetadataMaps = ({
isDefined(flatFieldMetadata.settings.joinColumnName)
) {
expect(
petObject.fieldIdByJoinColumnName[
matchingFlatObjectMetadata.fieldIdByJoinColumnName[
flatFieldMetadata.settings.joinColumnName
],
).toEqual(flatFieldMetadataId);
}
expect(petObject.fieldsById[flatFieldMetadataId]).toBeDefined();
expect(petObject.fieldsById[flatFieldMetadataId]).toEqual(flatFieldMetadata);
expect(petObject.fieldIdByName[flatFieldMetadata.name]).toEqual(
flatFieldMetadataId,
expect(
matchingFlatObjectMetadata.fieldsById[flatFieldMetadataId],
).toBeDefined();
expect(matchingFlatObjectMetadata.fieldsById[flatFieldMetadataId]).toEqual(
flatFieldMetadata,
);
expect(matchingFlatObjectMetadata.flatFieldMetadatas).toContain(
flatFieldMetadata,
);
expect(
matchingFlatObjectMetadata.fieldIdByName[flatFieldMetadata.name],
).toEqual(flatFieldMetadataId);
};
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow should throw when extracting twice the same object 1`] = `"addFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadataMapsOrThrow: flat object metadata with fields maps to add already exists"`;
exports[`extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow should throw when object metadata id is not found 1`] = `"extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow object metadata not found"`;
@@ -0,0 +1,65 @@
import {
EachTestingContext,
eachTestingContextFilter,
} 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 { FLAT_OBJECT_METADATA_MAPS_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata-maps/mocks/flat-object-metadata-maps.mock';
import { AddFlatFieldMetadataInFlatObjectMetadataMapsOrThrowArgs } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
import { addFlatFieldMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps.util';
type AddFlatFieldMetadataInFlatObjectMetadataMapsArgsTestCase = {
input: AddFlatFieldMetadataInFlatObjectMetadataMapsOrThrowArgs;
};
describe('addFlatFieldMetadataInFlatObjectMetadataMaps', () => {
const testCases: EachTestingContext<AddFlatFieldMetadataInFlatObjectMetadataMapsArgsTestCase>[] =
[
{
title:
'should return undefined when field metadata parent object metadata does not exist',
context: {
input: {
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatFieldMetadata: getFlatFieldMetadataMock({
objectMetadataId: 'non-existent',
type: FieldMetadataType.TEXT,
uniqueIdentifier: 'unique-id-1',
id: 'unique-id-1',
}),
},
},
},
{
title:
'should return undefined when field metadata to add already exist in object',
context: {
input: {
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
flatFieldMetadata: PET_FLAT_FIELDS_MOCK.species,
},
},
},
];
const filteredTestCases = eachTestingContextFilter(testCases);
it.each(filteredTestCases)(
'$title',
({
context: {
input: { flatFieldMetadata, flatObjectMetadataMaps },
},
}) => {
const updatedFlatObjectMetadataMaps =
addFlatFieldMetadataInFlatObjectMetadataMaps({
flatFieldMetadata,
flatObjectMetadataMaps,
});
expect(updatedFlatObjectMetadataMaps).toBeUndefined();
},
);
});
@@ -78,7 +78,7 @@ describe('addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow', () => {
flatObjectMetadataMaps,
});
expect(updatedFlatObjectMetadataMaps).toEqual(expected);
expect(updatedFlatObjectMetadataMaps).toStrictEqual(expected);
}
},
);
@@ -0,0 +1,109 @@
import { jestExpectToBeDefined } from 'test/utils/expect-to-be-defined.util.test';
import {
EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FLAT_OBJECT_METADATA_MAPS_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata-maps/mocks/flat-object-metadata-maps.mock';
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import {
ExtractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrowArgs,
extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow,
} from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/extract-flat-object-metadata-maps-out-of-flat-object-metadata-maps-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 ExtractFlatObjectMetadataMapsTestCase = {
input: ExtractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrowArgs;
shouldThrow?: true;
expected?: FlatObjectMetadataMaps;
};
describe('extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow', () => {
const testCases: EachTestingContext<ExtractFlatObjectMetadataMapsTestCase>[] =
[
{
title: 'should throw when object metadata id is not found',
context: {
input: {
objectMetadataIds: ['non-existent-id'],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
shouldThrow: true,
},
},
{
title: 'should throw when extracting twice the same object',
context: {
input: {
objectMetadataIds: [
PET_FLAT_OBJECT_MOCK.id,
PET_FLAT_OBJECT_MOCK.id,
],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
shouldThrow: true,
},
},
{
title: 'should extract single object metadata from maps',
context: {
input: {
objectMetadataIds: [PET_FLAT_OBJECT_MOCK.id],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
expected: fromFlatObjectMetadatasToFlatObjectMetadataMaps([
PET_FLAT_OBJECT_MOCK,
]),
},
},
{
title: 'should extract multiple object metadata from maps',
context: {
input: {
objectMetadataIds: [
PET_FLAT_OBJECT_MOCK.id,
ROCKET_FLAT_OBJECT_MOCK.id,
],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
expected: fromFlatObjectMetadatasToFlatObjectMetadataMaps([
PET_FLAT_OBJECT_MOCK,
ROCKET_FLAT_OBJECT_MOCK,
]),
},
},
];
const filteredTestCases = eachTestingContextFilter(testCases);
it.each(filteredTestCases)(
'$title',
({
context: {
input: { objectMetadataIds, flatObjectMetadataMaps },
shouldThrow = false,
expected,
},
}) => {
if (shouldThrow) {
expect(() =>
extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow({
objectMetadataIds,
flatObjectMetadataMaps,
}),
).toThrowErrorMatchingSnapshot();
} else {
jestExpectToBeDefined(expected);
const result =
extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow({
objectMetadataIds,
flatObjectMetadataMaps,
});
expect(result).toEqual(expected);
}
},
);
});
@@ -0,0 +1,58 @@
import {
EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FLAT_OBJECT_METADATA_MAPS_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata-maps/mocks/flat-object-metadata-maps.mock';
import { ExtractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrowArgs } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/extract-flat-object-metadata-maps-out-of-flat-object-metadata-maps-or-throw.util';
import { extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/extract-flat-object-metadata-maps-out-of-flat-object-metadata-maps.util';
import { PET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock';
type ExtractFlatObjectMetadataMapsTestCase = {
input: ExtractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrowArgs;
};
describe('extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMaps', () => {
const testCases: EachTestingContext<ExtractFlatObjectMetadataMapsTestCase>[] =
[
{
title: 'should throw when object metadata id is not found',
context: {
input: {
objectMetadataIds: ['non-existent-id'],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
},
},
{
title: 'should throw when extracting twice the same object',
context: {
input: {
objectMetadataIds: [
PET_FLAT_OBJECT_MOCK.id,
PET_FLAT_OBJECT_MOCK.id,
],
flatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS,
},
},
},
];
const filteredTestCases = eachTestingContextFilter(testCases);
it.each(filteredTestCases)(
'$title',
({
context: {
input: { objectMetadataIds, flatObjectMetadataMaps },
},
}) => {
const result = extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMaps({
objectMetadataIds,
flatObjectMetadataMaps,
});
expect(result).toBeUndefined();
},
);
});
@@ -55,6 +55,10 @@ describe('findFlatObjectMetadataInFlatObjectMetadataMaps', () => {
flatObjectMetadataMaps,
});
expect(result?.flatFieldMetadatas.length).toEqual(
expected?.flatFieldMetadatas.length,
);
expect(result).toEqual(expected);
},
);
@@ -35,6 +35,6 @@ export const addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow = ({
flatObjectMetadataWithFlatFieldMaps,
}),
},
idByNameSingular: flatObjectMetadataMaps.idByNameSingular,
idByNameSingular: { ...flatObjectMetadataMaps.idByNameSingular },
};
};
@@ -0,0 +1,15 @@
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import {
AddFlatFieldMetadataInFlatObjectMetadataMapsOrThrowArgs,
addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow,
} from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
export const addFlatFieldMetadataInFlatObjectMetadataMaps = (
arg: AddFlatFieldMetadataInFlatObjectMetadataMapsOrThrowArgs,
): FlatObjectMetadataMaps | undefined => {
try {
return addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow(arg);
} catch (e) {
return undefined;
}
};
@@ -67,5 +67,9 @@ export const addFlatFieldMetadataToFlatObjectMetadataWithFlatFieldMapsOrThrow =
...flatObjectMetadataWithFlatFieldMaps.fieldsById,
[flatFieldMetadata.id]: flatFieldMetadata,
},
flatFieldMetadatas: [
...flatObjectMetadataWithFlatFieldMaps.flatFieldMetadatas,
flatFieldMetadata,
],
};
};
@@ -0,0 +1,43 @@
import { isDefined } from 'twenty-shared/utils';
import {
FlatObjectMetadataMapsException,
FlatObjectMetadataMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-object-metadata-maps/flat-object-metadata-maps.exception';
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { FlatObjectMetadataWithFlatFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-with-flat-field-metadata-maps.type';
type AddFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadataMapsOrThrowArgs =
{
flatObjectMetadataWithFlatFieldMaps: FlatObjectMetadataWithFlatFieldMaps;
flatObjectMetadataMaps: FlatObjectMetadataMaps;
};
export const addFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadataMapsOrThrow =
({
flatObjectMetadataWithFlatFieldMaps,
flatObjectMetadataMaps,
}: AddFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadataMapsOrThrowArgs): FlatObjectMetadataMaps => {
if (
isDefined(
flatObjectMetadataMaps.byId[flatObjectMetadataWithFlatFieldMaps.id],
)
) {
throw new FlatObjectMetadataMapsException(
'addFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadataMapsOrThrow: flat object metadata with fields maps to add already exists',
FlatObjectMetadataMapsExceptionCode.OBJECT_METADATA_ALREADY_EXISTS,
);
}
return {
byId: {
...flatObjectMetadataMaps.byId,
[flatObjectMetadataWithFlatFieldMaps.id]:
flatObjectMetadataWithFlatFieldMaps,
},
idByNameSingular: {
...flatObjectMetadataMaps.idByNameSingular,
[flatObjectMetadataWithFlatFieldMaps.nameSingular]:
flatObjectMetadataWithFlatFieldMaps.id,
},
};
};
@@ -0,0 +1,46 @@
import { isDefined } from 'twenty-shared/utils';
import {
FlatObjectMetadataMapsException,
FlatObjectMetadataMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-object-metadata-maps/flat-object-metadata-maps.exception';
import { 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';
export type ExtractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrowArgs =
{
objectMetadataIds: string[];
flatObjectMetadataMaps: FlatObjectMetadataMaps;
};
export const extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow =
({
flatObjectMetadataMaps: sourceFlatObjectMetadataMaps,
objectMetadataIds,
}: ExtractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrowArgs): FlatObjectMetadataMaps => {
const emptyFlatObjectMetadataMaps: FlatObjectMetadataMaps = {
byId: {},
idByNameSingular: {},
};
return objectMetadataIds.reduce(
(flatObjectMetadataMaps, objectMetadataId) => {
const flatObjectMetadataWithFlatFieldMaps =
sourceFlatObjectMetadataMaps.byId[objectMetadataId];
if (!isDefined(flatObjectMetadataWithFlatFieldMaps)) {
throw new FlatObjectMetadataMapsException(
'extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow object metadata not found',
FlatObjectMetadataMapsExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
return addFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadataMapsOrThrow(
{
flatObjectMetadataMaps,
flatObjectMetadataWithFlatFieldMaps,
},
);
},
emptyFlatObjectMetadataMaps,
);
};
@@ -0,0 +1,17 @@
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import {
ExtractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrowArgs,
extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow,
} from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/extract-flat-object-metadata-maps-out-of-flat-object-metadata-maps-or-throw.util';
export const extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMaps = (
args: ExtractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrowArgs,
): FlatObjectMetadataMaps | undefined => {
try {
return extractFlatObjectMetadataMapsOutOfFlatObjectMetadataMapsOrThrow(
args,
);
} catch {
return undefined;
}
};
@@ -0,0 +1,13 @@
import { isDefined } from 'twenty-shared/utils';
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { 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';
export const fromFlatObjectMetadataMapsToFlatObjectMetadatas = (
flatObjectMetadataMaps: FlatObjectMetadataMaps,
): FlatObjectMetadata[] => {
return Object.values(flatObjectMetadataMaps.byId)
.filter(isDefined)
.map(fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata);
};
@@ -8,6 +8,8 @@ export const fromFlatObjectMetadataToFlatObjectMetadataWithFlatFieldMaps = (
const emptyFlatObjectMetadataWithFlatFieldMaps: FlatObjectMetadataWithFlatFieldMaps =
{
...flatObjectMetadata,
flatIndexMetadatas: [],
flatFieldMetadatas: [],
fieldIdByJoinColumnName: {},
fieldIdByName: {},
fieldsById: {},
@@ -1,18 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fromObjectMetadataItemWithFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-item-with-field-maps-to-flat-object-metadata.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export const fromObjectMetadataMapsToFlatObjectMetadatas = (
objectMetadataMaps: ObjectMetadataMaps,
): FlatObjectMetadata[] => {
return Object.values(objectMetadataMaps.byId)
.filter(isDefined)
.map((objectMetadataItemWithFieldMaps) =>
fromObjectMetadataItemWithFieldMapsToFlatObjectMetadata({
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
}),
);
};
@@ -5,10 +5,11 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { fromCreateObjectInputToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata.util';
import { fromObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-maps-to-flat-object-metadatas.util';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service';
import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import { fromFlatObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-maps-to-flat-object-metadatas.util';
import { ObjectMetadataEntity } from './object-metadata.entity';
@@ -30,8 +31,8 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataE
override async createOne(
objectMetadataInput: CreateObjectInput,
): Promise<ObjectMetadataEntity> {
const { objectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps(
const { flatObjectMetadataMaps } =
await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps(
{
workspaceId: objectMetadataInput.workspaceId,
},
@@ -39,19 +40,23 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataE
const createdRawFlatObjectMetadata =
fromCreateObjectInputToFlatObjectMetadata(objectMetadataInput);
const existingFlatObjectMetadatas =
fromObjectMetadataMapsToFlatObjectMetadatas(objectMetadataMaps);
const existingFlatObjectMetadatas: FlatObjectMetadataMaps = {
byId: {},
idByNameSingular: {},
};
// @ts-expect-error TODO implement validateFlatObjectMetadata
const createdFlatObjectMetadata = validateFlatObjectMetadata({
existing:
// Here we assume that EVERYTHING is in cache and up to date, this is very critical, also race condition prone :thinking:
fromObjectMetadataMapsToFlatObjectMetadatas(objectMetadataMaps),
fromFlatObjectMetadataMapsToFlatObjectMetadatas(flatObjectMetadataMaps),
toValidate: [createdRawFlatObjectMetadata],
});
const workspaceMigration = this.workspaceMigrationBuilderV2.build({
objectMetadataFromToInputs: {
from: existingFlatObjectMetadatas,
from: fromFlatObjectMetadataMapsToFlatObjectMetadatas(
existingFlatObjectMetadatas,
),
to: [createdFlatObjectMetadata],
},
inferDeletionFromMissingObjectFieldIndex: false,