@krizaka/db
v0.3.1
Published
The database foundation of every Krizaka module: shared Query, Connection and Clock types, transactions, versioned and frozen module schemas embedded in the product's own migration sequence, a runner for products without one, and a test database helper. N
Maintainers
Readme
@krizaka/db
The database foundation of every Krizaka module (ADR-001). It has no database
client: you pass your own connection (pg's Client or PoolClient fits as it is).
- The shared types:
Query,Connection,Connect,Clock. transaction(connect, work):BEGIN … COMMIT, orROLLBACKwhen the work throws.- Module schemas (
defineModule): each module's tables live in a Postgres schema named after it. Its versions are frozen and checksummed. emitMigrationwrites a module version into your own migration sequence, followed by a marker inpublic.krizaka_module_versions.adoptMigrationadopts tables you already have: markers only, no DDL.assertModules, at boot, refuses a database that lacks a version the code needs.migrateis a runner for products without one: an advisory lock, a history table, one transaction per step.@krizaka/db/testing:createTestDatabase,endPool(ends a pg pool and waits for its sockets, before the database is dropped),assertEmbedded(your drift test) andassertUpgrade.
pnpm add @krizaka/dbA module declares its schema
import { defineModule } from "@krizaka/db";
export const auditSchema = defineModule({
module: "audit", // also its Postgres schema, unless the product renames it (or the module sets `schema: "public"`, for tables that existed before and are adopted)
tables: ["owner"], // the product tables it references, by role, and only those
migrations: [
{ version: 1, name: "entries", sql: (c) => `CREATE TABLE ${c.schema}.entry (id bigserial PRIMARY KEY, actor_id uuid REFERENCES ${c.tables.owner}(id) ON DELETE SET NULL);` },
], // 1, 2, 3… in order; a released version never changes
});A product embeds it in its own sequence
import { emitMigration } from "@krizaka/db";
import { auditSchema } from "@krizaka/audit";
const context = { tables: { owner: "accounts" } };
// drizzle-kit: `drizzle-kit generate --custom --name=audit_1`, then write this text into the generated file.
// Your own list: append { version: "0031_audit_1", sql: emitMigration(auditSchema, 1, context) }.
process.stdout.write(emitMigration(auditSchema, 1, context));Commit what it prints. Never generate it at runtime: the database changes only through your reviewed sequence.
Upgrading a module is a package bump. Your drift test then names the version to add. It compares the SQL text itself with what
emitMigrationwrites today (whitespace and drizzle's--> statement-breakpointaside), not only the marker, so an edited or deleted statement fails too. It also checks that a module comes after the modules it requires, within a file as across files:import { assertEmbedded } from "@krizaka/db/testing"; test("every module version is embedded", () => assertEmbedded(readMigrationFiles(), [{ schema: auditSchema, context }]));At boot, after your migrations:
await assertModules(query, [{ schema: auditSchema, context }]).Tables that already exist (copied from a package before
@krizaka/db): embedadoptMigration(auditSchema, 1, context)once. It writes the markers, and nothing is rebuilt.
One use case, one transaction
import { transaction } from "@krizaka/db";
await transaction(() => pool.connect(), async (tx) => {
await coupons.redeem(tx, input);
await audit.record(tx, entry); // both commit, or neither
});The runner, for products without one
import { migrate } from "@krizaka/db";
await migrate(() => pool.connect(), migrations, { lockKey: 73191 }); // history table `schema_migrations (version text)`, by defaultTests on a real PostgreSQL
import { createTestDatabase } from "@krizaka/db/testing";
const db = await createTestDatabase({
connect: async (database) => { const c = new pg.Client({ connectionString: urlOf(database) }); await c.connect(); return { query: (s, p) => c.query(s, p), release: () => c.end() }; },
migrate: (connect) => migrate(connect, migrations, { lockKey: 1 }),
prefix: "ak",
});
// … tests on db.name …
await db.drop();- No container and no network beyond the server you point at (CI's service).
- Errors are
DbError, with a stablecode(DB_MISSING_MODULES,DB_FROZEN_VERSION_CHANGED,DB_MODULE_ORDER,DB_UNSAFE_IDENTIFIER…) and the items indetails.
