Validate universalIdentifier uniqueness among application and its dependencies (#19767)
# Introduction Gracefully validating that when creating an entity its `universalIdentifier` is available within the all application metadata maps context ( current app + twenty standard, currently the only managed dependencies )
This commit is contained in:
+6
-3
@@ -13,6 +13,7 @@ import {
|
||||
import { AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
|
||||
import { aggregateOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report.util';
|
||||
import { crossEntityTransversalValidation } from 'src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util';
|
||||
import { mergeOrchestratorFailureReports } from 'src/engine/workspace-manager/workspace-migration/utils/merge-orchestrator-failure-reports.util';
|
||||
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
|
||||
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
|
||||
import { WorkspaceMigrationFieldPermissionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/workspace-migration-field-permission-actions-builder.service';
|
||||
@@ -822,14 +823,16 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
}
|
||||
}
|
||||
|
||||
const { objectMetadata, viewField } = crossEntityTransversalValidation({
|
||||
const crossEntityFailureReport = crossEntityTransversalValidation({
|
||||
optimisticUniversalFlatMaps: optimisticAllFlatEntityMaps,
|
||||
orchestratorActionsReport,
|
||||
preDeletionFlatViewFieldMaps,
|
||||
});
|
||||
|
||||
orchestratorFailureReport.objectMetadata.push(...objectMetadata);
|
||||
orchestratorFailureReport.viewField.push(...viewField);
|
||||
mergeOrchestratorFailureReports({
|
||||
target: orchestratorFailureReport,
|
||||
source: crossEntityFailureReport,
|
||||
});
|
||||
|
||||
const allErrors = Object.values(orchestratorFailureReport);
|
||||
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
|
||||
import { MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { buildAllUniversalIdentifierMap } from 'src/engine/workspace-manager/workspace-migration/utils/build-all-universal-identifier-map.util';
|
||||
|
||||
describe('buildAllUniversalIdentifierMap', () => {
|
||||
it('should return an empty map when all flat entity maps are empty', () => {
|
||||
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
|
||||
|
||||
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
|
||||
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should collect a single entity from one metadata type', () => {
|
||||
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
|
||||
|
||||
allFlatEntityMaps.flatRoleMaps.byUniversalIdentifier = {
|
||||
'role-uid-1': {
|
||||
universalIdentifier: 'role-uid-1',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
} as MetadataFlatEntity<'role'>,
|
||||
};
|
||||
|
||||
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get('role-uid-1')).toEqual({
|
||||
metadataName: 'role',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should collect entities across multiple metadata types', () => {
|
||||
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
|
||||
|
||||
allFlatEntityMaps.flatRoleMaps.byUniversalIdentifier = {
|
||||
'role-uid-1': {
|
||||
universalIdentifier: 'role-uid-1',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
} as MetadataFlatEntity<'role'>,
|
||||
};
|
||||
|
||||
allFlatEntityMaps.flatObjectPermissionMaps.byUniversalIdentifier = {
|
||||
'perm-uid-1': {
|
||||
universalIdentifier: 'perm-uid-1',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
} as MetadataFlatEntity<'objectPermission'>,
|
||||
};
|
||||
|
||||
allFlatEntityMaps.flatViewMaps.byUniversalIdentifier = {
|
||||
'view-uid-1': {
|
||||
universalIdentifier: 'view-uid-1',
|
||||
applicationUniversalIdentifier: 'app-uid-2',
|
||||
} as MetadataFlatEntity<'view'>,
|
||||
};
|
||||
|
||||
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
|
||||
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.get('role-uid-1')).toEqual({
|
||||
metadataName: 'role',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
});
|
||||
expect(result.get('perm-uid-1')).toEqual({
|
||||
metadataName: 'objectPermission',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
});
|
||||
expect(result.get('view-uid-1')).toEqual({
|
||||
metadataName: 'view',
|
||||
applicationUniversalIdentifier: 'app-uid-2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip undefined entries in byUniversalIdentifier', () => {
|
||||
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
|
||||
|
||||
allFlatEntityMaps.flatRoleMaps.byUniversalIdentifier = {
|
||||
'role-uid-1': undefined,
|
||||
'role-uid-2': {
|
||||
universalIdentifier: 'role-uid-2',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
} as MetadataFlatEntity<'role'>,
|
||||
};
|
||||
|
||||
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.has('role-uid-1')).toBe(false);
|
||||
expect(result.get('role-uid-2')).toEqual({
|
||||
metadataName: 'role',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple entities within the same metadata type', () => {
|
||||
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
|
||||
|
||||
allFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier = {
|
||||
'field-uid-1': {
|
||||
universalIdentifier: 'field-uid-1',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
} as MetadataFlatEntity<'fieldMetadata'>,
|
||||
'field-uid-2': {
|
||||
universalIdentifier: 'field-uid-2',
|
||||
applicationUniversalIdentifier: 'app-uid-1',
|
||||
} as MetadataFlatEntity<'fieldMetadata'>,
|
||||
'field-uid-3': {
|
||||
universalIdentifier: 'field-uid-3',
|
||||
applicationUniversalIdentifier: 'app-uid-2',
|
||||
} as MetadataFlatEntity<'fieldMetadata'>,
|
||||
};
|
||||
|
||||
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
|
||||
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.get('field-uid-1')?.metadataName).toBe('fieldMetadata');
|
||||
expect(result.get('field-uid-2')?.metadataName).toBe('fieldMetadata');
|
||||
expect(result.get('field-uid-3')?.applicationUniversalIdentifier).toBe(
|
||||
'app-uid-2',
|
||||
);
|
||||
});
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
ALL_METADATA_NAME,
|
||||
type AllMetadataName,
|
||||
} from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
|
||||
|
||||
export type UniversalIdentifierOwner = {
|
||||
metadataName: AllMetadataName;
|
||||
applicationUniversalIdentifier: string;
|
||||
};
|
||||
|
||||
export type AllUniversalIdentifierMap = Map<string, UniversalIdentifierOwner>;
|
||||
|
||||
export const buildAllUniversalIdentifierMap = (
|
||||
allFlatEntityMaps: AllUniversalFlatEntityMaps,
|
||||
): AllUniversalIdentifierMap => {
|
||||
const universalIdentifierMap: AllUniversalIdentifierMap = new Map();
|
||||
|
||||
for (const metadataName of Object.values(ALL_METADATA_NAME)) {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName);
|
||||
const flatEntityMaps = allFlatEntityMaps[flatEntityMapsKey];
|
||||
|
||||
if (!isDefined(flatEntityMaps)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [universalIdentifier, entity] of Object.entries(
|
||||
flatEntityMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (isDefined(entity)) {
|
||||
universalIdentifierMap.set(universalIdentifier, {
|
||||
metadataName,
|
||||
applicationUniversalIdentifier: entity.applicationUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return universalIdentifierMap;
|
||||
};
|
||||
+15
-2
@@ -4,9 +4,11 @@ import {
|
||||
type OrchestratorActionsReport,
|
||||
type OrchestratorFailureReport,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
|
||||
import { EMPTY_ORCHESTRATOR_FAILURE_REPORT } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-failure-report.constant';
|
||||
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
|
||||
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
import { validateUniversalIdentifierCrossEntityUniquenessThroughReportMutation } from 'src/engine/workspace-manager/workspace-migration/utils/validate-universal-identifier-cross-entity-uniqueness-through-report-mutation.util';
|
||||
|
||||
export const crossEntityTransversalValidation = ({
|
||||
optimisticUniversalFlatMaps,
|
||||
@@ -16,7 +18,9 @@ export const crossEntityTransversalValidation = ({
|
||||
optimisticUniversalFlatMaps: AllUniversalFlatEntityMaps;
|
||||
orchestratorActionsReport: OrchestratorActionsReport;
|
||||
preDeletionFlatViewFieldMaps: UniversalFlatEntityMaps<UniversalFlatViewField>;
|
||||
}): Pick<OrchestratorFailureReport, 'objectMetadata' | 'viewField'> => {
|
||||
}): OrchestratorFailureReport => {
|
||||
const crossEntityFailureReport = EMPTY_ORCHESTRATOR_FAILURE_REPORT();
|
||||
|
||||
const { objectMetadata } = validateObjectMetadataCrossEntity({
|
||||
optimisticUniversalFlatMaps,
|
||||
orchestratorActionsReport,
|
||||
@@ -28,5 +32,14 @@ export const crossEntityTransversalValidation = ({
|
||||
preDeletionFlatViewFieldMaps,
|
||||
});
|
||||
|
||||
return { objectMetadata, viewField };
|
||||
crossEntityFailureReport.objectMetadata.push(...objectMetadata);
|
||||
crossEntityFailureReport.viewField.push(...viewField);
|
||||
|
||||
validateUniversalIdentifierCrossEntityUniquenessThroughReportMutation({
|
||||
optimisticUniversalFlatMaps,
|
||||
orchestratorActionsReport,
|
||||
orchestratorFailureReport: crossEntityFailureReport,
|
||||
});
|
||||
|
||||
return crossEntityFailureReport;
|
||||
};
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
ALL_METADATA_NAME,
|
||||
type AllMetadataName,
|
||||
} from 'twenty-shared/metadata';
|
||||
|
||||
import { type WorkspaceMigrationActionType } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type';
|
||||
import { type OrchestratorFailureReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
|
||||
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
|
||||
type AnyFailedValidation = FailedFlatEntityValidation<
|
||||
AllMetadataName,
|
||||
WorkspaceMigrationActionType
|
||||
>;
|
||||
|
||||
export const pushToOrchestratorFailureReport = <
|
||||
TMetadataName extends AllMetadataName,
|
||||
>({
|
||||
report,
|
||||
metadataName,
|
||||
items,
|
||||
}: {
|
||||
report: OrchestratorFailureReport;
|
||||
metadataName: TMetadataName;
|
||||
items: FailedFlatEntityValidation<
|
||||
TMetadataName,
|
||||
WorkspaceMigrationActionType
|
||||
>[];
|
||||
}): void => {
|
||||
(report[metadataName] as AnyFailedValidation[]).push(...items);
|
||||
};
|
||||
|
||||
export const mergeOrchestratorFailureReports = ({
|
||||
target,
|
||||
source,
|
||||
}: {
|
||||
target: OrchestratorFailureReport;
|
||||
source: OrchestratorFailureReport;
|
||||
}): void => {
|
||||
for (const metadataName of Object.values(ALL_METADATA_NAME)) {
|
||||
pushToOrchestratorFailureReport({
|
||||
report: target,
|
||||
metadataName,
|
||||
items: source[metadataName],
|
||||
});
|
||||
}
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FlatEntityMapsExceptionCode } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import {
|
||||
type OrchestratorActionsReport,
|
||||
type OrchestratorFailureReport,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
|
||||
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
|
||||
import { buildAllUniversalIdentifierMap } from 'src/engine/workspace-manager/workspace-migration/utils/build-all-universal-identifier-map.util';
|
||||
import { pushToOrchestratorFailureReport } from 'src/engine/workspace-manager/workspace-migration/utils/merge-orchestrator-failure-reports.util';
|
||||
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
|
||||
|
||||
export const validateUniversalIdentifierCrossEntityUniquenessThroughReportMutation =
|
||||
({
|
||||
optimisticUniversalFlatMaps,
|
||||
orchestratorActionsReport,
|
||||
orchestratorFailureReport,
|
||||
}: {
|
||||
optimisticUniversalFlatMaps: AllUniversalFlatEntityMaps;
|
||||
orchestratorActionsReport: OrchestratorActionsReport;
|
||||
orchestratorFailureReport: OrchestratorFailureReport;
|
||||
}): void => {
|
||||
const allUniversalIdentifierMap = buildAllUniversalIdentifierMap(
|
||||
optimisticUniversalFlatMaps,
|
||||
);
|
||||
|
||||
for (const metadataName of Object.values(ALL_METADATA_NAME)) {
|
||||
const createActions = orchestratorActionsReport[metadataName]?.create;
|
||||
|
||||
if (!isDefined(createActions) || createActions.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const createAction of createActions) {
|
||||
const universalIdentifier = createAction.flatEntity
|
||||
.universalIdentifier as string;
|
||||
|
||||
const existingOwner =
|
||||
allUniversalIdentifierMap.get(universalIdentifier);
|
||||
|
||||
if (!existingOwner || existingOwner.metadataName === metadataName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const failedValidation = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier,
|
||||
},
|
||||
metadataName,
|
||||
type: 'create',
|
||||
});
|
||||
|
||||
failedValidation.errors.push({
|
||||
code: FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS,
|
||||
message: `Cannot create ${metadataName}: universalIdentifier "${universalIdentifier}" is already taken by ${existingOwner.metadataName} from application "${existingOwner.applicationUniversalIdentifier}"`,
|
||||
});
|
||||
|
||||
pushToOrchestratorFailureReport({
|
||||
report: orchestratorFailureReport,
|
||||
metadataName,
|
||||
items: [failedValidation],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+45
-4
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
import { AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { type 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';
|
||||
@@ -18,6 +18,7 @@ import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-e
|
||||
import { WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-builder-additional-cache-data-maps.type';
|
||||
import { AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
|
||||
import { MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-maps.type';
|
||||
import { UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
|
||||
import { addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
|
||||
import { deleteUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util';
|
||||
import { deleteUniversalFlatEntityFromUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-from-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
|
||||
@@ -362,26 +363,66 @@ export abstract class WorkspaceEntityMigrationBuilderService<
|
||||
return [];
|
||||
}
|
||||
|
||||
private validateUniversalIdentifierNotAlreadyInCurrentMetadataMaps({
|
||||
universalIdentifier,
|
||||
universalFlatEntityMaps,
|
||||
}: {
|
||||
universalFlatEntityMaps: UniversalFlatEntityMaps<
|
||||
MetadataFlatEntity<typeof this.metadataName>
|
||||
>;
|
||||
universalIdentifier: string;
|
||||
}): FlatEntityValidationError[] {
|
||||
const existingEntity = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: universalFlatEntityMaps,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
if (isDefined(existingEntity)) {
|
||||
return [
|
||||
{
|
||||
code: FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS,
|
||||
message: `Cannot create ${this.metadataName}: universalIdentifier "${universalIdentifier}" already exists in ${this.metadataName} maps from application "${existingEntity.applicationUniversalIdentifier}"`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private async innerValidateFlatEntityCreation(
|
||||
args: UniversalFlatEntityValidationArgs<T>,
|
||||
): Promise<UniversalFlatEntityValidationReturnType<T, 'create'>> {
|
||||
const uuidValidationResult = this.validateUniversalIdentifier(args);
|
||||
const perTypeExistenceResult =
|
||||
this.validateUniversalIdentifierNotAlreadyInCurrentMetadataMaps({
|
||||
universalIdentifier: args.flatEntityToValidate.universalIdentifier,
|
||||
universalFlatEntityMaps:
|
||||
args.optimisticFlatEntityMapsAndRelatedFlatEntityMaps[
|
||||
getMetadataFlatEntityMapsKey(this.metadataName)
|
||||
],
|
||||
});
|
||||
|
||||
const centralizedErrors = [
|
||||
...uuidValidationResult,
|
||||
...perTypeExistenceResult,
|
||||
];
|
||||
|
||||
const result = await this.validateFlatEntityCreation(args);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
return {
|
||||
...result,
|
||||
errors: [...result.errors, ...uuidValidationResult],
|
||||
errors: [...result.errors, ...centralizedErrors],
|
||||
};
|
||||
}
|
||||
|
||||
if (result.status === 'success' && uuidValidationResult.length > 0) {
|
||||
if (result.status === 'success' && centralizedErrors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier: args.flatEntityToValidate.universalIdentifier,
|
||||
} as Partial<MetadataFlatEntity<T>>,
|
||||
errors: uuidValidationResult,
|
||||
errors: centralizedErrors,
|
||||
metadataName: this.metadataName,
|
||||
type: 'create',
|
||||
};
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`Sync application should fail on universalIdentifier conflicts should fail when an entity uses a universalIdentifier from the twenty-standard app 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"role": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "ENTITY_ALREADY_EXISTS",
|
||||
"message": "Cannot create role: universalIdentifier "20202020-02c2-43f2-b94d-cab1f2b532eb" already exists in role maps from application "20202020-64aa-4b6f-b003-9c74b97cee20"",
|
||||
},
|
||||
],
|
||||
"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[`Sync application should fail on universalIdentifier conflicts should fail when two entities in the same manifest share the same universalIdentifier 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"role": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "ENTITY_ALREADY_EXISTS",
|
||||
"message": "Cannot create role: universalIdentifier "a1b2c3d4-0003-4000-a000-000000000003" is already taken by objectPermission from application "a1b2c3d4-0001-4000-a000-000000000001"",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "role",
|
||||
"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",
|
||||
}
|
||||
`;
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
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 { 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 { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant';
|
||||
|
||||
const TEST_APP_ID = 'a1b2c3d4-0001-4000-a000-000000000001';
|
||||
const TEST_ROLE_ID = 'a1b2c3d4-0002-4000-a000-000000000002';
|
||||
const DUPLICATED_UNIVERSAL_IDENTIFIER =
|
||||
'a1b2c3d4-0003-4000-a000-000000000003';
|
||||
|
||||
describe('Sync application should fail on universalIdentifier conflicts', () => {
|
||||
beforeAll(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test Universal Id Conflict App',
|
||||
description: 'App for testing universalIdentifier conflict detection',
|
||||
sourcePath: 'test-universal-id-conflict',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await globalThis.testDataSource.query(
|
||||
`DELETE FROM core."role" WHERE "universalIdentifier" = $1`,
|
||||
[TEST_ROLE_ID],
|
||||
);
|
||||
|
||||
await globalThis.testDataSource.query(
|
||||
`DELETE FROM core."file" WHERE "applicationId" IN (
|
||||
SELECT id FROM core."application" WHERE "universalIdentifier" = $1
|
||||
)`,
|
||||
[TEST_APP_ID],
|
||||
);
|
||||
|
||||
await globalThis.testDataSource.query(
|
||||
`DELETE FROM core."application"
|
||||
WHERE "universalIdentifier" = $1`,
|
||||
[TEST_APP_ID],
|
||||
);
|
||||
|
||||
await globalThis.testDataSource.query(
|
||||
`DELETE FROM core."applicationRegistration"
|
||||
WHERE "universalIdentifier" = $1`,
|
||||
[TEST_APP_ID],
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail when two entities in the same manifest share the same universalIdentifier', async () => {
|
||||
const manifest = buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides: {
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'First Role',
|
||||
description: 'First role',
|
||||
},
|
||||
{
|
||||
universalIdentifier: DUPLICATED_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Second Role',
|
||||
description: 'Second role',
|
||||
objectPermissions: [
|
||||
{
|
||||
universalIdentifier: DUPLICATED_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECTS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { errors } = await syncApplication({
|
||||
manifest,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
}, 60000);
|
||||
|
||||
it('should fail when an entity uses a universalIdentifier from the twenty-standard app', async () => {
|
||||
const manifest = buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides: {
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'App Default Role',
|
||||
description: 'Default role for the test app',
|
||||
},
|
||||
{
|
||||
universalIdentifier: STANDARD_ROLE.admin.universalIdentifier,
|
||||
label: 'Stolen Admin Role',
|
||||
description:
|
||||
'Attempts to create a role with the standard admin universalIdentifier',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { errors } = await syncApplication({
|
||||
manifest,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
}, 60000);
|
||||
});
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.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 { type FieldManifest } from 'twenty-shared/application';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
const REUSABLE_UID = uuidv4();
|
||||
|
||||
const TEST_OBJECT = buildDefaultObjectManifest({
|
||||
universalIdentifier: REUSABLE_UID,
|
||||
nameSingular: 'ephemeral',
|
||||
namePlural: 'ephemerals',
|
||||
labelSingular: 'Ephemeral',
|
||||
labelPlural: 'Ephemerals',
|
||||
description: 'An ephemeral object that will be removed on second sync',
|
||||
icon: 'IconTrash',
|
||||
});
|
||||
|
||||
const REUSED_UID_FIELD: FieldManifest = {
|
||||
universalIdentifier: REUSABLE_UID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'reusedUidField',
|
||||
label: 'Reused UID Field',
|
||||
description:
|
||||
'Field that reuses the universalIdentifier from the deleted object',
|
||||
icon: 'IconRefresh',
|
||||
objectUniversalIdentifier: STANDARD_OBJECTS.company.universalIdentifier,
|
||||
};
|
||||
|
||||
describe('Cross-entity universalIdentifier reuse across syncs', () => {
|
||||
beforeAll(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test UID Reuse App',
|
||||
description:
|
||||
'App for testing universalIdentifier reuse across entity types',
|
||||
sourcePath: 'test-uid-reuse',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await uninstallApplication({
|
||||
universalIdentifier: TEST_APP_ID,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow reusing a universalIdentifier from a deleted object as a field in the same sync', async () => {
|
||||
const firstManifest = buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides: {
|
||||
objects: [TEST_OBJECT],
|
||||
},
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: firstManifest,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const secondManifest = buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides: {
|
||||
objects: [],
|
||||
fields: [REUSED_UID_FIELD],
|
||||
},
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: secondManifest,
|
||||
expectToFail: false,
|
||||
});
|
||||
}, 60000);
|
||||
});
|
||||
Reference in New Issue
Block a user