@venturekit/data
v0.0.48
Published
Database and data layer for VentureKit
Readme
@venturekit/data
Warning: This package is in active development and not production-ready. APIs may change without notice.
Database and data layer for VentureKit — RDS configuration, migrations, query utilities, and transactions.
Installation
npm install @venturekit/data@devOverview
@venturekit/data provides:
- RDS configuration —
createRdsConfig(),buildRdsConfig()for PostgreSQL and MySQL - Migration utilities —
createMigrationConfig(), Flyway environment helpers - Query utilities —
query(),getPool(),mapResults(),mapRow() - Transaction support —
beginTransaction(),withTransaction(),buildTransaction()
Query Utilities
import { query, getPool, mapResults } from '@venturekit/data';
// Simple query
const result = await query('SELECT * FROM tasks WHERE id = $1', [taskId]);
// Get the connection pool
const pool = getPool();
// Map results with a custom mapper
const tasks = mapResults(result, row => ({
id: row.id,
title: row.title,
completed: row.completed,
}));Transactions
import { withTransaction, beginTransaction } from '@venturekit/data';
// Automatic commit/rollback
const result = await withTransaction(async (tx) => {
await tx.query('INSERT INTO tasks (title) VALUES ($1)', ['New task']);
await tx.query('UPDATE counters SET count = count + 1 WHERE name = $1', ['tasks']);
return { created: true };
});
// Manual transaction control
const tx = await beginTransaction();
try {
await tx.query('INSERT INTO tasks (title) VALUES ($1)', ['New task']);
await tx.commit();
} catch (error) {
await tx.rollback();
throw error;
}Transactional Handlers
When used with @venturekit/runtime, enable automatic transactions per request:
import { handler } from '@venturekit/runtime';
export const main = handler(async (body, ctx, logger) => {
// ctx.tx is available — auto-commits on success, rolls back on error
await ctx.tx.query('INSERT INTO tasks (title) VALUES ($1)', [body.title]);
return { created: true };
}, { scopes: ['tasks.write'], transactional: true });RDS Proxy (opt-in)
Each warm Lambda container holds its own pg pool (DB_POOL_MAX, default 4
in Lambda), so a burst of containers can exhaust a small instance's
max_connections. databases[].proxy: true puts an RDS Proxy in front:
DB_HOST becomes the proxy endpoint (same port, same secret, TLS required —
the vendored RDS CA bundle covers proxy endpoints), the Lambda security group
is admitted to the proxy and the proxy to the instance; the in-VPC migration
runner keeps talking to the instance. It is billed per vCPU-hour of the
target (≈ $22/month per small database), which is why it is off by default.
databases: [{ id: 'main', type: 'postgres', name: 'app', proxy: { maxConnectionsPercent: 80 } }]SET LOCAL inside a transaction — how the tenancy helpers set RLS GUCs — is
compatible with the proxy's transaction-level pooling.
RDS Configuration
import { createRdsConfig, DEFAULT_RDS_CONFIG } from '@venturekit/data';
const rdsConfig = createRdsConfig({
engine: 'postgres',
instanceSize: 'small',
databaseName: 'mydb',
});Migrations
@venturekit/data ships a pure-SQL migration runner. Drop .sql files
into db/migrations/ and apply them with the CLI:
vk migrate # Apply pending schema migrations
vk migrate --seed # Apply migrations, then seeds from db/seeds/
vk migrate status # Show which migrations and seeds have been appliedApplied files are tracked in __vk_migrations / __vk_seeds. The
file-content hash is locked once recorded — editing an applied file
hard-fails on the next run; write a new migration instead.
Feature flags and tenant settings
@venturekit/data/flags — a flag has a global default, a percentage rollout
and pins per tenant / user (user pin > tenant pin > global > rollout). The
evaluator keeps an in-memory snapshot (30 s TTL by default), so a check costs
no query; unknown keys are false, never an error.
import { createFlags, upsertFlag, setFlagOverride, getTenantSettings, setTenantSettings } from '@venturekit/data/flags';
import { query } from '@venturekit/data';
const flags = createFlags({ querier: query }); // module scope
// in a handler
if (await flags.isEnabled('new-editor', { tenantId: ctx.tenant?.id, userId: ctx.user?.id })) { … }
ctx.flags = await flags.evaluateAll({ tenantId, userId }); // hydrate once per request
// admin
await upsertFlag(query, { key: 'new-editor', rolloutPercent: 10 });
await setFlagOverride(query, { key: 'new-editor', scope: 'tenant', scopeId: 'acme', enabled: true });
// one JSONB document per tenant, shallow-merged in SQL (null deletes a key)
await setTenantSettings(query, tenantId, { theme: 'dark', legacy: null }, { validate: (d) => settingsSchema.parse(d) });
const settings = await getTenantSettings(query, tenantId);Search helpers
@venturekit/data/search — pure SQL builders: containsPattern /
prefixPattern escape % and _ for ILIKE (plus trigramIndexSql for the
pg_trgm index); toWebSearchQuery bounds user input for
websearch_to_tsquery, ftsColumnSql / ftsIndexSql generate the migration
lines for a weighted, STORED tsvector column, and ftsWhereSql /
ftsRankSql / ftsHeadlineSql are the query-side fragments; encodeCursor /
decodeCursor / keysetWhereSql implement keyset pagination.
const q = toWebSearchQuery(input);
const rows = q
? await query(`SELECT id, title, ${ftsHeadlineSql('body', 1)} AS snippet FROM posts
WHERE ${ftsWhereSql(1)} ORDER BY ${ftsRankSql(1)} DESC LIMIT 20`, [q])
: [];API Reference
See the API reference for full documentation.
License
Apache-2.0 — see LICENSE for details.
