fix(server): repair missing keyValuePair.applicationId on 2.23 upgrades (#23272)
Fixes #23254 ## Problem Upgrading a self-hosted instance from `2.23.x` to `2.24.0` leaves `core.keyValuePair` without the `applicationId` column. Database-backed config loading then fails on startup and on every refresh (~every 15s) with: ``` column KeyValuePairEntity.applicationId does not exist ``` The frontend shows "Unable to reach the backend". ## Root cause `AddApplicationIdToKeyValuePairFastInstanceCommand` was added in #23089 (after `2.23.x` shipped) but registered under the already-released `2.23.0` segment: ```ts @RegisteredInstanceCommand('2.23.0', 1784659343818) ``` The upgrade cursor is **positional and forward-only**: - `resolveStartCursor` resumes at `lastAttemptedIndex + 1`. A fully-upgraded `2.23.x` instance has its cursor at the last `2.23` workspace command, which sits *after* this newly-inserted fast command in the sequence. So the runner steps right over it and the DDL never runs. - The upgrade-aware metadata layer decides "applied" the same way (`stepIndex < currentCursor` in `upgrade-aware-entity-metadata.adapter.ts`). Since the step index is below the cursor, the column is considered applied and is **not** hidden from TypeORM SELECTs, so every query references a column that was never created. Fresh `2.24.0` installs replay the whole sequence, so only `2.23.x -> 2.24.0` upgrades are affected. The instance log `1 fast instance ... for 2.24.0` confirms the command landed in the `2.23.0` bundle rather than `2.24.0`. ## Fix - Add `RepairKeyValuePairApplicationIdFastInstanceCommand` under the current version (`2.24.0`) with a fresh timestamp, so it sorts last in the sequence and runs for every existing instance regardless of cursor position. Its DDL mirrors the original command and is fully idempotent (`ADD COLUMN IF NOT EXISTS`, `DROP INDEX IF EXISTS` + recreate, `ADD VALUE IF NOT EXISTS`), so it is a no-op on healthy instances. `down()` is intentionally empty: the column lifecycle is owned by the `2.23.0` introduction command. - Repoint the entity's `@WasIntroducedInUpgrade` to the new command so the column stays hidden from queries until the repair has actually run, eliminating the error window during the migration itself. ## Notes - `2.24.0` (`TWENTY_CURRENT_VERSION`) is the correct target: the upgrade sequence only covers previous + current versions, so a command under `2.25.0` (a next version) would not run. If a version bump lands before this merges, the command should be moved to the new current version. - Follow-up worth considering: nothing currently prevents registering a command under a version in `TWENTY_PREVIOUS_VERSIONS`. A startup validation rejecting that would have caught this at PR time. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23272?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+57
@@ -0,0 +1,57 @@
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
// Repairs instances that upgraded from 2.23.x, where
|
||||
// AddApplicationIdToKeyValuePairFastInstanceCommand was registered under the
|
||||
// already-released 2.23.0 segment and got skipped by the forward-only upgrade
|
||||
// cursor, leaving core.keyValuePair without applicationId. The DDL mirrors that
|
||||
// command and is fully idempotent, so it is a no-op on healthy instances.
|
||||
@RegisteredInstanceCommand('2.24.0', 1784897347051)
|
||||
export class RepairKeyValuePairApplicationIdFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TYPE "core"."keyValuePair_type_enum" ADD VALUE IF NOT EXISTS 'APPLICATION_VARIABLE'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."keyValuePair" ADD COLUMN IF NOT EXISTS "applicationId" uuid',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."keyValuePair" DROP CONSTRAINT IF EXISTS "FK_e31d245e30cd82307e5416450fc"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."keyValuePair" ADD CONSTRAINT "FK_e31d245e30cd82307e5416450fc" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_KEY_VALUE_PAIR_APPLICATION_ID" ON "core"."keyValuePair" ("applicationId")',
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
'DROP INDEX IF EXISTS "core"."IDX_KEY_VALUE_PAIR_KEY_WORKSPACE_ID_NULL_USER_ID_UNIQUE"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'CREATE UNIQUE INDEX "IDX_KEY_VALUE_PAIR_KEY_WORKSPACE_ID_NULL_USER_ID_UNIQUE" ON "core"."keyValuePair" ("key", "workspaceId") WHERE "userId" IS NULL AND "applicationId" IS NULL',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'DROP INDEX IF EXISTS "core"."IDX_KEY_VALUE_PAIR_KEY_NULL_USER_ID_NULL_WORKSPACE_ID_UNIQUE"',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'CREATE UNIQUE INDEX "IDX_KEY_VALUE_PAIR_KEY_NULL_USER_ID_NULL_WORKSPACE_ID_UNIQUE" ON "core"."keyValuePair" ("key") WHERE "userId" IS NULL AND "workspaceId" IS NULL AND "applicationId" IS NULL',
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_KEY_VALUE_PAIR_KEY_APPLICATION_ID_WORKSPACE_UNIQUE" ON "core"."keyValuePair" ("key", "applicationId") WHERE "applicationId" IS NOT NULL AND "workspaceId" IS NOT NULL',
|
||||
);
|
||||
await queryRunner.query(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_KEY_VALUE_PAIR_KEY_APPLICATION_ID_GLOBAL_UNIQUE" ON "core"."keyValuePair" ("key", "applicationId") WHERE "applicationId" IS NOT NULL AND "workspaceId" IS NULL',
|
||||
);
|
||||
}
|
||||
|
||||
// No-op: the applicationId column lifecycle is owned by the 2.23.0
|
||||
// introduction command. This command only repairs instances that skipped it,
|
||||
// so rolling it back must not drop the column or its constraints.
|
||||
public async down(): Promise<void> {}
|
||||
}
|
||||
+2
@@ -123,6 +123,7 @@ import { UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand } from '.
|
||||
import { AddStatusesToBillingSubscriptionIndexSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784650048045-add-statuses-to-billing-subscription-index';
|
||||
import { AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand } from './2-24/2-24-instance-command-fast-1784712843602-add-on-connect-logic-function-to-connection-provider';
|
||||
import { AddAgentForeignKeyToRoleTargetFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-24/2-24-instance-command-fast-1784820332810-add-agent-foreign-key-to-role-target';
|
||||
import { RepairKeyValuePairApplicationIdFastInstanceCommand } from './2-24/2-24-instance-command-fast-1784897347051-repair-key-value-pair-application-id';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||
@@ -248,4 +249,5 @@ export const INSTANCE_COMMANDS = [
|
||||
AddStatusesToBillingSubscriptionIndexSlowInstanceCommand,
|
||||
AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand,
|
||||
AddAgentForeignKeyToRoleTargetFastInstanceCommand,
|
||||
RepairKeyValuePairApplicationIdFastInstanceCommand,
|
||||
];
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ export class KeyValuePairEntity {
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
@WasIntroducedInUpgrade({
|
||||
upgradeCommandName:
|
||||
'2.23.0_AddApplicationIdToKeyValuePairFastInstanceCommand_1784659343818',
|
||||
'2.24.0_RepairKeyValuePairApplicationIdFastInstanceCommand_1784897347051',
|
||||
})
|
||||
applicationId: string | null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user