a tested migration script for the capabilities→permissions rename

Other machines have to make the same database change, and pasted SQL is the
wrong way to ship it. `scripts/rename-capabilities-to-permissions.ts` renames
the table, its column, both indexes and its three CHECK constraints in one
transaction, and refuses to guess:

  neither table        nothing to do; db:push will create it
  already renamed      no-op, prints the grant count
  BOTH tables present  stops and says a human must decide
  old table only       migrates, counts before and after, prints every grant

Testing it found a bug that reading it had not. The existence checks were
inside the transaction but ran on the pool rather than on `tx`, so they could
not see the uncommitted rename: `columnExists('role_permissions','capability')`
answered false because that table did not exist yet on that connection, and the
COLUMN rename was silently skipped. The result was a `role_permissions` table
with a `capability` column — half migrated, and only failing on the next query.

That was found by building a scratch database in edge-pertento's exact shape
and running the script against it, rather than by review. Every check now
happens before the transaction, against the old names.

Verified end to end on that scratch: 6 grants migrated intact, a second run
correctly no-ops, and `db:push` afterwards leaves role_permissions and its rows
alone while creating the rest of the schema. Indexes come out as
role_permissions_pkey and uq_role_permissions_role_permission.

Also swept the last all-caps survivors the case-sensitive passes missed:
CORE_CAPABILITIES → CORE_PERMISSIONS, and a react-query key still spelling
['ROLE_CAPABILITIES']. The only CAPABILITIES left in src/ is the wallet's, which
is Lightning and stays.
This commit is contained in:
2026-08-15 16:40:18 +00:00
parent 027b10bd6e
commit 9c2d6a97f7
4 changed files with 128 additions and 4 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ import { getUserById, getRoleGrants } from 'officerdb';
import type { UserRole } from 'officerdb';
import {
PERMISSION_BY_KEY,
CORE_CAPABILITIES,
CORE_PERMISSIONS,
permissionForApiPath,
permissionForWsProvider,
isRequestAllowedAtLevel,
@@ -74,7 +74,7 @@ export async function getEffectivePermissions(userId: number | undefined): Promi
if (user.role === 'Super Admin') return { isOwner: true, grants: new Map() };
const grants = new Map<string, PermissionLevel>();
for (const permission of CORE_CAPABILITIES) grants.set(permission.key, 'write');
for (const permission of CORE_PERMISSIONS) grants.set(permission.key, 'write');
// Whether the kernel can enforce a boundary for this account. `confined` permissions are dropped
// without it — see below.