import { identityShimEnabled } from "@/config/env"; import type { Db } from "./types"; /** * Migrations que **este build** espera encontrar aplicadas. * * É uma lista estática de propósito. O health check e o worker precisam * responder "o banco está na versão que este código espera?", não "o banco * combina com os arquivos que por acaso estão no disco". Ler o diretório aqui * também fazia o Next tracear o projeto inteiro para dentro da saída * `standalone`. * * `migrate()` confere que esta lista e o diretório `migrations/` não divergiram. */ export const EXPECTED_MIGRATIONS = [ "0001_platform_shim.sql", "0002_spreadsheet_adapter.sql", "0003_rls.sql", "0004_audit_append_only.sql", "0005_maintenance_roles.sql", ] as const; /** Migrations que criam o shim local de identidade do Elo Pro. */ export const SHIM_MIGRATIONS: ReadonlySet = new Set(["0001_platform_shim.sql"]); export function isShimMigration(file: string): boolean { return SHIM_MIGRATIONS.has(file); } export type PendingOptions = { applyIdentityShim?: boolean }; export type MigrationLedger = { state: "ausente" } | { state: "presente"; ids: string[] }; /** A leitura do histórico falhou. Não é o mesmo que tabela ausente. */ export class MigrationLedgerError extends Error { readonly code = "historico_indisponivel"; constructor() { super("historico_indisponivel"); this.name = "MigrationLedgerError"; } } /** * Lê o histórico sem criá-lo. Tabela ausente é um estado explícito. Falha de * permissão, conexão ou consulta interrompe: não vira lista vazia. */ export async function readMigrationLedger(db: Db): Promise { let presence: { name: string | null }[]; try { presence = await db.withService((query) => query.query<{ name: string | null }>("select to_regclass('public.schema_migrations')::text as name"), ); } catch { throw new MigrationLedgerError(); } if (!presence[0]?.name) return { state: "ausente" }; try { const rows = await db.withService((query) => query.query<{ id: string }>("select id from public.schema_migrations order by id"), ); return { state: "presente", ids: rows.map((row) => row.id) }; } catch { throw new MigrationLedgerError(); } } /** Migrations esperadas que ainda não constam em `schema_migrations`. */ export async function pendingMigrations(db: Db, options: PendingOptions = {}): Promise { const applyShim = options.applyIdentityShim ?? identityShimEnabled(); const ledger = await readMigrationLedger(db); const applied = new Set(ledger.state === "presente" ? ledger.ids : []); return EXPECTED_MIGRATIONS.filter((file) => !applied.has(file) && (applyShim || !isShimMigration(file))); }