Phase 0 of moving roles out of a TypeScript union. The four the code names become rows, marked isSystem because each is referenced by name somewhere: rolePermitsDevTools tests for 'Developer', bootstrap writes 'Super Admin', users.role defaults to 'Member', and 'Admin' is what rolePermitsDevTools deliberately excludes. Renaming or deleting one of those is a code change, so the flag is there for a future delete path to refuse on rather than for the failure to surface later as an account that cannot log in. DELIBERATELY NO FOREIGN KEY on users.role. That change cannot go through db:push: push creates the table empty and adds the constraint in the same run, so every existing account violates it. Verified against a scratch database — push reported the violation, created the table, skipped the constraint, and exited 0. The FK is a separate release, once every host holds these rows. So this phase is additive and cannot fail, which is what makes it safe to hand to an unattended rollout across five hosts. Nothing reads the table yet; USER_ROLES is still the type and authorize.ts is untouched. The seed runs from postdb:push, so a host gets the rows on its next schema push with no extra step in a deploy plan. It never overwrites: a description edited on one server must not be reverted by the next deploy. Verified here the way the deploy doc now requires — by query, not by exit code. Four rows present, seed idempotent on a second run, and a second push is a no-op, which is the proof that the schema and the database agree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 lines
2.9 KiB
TypeScript
54 lines
2.9 KiB
TypeScript
/**
|
|
* 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: `rolePermitsDevTools()` tests for `'Developer'`,
|
|
* `bootstrap.ts` writes `'Super Admin'`, `users.role` defaults to `'Member'`, and `'Admin'` is the one
|
|
* `rolePermitsDevTools` deliberately excludes. 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.
|
|
*
|
|
* ── What this deliberately does NOT do ──
|
|
*
|
|
* No foreign key from `users.role`. That change cannot go through `db:push`: push creates the table empty
|
|
* and adds the constraint in the same run, so every existing account violates it — verified against a
|
|
* scratch database on 2026-08-23, where push reported the violation, created the table, skipped the
|
|
* constraint, and exited 0. The FK is a separate release, once every host holds these rows.
|
|
*
|
|
* So today `users.role` is still a plain text column and nothing enforces that its value is a real role.
|
|
* That is the intended state of this phase: additive, unable to fail, and safe to run on five hosts
|
|
* unattended.
|
|
*
|
|
* Never overwrites. A description edited on one server must not be reverted by the next deploy.
|
|
*/
|
|
import { seedRole, waitForDatabase } from '../src/databases/officer_db/src/index';
|
|
|
|
const SEED: { name: string; description: string }[] = [
|
|
{ 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' },
|
|
{ name: 'Member', description: 'The default. Reaches only what their role has been granted' },
|
|
];
|
|
|
|
async function main(): Promise<void> {
|
|
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));
|