Centralized universal identifier validation on create (#18258)
This commit is contained in:
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { type WorkspaceMigrationActionType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { type WorkspaceMigrationActionType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
|
||||
export type FlatEntityValidationError<TCode extends string = string> = {
|
||||
code: TCode;
|
||||
|
||||
+50
-1
@@ -3,6 +3,7 @@ import { Inject } from '@nestjs/common';
|
||||
import { AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { validate as uuidValidate, version as uuidVersion } from 'uuid';
|
||||
|
||||
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
|
||||
import {
|
||||
@@ -26,6 +27,7 @@ import { resetUniversalFlatEntityForeignKeyAggregators } from 'src/engine/worksp
|
||||
import { flatEntityDeletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/universal-flat-entity-deleted-created-updated-matrix-dispatcher.util';
|
||||
import { getMetadataEmptyWorkspaceMigrationActionRecord } from 'src/engine/workspace-manager/workspace-migration/utils/get-metadata-empty-workspace-migration-action-record.util';
|
||||
import { shouldInferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/utils/should-infer-deletion-from-missing-entities.util';
|
||||
import { FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
import { FailedFlatEntityValidateAndBuild } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/failed-flat-entity-validate-and-build.type';
|
||||
import { SuccessfulFlatEntityValidateAndBuild } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/successful-flat-entity-validate-and-build.type';
|
||||
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
|
||||
@@ -132,7 +134,7 @@ export abstract class WorkspaceEntityMigrationBuilderService<
|
||||
},
|
||||
);
|
||||
|
||||
const validationResult = await this.validateFlatEntityCreation({
|
||||
const validationResult = await this.innerValidateFlatEntityCreation({
|
||||
additionalCacheDataMaps,
|
||||
flatEntityToValidate: universalFlatEntityToCreate,
|
||||
workspaceId,
|
||||
@@ -331,6 +333,53 @@ export abstract class WorkspaceEntityMigrationBuilderService<
|
||||
};
|
||||
}
|
||||
|
||||
private validateUniversalIdentifier({
|
||||
flatEntityToValidate: { universalIdentifier },
|
||||
}: UniversalFlatEntityValidationArgs<T>): FlatEntityValidationError[] {
|
||||
if (
|
||||
!uuidValidate(universalIdentifier) ||
|
||||
uuidVersion(universalIdentifier) !== 4
|
||||
) {
|
||||
return [
|
||||
{
|
||||
code: FlatEntityMapsExceptionCode.ENTITY_MALFORMED,
|
||||
message: `Invalid universalIdentifier: "${universalIdentifier}" is not a valid UUID v4`,
|
||||
value: universalIdentifier,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private async innerValidateFlatEntityCreation(
|
||||
args: UniversalFlatEntityValidationArgs<T>,
|
||||
): Promise<UniversalFlatEntityValidationReturnType<T, 'create'>> {
|
||||
const uuidValidationResult = this.validateUniversalIdentifier(args);
|
||||
const result = await this.validateFlatEntityCreation(args);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
return {
|
||||
...result,
|
||||
errors: [...result.errors, ...uuidValidationResult],
|
||||
};
|
||||
}
|
||||
|
||||
if (result.status === 'success' && uuidValidationResult.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier: args.flatEntityToValidate.universalIdentifier,
|
||||
} as Partial<MetadataFlatEntity<T>>,
|
||||
errors: uuidValidationResult,
|
||||
metadataName: this.metadataName,
|
||||
type: 'create',
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected abstract validateFlatEntityCreation(
|
||||
args: UniversalFlatEntityValidationArgs<T>,
|
||||
):
|
||||
|
||||
+35
@@ -1,5 +1,40 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`Install application should fail when entity does not exist should fail when a role has an invalid universalIdentifier 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"role": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "ENTITY_MALFORMED",
|
||||
"message": "Invalid universalIdentifier: "not-a-valid-uuid" is not a valid UUID v4",
|
||||
"value": "not-a-valid-uuid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "role",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 role",
|
||||
"summary": {
|
||||
"role": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Install application should fail when entity does not exist should fail with execution error when deleting non-existent field metadata 1`] = `
|
||||
{
|
||||
"eventId": Any<String>,
|
||||
|
||||
+54
-1
@@ -1,10 +1,20 @@
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { installApplication } from 'test/integration/metadata/suites/application/utils/install-application.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
const INVALID_UUID_APP_ID = uuidv4();
|
||||
const INVALID_UUID_ROLE_ID = uuidv4();
|
||||
|
||||
describe('Install application should fail when entity does not exist', () => {
|
||||
let appCreated = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag:
|
||||
@@ -12,7 +22,16 @@ describe('Install application should fail when entity does not exist', () => {
|
||||
value: true,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: INVALID_UUID_APP_ID,
|
||||
name: 'Test Invalid UUID App',
|
||||
description: 'App for testing UUID v4 validation',
|
||||
sourcePath: 'test-invalid-uuid',
|
||||
});
|
||||
|
||||
appCreated = true;
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
@@ -23,6 +42,17 @@ describe('Install application should fail when entity does not exist', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!appCreated) {
|
||||
return;
|
||||
}
|
||||
|
||||
await uninstallApplication({
|
||||
universalIdentifier: INVALID_UUID_APP_ID,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with execution error when deleting non-existent field metadata', async () => {
|
||||
const { errors } = await installApplication({
|
||||
expectToFail: true,
|
||||
@@ -41,4 +71,27 @@ describe('Install application should fail when entity does not exist', () => {
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
});
|
||||
|
||||
it('should fail when a role has an invalid universalIdentifier', async () => {
|
||||
const manifest = buildBaseManifest({
|
||||
appId: INVALID_UUID_APP_ID,
|
||||
roleId: INVALID_UUID_ROLE_ID,
|
||||
overrides: {
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: 'not-a-valid-uuid',
|
||||
label: 'Invalid UUID Role',
|
||||
description: 'Role with invalid universalIdentifier',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { errors } = await syncApplication({
|
||||
manifest,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user