58 lines
2.0 KiB
TypeScript
58 lines
2.0 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
/**
|
|
* Deployment-wide safety net for the "always retain at least one admin"
|
|
* invariant. Even if a future code path bypasses
|
|
* `UsersService.applyLastAdminSafeMutation`, the database itself aborts
|
|
* the offending UPDATE/DELETE with a clear `LAST_ADMIN` error message.
|
|
*
|
|
* Triggers fire before the row mutation, so the offending statement never
|
|
* commits. The service-level wrapper still rolls back with 409 LAST_ADMIN
|
|
* in normal operation; the triggers are the belt-and-braces guard for
|
|
* cross-instance / out-of-band mistakes.
|
|
*
|
|
* Forward-only.
|
|
*/
|
|
export class AddUserLastAdminTriggers1700000000800 implements MigrationInterface {
|
|
name = 'AddUserLastAdminTriggers1700000000800';
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
// UPDATE trigger: blocks any role mutation that would leave the system
|
|
// with zero admins. We use WHEN to short-circuit so the trigger body
|
|
// only fires when (a) the old role was admin, (b) the new role is no
|
|
// longer admin, and (c) no other admin row exists.
|
|
await queryRunner.query(`
|
|
CREATE TRIGGER IF NOT EXISTS "trg_user_last_admin_update"
|
|
BEFORE UPDATE OF "role" ON "user"
|
|
FOR EACH ROW
|
|
WHEN OLD."role" = 'admin' AND NEW."role" <> 'admin'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM "user" AS "other"
|
|
WHERE "other"."role" = 'admin' AND "other"."id" <> OLD."id"
|
|
)
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'LAST_ADMIN');
|
|
END;
|
|
`);
|
|
|
|
// DELETE trigger: blocks deletion of the only remaining admin row.
|
|
await queryRunner.query(`
|
|
CREATE TRIGGER IF NOT EXISTS "trg_user_last_admin_delete"
|
|
BEFORE DELETE ON "user"
|
|
FOR EACH ROW
|
|
WHEN OLD."role" = 'admin'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM "user" AS "other"
|
|
WHERE "other"."role" = 'admin' AND "other"."id" <> OLD."id"
|
|
)
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'LAST_ADMIN');
|
|
END;
|
|
`);
|
|
}
|
|
|
|
public async down(): Promise<void> {
|
|
// Forward-only.
|
|
}
|
|
}
|