@kequtech/tenon-orm 0.2.1 → 0.2.2
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 +15 -0
- package/README.md +27 -70
- package/dist/connection.d.ts +8 -2
- package/dist/connection.js +13 -0
- package/dist/database.js +34 -8
- package/dist/driver/listener.d.ts +3 -0
- package/dist/driver/listener.js +63 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/inspection/cli.js +3 -3
- package/dist/inspection/options.d.ts +3 -3
- package/dist/inspection/options.js +9 -1
- package/dist/inspection/types.d.ts +8 -8
- package/dist/query-options.js +2 -2
- package/dist/types.d.ts +5 -1
- package/docs/adoption-guide.md +105 -121
- package/docs/agent-guide.md +114 -223
- package/docs/human-guide.md +35 -47
- package/docs/mutations.md +57 -78
- package/docs/query-inspection.md +99 -89
- package/docs/reference.md +266 -319
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
- Closing a Tenon-owned client through `db.close()` prevents native LISTEN reconnects from
|
|
6
|
+
restoring connections after shutdown, including pending subscription setup. Borrowed clients
|
|
7
|
+
retain application ownership.
|
|
8
|
+
- Added `connection: { url, options? }` to preserve a deployment URI while configuring the native
|
|
9
|
+
driver. `ConnectionUri` is exported; `ConnectionOptions` includes the pinned driver's constructor
|
|
10
|
+
`onnotify` callback. Internally created clients remain Tenon-owned.
|
|
11
|
+
- Inspection accepts explicit `undefined` for optional session and timeout settings, including in
|
|
12
|
+
strict TypeScript configurations; unknown keys and invalid defined values still fail.
|
|
13
|
+
- `db.close({ timeoutMs? })` now shares one shutdown outcome across concurrent/later callers and
|
|
14
|
+
defaults to a 5,000 ms grace period before requesting owned driver termination; `0` skips the wait.
|
|
15
|
+
- Added `CardinalityError` for `one`/`oneOrNone` mismatches, with code
|
|
16
|
+
`TENON_CARDINALITY_MISMATCH`, expected mode, actual count, and optional label.
|
|
17
|
+
|
|
3
18
|
## 0.2.1
|
|
4
19
|
|
|
5
20
|
- Added readonly `db.native` access to Tenon's existing Postgres.js client for LISTEN, cursors,
|
package/README.md
CHANGED
|
@@ -2,21 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A PostgreSQL ORM designed for coding agents. Built for Node.js and TypeScript.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
transactions,
|
|
7
|
-
|
|
8
|
-
run against the database.
|
|
9
|
-
|
|
10
|
-
Tenon takes the place of an ORM without hiding PostgreSQL behind a query builder. You or your agent
|
|
11
|
-
write the SQL; Tenon handles binding values and returning results. Your application controls the
|
|
12
|
-
connection configuration and validation. Tenon includes its pinned Postgres.js driver.
|
|
5
|
+
Write SQL directly, bind values with a template tag, and reuse queries through ordinary functions.
|
|
6
|
+
Tenon provides transactions, affected-row guards, SQL migrations, generated database types, and
|
|
7
|
+
query inspection. It includes its pinned Postgres.js driver.
|
|
13
8
|
|
|
14
9
|
```sh
|
|
15
10
|
npm install @kequtech/tenon-orm
|
|
16
11
|
```
|
|
17
12
|
|
|
18
|
-
Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and
|
|
19
|
-
|
|
13
|
+
Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
|
|
14
|
+
development typings.
|
|
20
15
|
|
|
21
16
|
```ts
|
|
22
17
|
import { createTenon, sql } from '@kequtech/tenon-orm';
|
|
@@ -39,70 +34,32 @@ try {
|
|
|
39
34
|
}
|
|
40
35
|
```
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Use [`db.native`](docs/reference.md#native-driver-access) to access the underlying Postgres.js client
|
|
47
|
-
for LISTEN, cursors, COPY, and other native features without installing another driver. Native calls
|
|
48
|
-
use the pool directly and bypass Tenon's query and transaction guards.
|
|
49
|
-
|
|
50
|
-
Tenon also provides:
|
|
51
|
-
|
|
52
|
-
- Transactions and affected-row limits that roll back rejected writes.
|
|
53
|
-
- SQL migration files and a CLI to apply, inspect, and roll back migrations.
|
|
54
|
-
- TypeScript row, insert, and update types generated from your database.
|
|
55
|
-
- SQL fragments for sharing query parts or assembling variable conditions.
|
|
56
|
-
- Query descriptions from PostgreSQL, including parameter/result types and session context.
|
|
57
|
-
|
|
58
|
-
Joins, relationships, aggregates, and upserts use ordinary PostgreSQL SQL. Tenon does not introduce
|
|
59
|
-
model classes or automatically load related records. Input validation, authorization, and database
|
|
60
|
-
performance remain application concerns. You can keep an existing migration runner; Tenon's
|
|
61
|
-
migrations and table type generation are independent conveniences.
|
|
62
|
-
|
|
63
|
-
## Here's what a coding agent says about it
|
|
37
|
+
The example assumes `public.users` exists. In a server, reuse the instance and close it at shutdown.
|
|
38
|
+
`oneOrNone` rejects multiple rows; use `one` for exactly one row, `many` for an array, or omit
|
|
39
|
+
`returns` for the driver's result array and metadata.
|
|
64
40
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
>
|
|
70
|
-
> — Codex, after working on Tenon. An interested party, to put it mildly.
|
|
41
|
+
Joins, relationships, aggregates, and upserts use PostgreSQL SQL. Object-based writes can use
|
|
42
|
+
`sql.insertValues` and `sql.assignments` while keeping the surrounding statement explicit.
|
|
43
|
+
`db.native` exposes the driver for LISTEN, cursors, COPY, and other native features; those calls
|
|
44
|
+
bypass Tenon's query and transaction guards.
|
|
71
45
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
support selected columns.
|
|
77
|
-
Use [generated input types](docs/reference.md#insert-and-update-inputs) to check those objects
|
|
78
|
-
against your schema.
|
|
79
|
-
|
|
80
|
-
## Checking query results
|
|
81
|
-
|
|
82
|
-
The type in `sql<{ id: number; email: string }>` describes what you expect back. TypeScript alone
|
|
83
|
-
cannot verify that your SQL actually selects those columns.
|
|
84
|
-
|
|
85
|
-
Use `describeQuery({ connection, query })` to inspect the finished SQL, including dynamic fragments,
|
|
86
|
-
or `npx tenon describe query.sql` for a SQL file. It reports PostgreSQL types, Postgres.js values,
|
|
87
|
-
session context, and unresolved findings without executing the statement. No checking packages or
|
|
88
|
-
compiler API are required. Keep database tests for actual values, nullability, authorization, and
|
|
89
|
-
write effects; descriptions do not verify `sql<Row>`. See [query inspection](docs/query-inspection.md).
|
|
46
|
+
`sql<Row>` declares an expected result type. `describeQuery` inspects the finished SQL through
|
|
47
|
+
PostgreSQL without executing it; it does not verify that annotation. Keep database tests for actual
|
|
48
|
+
rows and write effects. Validation, authorization, and retry policy belong to the application.
|
|
49
|
+
Existing migration runners can be retained; Tenon's migrations and type generation are optional.
|
|
90
50
|
|
|
91
51
|
## Documentation
|
|
92
52
|
|
|
93
|
-
- **[
|
|
94
|
-
- **[
|
|
95
|
-
- **[
|
|
96
|
-
|
|
97
|
-
- **[
|
|
98
|
-
|
|
99
|
-
- **[
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
The complete `docs/` folder ships with the npm package. Maintainer instructions are in
|
|
105
|
-
[CONTRIBUTING.md](https://github.com/kequtech/tenon-orm/blob/main/CONTRIBUTING.md), which is not
|
|
106
|
-
bundled.
|
|
53
|
+
- **[Human guide](docs/human-guide.md)** — a table, queries, transactions, and generated types.
|
|
54
|
+
- **[Agent guide](docs/agent-guide.md)** — implementation contracts and upgrade workflow.
|
|
55
|
+
- **[Reference](docs/reference.md)** — API options, value mappings, CLI, and limitations.
|
|
56
|
+
- **[Adoption guide](docs/adoption-guide.md)** — replace an ORM and transfer migration ownership.
|
|
57
|
+
- **[Mutation cookbook](docs/mutations.md)** — defaults, patches, upserts, arrays, and batching.
|
|
58
|
+
- **[Query inspection](docs/query-inspection.md)** — Describe reports and database tests.
|
|
59
|
+
- **[Changelog](CHANGELOG.md)** — versioned changes and upgrade instructions.
|
|
60
|
+
|
|
61
|
+
These documents all ship with the npm package, including the changelog so an agent upgrading a
|
|
62
|
+
project can read the relevant release entries locally. Repository maintenance instructions are in
|
|
63
|
+
[CONTRIBUTING.md](https://github.com/kequtech/tenon-orm/blob/main/CONTRIBUTING.md).
|
|
107
64
|
|
|
108
65
|
[ISC licensed](LICENSE). Maintained by [Kequtech](https://github.com/kequtech).
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import postgres from 'postgres';
|
|
2
2
|
import type { UnknownRow } from './query.js';
|
|
3
|
-
export type ConnectionOptions = postgres.Options<{}
|
|
4
|
-
|
|
3
|
+
export type ConnectionOptions = postgres.Options<{}> & {
|
|
4
|
+
readonly onnotify?: ((channel: string, payload: string) => void) | undefined;
|
|
5
|
+
};
|
|
6
|
+
export interface ConnectionUri {
|
|
7
|
+
readonly url: string;
|
|
8
|
+
readonly options?: ConnectionOptions | undefined;
|
|
9
|
+
}
|
|
10
|
+
export type Connection = string | ConnectionOptions | ConnectionUri | postgres.Sql;
|
|
5
11
|
export type QueryResult<Row extends object = UnknownRow> = postgres.RowList<Row[]>;
|
|
6
12
|
/** The driver operations shared by ordinary and reserved connections. */
|
|
7
13
|
export interface SqlConnection {
|
package/dist/connection.js
CHANGED
|
@@ -11,6 +11,19 @@ export function createConnection(connection) {
|
|
|
11
11
|
return { sql: postgres(connection), owned: true };
|
|
12
12
|
}
|
|
13
13
|
plainObject(connection, 'Connection configuration');
|
|
14
|
+
if ('url' in connection) {
|
|
15
|
+
for (const key of Object.keys(connection)) {
|
|
16
|
+
if (key !== 'url' && key !== 'options') {
|
|
17
|
+
throw new Error(`Unknown URI connection option: ${key}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (typeof connection.url !== 'string' || !connection.url.trim()) {
|
|
21
|
+
throw new Error('Connection string must not be empty.');
|
|
22
|
+
}
|
|
23
|
+
if (connection.options !== undefined)
|
|
24
|
+
plainObject(connection.options, 'Connection options');
|
|
25
|
+
return { sql: postgres(connection.url, connection.options), owned: true };
|
|
26
|
+
}
|
|
14
27
|
return { sql: postgres(connection), owned: true };
|
|
15
28
|
}
|
|
16
29
|
export function executeSql(sql, text, input = []) {
|
package/dist/database.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { createClient } from './client.js';
|
|
2
2
|
import { createConnection, executeSql } from './connection.js';
|
|
3
3
|
import { prepareArrays } from './driver/array.js';
|
|
4
|
+
import { guardListenerShutdown } from './driver/listener.js';
|
|
4
5
|
import { withTransaction } from './transaction.js';
|
|
5
6
|
import { plainObject } from './validation.js';
|
|
7
|
+
function closeTimeout(options) {
|
|
8
|
+
const settings = plainObject(options === undefined ? {} : options, 'Close options');
|
|
9
|
+
for (const key of Object.keys(settings)) {
|
|
10
|
+
if (key !== 'timeoutMs')
|
|
11
|
+
throw new Error(`Unknown close option: ${key}`);
|
|
12
|
+
}
|
|
13
|
+
const timeoutMs = settings.timeoutMs === undefined ? 5000 : settings.timeoutMs;
|
|
14
|
+
if (typeof timeoutMs !== 'number' ||
|
|
15
|
+
!Number.isInteger(timeoutMs) ||
|
|
16
|
+
timeoutMs < 0 ||
|
|
17
|
+
timeoutMs > 2147483647) {
|
|
18
|
+
throw new Error('Close timeoutMs must be an integer from 0 to 2147483647 milliseconds.');
|
|
19
|
+
}
|
|
20
|
+
return timeoutMs;
|
|
21
|
+
}
|
|
6
22
|
export function createTenon(options) {
|
|
7
23
|
for (const key of Object.keys(plainObject(options, 'Tenon options'))) {
|
|
8
24
|
if (key !== 'connection' && key !== 'onQuery') {
|
|
@@ -13,9 +29,10 @@ export function createTenon(options) {
|
|
|
13
29
|
throw new Error('onQuery must be a function.');
|
|
14
30
|
}
|
|
15
31
|
const { sql: pool, owned } = createConnection(options.connection);
|
|
16
|
-
|
|
32
|
+
const stopListeners = owned ? guardListenerShutdown(pool) : undefined;
|
|
33
|
+
let closing;
|
|
17
34
|
function checkOpen() {
|
|
18
|
-
if (
|
|
35
|
+
if (closing) {
|
|
19
36
|
throw new Error('This database instance is closed.');
|
|
20
37
|
}
|
|
21
38
|
}
|
|
@@ -34,13 +51,22 @@ export function createTenon(options) {
|
|
|
34
51
|
checkOpen();
|
|
35
52
|
return withTransaction(pool, (query) => callback(createClient(query, options.onQuery)));
|
|
36
53
|
},
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
return;
|
|
54
|
+
close(options) {
|
|
55
|
+
if (closing)
|
|
56
|
+
return closing;
|
|
57
|
+
try {
|
|
58
|
+
const timeoutMs = closeTimeout(options);
|
|
59
|
+
// Cache before invoking driver callbacks, so reentrant/concurrent callers share shutdown.
|
|
60
|
+
closing = Promise.resolve().then(() => {
|
|
61
|
+
if (owned) {
|
|
62
|
+
stopListeners();
|
|
63
|
+
return pool.end({ timeout: timeoutMs / 1000 });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return closing;
|
|
40
67
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await pool.end();
|
|
68
|
+
catch (error) {
|
|
69
|
+
return Promise.reject(error);
|
|
44
70
|
}
|
|
45
71
|
},
|
|
46
72
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createConnection } from 'node:net';
|
|
2
|
+
function closedError() {
|
|
3
|
+
return Object.assign(new Error('The owned PostgreSQL listener is closed.'), {
|
|
4
|
+
code: 'CONNECTION_ENDED',
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
/** Prevent an owned LISTEN client's queued reconnect from reopening it after shutdown. */
|
|
8
|
+
export function guardListenerShutdown(sql) {
|
|
9
|
+
const options = sql.options;
|
|
10
|
+
const listen = sql.listen;
|
|
11
|
+
const sockets = new Set();
|
|
12
|
+
let closed = false;
|
|
13
|
+
let hostIndex = 0;
|
|
14
|
+
function track(socket) {
|
|
15
|
+
// TLS adoption removes raw socket listeners; prune those closed transports on reconnect.
|
|
16
|
+
for (const previous of sockets)
|
|
17
|
+
if (previous.destroyed)
|
|
18
|
+
sockets.delete(previous);
|
|
19
|
+
sockets.add(socket);
|
|
20
|
+
socket.once('close', () => sockets.delete(socket));
|
|
21
|
+
if (closed) {
|
|
22
|
+
socket.destroy();
|
|
23
|
+
throw closedError();
|
|
24
|
+
}
|
|
25
|
+
return socket;
|
|
26
|
+
}
|
|
27
|
+
sql.listen = (...args) => {
|
|
28
|
+
if (closed)
|
|
29
|
+
return Promise.reject(closedError());
|
|
30
|
+
const previous = options.socket;
|
|
31
|
+
// Postgres.js synchronously clones these options for its dedicated max:1 listener.
|
|
32
|
+
// Restore immediately so ordinary queries retain their native transport configuration.
|
|
33
|
+
options.socket = (settings) => {
|
|
34
|
+
if (closed)
|
|
35
|
+
throw closedError();
|
|
36
|
+
if (previous)
|
|
37
|
+
return Promise.resolve(previous(settings)).then(track);
|
|
38
|
+
const host = settings.host[hostIndex];
|
|
39
|
+
const port = settings.port[hostIndex];
|
|
40
|
+
const socket = createConnection(settings.path ? { path: settings.path } : { host, port });
|
|
41
|
+
if (!settings.path) {
|
|
42
|
+
hostIndex = (hostIndex + 1) % settings.port.length;
|
|
43
|
+
// The driver's TLS negotiation uses this metadata for SNI and verification.
|
|
44
|
+
Object.assign(socket, { host, port });
|
|
45
|
+
}
|
|
46
|
+
// Node buffers startup writes while connecting; the driver retains its connect timeout,
|
|
47
|
+
// authentication, TLS negotiation and reconnect handling.
|
|
48
|
+
return track(socket);
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
return listen(...args);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
options.socket = previous;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return () => {
|
|
58
|
+
closed = true;
|
|
59
|
+
for (const socket of sockets)
|
|
60
|
+
socket.destroy();
|
|
61
|
+
sockets.clear();
|
|
62
|
+
};
|
|
63
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { AffectedRowsConstraint } from './affected-rows.js';
|
|
2
|
+
export declare class CardinalityError extends Error {
|
|
3
|
+
name: string;
|
|
4
|
+
readonly code = "TENON_CARDINALITY_MISMATCH";
|
|
5
|
+
readonly expected: 'one' | 'oneOrNone';
|
|
6
|
+
readonly actual: number;
|
|
7
|
+
readonly label?: string;
|
|
8
|
+
constructor(expected: 'one' | 'oneOrNone', actual: number, label?: string);
|
|
9
|
+
}
|
|
2
10
|
export declare class AffectedRowsError extends Error {
|
|
3
11
|
name: string;
|
|
4
12
|
readonly code: 'TENON_AFFECTED_ROWS_MISMATCH' | 'TENON_AFFECTED_ROWS_UNSUPPORTED';
|
package/dist/errors.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
export class CardinalityError extends Error {
|
|
2
|
+
name = 'CardinalityError';
|
|
3
|
+
code = 'TENON_CARDINALITY_MISMATCH';
|
|
4
|
+
expected;
|
|
5
|
+
actual;
|
|
6
|
+
label;
|
|
7
|
+
constructor(expected, actual, label) {
|
|
8
|
+
const count = expected === 'one' ? 'exactly one row' : 'at most one row';
|
|
9
|
+
super(`${label ?? 'Query'} expected ${count}, but returned ${actual} rows.`);
|
|
10
|
+
this.expected = expected;
|
|
11
|
+
this.actual = actual;
|
|
12
|
+
if (label !== undefined)
|
|
13
|
+
this.label = label;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
1
16
|
export class AffectedRowsError extends Error {
|
|
2
17
|
name = 'AffectedRowsError';
|
|
3
18
|
code;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export type { JSONValue } from 'postgres';
|
|
2
|
-
export type { Connection, ConnectionOptions, QueryResult } from './connection.js';
|
|
2
|
+
export type { Connection, ConnectionOptions, ConnectionUri, QueryResult } from './connection.js';
|
|
3
3
|
export type { AffectedRowsConstraint } from './affected-rows.js';
|
|
4
4
|
export { createTenon } from './database.js';
|
|
5
5
|
export type { TenonOptions } from './database.js';
|
|
6
|
-
export { AffectedRowsError } from './errors.js';
|
|
6
|
+
export { AffectedRowsError, CardinalityError } from './errors.js';
|
|
7
7
|
export type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
|
|
8
8
|
export { identifier, sql, unsafeSql } from './query.js';
|
|
9
9
|
export type { SqlFragment, SqlQuery, UnknownRow } from './query.js';
|
|
10
|
-
export type { Database, QueryClient, QueryEvent, Transaction } from './types.js';
|
|
10
|
+
export type { CloseOptions, Database, QueryClient, QueryEvent, Transaction } from './types.js';
|
|
11
11
|
export { describeQuery } from './describe.js';
|
|
12
12
|
export type { DescribeOptions, DescribedColumn, DescribedType, DescriptionIssue, InspectionContext, InspectionSession, InspectionTimeouts, QueryDescription, } from './inspection/types.js';
|
|
13
13
|
export { adoptBaseline, captureBaseline } from './baseline.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createTenon } from './database.js';
|
|
2
|
-
export { AffectedRowsError } from './errors.js';
|
|
2
|
+
export { AffectedRowsError, CardinalityError } from './errors.js';
|
|
3
3
|
export { identifier, sql, unsafeSql } from './query.js';
|
|
4
4
|
export { describeQuery } from './describe.js';
|
|
5
5
|
export { adoptBaseline, captureBaseline } from './baseline.js';
|
package/dist/inspection/cli.js
CHANGED
|
@@ -14,9 +14,9 @@ export async function runDescribe(filename, connection, values) {
|
|
|
14
14
|
connection,
|
|
15
15
|
query: await readFile(filename, 'utf8'),
|
|
16
16
|
session: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
role: values.role,
|
|
18
|
+
searchPath: values['search-path'],
|
|
19
|
+
timeZone: values['time-zone'],
|
|
20
20
|
},
|
|
21
21
|
timeouts: {
|
|
22
22
|
...(values['connect-timeout-ms'] !== undefined
|
|
@@ -8,8 +8,8 @@ export declare function inspectionOptions(options: Pick<DescribeOptions, 'connec
|
|
|
8
8
|
lockMs: number;
|
|
9
9
|
};
|
|
10
10
|
session: {
|
|
11
|
-
role?: string;
|
|
12
|
-
searchPath?: string;
|
|
13
|
-
timeZone?: string;
|
|
11
|
+
role?: string | undefined;
|
|
12
|
+
searchPath?: string | undefined;
|
|
13
|
+
timeZone?: string | undefined;
|
|
14
14
|
};
|
|
15
15
|
};
|
|
@@ -10,6 +10,8 @@ export function inspectionOptions(options) {
|
|
|
10
10
|
if (!['role', 'searchPath', 'timeZone'].includes(key)) {
|
|
11
11
|
throw new Error(`Unknown inspection session option: ${key}`);
|
|
12
12
|
}
|
|
13
|
+
if (value === undefined)
|
|
14
|
+
continue;
|
|
13
15
|
if (typeof value !== 'string' || !value.trim() || value.includes('\0')) {
|
|
14
16
|
throw new Error(`Inspection session ${key} requires a nonempty string without NUL.`);
|
|
15
17
|
}
|
|
@@ -20,11 +22,17 @@ export function inspectionOptions(options) {
|
|
|
20
22
|
for (const [key, value] of Object.entries(timeouts)) {
|
|
21
23
|
if (!Object.hasOwn(defaults, key))
|
|
22
24
|
throw new Error(`Unknown inspection timeout: ${key}`);
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
continue;
|
|
23
27
|
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 2147483647) {
|
|
24
28
|
throw new Error(`Inspection timeout ${key} must be a positive integer in milliseconds.`);
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
|
-
const limits = {
|
|
31
|
+
const limits = {
|
|
32
|
+
connectMs: timeouts.connectMs ?? defaults.connectMs,
|
|
33
|
+
statementMs: timeouts.statementMs ?? defaults.statementMs,
|
|
34
|
+
lockMs: timeouts.lockMs ?? defaults.lockMs,
|
|
35
|
+
};
|
|
28
36
|
// URL startup options must not disable the inspection's server-side bounds.
|
|
29
37
|
let url;
|
|
30
38
|
try {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { SqlQuery } from '../query.js';
|
|
2
2
|
export interface InspectionSession {
|
|
3
|
-
readonly role?: string;
|
|
3
|
+
readonly role?: string | undefined;
|
|
4
4
|
/** PostgreSQL's search_path setting, as used in the application's connection options. */
|
|
5
|
-
readonly searchPath?: string;
|
|
6
|
-
readonly timeZone?: string;
|
|
5
|
+
readonly searchPath?: string | undefined;
|
|
6
|
+
readonly timeZone?: string | undefined;
|
|
7
7
|
}
|
|
8
8
|
export interface InspectionTimeouts {
|
|
9
|
-
readonly connectMs?: number;
|
|
10
|
-
readonly statementMs?: number;
|
|
11
|
-
readonly lockMs?: number;
|
|
9
|
+
readonly connectMs?: number | undefined;
|
|
10
|
+
readonly statementMs?: number | undefined;
|
|
11
|
+
readonly lockMs?: number | undefined;
|
|
12
12
|
}
|
|
13
13
|
export interface InspectionContext {
|
|
14
14
|
readonly mode: 'separate-session';
|
|
@@ -27,8 +27,8 @@ export interface DescribeOptions {
|
|
|
27
27
|
readonly connection: string;
|
|
28
28
|
/** Finished SQL; query values are neither bound nor executed. */
|
|
29
29
|
readonly query: string | Pick<SqlQuery, 'text'>;
|
|
30
|
-
readonly session?: InspectionSession;
|
|
31
|
-
readonly timeouts?: InspectionTimeouts;
|
|
30
|
+
readonly session?: InspectionSession | undefined;
|
|
31
|
+
readonly timeouts?: InspectionTimeouts | undefined;
|
|
32
32
|
}
|
|
33
33
|
export type DescriptionIssue = {
|
|
34
34
|
readonly code: 'nullability-unknown';
|
package/dist/query-options.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateAffectedRows } from './affected-rows.js';
|
|
2
2
|
import { plainObject } from './validation.js';
|
|
3
|
+
import { CardinalityError } from './errors.js';
|
|
3
4
|
export function validateOptions(options) {
|
|
4
5
|
const config = options === undefined ? {} : plainObject(options, 'Query options');
|
|
5
6
|
for (const key of Object.keys(config)) {
|
|
@@ -27,9 +28,8 @@ export function queryOutput(options, result) {
|
|
|
27
28
|
return result;
|
|
28
29
|
if (options.returns === 'many')
|
|
29
30
|
return [...result];
|
|
30
|
-
const expected = options.returns === 'oneOrNone' ? 'at most one row' : 'exactly one row';
|
|
31
31
|
if (result.length > 1 || (options.returns === 'one' && result.length === 0)) {
|
|
32
|
-
throw new
|
|
32
|
+
throw new CardinalityError(options.returns === 'oneOrNone' ? 'oneOrNone' : 'one', result.length, options.label);
|
|
33
33
|
}
|
|
34
34
|
return result[0];
|
|
35
35
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,12 +12,16 @@ export interface QueryClient {
|
|
|
12
12
|
query<Row extends object>(query: SqlQuery<Row>, options: QueryOptions | undefined): NoInfer<Promise<QueryOutput<Row, QueryReturnMode>>>;
|
|
13
13
|
}
|
|
14
14
|
export type Transaction = QueryClient;
|
|
15
|
+
export interface CloseOptions {
|
|
16
|
+
/** Grace period before requesting owned driver termination; milliseconds, default 5000. */
|
|
17
|
+
readonly timeoutMs?: number | undefined;
|
|
18
|
+
}
|
|
15
19
|
export type Database = Transaction & {
|
|
16
20
|
/** The underlying pool-level driver client; native calls bypass Tenon's query and transaction guards. */
|
|
17
21
|
readonly native: postgres.Sql;
|
|
18
22
|
/** Await all work inside the callback. Commit on success; rollback on an escaping error. */
|
|
19
23
|
transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
|
|
20
|
-
close(): Promise<void>;
|
|
24
|
+
close(options?: CloseOptions): Promise<void>;
|
|
21
25
|
};
|
|
22
26
|
/** One completed public query call, including rollback/commit time for guarded calls. */
|
|
23
27
|
export interface QueryEvent {
|