Ir para o conteúdo

Banco de dados avançado

Sobre o guia base de banco, o SDK traz quatro peças opcionais para domínios mais exigentes — porte fiel de db.tenant / db.audit / db.outbox / db.user_model do tempest-fastapi-sdk.

Colunas anuláveis são obrigatórias no insert

No tempest-db-js, uma coluna sem .notNull() e sem .default(...) é obrigatória no create — você passa null explicitamente. É por isso que os exemplos abaixo passam lastLoginAt: null, context: null etc.


1. Multi-tenant sem vazamento

TenantScopedRepository amarra um tenantId na construção e o injeta em toda leitura e escrita — impossível esquecer um WHERE tenant_id = ?.

import { BaseModel, TenantScopedRepository, column, tableNameFor } from "tempest-express-sdk";

class OrderModel extends BaseModel {
  static tablename = tableNameFor("OrderModel"); // "order"
  tenantId = column.uuid().notNull();
  total = column.numeric(12, 2).notNull();
}

const repo = new TenantScopedRepository(OrderModel, session, currentTenantId);

await repo.list();                    // WHERE tenant_id = currentTenantId
await repo.create({ total: "10.00" }); // tenant_id carimbado (não passe você)
await repo.getById(otherTenantOrderId); // lança RecordNotFound entre tenants

Reads (list/first/count/exists/getById/paginate) filtram pelo tenant; create/createMany carimbam o tenant; update/delete só atingem linhas do tenant, e update nunca move uma linha para outro tenant. Campo custom: new TenantScopedRepository<typeof M, "orgId">(M, session, id, "orgId").


2. Outbox transacional

Grave a mudança de domínio e o evento a publicar na mesma transação (numa tabela BaseOutboxModel). O evento nunca se perde, mesmo se o broker estiver fora. O OutboxRelay faz o polling e publica com entrega ao-menos-uma-vez.

import {
  BaseOutboxModel,
  BaseRepository,
  OutboxRelay,
  createEngine,
} from "tempest-express-sdk";

class OutboxModel extends BaseOutboxModel {
  static tablename = "outbox";
}

// dentro da transação da escrita de domínio:
await engine.transaction(async (tx) => {
  const orders = new OrderRepository(tx);
  const outbox = new BaseRepository(OutboxModel, tx);
  const order = await orders.create({ tenantId, total: "10.00" });
  await outbox.create({
    topic: "order.created",
    payload: { id: order.id },
    sentAt: null,
    lastError: null,
  });
});

// em um worker de fundo:
const relay = new OutboxRelay(
  new BaseRepository(OutboxModel, engine.session()),
  async (topic, payload) => broker.publish(topic, payload), // seu broker
);
await relay.run(1000); // drena a cada 1s; relay.stop() encerra

Falhas incrementam attempts e reagendam com backoff (availableAt); após maxAttempts a linha vira failed com o lastError.


3. Trilha de auditoria

BaseAuditLogModel é um log append-only de quem mudou o quê. snapshot congela uma linha e diffSnapshots calcula o diff antes/depois — o que o service grava.

import {
  AuditAction,
  BaseAuditLogModel,
  BaseRepository,
  diffSnapshots,
  snapshot,
} from "tempest-express-sdk";

class AuditLogModel extends BaseAuditLogModel {
  static tablename = "audit_log";
}

// no service, ao atualizar um usuário:
const before = snapshot(user, ["hashedPassword"]); // exclui campos sensíveis
await users.update({ id: user.id }, { name: "Ana Maria" });
const after = snapshot(await users.getById(user.id), ["hashedPassword"]);

await new BaseRepository(AuditLogModel, session).create({
  entity: "UserModel",
  entityId: user.id,
  action: AuditAction.UPDATE,
  actor: actorId,
  changes: diffSnapshots(before, after), // { name: { before, after } }
  context: { requestId },
});

4. Modelos base de usuário e token

Pontos de partida opcionais que já trazem as colunas comuns. Estenda, fixe tablename, adicione o que faltar:

import {
  BaseUserModel,
  BaseUserRefreshTokenModel,
  BaseUserTokenModel,
  UserTokenPurpose,
  column,
  tableNameFor,
} from "tempest-express-sdk";

class UserModel extends BaseUserModel {
  static tablename = tableNameFor("UserModel"); // "user"
  name = column.text().notNull();
}
// BaseUserModel: email, hashedPassword, isAdmin, lastLoginAt

class UserTokenModel extends BaseUserTokenModel {
  static tablename = "user_token";
}
// BaseUserTokenModel: userId, tokenHash, purpose, expiresAt, usedAt, payload

class RefreshTokenModel extends BaseUserRefreshTokenModel {
  static tablename = "refresh_token";
}
// BaseUserRefreshTokenModel: userId, tokenHash, familyId, expiresAt, usedAt, revokedAt

UserTokenPurpose traz os valores canônicos (ACTIVATION, PASSWORD_RESET, EMAIL_VERIFICATION, EMAIL_CHANGE). Guarde sempre o hash do token.


5. Log de queries lentas

O tempest-db-js expõe SQL+params no onQuery, mas sem duração — então o timing acontece na camada do driver. wrapWithSlowQueryLog embrulha um AsyncDriver e loga cada statement que passar de um limiar (inclusive dentro de transações):

import { AsyncEngine, NodeSqliteDriver, wrapWithSlowQueryLog } from "tempest-express-sdk";

const sync = NodeSqliteDriver.open("app.db");
const timed = wrapWithSlowQueryLog(
  { execute: (s, p) => Promise.resolve(sync.execute(s, p)), close: async () => sync.close() },
  { thresholdMs: 200 }, // logParameters só em dev (pode conter PII)
);
const engine = new AsyncEngine(timed, "sqlite");

Aplica-se a engines construídos por você

createEngine ainda não expõe injeção de driver, então o wrap vale quando você constrói o AsyncEngine a partir de um driver (SQLite, ou o mesmo padrão dos testes). Um hook de timing no createEngine é upstream.

6. Backup do banco

backupDatabase detecta o dialeto pela URL: pg_dump no Postgres (precisa do binário no PATH), cópia de arquivo no SQLite.

import { backupDatabase } from "tempest-express-sdk";

await backupDatabase("sqlite://./app.db", "./backups/app-2026-07-06.db");
await backupDatabase(
  "postgresql://app@localhost/app",
  "./backups/app.dump",
  { pgDumpArgs: ["-Fc", "--no-owner"] },
);

SQLite em memória lança erro (nada a copiar).

Recapitulando

  • TenantScopedRepository — isolamento por tenant à prova de esquecimento.
  • BaseOutboxModel + OutboxRelay — eventos publicados atomicamente com a escrita.
  • BaseAuditLogModel + snapshot/diffSnapshots — quem mudou o quê.
  • BaseUserModel / BaseUserTokenModel / BaseUserRefreshTokenModel — modelos base opt-in.
  • wrapWithSlowQueryLog — loga queries lentas; backupDatabase — backup por dialeto. ✅