fix: make 1.19 upgrade commands resilient to pre-existing data (#18716)
## Summary - **BackfillMissingStandardViewsCommand**: When view validation fails (e.g. a viewField references a field metadata that doesn't exist in the workspace), log a warning and skip instead of throwing — so the workspace upgrade continues with the remaining commands. - **AddMissingSystemFieldsToStandardObjectsCommand**: Wrap both the non-tsVector batch migration and each individual tsVector migration in try-catch. If a field already exists (e.g. duplicate key on `name + objectMetadataId + workspaceId`), the error is logged as a warning and the command moves on to the next field. These errors were observed during the 1.18 → 1.19 production upgrade for workspaces with non-standard state (missing "owner" field metadata on Opportunity, or searchVector fields already present with a different universalIdentifier). ## Test plan - [ ] Re-run upgrade on the affected production workspaces - [ ] Verify upgrade completes successfully with warnings instead of failures - [ ] Confirm that workspaces which were already upgrading cleanly are unaffected Made with [Cursor](https://cursor.com)
This commit is contained in:
+99
-23
@@ -8,6 +8,7 @@ import { Repository } from 'typeorm';
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ADD_MISSING_SYSTEM_FIELDS_TO_STANDARD_OBJECTS_1771420702241 } from 'src/database/commands/upgrade-version-command/workspace-migrations/1771420702241-add-missing-system-fields-to-standard-objects';
|
||||
import { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
@@ -37,9 +38,44 @@ const TS_VECTOR_INDIVIDUAL_MIGRATIONS = allActions
|
||||
} satisfies WorkspaceMigration,
|
||||
}));
|
||||
|
||||
const NON_TS_VECTOR_INDIVIDUAL_MIGRATIONS = allActions
|
||||
.filter((action) => action.flatEntity.type !== FieldMetadataType.TS_VECTOR)
|
||||
.map((action) => ({
|
||||
universalIdentifier: action.flatEntity.universalIdentifier,
|
||||
fieldName: action.flatEntity.name,
|
||||
objectIdentifier: action.flatEntity.objectMetadataUniversalIdentifier,
|
||||
migration: {
|
||||
applicationUniversalIdentifier,
|
||||
actions: [action],
|
||||
} satisfies WorkspaceMigration,
|
||||
}));
|
||||
|
||||
const FIRST_NON_TS_VECTOR_UNIVERSAL_IDENTIFIER =
|
||||
allActions[0].flatEntity.universalIdentifier;
|
||||
|
||||
const DUPLICATE_KEY_MESSAGE = 'duplicate key value violates unique constraint';
|
||||
|
||||
const isUniqueViolationError = (error: Error): boolean => {
|
||||
if (error.message.includes(DUPLICATE_KEY_MESSAGE)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error instanceof WorkspaceMigrationRunnerException) {
|
||||
const nestedErrors = [
|
||||
error.errors?.metadata,
|
||||
error.errors?.workspaceSchema,
|
||||
error.errors?.actionTranspilation,
|
||||
];
|
||||
|
||||
return nestedErrors.some(
|
||||
(nestedError) =>
|
||||
nestedError?.message?.includes(DUPLICATE_KEY_MESSAGE) === true,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-19:add-missing-system-fields-to-standard-objects',
|
||||
description:
|
||||
@@ -57,6 +93,46 @@ export class AddMissingSystemFieldsToStandardObjectsCommand extends ActiveOrSusp
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
private async runIndividualMigrations(
|
||||
workspaceId: string,
|
||||
migrations: {
|
||||
universalIdentifier: string;
|
||||
fieldName: string;
|
||||
objectIdentifier: string;
|
||||
migration: WorkspaceMigration;
|
||||
}[],
|
||||
): Promise<void> {
|
||||
for (const entry of migrations) {
|
||||
const alreadyCreated = await this.hasFieldBeenCreated(
|
||||
workspaceId,
|
||||
entry.universalIdentifier,
|
||||
);
|
||||
|
||||
if (alreadyCreated) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Adding field ${entry.fieldName} on object ${entry.objectIdentifier} in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.workspaceMigrationRunnerService.run({
|
||||
workspaceId,
|
||||
workspaceMigration: entry.migration,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isUniqueViolationError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
`Field ${entry.fieldName} on object ${entry.objectIdentifier} already exists in workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async hasFieldBeenCreated(
|
||||
workspaceId: string,
|
||||
universalIdentifier: string,
|
||||
@@ -99,35 +175,35 @@ export class AddMissingSystemFieldsToStandardObjectsCommand extends ActiveOrSusp
|
||||
`Adding ${NON_TS_VECTOR_MIGRATION.actions.length} non-tsVector fields (position, createdBy, updatedBy) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.workspaceMigrationRunnerService.run({
|
||||
workspaceId,
|
||||
workspaceMigration: NON_TS_VECTOR_MIGRATION,
|
||||
});
|
||||
try {
|
||||
await this.workspaceMigrationRunnerService.run({
|
||||
workspaceId,
|
||||
workspaceMigration: NON_TS_VECTOR_MIGRATION,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isUniqueViolationError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
`Batch non-tsVector migration hit a duplicate field in workspace ${workspaceId}, falling back to individual field migrations`,
|
||||
);
|
||||
|
||||
await this.runIndividualMigrations(
|
||||
workspaceId,
|
||||
NON_TS_VECTOR_INDIVIDUAL_MIGRATIONS,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Non-tsVector fields already exist in workspace ${workspaceId}, skipping.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const tsVectorEntry of TS_VECTOR_INDIVIDUAL_MIGRATIONS) {
|
||||
const alreadyCreated = await this.hasFieldBeenCreated(
|
||||
workspaceId,
|
||||
tsVectorEntry.universalIdentifier,
|
||||
);
|
||||
|
||||
if (alreadyCreated) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Adding tsVector field ${tsVectorEntry.fieldName} on object ${tsVectorEntry.objectIdentifier} in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.workspaceMigrationRunnerService.run({
|
||||
workspaceId,
|
||||
workspaceMigration: tsVectorEntry.migration,
|
||||
});
|
||||
}
|
||||
await this.runIndividualMigrations(
|
||||
workspaceId,
|
||||
TS_VECTOR_INDIVIDUAL_MIGRATIONS,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully added missing system fields to standard objects in workspace ${workspaceId}`,
|
||||
|
||||
+4
-5
@@ -171,12 +171,11 @@ export class BackfillMissingStandardViewsCommand extends ActiveOrSuspendedWorksp
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to backfill missing standard views:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to backfill missing standard views for workspace ${workspaceId}`,
|
||||
this.logger.warn(
|
||||
`Failed to backfill missing standard views for workspace ${workspaceId}, skipping:\n${JSON.stringify(validateAndBuildResult)}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
|
||||
Reference in New Issue
Block a user