Builder runs delete update and then create (#18272)

# Introduction
We need to build and validate the flat entity operation in the following
order delete update and create
For example if not, if a created field has the same name than a deleted
one than it will fail whereas it should not
This commit is contained in:
Paul Rastoin
2026-03-03 11:26:06 +01:00
committed by GitHub
parent 0b766464e4
commit 58e37a118c
3 changed files with 489 additions and 74 deletions
@@ -103,79 +103,7 @@ export abstract class WorkspaceEntityMigrationBuilderService<
);
const allValidationResult: FailedFlatEntityValidateAndBuild<T>['errors'] =
[];
const remainingFlatEntityMapsToCreate = structuredClone(
createdFlatEntityMaps,
);
this.logger.time(
`EntityBuilder ${this.metadataName}`,
'creation validation',
);
for (const flatEntityToCreateUniversalIdentifier in createdFlatEntityMaps.byUniversalIdentifier) {
const rawUniversalflatEntityToCreate =
findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier: flatEntityToCreateUniversalIdentifier,
flatEntityMaps: createdFlatEntityMaps,
});
const universalFlatEntityToCreate =
resetUniversalFlatEntityForeignKeyAggregators({
metadataName: this.metadataName,
universalFlatEntity: rawUniversalflatEntityToCreate,
});
const universalIdentifierToDelete =
universalFlatEntityToCreate.universalIdentifier;
deleteUniversalFlatEntityFromUniversalFlatEntityMapsThroughMutationOrThrow(
{
universalIdentifierToDelete,
universalFlatEntityMapsToMutate: remainingFlatEntityMapsToCreate,
},
);
const validationResult = await this.innerValidateFlatEntityCreation({
additionalCacheDataMaps,
flatEntityToValidate: universalFlatEntityToCreate,
workspaceId,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
remainingFlatEntityMapsToValidate: remainingFlatEntityMapsToCreate,
buildOptions,
});
if (validationResult.status === 'fail') {
allValidationResult.push(validationResult);
continue;
}
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
universalFlatEntity: universalFlatEntityToCreate,
universalFlatEntityAndRelatedMapsToMutate:
optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
metadataName: this.metadataName,
},
);
const formattedNewCreateAction: AllUniversalWorkspaceMigrationAction<
'create',
typeof this.metadataName
> = {
...validationResult.action,
flatEntity: deleteUniversalFlatEntityForeignKeyAggregators({
metadataName: this.metadataName,
universalFlatEntity: validationResult.action
.flatEntity as MetadataFlatEntity<T>,
}),
};
actionsResult.create.push(formattedNewCreateAction);
}
this.logger.timeEnd(
`EntityBuilder ${this.metadataName}`,
'creation validation',
);
this.logger.time(
`EntityBuilder ${this.metadataName}`,
'deletion validation',
@@ -310,6 +238,80 @@ export abstract class WorkspaceEntityMigrationBuilderService<
`EntityBuilder ${this.metadataName}`,
'update validation',
);
const remainingFlatEntityMapsToCreate = structuredClone(
createdFlatEntityMaps,
);
this.logger.time(
`EntityBuilder ${this.metadataName}`,
'creation validation',
);
for (const flatEntityToCreateUniversalIdentifier in createdFlatEntityMaps.byUniversalIdentifier) {
const rawUniversalflatEntityToCreate =
findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier: flatEntityToCreateUniversalIdentifier,
flatEntityMaps: createdFlatEntityMaps,
});
const universalFlatEntityToCreate =
resetUniversalFlatEntityForeignKeyAggregators({
metadataName: this.metadataName,
universalFlatEntity: rawUniversalflatEntityToCreate,
});
const universalIdentifierToDelete =
universalFlatEntityToCreate.universalIdentifier;
deleteUniversalFlatEntityFromUniversalFlatEntityMapsThroughMutationOrThrow(
{
universalIdentifierToDelete,
universalFlatEntityMapsToMutate: remainingFlatEntityMapsToCreate,
},
);
const validationResult = await this.innerValidateFlatEntityCreation({
additionalCacheDataMaps,
flatEntityToValidate: universalFlatEntityToCreate,
workspaceId,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
remainingFlatEntityMapsToValidate: remainingFlatEntityMapsToCreate,
buildOptions,
});
if (validationResult.status === 'fail') {
allValidationResult.push(validationResult);
continue;
}
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
universalFlatEntity: universalFlatEntityToCreate,
universalFlatEntityAndRelatedMapsToMutate:
optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
metadataName: this.metadataName,
},
);
const formattedNewCreateAction: AllUniversalWorkspaceMigrationAction<
'create',
typeof this.metadataName
> = {
...validationResult.action,
flatEntity: deleteUniversalFlatEntityForeignKeyAggregators({
metadataName: this.metadataName,
universalFlatEntity: validationResult.action
.flatEntity as MetadataFlatEntity<T>,
}),
};
actionsResult.create.push(formattedNewCreateAction);
}
this.logger.timeEnd(
`EntityBuilder ${this.metadataName}`,
'creation validation',
);
this.logger.timeEnd(
`EntityBuilder ${this.metadataName}`,
'entity processing',
@@ -63,6 +63,358 @@ exports[`syncApplication should create a TEXT field on the standard Company obje
}
`;
exports[`syncApplication should delete old field and create equivalent one when field universalIdentifier changes 1`] = `
{
"syncApplication": {
"actions": [
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"description": "A support ticket",
"duplicateCriteria": null,
"icon": "IconTicket",
"imageIdentifierFieldMetadataUniversalIdentifier": null,
"isActive": true,
"isAuditLogged": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isRemote": false,
"isSearchable": false,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataUniversalIdentifier": Any<String>,
"labelPlural": "Tickets",
"labelSingular": "Ticket",
"namePlural": "tickets",
"nameSingular": "ticket",
"shortcut": null,
"standardOverrides": null,
"targetTableName": "DEPRECATED",
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "objectMetadata",
"type": "create",
"universalFlatFieldMetadatas": [
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Id",
"morphId": null,
"name": "id",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "UUID",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Creation date",
"morphId": null,
"name": "createdAt",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "DATE_TIME",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Last update",
"morphId": null,
"name": "updatedAt",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "DATE_TIME",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Deleted at",
"morphId": null,
"name": "deletedAt",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "DATE_TIME",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": {
"name": "'System'",
"source": "'MANUAL'",
"workspaceMemberId": null,
},
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Created by",
"morphId": null,
"name": "createdBy",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "ACTOR",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": {
"name": "'System'",
"source": "'MANUAL'",
"workspaceMemberId": null,
},
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Updated by",
"morphId": null,
"name": "updatedBy",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "ACTOR",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Position",
"morphId": null,
"name": "position",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "POSITION",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Search vector",
"morphId": null,
"name": "searchVector",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "TS_VECTOR",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": "Ticket description",
"icon": "IconFileDescription",
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Description",
"morphId": null,
"name": "description",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
],
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"canAccessAllTools": false,
"canBeAssignedToAgents": true,
"canBeAssignedToApiKeys": true,
"canBeAssignedToUsers": true,
"canDestroyAllObjectRecords": false,
"canReadAllObjectRecords": false,
"canSoftDeleteAllObjectRecords": false,
"canUpdateAllObjectRecords": false,
"canUpdateAllSettings": false,
"createdAt": Any<String>,
"description": "A test role",
"icon": null,
"isEditable": true,
"label": "Test Role",
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "role",
"type": "create",
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
exports[`syncApplication should delete old field and create equivalent one when field universalIdentifier changes 2`] = `
{
"syncApplication": {
"actions": [
{
"metadataName": "fieldMetadata",
"type": "delete",
"universalIdentifier": Any<String>,
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": "Ticket description",
"icon": "IconFileDescription",
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Description",
"morphId": null,
"name": "description",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
"metadataName": "fieldMetadata",
"type": "create",
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
exports[`syncApplication should return workspace migration actions on initial sync then on second sync with field rename and new role 1`] = `
{
"syncApplication": {
@@ -1,7 +1,9 @@
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 { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util';
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util';
import { findSkills } from 'test/integration/metadata/suites/skill/utils/find-skills.util';
@@ -10,8 +12,6 @@ import { type FieldManifest, type Manifest } from 'twenty-shared/application';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util';
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
@@ -177,6 +177,67 @@ describe('syncApplication', () => {
});
}, 60000);
it('should delete old field and create equivalent one when field universalIdentifier changes', async () => {
const originalFieldId = uuidv4();
const updatedFieldId = uuidv4();
const testObject = buildDefaultObjectManifest({
nameSingular: 'ticket',
namePlural: 'tickets',
labelSingular: 'Ticket',
labelPlural: 'Tickets',
description: 'A support ticket',
icon: 'IconTicket',
});
const baseField: FieldManifest = {
universalIdentifier: originalFieldId,
type: FieldMetadataType.TEXT,
name: 'description',
label: 'Description',
description: 'Ticket description',
icon: 'IconFileDescription',
objectUniversalIdentifier: testObject.universalIdentifier,
};
const { data: firstSyncData } = await syncApplication({
manifest: buildBaseManifest({
appId: TEST_APP_ID,
roleId: TEST_ROLE_ID,
overrides: {
objects: [testObject],
fields: [baseField],
},
}),
expectToFail: false,
});
expect(firstSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstSyncData),
);
const { data: secondSyncData } = await syncApplication({
manifest: buildBaseManifest({
appId: TEST_APP_ID,
roleId: TEST_ROLE_ID,
overrides: {
objects: [testObject],
fields: [
{
...baseField,
universalIdentifier: updatedFieldId,
},
],
},
}),
expectToFail: false,
});
expect(secondSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
);
}, 60000);
it('should create a TEXT field on the standard Company object', async () => {
const companyFieldId = uuidv4();