Adapters
A modular adapter system
Section titled “A modular adapter system”The adapter layer isolates connection lifecycle, query execution, transactions, and health checks from the ORM core. The core package has zero database driver dependencies.
Register adapters in jsorm.config.ts
Section titled “Register adapters in jsorm.config.ts”Each connection source is defined with defineConnectionSource() and registered in defineJsormConfig():
// jsorm.config.tsimport { defineConnectionSource, defineJsormConfig } from 'jsorm';import { pgAdapter } from 'jsorm-pg';
const main = defineConnectionSource({ adapter: pgAdapter({ name: 'main', connectionString: process.env.DATABASE_URL!, pool: { min: 2, max: 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, }, }),});
export default defineJsormConfig({ connectionSources: { main }, defaults: { connectionSource: 'main' },});Requires pg peer dependency:
pnpm add jsorm-pg pg// jsorm.config.tsimport { defineConnectionSource, defineJsormConfig } from 'jsorm';import { mysqlAdapter } from 'jsorm-mysql';
const main = defineConnectionSource({ adapter: mysqlAdapter({ name: 'main', host: process.env.DB_HOST!, port: parseInt(process.env.DB_PORT ?? '3306'), user: process.env.DB_USER!, password: process.env.DB_PASSWORD!, database: process.env.DB_NAME!, pool: { min: 2, max: 10 }, }),});
export default defineJsormConfig({ connectionSources: { main }, defaults: { connectionSource: 'main' },});Requires mysql2 peer dependency:
pnpm add jsorm-mysql mysql2// jsorm.config.tsimport { defineConnectionSource, defineJsormConfig } from 'jsorm';import { sqliteAdapter } from 'jsorm-sqlite';
const main = defineConnectionSource({ adapter: sqliteAdapter({ name: 'main', file: process.env.DB_PATH ?? './data/app.db', }),});
export default defineJsormConfig({ connectionSources: { main }, defaults: { connectionSource: 'main' },});Requires sqlite3 peer dependency:
pnpm add jsorm-sqlite sqlite3Multi-database configuration
Section titled “Multi-database configuration”Define multiple named connection sources and target them with jsorm.use() or jsorm.with():
// jsorm.config.tsimport { defineConnectionSource, defineJsormConfig } from 'jsorm';import { pgAdapter } from 'jsorm-pg';import { mysqlAdapter } from 'jsorm-mysql';import { sqliteAdapter } from 'jsorm-sqlite';
const main = defineConnectionSource({ adapter: pgAdapter({ name: 'main', connectionString: process.env.DATABASE_URL! }),});
const analytics = defineConnectionSource({ adapter: mysqlAdapter({ name: 'analytics', host: process.env.ANALYTICS_HOST!, user: process.env.ANALYTICS_USER!, password: process.env.ANALYTICS_PASSWORD!, database: process.env.ANALYTICS_DB!, }),});
const cache = defineConnectionSource({ adapter: sqliteAdapter({ name: 'cache', file: ':memory:' }),});
export default defineJsormConfig({ connectionSources: { main, analytics, cache }, defaults: { connectionSource: 'main' },});Target a specific source at runtime:
import { jsorm } from 'jsorm';
// use() returns a scoped client (no global mutation)const rows = await jsorm.use('analytics').get(Event, { select: { id: true, type: true }, pagination: { perPage: 100, currentPage: 1 },});
// with() scopes a callback and releases automaticallyawait jsorm.with('cache', async (cacheDb) => { await cacheDb.executeSql('SELECT 1');});Health check
Section titled “Health check”Use jsorm.healthCheck() to verify adapter connectivity — useful in readiness probes:
const health = await jsorm.healthCheck();// { main: 'ok', analytics: 'ok', cache: 'ok' }Also available via CLI:
jsorm db:checkTransactions
Section titled “Transactions”await jsorm.transaction(async (tx) => { await tx.insert(User, { name: 'Alice', createdAt: new Date(), role: { connect: 1 }, }); await tx.update(Role, { data: { name: 'member' }, where: { id: { eq: 1 } }, }); // rolls back automatically if either throws});Custom adapter injection
Section titled “Custom adapter injection”If your application already manages its own pool or connection lifecycle, implement the createPooledAdapter contract:
import { defineConnectionSource, defineJsormConfig, jsorm, createPooledAdapter, type DBRow,} from 'jsorm';import { Pool, type PoolClient } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const toPostgresSql = (sql: string) => sql.replace(/\?/g, (_, i) => `$${++i}`);
const adapter = createPooledAdapter<Pool, PoolClient>({ name: 'main', connect: async () => pool, disconnect: async (p) => p.end(), getConnection: async (p) => p.connect(), releaseConnection: async (c) => c.release(), query: async (c, sql, params): Promise<readonly DBRow[]> => { const result = await c.query(toPostgresSql(sql), [...params]); return result.rows as readonly DBRow[]; }, transaction: async (p, run) => { const c = await p.connect(); try { await c.query('BEGIN'); const result = await run(c); await c.query('COMMIT'); return result; } catch (e) { await c.query('ROLLBACK'); throw e; } finally { c.release(); } }, metadata: { dialect: 'postgres', driver: 'pg', packageName: 'custom', peerDependencies: ['pg'], capabilities: { pooling: true, transactions: true, healthCheck: true }, },});
const main = defineConnectionSource({ adapter });
export default defineJsormConfig({ connectionSources: { main }, defaults: { connectionSource: 'main' },});Adapter lifecycle
Section titled “Adapter lifecycle”- Adapters are lazy-initialized on the first query — pools do not open until used.
- Pools are reused across queries in the same process.
- Call
jsorm.close()during graceful shutdown to drain all active pools.
Best practices
Section titled “Best practices”- Never create a new adapter per request — always reuse.
- Use
jsorm.use('name')for occasional cross-database reads; usejsorm.with('name', fn)for scoped callback patterns. - Use
jsorm.healthCheck()in readiness endpoints and deploy verification. - Keep
JSORM_DEBUG=1enabled in local development to log config resolution and pool events.