Introduz ADAPTER_ENV e endurece configuração e infraestrutura

`next start` força NODE_ENV=production, então homologação não tinha como se
distinguir de produção: o login local ficava desligado (correto) e o módulo
exigia o SSO do Elo Pro, que ainda não existe. Não havia terceiro caminho.

- `ADAPTER_ENV` com development, staging e production, independente de
  NODE_ENV. Toda política passa a derivar dele.
- Fail-fast de configuração em `fail-fast.ts`, separado de `env.ts` porque
  `process.exit` não existe no Edge Runtime.
- `requireDatabaseUrl` elimina o fallback silencioso para PGlite: um segredo
  ausente em staging migrava um arquivo local e parecia sucesso.
- Shim de identidade passa a ser decisão explícita via
  `ADAPTER_APPLY_IDENTITY_SHIM`; aplicá-lo sobre o schema real do Elo Pro é
  destrutivo em potencial.
- `migration-manifest.ts` responde o estado do schema consultando só o banco.
  Ler o diretório numa rota fazia o Next tracear o projeto inteiro para a
  saída standalone, levando docs, testes e fontes para a imagem.
- Redis e Supabase viram singleton por processo. Criar um ioredis por
  requisição vazava conexão: o cliente é lazy, abre no primeiro uso e nunca
  fecha.
- Namespace de chaves deriva de ADAPTER_NAMESPACE e do ambiente lógico, não
  de SENTRY_ENVIRONMENT. Observabilidade e isolamento de dados são decisões
  diferentes, e a variável única fazia homologação e produção colidirem.
- Listagem do bucket passa a paginar e descer em subpastas. Parando em 1000
  entradas, a limpeza por TTL deixava de ver arquivos e temporários venciam
  sem nunca ser removidos.
- `ping` e `hit` no coordenador, e `healthcheck` no armazenamento, para o
  health check tocar as dependências de verdade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
camilafagundesandClaude Opus 5 committed 2026-10-15 20:38:32 -03:00
1 parent 419325db12
commit b972589949
13 files changed
+474 -43

No files matched your search

+53 -4
View File
@@ -1,28 +1,77 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { identityShimEnabled } from "@/config/env";
import { EXPECTED_MIGRATIONS, isShimMigration } from "./migration-manifest";
import type { Db } from "./types";
/**
* Executor de migrations. Lê o diretório `migrations/`, por isso só é usado
* pelos scripts operacionais e pelo banco local de development — nunca por uma
* rota. Quem precisa saber o estado do schema em runtime usa
* `migration-manifest.ts`, que só consulta o banco.
*
* O caminho é estaticamente limitado a `<cwd>/migrations`: montar o caminho a
* partir de uma variável fazia o Next tracear o projeto inteiro para dentro da
* saída `standalone`, levando docs, testes e fontes para a imagem.
*/
const MIGRATIONS_FOLDER = "migrations";
export type MigrateOptions = {
/** Aplica o shim de identidade. Padrão: `ADAPTER_APPLY_IDENTITY_SHIM`. */
applyIdentityShim?: boolean;
};
export function migrationsDir(): string {
return path.join(process.cwd(), "migrations");
return path.join(process.cwd(), MIGRATIONS_FOLDER);
}
export async function migrate(db: Db, directory = migrationsDir()): Promise<string[]> {
export async function listMigrationFiles(): Promise<string[]> {
const entries = await readdir(path.join(process.cwd(), MIGRATIONS_FOLDER));
return entries.filter((file) => file.endsWith(".sql")).sort();
}
/**
* O manifesto estático e o diretório precisam contar a mesma história. Um
* arquivo novo sem entrada no manifesto passaria em branco pelo health check.
*/
export function assertManifestMatches(files: string[]): void {
const expected: string[] = [...EXPECTED_MIGRATIONS];
const extra = files.filter((file) => !expected.includes(file));
const missing = expected.filter((file) => !files.includes(file));
if (extra.length === 0 && missing.length === 0) return;
const parts = [
extra.length ? `fora do manifesto: ${extra.join(", ")}` : "",
missing.length ? `ausentes no diretório: ${missing.join(", ")}` : "",
].filter(Boolean);
throw new Error(
`migrations/ e EXPECTED_MIGRATIONS divergiram (${parts.join("; ")}). Atualize src/infra/db/migration-manifest.ts.`,
);
}
export async function migrate(db: Db, options: MigrateOptions = {}): Promise<string[]> {
const applyShim = options.applyIdentityShim ?? identityShimEnabled();
await db.exec(`
create table if not exists public.schema_migrations (
id text primary key,
applied_at timestamptz not null default now()
);
`);
const files = (await readdir(directory)).filter((file) => file.endsWith(".sql")).sort();
const files = await listMigrationFiles();
assertManifestMatches(files);
const appliedRows = await db.withService((query) => query.query<{ id: string }>("select id from public.schema_migrations"));
const applied = new Set(appliedRows.map((row) => row.id));
const ran: string[] = [];
for (const file of files) {
if (applied.has(file)) continue;
const sql = await readFile(path.join(directory, file), "utf8");
if (isShimMigration(file) && !applyShim) continue;
const sql = await readFile(path.join(process.cwd(), MIGRATIONS_FOLDER, file), "utf8");
await db.exec(sql);
await db.withService((query) => query.query("insert into public.schema_migrations (id) values ($1)", [file]));
ran.push(file);
}
return ran;
}
export { EXPECTED_MIGRATIONS, SHIM_MIGRATIONS, isShimMigration, pendingMigrations } from "./migration-manifest";