import type { Dialect } from "@wrnexus/db"; /** * DDL for the two assignment tables, as a list of statements rather than one * blob: splitting a blob on a separator makes runtime correctness depend on * source formatting, and only the sqlite driver accepts multi-statement exec. * * `scope` holds a tenant id, or the empty string for a global assignment, so * the unique constraints work on every dialect (NULL is not comparable in a * UNIQUE index). `effect` is CHECK-constrained: an unrecognised value would * otherwise be dropped from both the grant and deny buckets on read, silently * turning a deny into a no-op. */ export function authzMigrationSql(dialect: Dialect): { up: string[]; down: string[] } { const id = dialect === "postgres" ? "SERIAL PRIMARY KEY" : dialect === "mysql" ? "INT AUTO_INCREMENT PRIMARY KEY" : "INTEGER PRIMARY KEY AUTOINCREMENT"; const timestamp = dialect === "sqlite" ? "TEXT" : "TIMESTAMP"; // MySQL's default collation is case- and accent-insensitive, which would let // tenant "T1" match "t1" and collapse roles "admin"/"Admin" onto one row. const exact = dialect === "mysql" ? " COLLATE utf8mb4_bin" : ""; const key = `VARCHAR(255)${exact} NOT NULL`; return { up: [ `CREATE TABLE IF NOT EXISTS _wrn_authz_assignment ( id ${id}, subject_id ${key}, scope ${key} DEFAULT '', role ${key}, created_at ${timestamp} NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT _wrn_authz_assignment_unique UNIQUE (subject_id, scope, role) )`, `CREATE TABLE IF NOT EXISTS _wrn_authz_grant ( id ${id}, subject_id ${key}, scope ${key} DEFAULT '', permission ${key}, effect VARCHAR(16) NOT NULL CHECK (effect IN ('allow', 'deny')), created_at ${timestamp} NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT _wrn_authz_grant_unique UNIQUE (subject_id, scope, permission) )`, ], down: ["DROP TABLE IF EXISTS _wrn_authz_grant", "DROP TABLE IF EXISTS _wrn_authz_assignment"], }; }