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:
Paul Rastoin
2026-04-16 18:59:12 +02:00
committed by GitHub
parent bf410ae438
commit 75235f4621
10 changed files with 610 additions and 9 deletions
@@ -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);
@@ -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',
);
});
});
@@ -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;
};
@@ -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;
};
@@ -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],
});
}
};
@@ -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],
});
}
}
};
@@ -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',
};
@@ -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",
}
`;
@@ -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);
});
@@ -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);
});