@kequtech/tenon-orm 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.4
4
+
5
+ - Pass the shared `TenonConfig` directly to `createTenon`, including its connection, codecs, and
6
+ `onQuery` observer. `TenonConfig` replaces the `TenonOptions` type.
7
+ - Defer pool creation and connection validation until the first query, transaction, or native-pool
8
+ access. Importing configuration and closing an unused instance need no credentials. Generated
9
+ configs use plain environment reads; remove eager credential checks from existing configs.
10
+ - Support named config exports for multiple databases and CLI selection with `--database`. A sole
11
+ config is selected automatically; ambiguous or unknown names fail without fallback. `tenon init`
12
+ generates a named `main` export, and existing default exports remain supported.
13
+ - Allow migration-file creation without database credentials while retaining each selected config's
14
+ migration, generation, and checking settings.
15
+
3
16
  ## 0.4.3
4
17
 
5
18
  - Add shared runtime/CLI configuration and offline `tenon init`. Restore `tenon generate`, share migration defaults, and require config for generation/checking/inspection.
package/README.md CHANGED
@@ -15,14 +15,14 @@ typings. The PostgreSQL driver, [pgclear](https://github.com/kequtech/pgclear),
15
15
 
16
16
  ## Query with SQL
17
17
 
18
- This example assumes a `public.users` table with integer `id` and text `email` columns:
18
+ This example assumes a `public.users` table with integer `id` and text `email` columns and a
19
+ `main` export in your [shared configuration](docs/configuration.md):
19
20
 
20
21
  ```ts
21
22
  import { createTenon, sql } from '@kequtech/tenon-orm';
23
+ import { main } from './tenon.config.ts';
22
24
 
23
- const connection = process.env.DATABASE_URL;
24
- if (!connection) throw new Error('DATABASE_URL is required');
25
- const db = createTenon({ connection });
25
+ const db = createTenon(main);
26
26
 
27
27
  try {
28
28
  const email = 'reader@example.test';
@@ -97,10 +97,12 @@ choosing application types.
97
97
 
98
98
  ## Share configuration with the CLI
99
99
 
100
- Run `tenon init` to create `tenon.config.ts`, put connection settings and codecs there, and use its
101
- `connection` in `createTenon`.
102
- Then run `tenon generate`, `tenon generate --check`, `tenon check`, and `tenon describe query.sql`
103
- without custom scripts. Migration commands also use the shared connection and migration defaults.
100
+ Run `tenon init` to create `tenon.config.ts`. Put connection settings and codecs in its `main` export,
101
+ then reuse that object with `createTenon(main)`. Initialization stays lazy, so builds that only import
102
+ the database instance need no credentials.
103
+
104
+ The CLI uses the same config for migrations, `tenon generate`, freshness checks, `tenon check`, and
105
+ `tenon describe query.sql`. For multiple databases, add named exports and select one with `--database`.
104
106
  See [shared configuration](docs/configuration.md) for the complete setup.
105
107
 
106
108
  ## Documentation
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { adoptBaseline, captureBaseline } from './baseline.js';
7
7
  import { describeFlags, inspectionSettings } from './inspection/cli.js';
8
8
  import { loadConfig } from './config/load.js';
9
9
  import { initConfig } from './config/init.js';
10
+ import { configConnection } from './config/connection.js';
10
11
  import { generateDefinitions } from './generate.js';
11
12
  import { writeDefinitions } from './generation/output.js';
12
13
  import { validatePath } from './validation.js';
@@ -26,7 +27,8 @@ describe: [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-time
26
27
  describe emits JSON metadata; it does not execute the query or verify result annotations.
27
28
  check compares source contracts; mismatches, unchecked sites and no sites fail. Unproven types are notes.
28
29
  rollback: --steps 1
29
- All commands accept --config path. Migration commands fall back to DATABASE_URL only without config.
30
+ All commands accept --config path. Database commands accept --database <export> to select a named config.
31
+ Multiple database exports require --database. Migration commands fall back to DATABASE_URL only without config.
30
32
  `;
31
33
  function warn(message) {
32
34
  process.stderr.write(`Warning: ${message}\n`);
@@ -59,6 +61,7 @@ async function main() {
59
61
  connection: { type: 'string' },
60
62
  namespace: { type: 'string' },
61
63
  config: { type: 'string' },
64
+ database: { type: 'string' },
62
65
  out: { type: 'string' },
63
66
  check: { type: 'boolean', default: false },
64
67
  schema: { type: 'string', multiple: true },
@@ -94,6 +97,8 @@ async function main() {
94
97
  allowed.add('check');
95
98
  }
96
99
  allowed.add('config');
100
+ if (command !== 'init')
101
+ allowed.add('database');
97
102
  if (command === 'baseline' && argument === 'capture') {
98
103
  allowed.add('schema');
99
104
  allowed.add('connection');
@@ -122,7 +127,7 @@ async function main() {
122
127
  if (positionals.length !== 2) {
123
128
  throw new Error('Use new <name> to create a migration pair.');
124
129
  }
125
- const config = await loadConfig(values.config, false);
130
+ const config = await loadConfig(values.config, values.database, false);
126
131
  const files = await createMigration({
127
132
  name: argument,
128
133
  directory: values.directory ?? config?.migrations?.directory ?? 'migrations',
@@ -141,14 +146,14 @@ async function main() {
141
146
  throw new Error('Choose init, new, baseline capture|adopt, migrate, generate, describe <query.sql>, check [file.ts[:line]], status, or rollback. Use --help for options.');
142
147
  }
143
148
  if (command === 'generate' || command === 'check' || command === 'describe') {
144
- const config = await loadConfig(values.config);
149
+ const config = await loadConfig(values.config, values.database);
145
150
  if (command === 'generate') {
146
151
  const out = values.out === undefined ? config.generate?.out : resolve(values.out);
147
152
  if (out === undefined)
148
153
  throw new Error('Configure generate.out or pass --out.');
149
154
  const namespace = values.namespace ?? config.generate?.namespace ?? config.migrations?.namespace;
150
155
  const source = await generateDefinitions({
151
- connection: config.connection,
156
+ connection: configConnection(config.connection),
152
157
  onWarning: warn,
153
158
  ...(namespace !== undefined && { namespace }),
154
159
  });
@@ -158,7 +163,7 @@ async function main() {
158
163
  }
159
164
  const flags = inspectionSettings(values);
160
165
  const settings = {
161
- connection: config.connection,
166
+ connection: configConnection(config.connection),
162
167
  session: { ...config.session, ...flags.session },
163
168
  timeouts: { ...config.timeouts, ...flags.timeouts },
164
169
  };
@@ -185,11 +190,11 @@ async function main() {
185
190
  process.exitCode = 1;
186
191
  return;
187
192
  }
188
- const config = await loadConfig(values.config, false);
193
+ const config = await loadConfig(values.config, values.database, false);
189
194
  const capture = command === 'baseline' && argument === 'capture';
190
195
  const connection = values.connection ??
191
196
  (capture ? config?.baseline?.connection : undefined) ??
192
- config?.connection ??
197
+ (config === undefined ? undefined : configConnection(config.connection)) ??
193
198
  process.env.DATABASE_URL;
194
199
  if (!connection)
195
200
  throw new Error('DATABASE_URL is required.');
@@ -0,0 +1,3 @@
1
+ import type { ConnectionOptions } from '../connection.js';
2
+ /** Resolve only when a command or application actually needs its configured database. */
3
+ export declare function configConnection(value: unknown): string | ConnectionOptions;
@@ -0,0 +1,22 @@
1
+ import { plainObject } from '../validation.js';
2
+ /** Resolve only when a command or application actually needs its configured database. */
3
+ export function configConnection(value) {
4
+ if (value === undefined) {
5
+ throw new Error('Tenon config connection is required. Check its database environment variables.');
6
+ }
7
+ if (typeof value === 'string') {
8
+ if (!value.trim())
9
+ throw new Error('Tenon config connection string must be nonempty.');
10
+ return value;
11
+ }
12
+ if (value !== null && typeof value === 'object' && ('acquire' in value || 'close' in value)) {
13
+ throw new Error('Tenon config requires connection options, not an open pool.');
14
+ }
15
+ const connection = plainObject(value, 'Tenon config connection');
16
+ // An explicitly configured URI must never silently fall back to the driver's local defaults.
17
+ if ('connectionString' in connection &&
18
+ (typeof connection.connectionString !== 'string' || !connection.connectionString.trim())) {
19
+ throw new Error('Tenon config connectionString must be nonempty. Check its database environment variables.');
20
+ }
21
+ return connection;
22
+ }
@@ -18,11 +18,8 @@ export async function initConfig(filename) {
18
18
  }
19
19
  }
20
20
  const typed = extension === '.ts' || extension === '.mts';
21
- const source = `${typed ? "import type { TenonConfig } from '@kequtech/tenon-orm';\n\n" : ''}const connectionString = process.env.DATABASE_URL;
22
- if (!connectionString) throw new Error('DATABASE_URL is required');
23
-
24
- export default {
25
- connection: { connectionString },
21
+ const source = `${typed ? "import type { TenonConfig } from '@kequtech/tenon-orm';\n\n" : ''}export const main = {
22
+ connection: { connectionString: process.env.DATABASE_URL },
26
23
  migrations: { directory: 'migrations', namespace: 'public' },
27
24
  generate: { out: 'src/generated-schema.d.ts' },
28
25
  check: { project: 'tsconfig.json' },
@@ -3,5 +3,5 @@ export declare const configNames: string[];
3
3
  /** Find configuration without importing it, including for offline initialization. */
4
4
  export declare function findConfig(directory?: string): Promise<string | undefined>;
5
5
  /** Import only the selected trusted configuration module; never discover application entry points. */
6
- export declare function loadConfig(filename?: string): Promise<TenonConfig>;
7
- export declare function loadConfig(filename: string | undefined, required: false): Promise<TenonConfig | undefined>;
6
+ export declare function loadConfig(filename?: string, database?: string): Promise<TenonConfig>;
7
+ export declare function loadConfig(filename: string | undefined, database: string | undefined, required: false): Promise<TenonConfig | undefined>;
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
4
4
  import { sessionOptions, timeoutOptions } from '../inspection/options.js';
5
5
  import { validateName } from '../sql.js';
6
6
  import { plainObject, validatePath } from '../validation.js';
7
+ import { configObject, configSection } from './validate.js';
7
8
  export const configNames = [
8
9
  'tenon.config.ts',
9
10
  'tenon.config.mts',
@@ -20,13 +21,20 @@ async function exists(path) {
20
21
  throw error;
21
22
  }
22
23
  }
23
- function keys(value, label, allowed) {
24
- const config = plainObject(value, label);
25
- for (const key of Object.keys(config)) {
26
- if (!allowed.includes(key))
27
- throw new Error(`Unknown ${label} option: ${key}`);
24
+ function selectDatabase(exports, database) {
25
+ const names = Object.keys(exports).filter((name) => {
26
+ const value = exports[name];
27
+ return (name === 'default' || (value !== null && typeof value === 'object' && 'connection' in value));
28
+ });
29
+ if (database !== undefined && !names.includes(database)) {
30
+ throw new Error(`Unknown database export: ${database}. Available: ${names.join(', ') || 'none'}.`);
28
31
  }
29
- return config;
32
+ if (database === undefined && names.length !== 1) {
33
+ throw new Error(names.length
34
+ ? `Multiple databases exported: ${names.join(', ')}. Select one with --database.`
35
+ : 'No database configurations exported. Export a Tenon config with a connection field.');
36
+ }
37
+ return exports[database ?? names[0]];
30
38
  }
31
39
  /** Find configuration without importing it, including for offline initialization. */
32
40
  export async function findConfig(directory = process.cwd()) {
@@ -47,50 +55,35 @@ export async function findConfig(directory = process.cwd()) {
47
55
  directory = parent;
48
56
  }
49
57
  }
50
- export async function loadConfig(filename, required = true) {
58
+ export async function loadConfig(filename, database, required = true) {
51
59
  if (filename !== undefined)
52
60
  validatePath(filename, 'Config filename');
61
+ if (database !== undefined && (!database.trim() || database.includes('\0'))) {
62
+ throw new Error('--database must be a nonempty export name without NUL.');
63
+ }
53
64
  const selected = filename === undefined ? await findConfig() : resolve(filename);
54
- if (!selected && !required)
65
+ if (!selected && !required && database === undefined)
55
66
  return undefined;
56
67
  if (!selected)
57
68
  throw new Error('Tenon configuration is required. Run tenon init or pass --config.');
58
- let value;
69
+ let exports;
59
70
  try {
60
- value = (await import(pathToFileURL(selected).href)).default;
71
+ exports = await import(pathToFileURL(selected).href);
61
72
  }
62
73
  catch {
63
74
  // Import errors may contain credentials from environment validation or dependency messages.
64
75
  throw new Error(`Could not load Tenon configuration: ${selected}. Check the module and its imports.`);
65
76
  }
66
- const config = keys(value, 'Tenon config', [
67
- 'connection',
68
- 'generate',
69
- 'migrations',
70
- 'baseline',
71
- 'check',
72
- 'session',
73
- 'timeouts',
74
- ]);
75
- if (typeof config.connection === 'string') {
76
- if (!config.connection.trim())
77
- throw new Error('Tenon config connection must be nonempty.');
78
- }
79
- else {
80
- const connection = plainObject(config.connection, 'Tenon config connection');
81
- if ('acquire' in connection || 'close' in connection) {
82
- throw new Error('Tenon config requires connection options, not an open pool.');
83
- }
84
- }
77
+ const config = configObject(selectDatabase(exports, database));
85
78
  const root = dirname(selected);
86
- const migrations = keys(config.migrations === undefined ? {} : config.migrations, 'migration config', ['directory', 'namespace']);
79
+ const migrations = configSection(config.migrations === undefined ? {} : config.migrations, 'migration config', ['directory', 'namespace']);
87
80
  if (migrations.directory !== undefined)
88
81
  validatePath(migrations.directory, 'Migration directory');
89
82
  if (migrations.namespace !== undefined)
90
83
  validateName(migrations.namespace);
91
84
  let generate;
92
85
  if (config.generate !== undefined) {
93
- const options = keys(config.generate, 'generation config', ['out', 'namespace']);
86
+ const options = configSection(config.generate, 'generation config', ['out', 'namespace']);
94
87
  validatePath(options.out, 'Generation out');
95
88
  if (options.namespace !== undefined)
96
89
  validateName(options.namespace);
@@ -99,7 +92,7 @@ export async function loadConfig(filename, required = true) {
99
92
  ...(options.namespace !== undefined && { namespace: options.namespace }),
100
93
  };
101
94
  }
102
- const check = keys(config.check === undefined ? {} : config.check, 'checking config', [
95
+ const check = configSection(config.check === undefined ? {} : config.check, 'checking config', [
103
96
  'project',
104
97
  'readTypes',
105
98
  ]);
@@ -116,10 +109,7 @@ export async function loadConfig(filename, required = true) {
116
109
  }
117
110
  }
118
111
  }
119
- const baseline = keys(config.baseline === undefined ? {} : config.baseline, 'baseline config', [
120
- 'connection',
121
- 'schemas',
122
- ]);
112
+ const baseline = configSection(config.baseline === undefined ? {} : config.baseline, 'baseline config', ['connection', 'schemas']);
123
113
  if (baseline.connection !== undefined &&
124
114
  (typeof baseline.connection !== 'string' ||
125
115
  !baseline.connection.trim() ||
@@ -136,6 +126,9 @@ export async function loadConfig(filename, required = true) {
136
126
  const timeouts = timeoutOptions(config.timeouts === undefined ? {} : config.timeouts);
137
127
  return {
138
128
  connection: config.connection,
129
+ ...(config.onQuery !== undefined && {
130
+ onQuery: config.onQuery,
131
+ }),
139
132
  migrations: {
140
133
  directory: resolve(root, migrations.directory ?? 'migrations'),
141
134
  ...(migrations.namespace !== undefined && { namespace: migrations.namespace }),
@@ -0,0 +1,3 @@
1
+ export declare function configSection(value: unknown, label: string, allowed: readonly string[]): Record<string, unknown>;
2
+ /** Check the shared shape without resolving credentials or constructing a pool. */
3
+ export declare function configObject(value: unknown): Record<string, unknown>;
@@ -0,0 +1,28 @@
1
+ import { plainObject } from '../validation.js';
2
+ export function configSection(value, label, allowed) {
3
+ const config = plainObject(value, label);
4
+ for (const key of Object.keys(config)) {
5
+ if (!allowed.includes(key))
6
+ throw new Error(`Unknown ${label} option: ${key}`);
7
+ }
8
+ return config;
9
+ }
10
+ /** Check the shared shape without resolving credentials or constructing a pool. */
11
+ export function configObject(value) {
12
+ const config = configSection(value, 'Tenon config', [
13
+ 'connection',
14
+ 'onQuery',
15
+ 'generate',
16
+ 'migrations',
17
+ 'baseline',
18
+ 'check',
19
+ 'session',
20
+ 'timeouts',
21
+ ]);
22
+ if (!('connection' in config))
23
+ throw new Error('Tenon config must declare connection.');
24
+ if (config.onQuery !== undefined && typeof config.onQuery !== 'function') {
25
+ throw new Error('Tenon config onQuery must be a function.');
26
+ }
27
+ return config;
28
+ }
package/dist/config.d.ts CHANGED
@@ -1,8 +1,13 @@
1
+ import type { Pool } from '@kequtech/pgclear';
1
2
  import type { ConnectionConfig } from './connection.js';
3
+ import type { QueryEvent } from './types.js';
2
4
  import type { InspectionSession, InspectionTimeouts } from './inspection/types.js';
3
5
  /** Shared runtime connection values and CLI defaults. Export without opening a database. */
4
6
  export interface TenonConfig {
5
- readonly connection: ConnectionConfig;
7
+ /** Environment values may be absent during builds; database use validates them. */
8
+ readonly connection: ConnectionConfig | Pool | undefined;
9
+ /** Observe runtime queries; native driver calls and CLI tooling do not invoke this callback. */
10
+ readonly onQuery?: (event: QueryEvent) => void | Promise<void>;
6
11
  readonly generate?: {
7
12
  readonly out: string;
8
13
  readonly namespace?: string;
@@ -13,7 +18,7 @@ export interface TenonConfig {
13
18
  };
14
19
  /** Native pg_dump connection/scope; defaults to the main connection only when it is a string. */
15
20
  readonly baseline?: {
16
- readonly connection?: string;
21
+ readonly connection?: string | undefined;
17
22
  readonly schemas?: readonly string[];
18
23
  };
19
24
  readonly check?: {
@@ -2,13 +2,16 @@ import { type Pool, type PoolOptions, type QueryResult as DriverResult, type Exe
2
2
  import type { UnknownRow } from './query.js';
3
3
  export type ConnectionOptions = PoolOptions;
4
4
  /** Connection settings and executable credentials/codecs, without a live pool. */
5
- export type ConnectionConfig = string | (ConnectionOptions & {
5
+ export type ConnectionConfig = string | (Omit<ConnectionOptions, 'connectionString'> & {
6
+ /** May be absent during builds; operations reject an explicitly missing URI. */
7
+ readonly connectionString?: string | undefined;
6
8
  readonly acquire?: never;
7
9
  readonly close?: never;
8
10
  });
9
11
  export type Connection = string | ConnectionOptions | Pool;
10
12
  export type QueryResult<Row extends object = UnknownRow> = DriverResult<Row>;
11
13
  export type QueryExecutor = <T>(text: string, input: readonly unknown[], readResult: (result: QueryResult) => T, guarded: boolean) => Promise<T>;
14
+ export declare function isPool(connection: unknown): connection is Pool;
12
15
  /** Constructed pools are owned; supplied public pgclear pools remain application-owned. */
13
16
  export declare function createConnection(connection: Connection): {
14
17
  pool: Pool;
@@ -1,6 +1,6 @@
1
1
  import { createPool, } from '@kequtech/pgclear';
2
2
  import { plainObject } from './validation.js';
3
- function isPool(connection) {
3
+ export function isPool(connection) {
4
4
  return (connection !== null &&
5
5
  typeof connection === 'object' &&
6
6
  'acquire' in connection &&
@@ -1,7 +1,3 @@
1
- import type { Connection } from './connection.js';
2
- import type { Database, QueryEvent } from './types.js';
3
- export interface TenonOptions {
4
- readonly connection: Connection;
5
- readonly onQuery?: (event: QueryEvent) => void | Promise<void>;
6
- }
7
- export declare function createTenon(options: TenonOptions): Database;
1
+ import type { TenonConfig } from './config.js';
2
+ import type { Database } from './types.js';
3
+ export declare function createTenon(config: TenonConfig): Database;
package/dist/database.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createClient } from './client.js';
2
- import { createConnection, executeSql } from './connection.js';
2
+ import { createConnection, executeSql, isPool } from './connection.js';
3
+ import { configConnection } from './config/connection.js';
4
+ import { configObject } from './config/validate.js';
3
5
  import { withTransaction } from './transaction.js';
4
6
  import { plainObject } from './validation.js';
5
7
  function closeTimeout(options) {
@@ -17,34 +19,40 @@ function closeTimeout(options) {
17
19
  }
18
20
  return timeoutMs;
19
21
  }
20
- export function createTenon(options) {
21
- for (const key of Object.keys(plainObject(options, 'Tenon options'))) {
22
- if (key !== 'connection' && key !== 'onQuery') {
23
- throw new Error(`Unknown Tenon option: ${key}`);
24
- }
25
- }
26
- if (options.onQuery !== undefined && typeof options.onQuery !== 'function') {
27
- throw new Error('onQuery must be a function.');
28
- }
29
- const { pool, close } = createConnection(options.connection);
22
+ export function createTenon(config) {
23
+ configObject(config);
24
+ const onQuery = config.onQuery;
25
+ let connection;
30
26
  let closing;
31
27
  function checkOpen() {
32
28
  if (closing) {
33
29
  throw new Error('This database instance is closed.');
34
30
  }
35
31
  }
32
+ function currentConnection() {
33
+ if (!connection) {
34
+ checkOpen();
35
+ const value = config.connection;
36
+ connection = createConnection(isPool(value) ? value : configConnection(value));
37
+ }
38
+ return connection;
39
+ }
36
40
  const query = (text, input, readResult, guarded) => {
37
41
  checkOpen();
42
+ const { pool } = currentConnection();
38
43
  if (guarded)
39
44
  return withTransaction(pool, (query) => query(text, input, readResult, false));
40
45
  return executeSql(pool, text, input).then(readResult);
41
46
  };
42
47
  return {
43
- ...createClient(query, options.onQuery),
44
- native: pool,
48
+ ...createClient(query, onQuery),
49
+ get native() {
50
+ return currentConnection().pool;
51
+ },
45
52
  async transaction(callback) {
46
53
  checkOpen();
47
- return withTransaction(pool, (query) => callback(createClient(query, options.onQuery)));
54
+ const { pool } = currentConnection();
55
+ return withTransaction(pool, (query) => callback(createClient(query, onQuery)));
48
56
  },
49
57
  close(options) {
50
58
  if (closing)
@@ -52,7 +60,7 @@ export function createTenon(options) {
52
60
  try {
53
61
  const timeoutMs = closeTimeout(options);
54
62
  // Cache before invoking driver callbacks, so reentrant/concurrent callers share shutdown.
55
- closing = Promise.resolve().then(() => close(timeoutMs));
63
+ closing = Promise.resolve().then(() => connection?.close(timeoutMs));
56
64
  return closing;
57
65
  }
58
66
  catch (error) {
package/dist/index.d.ts CHANGED
@@ -3,7 +3,6 @@ export type { Codec, CodecRegistry, Parameter, PgArray, TemporalConstructors, Te
3
3
  export type { Connection, ConnectionConfig, ConnectionOptions, QueryResult } from './connection.js';
4
4
  export type { AffectedRowsConstraint } from './affected-rows.js';
5
5
  export { createTenon } from './database.js';
6
- export type { TenonOptions } from './database.js';
7
6
  export type { TenonConfig } from './config.js';
8
7
  export { AffectedRowsError, CardinalityError } from './errors.js';
9
8
  export type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
@@ -1,4 +1,5 @@
1
1
  import { plainObject } from '../validation.js';
2
+ import { configConnection } from '../config/connection.js';
2
3
  const defaults = { connectMs: 3000, statementMs: 5000, lockMs: 1000 };
3
4
  export function sessionOptions(value) {
4
5
  const input = plainObject(value, 'Inspection session');
@@ -31,19 +32,8 @@ export function timeoutOptions(value) {
31
32
  }
32
33
  /** Validate and capture caller-owned settings before opening a connection. */
33
34
  export function inspectionOptions(options) {
34
- let connection;
35
- if (typeof options.connection === 'string') {
36
- if (!options.connection.trim())
37
- throw new Error('Describe requires an explicit nonempty connection string.');
38
- connection = { connectionString: options.connection };
39
- }
40
- else {
41
- plainObject(options.connection, 'Inspection connection');
42
- if ('acquire' in options.connection || 'close' in options.connection) {
43
- throw new Error('Inspection requires connection options, not an open pool.');
44
- }
45
- connection = { ...options.connection };
46
- }
35
+ const selected = configConnection(options.connection);
36
+ let connection = typeof selected === 'string' ? { connectionString: selected } : { ...selected };
47
37
  if (connection.startupParameters !== undefined) {
48
38
  plainObject(connection.startupParameters, 'Startup parameters');
49
39
  }
@@ -25,7 +25,7 @@ export interface InspectionContext {
25
25
  }
26
26
  export interface DescribeOptions {
27
27
  /** URI or pool configuration values. Inspection owns a separate, short-lived connection. */
28
- readonly connection: ConnectionConfig;
28
+ readonly connection: ConnectionConfig | undefined;
29
29
  /** Finished SQL; query values are neither bound nor executed. */
30
30
  readonly query: string | DescribeStatement;
31
31
  /** Use the same registry as execution; custom decoder types can remain unresolved. */
package/dist/types.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface CloseOptions {
17
17
  readonly timeoutMs?: number | undefined;
18
18
  }
19
19
  export type Database = Transaction & {
20
- /** The underlying pool-level driver client; native calls bypass Tenon's query and transaction guards. */
20
+ /** The underlying pool; access initializes shared config. Native calls bypass Tenon's guards. */
21
21
  readonly native: Pool;
22
22
  /** Await all work inside the callback. Commit on success; rollback on an escaping error. */
23
23
  transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
@@ -104,9 +104,8 @@ establishes that contract; do not cast merely to silence a diagnostic. See
104
104
  [responding to findings](query-checking.md#responding-to-findings).
105
105
 
106
106
  Share runtime connection/codecs through `tenon.config.ts`; CLI generation, checking, inspection,
107
- and migrations read it. Use `--config` for another role/deployment. Match runtime role, search path,
108
- timezone, and codecs. Describe uses a separate session and
109
- cannot establish result nullability, JSON members, cardinality, or execution permissions. Native
107
+ and migrations read it. Select a named export with `--database`, or another file with `--config`.
108
+ Match runtime role, search path, timezone, and codecs. Describe uses a separate session and cannot establish result nullability, JSON members, cardinality, or execution permissions. Native
110
109
  calls bypass Tenon's guards/observations and need explicit resource ownership; see
111
110
  [connections and driver access](connections.md). For common failures, use
112
111
  [troubleshooting](reference.md#troubleshooting).
@@ -1,56 +1,120 @@
1
1
  # Shared runtime and CLI configuration
2
2
 
3
- Keep connection settings and codec choices in one module used by the application and Tenon's CLI.
4
- From the project root, create the configuration file:
3
+ A `TenonConfig` describes one database: its connection, codecs, runtime query observer, and CLI
4
+ defaults. The application and CLI use the same object, so generated types and query checks follow
5
+ the representations returned at runtime.
6
+
7
+ Create the configuration from the project root:
5
8
 
6
9
  ```sh
7
10
  npx tenon init
8
11
  ```
9
12
 
10
- Initialization opens no database connection, imports no application code, and never overwrites a
11
- file. Without `--config`, it refuses when a config is already discoverable in this directory or a
12
- parent. Use `tenon init --config path.mjs` for an explicit additional destination or JavaScript output. The generated TypeScript config looks like this:
13
+ The command writes `tenon.config.ts` with a named `main` export:
13
14
 
14
15
  ```ts
15
16
  import type { TenonConfig } from '@kequtech/tenon-orm';
16
17
 
17
- const connectionString = process.env.DATABASE_URL;
18
- if (!connectionString) throw new Error('DATABASE_URL is required');
19
-
20
- export default {
21
- connection: {
22
- connectionString,
23
- },
18
+ export const main = {
19
+ connection: { connectionString: process.env.DATABASE_URL },
24
20
  migrations: { directory: 'migrations', namespace: 'public' },
25
21
  generate: { out: 'src/generated-schema.d.ts' },
26
22
  check: { project: 'tsconfig.json' },
27
23
  } satisfies TenonConfig;
28
24
  ```
29
25
 
30
- Add the application's registry as `connection.codecs` when choosing custom representations.
26
+ Add `connection.codecs` when choosing custom representations. Initialization needs no credentials,
27
+ imports no application code, and never overwrites a file. It refuses to create a default config when
28
+ one is already discoverable in the current directory or a parent. Use `--config path.mjs` for an
29
+ explicit destination or JavaScript output.
31
30
 
32
- `TenonConfig` and `ConnectionConfig` are exported TypeScript types; configuration is a plain
33
- default-exported object. Use that connection when constructing the application instance:
31
+ Import the config into the application and pass it directly to `createTenon`:
34
32
 
35
33
  ```ts
36
34
  // src/db.ts
37
35
  import { createTenon } from '@kequtech/tenon-orm';
38
- import config from '../tenon.config.ts';
36
+ import { main } from '../tenon.config.ts';
39
37
 
40
- export const db = createTenon({ connection: config.connection });
38
+ export const db = createTenon(main);
41
39
  ```
42
40
 
43
- The `.ts` imports above run directly with Node.js 24+ TypeScript support. For typechecking with
44
- `noEmit`, enable `allowImportingTsExtensions`; emitting projects can use
45
- `rewriteRelativeImportExtensions`. Config and its runtime imports must be runnable by Node without
46
- bundler aliases or transformations. ESM JavaScript is also supported.
41
+ ## Initialization and imports
42
+
43
+ Constructing `db` checks the config's shape without validating credentials or creating a pool. The
44
+ first query or transaction initializes one pool, which subsequent and concurrent calls reuse.
45
+ Accessing `db.native` also initializes the pool and validates its settings; the driver connects when
46
+ an operation needs a session. Closing an unused instance creates no pool and needs no credentials.
47
+
48
+ Read environment values in the config without a top-level credential check. This lets builds import
49
+ application modules without database access. An undefined or empty `connection`, or an explicitly
50
+ undefined or empty `connectionString`, fails when the database is used and never selects the driver's
51
+ localhost defaults. Environment values are captured when the module is imported, so supply runtime
52
+ variables before starting the application. Queries performed during a build still need credentials.
47
53
 
48
- Export configuration values, not an open pool, database instance, or factory. Importing the config
49
- must not start the server, open listeners, or depend on generated runtime files. Tenon opens and
50
- closes its own tooling resources. `createTenon` still accepts application-owned pools separately.
54
+ The `.ts` imports above run with Node.js 24+ TypeScript support. For typechecking with `noEmit`, enable
55
+ `allowImportingTsExtensions`; emitting projects can use `rewriteRelativeImportExtensions`. Config
56
+ files and their imports must run in Node without bundler aliases or transformations. ESM JavaScript
57
+ is also supported.
58
+
59
+ Keep config files free of resource initialization: export connection values and codecs, and let Tenon
60
+ open and close its tooling resources. The application reuses its database instance and closes it at
61
+ shutdown. Programmatic configs can also borrow an existing pool; see [pool ownership](connections.md#use-an-application-owned-pool).
62
+
63
+ ## Multiple databases
64
+
65
+ Export one config per database from the same file. For example, add `analytics` beside `main` in
66
+ `tenon.config.ts`:
67
+
68
+ ```ts
69
+ export const analytics = {
70
+ connection: { connectionString: process.env.ANALYTICS_DATABASE_URL },
71
+ migrations: { directory: 'analytics/migrations', namespace: 'public' },
72
+ generate: { out: 'src/analytics-schema.d.ts' },
73
+ check: { project: 'tsconfig.json' },
74
+ } satisfies TenonConfig;
75
+ ```
76
+
77
+ Each export has its own connection settings and codecs. Use the same `connection.codecs` value when
78
+ databases share a registry, and give each database its own migrations and generated output.
79
+ Application code selects the export through an ordinary import:
80
+
81
+ ```ts
82
+ import { createTenon } from '@kequtech/tenon-orm';
83
+ import { main, analytics } from '../tenon.config.ts';
84
+
85
+ export const db = createTenon(main);
86
+ export const analyticsDb = createTenon(analytics);
87
+ ```
88
+
89
+ The CLI selects the corresponding export with `--database`:
90
+
91
+ ```sh
92
+ npx tenon migrate --database main
93
+ npx tenon migrate --database analytics
94
+ npx tenon generate --database analytics
95
+ npx tenon check src/analytics/queries.ts --database analytics
96
+ npx tenon new add_events --database analytics
97
+ ```
98
+
99
+ A file with one config is selected automatically. Multiple database exports require `--database`;
100
+ an unknown name fails without selecting another connection. Named objects with a `connection` field
101
+ are database configs, while other helper exports are ignored. Existing default exports remain
102
+ supported and can be selected explicitly as `--database default`.
103
+
104
+ Only the selected config's settings are validated, although importing the file evaluates the whole
105
+ module. Keep credentials as plain environment reads so one database does not require another's
106
+ credentials. Runtime selection uses the imported object and does not depend on the working directory.
107
+ Close each instance separately. For databases with different schemas, use separate checker projects
108
+ or select the relevant query file when running `tenon check`.
109
+
110
+ `--config` selects a file and `--database` selects an export within it. For example,
111
+ `tenon init --config config/tenon.production.ts` creates another file, and
112
+ `tenon migrate --config config/tenon.production.ts --database main` selects its `main` export.
51
113
 
52
114
  ## Run commands
53
115
 
116
+ With the single config generated by `tenon init`:
117
+
54
118
  ```sh
55
119
  npx tenon new create_users
56
120
  npx tenon migrate
@@ -61,86 +125,87 @@ npx tenon check src/users.ts:23 --json
61
125
  npx tenon describe query.sql
62
126
  ```
63
127
 
64
- `generate`, `check`, and `describe` require configuration. They use its connection and codec registry
65
- and never fall back to `DATABASE_URL` merely because a config is missing or invalid. Your config
66
- chooses which environment variables to read; Tenon does not load `.env` files automatically.
128
+ Generation, checking, and inspection require configuration and use its connection and codecs. A
129
+ missing or invalid config fails instead of falling back to `DATABASE_URL`. The config chooses which
130
+ environment variables to read; Tenon does not load `.env` files.
67
131
 
68
- `new`, migration/status/rollback, and baseline commands also discover configuration. Without a
69
- config, they retain the existing migration-directory defaults and `DATABASE_URL` connection
70
- workflow. `init`, `new`, and `--help` open no database connection. Config imports may still require environment
71
- values, according to the module you wrote. Invalid session/timeout settings are rejected when config
72
- loads, even for commands that do not inspect queries. Migration SQL and history reads use default decoding;
73
- application codec overrides do not change migration bookkeeping.
132
+ Migration creation, migration/status/rollback, and baseline commands also discover configuration.
133
+ Without a config or an explicit `--database`, they retain the migration-directory defaults and
134
+ `DATABASE_URL` workflow. `init`, `new`, and `--help` need no database credentials. `new` reads migration
135
+ settings without validating the connection; `--help` loads no config. Any eager checks in your own
136
+ config module still run when that module is imported.
137
+
138
+ The CLI validates the selected config's tooling sections when loading it, including session and
139
+ timeout settings for commands that do not inspect queries. Migration SQL and history reads use
140
+ default decoding, independently of application codec overrides.
74
141
 
75
142
  ## Discovery, paths, and overrides
76
143
 
77
- Tenon searches the working directory and then parents for the nearest directory containing
78
- `tenon.config.ts`, `.mts`, `.js`, or `.mjs`. Multiple recognized files in that directory are an error.
79
- Select one explicitly when needed:
144
+ Tenon searches the working directory and then its parents for the nearest directory containing
145
+ `tenon.config.ts`, `.mts`, `.js`, or `.mjs`. Multiple recognized files in that directory require an
146
+ explicit selection:
80
147
 
81
148
  ```sh
82
149
  npx tenon generate --config config/tenon.production.mts --check
83
150
  ```
84
151
 
85
- `--config` resolves from the working directory. An explicit missing file or an import failure is an
86
- error; Tenon never switches to another configuration after that failure.
152
+ `--config` resolves from the working directory. A missing file, missing database export, or import
153
+ failure is an error; Tenon does not switch to another configuration after a failure.
87
154
 
88
- Paths stored in config resolve from the config file's directory. Configured migration directories,
89
- generation output, and checker projects therefore work when a command runs from a nested directory.
90
- An omitted migration directory means `migrations` beside the config; an omitted checker project
91
- means `tsconfig.json` beside it. Relative CLI path arguments and flags resolve from the working
92
- directory. CLI flags override the corresponding configured values.
155
+ Configured paths resolve from the config file's directory. An omitted migration directory means
156
+ `migrations` beside the config, and an omitted checker project means `tsconfig.json` beside it. CLI
157
+ path flags resolve from the working directory and override the corresponding configured values.
93
158
 
94
159
  ## Configuration fields
95
160
 
96
- | Field | Purpose |
97
- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
98
- | `connection` | Required PostgreSQL URI or pgclear pool-option values, including TLS, password callbacks, startup parameters, and codecs. Open pools are rejected. |
99
- | `migrations.directory` | Migration files; defaults to `migrations` relative to config. |
100
- | `migrations.namespace` | Migration/history schema; defaults to `public`. Also supplies the generation namespace when generation does not specify one. |
101
- | `generate.out` | Required when a `generate` section is present. Otherwise supply `--out` to the generation command. |
102
- | `generate.namespace` | Schema to generate; overrides the migration namespace for generation only. |
103
- | `baseline.schemas` | Optional nonempty list of schemas for native baseline capture. Omission captures the whole database, independently of the migration namespace. |
104
- | `baseline.connection` | Optional native libpq connection for baseline capture; `--connection` overrides it. |
105
- | `check.project` | TypeScript project; defaults to `tsconfig.json` relative to config. |
106
- | `check.readTypes` | Explicit type expressions for custom decoders whose representations are unidentified. See [checking](query-checking.md#programmatic-use-and-custom-codecs). |
107
- | `session` | Optional inspection/checking `role`, `searchPath`, and `timeZone` overrides. They do not configure runtime queries or migrations. |
108
- | `timeouts` | Optional inspection/checking `connectMs`, `statementMs`, and `lockMs` bounds. |
109
-
110
- Generation and runtime queries use `connection.codecs`. Checking and inspection inherit that registry
111
- too. Their internal catalog queries keep default decoding. Runtime startup parameters are retained,
112
- with inspection's statement/lock timeout bounds applied regardless of startup-parameter casing; explicit `session` settings and CLI flags
113
- then override role, search path, or timezone for inspection. See [connections](connections.md) and
114
- [inspection limits](query-inspection.md#session-and-timeouts).
115
-
116
- Keep credentials and deployment-specific addresses in environment variables or the existing secret
117
- provider. For a migration role different from the application role, select a separate config that
118
- imports shared settings and supplies that role's connection. Avoid duplicating codec definitions.
119
-
120
- Baseline capture uses native `pg_dump`, which accepts a libpq connection string rather than pgclear
121
- option objects or password callbacks. Configure its native connection and scope explicitly when
122
- needed:
161
+ | Field | Purpose |
162
+ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
163
+ | `connection` | PostgreSQL URI or pgclear pool options, including TLS, password callbacks, startup parameters, and codecs. Required when the database is used. |
164
+ | `onQuery` | Optional observer for completed runtime queries. CLI tooling and native calls do not invoke it. |
165
+ | `migrations.directory` | Migration files; defaults to `migrations` beside the config. |
166
+ | `migrations.namespace` | Migration/history schema; defaults to `public`. Also supplies the generation namespace unless overridden. |
167
+ | `generate.out` | Required when `generate` is present. Otherwise supply `--out`. |
168
+ | `generate.namespace` | Schema to generate; overrides the migration namespace for generation. |
169
+ | `baseline.schemas` | Optional nonempty schema list for native baseline capture. Omission captures the whole database. |
170
+ | `baseline.connection` | Optional native libpq connection for baseline capture; `--connection` overrides it. |
171
+ | `check.project` | TypeScript project; defaults to `tsconfig.json` beside the config. |
172
+ | `check.readTypes` | Fallback type expressions for unidentified decoder representations. See [query checking](query-checking.md#programmatic-use-and-custom-codecs). |
173
+ | `session` | Inspection/checking overrides for `role`, `searchPath`, and `timeZone`. |
174
+ | `timeouts` | Inspection/checking bounds for `connectMs`, `statementMs`, and `lockMs`. |
175
+
176
+ Generation, checking, and inspection inherit `connection.codecs`; their internal catalog queries
177
+ keep default decoding. Runtime startup parameters are retained. Inspection applies its statement
178
+ and lock timeout bounds regardless of startup-parameter casing, then applies explicit session and
179
+ CLI overrides. These inspection settings do not change runtime queries or migrations. See
180
+ [connection settings](connections.md) and [inspection limits](query-inspection.md#session-and-timeouts).
181
+
182
+ Keep credentials and deployment addresses in environment variables or the existing secret provider.
183
+ For a separate migration role, add a config export with that role's connection and the shared codecs.
184
+
185
+ ## Native baseline capture
186
+
187
+ Baseline capture uses `pg_dump`, which requires a native libpq connection string. If the main
188
+ connection uses a driver-options object, add the native URI to the config:
123
189
 
124
190
  ```ts
125
- // Additional fields in the shared config:
126
- baseline: { connection: connectionString, schemas: ['public'] }
191
+ // Within the database config:
192
+ baseline: { connection: process.env.DATABASE_URL, schemas: ['public'] }
127
193
  ```
128
194
 
129
195
  Capture uses `--connection`, then `baseline.connection`, then a string main connection or the
130
196
  no-config `DATABASE_URL` fallback. `--schema` flags replace `baseline.schemas`. Adoption ignores
131
- capture-only settings. Supply the same database and required native TLS/authentication settings;
132
- Tenon does not translate driver-specific settings into libpq settings.
197
+ capture-only settings. Supply the same database and its required native TLS/authentication settings;
198
+ Tenon does not translate driver options into libpq settings.
133
199
 
134
200
  ## Programmatic APIs
135
201
 
136
- The CLI owns generated-file output and freshness checks. `generateDefinitions` returns source text
137
- without file handling for callers building custom tooling; it accepts explicit connection/options.
202
+ `createTenon(config)` accepts the shared object and initializes its connection lazily. Other
203
+ programmatic APIs take explicit connection and operation options; none discover config files.
138
204
 
139
- `describeQuery` and `checkQueries` accept a URI or connection-option values and inherit
140
- `connection.codecs`. An explicit `codecs` argument selects the read mappings reported by those
141
- inspection APIs. They own separate, short-lived sessions and do not join callback transactions.
142
- Migration APIs continue accepting explicit connection/options. No programmatic API discovers config.
205
+ The CLI owns generated-file output and freshness checks. `generateDefinitions` returns source text
206
+ for custom tooling without writing files. `describeQuery` and `checkQueries` inherit codecs from
207
+ connection-option values; an explicit `codecs` argument selects their reported read mappings. These
208
+ inspection APIs own separate, short-lived sessions and do not join callback transactions.
143
209
 
144
- The CLI imports the selected trusted config and its dependencies. Source checking subsequently reads
145
- query source without importing those query modules or executing application SQL. TypeScript loads
146
- only for source checking, not for normal queries, migrations, generation, or inspection.
210
+ The CLI imports the selected trusted config and its dependencies. Source checking then reads query
211
+ modules without importing them or executing their SQL. TypeScript loads only for source checking.
@@ -5,32 +5,49 @@ needed only when your application imports native pgclear APIs directly.
5
5
 
6
6
  ## Configure a database instance
7
7
 
8
- ```ts
9
- import { createTenon } from '@kequtech/tenon-orm';
8
+ Define connection settings in the shared config:
10
9
 
11
- const connection = process.env.DATABASE_URL;
12
- if (!connection) throw new Error('DATABASE_URL is required');
10
+ ```ts
11
+ // tenon.config.ts
12
+ import type { TenonConfig } from '@kequtech/tenon-orm';
13
13
 
14
- export const db = createTenon({
14
+ export const main = {
15
15
  connection: {
16
- connectionString: connection,
16
+ connectionString: process.env.DATABASE_URL,
17
17
  maxConnections: 10,
18
18
  connectTimeoutMs: 5000,
19
19
  acquireTimeoutMs: 5000,
20
20
  idleTimeoutMs: 30_000,
21
21
  startupParameters: { search_path: 'app,public' },
22
22
  },
23
- });
23
+ } satisfies TenonConfig;
24
24
  ```
25
25
 
26
- `connection` accepts a PostgreSQL URI, pgclear `PoolOptions`, or an existing pgclear `Pool`.
27
- `ConnectionOptions` is Tenon's alias for `PoolOptions`. Object fields override URI settings.
28
- Reuse an instance for each database configuration.
26
+ Pass the config to the constructor:
29
27
 
30
- The driver does not load environment variables, `.env`, pgpass, or service files.
31
- [Shared configuration](configuration.md) supplies connection values to the CLI and application.
32
- Migration commands retain `DATABASE_URL` fallback when no config exists. An empty options object
33
- uses localhost:5432, the current OS user and a database with that user's name.
28
+ ```ts
29
+ import { createTenon } from '@kequtech/tenon-orm';
30
+ import { main } from '../tenon.config.ts';
31
+
32
+ export const db = createTenon(main);
33
+ ```
34
+
35
+ `createTenon` takes one `TenonConfig` object. Connection validation and pool creation wait until the
36
+ first query, transaction, or `db.native` access. The pool connects when it needs a session. Closing an
37
+ unused instance does not resolve its connection. Subsequent operations reuse the same pool.
38
+
39
+ The config's `connection` field accepts a PostgreSQL URI or pgclear pool-option values.
40
+ `ConnectionOptions` is Tenon's alias for `PoolOptions`. Object fields override URI settings. A
41
+ programmatically constructed config may also supply an existing pool, preserving its ownership.
42
+
43
+ Keep codecs in `connection.codecs` so runtime and tooling use the same representations. See
44
+ [shared configuration](configuration.md) for build-safe environment values and
45
+ [multiple databases](configuration.md#multiple-databases) for named exports and CLI selection.
46
+
47
+ The config supplies environment values to the driver, which does not load `.env`, pgpass, or service
48
+ files. An explicitly missing URI fails when the instance is used. Deliberately configuring
49
+ `connection: {}` uses the driver's local defaults: localhost:5432, the current OS user, and a database
50
+ with that user's name.
34
51
 
35
52
  Use `tls: {}` for TLS with certificate and hostname verification using system trust. Supply the
36
53
  provider's CA or other required TLS settings through pgclear's `tls` options. Preserve the
@@ -68,10 +85,9 @@ without closing the instance.
68
85
  When another part of the application already owns a pgclear pool:
69
86
 
70
87
  ```ts
71
- import { createPool } from '@kequtech/pgclear';
72
88
  import { createTenon } from '@kequtech/tenon-orm';
89
+ import { pool } from './existing-pool.js';
73
90
 
74
- const pool = createPool(connection);
75
91
  const db = createTenon({ connection: pool });
76
92
  // db.native === pool
77
93
 
@@ -84,37 +100,42 @@ outstanding work. Tool APIs that accept `Connection` follow the same ownership r
84
100
 
85
101
  ## Choose output conversions
86
102
 
87
- Outputs follow the pool's codec registry. For example, to read int8 as JavaScript bigint and
88
- `timestamptz` as Date:
103
+ Outputs follow the configured codec registry. To read int8 as JavaScript bigint and `timestamptz` as
104
+ Date, set the registry in `tenon.config.ts`:
89
105
 
90
106
  ```ts
91
- import { builtin, createTenon, defaultCodecs } from '@kequtech/tenon-orm';
107
+ import { builtin, defaultCodecs, type TenonConfig } from '@kequtech/tenon-orm';
92
108
 
93
109
  const codecs = defaultCodecs.with(builtin.int8BigInt).with(builtin.timestamptzDate);
94
- const db = createTenon({ connection: { connectionString: connection, codecs } });
110
+ export const main = {
111
+ connection: { connectionString: process.env.DATABASE_URL, codecs },
112
+ } satisfies TenonConfig;
95
113
  ```
96
114
 
97
115
  These choices apply across the database instance, including callback transactions. Tenon's query
98
116
  options have no per-query codec override. Input encoders remain separate: a `param(value, codec)`
99
117
  carries its own encoder, and passing a Date does not select a Date output decoder.
100
118
 
101
- Place the registry in shared `connection.codecs` for runtime queries and CLI generation, inspection,
102
- and checking. Programmatic
103
- `describeQuery` and `checkQueries` inherit codecs from connection-option values too. See
119
+ The CLI uses that registry for generation, inspection, and checking. Programmatic `describeQuery`
120
+ and `checkQueries` inherit codecs from connection-option values too. See
104
121
  [schema types and values](schema-types.md#custom-codecs) for custom representations and Temporal.
105
122
 
106
123
  ## Observe completed queries
107
124
 
125
+ Set `onQuery` on the database config:
126
+
108
127
  ```ts
109
- const db = createTenon({
110
- connection,
128
+ import type { TenonConfig } from '@kequtech/tenon-orm';
129
+
130
+ export const main = {
131
+ connection: { connectionString: process.env.DATABASE_URL },
111
132
  onQuery(event) {
112
133
  console.log(event.label, event.durationMs, event.outcome, event.rowCount);
113
134
  },
114
- });
135
+ } satisfies TenonConfig;
115
136
  ```
116
137
 
117
- Set `{ label: 'user-by-email' }` on a query to identify it. `onQuery` receives duration, outcome,
138
+ Set `{ label: 'user-by-email' }` on a query to identify it. The observer receives duration, outcome,
118
139
  and available command/count/error-code metadata, with no SQL text or bound values. Validation
119
140
  failures before execution are not observed. Native driver calls and tooling are outside this hook.
120
141
 
@@ -153,7 +174,8 @@ import the native one as `pgSql`.
153
174
 
154
175
  ### Listen for notifications
155
176
 
156
- Import `listen` directly from pgclear for a dedicated connection:
177
+ During application startup, pass a validated PostgreSQL URI (`connection` below) to pgclear's
178
+ `listen` for a dedicated notification connection:
157
179
 
158
180
  ```ts
159
181
  import { listen } from '@kequtech/pgclear';
@@ -34,10 +34,8 @@ The command writes `tenon.config.ts` without opening a database or overwriting e
34
34
  // tenon.config.ts
35
35
  import type { TenonConfig } from '@kequtech/tenon-orm';
36
36
 
37
- const connectionString = process.env.DATABASE_URL;
38
- if (!connectionString) throw new Error('DATABASE_URL is required');
39
- export default {
40
- connection: { connectionString },
37
+ export const main = {
38
+ connection: { connectionString: process.env.DATABASE_URL },
41
39
  migrations: { directory: 'migrations', namespace: 'public' },
42
40
  generate: { out: 'src/generated-schema.d.ts' },
43
41
  check: { project: 'tsconfig.json' },
@@ -49,14 +47,20 @@ Use it in the application too:
49
47
  ```ts
50
48
  // src/db.ts
51
49
  import { createTenon } from '@kequtech/tenon-orm';
52
- import config from '../tenon.config.ts';
50
+ import { main } from '../tenon.config.ts';
53
51
 
54
- export const db = createTenon({ connection: config.connection });
52
+ export const db = createTenon(main);
55
53
  ```
56
54
 
57
55
  These `.ts` imports use Node.js 24+ TypeScript support. See
58
56
  [shared configuration](configuration.md) for import conventions, custom codecs, discovery, and overrides.
59
57
 
58
+ Importing the config and constructing `db` need no database credentials. The first query or transaction
59
+ validates the connection and initializes its pool; `db.native` access also initializes it. Keep
60
+ database operations out of module imports that run during builds. For another database, add a named
61
+ config export in the same file, pass it to `createTenon`, and select that export in CLI commands with
62
+ `--database`.
63
+
60
64
  Reuse `db` across requests. Call `await db.close()` when a standalone script finishes or a server
61
65
  shuts down. See [connections](connections.md) for pool options and application-owned pools.
62
66
 
@@ -9,9 +9,9 @@ compare TypeScript source annotations instead, use [query checking](query-checki
9
9
  ```ts
10
10
  // scripts/inspect-query.ts
11
11
  import { describeQuery, sql } from '@kequtech/tenon-orm';
12
- import config from '../tenon.config.ts';
12
+ import { main } from '../tenon.config.ts';
13
13
 
14
- const connection = config.connection;
14
+ const connection = main.connection;
15
15
 
16
16
  const query = sql<{ id: number; total: string }>`
17
17
  SELECT ${42}::integer AS id, count(*) AS total FROM pg_catalog.pg_class
package/docs/reference.md CHANGED
@@ -10,12 +10,19 @@ Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScr
10
10
  development typings. Install `@kequtech/tenon-orm`; pgclear is included as a runtime dependency.
11
11
 
12
12
  ```text
13
- createTenon({ connection, onQuery? }): Database
13
+ createTenon(config: TenonConfig): Database
14
14
  ```
15
15
 
16
- `connection` is required: a PostgreSQL URI string, pgclear `PoolOptions`, or an existing pgclear
17
- `Pool`. `Connection` names this union; `ConnectionOptions` aliases `PoolOptions`. A supplied pool
18
- retains application ownership. Unknown Tenon or driver configuration options reject.
16
+ Pass one database config directly. Its `connection` holds a PostgreSQL URI, pgclear pool-option
17
+ values, or an application-owned pool. Its optional `onQuery` callback observes runtime queries.
18
+ `ConnectionOptions` aliases pgclear `PoolOptions`; a supplied pool retains application ownership.
19
+ Unknown config keys reject. Connection validation and pool creation wait until the first query,
20
+ transaction, or native-pool access. Closing an unused instance needs no credentials.
21
+
22
+ `tenon init` generates a named `main` config with plain environment reads and no eager credential
23
+ check. Import it and call `createTenon(main)`. Runtime does not discover files. For multiple databases,
24
+ export one config per database and select the matching export in the CLI with `--database`.
25
+ See [shared configuration](configuration.md) for the complete setup.
19
26
 
20
27
  The returned `Database` provides:
21
28
 
@@ -280,21 +287,23 @@ prepared statements, and dedicated notification listeners.
280
287
 
281
288
  ## Troubleshooting
282
289
 
283
- | Symptom | Response |
284
- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
285
- | Missing shared configuration | Run `tenon init` or select an existing config with `--config`. Generation, checking, and inspection require it. |
286
- | `DATABASE_URL is required` | Supply the variable for migration commands without config, or from your shared config module. No `.env` files are loaded automatically. |
287
- | `PGCLEAR_ENCODE` | Check inputs: objects/arrays need `json`/`param`; direct undefined, unsafe integers, nonfinite numbers, and invalid Dates reject. JSON still rejects bigint. |
288
- | `CardinalityError` | Check the expected row count and predicate. To choose one of several rows, author deterministic `ORDER BY ... LIMIT 1`. |
289
- | `TENON_AFFECTED_ROWS_MISMATCH` | The guarded operation rolled back. Inspect `actual` and the predicate; retain the intended bound. |
290
- | `TENON_AFFECTED_ROWS_UNSUPPORTED` | The final command lacks a supported write count. Use guards on INSERT/UPDATE/DELETE/MERGE; result modes check returned rows. |
291
- | Empty assignments | All selected fields were missing/undefined. Skip the update or branch to an expression-only statement. |
292
- | Duplicate result columns | Alias columns uniquely, especially in joins. The driver rejects duplicate object keys. |
293
- | SQLSTATE `42P18` / `42P08` during inspection | Add the intended parameter cast or Describe OID hints. Inspection binds no values. |
294
- | Checker reports `unchecked` | Follow the diagnostic. Runtime fragments and optional patches can execute correctly yet remain unsupported by source recovery. |
295
- | Missing/stale generated output | Check schema, namespace, registry, driver version, and path. Regenerate, review, and commit intended output. |
296
- | Migration history conflict | Restore original applied files. Add new changes in a forward migration. Checksums include down SQL too. |
297
- | Tooling lock wait | Check concurrent migration/DDL work, use direct or session pooling, and configure server timeouts. |
290
+ | Symptom | Response |
291
+ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
292
+ | Missing shared configuration | Run `tenon init` or select an existing config with `--config`. Generation, checking, and inspection require it. |
293
+ | Multiple databases or unknown database export | Select a named config with `--database name`. Use `--config path` when it belongs to another file. |
294
+ | Missing or empty config connection | Set the environment variable read by that database's config before using it. Imports alone need no credentials. |
295
+ | `DATABASE_URL is required` | Supply the variable for migration commands without config. Tenon does not load `.env` files. |
296
+ | `PGCLEAR_ENCODE` | Check inputs: objects/arrays need `json`/`param`; direct undefined, unsafe integers, nonfinite numbers, and invalid Dates reject. JSON still rejects bigint. |
297
+ | `CardinalityError` | Check the expected row count and predicate. To choose one of several rows, author deterministic `ORDER BY ... LIMIT 1`. |
298
+ | `TENON_AFFECTED_ROWS_MISMATCH` | The guarded operation rolled back. Inspect `actual` and the predicate; retain the intended bound. |
299
+ | `TENON_AFFECTED_ROWS_UNSUPPORTED` | The final command lacks a supported write count. Use guards on INSERT/UPDATE/DELETE/MERGE; result modes check returned rows. |
300
+ | Empty assignments | All selected fields were missing/undefined. Skip the update or branch to an expression-only statement. |
301
+ | Duplicate result columns | Alias columns uniquely, especially in joins. The driver rejects duplicate object keys. |
302
+ | SQLSTATE `42P18` / `42P08` during inspection | Add the intended parameter cast or Describe OID hints. Inspection binds no values. |
303
+ | Checker reports `unchecked` | Follow the diagnostic. Runtime fragments and optional patches can execute correctly yet remain unsupported by source recovery. |
304
+ | Missing/stale generated output | Check schema, namespace, registry, driver version, and path. Regenerate, review, and commit intended output. |
305
+ | Migration history conflict | Restore original applied files. Add new changes in a forward migration. Checksums include down SQL too. |
306
+ | Tooling lock wait | Check concurrent migration/DDL work, use direct or session pooling, and configure server timeouts. |
298
307
 
299
308
  After a guarded query rejects, savepoint cleanup has finished. An unguarded server error normally
300
309
  aborts a transaction. A timeout or lost connection does not itself establish whether a commit
@@ -302,12 +311,14 @@ happened; review the transaction contract before adding retries.
302
311
 
303
312
  ## Command reference
304
313
 
305
- All commands accept `--config path`. `generate`, `check`, and `describe` require
314
+ All commands accept `--config path`. Commands using a database config also accept `--database name`
315
+ to select its export. A file with one config is selected automatically; multiple configs require an
316
+ explicit selection. `generate`, `check`, and `describe` require
306
317
  [shared configuration](configuration.md) and use its connection/codecs. Migration and baseline
307
318
  commands use config when present and fall back to `DATABASE_URL` only when no config is found.
308
319
  `init` creates config without importing application code; `new` uses the configured migration
309
- directory without opening a database; `--help` loads no config.
310
- Config modules may read environment variables, but Tenon loads no `.env` files automatically.
320
+ directory without opening a database; `--help` loads no config. Config modules may read environment variables, but Tenon loads no `.env`
321
+ files automatically.
311
322
  Configured paths resolve from the config file; CLI path flags resolve from the working directory.
312
323
  Unsupported command flags reject.
313
324
 
@@ -163,17 +163,22 @@ both become null. For raw JSON text or Date/bigint outputs, configure codecs.
163
163
 
164
164
  ## Custom codecs
165
165
 
166
- Configure the registry on the application's connection. For example:
166
+ Configure the registry in `tenon.config.ts` so runtime queries and generated declarations use the
167
+ same representations:
167
168
 
168
169
  ```ts
169
- import { builtin, createTenon, defaultCodecs } from '@kequtech/tenon-orm';
170
+ import { builtin, defaultCodecs, type TenonConfig } from '@kequtech/tenon-orm';
170
171
 
171
172
  const codecs = defaultCodecs.with(builtin.int8BigInt).with(builtin.timestamptzDate);
172
- const db = createTenon({ connection: { connectionString: connection, codecs } });
173
+ export const main = {
174
+ connection: { connectionString: process.env.DATABASE_URL, codecs },
175
+ generate: { out: 'src/generated-schema.d.ts' },
176
+ } satisfies TenonConfig;
173
177
  ```
174
178
 
175
- Int8 now reads as bigint, and timestamptz reads as a millisecond-precision Date. Regenerate using this
176
- instance. Scalar overrides affect derived built-in arrays; explicit array overrides win. Date and
179
+ Int8 now reads as bigint, and timestamptz reads as a millisecond-precision Date. Run `tenon generate`
180
+ with this config to update declarations. Scalar overrides affect derived built-in arrays; explicit
181
+ array overrides win. Date and
177
182
  Temporal output codecs reject infinity and values outside their supported ranges.
178
183
 
179
184
  For Temporal, import `createTemporalCodecs` from Tenon and supply a native or compatible polyfill
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kequtech/tenon-orm",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "A PostgreSQL ORM designed for coding agents, with explicit SQL, typed queries, and migrations",