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:
@@ -108,16 +108,10 @@ permissionAdminRouter.get('/permissions', ownerGate, async (ctx) => {
|
||||
// Roles a grant may name. Super Admin is excluded: the owner bypasses this table entirely, and the
|
||||
// database refuses a row for that role.
|
||||
roles: USER_ROLES.filter((r) => r !== 'Super Admin'),
|
||||
// Mapped rather than passed through: `capability` is the DATABASE's column name, and the wire should
|
||||
// not leak it. Doing it here means renaming the column changes nothing any client can see — and the
|
||||
// round trip below already expects `permission`, so passing the row straight out left the screen
|
||||
// reading `grant.permission` on an object that only had `grant.capability`. Every role rendered as
|
||||
// holding nothing, with no error anywhere.
|
||||
grants: (await getAllRoleGrants()).map(({ role, capability, level }) => ({
|
||||
role,
|
||||
permission: capability,
|
||||
level,
|
||||
})),
|
||||
// The column is `permission` now too, so this is a straight pass-through again. It was briefly a
|
||||
// mapping — the wire said `permission` while the column still said `capability` — which is what let
|
||||
// the table be renamed without any client noticing.
|
||||
grants: await getAllRoleGrants(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,8 +124,7 @@ permissionAdminRouter.put('/permissions/:role', ownerGate, async (ctx) => {
|
||||
const raw = body?.grants;
|
||||
if (!Array.isArray(raw)) throw errors.BAD_REQUEST('Expected { grants: [{ permission, level }] }');
|
||||
|
||||
// `capability` is the DATABASE's column name; it becomes `permission` when the table is renamed.
|
||||
const grants: { capability: string; level: 'read' | 'write' }[] = [];
|
||||
const grants: { permission: string; level: 'read' | 'write' }[] = [];
|
||||
for (const entry of raw) {
|
||||
const { permission, level } = (entry ?? {}) as { permission?: unknown; level?: unknown };
|
||||
if (typeof permission !== 'string') throw errors.BAD_REQUEST('Each grant needs a permission key');
|
||||
@@ -149,8 +142,7 @@ permissionAdminRouter.put('/permissions/:role', ownerGate, async (ctx) => {
|
||||
: `${known.label} is not grantable`,
|
||||
);
|
||||
}
|
||||
// `capability` is the DATABASE's column name until the table rename.
|
||||
grants.push({ capability: permission, level });
|
||||
grants.push({ permission, level });
|
||||
}
|
||||
|
||||
await replaceRoleGrants(role, grants);
|
||||
|
||||
Reference in New Issue
Block a user