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 { // 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 { // Forward-only. } }