step 2/4: role_capabilities becomes role_permissions, without losing a grant

The dangerous one. `drizzle-kit push` does not understand renames — it sees a
table gone and a table added, and with --force it resolves that by dropping and
creating. The 9 live grants would have vanished silently and every member would
have lost terminal, chat and files until someone noticed and re-granted by hand.

So the database moved FIRST, by explicit ALTER, and the code followed:

  ALTER TABLE role_capabilities RENAME TO role_permissions
  ALTER TABLE role_permissions RENAME COLUMN capability TO permission
  ALTER INDEX uq_role_capabilities_role_capability RENAME TO uq_role_permissions_role_permission
  ALTER TABLE ... RENAME CONSTRAINT ck_role_capabilities_{role,level,not_owner} TO ck_role_permissions_*
  ALTER INDEX role_capabilities_pkey RENAME TO role_permissions_pkey

Seven objects, not one, and all in a single transaction: a partial rename would
leave drizzle-kit seeing a table it half-recognised, which is the same drop.
The names were read out of pg_indexes and pg_constraint rather than assumed.

`bun db:push` then reported **No changes detected** — which is the whole proof.
It means the ALTERs matched the schema code exactly, so there was nothing for
push to reconcile and nothing for --force to destroy.

Grants verified against a JSON backup taken before the first ALTER: 9 rows,
byte-identical, `role_capabilities` gone from the database.

No client noticed. Step 1 had already moved the wire to `permission` by mapping
in the route while the column still said `capability`, so this step deleted the
mapping rather than changing any response. The two call sites that carried a
`capability` key with a comment explaining why now say `permission` and the
comments are gone.

Also renamed officer_db/src/capabilities/ → permissions/, roleCapabilities →
rolePermissions, CapabilityLevelValue → PermissionLevelValue.

Verified live after restart: self 200, admin list 200, gated route 200, no
token 401, grants read back as {role, permission, level}, and a PUT round trip
left 9 grants intact.

tsgo clean. 797 tests, 787 pass, same 7.
This commit is contained in:
2026-08-15 16:08:05 +00:00
parent 2e8ec845c8
commit 5ec354cfcb
8 changed files with 52 additions and 61 deletions
+1 -2
View File
@@ -55,8 +55,7 @@ export const bootstrapHandler: Handler = async function (ctx) {
for (const role of USER_ROLES.filter((r) => r !== 'Super Admin')) {
await replaceRoleGrants(
role,
// `capability` is the DATABASE's column name, renamed in the step that renames the table.
DEFAULT_ROLE_PERMISSIONS.map((permission) => ({ capability: permission, level: 'write' as const })),
DEFAULT_ROLE_PERMISSIONS.map((permission) => ({ permission, level: 'write' as const })),
);
}
} catch (ex) {