Route pre-2.19 upgrade commands through a legacy validate-build path (#22884)

## Problem

Since the centralized metadata side-effect engine landed in v2.19,
`WorkspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromRecord`
runs `metadataSideEffectEngineService.expandWithSideEffects(...)` before
building. As a result every historical upgrade command
(`upgrade-version-command/1-21/*` … `2-18/*`), authored before the
engine existed, now flows through it. Their operation matrix is no
longer applied literally: the engine injects/cascades companions (system
fields, `searchVector` field + GIN index, `searchFieldMetadata` rows,
unique backing indexes) and can hard-fail on reserved-identifier
collisions (`RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER`).

Two hazards for already-shipped commands:

1. **Collision → hard failure**: a command declaring a companion the
engine now owns collides with the engine's deterministic
`universalIdentifier`.
2. **Silent drift**: on object/field create/delete the engine
adds/cascades companions the command author never intended, so
workspaces upgraded now differ structurally from those upgraded
incrementally before 2.19.

Suspected real-world impact: a self-hosted user upgrading v2.6.1 →
v2.21.0 hit `duplicate key value violates unique constraint
"IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE"` in
`upgrade:2-16:backfill-search-field-metadata`, because object-creating
commands now cascade and pre-create the deterministic
`searchFieldMetadata` rows the standalone backfill then re-inserts.

## Changes

- `workspace-migration-validate-build-and-run-service.ts`: extract the
shared compute-and-run tail into a private method, and add
`validateBuildAndRunLegacyWorkspaceMigration` (marked `@deprecated`)
that skips `expandWithSideEffects` and applies the matrix literally. The
existing side-effect entry points are unchanged (the live API and
application manifests depend on them).
- Repoint **all** pre-2.19 upgrade command call sites (1-21 … 2-18,
including `2-10 sync-call-recording-standard-objects`) to the legacy
method. Only the four `2-20/*` commands (target version ≥ 2.19) remain
on the side-effect path.
- `2-16 backfill-search-field-metadata`: recompute
`flatSearchFieldMetadataMaps` from the database before building the
existing-rows dedupe set. The migration runner only invalidates the
flat-maps keys a migration touched, so during a cross-version upgrade
earlier commands can leave this map stale; a stale map breaks the dedupe
and re-inserts rows, tripping
`IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE`. This is the direct fix
for the reported failure.
- Export `FlatEntityMapsBundle` so the shared tail can be typed.
- Document the side-effect vs legacy path and the selection rule in
`packages/twenty-server/docs/UPGRADE_COMMANDS.md`.

Selection rule: target version **< 2.19** → legacy path; **≥ 2.19** →
side-effect path (default). No exceptions.

## Known gap / merge ordering

The static twenty-standard definition declares all of `callRecording`'s
fields (including the `searchVector` system field) but **not** its
`searchVector` GIN index — every other searchable standard object
declares its GIN index statically. On the legacy path, workspaces
upgrading through `2-10 sync-call-recording-standard-objects` therefore
create the `searchVector` column unindexed (`searchFieldMetadata` rows
are created later in the same pipeline by the 2-16 backfill). The static
GIN index declaration plus a backfill for already-upgraded workspaces
land in a follow-up (twentyhq/core-team-issues#2672), which must ship in
the same release as this PR.

## Out of scope (separate follow-ups)

- `UpgradeMigrationService.getLastAttemptedInstanceCommand()` ordering.
- callRecording `searchVector` GIN index static declaration + backfill
(twentyhq/core-team-issues#2672, same-release dependency, see above).

## Test plan

- `nx typecheck twenty-server` passes.
- `nx lint:diff-with-main twenty-server` (oxlint + oxfmt) clean on
changed files.
- 2-20 command specs (which exercise the unchanged side-effect path)
pass.

---------

Co-authored-by: twenty <noreply@twenty.com>
This commit is contained in:
Paul Rastoin
2026-07-15 14:50:26 +02:00
committed by GitHub
parent 4e83a64f81
commit a28c3a905a
43 changed files with 197 additions and 71 deletions
@@ -106,6 +106,24 @@ export class BackfillStandardSkillsCommand
The base class `ActiveOrSuspendedWorkspaceCommandRunner` handles workspace iteration and provides `--dry-run`, `--verbose`, and workspace filter options automatically.
### Applying a migration matrix: side-effect vs legacy path
Commands that build a metadata migration go through `WorkspaceMigrationValidateBuildAndRunService`. Two entry points exist:
- `validateBuildAndRunWorkspaceMigration` (default): runs the operation matrix through the metadata side-effect engine (`expandWithSideEffects`) before building. The engine injects and cascades engine-owned companions (system fields and relations, the `searchVector` field and its GIN index, `searchFieldMetadata` rows, unique backing indexes). This is what the live API and application manifests rely on, so new commands should use it.
- `validateBuildAndRunLegacyWorkspaceMigration`: skips side-effect expansion and applies the matrix literally, exactly as it was authored.
The side-effect engine landed in v2.19. Commands authored before then declared their companions explicitly and were never designed to flow through the engine. Running them through it retroactively changes their behavior: it can hard-fail on reserved-identifier collisions (`RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER`) and silently create rows the command never intended (for example, the deterministic `searchFieldMetadata` rows that the standalone `upgrade:2-16:backfill-search-field-metadata` backfill then re-inserts, hitting `IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE`).
Rule of thumb:
- Target version **< 2.19** → use the **legacy** method.
- Target version **>= 2.19** → use the default side-effect method.
All pre-2.19 commands follow this rule, including `upgrade:2-10:sync-call-recording-standard-objects`: it builds its create-set from the static twenty-standard definition (which declares all of `callRecording`'s fields, including the `searchVector` system field) and runs it through the legacy path so nothing is injected on top. Its matrix contains no `searchFieldMetadata` operations; the deterministic rows are created later in the same upgrade pipeline by `upgrade:2-16:backfill-search-field-metadata`, which derives them from the standard definition.
Known gap: the static definition does not yet declare `callRecording`'s `searchVector` GIN index (every other searchable standard object declares its GIN index statically), so workspaces upgrading through 2-10 on the legacy path create the `searchVector` column unindexed. The static declaration plus a backfill for already-upgraded workspaces land in a follow-up (twentyhq/core-team-issues#2672), which must ship in the same release as this legacy path.
## Execution Order
Within a given version of Twenty, the upgrade pipeline runs commands in this order, sorted by timestamp within each group:
@@ -92,7 +92,7 @@ export class AddComposeEmailCommandMenuItemCommand extends ActiveOrSuspendedWork
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -112,7 +112,7 @@ export class BackfillMessageThreadSubjectCommand extends ActiveOrSuspendedWorksp
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
@@ -229,7 +229,7 @@ export class BackfillMessageThreadSubjectCommand extends ActiveOrSuspendedWorksp
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
@@ -118,7 +118,7 @@ export class DeduplicateEngineCommandsCommand extends ActiveOrSuspendedWorkspace
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -109,7 +109,7 @@ export class FixSelectAllCommandMenuItemsCommand extends ActiveOrSuspendedWorksp
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -129,7 +129,7 @@ export class MigrateAiAgentTextToJsonResponseFormatCommand extends ActiveOrSuspe
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
agent: {
@@ -290,7 +290,7 @@ export class RefactorNavigationCommandsCommand extends ActiveOrSuspendedWorkspac
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -147,7 +147,7 @@ export class FixMessageThreadViewAndLabelIdentifierCommand extends ActiveOrSuspe
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
objectMetadata: {
@@ -107,7 +107,7 @@ export class UpdateSearchCommandMenuItemLabelsCommand extends ActiveOrSuspendedW
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -103,7 +103,7 @@ export class AddSendEmailRecordSelectionCommandMenuItemsCommand extends ActiveOr
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -87,7 +87,7 @@ export class BackfillStandardSkillsCommand extends ActiveOrSuspendedWorkspaceCom
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
skill: {
@@ -107,7 +107,7 @@ export class FixMergeCommandSelectAllCommand extends ActiveOrSuspendedWorkspaceC
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -182,7 +182,7 @@ export class BackfillRecordPageLayoutsCommand extends ActiveOrSuspendedWorkspace
);
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
viewField: {
@@ -400,7 +400,7 @@ export class BackfillRecordPageLayoutsCommand extends ActiveOrSuspendedWorkspace
);
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
@@ -544,7 +544,7 @@ export class BackfillRecordPageLayoutsCommand extends ActiveOrSuspendedWorkspace
}
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
@@ -110,7 +110,7 @@ export class UpdateGlobalObjectContextCommandMenuItemsCommand extends ActiveOrSu
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -109,7 +109,7 @@ export class GateExportImportCommandMenuItemsByPermissionFlagCommand extends Act
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -148,7 +148,7 @@ export class AddLayoutCustomizationGuardToEditCommandsCommand extends ActiveOrSu
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -163,7 +163,7 @@ export class RenameConflictingCustomFieldsCommand extends ActiveOrSuspendedWorks
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
@@ -150,7 +150,7 @@ export class AddInactiveGenericStandardFieldsCommand extends ActiveOrSuspendedWo
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
@@ -488,7 +488,7 @@ export class SyncCallRecordingStandardObjectsCommand extends ActiveOrSuspendedWo
allFlatEntityOperationByMetadataName,
} of collisionRenameMigrations) {
const renameResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
applicationUniversalIdentifier,
@@ -509,7 +509,7 @@ export class SyncCallRecordingStandardObjectsCommand extends ActiveOrSuspendedWo
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
applicationUniversalIdentifier:
@@ -101,7 +101,7 @@ export class SyncCreateRecordCommandAvailabilityExpressionCommand extends Active
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -120,7 +120,7 @@ export class FixStandardRelationFieldLabelsIconsCommand extends ActiveOrSuspende
}
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
@@ -141,7 +141,7 @@ export class SyncCallRecordingRequestStatusCommand extends ActiveOrSuspendedWork
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
applicationUniversalIdentifier:
@@ -72,7 +72,7 @@ export class DropCalendarEventRecordingPreferenceCommand extends ActiveOrSuspend
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
allFlatEntityOperationByMetadataName: {
@@ -217,7 +217,7 @@ export class SyncCalendarEventRecordPageCommand extends ActiveOrSuspendedWorkspa
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
applicationUniversalIdentifier:
@@ -7,8 +7,8 @@ import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/w
import { buildSearchFieldMetadataBackfillOperations } from 'src/database/commands/upgrade-version-command/2-16/utils/build-search-field-metadata-backfill-operations.util';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@RegisteredWorkspaceCommand('2.16.0', 1799100000000)
@@ -33,6 +33,15 @@ export class BackfillSearchFieldMetadataCommand extends ActiveOrSuspendedWorkspa
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
// The migration runner only invalidates the flat-maps keys a migration touched,
// so during a cross-version upgrade earlier commands can leave this map stale.
// A stale map breaks the existing-rows dedupe below and re-inserts rows,
// tripping IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE. Recompute from the
// database before deriving the create-set.
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'flatSearchFieldMetadataMaps',
]);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
@@ -58,16 +67,15 @@ export class BackfillSearchFieldMetadataCommand extends ActiveOrSuspendedWorkspa
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const {
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
} = buildSearchFieldMetadataBackfillOperations({
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatSearchFieldMetadataMaps,
standardFlatSearchFieldMetadataMaps:
standardAllFlatEntityMaps.flatSearchFieldMetadataMaps,
customApplicationId: workspaceCustomFlatApplication.id,
});
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
buildSearchFieldMetadataBackfillOperations({
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatSearchFieldMetadataMaps,
standardFlatSearchFieldMetadataMaps:
standardAllFlatEntityMaps.flatSearchFieldMetadataMaps,
customApplicationId: workspaceCustomFlatApplication.id,
});
const applicationUniversalIdentifiers = Object.keys(
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
@@ -114,7 +122,7 @@ export class BackfillSearchFieldMetadataCommand extends ActiveOrSuspendedWorkspa
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
allFlatEntityOperationByMetadataName: {
@@ -121,7 +121,7 @@ export class SyncCallRecordingStatusCommand extends ActiveOrSuspendedWorkspaceCo
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
applicationUniversalIdentifier:
@@ -71,7 +71,7 @@ export class SyncCallRecordingNavigationCommandMenuItemAvailabilityExpressionCom
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
workspaceId,
@@ -64,7 +64,7 @@ export class AddReplyToMessageParticipantRoleOptionCommand extends ActiveOrSuspe
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
workspaceId,
@@ -116,7 +116,7 @@ export class AddWorkspaceMemberJobTitleFieldCommand extends ActiveOrSuspendedWor
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
@@ -107,7 +107,7 @@ export class AddMessageIsDraftFieldCommand extends ActiveOrSuspendedWorkspaceCom
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
allFlatEntityOperationByMetadataName: {
@@ -92,7 +92,7 @@ export class SetCalendarEventDescriptionDisplayedMaxRowsCommand extends ActiveOr
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
@@ -72,7 +72,7 @@ export class DropMessageDirectionFieldCommand extends ActiveOrSuspendedWorkspace
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
@@ -100,7 +100,7 @@ export class BackfillImageIdentifierFieldMetadataIdCommand extends ActiveOrSuspe
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
objectMetadata: {
@@ -82,7 +82,7 @@ export class DeleteGaugeWidgetsCommand extends ActiveOrSuspendedWorkspaceCommand
);
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
@@ -171,7 +171,7 @@ export class NormalizeCompositeFieldDefaultsCommand extends ActiveOrSuspendedWor
}));
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
@@ -101,7 +101,7 @@ export class SyncCommandMenuItemAvailabilityExpressionsCommand extends ActiveOrS
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -99,7 +99,7 @@ export class DropConnectedAccountStandardObjectCommand extends ActiveOrSuspended
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
allFlatEntityOperationByMetadataName: {
@@ -86,7 +86,7 @@ export class DropChannelStandardObjectsCommand extends ActiveOrSuspendedWorkspac
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
allFlatEntityOperationByMetadataName: {
@@ -194,7 +194,7 @@ export class BackfillRelationJoinColumnIndexesCommand extends ActiveOrSuspendedW
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
allFlatEntityOperationByMetadataName: {
@@ -127,7 +127,7 @@ export class GateDefaultCommandMenuItemsByPermissionFlagCommand extends ActiveOr
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
@@ -103,7 +103,7 @@ export class BackfillFieldsWidgetNewFieldDefaultVisibilityCommand extends Active
updatedWidgets,
] of widgetsToBackfillByApplicationUniversalIdentifier) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
@@ -28,7 +28,7 @@ import { InferDeletionFromMissingEntities } from 'src/engine/workspace-manager/w
export type WorkspaceMigrationRelatedFlatEntityMaps =
Partial<AllFlatEntityMaps> & WorkspaceMigrationBuilderAdditionalCacheDataMaps;
type FlatEntityMapsBundle = {
export type FlatEntityMapsBundle = {
flatApplicationMaps: FlatApplicationCacheMaps;
allRelatedFlatEntityMaps: WorkspaceMigrationRelatedFlatEntityMaps;
allMetadataNameCacheToCompute: AllMetadataName[];
@@ -19,7 +19,10 @@ import {
IdByUniversalIdentifierByMetadataName,
} from 'src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util';
import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service';
import { WorkspaceMigrationFlatEntityMapsService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-flat-entity-maps.service';
import {
FlatEntityMapsBundle,
WorkspaceMigrationFlatEntityMapsService,
} from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-flat-entity-maps.service';
import {
WorkspaceMigrationOrchestratorBuildArgs,
WorkspaceMigrationOrchestratorFailedResult,
@@ -43,6 +46,21 @@ type ValidateBuildAndRunWorkspaceMigrationFromRecordArgs = {
dryRun?: boolean;
};
type ValidateBuildAndRunWorkspaceMigrationFromRecordInternalArgs =
ValidateBuildAndRunWorkspaceMigrationFromRecordArgs & {
// Skips the metadata side-effect engine (expandWithSideEffects) and applies the
// matrix literally. Only the deprecated legacy path sets this to true.
skipSideEffectExpandEngine: boolean;
};
type ComputeAndRunWorkspaceMigrationFromResolvedOperationsArgs = {
workspaceId: string;
allFlatEntityOperationRecordByMetadataName: AllFlatEntityOperationRecordByMetadataName;
isSystemBuild: boolean;
applicationUniversalIdentifier: string;
dryRun?: boolean;
} & FlatEntityMapsBundle;
@Injectable()
export class WorkspaceMigrationValidateBuildAndRunService {
private readonly isDebugEnabled: boolean;
@@ -185,13 +203,60 @@ export class WorkspaceMigrationValidateBuildAndRunService {
});
}
public async validateBuildAndRunWorkspaceMigrationFromRecord({
public async validateBuildAndRunWorkspaceMigrationFromRecord(
args: ValidateBuildAndRunWorkspaceMigrationFromRecordArgs,
): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| (WorkspaceMigrationOrchestratorSuccessfulResult & {
hasSchemaMetadataChanged: boolean;
})
> {
return await this.validateBuildAndRunWorkspaceMigrationFromRecordInternal({
...args,
skipSideEffectExpandEngine: false,
});
}
/**
* @deprecated Legacy path for upgrade commands authored before the metadata
* side-effect engine landed in v2.19. These commands declare their operation
* matrix literally and must not flow through expandWithSideEffects, which
* would inject engine-owned companions and collide on reserved identifiers.
* See packages/twenty-server/docs/UPGRADE_COMMANDS.md.
*/
public async validateBuildAndRunLegacyWorkspaceMigration({
allFlatEntityOperationByMetadataName,
workspaceId,
isSystemBuild = false,
applicationUniversalIdentifier,
dryRun,
}: ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| (WorkspaceMigrationOrchestratorSuccessfulResult & {
hasSchemaMetadataChanged: boolean;
})
> {
return await this.validateBuildAndRunWorkspaceMigrationFromRecordInternal({
allFlatEntityOperationRecordByMetadataName:
transpileFlatEntityOperationArrayToRecord(
allFlatEntityOperationByMetadataName,
),
workspaceId,
isSystemBuild,
applicationUniversalIdentifier,
dryRun,
skipSideEffectExpandEngine: true,
});
}
private async validateBuildAndRunWorkspaceMigrationFromRecordInternal({
allFlatEntityOperationRecordByMetadataName,
workspaceId,
isSystemBuild = false,
applicationUniversalIdentifier,
dryRun,
}: ValidateBuildAndRunWorkspaceMigrationFromRecordArgs): Promise<
skipSideEffectExpandEngine,
}: ValidateBuildAndRunWorkspaceMigrationFromRecordInternalArgs): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| (WorkspaceMigrationOrchestratorSuccessfulResult & {
hasSchemaMetadataChanged: boolean;
@@ -213,19 +278,55 @@ export class WorkspaceMigrationValidateBuildAndRunService {
},
);
const sideEffectExpansionResult =
this.metadataSideEffectEngineService.expandWithSideEffects({
allFlatEntityOperationRecordByMetadataName,
sideEffectRelatedFlatEntityMaps: allRelatedFlatEntityMaps,
context: {
buildOptions: { isSystemBuild, applicationUniversalIdentifier },
},
});
let resolvedFlatEntityOperationRecordByMetadataName =
allFlatEntityOperationRecordByMetadataName;
if (sideEffectExpansionResult.status === 'fail') {
return sideEffectExpansionResult;
if (!skipSideEffectExpandEngine) {
const sideEffectExpansionResult =
this.metadataSideEffectEngineService.expandWithSideEffects({
allFlatEntityOperationRecordByMetadataName,
sideEffectRelatedFlatEntityMaps: allRelatedFlatEntityMaps,
context: {
buildOptions: { isSystemBuild, applicationUniversalIdentifier },
},
});
if (sideEffectExpansionResult.status === 'fail') {
return sideEffectExpansionResult;
}
resolvedFlatEntityOperationRecordByMetadataName =
sideEffectExpansionResult.allFlatEntityOperationRecordByMetadataName;
}
return await this.computeAndRunWorkspaceMigrationFromResolvedOperations({
allFlatEntityOperationRecordByMetadataName:
resolvedFlatEntityOperationRecordByMetadataName,
workspaceId,
isSystemBuild,
applicationUniversalIdentifier,
dryRun,
flatApplicationMaps,
allRelatedFlatEntityMaps,
allMetadataNameCacheToCompute,
});
}
private async computeAndRunWorkspaceMigrationFromResolvedOperations({
allFlatEntityOperationRecordByMetadataName,
workspaceId,
isSystemBuild,
applicationUniversalIdentifier,
dryRun,
flatApplicationMaps,
allRelatedFlatEntityMaps,
allMetadataNameCacheToCompute,
}: ComputeAndRunWorkspaceMigrationFromResolvedOperationsArgs): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| (WorkspaceMigrationOrchestratorSuccessfulResult & {
hasSchemaMetadataChanged: boolean;
})
> {
const {
fromToAllFlatEntityMaps,
inferDeletionFromMissingEntities,
@@ -235,8 +336,7 @@ export class WorkspaceMigrationValidateBuildAndRunService {
} =
this.workspaceMigrationFlatEntityMapsService.computeFromToAllFlatEntityMapsAndBuildOptions(
{
allFlatEntityOperationRecordByMetadataName:
sideEffectExpansionResult.allFlatEntityOperationRecordByMetadataName,
allFlatEntityOperationRecordByMetadataName,
applicationUniversalIdentifier,
flatApplicationMaps,
allRelatedFlatEntityMaps,