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
@@ -114,12 +114,23 @@ export class WorkspaceMigrationBuildOrchestratorService {
await this.workspaceMigrationV2ObjectActionsBuilderService.validateAndBuild(
{
buildOptions,
dependencyOptimisticFlatEntityMaps: undefined,
// TODO That's hacky also not reliable ?
// dependencyOptimisticFlatEntityMaps: {
// flatFieldMetadataMaps:
// flatFieldMetadataMaps?.to ?? EMPTY_FLAT_ENTITY_MAPS,
// },
// Note: That's a hacky way to allow validating object against field metadatas, not optimal
dependencyOptimisticFlatEntityMaps: {
flatFieldMetadataMaps: {
byId: {
...dependencyAllFlatEntityMaps?.flatFieldMetadataMaps?.byId,
...flatFieldMetadataMaps?.from.byId,
...flatFieldMetadataMaps?.to.byId,
},
idByUniversalIdentifier: {
...dependencyAllFlatEntityMaps?.flatFieldMetadataMaps
?.idByUniversalIdentifier,
...flatFieldMetadataMaps?.from.idByUniversalIdentifier,
...flatFieldMetadataMaps?.to.idByUniversalIdentifier,
},
},
},
///
from: fromFlatObjectMetadataMaps,
to: toFlatObjectMetadataMaps,
workspaceId,
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { compareTwoFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util';
import {
@@ -13,13 +14,13 @@ import {
WorkspaceMigrationObjectActionV2,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service';
// export type ObjectMetadataRelatedFlatEntityMaps = Pick<
// AllFlatEntityMaps,
// 'flatFieldMetadataMaps'
// >;
export type ObjectMetadataRelatedFlatEntityMaps = Pick<
AllFlatEntityMaps,
'flatFieldMetadataMaps'
>;
// TODO find a solution in order to handle label indentifier validation
export type ObjectMetadataRelatedFlatEntityMaps = undefined;
// export type ObjectMetadataRelatedFlatEntityMaps = undefined;
@Injectable()
export class WorkspaceMigrationV2ObjectActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
@@ -134,12 +135,12 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService extends WorkspaceEn
>
| undefined
> {
const objectMetadataUpdatedProperties = compareTwoFlatObjectMetadata({
const flatObjectPropertiesUpdates = compareTwoFlatObjectMetadata({
fromFlatObjectMetadata,
toFlatObjectMetadata,
});
if (objectMetadataUpdatedProperties.length === 0) {
if (flatObjectPropertiesUpdates.length === 0) {
return undefined;
}
@@ -149,6 +150,7 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService extends WorkspaceEn
flatObjectMetadataToValidate: toFlatObjectMetadata,
optimisticFlatObjectMetadataMaps,
dependencyOptimisticFlatEntityMaps,
flatObjectPropertiesUpdates: flatObjectPropertiesUpdates,
});
if (validationResult.errors.length > 0) {
@@ -161,7 +163,7 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService extends WorkspaceEn
const updateObjectAction: UpdateObjectAction = {
type: 'update_object',
objectMetadataId: toFlatObjectMetadata.id,
updates: objectMetadataUpdatedProperties,
updates: flatObjectPropertiesUpdates,
};
return {
@@ -9,17 +9,19 @@ export type CreateObjectAction = {
flatFieldMetadatas: FlatFieldMetadata[];
};
export type FlatObjectPropertiesUpdates = Array<
{
[P in FlatObjectMetadataPropertiesToCompare]: PropertyUpdate<
FlatObjectMetadata,
P
>;
}[FlatObjectMetadataPropertiesToCompare]
>;
export type UpdateObjectAction = {
type: 'update_object';
objectMetadataId: string;
updates: Array<
{
[P in FlatObjectMetadataPropertiesToCompare]: PropertyUpdate<
FlatObjectMetadata,
P
>;
}[FlatObjectMetadataPropertiesToCompare]
>;
updates: FlatObjectPropertiesUpdates;
};
export type DeleteObjectAction = {
@@ -4,17 +4,15 @@ import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
import { 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 { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util';
import { validateFlatObjectMetadataNameAndLabels } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util';
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { doesOtherObjectWithSameNameExists } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util';
import { ObjectMetadataRelatedFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type';
import { FlatObjectPropertiesUpdates } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
export type ValidateOneObjectMetadataArgs = {
flatObjectMetadataToValidate: FlatObjectMetadata;
@@ -28,8 +26,11 @@ export class FlatObjectMetadataValidatorService {
public validateFlatObjectMetadataUpdate({
optimisticFlatObjectMetadataMaps,
flatObjectMetadataToValidate: updatedFlatObjectMetadata,
// dependencyOptimisticFlatEntityMaps: { flatFieldMetadataMaps },
}: ValidateOneObjectMetadataArgs): FailedFlatEntityValidation<FlatObjectMetadata> {
dependencyOptimisticFlatEntityMaps: { flatFieldMetadataMaps },
flatObjectPropertiesUpdates,
}: ValidateOneObjectMetadataArgs & {
flatObjectPropertiesUpdates: FlatObjectPropertiesUpdates;
}): FailedFlatEntityValidation<FlatObjectMetadata> {
const validationResult: FailedFlatEntityValidation<FlatObjectMetadata> = {
type: 'update_object',
errors: [],
@@ -57,24 +58,34 @@ export class FlatObjectMetadataValidatorService {
};
validationResult.errors.push(
...this.validateFlatObjectMetadataNameAndLabels({
...validateFlatObjectMetadataNameAndLabels({
optimisticFlatObjectMetadataMaps,
flatObjectMetadataToValidate: updatedFlatObjectMetadata,
}),
);
// const { objectFlatFieldMetadatas } =
// findObjectFieldsInFlatFieldMetadataMapsOrThrow({
// flatFieldMetadataMaps,
// flatObjectMetadata: existingFlatObjectMetadata,
// });
const labelIdentifierFieldMetadataIdUpdate =
flatObjectPropertiesUpdates.find(
(update) => update.property === 'labelIdentifierFieldMetadataId',
);
// validationResult.errors.push(
// ...validateFlatObjectMetadataIdentifiers({
// flatObjectMetadata: existingFlatObjectMetadata,
// objectFlatFieldMetadatas,
// }),
// );
// TODO remove this once we migrated labelIdentifierFieldMetadataId as non nullable
if (isDefined(labelIdentifierFieldMetadataIdUpdate)) {
if (!isDefined(labelIdentifierFieldMetadataIdUpdate.to)) {
validationResult.errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: 'labelIdentifierFieldMetadataId cannot be null',
userFriendlyMessage: t`Field label identifier is required`,
});
}
validationResult.errors.push(
...validateFlatObjectMetadataIdentifiers({
flatObjectMetadata: updatedFlatObjectMetadata,
flatFieldMetadataMaps,
}),
);
}
return validationResult;
}
@@ -142,7 +153,7 @@ export class FlatObjectMetadataValidatorService {
public async validateFlatObjectMetadataCreation({
optimisticFlatObjectMetadataMaps,
flatObjectMetadataToValidate,
// dependencyOptimisticFlatEntityMaps: { flatFieldMetadataMaps },
dependencyOptimisticFlatEntityMaps: { flatFieldMetadataMaps },
}: ValidateOneObjectMetadataArgs): Promise<
FailedFlatEntityValidation<FlatObjectMetadata>
> {
@@ -177,20 +188,14 @@ export class FlatObjectMetadataValidatorService {
});
}
// const { objectFlatFieldMetadatas } =
// findObjectFieldsInFlatFieldMetadataMapsOrThrow({
// flatFieldMetadataMaps,
// flatObjectMetadata: flatObjectMetadataToValidate,
// });
// objectValidationResult.errors.push(
// ...validateFlatObjectMetadataIdentifiers({
// flatObjectMetadata: flatObjectMetadataToValidate,
// objectFlatFieldMetadatas,
// }),
// );
objectValidationResult.errors.push(
...this.validateFlatObjectMetadataNameAndLabels({
...validateFlatObjectMetadataIdentifiers({
flatObjectMetadata: flatObjectMetadataToValidate,
flatFieldMetadataMaps,
}),
);
objectValidationResult.errors.push(
...validateFlatObjectMetadataNameAndLabels({
optimisticFlatObjectMetadataMaps,
flatObjectMetadataToValidate,
}),
@@ -198,56 +203,4 @@ export class FlatObjectMetadataValidatorService {
return objectValidationResult;
}
private 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;
}
}
@@ -8,7 +8,7 @@ import {
import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
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 { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -90,11 +90,10 @@ export class DeleteObjectActionHandlerService extends WorkspaceMigrationRunnerAc
schemaName,
tableName,
});
const { objectFlatFieldMetadatas } =
findObjectFieldsInFlatFieldMetadataMapsOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata,
});
const { objectFlatFieldMetadatas } = findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata,
});
const enumOrCompositeFlatFieldMetadatas = objectFlatFieldMetadatas.filter(
(field) =>
isEnumFlatFieldMetadata(field) || isCompositeFlatFieldMetadata(field),
@@ -8,7 +8,7 @@ import {
import { 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 { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
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 { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -122,7 +122,7 @@ export class UpdateObjectActionHandlerService extends WorkspaceMigrationRunnerAc
});
const { objectFlatFieldMetadatas } =
findObjectFieldsInFlatFieldMetadataMapsOrThrow({
findObjectFlatFieldMetadatasOrThrow({
flatFieldMetadataMaps,
flatObjectMetadata: updatedObjectMetadata,
});