Label identifier validation v2 (#14867)

# Introduction
Adding a hacky way to validate object against fields before fields
validation ( bi-directional validation process )
If you encounter an identical setup we will add a specific devXp as
cleanup validation but for the moment this seems enough

close https://github.com/twentyhq/core-team-issues/issues/1639
This commit is contained in:
Paul Rastoin
2025-10-07 13:27:21 +02:00
committed by GitHub
parent 0670bb2043
commit 23e21cbeea
22 changed files with 417 additions and 233 deletions
@@ -9,7 +9,7 @@ import { type FlatObjectMetadataEntityJsonbProperties } from 'src/engine/metadat
import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { type UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { type FlatObjectPropertiesUpdates } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
export const compareTwoFlatObjectMetadata = ({
@@ -50,42 +50,42 @@ export const compareTwoFlatObjectMetadata = ({
);
const objectMetadataDifference = diff(fromCompare, omit(toCompare, 'fields'));
return objectMetadataDifference.flatMap<
UpdateObjectAction['updates'][number]
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatObjectMetadataPropertiesToCompare;
const isJsonb = FLAT_OBJECT_METADATA_JSONB_PROPERTIES.includes(
property as FlatObjectMetadataEntityJsonbProperties,
);
return objectMetadataDifference.flatMap<FlatObjectPropertiesUpdates[number]>(
(difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatObjectMetadataPropertiesToCompare;
const isJsonb = FLAT_OBJECT_METADATA_JSONB_PROPERTIES.includes(
property as FlatObjectMetadataEntityJsonbProperties,
);
if (isJsonb) {
return {
from: parseJson(oldValue),
to: parseJson(value),
property,
};
}
if (isJsonb) {
return {
from: parseJson(oldValue),
to: parseJson(value),
from: oldValue,
to: value,
property,
};
}
return {
from: oldValue,
to: value,
property,
};
case 'CREATE':
case 'REMOVE': {
// Should never occurs ? should throw ?
return [];
}
default: {
assertUnreachable(
difference,
`Unexpected difference type: ${difference['type']}`,
);
}
}
case 'CREATE':
case 'REMOVE': {
// Should never occurs ? should throw ?
return [];
}
default: {
assertUnreachable(
difference,
`Unexpected difference type: ${difference['type']}`,
);
}
}
});
},
);
};
@@ -6,7 +6,7 @@ import {
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
@@ -52,11 +52,10 @@ export const fromDeleteObjectInputToFlatFieldMetadatasToDelete = ({
);
}
const { objectFlatFieldMetadatas } =
findObjectFieldsInFlatFieldMetadataMapsOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: flatObjectMetadataToDelete,
});
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: flatObjectMetadataToDelete,
});
const flatFieldMetadatasToDelete = objectFlatFieldMetadatas.flatMap(
(flatFieldMetadata) => {
if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
@@ -1,7 +1,7 @@
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
@@ -31,11 +31,10 @@ export const recomputeIndexAfterFlatObjectMetadataSingularNameUpdate = ({
nameSingular: updatedSingularName,
};
const { objectFlatFieldMetadatas } =
findObjectFieldsInFlatFieldMetadataMapsOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: optimisticFlatObjectMetadata,
});
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: optimisticFlatObjectMetadata,
});
return allRelatedFlatIndexMetadata.map<FlatIndexMetadata>((flatIndex) => {
const newIndex = generateFlatIndexMetadataWithNameOrThrow({
@@ -8,7 +8,7 @@ import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findObjectFieldsInFlatFieldMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util';
@@ -43,11 +43,10 @@ export const renameRelatedMorphFieldOnObjectNamesUpdate = ({
toFlatObjectMetadata,
flatFieldMetadataMaps,
}: RenameRelatedMorphFieldOnObjectNamesUpdateArgs): FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>[] => {
const { objectFlatFieldMetadatas } =
findObjectFieldsInFlatFieldMetadataMapsOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: fromFlatObjectMetadata,
});
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: fromFlatObjectMetadata,
});
const manyToOneMorphRelationFlatFieldMetadatas =
getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
@@ -4,6 +4,8 @@ import {
isLabelIdentifierFieldMetadataTypes,
} from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -11,24 +13,25 @@ import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-
export const validateFlatObjectMetadataIdentifiers = ({
flatObjectMetadata,
objectFlatFieldMetadatas,
flatFieldMetadataMaps,
}: {
flatObjectMetadata: Pick<
FlatObjectMetadata,
'labelIdentifierFieldMetadataId' | 'imageIdentifierFieldMetadataId'
>;
objectFlatFieldMetadatas: FlatFieldMetadata[];
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}) => {
const errors: FlatObjectMetadataValidationError[] = [];
const { labelIdentifierFieldMetadataId, imageIdentifierFieldMetadataId } =
flatObjectMetadata;
// TODO should not be nullable
if (isDefined(labelIdentifierFieldMetadataId)) {
const flatFieldMetadata = objectFlatFieldMetadatas.find(
(flatFieldMetadata) =>
flatFieldMetadata.id === labelIdentifierFieldMetadataId,
);
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: labelIdentifierFieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(flatFieldMetadata)) {
errors.push({
@@ -48,10 +51,10 @@ export const validateFlatObjectMetadataIdentifiers = ({
}
if (isDefined(imageIdentifierFieldMetadataId)) {
const relatedFlatFieldMetadata = objectFlatFieldMetadatas.find(
(flatFieldMetadata) =>
flatFieldMetadata.id === imageIdentifierFieldMetadataId,
);
const relatedFlatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: imageIdentifierFieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(relatedFlatFieldMetadata)) {
errors.push({
@@ -0,0 +1,62 @@
import { t } from '@lingui/core/macro';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util';
import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util';
import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util';
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util';
export const validateFlatObjectMetadataNameAndLabels = ({
optimisticFlatObjectMetadataMaps,
flatObjectMetadataToValidate,
}: {
flatObjectMetadataToValidate: FlatObjectMetadata;
optimisticFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
}): FlatObjectMetadataValidationError[] => {
const errors: FlatObjectMetadataValidationError[] = [];
errors.push(
...validateFlatObjectMetadataNames({
namePlural: flatObjectMetadataToValidate.namePlural,
nameSingular: flatObjectMetadataToValidate.nameSingular,
}),
);
errors.push(
...validateFlatObjectMetadataLabel({
labelPlural: flatObjectMetadataToValidate.labelPlural,
labelSingular: flatObjectMetadataToValidate.labelSingular,
}),
);
if (
flatObjectMetadataToValidate.isLabelSyncedWithName &&
!areFlatObjectMetadataNamesSyncedWithLabels(flatObjectMetadataToValidate)
) {
errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: t`Names are not synced with labels`,
userFriendlyMessage: t`Names are not synced with labels`,
});
}
if (
doesOtherObjectWithSameNameExists({
objectMetadataNamePlural: flatObjectMetadataToValidate.namePlural,
objectMetadataNameSingular: flatObjectMetadataToValidate.nameSingular,
objectMetadataMaps: optimisticFlatObjectMetadataMaps,
existingObjectMetadataId: flatObjectMetadataToValidate.id,
})
) {
errors.push({
code: ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
message: 'Object already exists',
userFriendlyMessage: t`Object already exists`,
});
}
return errors;
};