@kequtech/tenon-orm 0.2.2 → 0.3.1
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 +46 -42
- package/README.md +4 -4
- package/dist/affected-rows.js +6 -6
- package/dist/baseline.js +4 -4
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +15 -22
- package/dist/connection.js +122 -25
- package/dist/database.js +10 -17
- package/dist/driver/types.d.ts +1 -1
- package/dist/driver/types.js +11 -11
- package/dist/generate.js +7 -7
- package/dist/generation/catalog.d.ts +2 -2
- package/dist/generation/catalog.js +4 -4
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +11 -11
- package/dist/generation/table-types.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/inspection/catalog.d.ts +4 -3
- package/dist/inspection/catalog.js +13 -10
- package/dist/inspection/connection.d.ts +2 -2
- package/dist/inspection/connection.js +22 -22
- package/dist/inspection/protocol.d.ts +7 -0
- package/dist/inspection/protocol.js +45 -0
- package/dist/inspection/result.d.ts +1 -1
- package/dist/inspection/result.js +6 -6
- package/dist/inspection/types.d.ts +2 -2
- package/dist/migrations/connection.d.ts +3 -2
- package/dist/migrations/connection.js +18 -20
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -7
- package/dist/query-options.js +4 -4
- package/dist/query.js +2 -2
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +19 -14
- package/dist/types.d.ts +2 -2
- package/docs/adoption-guide.md +40 -27
- package/docs/agent-guide.md +55 -48
- package/docs/human-guide.md +25 -20
- package/docs/mutations.md +13 -10
- package/docs/query-inspection.md +12 -14
- package/docs/reference.md +304 -131
- package/package.json +3 -2
- package/dist/driver/array.d.ts +0 -5
- package/dist/driver/array.js +0 -70
- package/dist/driver/lifecycle.d.ts +0 -15
- package/dist/driver/lifecycle.js +0 -57
- package/dist/driver/listener.d.ts +0 -3
- package/dist/driver/listener.js +0 -63
- package/dist/driver/session.d.ts +0 -8
- package/dist/driver/session.js +0 -92
package/docs/reference.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Tenon reference
|
|
2
2
|
|
|
3
|
-
Exact API contracts and limitations. The [human guide](human-guide.md) introduces the workflow;
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Exact API contracts and limitations. The [human guide](human-guide.md) introduces the workflow; the
|
|
4
|
+
[agent guide](agent-guide.md) covers routine implementation. Examples below use the `public.users`
|
|
5
|
+
table from the human guide and an existing `db` instance, unless stated otherwise.
|
|
6
6
|
|
|
7
7
|
## Installation and connection
|
|
8
8
|
|
|
9
9
|
Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
|
|
10
|
-
development typings. Tenon includes `
|
|
11
|
-
required. A usual Node TypeScript setup uses `module: "NodeNext"`.
|
|
10
|
+
development typings. Tenon includes `pg@8.23.1` and its TypeScript declarations; no driver peer or
|
|
11
|
+
special compiler plugin is required. A usual Node TypeScript setup uses `module: "NodeNext"`.
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
14
|
npm install @kequtech/tenon-orm
|
|
@@ -18,71 +18,147 @@ npm install @kequtech/tenon-orm
|
|
|
18
18
|
and `close`. The required `connection` accepts:
|
|
19
19
|
|
|
20
20
|
- A PostgreSQL URI string.
|
|
21
|
-
- `
|
|
22
|
-
-
|
|
23
|
-
- An existing Postgres.js client, borrowed by identity.
|
|
21
|
+
- A pg `PoolConfig` object; an explicit `{}` uses the driver's environment defaults.
|
|
22
|
+
- An existing `pg.Pool`, borrowed by identity.
|
|
24
23
|
|
|
25
24
|
```ts
|
|
26
25
|
const db = createTenon({
|
|
27
26
|
connection: {
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
connectionString: connection,
|
|
28
|
+
max: 10,
|
|
29
|
+
idleTimeoutMillis: 30_000,
|
|
30
|
+
connectionTimeoutMillis: 30_000,
|
|
31
|
+
options: '-c search_path=app,public',
|
|
30
32
|
},
|
|
31
33
|
});
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
36
|
+
Strings become pg's `connectionString`; configuration objects pass to `new Pool(config)`.
|
|
37
|
+
`ConnectionOptions` is an alias for pg's `PoolConfig`. Driver options and defaults are unchanged,
|
|
38
|
+
including no acquisition timeout unless `connectionTimeoutMillis` is configured. Pool timeouts use
|
|
39
|
+
milliseconds. pg parses the URI for connection fields; matching URI values override client
|
|
40
|
+
configuration, including an `options` URI parameter overriding the configuration's `options` string.
|
|
41
|
+
The pool sizes and timeouts above are application choices. Configure pool policy in `PoolConfig`;
|
|
42
|
+
old driver-specific URI settings such as `idle_timeout` do not configure pg's idle timeout. Preserve
|
|
43
|
+
existing URI startup options when appending settings such as UTC and `search_path`.
|
|
44
|
+
Preserve deployment credentials and TLS requirements; see
|
|
45
|
+
[pg connection configuration](https://node-postgres.com/features/connecting).
|
|
46
|
+
|
|
47
|
+
Reuse an instance for each database configuration. `db.close({ timeoutMs? })` immediately rejects
|
|
48
|
+
new Tenon work. For owned pools, accepted Tenon operations can drain for up to 5,000 ms by default;
|
|
49
|
+
at the deadline, remaining operations reject and Tenon requests driver termination. `0` skips the
|
|
50
|
+
grace period. The timeout must be an integer from 0 to 2,147,483,647 milliseconds; `CloseOptions` is
|
|
51
|
+
exported. This bounds Tenon's wait, not every native transport's teardown or server execution. It
|
|
52
|
+
sends no PostgreSQL cancellation request and does not cancel JavaScript callbacks. An unbounded
|
|
53
|
+
authentication attempt can keep a connection open after close resolves. Configure pg's
|
|
54
|
+
`connectionTimeoutMillis` when connection establishment also needs a deadline.
|
|
55
|
+
|
|
56
|
+
The first valid close call chooses the timeout. Concurrent and later calls share the same promise
|
|
57
|
+
and outcome. Invalid initial options reject without closing the instance. For borrowed pools, close
|
|
58
|
+
only disables the Tenon wrapper; the application retains the pool and outstanding work. Tenon does
|
|
59
|
+
not load environment files.
|
|
60
|
+
|
|
61
|
+
Qualify SQL names or configure runtime `search_path`. CLI `--namespace` selects tooling scope; it
|
|
62
|
+
does not configure application queries. Baseline capture and Describe accept connection strings
|
|
63
|
+
rather than the runtime connection union.
|
|
57
64
|
|
|
58
65
|
## Native driver access
|
|
59
66
|
|
|
60
|
-
`db.native` is the actual
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
native preparation defaults and do not join a Tenon transaction; callback clients expose only
|
|
64
|
-
`tx.query`.
|
|
67
|
+
`db.native` is the actual `pg.Pool`, readonly in TypeScript. Native calls bypass Tenon's result
|
|
68
|
+
modes, affected-row guards, SQL checks, and `onQuery`. They follow pg's query configuration and do
|
|
69
|
+
not join a Tenon transaction; callback clients expose only `tx.query`.
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
Use pg's [native APIs](https://node-postgres.com/apis/pool) and compatible cursor/COPY packages when
|
|
72
|
+
needed. For LISTEN, check out a client with `await db.native.connect()`, attach `notification`,
|
|
73
|
+
`error`, and `end` listeners, and issue `LISTEN` on that client. Keep it checked out while listening;
|
|
74
|
+
release it after `UNLISTEN`. Native queued acquisitions follow pg's shutdown behavior and must be
|
|
75
|
+
settled by their caller before closing. A disconnected client does not reconnect. The application owns
|
|
76
|
+
resubscription and catch-up; notifications are not replayed.
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
owns catch-up and callback errors. That dedicated client installs its own close handler, so a
|
|
72
|
-
constructor `onclose` does not report its outages. Explicit LISTEN on a dedicated reserved native
|
|
73
|
-
connection can use constructor callbacks when disconnect detection is required. Subscription
|
|
74
|
-
cancellation follows the pinned driver; known reconnect limitations are recorded in
|
|
75
|
-
[maintainer considerations](https://github.com/kequtech/tenon-orm/blob/main/CONSIDERATIONS.md#native-driver-access-and-listen-lifecycle).
|
|
78
|
+
Close owned pools through `db.close()` for bounded shutdown, including checked-out native clients.
|
|
79
|
+
Borrowed pools and their listeners remain usable after wrapper close until their owner ends them.
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
### LISTEN lifecycle example
|
|
82
|
+
|
|
83
|
+
This helper owns a dedicated single-client pool. It returns an intentional-close function only
|
|
84
|
+
after LISTEN is ready. Callbacks are synchronous; notification exceptions are isolated, and an
|
|
85
|
+
unexpected disconnect invalidates this generation once before notifying its consumer. Failed
|
|
86
|
+
connection acquisition or LISTEN setup closes the owned resources and rejects the setup promise.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { createTenon } from '@kequtech/tenon-orm';
|
|
90
|
+
|
|
91
|
+
async function listenForChanges(
|
|
92
|
+
connectionString: string,
|
|
93
|
+
onChange: (payload: string | undefined) => void,
|
|
94
|
+
onDisconnect: (error: Error) => void,
|
|
95
|
+
) {
|
|
96
|
+
const db = createTenon({
|
|
97
|
+
connection: { connectionString, max: 1, idleTimeoutMillis: 0, connectionTimeoutMillis: 30_000 },
|
|
98
|
+
});
|
|
99
|
+
const client = await db.native.connect().catch(async (error: unknown) => {
|
|
100
|
+
await db.close();
|
|
101
|
+
throw error;
|
|
102
|
+
});
|
|
103
|
+
let active = true;
|
|
104
|
+
let closing: Promise<void> | undefined;
|
|
105
|
+
|
|
106
|
+
function stop(error?: Error): Promise<void> {
|
|
107
|
+
if (closing) return closing;
|
|
108
|
+
active = false;
|
|
109
|
+
closing = Promise.resolve().then(async () => {
|
|
110
|
+
try {
|
|
111
|
+
if (error) onDisconnect(error);
|
|
112
|
+
} finally {
|
|
113
|
+
client.release(true);
|
|
114
|
+
await db.close();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return closing;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const failed = (error: Error) => {
|
|
121
|
+
if (active) void stop(error).catch(console.error);
|
|
122
|
+
};
|
|
123
|
+
client.on('error', failed);
|
|
124
|
+
client.on('end', () => failed(new Error('The notification connection ended.')));
|
|
125
|
+
client.on('notification', (event) => {
|
|
126
|
+
if (!active || event.channel !== 'app_changes') return;
|
|
127
|
+
try {
|
|
128
|
+
onChange(event.payload);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
await client.query('LISTEN "app_changes"');
|
|
135
|
+
if (!active) throw new Error('The notification connection closed during setup.');
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await stop();
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
return () => stop();
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Both failure and intentional close discard the dedicated checkout with `release(true)` before
|
|
145
|
+
closing its pool, so no UNLISTEN is needed and normal shutdown does not wait for Tenon's deadline.
|
|
146
|
+
The active flag suppresses duplicate error/end callbacks and intentional-close notifications.
|
|
147
|
+
Create a new helper instance to recover, and reconcile missed events through persistent data.
|
|
148
|
+
|
|
149
|
+
This helper's close function is available after readiness. A manager that also supports replacement
|
|
150
|
+
or shutdown while setup is pending must assign a generation token before awaiting setup. Only the
|
|
151
|
+
current generation may change shared readiness/subscription state or notify its consumers. If stale
|
|
152
|
+
setup later succeeds, close that setup's own listener; if it fails or finishes cleanup, leave the
|
|
153
|
+
replacement alone. Invalidate the current token before intentional shutdown. Keep those guards in
|
|
154
|
+
the application's subscription manager. The fixed channel above is trusted; validate and quote
|
|
155
|
+
application-selected channel names before placing them in native SQL.
|
|
80
156
|
|
|
81
157
|
## Queries and result modes
|
|
82
158
|
|
|
83
159
|
`sql<Row>` creates a detached `SqlQuery<Row>` with SQL `text` and ordered `values`; it does not
|
|
84
|
-
execute. `Row` declares one selected row. Without an annotation, fields are `unknown`. An
|
|
85
|
-
|
|
160
|
+
execute. `Row` declares one selected row. Without an annotation, fields are `unknown`. An annotation
|
|
161
|
+
does not verify the SQL or validate returned data.
|
|
86
162
|
|
|
87
163
|
```ts
|
|
88
164
|
const user = await db.query(
|
|
@@ -94,27 +170,47 @@ const user = await db.query(
|
|
|
94
170
|
`query` options are `{ returns?, affectedRows?, label? }`; unknown keys fail. A supplied `label`
|
|
95
171
|
must be a nonempty string.
|
|
96
172
|
|
|
97
|
-
| `returns` | Output
|
|
98
|
-
| ------------------ |
|
|
99
|
-
| `native` (default) | `QueryResult<Row>` driver
|
|
100
|
-
| `many` | Plain `Row[]`
|
|
101
|
-
| `oneOrNone` | `Row \| undefined`
|
|
102
|
-
| `one` | `Row`
|
|
173
|
+
| `returns` | Output | Allowed returned rows |
|
|
174
|
+
| ------------------ | -------------------------------- | --------------------- |
|
|
175
|
+
| `native` (default) | `QueryResult<Row>` driver object | Any |
|
|
176
|
+
| `many` | Plain `Row[]` | Any |
|
|
177
|
+
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
178
|
+
| `one` | `Row` | Exactly one |
|
|
103
179
|
|
|
104
|
-
Native
|
|
105
|
-
the command's affected-row count, independent of `RETURNING`; it can be null for
|
|
106
|
-
a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch
|
|
107
|
-
undo an autocommitted write; use `affectedRows` for write bounds that roll back
|
|
180
|
+
Native results expose `rows`, `rowCount`, `command`, and `fields`, following pg's `QueryResult`.
|
|
181
|
+
`rowCount` is the command's affected-row count, independent of `RETURNING`; it can be null for
|
|
182
|
+
commands without a count. Result modes never add `LIMIT` or `RETURNING`. A returned-row mismatch
|
|
183
|
+
alone does not undo an autocommitted write; use `affectedRows` for write bounds that roll back.
|
|
108
184
|
|
|
109
185
|
Functions accepting `QueryClient` can use either `Database` or `Transaction`. Public types also
|
|
110
186
|
include `QueryOptions`, `QueryReturnMode`, and `QueryOutput<Row, Mode>`.
|
|
111
187
|
|
|
188
|
+
### Adapting transactional results
|
|
189
|
+
|
|
190
|
+
For a job library or other adapter expecting `{ rows }`, extract the native result's row array:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { unsafeSql, type QueryClient } from '@kequtech/tenon-orm';
|
|
194
|
+
|
|
195
|
+
async function queryRows(tx: QueryClient, text: string, values: readonly unknown[]) {
|
|
196
|
+
const result = await tx.query(unsafeSql(text, values));
|
|
197
|
+
return { rows: result.rows };
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Alternatively, request `{ returns: 'many' }` and wrap the returned array in `{ rows }`. Supply the
|
|
202
|
+
callback's `tx` when the adapter's work must commit with application writes. A native pool query
|
|
203
|
+
uses a separate checkout and does not join that transaction. Await adapter calls before returning
|
|
204
|
+
from the callback, retain commit/rollback tests, and make recording helpers return pg-shaped native
|
|
205
|
+
results. The adapter's SQL text must be trusted and values must remain separately bound.
|
|
206
|
+
|
|
112
207
|
## SQL parameters and identifiers
|
|
113
208
|
|
|
114
209
|
Ordinary `sql` interpolations bind values. Do not quote them or write manual `$n` placeholders
|
|
115
|
-
inside templates.
|
|
116
|
-
arrays
|
|
117
|
-
|
|
210
|
+
inside templates. Values follow pg serialization: `null` and `undefined` bind as SQL null, arrays
|
|
211
|
+
bind as PostgreSQL arrays, and objects use pg's JSON serialization. For JSON input, use
|
|
212
|
+
`JSON.stringify(value)`, including for JSON arrays, strings, and JSON null. Generated JSON write
|
|
213
|
+
types accept encoded strings. Cast standalone parameters when SQL must establish their type.
|
|
118
214
|
|
|
119
215
|
```ts
|
|
120
216
|
const ids = [1, 2, 3];
|
|
@@ -122,26 +218,27 @@ const query = sql<{ id: number }>`SELECT id FROM public.users
|
|
|
122
218
|
WHERE id = ANY(${ids}::integer[])`;
|
|
123
219
|
```
|
|
124
220
|
|
|
125
|
-
`identifier(schema, table)` quotes components separately. Names must contain 1–63 UTF-8 bytes and
|
|
126
|
-
|
|
127
|
-
|
|
221
|
+
`identifier(schema, table)` quotes components separately. Names must contain 1–63 UTF-8 bytes and no
|
|
222
|
+
NUL. Choose application-approved names; quoting does not authorize access. SQL syntax such as `DESC`
|
|
223
|
+
belongs in fixed query alternatives or fragments, not a string parameter.
|
|
128
224
|
|
|
129
225
|
`unsafeSql<Row>(text, values)` accepts trusted external SQL and an explicit values array, including
|
|
130
226
|
`[]`. Never concatenate request input into its text. Full `sql` and `unsafeSql` queries cannot be
|
|
131
227
|
nested; use fragments for partial SQL.
|
|
132
228
|
|
|
133
|
-
Each Tenon query executes one statement through the extended protocol. Application execution
|
|
134
|
-
|
|
135
|
-
|
|
229
|
+
Each Tenon query executes one statement through the extended protocol. Application execution uses
|
|
230
|
+
unnamed queries without a `name` or prepared-statement cache. Values remain bound separately.
|
|
231
|
+
Migrations deliberately support multi-statement scripts.
|
|
136
232
|
|
|
137
233
|
## SQL fragments and joins
|
|
138
234
|
|
|
139
235
|
`sql.fragment` creates immutable partial SQL with bound values. It has no row type, cannot execute
|
|
140
236
|
alone, and rejects blank SQL. Insert it into a complete `sql<Row>` query.
|
|
141
237
|
|
|
142
|
-
`sql.join(fragments, separator)` requires a nonempty list. Separators are `'AND'`, `'OR'`, and
|
|
143
|
-
It does not add parentheses or change SQL precedence. Each fragment use receives correctly
|
|
144
|
-
parameters, including repeated and nested uses; literal `$1` text in strings/comments is
|
|
238
|
+
`sql.join(fragments, separator)` requires a nonempty list. Separators are `'AND'`, `'OR'`, and
|
|
239
|
+
`','`. It does not add parentheses or change SQL precedence. Each fragment use receives correctly
|
|
240
|
+
ordered parameters, including repeated and nested uses; literal `$1` text in strings/comments is
|
|
241
|
+
unchanged.
|
|
145
242
|
|
|
146
243
|
```ts
|
|
147
244
|
const email: string | undefined = 'reader@example.test';
|
|
@@ -178,10 +275,10 @@ fragment construction; nested objects retain ordinary driver serialization behav
|
|
|
178
275
|
| SQL fragment, identifier, or query as a cell | Rejected | Rejected |
|
|
179
276
|
|
|
180
277
|
Inserts accept one object or an array. Without `columns`, they infer the union of enumerable keys
|
|
181
|
-
with defined values in first-seen order. Heterogeneous rows stay aligned. A single default-only
|
|
182
|
-
|
|
183
|
-
Assignments omit undefined values and fail when no assignments remain, even if surrounding SQL
|
|
184
|
-
|
|
278
|
+
with defined values in first-seen order. Heterogeneous rows stay aligned. A single default-only row
|
|
279
|
+
emits `DEFAULT VALUES`. Empty batches fail; multiple default-only rows need explicit columns.
|
|
280
|
+
Assignments omit undefined values and fail when no assignments remain, even if surrounding SQL has
|
|
281
|
+
other assignments.
|
|
185
282
|
|
|
186
283
|
A supplied column list must be nonempty, unique, and valid. It selects and orders fields, ignoring
|
|
187
284
|
unlisted keys. Missing insert cells use defaults; missing update fields are omitted. TypeScript
|
|
@@ -190,7 +287,7 @@ Helpers have no schema metadata; PostgreSQL enforces identity, generated-column,
|
|
|
190
287
|
rules. Use [generated inputs](#insert-and-update-inputs) and application validation.
|
|
191
288
|
|
|
192
289
|
Write arithmetic, `column = DEFAULT`, and `excluded.column` in the surrounding SQL. There is no
|
|
193
|
-
expression/default sentinel. A complete query accepts at most 65,
|
|
290
|
+
expression/default sentinel. A complete query accepts at most 65,535 bound parameters, including
|
|
194
291
|
surrounding interpolations; Tenon does not split batches. See the [mutation cookbook](mutations.md).
|
|
195
292
|
|
|
196
293
|
## Transactions, guarded writes, and errors
|
|
@@ -221,8 +318,8 @@ Sequence advances and external effects are not reversible. A lost commit acknowl
|
|
|
221
318
|
the outcome unknown; applications own retry and idempotency policy.
|
|
222
319
|
|
|
223
320
|
Set isolation before data queries with `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`, and use
|
|
224
|
-
`SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are
|
|
225
|
-
reserve names beginning with `tenon_affected_rows_` for Tenon.
|
|
321
|
+
`SET LOCAL statement_timeout = '5s'` to bound statements in that transaction. Manual savepoints are
|
|
322
|
+
supported; reserve names beginning with `tenon_affected_rows_` for Tenon.
|
|
226
323
|
|
|
227
324
|
SQL errors retain native driver details. Invalid inputs throw ordinary `Error` objects.
|
|
228
325
|
`CardinalityError` has code `TENON_CARDINALITY_MISMATCH`, `expected` (`one` or `oneOrNone`),
|
|
@@ -233,59 +330,134 @@ produce `AggregateError`.
|
|
|
233
330
|
|
|
234
331
|
Optional `onQuery` observes completed calls with `label`, `durationMs`, `outcome`, and available
|
|
235
332
|
`command`, `rowCount`, and `errorCode`. It receives no SQL or values. Validation failures before
|
|
236
|
-
execution are not observed. Observers are not awaited; failures emit warnings without changing
|
|
237
|
-
|
|
333
|
+
execution are not observed. Observers are not awaited; failures emit warnings without changing the
|
|
334
|
+
query outcome. Query success inside a callback does not establish outer commit.
|
|
238
335
|
|
|
239
336
|
## Generated types and checking
|
|
240
337
|
|
|
241
|
-
`generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript declarations
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
338
|
+
`generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript declarations from a
|
|
339
|
+
consistent read-only snapshot; it does not need migration files or history. The CLI writes them
|
|
340
|
+
atomically. Default namespace is `public`; default output is `src/generated-schema.ts`. Both `.ts`
|
|
341
|
+
and `.d.ts` outputs are supported. Commit exact output and exclude it from formatting.
|
|
245
342
|
|
|
246
343
|
```sh
|
|
247
344
|
npx tenon generate --out src/generated-schema.d.ts
|
|
248
345
|
npx tenon generate --out src/generated-schema.d.ts --check
|
|
249
346
|
```
|
|
250
347
|
|
|
251
|
-
`--check` never rewrites the file. Missing/stale output fails with a diagnostic and a short diff
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
348
|
+
`--check` never rewrites the file. Missing/stale output fails with a diagnostic and a short diff for
|
|
349
|
+
changes. It compares the generated representation, not pending migrations or every database object.
|
|
350
|
+
Output includes relation/constraint/index summaries and referenced domains, not function bodies,
|
|
351
|
+
grants, or a complete schema fingerprint.
|
|
352
|
+
|
|
353
|
+
**Generated declarations are not a version-stable schema fingerprint.** TypeScript mappings can
|
|
354
|
+
change without schema drift. The schema-summary comments describe SQL column types, defaults,
|
|
355
|
+
constraints, indexes, and referenced domains independently of those mappings, but their formatting
|
|
356
|
+
and PostgreSQL's rendering of definitions are not a versioned comparison contract either. Preserve
|
|
357
|
+
reviewed generator-specific references if an existing guard hashes this output; see
|
|
358
|
+
[baseline verification](adoption-guide.md#check-and-adopt-an-existing-database).
|
|
255
359
|
|
|
256
360
|
Generation covers ordinary tables and partitioned roots. Views, foreign tables, and attached
|
|
257
361
|
partitions need query-specific types. A missing namespace fails; an existing empty schema is valid.
|
|
258
362
|
Exact table names are available through `Rows['users']`; valid nonreserved identifiers also receive
|
|
259
363
|
named aliases. Use `Pick` for projections; a join or aggregate needs its actual selected shape.
|
|
260
364
|
|
|
261
|
-
| PostgreSQL type | Read representation | Generated write input
|
|
262
|
-
| ---------------------------------------------- | -------------------- |
|
|
263
|
-
| smallint, integer, oid, real, double precision | `number` | `number`
|
|
264
|
-
| bigint | `string` | `string \| bigint`
|
|
265
|
-
| numeric | `string` | `string \| number`
|
|
266
|
-
| boolean | `boolean` | `boolean`
|
|
267
|
-
| text, name, varchar, char, uuid, time, timetz | `string` | `string`
|
|
268
|
-
| date, timestamp, timestamptz | `Date`
|
|
269
|
-
| enum | String literal union | Same union; `never` for an empty enum
|
|
270
|
-
| json, jsonb | `unknown` | `
|
|
271
|
-
| bytea | `Buffer` | `Buffer`
|
|
272
|
-
|
|
273
|
-
Nullable columns add null; `unknown` already includes it. Domains retain verified base mappings
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
365
|
+
| PostgreSQL type | Read representation | Generated write input |
|
|
366
|
+
| ---------------------------------------------- | -------------------- | ------------------------------------- |
|
|
367
|
+
| smallint, integer, oid, real, double precision | `number` | `number` |
|
|
368
|
+
| bigint | `string` | `string \| bigint` |
|
|
369
|
+
| numeric | `string` | `string \| number` |
|
|
370
|
+
| boolean | `boolean` | `boolean` |
|
|
371
|
+
| text, name, varchar, char, uuid, time, timetz | `string` | `string` |
|
|
372
|
+
| date, timestamp, timestamptz | `Date \| number` | `Date \| string` |
|
|
373
|
+
| enum | String literal union | Same union; `never` for an empty enum |
|
|
374
|
+
| json, jsonb | `unknown` | `string` containing encoded JSON |
|
|
375
|
+
| bytea | `Buffer` | `Buffer` |
|
|
376
|
+
|
|
377
|
+
Nullable columns add null; `unknown` already includes it. Domains retain verified base mappings and
|
|
378
|
+
the column's read nullability. Verified arrays use `PgArray<T> = (T | null | PgArray<T>)[]` for null
|
|
379
|
+
elements and multiple dimensions. Numeric arrays contain numbers and can lose precision; select/cast
|
|
380
|
+
them as text when exact values are required. Bigint arrays contain strings; enum arrays and arrays
|
|
381
|
+
of domains are unverified. Unmapped types warn and use `unknown` reads / `never` non-null writes.
|
|
277
382
|
Custom parsers need matching application types.
|
|
278
383
|
|
|
279
|
-
Date-only
|
|
280
|
-
|
|
281
|
-
|
|
384
|
+
Date-only values and timestamps without time zone use Node's local timezone. Infinite temporal reads
|
|
385
|
+
are numbers (`Infinity` or `-Infinity`); narrow with `value instanceof Date` before date methods.
|
|
386
|
+
Temporal write inputs accept `Date | string`; use PostgreSQL's text `infinity` or `-infinity` when
|
|
387
|
+
needed.
|
|
282
388
|
|
|
283
389
|
### Persistence and domain values
|
|
284
390
|
|
|
285
|
-
Generated types describe storage representations. Text `CHECK` constraints do not become unions,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
391
|
+
Generated types describe storage representations. Text `CHECK` constraints do not become unions, and
|
|
392
|
+
JSON reads remain unknown. Validate values before narrowing to application domain types.
|
|
393
|
+
|
|
394
|
+
For example, assume this additional table and regenerate `Rows` from the resulting schema:
|
|
395
|
+
|
|
396
|
+
```sql
|
|
397
|
+
CREATE TABLE public.sessions (
|
|
398
|
+
id text PRIMARY KEY,
|
|
399
|
+
data jsonb NOT NULL,
|
|
400
|
+
expires_at timestamptz NOT NULL
|
|
401
|
+
);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Use the same application validator before encoding a write and when restoring a domain value:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import { sql } from '@kequtech/tenon-orm';
|
|
408
|
+
import type { Rows } from './generated-schema.js';
|
|
409
|
+
|
|
410
|
+
function sessionData(value: unknown): { userId: string } {
|
|
411
|
+
if (
|
|
412
|
+
typeof value !== 'object' ||
|
|
413
|
+
value === null ||
|
|
414
|
+
Array.isArray(value) ||
|
|
415
|
+
!('userId' in value) ||
|
|
416
|
+
typeof value.userId !== 'string' ||
|
|
417
|
+
value.userId.length === 0
|
|
418
|
+
) {
|
|
419
|
+
throw new Error('Expected session data with a nonempty userId');
|
|
420
|
+
}
|
|
421
|
+
return { userId: value.userId };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function finiteDate(value: Date | number): Date {
|
|
425
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
|
|
426
|
+
throw new Error('Expected a finite database timestamp');
|
|
427
|
+
}
|
|
428
|
+
return value;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const input = {
|
|
432
|
+
id: 'example-session',
|
|
433
|
+
data: JSON.stringify(sessionData({ userId: 'user-42' })),
|
|
434
|
+
expires_at: new Date(Date.now() + 60_000),
|
|
435
|
+
} satisfies Rows.Insert['sessions'];
|
|
436
|
+
await db.query(sql`INSERT INTO public.sessions ${sql.insertValues(input)}`);
|
|
437
|
+
|
|
438
|
+
const row = await db.query(
|
|
439
|
+
sql<Rows['sessions']>`SELECT id, data, expires_at FROM public.sessions
|
|
440
|
+
WHERE id = ${input.id} AND isfinite(expires_at) AND expires_at > now()`,
|
|
441
|
+
{ returns: 'oneOrNone' },
|
|
442
|
+
);
|
|
443
|
+
const session =
|
|
444
|
+
row === undefined
|
|
445
|
+
? undefined
|
|
446
|
+
: {
|
|
447
|
+
id: row.id,
|
|
448
|
+
data: sessionData(row.data),
|
|
449
|
+
expiresAt: finiteDate(row.expires_at),
|
|
450
|
+
};
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The generated JSON read remains `unknown` and the timestamp remains `Date | number`. The projection
|
|
454
|
+
validates both without assertions or edits to generated types. Preserve null separately for nullable
|
|
455
|
+
columns. For an authentication policy requiring finite expiry, `isfinite(expires_at)` excludes both
|
|
456
|
+
infinities before the expiry comparison; `expires_at > now()` alone would accept positive infinity.
|
|
457
|
+
Include such predicates in expiry-sensitive updates/deletes too, and test infinite and expired
|
|
458
|
+
values. This example illustrates representation validation; applications must also retain their
|
|
459
|
+
token verification, revocation, and authorization checks. See
|
|
460
|
+
[JSON and PostgreSQL arrays](mutations.md#json-and-postgresql-arrays) for array write boundaries.
|
|
289
461
|
|
|
290
462
|
Bigint and `count(*)` reads are decimal strings. Use SQL or BigInt for exact arithmetic and decimal
|
|
291
463
|
strings at JSON boundaries; convert to number only when bounded or approximate values are intended.
|
|
@@ -303,14 +475,14 @@ sql.assignments(patch);
|
|
|
303
475
|
|
|
304
476
|
Insert fields are required when nonnullable, writable, and without a default. Other writable insert
|
|
305
477
|
fields and all writable update fields are optional and accept explicit undefined, including with
|
|
306
|
-
`exactOptionalPropertyTypes`. Explicit null requires both column and domain-chain permission.
|
|
307
|
-
|
|
478
|
+
`exactOptionalPropertyTypes`. Explicit null requires both column and domain-chain permission. Domain
|
|
479
|
+
defaults count; column defaults take precedence.
|
|
308
480
|
|
|
309
481
|
Computed and `GENERATED ALWAYS` identity columns use optional `never` fields. Serial and
|
|
310
|
-
`GENERATED BY DEFAULT` identities are optional on insert and writable on update; other primary
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
482
|
+
`GENERATED BY DEFAULT` identities are optional on insert and writable on update; other primary keys
|
|
483
|
+
remain writable. Use `Pick`/`Omit` for application restrictions. Apply the generated types through
|
|
484
|
+
annotations, `satisfies`, or helper generic arguments; they do not validate constraints, JSON
|
|
485
|
+
structure, authorization, or nonempty input.
|
|
314
486
|
|
|
315
487
|
For unverified write mappings, provide a reviewed application-owned replacement, for example
|
|
316
488
|
`Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for interval text input. Selected columns
|
|
@@ -319,19 +491,20 @@ can discard required fields; see [the cookbook](mutations.md#input-types-and-run
|
|
|
319
491
|
## Query inspection
|
|
320
492
|
|
|
321
493
|
`describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL URI and a finished
|
|
322
|
-
SQL string or `{ text }` object. It returns PostgreSQL metadata, conservative read mappings and
|
|
323
|
-
|
|
324
|
-
|
|
494
|
+
SQL string or `{ text }` object. It returns PostgreSQL metadata, conservative read mappings and row
|
|
495
|
+
declaration, session context, and coded findings. It does not bind values, execute the statement, or
|
|
496
|
+
verify `sql<Row>`.
|
|
325
497
|
|
|
326
498
|
Inspection owns a separate session with bounded timeouts. Match runtime settings; temporary tables,
|
|
327
|
-
uncommitted changes
|
|
499
|
+
and uncommitted changes are not inherited. Reported mappings assume pg's default parsers; parser
|
|
500
|
+
overrides need matching application types. Nullability stays unknown. See
|
|
328
501
|
[query inspection](query-inspection.md) for settings, result fields, issue codes, and testing.
|
|
329
502
|
|
|
330
503
|
## Command reference
|
|
331
504
|
|
|
332
|
-
Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files
|
|
333
|
-
|
|
334
|
-
|
|
505
|
+
Database commands require `DATABASE_URL`. `new` and `--help` work offline. No environment files are
|
|
506
|
+
loaded. Paths are relative to the working directory. Explicit flags unsupported by a command are
|
|
507
|
+
rejected; `--check` is only a generation check, not a migration dry run.
|
|
335
508
|
|
|
336
509
|
```text
|
|
337
510
|
tenon new <name> [--directory migrations]
|
|
@@ -356,10 +529,10 @@ file has down SQL before executing any; `steps` is a positive safe integer. Use
|
|
|
356
529
|
function bodies and explicit `E'...'` backslash escapes. Nontransactional DDL such as
|
|
357
530
|
`CREATE INDEX CONCURRENTLY` requires a separate procedure.
|
|
358
531
|
|
|
359
|
-
Migration/status/rollback/adoption use session advisory locks per database and namespace;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
532
|
+
Migration/status/rollback/adoption use session advisory locks per database and namespace; generation
|
|
533
|
+
uses the same lock in shared mode. Use direct or session-pooled connections and server timeouts to
|
|
534
|
+
bound waits. Baseline capture uses a local creation lock and native `pg_dump`; it does not acquire
|
|
535
|
+
the runner's database advisory lock. See [adoption](adoption-guide.md).
|
|
363
536
|
|
|
364
537
|
Programmatic `migrate`, `migrationStatus`, and `rollback` accept
|
|
365
538
|
`{ connection, directory?, namespace? }`; rollback also accepts `steps`. Migration/rollback return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kequtech/tenon-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A PostgreSQL ORM designed for coding agents, with explicit SQL, typed queries, and migrations",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"benchmark": "npm run build && node scripts/benchmark.mjs"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"
|
|
57
|
+
"pg": "8.23.1",
|
|
58
|
+
"@types/pg": "8.23.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@types/node": "^24.19.2",
|
package/dist/driver/array.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type postgres from 'postgres';
|
|
2
|
-
/** Preserve driver scalar parsers and serializers; replace only lossy array parsing. */
|
|
3
|
-
export declare function prepareArrays(sql: postgres.Sql): Promise<void>;
|
|
4
|
-
/** PostgreSQL text arrays: only unquoted NULL is null; quoted values use the scalar parser. */
|
|
5
|
-
export declare function parseArray(text: string, parse: (value: string) => unknown, delimiter?: string): unknown[];
|