@kequtech/tenon-orm 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -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/generation/render.js +5 -4
- 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 +63 -32
- package/docs/human-guide.md +10 -6
- package/docs/query-inspection.md +2 -2
- package/docs/reference.md +34 -23
- package/docs/schema-types.md +31 -23
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.5
|
|
4
|
+
|
|
5
|
+
- Preserve generated enum and enum-domain label unions when unrelated codecs change, including
|
|
6
|
+
Date and bigint decoding. Custom enum decoders retain their declared output types; unidentified
|
|
7
|
+
decoders still warn and generate `unknown`. Enum write inputs retain catalog choices.
|
|
8
|
+
- Require pgclear `^0.3.3` for per-type default-decoder metadata and dynamic notification
|
|
9
|
+
subscriptions. Native listeners can add and remove channels on one dedicated connection; see
|
|
10
|
+
[notifications](docs/connections.md#listen-for-notifications) for ownership and recovery.
|
|
11
|
+
|
|
12
|
+
## 0.4.4
|
|
13
|
+
|
|
14
|
+
- Pass the shared `TenonConfig` directly to `createTenon`, including its connection, codecs, and
|
|
15
|
+
`onQuery` observer. `TenonConfig` replaces the `TenonOptions` type.
|
|
16
|
+
- Defer pool creation and connection validation until the first query, transaction, or native-pool
|
|
17
|
+
access. Importing configuration and closing an unused instance need no credentials. Generated
|
|
18
|
+
configs use plain environment reads; remove eager credential checks from existing configs.
|
|
19
|
+
- Support named config exports for multiple databases and CLI selection with `--database`. A sole
|
|
20
|
+
config is selected automatically; ambiguous or unknown names fail without fallback. `tenon init`
|
|
21
|
+
generates a named `main` export, and existing default exports remain supported.
|
|
22
|
+
- Allow migration-file creation without database credentials while retaining each selected config's
|
|
23
|
+
migration, generation, and checking settings.
|
|
24
|
+
|
|
3
25
|
## 0.4.3
|
|
4
26
|
|
|
5
27
|
- Add shared runtime/CLI configuration and offline `tenon init`. Restore `tenon generate`, share migration defaults, and require config for generation/checking/inspection.
|
package/README.md
CHANGED
|
@@ -15,14 +15,14 @@ typings. The PostgreSQL driver, [pgclear](https://github.com/kequtech/pgclear),
|
|
|
15
15
|
|
|
16
16
|
## Query with SQL
|
|
17
17
|
|
|
18
|
-
This example assumes a `public.users` table with integer `id` and text `email` columns
|
|
18
|
+
This example assumes a `public.users` table with integer `id` and text `email` columns and a
|
|
19
|
+
`main` export in your [shared configuration](docs/configuration.md):
|
|
19
20
|
|
|
20
21
|
```ts
|
|
21
22
|
import { createTenon, sql } from '@kequtech/tenon-orm';
|
|
23
|
+
import { main } from './tenon.config.ts';
|
|
22
24
|
|
|
23
|
-
const
|
|
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) {
|
|
@@ -6,12 +6,13 @@ function columnTypes(column, codecs, path, warn) {
|
|
|
6
6
|
warn(`Column ${path} uses a custom decoder without a known TypeScript representation; generated as unknown.`);
|
|
7
7
|
read = 'unknown';
|
|
8
8
|
}
|
|
9
|
-
const choices = column.choices?.map((choice) => JSON.stringify(choice)).join(' | ') || 'never';
|
|
10
|
-
if (column.typeKind === 'e' && codecs === defaultCodecs && read === 'string')
|
|
11
|
-
read = choices;
|
|
12
9
|
let write = defaultCodecs.typescript(oid);
|
|
13
|
-
if (column.typeKind === 'e')
|
|
10
|
+
if (column.typeKind === 'e') {
|
|
11
|
+
const choices = column.choices?.map((choice) => JSON.stringify(choice)).join(' | ') || 'never';
|
|
12
|
+
if (read === 'string' && codecs.isDefaultDecoder(oid))
|
|
13
|
+
read = choices;
|
|
14
14
|
write = choices;
|
|
15
|
+
}
|
|
15
16
|
if (write === 'unknown' || write.startsWith('PgArray<'))
|
|
16
17
|
write = 'string';
|
|
17
18
|
if ([20, 1700].includes(oid))
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ export type { Codec, CodecRegistry, Parameter, PgArray, TemporalConstructors, Te
|
|
|
3
3
|
export type { Connection, ConnectionConfig, ConnectionOptions, QueryResult } from './connection.js';
|
|
4
4
|
export type { AffectedRowsConstraint } from './affected-rows.js';
|
|
5
5
|
export { createTenon } from './database.js';
|
|
6
|
-
export type { TenonOptions } from './database.js';
|
|
7
6
|
export type { TenonConfig } from './config.js';
|
|
8
7
|
export { AffectedRowsError, CardinalityError } from './errors.js';
|
|
9
8
|
export type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { plainObject } from '../validation.js';
|
|
2
|
+
import { configConnection } from '../config/connection.js';
|
|
2
3
|
const defaults = { connectMs: 3000, statementMs: 5000, lockMs: 1000 };
|
|
3
4
|
export function sessionOptions(value) {
|
|
4
5
|
const input = plainObject(value, 'Inspection session');
|
|
@@ -31,19 +32,8 @@ export function timeoutOptions(value) {
|
|
|
31
32
|
}
|
|
32
33
|
/** Validate and capture caller-owned settings before opening a connection. */
|
|
33
34
|
export function inspectionOptions(options) {
|
|
34
|
-
|
|
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).
|