75 lines
2.7 KiB
TypeScript
75 lines
2.7 KiB
TypeScript
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<string> = 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<MigrationLedger> {
|
|
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<string[]> {
|
|
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)));
|
|
}
|