From c05f01f2a39e0f88a81f2c7281b0a571a414a9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Sun, 14 Jun 2026 05:51:23 +0200 Subject: [PATCH] =?UTF-8?q?fix(server):=20repair=202.13=20isUIReadOnly?= =?UTF-8?q?=E2=86=92isUIEditable=20rename=20fallout=20(#21504)=20(#21537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Follow-up to #21504 ("Rename isUIReadOnly to isUIEditable, add isUICreatable…"), which surfaced two issues: 1. **`column FieldMetadataEntity.isUIReadOnly does not exist`** on twenty-main.com. 2. **`cross-version-upgrade` CI failure** — `SyncStandardUiCapabilityFlags` aborts the v1.22 → 2.13 upgrade. ## Fix 1 — don't drop `isUIReadOnly` in the 2.13 rename command The 2.13 fast instance command physically dropped `isUIReadOnly` from `core."fieldMetadata"` and `core."objectMetadata"`. But migrations run in an ArgoCD **PreSync** hook **before** the new pods roll out (`charts/prod-eu/apps/twenty-server` migration Job is `hook: PreSync`, sync-wave `2`; the api/worker Deployments are sync-wave `10`). So the **previous** release's pods keep serving and still `SELECT isUIReadOnly`, throwing `column ... does not exist` from the moment the column is dropped until the rollout finishes. This keeps the column (already hidden from the app via `@WasRemovedInUpgrade` on both entities) and **defers the physical drop** to a later release. Since 2.13 hasn't shipped to self-hosters yet, the committed command is amended in place. Both tables handled; `isUICreatable` (new, additive column) is unaffected. The eventual physical drop + GraphQL-compat removals are tracked in twentyhq/core-team-issues#2542. ## Fix 2 — allow `isUIEditable` updates on relation field metadata `SyncStandardUiCapabilityFlags` re-syncs `isUIEditable` on standard fields, including morph/relation fields (the activityTargets `target*` relations). The flat-field-metadata validator only permits a fixed property allow-list on relation fields, which omitted `isUIEditable`, so the command failed with `FIELD_MUTATION_NOT_ALLOWED` and aborted the upgrade (leaving workspaces in a FAILED state). `isUIEditable` is a per-field UI-affordance flag that applies to relation fields too, so it's added to the relation-field updatable properties (a constant used **only** by that validator — no diff-engine side effects). ## Coherence notes - Object-level is covered: the drop is deferred on **both** tables, and `ObjectMetadataEntity` has the identical decorators. - `isUICreatable` needs no change: object-only and additive (no drop → no rolling-deploy hazard), and never reaches the field relation allow-list. - The object-metadata validator has no relation allow-list, so there's no object-level analog to change. ## Verification - `nx typecheck twenty-server` ✅ (the `satisfies` guard holds — `isUIEditable` is a `toCompare` property of `fieldMetadata`) - `oxlint --type-aware` + `oxfmt --check` ✅ on changed files - Fix 2's path is exercised end-to-end by the `cross-version-upgrade` CI that originally caught it. --------- Co-authored-by: Claude --- ...ename-is-ui-read-only-to-is-ui-editable.ts | 33 +++++++------------ .../field-metadata/field-metadata.entity.ts | 8 ++--- ...relation-properties-to-compare.constant.ts | 1 + .../object-metadata/object-metadata.entity.ts | 8 ++--- 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-13/2-13-instance-command-fast-1781277453604-rename-is-ui-read-only-to-is-ui-editable.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-13/2-13-instance-command-fast-1781277453604-rename-is-ui-read-only-to-is-ui-editable.ts index d9dcf4a89f..98c99f563c 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-13/2-13-instance-command-fast-1781277453604-rename-is-ui-read-only-to-is-ui-editable.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-13/2-13-instance-command-fast-1781277453604-rename-is-ui-read-only-to-is-ui-editable.ts @@ -3,9 +3,19 @@ import { QueryRunner } from 'typeorm'; import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface'; -// Replaces the negative isUIReadOnly flag with the positive isUIEditable flag -// (inverted polarity) on both objectMetadata and fieldMetadata, and adds the +// Adds the positive isUIEditable flag (inverted polarity of the legacy +// isUIReadOnly flag) on both objectMetadata and fieldMetadata, and adds the // object-level isUICreatable flag. +// +// The legacy isUIReadOnly column is intentionally NOT dropped here. The +// migration runs in an ArgoCD PreSync hook before the new pods roll out, so the +// previous release's pods keep serving while the schema changes — and they +// still SELECT isUIReadOnly on every fieldMetadata read. Dropping it in the +// same release that stops using it makes those pods throw "column isUIReadOnly +// does not exist" until the rollout completes. isUIReadOnly stays a plain +// column on both entities (no longer @WasRemovedInUpgrade, so it isn't +// dropped); its removal — decorator + physical drop — is deferred to a later +// release once no running code references it (core-team-issues#2542). @RegisteredInstanceCommand('2.13.0', 1781277453604) export class RenameIsUiReadOnlyToIsUiEditableFastInstanceCommand implements FastInstanceCommand @@ -17,9 +27,6 @@ export class RenameIsUiReadOnlyToIsUiEditableFastInstanceCommand await queryRunner.query( `UPDATE "core"."objectMetadata" SET "isUIEditable" = false WHERE "isUIReadOnly" = true`, ); - await queryRunner.query( - 'ALTER TABLE "core"."objectMetadata" DROP COLUMN IF EXISTS "isUIReadOnly"', - ); await queryRunner.query( 'ALTER TABLE "core"."objectMetadata" ADD COLUMN IF NOT EXISTS "isUICreatable" boolean NOT NULL DEFAULT true', @@ -31,18 +38,9 @@ export class RenameIsUiReadOnlyToIsUiEditableFastInstanceCommand await queryRunner.query( `UPDATE "core"."fieldMetadata" SET "isUIEditable" = false WHERE "isUIReadOnly" = true`, ); - await queryRunner.query( - 'ALTER TABLE "core"."fieldMetadata" DROP COLUMN IF EXISTS "isUIReadOnly"', - ); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - 'ALTER TABLE "core"."fieldMetadata" ADD COLUMN IF NOT EXISTS "isUIReadOnly" boolean NOT NULL DEFAULT false', - ); - await queryRunner.query( - `UPDATE "core"."fieldMetadata" SET "isUIReadOnly" = true WHERE "isUIEditable" = false`, - ); await queryRunner.query( 'ALTER TABLE "core"."fieldMetadata" DROP COLUMN IF EXISTS "isUIEditable"', ); @@ -50,13 +48,6 @@ export class RenameIsUiReadOnlyToIsUiEditableFastInstanceCommand await queryRunner.query( 'ALTER TABLE "core"."objectMetadata" DROP COLUMN IF EXISTS "isUICreatable"', ); - - await queryRunner.query( - 'ALTER TABLE "core"."objectMetadata" ADD COLUMN IF NOT EXISTS "isUIReadOnly" boolean NOT NULL DEFAULT false', - ); - await queryRunner.query( - `UPDATE "core"."objectMetadata" SET "isUIReadOnly" = true WHERE "isUIEditable" = false`, - ); await queryRunner.query( 'ALTER TABLE "core"."objectMetadata" DROP COLUMN IF EXISTS "isUIEditable"', ); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index 969862ae7e..00b59f461f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -129,10 +129,10 @@ export class FieldMetadataEntity< @Column({ default: true }) isUIEditable: boolean; - @WasRemovedInUpgrade({ - upgradeCommandName: - RENAME_IS_UI_READ_ONLY_TO_IS_UI_EDITABLE_UPGRADE_COMMAND_NAME, - }) + // Superseded by isUIEditable. Intentionally NOT @WasRemovedInUpgrade: dropping + // it in 2.13 would break the previous release's pods mid rolling-deploy, since + // they still SELECT it. The WasRemovedInUpgrade type is kept so callers may + // omit it; the decorator + physical drop are deferred (core-team-issues#2542). @Column({ type: 'boolean', default: false }) isUIReadOnly: WasRemovedInUpgrade; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant.ts index e68ca9d714..153c1541e7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant.ts @@ -8,4 +8,5 @@ export const FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE = [ 'icon', 'name', 'universalSettings', + 'isUIEditable', ] as const satisfies (typeof ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY.fieldMetadata.propertiesToCompare)[number][]; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts index 077471d39d..9721c55a4e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts @@ -95,10 +95,10 @@ export class ObjectMetadataEntity @Column({ default: true }) isUIEditable: boolean; - @WasRemovedInUpgrade({ - upgradeCommandName: - RENAME_IS_UI_READ_ONLY_TO_IS_UI_EDITABLE_UPGRADE_COMMAND_NAME, - }) + // Superseded by isUIEditable. Intentionally NOT @WasRemovedInUpgrade: dropping + // it in 2.13 would break the previous release's pods mid rolling-deploy, since + // they still SELECT it. The WasRemovedInUpgrade type is kept so callers may + // omit it; the decorator + physical drop are deferred (core-team-issues#2542). @Column({ type: 'boolean', default: false }) isUIReadOnly: WasRemovedInUpgrade;