Drop legacy rolePermissionFlag.flag column + fallback logic (#20730)

## Summary
- **New fast migration**
`2-7-instance-command-fast-1779600000000-finalize-role-permission-flag-cutover.ts`:
  - `DROP CONSTRAINT IDX_ROLE_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE`
  - `ALTER COLUMN permissionFlagId SET NOT NULL`
  - `DROP COLUMN flag`
- `down()` repopulates `flag` from the catalog via `permissionFlagId`
and restores the old unique.
- **Entity**: `RolePermissionFlagEntity` hides the `flag` column by
using the new decorator + drops old `@Unique` decorator;
`permissionFlagId` and the `permissionFlag` relation become
non-nullable.
- **Deletes** the synthesizer
`synthesize-flat-permission-flag-from-flag.util.ts` and every fallback
branch that used it

(`from-role-permission-flag-entity-to-flat-role-permission-flag.util.ts`,
`from-flat-role-permission-flag-to-role-permission-flag-dto.util.ts`,
`permissions.service.ts`,
`workspace-roles-permissions-cache.service.ts`,
`fromRoleEntityToRoleDto.util.ts`,
`flat-role-permission-flag-validator.service.ts`,
`role-permission-flag.service.ts:getEffectiveUniversalIdentifier`).
- **Write path**: ~~drops `flag` from `CreateRolePermissionFlagInput`,
the create util, and the application-manifest converter.~~
- **Metadata configs**: ~~removes `flag` from
`all-entity-properties-configuration-by-metadata-name.constant.ts`
(rolePermissionFlag block)~~ and flips `permissionFlag.isNullable` to
`false` in `all-many-to-one-metadata-relations.constant.ts`.

### Why the `flag` field stays declared in the entity

The decorator (`@WasRemovedInUpgrade`) is the right tool for the
lifecycle marker, but it's a **reflect-metadata** runtime decorator —
TypeScript can't see it at compile time. So while
the adapter
([`UpgradeAwareEntityMetadataAdapter`](packages/twenty-server/src/engine/twenty-orm/upgrade-aware/upgrade-aware-entity-metadata.adapter.ts))
now correctly flips
`isSelect`/`isInsert`/`isUpdate` to `false` once the drop migration's
cursor is crossed, the *static* TypeScript types derived from
`RolePermissionFlagEntity`
(`UniversalFlatRolePermissionFlag`, `FlatRolePermissionFlag`,
`MetadataEntityPropertyConfiguration<'rolePermissionFlag'>`, etc.) still
see `flag` as a required scalar property — because the entity declares
it.

That means every producer of one of those derived types must include
`flag`:
-
`from-create-role-permission-flag-input-to-flat-role-permission-flag-to-create.util.ts`
plumbs it through.
- `from-permission-flag-to-universal-flat-role-permission-flag.util.ts`
(the application-manifest converter) sets `flag: permissionFlag.flag`.
- `all-entity-properties-configuration-by-metadata-name.constant.ts` has
a `flag` entry under `rolePermissionFlag`.
- `CreateRolePermissionFlagInput` keeps the `flag` field.
- `RolePermissionFlagService.upsertPermissionFlags` passes `flag:
permissionFlag.key as PermissionFlagType` to the create util.

Explored phantom-brand approach (`RemovedInUpgrade<T>` wrapper on the
field type, key-filter mapped type applied inside `ScalarFlatEntity` /
`UniversalFlatEntityFrom`) but previous commands could have `flag ===
undefined` (downcast from the brand since we can't compare with
UpgradeMigrationName like we do with a decorator). That's a
**silent-read** failure mode: compiles fine, comparisons against `flag`
silently always-false, no error surfaces. Probably worth too much risk
for what's a small amount of plumbing?

The eventual full deletion of `flag` (entity field included) is a future
cleanup once we drop cross-upgrade support for versions ≤ 2.6

Note: Not sure if this PR (and even the decorator) is really needed in
the end, seems we need to keep a lot of code in place to handle legacy.
Maybe a simple noop [At]Deprecated is enough @charlesBochet (and a
migration to set the column nullable if that was not the case before +
remove associated constraints)
This commit is contained in:
Weiko
2026-05-21 13:56:06 +02:00
committed by GitHub
parent d192d2d492
commit d13cc7c349
15 changed files with 139 additions and 131 deletions
@@ -151,5 +151,7 @@ describe('UpgradeAwareEntityMetadataAdapter', () => {
expect(visibleColumn.isSelect).toBe(true);
expect(visibleColumn.isInsert).toBe(true);
expect(visibleColumn.isUpdate).toBe(true);
expect(metadata.columns).toEqual([visibleColumn]);
});
});
@@ -23,6 +23,7 @@ type EntityMetadataSnapshot = {
tableName: string;
tablePath: string;
givenTableName: string | undefined;
canonicalColumns: ReadonlyArray<ColumnMetadata>;
columnDatabaseNamesByPropertyName: ReadonlyMap<string, string>;
columnSelectByPropertyName: ReadonlyMap<string, boolean>;
columnInsertByPropertyName: ReadonlyMap<string, boolean>;
@@ -135,6 +136,7 @@ export class UpgradeAwareEntityMetadataAdapter implements OnModuleInit {
tableName: metadata.tableName,
tablePath: metadata.tablePath,
givenTableName: metadata.givenTableName,
canonicalColumns: [...metadata.columns],
columnDatabaseNamesByPropertyName,
columnSelectByPropertyName,
columnInsertByPropertyName,
@@ -277,9 +279,15 @@ export class UpgradeAwareEntityMetadataAdapter implements OnModuleInit {
metadata.givenTableName = resolved.effectiveTableName;
}
metadata.columns = [...snapshot.canonicalColumns];
for (const column of metadata.columns) {
this.applyColumnShape({ column, snapshot, resolved });
}
metadata.columns = metadata.columns.filter(
(column) => !resolved.hiddenPropertyNames.has(column.propertyName),
);
}
private applyColumnShape({