Skip to content

PostgreSQL

O Empilha aceita um runner compatível com PostgreSQL. O pacote @empilha/pg integra o driver pg e administra o pool no ciclo de vida da aplicação.

Instale

sh
bun add @empilha/pg pg
bun add -d @types/pg

Configure

ts
import { createApplication, defineModule } from "empilha";
import { postgres } from "@empilha/pg";

const database = postgres({
  url: process.env.DATABASE_URL!,
  sql: "./src/queries",
  healthCheck: "database",
});

const AppModule = defineModule({
  name: "app",
  controllers: [TaskController],
  plugins: [database],
});
const app = await createApplication(AppModule);

await app.run({ port: 4000 });

O plugin:

  • cria um pg.Pool;
  • carrega os arquivos SQL quando sql aponta para um arquivo ou diretório;
  • habilita queries e transações;
  • registra um check do banco;
  • encerra o pool em app.close().

Use um pool existente

Se a aplicação já cria o pool:

ts
import { Pool } from "pg";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

const AppModule = defineModule({
  name: "app",
  controllers: [TaskController],
});
const app = await createApplication(AppModule, {
  configure: (app) => app.postgres(pool, {
    sql: "./src/queries",
    healthCheck: "database",
  }),
});

Por padrão, app.close() também chama pool.end(). Se o pool pertence a outro componente do processo, preserve-o com close: false:

ts
app.postgres(pool, {
  close: false,
  healthCheck: false,
});

Execute migrations

Para executar migrations pelo ciclo de inicialização da aplicação, configure o runner e registre o diretório:

ts
app.migrations({ directory: "src/database" });

O runtime usa checksum e advisory lock no PostgreSQL. A mesma operação também está disponível como runMigrations(runner, options) para scripts.

Adicione:

json
{
  "scripts": {
    "migrate": "bun node_modules/empilha/scripts/database/migrate.ts"
  }
}

Execute:

sh
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/tasks \
  bun run migrate

Arquivos em src/database rodam em ordem lexicográfica. O histórico fica em empilha_migrations; alterar o conteúdo de uma migration já aplicada causa erro de checksum.

O migrador usa um advisory lock do PostgreSQL para que apenas uma execução por banco aplique migrations por vez. Cada arquivo SQL e seu registro de checksum são confirmados na mesma transação: se o arquivo falhar, o histórico também não é alterado.

O script bun run dev do scaffold usa esse mesmo migrador versionado antes de iniciar a aplicação. Portanto, reiniciar o ambiente de desenvolvimento não executa novamente migrations já registradas.

Verifique a saúde

Com health check configurado:

sh
curl http://localhost:4000/health/ready
json
{
  "status": "ok",
  "checks": {
    "database": "up"
  }
}

Uma falha em qualquer check responde 503 com status degraded.

Timeout

No plugin oficial, timeout configura o limite das operações do Empilha e, se statement_timeout não tiver sido informado, usa o mesmo valor no PostgreSQL:

ts
postgres({
  url: process.env.DATABASE_URL!,
  timeout: 5_000,
  lock_timeout: 2_000,
});
ts
app.postgres(pool, {
  timeout: 5_000,
  sql: "./src/queries",
});

O timeout gera 504 e envia um AbortSignal ao runner. O plugin oficial @empilha/pg usa uma conexão separada para solicitar o cancelamento da query no PostgreSQL. Em um runner próprio, o método query() precisa observar options.signal; caso contrário, o framework encerra a requisição pelo timeout de parede, mas o driver pode continuar o trabalho até o próprio limite.

Configure também statement_timeout e lock_timeout no PostgreSQL; o limite da aplicação não substitui limites do banco.

Outro driver

Para integrar outro driver, implemente o contrato mínimo:

ts
type PostgresQueryRunner = {
  query(
    sql: string,
    params?: unknown[],
    options?: { signal?: AbortSignal },
  ): Promise<{ rows: unknown[] }>;

  connect?(): Promise<{
    query: PostgresQueryRunner["query"];
    release(): void;
  }>;
};

Passe o runner a app.postgres(runner). Transações exigem connect().

Banco é um recurso da aplicação

Deixe criação, health check e fechamento próximos do bootstrap. Controllers devem conhecer queries, não o ciclo de vida do pool.

Feito para APIs que continuam simples quando crescem.