@kequtech/tenon-orm 0.4.2 → 0.4.3

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.3
4
+
5
+ - Add shared runtime/CLI configuration and offline `tenon init`. Restore `tenon generate`, share migration defaults, and require config for generation/checking/inspection.
6
+ - Remove `db.generateTypes()`. Add native baseline connection/schema defaults and fix inspection timeout overrides and migration-history lookup under custom search paths.
7
+
3
8
  ## 0.4.2
4
9
 
5
10
  - Rewrite the README and consumer guides around a complete getting-started workflow and task-based navigation.
package/README.md CHANGED
@@ -74,7 +74,7 @@ version. For work in an existing project, start with the [agent guide](docs/agen
74
74
  - **SQL defines the operation.** Use PostgreSQL joins, aggregates, constraints, and upserts directly.
75
75
  Reuse queries with ordinary functions; use fragments when parts of the SQL need to vary.
76
76
  - **TypeScript describes the values.** Declare query projections with `sql<Row>`. Optionally generate
77
- table read/insert/update types with `db.generateTypes({ out: 'src/generated-schema.d.ts' })`.
77
+ table read/insert/update types with `tenon generate`.
78
78
  - **Transactions define what commits together.** Use `db.transaction(async (tx) => { ... })` and run
79
79
  every participating query through `tx`.
80
80
  - **Write guards check affected rows.** `{ affectedRows: { exact: 1 } }` rolls back a write unless
@@ -95,6 +95,14 @@ temporal values also default to strings, and JSON/array inputs use explicit enco
95
95
  `rowCount` is a number or null. See [value mappings](docs/schema-types.md#value-mappings) before
96
96
  choosing application types.
97
97
 
98
+ ## Share configuration with the CLI
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.
104
+ See [shared configuration](docs/configuration.md) for the complete setup.
105
+
98
106
  ## Documentation
99
107
 
100
108
  Start with [Getting started](docs/human-guide.md) for a complete table-to-query example. Then choose
@@ -102,6 +110,7 @@ the guide for your task:
102
110
 
103
111
  | Guide | What it covers |
104
112
  | ---------------------------------------------------------- | ---------------------------------------------------------------------------- |
113
+ | [Shared configuration](docs/configuration.md) | Runtime/CLI connection settings, codecs, command defaults, and overrides |
105
114
  | [Writing queries](docs/sql.md) | Parameters, reusable functions, joins, aggregates, pagination, and fragments |
106
115
  | [Writing data](docs/mutations.md) | Inserts, patches, defaults, upserts, JSON, arrays, and batches |
107
116
  | [Schema types and values](docs/schema-types.md) | Generation, read/write types, codecs, and domain validation |
package/dist/check.js CHANGED
@@ -17,7 +17,10 @@ export async function checkQueries(options) {
17
17
  if (!['connection', 'target', 'project', 'session', 'timeouts', 'codecs', 'readTypes'].includes(name))
18
18
  throw new Error(`Unknown check option: ${name}`);
19
19
  }
20
- const project = loadProject(options);
20
+ const codecs = options.codecs ??
21
+ (typeof options.connection === 'object' ? options.connection?.codecs : undefined) ??
22
+ defaultCodecs;
23
+ const project = loadProject({ ...options, codecs });
21
24
  const libraries = librarySymbols(project);
22
25
  const diagnostics = [...project.diagnostics];
23
26
  const queries = project.selected.flatMap((source) => new SourceAnalysis(project, libraries).discover(source));
@@ -52,7 +55,6 @@ export async function checkQueries(options) {
52
55
  return { query, variants, diagnostics: analysis.issues };
53
56
  });
54
57
  const reports = [];
55
- const codecs = options.codecs ?? defaultCodecs;
56
58
  const readTypes = { ...options.readTypes };
57
59
  for (const oid of Object.keys(readTypes)) {
58
60
  if (codecs.typescript(Number(oid)) !== undefined)
package/dist/cli.js CHANGED
@@ -1,20 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import { defaultCodecs } from '@kequtech/pgclear';
3
6
  import { adoptBaseline, captureBaseline } from './baseline.js';
4
- import { describeFlags, inspectionSettings, runDescribe } from './inspection/cli.js';
7
+ import { describeFlags, inspectionSettings } from './inspection/cli.js';
8
+ import { loadConfig } from './config/load.js';
9
+ import { initConfig } from './config/init.js';
10
+ import { generateDefinitions } from './generate.js';
11
+ import { writeDefinitions } from './generation/output.js';
12
+ import { validatePath } from './validation.js';
13
+ import { describeQuery } from './describe.js';
5
14
  import { migrate, migrationStatus, rollback } from './migrations.js';
6
15
  import { createMigration } from './migrations/files.js';
7
- const help = `tenon migrate|status|rollback [--directory migrations] [--namespace public]
16
+ const help = `tenon init [--config path] (create config without a database)
17
+ tenon migrate|status|rollback [--directory migrations] [--namespace public]
8
18
  tenon baseline capture|adopt [--directory migrations] [--namespace public]
9
- baseline capture: [--schema name ...] (default: whole database schema)
19
+ baseline capture: [--schema name ...] [--connection native-uri] (default: whole database schema)
10
20
  tenon new <name> [--directory migrations] (no database required)
21
+ tenon generate [--out path] [--namespace public] [--check] [--config path]
11
22
  tenon describe <query.sql> [--role name] [--search-path schemas] [--time-zone zone]
12
23
  tenon check [file.ts[:line]] [--project tsconfig.json] [--json]
24
+ generate/check/describe require tenon.config.ts, .mts, .js or .mjs, or --config path.
13
25
  describe: [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
14
26
  describe emits JSON metadata; it does not execute the query or verify result annotations.
15
27
  check compares source contracts; mismatches, unchecked sites and no sites fail. Unproven types are notes.
16
28
  rollback: --steps 1
17
- Connection: DATABASE_URL
29
+ All commands accept --config path. Migration commands fall back to DATABASE_URL only without config.
18
30
  `;
19
31
  function warn(message) {
20
32
  process.stderr.write(`Warning: ${message}\n`);
@@ -22,7 +34,14 @@ function warn(message) {
22
34
  async function runBaseline(action, options) {
23
35
  warn("Capture one shared baseline at the old ORM's final schema checkpoint. Each existing database must be at that checkpoint before adoption. Tenon does not compare schemas or remove the old ORM.");
24
36
  if (action === 'capture') {
25
- const result = await captureBaseline({ ...options, onWarning: warn });
37
+ if (typeof options.connection !== 'string') {
38
+ throw new Error('Baseline capture needs a native libpq URI. Pass --connection or use a string config connection.');
39
+ }
40
+ const result = await captureBaseline({
41
+ ...options,
42
+ connection: options.connection,
43
+ onWarning: warn,
44
+ });
26
45
  process.stdout.write(`Captured ${result.upPath}. Source history and data were not changed. Commit this file and generated definitions.\n`);
27
46
  }
28
47
  else {
@@ -36,8 +55,12 @@ async function main() {
36
55
  allowPositionals: true,
37
56
  tokens: true,
38
57
  options: {
39
- directory: { type: 'string', default: 'migrations' },
40
- namespace: { type: 'string', default: 'public' },
58
+ directory: { type: 'string' },
59
+ connection: { type: 'string' },
60
+ namespace: { type: 'string' },
61
+ config: { type: 'string' },
62
+ out: { type: 'string' },
63
+ check: { type: 'boolean', default: false },
41
64
  schema: { type: 'string', multiple: true },
42
65
  steps: { type: 'string', default: '1' },
43
66
  help: { type: 'boolean', default: false },
@@ -51,22 +74,29 @@ async function main() {
51
74
  return;
52
75
  }
53
76
  const [command, argument] = positionals;
54
- if (command === 'generate') {
55
- throw new Error('The generate command has been removed. Use db.generateTypes({ out, namespace?, check? }) in an application script so generation uses your configured codecs.');
77
+ for (const name of ['out', 'directory', 'project']) {
78
+ if (values[name] !== undefined)
79
+ validatePath(values[name], `--${name}`);
56
80
  }
57
81
  // Validate explicitly supplied flags, not defaults. Silently ignoring --check
58
82
  // on migrate, for example, would turn an intended dry run into a schema change.
59
83
  const allowed = new Set(['help']);
60
- if (command !== 'describe' && command !== 'check')
84
+ if (!['describe', 'check', 'generate', 'init'].includes(command ?? ''))
61
85
  allowed.add('directory');
62
- if (command !== 'new' && command !== 'describe' && command !== 'check') {
86
+ if (!['new', 'describe', 'check', 'init'].includes(command ?? '')) {
63
87
  allowed.add('namespace');
64
88
  }
65
89
  if (command === 'rollback') {
66
90
  allowed.add('steps');
67
91
  }
92
+ if (command === 'generate') {
93
+ allowed.add('out');
94
+ allowed.add('check');
95
+ }
96
+ allowed.add('config');
68
97
  if (command === 'baseline' && argument === 'capture') {
69
98
  allowed.add('schema');
99
+ allowed.add('connection');
70
100
  }
71
101
  if (command === 'check') {
72
102
  allowed.add('project');
@@ -81,11 +111,22 @@ async function main() {
81
111
  throw new Error(`--${token.name} is not supported by ${command ?? 'this command'}.`);
82
112
  }
83
113
  }
114
+ if (command === 'init') {
115
+ if (positionals.length !== 1)
116
+ throw new Error('Use init [--config path] to create configuration.');
117
+ const path = await initConfig(values.config);
118
+ process.stdout.write(`Created ${path}\nSet DATABASE_URL before running database commands.\n`);
119
+ return;
120
+ }
84
121
  if (command === 'new') {
85
122
  if (positionals.length !== 2) {
86
123
  throw new Error('Use new <name> to create a migration pair.');
87
124
  }
88
- const files = await createMigration({ name: argument, directory: values.directory });
125
+ const config = await loadConfig(values.config, false);
126
+ const files = await createMigration({
127
+ name: argument,
128
+ directory: values.directory ?? config?.migrations?.directory ?? 'migrations',
129
+ });
89
130
  process.stdout.write(`Created ${files.upPath}\nCreated ${files.downPath}\n`);
90
131
  return;
91
132
  }
@@ -94,25 +135,44 @@ async function main() {
94
135
  ? positionals.length <= 2
95
136
  : baseline || command === 'describe'
96
137
  ? positionals.length === 2
97
- : positionals.length === 1 && ['migrate', 'status', 'rollback'].includes(command ?? '');
138
+ : positionals.length === 1 &&
139
+ ['migrate', 'generate', 'status', 'rollback'].includes(command ?? '');
98
140
  if (!valid) {
99
- throw new Error('Choose new, baseline capture|adopt, migrate, describe <query.sql>, check [file.ts[:line]], status, or rollback. Use --help for options.');
100
- }
101
- const connection = process.env.DATABASE_URL;
102
- if (!connection) {
103
- throw new Error('DATABASE_URL is required.');
104
- }
105
- if (command === 'describe') {
106
- await runDescribe(argument, connection, values);
107
- return;
108
- }
109
- if (command === 'check') {
141
+ 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
+ }
143
+ if (command === 'generate' || command === 'check' || command === 'describe') {
144
+ const config = await loadConfig(values.config);
145
+ if (command === 'generate') {
146
+ const out = values.out === undefined ? config.generate?.out : resolve(values.out);
147
+ if (out === undefined)
148
+ throw new Error('Configure generate.out or pass --out.');
149
+ const namespace = values.namespace ?? config.generate?.namespace ?? config.migrations?.namespace;
150
+ const source = await generateDefinitions({
151
+ connection: config.connection,
152
+ onWarning: warn,
153
+ ...(namespace !== undefined && { namespace }),
154
+ });
155
+ await writeDefinitions(source, out, values.check);
156
+ process.stdout.write(values.check ? 'Generated definitions are current.\n' : `Generated ${out}\n`);
157
+ return;
158
+ }
159
+ const flags = inspectionSettings(values);
160
+ const settings = {
161
+ connection: config.connection,
162
+ session: { ...config.session, ...flags.session },
163
+ timeouts: { ...config.timeouts, ...flags.timeouts },
164
+ };
165
+ if (command === 'describe') {
166
+ const result = await describeQuery({ ...settings, query: await readFile(argument, 'utf8') });
167
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
168
+ return;
169
+ }
110
170
  const { checkQueries } = await import('./check.js');
111
171
  const report = await checkQueries({
112
- connection,
172
+ ...settings,
173
+ ...config.check,
113
174
  ...(argument === undefined ? {} : { target: argument }),
114
175
  ...(values.project === undefined ? {} : { project: values.project }),
115
- ...inspectionSettings(values),
116
176
  });
117
177
  if (values.json)
118
178
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
@@ -125,11 +185,24 @@ async function main() {
125
185
  process.exitCode = 1;
126
186
  return;
127
187
  }
128
- const options = { connection, directory: values.directory, namespace: values.namespace };
188
+ const config = await loadConfig(values.config, false);
189
+ const capture = command === 'baseline' && argument === 'capture';
190
+ const connection = values.connection ??
191
+ (capture ? config?.baseline?.connection : undefined) ??
192
+ config?.connection ??
193
+ process.env.DATABASE_URL;
194
+ if (!connection)
195
+ throw new Error('DATABASE_URL is required.');
196
+ const options = {
197
+ connection: typeof connection === 'string' ? connection : { ...connection, codecs: defaultCodecs },
198
+ directory: values.directory ?? config?.migrations?.directory ?? 'migrations',
199
+ namespace: values.namespace ?? config?.migrations?.namespace ?? 'public',
200
+ };
129
201
  if (baseline) {
202
+ const schemas = values.schema ?? (capture ? config?.baseline?.schemas : undefined);
130
203
  await runBaseline(argument, {
131
204
  ...options,
132
- ...(values.schema ? { schemas: values.schema } : {}),
205
+ ...(schemas !== undefined && { schemas }),
133
206
  });
134
207
  return;
135
208
  }
@@ -0,0 +1,2 @@
1
+ /** Scaffold one value-only config, without importing existing application code or opening a pool. */
2
+ export declare function initConfig(filename?: string): Promise<string>;
@@ -0,0 +1,34 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { basename, dirname, extname, resolve } from 'node:path';
3
+ import { validatePath } from '../validation.js';
4
+ import { configNames, findConfig } from './load.js';
5
+ /** Scaffold one value-only config, without importing existing application code or opening a pool. */
6
+ export async function initConfig(filename) {
7
+ if (filename !== undefined)
8
+ validatePath(filename, 'Config filename');
9
+ const output = resolve(filename ?? 'tenon.config.ts');
10
+ const extension = extname(output);
11
+ if (!['.ts', '.mts', '.js', '.mjs'].includes(extension)) {
12
+ throw new Error('Config filename must end in .ts, .mts, .js, or .mjs.');
13
+ }
14
+ if (filename === undefined || configNames.includes(basename(output))) {
15
+ const existing = await findConfig(dirname(output));
16
+ if (existing && (filename === undefined || dirname(existing) === dirname(output))) {
17
+ throw new Error(`Tenon configuration already exists: ${existing}. Use or edit that file.`);
18
+ }
19
+ }
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 },
26
+ migrations: { directory: 'migrations', namespace: 'public' },
27
+ generate: { out: 'src/generated-schema.d.ts' },
28
+ check: { project: 'tsconfig.json' },
29
+ }${typed ? ' satisfies TenonConfig' : ''};
30
+ `;
31
+ await mkdir(dirname(output), { recursive: true });
32
+ await writeFile(output, source, { encoding: 'utf8', flag: 'wx' });
33
+ return output;
34
+ }
@@ -0,0 +1,7 @@
1
+ import type { TenonConfig } from '../config.js';
2
+ export declare const configNames: string[];
3
+ /** Find configuration without importing it, including for offline initialization. */
4
+ export declare function findConfig(directory?: string): Promise<string | undefined>;
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>;
@@ -0,0 +1,159 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { sessionOptions, timeoutOptions } from '../inspection/options.js';
5
+ import { validateName } from '../sql.js';
6
+ import { plainObject, validatePath } from '../validation.js';
7
+ export const configNames = [
8
+ 'tenon.config.ts',
9
+ 'tenon.config.mts',
10
+ 'tenon.config.js',
11
+ 'tenon.config.mjs',
12
+ ];
13
+ async function exists(path) {
14
+ try {
15
+ return (await stat(path)).isFile();
16
+ }
17
+ catch (error) {
18
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
19
+ return false;
20
+ throw error;
21
+ }
22
+ }
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}`);
28
+ }
29
+ return config;
30
+ }
31
+ /** Find configuration without importing it, including for offline initialization. */
32
+ export async function findConfig(directory = process.cwd()) {
33
+ while (true) {
34
+ const candidates = await Promise.all(configNames.map(async (name) => {
35
+ const candidate = join(directory, name);
36
+ return (await exists(candidate)) ? candidate : undefined;
37
+ }));
38
+ const found = candidates.filter((candidate) => candidate !== undefined);
39
+ if (found.length > 1)
40
+ throw new Error(`Multiple Tenon config files in ${directory}; select one with --config.`);
41
+ if (found.length) {
42
+ return found[0];
43
+ }
44
+ const parent = dirname(directory);
45
+ if (parent === directory)
46
+ return undefined;
47
+ directory = parent;
48
+ }
49
+ }
50
+ export async function loadConfig(filename, required = true) {
51
+ if (filename !== undefined)
52
+ validatePath(filename, 'Config filename');
53
+ const selected = filename === undefined ? await findConfig() : resolve(filename);
54
+ if (!selected && !required)
55
+ return undefined;
56
+ if (!selected)
57
+ throw new Error('Tenon configuration is required. Run tenon init or pass --config.');
58
+ let value;
59
+ try {
60
+ value = (await import(pathToFileURL(selected).href)).default;
61
+ }
62
+ catch {
63
+ // Import errors may contain credentials from environment validation or dependency messages.
64
+ throw new Error(`Could not load Tenon configuration: ${selected}. Check the module and its imports.`);
65
+ }
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
+ }
85
+ const root = dirname(selected);
86
+ const migrations = keys(config.migrations === undefined ? {} : config.migrations, 'migration config', ['directory', 'namespace']);
87
+ if (migrations.directory !== undefined)
88
+ validatePath(migrations.directory, 'Migration directory');
89
+ if (migrations.namespace !== undefined)
90
+ validateName(migrations.namespace);
91
+ let generate;
92
+ if (config.generate !== undefined) {
93
+ const options = keys(config.generate, 'generation config', ['out', 'namespace']);
94
+ validatePath(options.out, 'Generation out');
95
+ if (options.namespace !== undefined)
96
+ validateName(options.namespace);
97
+ generate = {
98
+ out: resolve(root, options.out),
99
+ ...(options.namespace !== undefined && { namespace: options.namespace }),
100
+ };
101
+ }
102
+ const check = keys(config.check === undefined ? {} : config.check, 'checking config', [
103
+ 'project',
104
+ 'readTypes',
105
+ ]);
106
+ if (check.project !== undefined)
107
+ validatePath(check.project, 'Checking project');
108
+ if (check.readTypes !== undefined) {
109
+ const mappings = plainObject(check.readTypes, 'Checking readTypes');
110
+ for (const [oid, expression] of Object.entries(mappings)) {
111
+ if (!/^[1-9]\d*$/.test(oid) ||
112
+ Number(oid) > 0xffffffff ||
113
+ typeof expression !== 'string' ||
114
+ !expression.trim()) {
115
+ throw new Error('Checking readTypes requires PostgreSQL OIDs and nonempty type expressions.');
116
+ }
117
+ }
118
+ }
119
+ const baseline = keys(config.baseline === undefined ? {} : config.baseline, 'baseline config', [
120
+ 'connection',
121
+ 'schemas',
122
+ ]);
123
+ if (baseline.connection !== undefined &&
124
+ (typeof baseline.connection !== 'string' ||
125
+ !baseline.connection.trim() ||
126
+ baseline.connection.includes('\0'))) {
127
+ throw new Error('Baseline connection must be a nonempty native libpq connection string.');
128
+ }
129
+ if (baseline.schemas !== undefined) {
130
+ if (!Array.isArray(baseline.schemas) || !baseline.schemas.length)
131
+ throw new Error('Baseline schemas must be a nonempty array.');
132
+ for (const schema of baseline.schemas)
133
+ validateName(schema);
134
+ }
135
+ const session = sessionOptions(config.session === undefined ? {} : config.session);
136
+ const timeouts = timeoutOptions(config.timeouts === undefined ? {} : config.timeouts);
137
+ return {
138
+ connection: config.connection,
139
+ migrations: {
140
+ directory: resolve(root, migrations.directory ?? 'migrations'),
141
+ ...(migrations.namespace !== undefined && { namespace: migrations.namespace }),
142
+ },
143
+ ...(generate !== undefined && { generate }),
144
+ check: {
145
+ project: resolve(root, check.project ?? 'tsconfig.json'),
146
+ ...(check.readTypes !== undefined && {
147
+ readTypes: check.readTypes,
148
+ }),
149
+ },
150
+ ...(config.baseline !== undefined && {
151
+ baseline: {
152
+ ...(baseline.connection !== undefined && { connection: baseline.connection }),
153
+ ...(baseline.schemas !== undefined && { schemas: [...baseline.schemas] }),
154
+ },
155
+ }),
156
+ session,
157
+ timeouts,
158
+ };
159
+ }
@@ -0,0 +1,25 @@
1
+ import type { ConnectionConfig } from './connection.js';
2
+ import type { InspectionSession, InspectionTimeouts } from './inspection/types.js';
3
+ /** Shared runtime connection values and CLI defaults. Export without opening a database. */
4
+ export interface TenonConfig {
5
+ readonly connection: ConnectionConfig;
6
+ readonly generate?: {
7
+ readonly out: string;
8
+ readonly namespace?: string;
9
+ };
10
+ readonly migrations?: {
11
+ readonly directory?: string;
12
+ readonly namespace?: string;
13
+ };
14
+ /** Native pg_dump connection/scope; defaults to the main connection only when it is a string. */
15
+ readonly baseline?: {
16
+ readonly connection?: string;
17
+ readonly schemas?: readonly string[];
18
+ };
19
+ readonly check?: {
20
+ readonly project?: string;
21
+ readonly readTypes?: Readonly<Record<number, string>>;
22
+ };
23
+ readonly session?: InspectionSession;
24
+ readonly timeouts?: InspectionTimeouts;
25
+ }
package/dist/config.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,11 @@
1
1
  import { type Pool, type PoolOptions, type QueryResult as DriverResult, type Executor } from '@kequtech/pgclear';
2
2
  import type { UnknownRow } from './query.js';
3
3
  export type ConnectionOptions = PoolOptions;
4
+ /** Connection settings and executable credentials/codecs, without a live pool. */
5
+ export type ConnectionConfig = string | (ConnectionOptions & {
6
+ readonly acquire?: never;
7
+ readonly close?: never;
8
+ });
4
9
  export type Connection = string | ConnectionOptions | Pool;
5
10
  export type QueryResult<Row extends object = UnknownRow> = DriverResult<Row>;
6
11
  export type QueryExecutor = <T>(text: string, input: readonly unknown[], readResult: (result: QueryResult) => T, guarded: boolean) => Promise<T>;
package/dist/database.js CHANGED
@@ -2,7 +2,6 @@ import { createClient } from './client.js';
2
2
  import { createConnection, executeSql } from './connection.js';
3
3
  import { withTransaction } from './transaction.js';
4
4
  import { plainObject } from './validation.js';
5
- import { generateTypes } from './generate.js';
6
5
  function closeTimeout(options) {
7
6
  const settings = plainObject(options === undefined ? {} : options, 'Close options');
8
7
  for (const key of Object.keys(settings)) {
@@ -43,10 +42,6 @@ export function createTenon(options) {
43
42
  return {
44
43
  ...createClient(query, options.onQuery),
45
44
  native: pool,
46
- async generateTypes(options) {
47
- checkOpen();
48
- await generateTypes(pool, options);
49
- },
50
45
  async transaction(callback) {
51
46
  checkOpen();
52
47
  return withTransaction(pool, (query) => callback(createClient(query, options.onQuery)));
package/dist/describe.js CHANGED
@@ -23,6 +23,8 @@ export async function describeQuery(options) {
23
23
  ? { parameterTypes: [...options.query.parameterTypes] }
24
24
  : {}),
25
25
  };
26
- const codecs = options.codecs ?? defaultCodecs;
26
+ const codecs = options.codecs ??
27
+ (typeof options.connection === 'object' ? options.connection?.codecs : undefined) ??
28
+ defaultCodecs;
27
29
  return withInspectionConnection(options, async (client, context) => queryDescription(await readStatementMetadata(client, query), context, codecs));
28
30
  }
@@ -4,16 +4,5 @@ export interface GenerateOptions {
4
4
  readonly namespace?: string;
5
5
  readonly onWarning?: (message: string) => void | Promise<void>;
6
6
  }
7
- export interface GenerateTypesOptions {
8
- /** Output path, resolved from the working directory. */
9
- readonly out: string;
10
- /** Schema to inspect; defaults to public. */
11
- readonly namespace?: string;
12
- /** Reject missing or stale output without writing. Defaults to false. */
13
- readonly check?: boolean;
14
- readonly onWarning?: (message: string) => void | Promise<void>;
15
- }
16
- /** Generate with the owning database's connection and preserve exact file freshness checks. */
17
- export declare function generateTypes(connection: Connection, options: GenerateTypesOptions): Promise<void>;
18
7
  /** Read one schema snapshot with default catalog codecs and the selected application read mappings. */
19
8
  export declare function generateDefinitions(options: GenerateOptions): Promise<string>;
package/dist/generate.js CHANGED
@@ -4,32 +4,7 @@ import { renderDatabase } from './generation/render.js';
4
4
  import { domainSummaries, schemaSummary, tableSummary } from './generation/schema-summary.js';
5
5
  import { withMigrationLock } from './migrations/connection.js';
6
6
  import { migrationTable } from './migrations/history.js';
7
- import { quote, validateName } from './sql.js';
8
- import { plainObject } from './validation.js';
9
- import { writeDefinitions } from './generation/output.js';
10
- /** Generate with the owning database's connection and preserve exact file freshness checks. */
11
- export async function generateTypes(connection, options) {
12
- const config = plainObject(options, 'Generation options');
13
- for (const key of Object.keys(config)) {
14
- if (!['out', 'namespace', 'check', 'onWarning'].includes(key)) {
15
- throw new Error(`Unknown generation option: ${key}`);
16
- }
17
- }
18
- if (typeof config.out !== 'string' || !config.out.trim() || config.out.includes('\0')) {
19
- throw new Error('Generation out must be a nonempty file path.');
20
- }
21
- if (config.check !== undefined && typeof config.check !== 'boolean') {
22
- throw new Error('Generation check must be a boolean.');
23
- }
24
- if (options.namespace !== undefined)
25
- validateName(options.namespace);
26
- const source = await generateDefinitions({
27
- connection,
28
- ...(options.namespace !== undefined && { namespace: options.namespace }),
29
- ...(options.onWarning !== undefined && { onWarning: options.onWarning }),
30
- });
31
- await writeDefinitions(source, config.out, config.check === true);
32
- }
7
+ import { quote } from './sql.js';
33
8
  /** Read one schema snapshot with default catalog codecs and the selected application read mappings. */
34
9
  export async function generateDefinitions(options) {
35
10
  if (options.onWarning !== undefined && typeof options.onWarning !== 'function') {
@@ -14,10 +14,10 @@ export async function writeDefinitions(source, filename, check) {
14
14
  if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
15
15
  throw error;
16
16
  }
17
- throw new Error(`Generated definitions file is missing: ${output}\nRun db.generateTypes with the same options and check: false, then commit the result.`);
17
+ throw new Error(`Generated definitions file is missing: ${output}\nRun tenon generate with the same options, omitting --check, then commit the result.`);
18
18
  }
19
19
  if (existing !== source) {
20
- throw new Error(`Generated definitions differ: ${output}\n${definitionDiff(existing, source)}\nRun db.generateTypes with the same options and check: false, then commit the result.`);
20
+ throw new Error(`Generated definitions differ: ${output}\n${definitionDiff(existing, source)}\nRun tenon generate with the same options, omitting --check, then commit the result.`);
21
21
  }
22
22
  return;
23
23
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export { builtin, createTemporalCodecs, defaultCodecs, json, param } from '@kequtech/pgclear';
2
2
  export type { Codec, CodecRegistry, Parameter, PgArray, TemporalConstructors, TemporalInput, } from '@kequtech/pgclear';
3
- export type { Connection, ConnectionOptions, QueryResult } from './connection.js';
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
6
  export type { TenonOptions } from './database.js';
7
+ export type { TenonConfig } from './config.js';
7
8
  export { AffectedRowsError, CardinalityError } from './errors.js';
8
9
  export type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
9
10
  export { identifier, sql, unsafeSql } from './query.js';
@@ -14,7 +15,7 @@ export type { DescribeOptions, DescribedColumn, DescribedType, DescriptionIssue,
14
15
  export { adoptBaseline, captureBaseline } from './baseline.js';
15
16
  export type { CaptureBaselineOptions, CapturedBaseline } from './baseline.js';
16
17
  export { generateDefinitions } from './generate.js';
17
- export type { GenerateOptions, GenerateTypesOptions } from './generate.js';
18
+ export type { GenerateOptions } from './generate.js';
18
19
  export { migrate, migrationStatus, rollback } from './migrations.js';
19
20
  export type { MigrationStatus } from './migrations.js';
20
21
  export type { DatabaseToolOptions } from './migrations/connection.js';