@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 +13 -0
- package/README.md +10 -8
- package/dist/cli.js +12 -7
- package/dist/config/connection.d.ts +3 -0
- package/dist/config/connection.js +22 -0
- package/dist/config/init.js +2 -5
- package/dist/config/load.d.ts +2 -2
- package/dist/config/load.js +29 -36
- package/dist/config/validate.d.ts +3 -0
- package/dist/config/validate.js +28 -0
- package/dist/config.d.ts +7 -2
- package/dist/connection.d.ts +4 -1
- package/dist/connection.js +1 -1
- package/dist/database.d.ts +3 -7
- package/dist/database.js +23 -15
- package/dist/index.d.ts +0 -1
- package/dist/inspection/options.js +3 -13
- package/dist/inspection/types.d.ts +1 -1
- package/dist/types.d.ts +1 -1
- package/docs/agent-guide.md +2 -3
- package/docs/configuration.md +148 -83
- package/docs/connections.md +50 -28
- package/docs/human-guide.md +10 -6
- package/docs/query-inspection.md +2 -2
- package/docs/reference.md +33 -22
- package/docs/schema-types.md +10 -5
- package/package.json +1 -1
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
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
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
|
|
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,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
|
+
}
|
package/dist/config/init.js
CHANGED
|
@@ -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
|
|
22
|
-
|
|
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' },
|
package/dist/config/load.d.ts
CHANGED
|
@@ -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>;
|
package/dist/config/load.js
CHANGED
|
@@ -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
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
69
|
+
let exports;
|
|
59
70
|
try {
|
|
60
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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?: {
|
package/dist/connection.d.ts
CHANGED
|
@@ -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;
|
package/dist/connection.js
CHANGED
package/dist/database.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Database
|
|
3
|
-
export
|
|
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(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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,
|
|
44
|
-
native
|
|
48
|
+
...createClient(query, onQuery),
|
|
49
|
+
get native() {
|
|
50
|
+
return currentConnection().pool;
|
|
51
|
+
},
|
|
45
52
|
async transaction(callback) {
|
|
46
53
|
checkOpen();
|
|
47
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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>;
|
package/docs/agent-guide.md
CHANGED
|
@@ -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.
|
|
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).
|
package/docs/configuration.md
CHANGED
|
@@ -1,56 +1,120 @@
|
|
|
1
1
|
# Shared runtime and CLI configuration
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
36
|
+
import { main } from '../tenon.config.ts';
|
|
39
37
|
|
|
40
|
-
export const db = createTenon(
|
|
38
|
+
export const db = createTenon(main);
|
|
41
39
|
```
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
config
|
|
70
|
-
workflow. `init`, `new`, and `--help`
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
79
|
-
|
|
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.
|
|
86
|
-
error; Tenon
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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` |
|
|
99
|
-
| `
|
|
100
|
-
| `migrations.
|
|
101
|
-
| `
|
|
102
|
-
| `generate.
|
|
103
|
-
| `
|
|
104
|
-
| `baseline.
|
|
105
|
-
| `
|
|
106
|
-
| `check.
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
126
|
-
baseline: { connection:
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
145
|
-
|
|
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.
|
package/docs/connections.md
CHANGED
|
@@ -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
|
-
|
|
9
|
-
import { createTenon } from '@kequtech/tenon-orm';
|
|
8
|
+
Define connection settings in the shared config:
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
```ts
|
|
11
|
+
// tenon.config.ts
|
|
12
|
+
import type { TenonConfig } from '@kequtech/tenon-orm';
|
|
13
13
|
|
|
14
|
-
export const
|
|
14
|
+
export const main = {
|
|
15
15
|
connection: {
|
|
16
|
-
connectionString:
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
88
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
102
|
-
and
|
|
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
|
-
|
|
110
|
-
|
|
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.
|
|
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
|
-
|
|
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';
|
package/docs/human-guide.md
CHANGED
|
@@ -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
|
|
38
|
-
|
|
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
|
|
50
|
+
import { main } from '../tenon.config.ts';
|
|
53
51
|
|
|
54
|
-
export const db = createTenon(
|
|
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
|
|
package/docs/query-inspection.md
CHANGED
|
@@ -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
|
|
12
|
+
import { main } from '../tenon.config.ts';
|
|
13
13
|
|
|
14
|
-
const 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(
|
|
13
|
+
createTenon(config: TenonConfig): Database
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`connection`
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
284
|
-
|
|
|
285
|
-
| Missing shared configuration
|
|
286
|
-
|
|
|
287
|
-
|
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
-
|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
294
|
-
|
|
|
295
|
-
|
|
|
296
|
-
|
|
|
297
|
-
|
|
|
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`.
|
|
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
|
-
|
|
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
|
|
package/docs/schema-types.md
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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.
|
|
176
|
-
|
|
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
|