@kequtech/tenon-orm 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +10 -1
- package/dist/check.js +4 -2
- package/dist/cli.js +101 -28
- package/dist/config/init.d.ts +2 -0
- package/dist/config/init.js +34 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.js +159 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +1 -0
- package/dist/connection.d.ts +5 -0
- package/dist/database.js +0 -5
- package/dist/describe.js +3 -1
- package/dist/generate.d.ts +0 -11
- package/dist/generate.js +1 -26
- package/dist/generation/output.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/inspection/cli.d.ts +3 -4
- package/dist/inspection/cli.js +3 -13
- package/dist/inspection/connection.js +23 -19
- package/dist/inspection/options.d.ts +5 -7
- package/dist/inspection/options.js +49 -22
- package/dist/inspection/types.d.ts +3 -2
- package/dist/migrations/history.d.ts +1 -1
- package/dist/migrations/history.js +3 -2
- package/dist/types.d.ts +0 -3
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +6 -0
- package/docs/adoption-guide.md +9 -3
- package/docs/agent-guide.md +13 -15
- package/docs/configuration.md +146 -0
- package/docs/connections.md +7 -5
- package/docs/human-guide.md +40 -25
- package/docs/migrations.md +13 -6
- package/docs/query-checking.md +14 -11
- package/docs/query-inspection.md +10 -7
- package/docs/reference.md +44 -32
- package/docs/schema-types.md +25 -22
- package/package.json +1 -1
package/dist/inspection/cli.d.ts
CHANGED
|
@@ -21,9 +21,9 @@ export declare const describeFlags: {
|
|
|
21
21
|
/** Translate CLI flags; validation and defaults belong to the public API. */
|
|
22
22
|
export declare function inspectionSettings(values: Partial<Record<keyof typeof describeFlags, string | undefined>>): {
|
|
23
23
|
session: {
|
|
24
|
-
|
|
25
|
-
searchPath
|
|
26
|
-
|
|
24
|
+
timeZone?: string;
|
|
25
|
+
searchPath?: string;
|
|
26
|
+
role?: string;
|
|
27
27
|
};
|
|
28
28
|
timeouts: {
|
|
29
29
|
lockMs?: number;
|
|
@@ -31,4 +31,3 @@ export declare function inspectionSettings(values: Partial<Record<keyof typeof d
|
|
|
31
31
|
connectMs?: number;
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
|
-
export declare function runDescribe(filename: string, connection: string, values: Partial<Record<keyof typeof describeFlags, string | undefined>>): Promise<void>;
|
package/dist/inspection/cli.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { describeQuery } from '../describe.js';
|
|
3
1
|
export const describeFlags = {
|
|
4
2
|
role: { type: 'string' },
|
|
5
3
|
'search-path': { type: 'string' },
|
|
@@ -12,9 +10,9 @@ export const describeFlags = {
|
|
|
12
10
|
export function inspectionSettings(values) {
|
|
13
11
|
return {
|
|
14
12
|
session: {
|
|
15
|
-
role: values.role,
|
|
16
|
-
searchPath: values['search-path'],
|
|
17
|
-
timeZone: values['time-zone'],
|
|
13
|
+
...(values.role !== undefined && { role: values.role }),
|
|
14
|
+
...(values['search-path'] !== undefined && { searchPath: values['search-path'] }),
|
|
15
|
+
...(values['time-zone'] !== undefined && { timeZone: values['time-zone'] }),
|
|
18
16
|
},
|
|
19
17
|
timeouts: {
|
|
20
18
|
...(values['connect-timeout-ms'] !== undefined
|
|
@@ -29,11 +27,3 @@ export function inspectionSettings(values) {
|
|
|
29
27
|
},
|
|
30
28
|
};
|
|
31
29
|
}
|
|
32
|
-
export async function runDescribe(filename, connection, values) {
|
|
33
|
-
const result = await describeQuery({
|
|
34
|
-
connection,
|
|
35
|
-
query: await readFile(filename, 'utf8'),
|
|
36
|
-
...inspectionSettings(values),
|
|
37
|
-
});
|
|
38
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
39
|
-
}
|
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createPool, defaultCodecs } from '@kequtech/pgclear';
|
|
2
2
|
import { inspectionOptions } from './options.js';
|
|
3
3
|
export async function withInspectionConnection(options, work) {
|
|
4
|
-
const {
|
|
5
|
-
const
|
|
6
|
-
|
|
4
|
+
const { connection, limits, session } = inspectionOptions(options);
|
|
5
|
+
const pool = createPool({
|
|
6
|
+
...connection,
|
|
7
|
+
codecs: defaultCodecs,
|
|
8
|
+
maxConnections: 1,
|
|
7
9
|
connectTimeoutMs: limits.connectMs,
|
|
8
10
|
startupParameters: {
|
|
11
|
+
...Object.fromEntries(Object.entries(connection.startupParameters ?? {}).filter(([name]) => !['statement_timeout', 'lock_timeout'].includes(name.toLowerCase()))),
|
|
9
12
|
statement_timeout: String(limits.statementMs),
|
|
10
13
|
lock_timeout: String(limits.lockMs),
|
|
11
14
|
},
|
|
12
15
|
});
|
|
13
|
-
const client = connection;
|
|
14
16
|
try {
|
|
15
|
-
|
|
16
|
-
[
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
return await pool.withSession(async (client) => {
|
|
18
|
+
for (const [key, name] of [
|
|
19
|
+
['role', 'role'],
|
|
20
|
+
['searchPath', 'search_path'],
|
|
21
|
+
['timeZone', 'TimeZone'],
|
|
22
|
+
]) {
|
|
23
|
+
const value = session[key];
|
|
24
|
+
if (value !== undefined)
|
|
25
|
+
await client.queryClassic('SELECT pg_catalog.set_config($1, $2, false)', [name, value]);
|
|
26
|
+
}
|
|
27
|
+
const { rows: [context], } = await client.queryClassic(`
|
|
25
28
|
SELECT pg_catalog.current_database() AS database,
|
|
26
29
|
session_user AS "sessionUser", current_user AS "currentUser",
|
|
27
30
|
pg_catalog.current_setting('search_path') AS "searchPath",
|
|
@@ -32,11 +35,12 @@ export async function withInspectionConnection(options, work) {
|
|
|
32
35
|
(SELECT setting::integer FROM pg_catalog.pg_settings
|
|
33
36
|
WHERE name = 'lock_timeout') AS "lockTimeoutMs"
|
|
34
37
|
`, []);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
if (!context)
|
|
39
|
+
throw new Error('Inspection context was not returned.');
|
|
40
|
+
return await work(client, { mode: 'separate-session', readMappings: 'pgclear', ...context });
|
|
41
|
+
});
|
|
38
42
|
}
|
|
39
43
|
finally {
|
|
40
|
-
await
|
|
44
|
+
await pool.close({ timeoutMs: limits.connectMs });
|
|
41
45
|
}
|
|
42
46
|
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import type { DescribeOptions } from './types.js';
|
|
1
|
+
import type { DescribeOptions, InspectionSession, InspectionTimeouts } from './types.js';
|
|
2
|
+
export declare function sessionOptions(value: unknown): InspectionSession;
|
|
3
|
+
export declare function timeoutOptions(value: unknown): InspectionTimeouts;
|
|
2
4
|
/** Validate and capture caller-owned settings before opening a connection. */
|
|
3
5
|
export declare function inspectionOptions(options: Pick<DescribeOptions, 'connection' | 'session' | 'timeouts'>): {
|
|
4
|
-
|
|
6
|
+
connection: import("@kequtech/pgclear").PoolOptions;
|
|
5
7
|
limits: {
|
|
6
8
|
connectMs: number;
|
|
7
9
|
statementMs: number;
|
|
8
10
|
lockMs: number;
|
|
9
11
|
};
|
|
10
|
-
session:
|
|
11
|
-
role?: string | undefined;
|
|
12
|
-
searchPath?: string | undefined;
|
|
13
|
-
timeZone?: string | undefined;
|
|
14
|
-
};
|
|
12
|
+
session: InspectionSession;
|
|
15
13
|
};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { plainObject } from '../validation.js';
|
|
2
|
-
|
|
3
|
-
export function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
plainObject(options.session ?? {}, 'Inspection session');
|
|
8
|
-
const session = { ...options.session };
|
|
2
|
+
const defaults = { connectMs: 3000, statementMs: 5000, lockMs: 1000 };
|
|
3
|
+
export function sessionOptions(value) {
|
|
4
|
+
const input = plainObject(value, 'Inspection session');
|
|
5
|
+
const session = { ...input };
|
|
9
6
|
for (const [key, value] of Object.entries(session)) {
|
|
10
7
|
if (!['role', 'searchPath', 'timeZone'].includes(key)) {
|
|
11
8
|
throw new Error(`Unknown inspection session option: ${key}`);
|
|
@@ -16,9 +13,11 @@ export function inspectionOptions(options) {
|
|
|
16
13
|
throw new Error(`Inspection session ${key} requires a nonempty string without NUL.`);
|
|
17
14
|
}
|
|
18
15
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
return session;
|
|
17
|
+
}
|
|
18
|
+
export function timeoutOptions(value) {
|
|
19
|
+
const input = plainObject(value, 'Inspection timeouts');
|
|
20
|
+
const timeouts = { ...input };
|
|
22
21
|
for (const [key, value] of Object.entries(timeouts)) {
|
|
23
22
|
if (!Object.hasOwn(defaults, key))
|
|
24
23
|
throw new Error(`Unknown inspection timeout: ${key}`);
|
|
@@ -28,24 +27,52 @@ export function inspectionOptions(options) {
|
|
|
28
27
|
throw new Error(`Inspection timeout ${key} must be a positive integer in milliseconds.`);
|
|
29
28
|
}
|
|
30
29
|
}
|
|
30
|
+
return timeouts;
|
|
31
|
+
}
|
|
32
|
+
/** Validate and capture caller-owned settings before opening a connection. */
|
|
33
|
+
export function inspectionOptions(options) {
|
|
34
|
+
let connection;
|
|
35
|
+
if (typeof options.connection === 'string') {
|
|
36
|
+
if (!options.connection.trim())
|
|
37
|
+
throw new Error('Describe requires an explicit nonempty connection string.');
|
|
38
|
+
connection = { connectionString: options.connection };
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
plainObject(options.connection, 'Inspection connection');
|
|
42
|
+
if ('acquire' in options.connection || 'close' in options.connection) {
|
|
43
|
+
throw new Error('Inspection requires connection options, not an open pool.');
|
|
44
|
+
}
|
|
45
|
+
connection = { ...options.connection };
|
|
46
|
+
}
|
|
47
|
+
if (connection.startupParameters !== undefined) {
|
|
48
|
+
plainObject(connection.startupParameters, 'Startup parameters');
|
|
49
|
+
}
|
|
50
|
+
const session = sessionOptions(options.session ?? {});
|
|
51
|
+
const timeouts = timeoutOptions(options.timeouts ?? {});
|
|
31
52
|
const limits = {
|
|
32
53
|
connectMs: timeouts.connectMs ?? defaults.connectMs,
|
|
33
54
|
statementMs: timeouts.statementMs ?? defaults.statementMs,
|
|
34
55
|
lockMs: timeouts.lockMs ?? defaults.lockMs,
|
|
35
56
|
};
|
|
36
57
|
// URL startup options must not disable the inspection's server-side bounds.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
if (connection.connectionString !== undefined) {
|
|
59
|
+
let url;
|
|
60
|
+
try {
|
|
61
|
+
url = new URL(connection.connectionString);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error('Describe requires a PostgreSQL connection URI.');
|
|
65
|
+
}
|
|
66
|
+
if (!['postgres:', 'postgresql:'].includes(url.protocol)) {
|
|
67
|
+
throw new Error('Describe requires a PostgreSQL connection URI.');
|
|
68
|
+
}
|
|
69
|
+
if (!url.searchParams.has('application_name') && connection.applicationName === undefined) {
|
|
70
|
+
url.searchParams.set('application_name', 'tenon-describe');
|
|
71
|
+
}
|
|
72
|
+
connection = { ...connection, connectionString: url.toString() };
|
|
46
73
|
}
|
|
47
|
-
if (
|
|
48
|
-
|
|
74
|
+
else if (connection.applicationName === undefined) {
|
|
75
|
+
connection = { ...connection, applicationName: 'tenon-describe' };
|
|
49
76
|
}
|
|
50
|
-
return {
|
|
77
|
+
return { connection, limits, session };
|
|
51
78
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CodecRegistry, DescribeStatement } from '@kequtech/pgclear';
|
|
2
|
+
import type { ConnectionConfig } from '../connection.js';
|
|
2
3
|
export interface InspectionSession {
|
|
3
4
|
readonly role?: string | undefined;
|
|
4
5
|
/** PostgreSQL's search_path setting, as used in the application's connection options. */
|
|
@@ -23,8 +24,8 @@ export interface InspectionContext {
|
|
|
23
24
|
readonly readMappings: 'pgclear';
|
|
24
25
|
}
|
|
25
26
|
export interface DescribeOptions {
|
|
26
|
-
/**
|
|
27
|
-
readonly connection:
|
|
27
|
+
/** URI or pool configuration values. Inspection owns a separate, short-lived connection. */
|
|
28
|
+
readonly connection: ConnectionConfig;
|
|
28
29
|
/** Finished SQL; query values are neither bound nor executed. */
|
|
29
30
|
readonly query: string | DescribeStatement;
|
|
30
31
|
/** Use the same registry as execution; custom decoder types can remain unresolved. */
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { defaultCodecs } from '@kequtech/pgclear';
|
|
1
2
|
import { qualified } from '../sql.js';
|
|
2
3
|
export const migrationTable = '__tenon_migrations';
|
|
3
4
|
export async function migrationState(client, namespace, migrations) {
|
|
4
|
-
const exists = await client.queryClassic('SELECT to_regclass($1)::text AS name', [qualified(namespace, migrationTable)]);
|
|
5
|
+
const exists = await client.queryClassic('SELECT pg_catalog.to_regclass($1)::text AS name', [qualified(namespace, migrationTable)], { codecs: defaultCodecs });
|
|
5
6
|
if (exists.rows[0].name === null) {
|
|
6
7
|
return [];
|
|
7
8
|
}
|
|
8
|
-
const { rows: applied } = await client.queryClassic(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`, []);
|
|
9
|
+
const { rows: applied } = await client.queryClassic(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`, [], { codecs: defaultCodecs });
|
|
9
10
|
for (const [i, record] of applied.entries()) {
|
|
10
11
|
const file = migrations[i];
|
|
11
12
|
if (!file ||
|
package/dist/types.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { Pool } from '@kequtech/pgclear';
|
|
|
2
2
|
import type { QueryResult } from './connection.js';
|
|
3
3
|
import type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
|
|
4
4
|
import type { SqlQuery } from './query.js';
|
|
5
|
-
import type { GenerateTypesOptions } from './generate.js';
|
|
6
5
|
export interface QueryClient {
|
|
7
6
|
query<Row extends object>(query: SqlQuery<Row>, options?: QueryOptions & {
|
|
8
7
|
readonly returns?: 'native';
|
|
@@ -20,8 +19,6 @@ export interface CloseOptions {
|
|
|
20
19
|
export type Database = Transaction & {
|
|
21
20
|
/** The underlying pool-level driver client; native calls bypass Tenon's query and transaction guards. */
|
|
22
21
|
readonly native: Pool;
|
|
23
|
-
/** Write or check schema declarations using this instance's configured codecs. */
|
|
24
|
-
generateTypes(options: GenerateTypesOptions): Promise<void>;
|
|
25
22
|
/** Await all work inside the callback. Commit on success; rollback on an escaping error. */
|
|
26
23
|
transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
|
|
27
24
|
close(options?: CloseOptions): Promise<void>;
|
package/dist/validation.d.ts
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
1
|
export declare function plainObject(value: unknown, name: string): Record<string, unknown>;
|
|
2
|
+
/** Validate before resolving paths so an empty override cannot become the working directory. */
|
|
3
|
+
export declare function validatePath(value: unknown, label: string): asserts value is string;
|
package/dist/validation.js
CHANGED
|
@@ -8,3 +8,9 @@ export function plainObject(value, name) {
|
|
|
8
8
|
}
|
|
9
9
|
return value;
|
|
10
10
|
}
|
|
11
|
+
/** Validate before resolving paths so an empty override cannot become the working directory. */
|
|
12
|
+
export function validatePath(value, label) {
|
|
13
|
+
if (typeof value !== 'string' || !value.trim() || value.includes('\0')) {
|
|
14
|
+
throw new Error(`${label} must be a nonempty path without NUL.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
package/docs/adoption-guide.md
CHANGED
|
@@ -15,10 +15,12 @@ Before replacing an ORM, identify behavior it currently supplies: IDs/defaults,
|
|
|
15
15
|
validation, hooks, serialization, relationship loading, and transaction boundaries. Preserve needed
|
|
16
16
|
behavior in application code, constraints, or SQL. Tenon has no model lifecycle or implicit loading.
|
|
17
17
|
|
|
18
|
+
Use [shared configuration](configuration.md) to reuse connection settings and codecs across runtime,
|
|
19
|
+
generation, checking, inspection, and migrations. Keep credentials supplied by the deployment.
|
|
20
|
+
|
|
18
21
|
## Replace queries
|
|
19
22
|
|
|
20
|
-
1. Optionally generate
|
|
21
|
-
`await db.generateTypes({ out: 'src/generated-schema.d.ts', namespace: 'public' })`.
|
|
23
|
+
1. Optionally run `tenon generate` with the application's shared configuration.
|
|
22
24
|
Use the application's schema and codec registry; no Tenon migration history is required.
|
|
23
25
|
2. Replace one workflow at a time with parameterized SQL in ordinary functions. Accept
|
|
24
26
|
`QueryClient` when callers may use either `db` or `tx`. Preserve selected columns, ordering,
|
|
@@ -48,9 +50,13 @@ reference database, preferably a disposable replay of the old migrations.
|
|
|
48
50
|
For an application owning `app` in a shared database:
|
|
49
51
|
|
|
50
52
|
```sh
|
|
51
|
-
npx tenon baseline capture --directory migrations --namespace app --schema app
|
|
53
|
+
npx tenon baseline capture --directory migrations --namespace app --schema app --connection "$DATABASE_URL"
|
|
52
54
|
```
|
|
53
55
|
|
|
56
|
+
This example supplies the native libpq URI for the same database through `DATABASE_URL`.
|
|
57
|
+
The native connection can instead be stored in `baseline.connection`, and the captured schemas in
|
|
58
|
+
`baseline.schemas`. A string main connection can be used directly. Preserve required native authentication and TLS settings.
|
|
59
|
+
|
|
54
60
|
The directory must contain no SQL files. Capture uses native
|
|
55
61
|
`pg_dump --schema-only --no-owner --no-privileges` and writes `0001_baseline.up.sql` without a down
|
|
56
62
|
file. Install compatible PostgreSQL client tools on PATH, preferably matching the server's major
|
package/docs/agent-guide.md
CHANGED
|
@@ -5,8 +5,8 @@ Use this as a short workflow for changing an existing Tenon application. For fir
|
|
|
5
5
|
|
|
6
6
|
## Read the project first
|
|
7
7
|
|
|
8
|
-
Inspect
|
|
9
|
-
commands. Reuse the application's package manager, connection configuration, namespace, and
|
|
8
|
+
Inspect `tenon.config.ts` (or its JavaScript equivalent), the database module, nearby query
|
|
9
|
+
functions, migration runner, generated types, and check commands. Reuse the application's package manager, connection configuration, namespace, and
|
|
10
10
|
conventions. Read the docs shipped with the installed Tenon version.
|
|
11
11
|
|
|
12
12
|
For an upgrade, find the previous resolved version in the lockfile and read the
|
|
@@ -52,8 +52,9 @@ Plain bigint, valid Date, and supported Temporal inputs are accepted; inputs do
|
|
|
52
52
|
decoders. Plain arrays/objects require `json`, `param`, or encoded text. Direct undefined rejects.
|
|
53
53
|
JSON encoding still rejects bigint. Import parameter/codec helpers from Tenon.
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
inspection/checking
|
|
55
|
+
Keep the registry in shared `connection.codecs` so queries and CLI tools agree. Programmatic
|
|
56
|
+
inspection/checking inherit codecs from connection-option values; pass the registry explicitly
|
|
57
|
+
when using a URI. Retain application validation, authorization, domain conversion, and retry
|
|
57
58
|
policy. See [schema types and values](schema-types.md).
|
|
58
59
|
|
|
59
60
|
## Change data deliberately
|
|
@@ -83,16 +84,11 @@ files or omit an irreversible down file, and apply it. Never change applied SQL;
|
|
|
83
84
|
both directions. Each file commits separately. Use direct or session pooling for tooling advisory
|
|
84
85
|
locks. See [migrations](migrations.md).
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Use the project's [generation script](human-guide.md#generate-types), review the diff, and commit
|
|
93
|
-
migrations with exact generated output. Keep handwritten aliases separate and generated files out
|
|
94
|
-
of formatting. In CI, migrate the test database and use `check: true` without first regenerating.
|
|
95
|
-
Production consumes committed declarations.
|
|
87
|
+
After migrations commit, run `tenon generate` with the project's
|
|
88
|
+
[shared configuration](configuration.md). Review the diff and commit migrations with exact
|
|
89
|
+
generated output. Keep handwritten aliases separate and generated files out of formatting. In CI,
|
|
90
|
+
migrate the test database and run `tenon generate --check` without first regenerating. Production
|
|
91
|
+
consumes committed declarations.
|
|
96
92
|
|
|
97
93
|
## Verify the changed workflow
|
|
98
94
|
|
|
@@ -107,7 +103,9 @@ intentional omission behavior and test it. Narrow to required fields only when a
|
|
|
107
103
|
establishes that contract; do not cast merely to silence a diagnostic. See
|
|
108
104
|
[responding to findings](query-checking.md#responding-to-findings).
|
|
109
105
|
|
|
110
|
-
|
|
106
|
+
Share runtime connection/codecs through `tenon.config.ts`; CLI generation, checking, inspection,
|
|
107
|
+
and migrations read it. Use `--config` for another role/deployment. Match runtime role, search path,
|
|
108
|
+
timezone, and codecs. Describe uses a separate session and
|
|
111
109
|
cannot establish result nullability, JSON members, cardinality, or execution permissions. Native
|
|
112
110
|
calls bypass Tenon's guards/observations and need explicit resource ownership; see
|
|
113
111
|
[connections and driver access](connections.md). For common failures, use
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Shared runtime and CLI configuration
|
|
2
|
+
|
|
3
|
+
Keep connection settings and codec choices in one module used by the application and Tenon's CLI.
|
|
4
|
+
From the project root, create the configuration file:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npx tenon init
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Initialization opens no database connection, imports no application code, and never overwrites a
|
|
11
|
+
file. Without `--config`, it refuses when a config is already discoverable in this directory or a
|
|
12
|
+
parent. Use `tenon init --config path.mjs` for an explicit additional destination or JavaScript output. The generated TypeScript config looks like this:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import type { TenonConfig } from '@kequtech/tenon-orm';
|
|
16
|
+
|
|
17
|
+
const connectionString = process.env.DATABASE_URL;
|
|
18
|
+
if (!connectionString) throw new Error('DATABASE_URL is required');
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
connection: {
|
|
22
|
+
connectionString,
|
|
23
|
+
},
|
|
24
|
+
migrations: { directory: 'migrations', namespace: 'public' },
|
|
25
|
+
generate: { out: 'src/generated-schema.d.ts' },
|
|
26
|
+
check: { project: 'tsconfig.json' },
|
|
27
|
+
} satisfies TenonConfig;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add the application's registry as `connection.codecs` when choosing custom representations.
|
|
31
|
+
|
|
32
|
+
`TenonConfig` and `ConnectionConfig` are exported TypeScript types; configuration is a plain
|
|
33
|
+
default-exported object. Use that connection when constructing the application instance:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// src/db.ts
|
|
37
|
+
import { createTenon } from '@kequtech/tenon-orm';
|
|
38
|
+
import config from '../tenon.config.ts';
|
|
39
|
+
|
|
40
|
+
export const db = createTenon({ connection: config.connection });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The `.ts` imports above run directly with Node.js 24+ TypeScript support. For typechecking with
|
|
44
|
+
`noEmit`, enable `allowImportingTsExtensions`; emitting projects can use
|
|
45
|
+
`rewriteRelativeImportExtensions`. Config and its runtime imports must be runnable by Node without
|
|
46
|
+
bundler aliases or transformations. ESM JavaScript is also supported.
|
|
47
|
+
|
|
48
|
+
Export configuration values, not an open pool, database instance, or factory. Importing the config
|
|
49
|
+
must not start the server, open listeners, or depend on generated runtime files. Tenon opens and
|
|
50
|
+
closes its own tooling resources. `createTenon` still accepts application-owned pools separately.
|
|
51
|
+
|
|
52
|
+
## Run commands
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npx tenon new create_users
|
|
56
|
+
npx tenon migrate
|
|
57
|
+
npx tenon generate
|
|
58
|
+
npx tenon generate --check
|
|
59
|
+
npx tenon check
|
|
60
|
+
npx tenon check src/users.ts:23 --json
|
|
61
|
+
npx tenon describe query.sql
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`generate`, `check`, and `describe` require configuration. They use its connection and codec registry
|
|
65
|
+
and never fall back to `DATABASE_URL` merely because a config is missing or invalid. Your config
|
|
66
|
+
chooses which environment variables to read; Tenon does not load `.env` files automatically.
|
|
67
|
+
|
|
68
|
+
`new`, migration/status/rollback, and baseline commands also discover configuration. Without a
|
|
69
|
+
config, they retain the existing migration-directory defaults and `DATABASE_URL` connection
|
|
70
|
+
workflow. `init`, `new`, and `--help` open no database connection. Config imports may still require environment
|
|
71
|
+
values, according to the module you wrote. Invalid session/timeout settings are rejected when config
|
|
72
|
+
loads, even for commands that do not inspect queries. Migration SQL and history reads use default decoding;
|
|
73
|
+
application codec overrides do not change migration bookkeeping.
|
|
74
|
+
|
|
75
|
+
## Discovery, paths, and overrides
|
|
76
|
+
|
|
77
|
+
Tenon searches the working directory and then parents for the nearest directory containing
|
|
78
|
+
`tenon.config.ts`, `.mts`, `.js`, or `.mjs`. Multiple recognized files in that directory are an error.
|
|
79
|
+
Select one explicitly when needed:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npx tenon generate --config config/tenon.production.mts --check
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`--config` resolves from the working directory. An explicit missing file or an import failure is an
|
|
86
|
+
error; Tenon never switches to another configuration after that failure.
|
|
87
|
+
|
|
88
|
+
Paths stored in config resolve from the config file's directory. Configured migration directories,
|
|
89
|
+
generation output, and checker projects therefore work when a command runs from a nested directory.
|
|
90
|
+
An omitted migration directory means `migrations` beside the config; an omitted checker project
|
|
91
|
+
means `tsconfig.json` beside it. Relative CLI path arguments and flags resolve from the working
|
|
92
|
+
directory. CLI flags override the corresponding configured values.
|
|
93
|
+
|
|
94
|
+
## Configuration fields
|
|
95
|
+
|
|
96
|
+
| Field | Purpose |
|
|
97
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `connection` | Required PostgreSQL URI or pgclear pool-option values, including TLS, password callbacks, startup parameters, and codecs. Open pools are rejected. |
|
|
99
|
+
| `migrations.directory` | Migration files; defaults to `migrations` relative to config. |
|
|
100
|
+
| `migrations.namespace` | Migration/history schema; defaults to `public`. Also supplies the generation namespace when generation does not specify one. |
|
|
101
|
+
| `generate.out` | Required when a `generate` section is present. Otherwise supply `--out` to the generation command. |
|
|
102
|
+
| `generate.namespace` | Schema to generate; overrides the migration namespace for generation only. |
|
|
103
|
+
| `baseline.schemas` | Optional nonempty list of schemas for native baseline capture. Omission captures the whole database, independently of the migration namespace. |
|
|
104
|
+
| `baseline.connection` | Optional native libpq connection for baseline capture; `--connection` overrides it. |
|
|
105
|
+
| `check.project` | TypeScript project; defaults to `tsconfig.json` relative to config. |
|
|
106
|
+
| `check.readTypes` | Explicit type expressions for custom decoders whose representations are unidentified. See [checking](query-checking.md#programmatic-use-and-custom-codecs). |
|
|
107
|
+
| `session` | Optional inspection/checking `role`, `searchPath`, and `timeZone` overrides. They do not configure runtime queries or migrations. |
|
|
108
|
+
| `timeouts` | Optional inspection/checking `connectMs`, `statementMs`, and `lockMs` bounds. |
|
|
109
|
+
|
|
110
|
+
Generation and runtime queries use `connection.codecs`. Checking and inspection inherit that registry
|
|
111
|
+
too. Their internal catalog queries keep default decoding. Runtime startup parameters are retained,
|
|
112
|
+
with inspection's statement/lock timeout bounds applied regardless of startup-parameter casing; explicit `session` settings and CLI flags
|
|
113
|
+
then override role, search path, or timezone for inspection. See [connections](connections.md) and
|
|
114
|
+
[inspection limits](query-inspection.md#session-and-timeouts).
|
|
115
|
+
|
|
116
|
+
Keep credentials and deployment-specific addresses in environment variables or the existing secret
|
|
117
|
+
provider. For a migration role different from the application role, select a separate config that
|
|
118
|
+
imports shared settings and supplies that role's connection. Avoid duplicating codec definitions.
|
|
119
|
+
|
|
120
|
+
Baseline capture uses native `pg_dump`, which accepts a libpq connection string rather than pgclear
|
|
121
|
+
option objects or password callbacks. Configure its native connection and scope explicitly when
|
|
122
|
+
needed:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// Additional fields in the shared config:
|
|
126
|
+
baseline: { connection: connectionString, schemas: ['public'] }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Capture uses `--connection`, then `baseline.connection`, then a string main connection or the
|
|
130
|
+
no-config `DATABASE_URL` fallback. `--schema` flags replace `baseline.schemas`. Adoption ignores
|
|
131
|
+
capture-only settings. Supply the same database and required native TLS/authentication settings;
|
|
132
|
+
Tenon does not translate driver-specific settings into libpq settings.
|
|
133
|
+
|
|
134
|
+
## Programmatic APIs
|
|
135
|
+
|
|
136
|
+
The CLI owns generated-file output and freshness checks. `generateDefinitions` returns source text
|
|
137
|
+
without file handling for callers building custom tooling; it accepts explicit connection/options.
|
|
138
|
+
|
|
139
|
+
`describeQuery` and `checkQueries` accept a URI or connection-option values and inherit
|
|
140
|
+
`connection.codecs`. An explicit `codecs` argument selects the read mappings reported by those
|
|
141
|
+
inspection APIs. They own separate, short-lived sessions and do not join callback transactions.
|
|
142
|
+
Migration APIs continue accepting explicit connection/options. No programmatic API discovers config.
|
|
143
|
+
|
|
144
|
+
The CLI imports the selected trusted config and its dependencies. Source checking subsequently reads
|
|
145
|
+
query source without importing those query modules or executing application SQL. TypeScript loads
|
|
146
|
+
only for source checking, not for normal queries, migrations, generation, or inspection.
|
package/docs/connections.md
CHANGED
|
@@ -18,7 +18,7 @@ export const db = createTenon({
|
|
|
18
18
|
connectTimeoutMs: 5000,
|
|
19
19
|
acquireTimeoutMs: 5000,
|
|
20
20
|
idleTimeoutMs: 30_000,
|
|
21
|
-
startupParameters: { search_path: 'app,public'
|
|
21
|
+
startupParameters: { search_path: 'app,public' },
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
24
|
```
|
|
@@ -27,8 +27,9 @@ export const db = createTenon({
|
|
|
27
27
|
`ConnectionOptions` is Tenon's alias for `PoolOptions`. Object fields override URI settings.
|
|
28
28
|
Reuse an instance for each database configuration.
|
|
29
29
|
|
|
30
|
-
The driver does not load environment variables, `.env`, pgpass, or service files.
|
|
31
|
-
|
|
30
|
+
The driver does not load environment variables, `.env`, pgpass, or service files.
|
|
31
|
+
[Shared configuration](configuration.md) supplies connection values to the CLI and application.
|
|
32
|
+
Migration commands retain `DATABASE_URL` fallback when no config exists. An empty options object
|
|
32
33
|
uses localhost:5432, the current OS user and a database with that user's name.
|
|
33
34
|
|
|
34
35
|
Use `tls: {}` for TLS with certificate and hostname verification using system trust. Supply the
|
|
@@ -97,8 +98,9 @@ These choices apply across the database instance, including callback transaction
|
|
|
97
98
|
options have no per-query codec override. Input encoders remain separate: a `param(value, codec)`
|
|
98
99
|
carries its own encoder, and passing a Date does not select a Date output decoder.
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
Place the registry in shared `connection.codecs` for runtime queries and CLI generation, inspection,
|
|
102
|
+
and checking. Programmatic
|
|
103
|
+
`describeQuery` and `checkQueries` inherit codecs from connection-option values too. See
|
|
102
104
|
[schema types and values](schema-types.md#custom-codecs) for custom representations and Temporal.
|
|
103
105
|
|
|
104
106
|
## Observe completed queries
|