@dokebi/db
v0.4.1
Published
Named database connections for Dokebi: create, readiness check and close pools of pg, mysql2, mariadb or mongodb.
Maintainers
Readme
@dokebi/db
A plugin that manages named resources while keeping the original API of the DB library you chose. Node.js >=24.0.0, core ^0.4.0. Version 0.4.1 (unpublished; 0.4.0 is on npm; resource(name) added in 0.1.2, status(name) and startup in 0.4.1). AI agents start with AI.md.
Usage
defineConnection(name, adapter) creates a definition with a type and a registration identity. The array of definitions is passed through the entry file. The factory input is { app, options?, entry? }; app is required (plugin contract v2).
// src/infra/db/connections.ts — the user-owned entry file the CLI created
import { defineConnection } from '@dokebi/db';
import { postgresAdapter } from './adapters.ts';
export const main = defineConnection('main', postgresAdapter);
export const connections = [main] as const;| Key | Type | Default | Description |
|---|---|---|---|
| connections.<name>.adapter | string | — (required) | Adapter id of the definition |
| connections.<name>.options | any | — (required) | The adapter's own options |
| connections.<name>.startup | required | optional | required | optional (D-0127): the app starts and runs while this database is down, and the connection recovers by itself |
| connections.<name>.recovery: { initialDelayMs, maxDelayMs } | integers | 1000, 30000 | Only with startup: optional: the bounded backoff of its retries (100–60000 and 1000–600000 ms) |
- CLI path:
npx dokebi add @dokebi/dbcreatessrc/infra/db/connections.tsand the settingdokebi-db.connections: {}. The generated code reads the file asimport * as entryNand passescreatePlugin({ app, options, entry: entryN }). Other exports of the module (mainand so on) are ignored. - Direct use:
createPlugin({ app: app.view, entry: { connections } }). A plain object holds onlyconnections. Without an entry there are no connections. - Options: given
optionsare used, otherwiseapp.config['dokebi-db']. An option error isINVALID_PLUGIN_OPTIONSat registration (causeDbErrorINVALID_OPTIONSwithdetails.issues), an entry errorINVALID_PLUGIN_INPUT(causeINVALID_ENTRY,INVALID_DEFINITIONorDUPLICATE_CONNECTION). At most 32 connections. - The entry's names and adapter IDs must match the YAML
options.dokebi-db.connectionsexactly. At start (setup) differences are told apart by these codes;DbError.connectionholds only the connection name.CONNECTION_NOT_CONFIGURED: in the entry, not in the settings.CONNECTION_NOT_DEFINED: in the settings, not in the entry.ADAPTER_MISMATCH: same name, but the configuredadapterdiffers from the definition's adapter ID.
- Another plugin that uses db puts
import { dependency } from '@dokebi/db'in its metadatadependenciesand gets the API withcontext.getDependency(dependency).
An adapter provides id, parseOptions and create. create returns a managed handle { value, open, check, close } synchronously. close is registered before open and check run, so a partial connection failure is cleaned up too. Cleaning up resources from a failure before create returns is the adapter's job. Bounded waits and cancellation for open and check follow the chosen library's settings.
create(options, { name, logger }) also gets the connection's name and a logger bound to it (D-0112): log pool decisions at debug and driver errors no caller receives at error. Pool limits come from dokebi.yaml (D-0112). The adapter templates dokebi add @dokebi/db --adapter pg|mysql2|mariadb|mongodb writes read max, waitTimeoutMs, the idle, connect, statement, query and check time limits (and mysql2's maxWaiting) from connections.<name>.options, check each with a range, and the CLI writes them with their defaults. The wait for a pool connection is bounded by time (waitTimeoutMs, default 3000; mysql2 has no deadline of its own, so its template adds one), and a wait that runs out answers 503 DB_UNAVAILABLE with Retry-After. The option table and the sizing rules (workers × max below the server's limit, the wait deadline against the request's budget, why a small waiting-count cap refuses work that can be done) are in AI.md §1.1 and §1.2.
db.connection(definition) returns the original pool or client as is. db.resource(name) returns the same resource by name (untyped, for tools such as @dokebi/migrate). db.check(definition) is an explicit readiness check, and concurrent checks of the same connection are shared. db.names is the immutable registration order. Other definition objects and unknown names are rejected. There is no lookup by string with an arbitrary generic assertion.
health is an optional dependency (dokebi-health ^0.4.0, optional). When health is registered in the same app it is ready first regardless of registration order, and db registers the check db:<registration index>:<first 55 characters of the name>. When it is not registered, nothing is created and db works without health. Only start and explicit checks count; it does not mean continuous connection monitoring.
Optional connections (D-0127). startup: optional on a connection (default required, as before) is for a store the site must keep running without, such as audit or job logs. A failed open or check at start is logged once at warn (DB_STARTUP_UNAVAILABLE, the connection name and the whole error, never the URL) and the start goes on; its health check is registered not required and shows failed, so readiness stays up. While it is down, open and check are retried by one timer per connection with a bounded backoff (recovery.initialDelayMs doubled up to recovery.maxDelayMs, with jitter), one attempt at a time however many callers ask, until a check passes (DB_RECOVERED). connection() still returns the client of an adapter whose resource says lazy: true (pools, MongoClient; all four templates), so its driver errors reach the app (and unhandled ones answer 503 DB_UNAVAILABLE); for any other adapter it throws a coded 503 DB_UNAVAILABLE with a retry time until open succeeded. db.status(name) tells the policy and state. Details, and what @dokebi/migrate, describe and the SQL stores of auth and oauth do with such a connection: AI.md §5.1.
When the check state changes, only { connection, code } is written to context.logger (DB_READY info, DB_CHECK_FAILED warn with the full error, DB_RECOVERED info). A repeated state is not logged.
Errors are DbErrors, core DokebiErrors with plugin: 'dokebi-db' and a fix; the code list is in AI.md.
Driver errors over HTTP (D-0093). An adapter may add classifyError(error), returning conflict, invalid, timeout, retryable, unavailable or undefined. @dokebi/db/errors exports pure classifiers that load no driver: classifyPostgresError (SQLSTATE), classifyMysqlError (MySQL and MariaDB errno) and classifyMongoError (MongoDB codes, labels and error classes, also of the error a bulk write error wraps), and the adapter templates the CLI writes wire them. dokebi-db contributes them to @dokebi/web's dokebi-web:error-translator, so a driver error a route leaves unhandled answers 409 DB_CONFLICT, 422 DB_INVALID_DATA, or 503 DB_TIMEOUT, DB_BUSY or DB_UNAVAILABLE with Retry-After, instead of 500. The body holds fixed sentences only (no driver text, values or constraint names); the error log WEB_ERROR_TRANSLATED holds the whole driver error. Violations that mean something to a user (a taken email) are still caught in the service and thrown as the app's own code. The tables are in AI.md §4.1.
Row budgets in tests (D-0117). @dokebi/db/testing, loaded only by tests, measures one production statement under the database's own plan instrumentation: measureQuery(resource, sql | command, params, { dialect }) with dialect postgres (EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)), mysql or mariadb (the Handler_read_* session counters around the statement, with the plan of EXPLAIN ANALYZE or ANALYZE FORMAT=JSON) or mongodb (explain with executionStats). It returns rows (documents) examined and returned, the indexes used, whether a table was read in full or every match was sorted, buffers or pages where the database reports them, and the raw plan. SQL runs in a transaction that is always rolled back, so a measured UPDATE or DELETE changes nothing; DDL is refused. assertRowBudget(result, { maxExamined, noFullScan, noSortOfAll }) fails a test with the query, the numbers, the indexes and the plan, and the optional captureQueries(resource, run) records the exact SQL (or MongoDB commands) the app's model sent, so the test measures production SQL rather than a copy. It loads no driver: it uses the connection's resource. The details and what the measurements showed per database are in AI.md §7; examples/21-db/row-budget.ts in the repository keeps a last-page budget on skewed PostgreSQL data.
Shutdown
Consumers never close borrowed resources. They wait for all accepted work in their own drain. Core starts the db drain after the drain of plugins that depend on db. The DB drain waits only for its own checks in progress and does not close pools. When the drain deadline passes and context.drainSignal aborts, it stops waiting and passes that signal to checks in progress. Existing work can still run later queries and transactions while stopping. After every plugin's drain succeeded, defer cleans up the resources. When another drain fails, shutdown.onDrainFailure decides: the default 'cleanup' cleans up, 'retain' keeps the DB.
Detached Promises, cursors and sessions of the original API are not tracked or reclaimed automatically. A timeout or socket close is not business completion. A close failure is reported and the cleanup of other resources continues. Query retries, shared SQL/DDL, transaction wrappers and automatic seeds are not provided. Table changes and seeds run with @dokebi/migrate at deployment.
Install and examples
DB drivers are not runtime dependencies. Install only the driver or ORM the app chose. npx dokebi add @dokebi/db prepares the following through the static manifest v2 declaration (dokebi.install); there is no install module.
- Entry file
templates/connections.ts→src/infra/db/connections.ts(an emptyconnectionsarray and a commented example). It is user-owned and never overwritten by an upgrade. - Setting default
dokebi-db: { connections: {} }. Empty connections are a normal configuration without resources.
Write an adapter, put the definition in the entry file and write the same name's adapter and options in YAML; the generated code connects them as is. Do not edit dokebi.generated.ts.
Standalone install and the original API types are verified by install tests. examples/21-db in the repository uses pg, mysql2, mariadb and mongodb each and verifies real Docker connections, DDL/seeds/transactions, translated driver errors over HTTP and cluster drain. The four libraries are not a support list the framework enforces.
