/** * Seed the `roles` table with the roles the platform's own code names. * * Runs from `postdb:push`, so every machine gets the rows on its next schema push rather than needing a * per-server step in a deploy plan. Bun fires `pre`/`post` hooks for colon-named scripts — the same * mechanism `backfill-session-models.ts` relies on. * * ── Why these four are `isSystem` ── * * Each is named by name somewhere in the code: this file matches `'Developer'`, `'Admin'` and `'Member'` * by name on every run, and `bootstrap.ts` writes `'Super Admin'`. Renaming or deleting one of those is not * a data change, it is a code change that has not happened — so the flag exists for the delete and rename * paths to refuse on, rather than for the failure to turn up later as a missing database role or an * account that cannot log in. * * ── `description` versus `grantsPostgres`/`grantsDocker` ── * * `seedRole` treats these two kinds of column differently, and it matters here specifically because this * is the one caller that seeds `isSystem: true` rows. `description` is written once, on insert, and never * touched again — an edit made on one server must not be reverted by the next deploy. `grantsPostgres` and * `grantsDocker` ARE re-asserted every run for a system role, because they are not the operator's data, * they are what "Developer" MEANS — the same standing as the name itself. `rolePermitsDevTools()` used to * be that declaration, hardcoded as `role === 'Developer'`; this table is where it lives now, and this * SEED array is the only place either flag is set for these four roles. See `seedRole`'s own comment in * `officer_db/src/roles/queries.ts` for the mechanics. * * ── What this deliberately does NOT do ── * * Never overwrite `description`, as above. And never touch a CUSTOM role's grants — those are set through * `PATCH /api/roles/:name/grants` (`setRoleGrants`), which itself refuses to touch a system role for the * mirror-image reason: writing there would look like it worked and then silently revert on the next run of * this script. */ import { seedRole, waitForDatabase } from '../src/databases/officer_db/src/index'; type SeedRoleEntry = { name: string; description: string; grantsPostgres?: boolean; grantsDocker?: boolean }; // Match today's behaviour exactly: only Developer passed `rolePermitsDevTools` before this table existed, // so Developer is the only role seeded with either flag true. const SEED: SeedRoleEntry[] = [ { name: 'Super Admin', description: 'Full authority over the platform, and an OS account with sudo' }, { name: 'Admin', description: 'Administers the platform. No operating-system privilege' }, { name: 'Developer', description: 'A member who is also trusted with developer tooling on the machine', grantsPostgres: true, grantsDocker: true, }, { name: 'Member', description: 'The default. Reaches only what their role has been granted' }, ]; async function main(): Promise { await waitForDatabase(); let created = 0; for (const role of SEED) { if (await seedRole({ ...role, isSystem: true })) created += 1; } console.log(`[seed:roles] ${created} seeded, ${SEED.length - created} already present (of ${SEED.length})`); } // Never fatal. This runs from a `post` hook after a schema push, and a seed that cannot write must not make // a successful push look like a failed one — the rows are additive and the next push tries again. main() .catch((err: unknown) => { console.error('[seed:roles] could not seed:', err instanceof Error ? err.message : err); }) .finally(() => process.exit(0));