@kequtech/tenon-orm 0.4.3 → 0.4.5

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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.5
4
+
5
+ - Preserve generated enum and enum-domain label unions when unrelated codecs change, including
6
+ Date and bigint decoding. Custom enum decoders retain their declared output types; unidentified
7
+ decoders still warn and generate `unknown`. Enum write inputs retain catalog choices.
8
+ - Require pgclear `^0.3.3` for per-type default-decoder metadata and dynamic notification
9
+ subscriptions. Native listeners can add and remove channels on one dedicated connection; see
10
+ [notifications](docs/connections.md#listen-for-notifications) for ownership and recovery.
11
+
12
+ ## 0.4.4
13
+
14
+ - Pass the shared `TenonConfig` directly to `createTenon`, including its connection, codecs, and
15
+ `onQuery` observer. `TenonConfig` replaces the `TenonOptions` type.
16
+ - Defer pool creation and connection validation until the first query, transaction, or native-pool
17
+ access. Importing configuration and closing an unused instance need no credentials. Generated
18
+ configs use plain environment reads; remove eager credential checks from existing configs.
19
+ - Support named config exports for multiple databases and CLI selection with `--database`. A sole
20
+ config is selected automatically; ambiguous or unknown names fail without fallback. `tenon init`
21
+ generates a named `main` export, and existing default exports remain supported.
22
+ - Allow migration-file creation without database credentials while retaining each selected config's
23
+ migration, generation, and checking settings.
24
+
3
25
  ## 0.4.3
4
26
 
5
27
  - 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) {
@@ -6,12 +6,13 @@ function columnTypes(column, codecs, path, warn) {
6
6
  warn(`Column ${path} uses a custom decoder without a known TypeScript representation; generated as unknown.`);
7
7
  read = 'unknown';
8
8
  }
9
- const choices = column.choices?.map((choice) => JSON.stringify(choice)).join(' | ') || 'never';
10
- if (column.typeKind === 'e' && codecs === defaultCodecs && read === 'string')
11
- read = choices;
12
9
  let write = defaultCodecs.typescript(oid);
13
- if (column.typeKind === 'e')
10
+ if (column.typeKind === 'e') {
11
+ const choices = column.choices?.map((choice) => JSON.stringify(choice)).join(' | ') || 'never';
12
+ if (read === 'string' && codecs.isDefaultDecoder(oid))
13
+ read = choices;
14
14
  write = choices;
15
+ }
15
16
  if (write === 'unknown' || write.startsWith('PgArray<'))
16
17
  write = 'string';
17
18
  if ([20, 1700].includes(oid))
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).