@kequtech/tenon-orm 0.2.1 → 0.3.0
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 +52 -36
- package/README.md +27 -70
- 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 -16
- package/dist/connection.js +124 -14
- package/dist/database.js +36 -17
- package/dist/driver/types.d.ts +1 -1
- package/dist/driver/types.js +11 -11
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -0
- 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 +2 -3
- package/dist/index.js +1 -1
- package/dist/inspection/catalog.d.ts +4 -3
- package/dist/inspection/catalog.js +13 -10
- package/dist/inspection/cli.js +3 -3
- package/dist/inspection/connection.d.ts +2 -2
- package/dist/inspection/connection.js +22 -22
- package/dist/inspection/options.d.ts +3 -3
- package/dist/inspection/options.js +9 -1
- 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 +10 -10
- 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 +5 -5
- package/dist/query.js +2 -2
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +19 -14
- package/dist/types.d.ts +7 -3
- package/docs/adoption-guide.md +109 -122
- package/docs/agent-guide.md +124 -228
- package/docs/human-guide.md +44 -55
- package/docs/mutations.md +58 -76
- package/docs/query-inspection.md +97 -89
- package/docs/reference.md +264 -319
- package/package.json +4 -4
- 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/session.d.ts +0 -8
- package/dist/driver/session.js +0 -92
package/docs/agent-guide.md
CHANGED
|
@@ -1,35 +1,27 @@
|
|
|
1
1
|
# Tenon agent guide
|
|
2
2
|
|
|
3
|
-
Tenon executes PostgreSQL SQL through
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
## Install and connect
|
|
26
|
-
|
|
27
|
-
Requires Node.js 24+, PostgreSQL 16+, ESM, TypeScript 6+, and Node development typings. Keep the
|
|
28
|
-
application's strict TypeScript configuration.
|
|
29
|
-
|
|
30
|
-
```sh
|
|
31
|
-
npm install @kequtech/tenon-orm
|
|
32
|
-
```
|
|
3
|
+
Tenon executes explicit PostgreSQL SQL through pg. Use ordinary functions for reusable queries. This
|
|
4
|
+
guide covers routine implementation; the [reference](reference.md) defines the full API and the
|
|
5
|
+
[human guide](human-guide.md) gives a step-by-step introduction.
|
|
6
|
+
|
|
7
|
+
## Start with the existing project
|
|
8
|
+
|
|
9
|
+
Inspect its connection configuration, migration runner, generated types, query functions, and check
|
|
10
|
+
commands. Preserve those conventions. Tenon queries and type generation do not require Tenon
|
|
11
|
+
migration history. Keep input validation, authorization, domain conversion, and retry policy in the
|
|
12
|
+
application.
|
|
13
|
+
|
|
14
|
+
For an upgrade, record the previous resolved Tenon version from the lockfile or installed package
|
|
15
|
+
and read the [Changelog](../CHANGELOG.md) entries through the target version before changing code.
|
|
16
|
+
It ships with the package alongside this guide. Follow its upgrade instructions, regenerate types
|
|
17
|
+
when mappings change, and run the project's typechecks and relevant database tests. Use the
|
|
18
|
+
[adoption guide](adoption-guide.md) when replacing another ORM or transferring migration ownership.
|
|
19
|
+
|
|
20
|
+
## Connect
|
|
21
|
+
|
|
22
|
+
Requires Node.js 24+, PostgreSQL 16+, ESM, TypeScript 6+, and Node development typings. Install with
|
|
23
|
+
`npm install @kequtech/tenon-orm`; retain the project's package manager and TypeScript
|
|
24
|
+
configuration.
|
|
33
25
|
|
|
34
26
|
```ts
|
|
35
27
|
import { createTenon } from '@kequtech/tenon-orm';
|
|
@@ -39,19 +31,15 @@ if (!connection) throw new Error('DATABASE_URL is required');
|
|
|
39
31
|
export const db = createTenon({ connection });
|
|
40
32
|
```
|
|
41
33
|
|
|
42
|
-
`connection` accepts a
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
installation. It is the existing pool-level client. Native calls bypass Tenon's guards and `onQuery`,
|
|
50
|
-
and do not join a Tenon transaction; transaction callbacks expose only `tx.query`. `db.close()` ends
|
|
51
|
-
owned clients and their listeners; borrowed clients stay application-owned. Follow the
|
|
52
|
-
[native access contract](reference.md#native-driver-access) for initialization and shutdown.
|
|
34
|
+
`connection` accepts a URI string, pg `PoolConfig` (including `connectionString`), or an existing
|
|
35
|
+
`pg.Pool`. Reuse an instance for each database configuration. `db.close()` ends internally created
|
|
36
|
+
pools with a default 5,000 ms grace period; supplied pools remain application-owned. Configure
|
|
37
|
+
shutdown with `db.close({ timeoutMs })`; concurrent callers share its outcome. Finish outstanding
|
|
38
|
+
work before closing. Tenon loads no environment files. Qualify table names or set runtime
|
|
39
|
+
`search_path`; CLI `--namespace` selects tooling scope, not runtime query names. See
|
|
40
|
+
[connection options](reference.md#installation-and-connection).
|
|
53
41
|
|
|
54
|
-
## Query contracts
|
|
42
|
+
## Query and result contracts
|
|
55
43
|
|
|
56
44
|
```ts
|
|
57
45
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
@@ -59,211 +47,119 @@ import type { Rows } from './generated-schema.js';
|
|
|
59
47
|
|
|
60
48
|
type Summary = Pick<Rows['users'], 'id' | 'email'>;
|
|
61
49
|
|
|
62
|
-
function
|
|
63
|
-
return sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}
|
|
50
|
+
function userById(client: QueryClient, { id }: { id: number }) {
|
|
51
|
+
return client.query(sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`, {
|
|
52
|
+
returns: 'oneOrNone',
|
|
53
|
+
});
|
|
64
54
|
}
|
|
65
|
-
|
|
66
|
-
function userById(client: QueryClient, input: { id: number }) {
|
|
67
|
-
return client.query(userByIdQuery(input), { returns: 'oneOrNone' });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const user = await userById(db, { id: 42 });
|
|
71
|
-
// In a transaction, call userById(tx, { id: 42 }).
|
|
72
55
|
```
|
|
73
56
|
|
|
74
|
-
`
|
|
75
|
-
|
|
76
|
-
|
|
57
|
+
`QueryClient` accepts either `db` or a transaction's `tx`. Keep SQL and execution options together.
|
|
58
|
+
`sql<Row>` declares one selected row; it does not verify SQL or validate data. Unannotated fields
|
|
59
|
+
are `unknown`. Generated table types can describe full rows or `Pick` projections; joins and
|
|
60
|
+
aggregates need types for their actual output.
|
|
77
61
|
|
|
78
|
-
| `returns` | Result
|
|
79
|
-
| ------------------ |
|
|
80
|
-
| `native` (default) |
|
|
81
|
-
| `many` | `Row[]`
|
|
82
|
-
| `oneOrNone` | `Row \| undefined`
|
|
83
|
-
| `one` | `Row`
|
|
62
|
+
| `returns` | Result | Required returned rows |
|
|
63
|
+
| ------------------ | -------------------------------------------------- | ---------------------- |
|
|
64
|
+
| `native` (default) | Driver object with `.rows`, `.rowCount`, `.fields` | Any |
|
|
65
|
+
| `many` | Plain `Row[]` | Any |
|
|
66
|
+
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
67
|
+
| `one` | `Row` | Exactly one |
|
|
84
68
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
and `.columns`; `many` returns a plain row array.
|
|
89
|
-
|
|
90
|
-
The Postgres.js driver is included. Undefined bound parameters fail. Pass JSON values directly,
|
|
91
|
-
including arrays, rather than calling `JSON.stringify`. Numeric and bigint scalars and arrays read
|
|
92
|
-
as strings. Dates read as `Date`, including invalid dates for timestamp infinities; check validity
|
|
93
|
-
before using them. SQL NULL array elements read as JavaScript null.
|
|
69
|
+
Options are `{ returns?, affectedRows?, label? }`; unknown keys fail. Modes do not add SQL limits or
|
|
70
|
+
`RETURNING`. A returned-row mismatch throws `CardinalityError` with code
|
|
71
|
+
`TENON_CARDINALITY_MISMATCH`; it alone does not undo an autocommitted write.
|
|
94
72
|
|
|
95
73
|
## Parameters and composition
|
|
96
74
|
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
`SELECT ${value}::
|
|
101
|
-
-
|
|
102
|
-
values.
|
|
103
|
-
- `
|
|
104
|
-
|
|
105
|
-
- `sql
|
|
106
|
-
|
|
107
|
-
- `sql.join(fragments, separator)` combines a variable-length list of fragments using `'AND'`,
|
|
108
|
-
`'OR'`, or `','`. Use it for optional filters, search terms, or lists of SQL expressions. The list
|
|
109
|
-
must be nonempty; group mixed `AND`/`OR` predicates with parentheses.
|
|
110
|
-
- `unsafeSql<Row>(text, values)` accepts trusted external SQL with an explicit values array. Never
|
|
111
|
-
concatenate request input into its text. Each query executes one statement.
|
|
112
|
-
|
|
113
|
-
For example, share a search condition between a page and count query:
|
|
75
|
+
- Interpolate values without quotes. Do not author `$n` placeholders inside templates. `undefined`
|
|
76
|
+
and `null` bind as SQL null, following pg.
|
|
77
|
+
- Use `id = ANY(${ids}::integer[])` for a set of IDs. Cast standalone parameters such as
|
|
78
|
+
`SELECT ${value}::integer AS value` when SQL must establish the type for inspection.
|
|
79
|
+
- `identifier(schema, table)` quotes separate approved names. Strings such as `'DESC'` remain bound
|
|
80
|
+
values; choose SQL syntax through fixed query alternatives or fragments.
|
|
81
|
+
- `sql.fragment` creates partial SQL with bound values; `sql.join(parts, 'AND' | 'OR' | ',')`
|
|
82
|
+
combines a nonempty list. Blank fragments fail. Group mixed conditions with parentheses.
|
|
83
|
+
- Full `sql` or `unsafeSql` queries cannot be nested. `unsafeSql<Row>(text, values)` is for trusted
|
|
84
|
+
external SQL with an explicit values array. Each Tenon query executes one statement.
|
|
114
85
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const page = sql<Summary>`SELECT id, email FROM public.users
|
|
118
|
-
WHERE ${search} ORDER BY id LIMIT ${limit}`;
|
|
119
|
-
const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
|
|
120
|
-
WHERE ${search}`;
|
|
121
|
-
```
|
|
86
|
+
Use fragments for actual reuse or variable composition. Keep fixed structure in SQL, and retain
|
|
87
|
+
mandatory authorization predicates when optional filters are absent.
|
|
122
88
|
|
|
123
|
-
|
|
124
|
-
[composition reference](reference.md#sql-fragments-and-joins) for grouping and list examples.
|
|
89
|
+
## Writes and generated values
|
|
125
90
|
|
|
126
|
-
|
|
91
|
+
`sql.insertValues(rowOrRows, columns?)` builds column/value fragments;
|
|
92
|
+
`sql.assignments(patch, columns?)` builds assignments. Missing/undefined insert fields use defaults;
|
|
93
|
+
update fields are omitted. Explicit null stays null. Empty batches and empty assignments fail.
|
|
94
|
+
Selected columns ignore unlisted fields and can discard required inputs. Put expressions, conflict
|
|
95
|
+
policy, and `RETURNING` in the surrounding SQL. Batch in the caller below the query-wide
|
|
96
|
+
65,535-parameter limit.
|
|
127
97
|
|
|
128
|
-
Use
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
PostgreSQL defaults; update missing/undefined fields are omitted. Explicit null stays null. Empty
|
|
132
|
-
batches/updates throw; a single default-only insert works. Omit generated columns and batch large
|
|
133
|
-
writes explicitly below the query-wide limit of 65,533 parameters. Expressions and queries are
|
|
134
|
-
rejected as cell values; write arithmetic, `DEFAULT`, and `excluded.column` in the surrounding SQL.
|
|
98
|
+
Use generated `Rows.Insert['users']` and `Rows.Update['users']` inputs; narrow editable fields with
|
|
99
|
+
`Pick`. These types account for defaults, writable columns, domains, and driver representations.
|
|
100
|
+
They do not validate constraints or JSON contents. See [mutation examples](mutations.md).
|
|
135
101
|
|
|
136
|
-
Use
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
102
|
+
Use `JSON.stringify(value)` for JSON write inputs; arrays otherwise bind as PostgreSQL arrays.
|
|
103
|
+
Bigint and numeric scalars read as strings, while numeric arrays read as numbers and can lose
|
|
104
|
+
precision. Temporal reads use `Date | number` for finite dates and infinities. SQL NULL array
|
|
105
|
+
elements remain JavaScript null. Keep domain validation at the persistence boundary. See
|
|
106
|
+
[value mappings](reference.md#generated-types-and-checking).
|
|
140
107
|
|
|
141
|
-
|
|
142
|
-
fields are ignored. See the [mutation contract](reference.md#object-based-mutation-fragments) and
|
|
143
|
-
[cookbook](mutations.md).
|
|
108
|
+
## Transactions and native access
|
|
144
109
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
Inspection ships with Tenon and needs no additional setup:
|
|
110
|
+
For an existing user with id 42:
|
|
148
111
|
|
|
149
112
|
```ts
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Pass the actual query object that will go to `db.query`, including fragments and conditional
|
|
157
|
-
predicates. A small ordinary query-building function can make the same statement available to
|
|
158
|
-
inspection and execution tests. Keep authorization policy inside the builder or shared fragments.
|
|
159
|
-
|
|
160
|
-
For a SQL file with PostgreSQL `$n` placeholders:
|
|
161
|
-
|
|
162
|
-
```sh
|
|
163
|
-
npx tenon describe query.sql
|
|
113
|
+
await db.transaction(async (tx) => {
|
|
114
|
+
await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${42}`, {
|
|
115
|
+
affectedRows: { exact: 1 },
|
|
116
|
+
});
|
|
117
|
+
});
|
|
164
118
|
```
|
|
165
119
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
execution
|
|
195
|
-
|
|
196
|
-
Run the same query through the real application connection against its test database. Assert full
|
|
197
|
-
relevant rows/effects and exercise meaningful branches: ownership, optional filters, absent joins,
|
|
198
|
-
empty aggregates, pagination ties, JSON validation, and failed/replayed/rolled-back writes as
|
|
199
|
-
applicable. Keep these tests alongside the query; describing a single instance covers only that
|
|
200
|
-
instance. See [query inspection](query-inspection.md) for session options, limits, and issue codes.
|
|
201
|
-
|
|
202
|
-
## Transactions and writes
|
|
203
|
-
|
|
204
|
-
Use `db.transaction(async (tx) => ...)`; use `tx` for every query in scope and await all work. The
|
|
205
|
-
callback commits on success and rolls back on an escaping error. Do not issue direct
|
|
206
|
-
BEGIN/COMMIT/ROLLBACK boundaries or use `tx` after return. Transactions do not span database
|
|
207
|
-
instances. Nested callback transactions and automatic retries are not provided.
|
|
208
|
-
|
|
209
|
-
Use `affectedRows: { exact: n }` or `{ max: n }` for INSERT/UPDATE/DELETE/MERGE when a write must
|
|
210
|
-
respect a count. Bounds are nonnegative safe integers. A guarded `db.query` uses a transaction; a
|
|
211
|
-
guarded `tx.query` uses a savepoint. Rejected writes are rolled back before throwing. A caught
|
|
212
|
-
guarded failure permits continued work; ordinary SQL failures may abort the transaction. Reserve
|
|
213
|
-
savepoint names beginning with `tenon_affected_rows_`.
|
|
214
|
-
|
|
215
|
-
Affected counts cover the final command, not extra trigger/cascade/CTE effects. They cannot detect
|
|
216
|
-
the wrong single row. Sequence advances and external effects are not reversible. A lost commit
|
|
217
|
-
acknowledgment may leave the outcome unknown; retry only according to application policy. Native Postgres.js
|
|
218
|
-
errors retain their details; `AffectedRowsError` has codes `TENON_AFFECTED_ROWS_MISMATCH` and
|
|
219
|
-
`TENON_AFFECTED_ROWS_UNSUPPORTED`. Cleanup failures can produce `AggregateError`.
|
|
220
|
-
|
|
221
|
-
Optional `onQuery` observes labels, duration, outcome, and available command/count/ error code,
|
|
222
|
-
without SQL or values. Observers are not awaited. Query success inside a callback does not mean the
|
|
223
|
-
outer transaction committed.
|
|
120
|
+
Use `tx` for every operation in the callback and await the work. Tenon commits on success and rolls
|
|
121
|
+
back on an escaping error; the callback client expires when the callback finishes. Direct
|
|
122
|
+
BEGIN/COMMIT/ROLLBACK boundaries, nested callback transactions, and automatic retries are not
|
|
123
|
+
provided. Transactions belong to one database instance.
|
|
124
|
+
|
|
125
|
+
`affectedRows: { exact: n }` or `{ max: n }` checks the final INSERT/UPDATE/DELETE/MERGE count.
|
|
126
|
+
Bounds are nonnegative safe integers. A guarded `db.query` uses a transaction; a guarded `tx.query`
|
|
127
|
+
uses a savepoint. Rejection rolls back that operation. A caught guarded failure permits continued
|
|
128
|
+
work; an unguarded SQL error normally aborts the transaction. Counts exclude extra trigger/cascade
|
|
129
|
+
and CTE effects. Reserve `tenon_affected_rows_` savepoint names for Tenon. A lost commit
|
|
130
|
+
acknowledgment can leave the outcome unknown.
|
|
131
|
+
|
|
132
|
+
`db.native` exposes the existing pool for driver features. Native calls bypass Tenon's guards and
|
|
133
|
+
`onQuery`; they do not join a Tenon transaction. Close owned pools through `db.close()`. Follow the
|
|
134
|
+
[native access contract](reference.md#native-driver-access) for resource ownership and LISTEN.
|
|
135
|
+
Optional `onQuery` reports completion metadata without SQL or values; success inside a callback does
|
|
136
|
+
not establish that the outer transaction committed.
|
|
137
|
+
|
|
138
|
+
## Inspect and validate
|
|
139
|
+
|
|
140
|
+
Use `describeQuery({ connection, query })` on completed SQL or `tenon describe query.sql` on a file.
|
|
141
|
+
Inspection owns a separate connection. Match the runtime role, search path, and timezone. Temporary
|
|
142
|
+
tables and transaction-local state are not inherited; reported mappings assume pg's default parsers.
|
|
143
|
+
Result nullability is unknown and JSON structure is unchecked. A successful description does not
|
|
144
|
+
verify `sql<Row>`, bound values, execution permission, or effects.
|
|
145
|
+
|
|
146
|
+
Pair inspection with database assertions for complete relevant rows, meaningful query variants, and
|
|
147
|
+
rollback. A small query-building function can share the same statement with inspection and
|
|
148
|
+
execution; keep simple queries inline when that is sufficient. See
|
|
149
|
+
[query inspection](query-inspection.md).
|
|
224
150
|
|
|
225
151
|
## Schema changes
|
|
226
152
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# Fill the generated up/down SQL before migrating.
|
|
241
|
-
npm run db:migrate
|
|
242
|
-
npx tenon generate --out src/generated-schema.d.ts --check
|
|
243
|
-
npx tenon status
|
|
244
|
-
npx tenon rollback --steps 1
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
Commit migrations and generated types together. Production runs only `npx tenon migrate` and deploys
|
|
248
|
-
the already-built application. Omit generation from the development script if table types are
|
|
249
|
-
unused.
|
|
250
|
-
|
|
251
|
-
Database commands require `DATABASE_URL`. Migration commands accept `--directory` (default
|
|
252
|
-
`migrations`) and `--namespace` (default `public`); generation accepts `--namespace`. Files use
|
|
253
|
-
`<numeric-id>_<name>.up.sql` with optional matching down SQL. Omit a down file for an intentionally
|
|
254
|
-
irreversible migration. Never edit applied files. For a custom namespace, pass it to both commands
|
|
255
|
-
in the development script.
|
|
256
|
-
|
|
257
|
-
Each migration commits with its history entry; earlier files remain applied if a later one fails.
|
|
258
|
-
Tools use session advisory locks: use a direct connection or session pooling and application
|
|
259
|
-
timeouts. Nontransactional DDL needs a separate procedure. `generate --check` detects representation
|
|
260
|
-
drift, not pending migrations or every possible schema change. Generation covers ordinary tables and
|
|
261
|
-
partitioned roots; views/foreign tables need authored query types.
|
|
262
|
-
|
|
263
|
-
When replacing another ORM, preserve its schema and behavior. If transferring migration ownership,
|
|
264
|
-
capture one reviewed baseline with `tenon baseline capture`, record it only on matching existing
|
|
265
|
-
databases with `tenon baseline adopt`, and use `tenon migrate` on fresh databases. Adoption does not
|
|
266
|
-
compare schemas or run baseline SQL. Fresh replay omits ownership and grants; apply deployment
|
|
267
|
-
security provisioning before application access, especially for `SECURITY DEFINER` functions.
|
|
268
|
-
See [adoption](adoption-guide.md) for the full transition procedure. A down migration or schema-only
|
|
269
|
-
baseline is not a data backup.
|
|
153
|
+
With Tenon's runner, development can use
|
|
154
|
+
`tenon migrate && tenon generate --out src/generated-schema.d.ts`. Commit migrations and exact
|
|
155
|
+
generated output together; exclude the generated file from automatic formatting. Production runs
|
|
156
|
+
migrations and deploys committed types. `generate --check` detects representation drift, not pending
|
|
157
|
+
migrations or every schema change.
|
|
158
|
+
|
|
159
|
+
All database CLI commands require `DATABASE_URL`. `tenon new <name>` creates empty up/down files
|
|
160
|
+
offline; fill them before migrating. Migration commands use `--directory migrations` and
|
|
161
|
+
`--namespace public` by default; generation also accepts `--namespace`. Pass a custom namespace to
|
|
162
|
+
both migration and generation. Omit the down file for an irreversible migration; never edit applied
|
|
163
|
+
history. Each file commits independently with its history entry. Tools use session advisory locks,
|
|
164
|
+
so use a direct or session-pooled connection. Nontransactional DDL needs a separate procedure. See
|
|
165
|
+
the [command reference](reference.md#command-reference).
|
package/docs/human-guide.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Using Tenon: a human guide
|
|
2
2
|
|
|
3
3
|
Tenon lets you use PostgreSQL directly from TypeScript. You write SQL; Tenon binds its inputs and
|
|
4
|
-
executes it through
|
|
4
|
+
executes it through pg. This guide follows a small application from its first table to reusable
|
|
5
5
|
queries, transactions, and result checking.
|
|
6
6
|
|
|
7
|
-
Use the [reference](reference.md)
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Use the [reference](reference.md) for exact contracts and [adoption](adoption-guide.md) when
|
|
8
|
+
replacing an existing ORM. Code blocks build on the connection and table introduced below;
|
|
9
|
+
query-function examples belong in their own module.
|
|
10
10
|
|
|
11
11
|
## Connect to your database
|
|
12
12
|
|
|
@@ -29,15 +29,13 @@ if (!connection) throw new Error('DATABASE_URL is required');
|
|
|
29
29
|
export const db = createTenon({ connection });
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Reuse this instance across the application and call `await db.close()` at shutdown.
|
|
33
|
-
|
|
34
|
-
application then closes that pool.
|
|
32
|
+
Reuse this instance across the application and call `await db.close()` at shutdown. You can also
|
|
33
|
+
supply an existing `pg.Pool`; your application then owns its shutdown.
|
|
35
34
|
|
|
36
35
|
## Create a table
|
|
37
36
|
|
|
38
|
-
If you already have a schema and migration runner,
|
|
39
|
-
|
|
40
|
-
Create a migration:
|
|
37
|
+
If you already have a schema and migration runner, keep them. For a fresh example database, create a
|
|
38
|
+
migration:
|
|
41
39
|
|
|
42
40
|
```sh
|
|
43
41
|
npx tenon new create_users
|
|
@@ -101,8 +99,8 @@ separately from the SQL text, even when the value contains SQL.
|
|
|
101
99
|
|
|
102
100
|
The type describes one selected row. `returns: 'oneOrNone'` gives you that row or `undefined`; it
|
|
103
101
|
throws if the query returns more than one row. Use `'one'` when absence should also be an error,
|
|
104
|
-
`'many'` for an array, or omit `returns` for the native result
|
|
105
|
-
These options do not change your SQL.
|
|
102
|
+
`'many'` for an array, or omit `returns` for the native result object with `.rows`, `.rowCount`, and
|
|
103
|
+
`.fields`. These options do not change your SQL.
|
|
106
104
|
|
|
107
105
|
Generated table types can help describe projections without repeating every field:
|
|
108
106
|
|
|
@@ -132,16 +130,16 @@ export function userByEmail(client: QueryClient, { email }: { email: string }) {
|
|
|
132
130
|
returns: 'oneOrNone',
|
|
133
131
|
});
|
|
134
132
|
}
|
|
135
|
-
|
|
136
|
-
const user = await userByEmail(db, { email: 'reader@example.test' });
|
|
137
133
|
```
|
|
138
134
|
|
|
139
|
-
`
|
|
140
|
-
the SQL and execution options together in the function.
|
|
135
|
+
Call `userByEmail(db, { email: 'reader@example.test' })` at the call site, or pass a transaction's
|
|
136
|
+
`tx`. Keep the SQL and execution options together in the function.
|
|
141
137
|
|
|
142
138
|
## Change data in a transaction
|
|
143
139
|
|
|
144
140
|
```ts
|
|
141
|
+
import { userByEmail } from './queries.js';
|
|
142
|
+
|
|
145
143
|
await db.transaction(async (tx) => {
|
|
146
144
|
const user = await userByEmail(tx, { email: 'reader@example.test' });
|
|
147
145
|
if (!user) throw new Error('User not found');
|
|
@@ -179,50 +177,31 @@ await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
|
|
|
179
177
|
WHERE email = ${input.email}`);
|
|
180
178
|
```
|
|
181
179
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
[input
|
|
185
|
-
|
|
186
|
-
Keep expressions, authorization, conflict handling and `RETURNING` in SQL. Selected column lists
|
|
187
|
-
choose which fields to write; they do not validate the complete object. See the
|
|
188
|
-
[mutation cookbook](mutations.md) for batches and upserts.
|
|
180
|
+
Use `Pick` to limit editable fields. Keep expressions, predicates, conflict handling, and
|
|
181
|
+
`RETURNING` in SQL. Selected columns choose fields; they do not validate the complete object. See
|
|
182
|
+
[input types](reference.md#insert-and-update-inputs) for mappings and the
|
|
183
|
+
[mutation cookbook](mutations.md) for defaults, batches, and upserts.
|
|
189
184
|
|
|
190
185
|
## Check your SQL and result types
|
|
191
186
|
|
|
192
|
-
Use `describeQuery({ connection, query })` on
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
transaction effects. See [query inspection](query-inspection.md).
|
|
197
|
-
|
|
198
|
-
TypeScript checks the application code and the result types you declare. Describe reports
|
|
199
|
-
PostgreSQL metadata; it does not generate or verify a TypeScript annotation. Use generated table
|
|
200
|
-
types where appropriate, declare projections explicitly, and check complete returned rows in tests.
|
|
187
|
+
Use `describeQuery({ connection, query })` on completed SQL, or `npx tenon describe query.sql` on a
|
|
188
|
+
file. It reports PostgreSQL metadata and a conservative `rowType` declaration; it does not update or
|
|
189
|
+
verify the application's `sql<Row>` annotation. Inspection uses a separate session, so match the
|
|
190
|
+
runtime role and settings. See [query inspection](query-inspection.md) for a worked test.
|
|
201
191
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
Input validation and authorization remain application responsibilities. Describe cannot establish
|
|
207
|
-
that a request is authorized, that its values are valid, or that returned JSON has an
|
|
208
|
-
application-specific shape.
|
|
192
|
+
After migrating the development/test database, inspect meaningful query variants, run TypeScript,
|
|
193
|
+
and test actual rows and effects. Keep those checks in CI. Execution assertions cover values, nulls,
|
|
194
|
+
JSON, authorization, and rollback that description alone cannot establish.
|
|
209
195
|
|
|
210
196
|
## Reuse or assemble parts of SQL when needed
|
|
211
197
|
|
|
212
|
-
For fixed conditions, write them directly:
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
const users = await db.query(
|
|
216
|
-
sql<UserSummary>`SELECT id, email FROM public.users WHERE active = ${true}`,
|
|
217
|
-
{ returns: 'many' },
|
|
218
|
-
);
|
|
219
|
-
```
|
|
220
|
-
|
|
221
198
|
A fragment is a piece of SQL that carries its bound values with it. It is useful when multiple
|
|
222
199
|
queries share a condition. For example, a search can use the same predicate for its result page and
|
|
223
200
|
its count:
|
|
224
201
|
|
|
225
202
|
```ts
|
|
203
|
+
const pattern = '%example%';
|
|
204
|
+
const limit = 20;
|
|
226
205
|
const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
|
|
227
206
|
const page = sql<UserSummary>`SELECT id, email FROM public.users
|
|
228
207
|
WHERE ${search} ORDER BY id LIMIT ${limit}`;
|
|
@@ -230,8 +209,8 @@ const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
|
|
|
230
209
|
WHERE ${search}`;
|
|
231
210
|
```
|
|
232
211
|
|
|
233
|
-
|
|
234
|
-
|
|
212
|
+
Execute each query with `db.query`. Validate application-supplied patterns and page sizes before
|
|
213
|
+
building queries. The complete query supplies the result type.
|
|
235
214
|
|
|
236
215
|
Use `sql.join` when the number of fragments varies, such as one search condition per word or a set
|
|
237
216
|
of optional filters. It combines fragments with `'AND'`, `'OR'`, or `','` and preserves their bound
|
|
@@ -248,10 +227,20 @@ const query = sql<UserSummary>`SELECT id, email FROM public.users
|
|
|
248
227
|
WHERE ${sql.join(filters, 'AND')} ORDER BY id`;
|
|
249
228
|
```
|
|
250
229
|
|
|
251
|
-
This matches active users whose email contains every term. With no terms, the active condition
|
|
252
|
-
|
|
230
|
+
This matches active users whose email contains every term. With no terms, the active condition still
|
|
231
|
+
applies. For an entirely optional set of filters, omit the whole `WHERE` clause when the list is
|
|
232
|
+
empty:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const optionalFilters = terms.map((term) => sql.fragment`email ILIKE ${'%' + term + '%'}`);
|
|
236
|
+
const unfilteredOrMatching = optionalFilters.length
|
|
237
|
+
? sql<UserSummary>`SELECT id, email FROM public.users WHERE ${sql.join(optionalFilters, 'AND')} ORDER BY id`
|
|
238
|
+
: sql<UserSummary>`SELECT id, email FROM public.users ORDER BY id`;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`sql.join` and fragments require nonempty input. Keep mandatory authorization predicates in the
|
|
242
|
+
initial list when applicable; an empty optional-filter list must not remove them. Use parentheses
|
|
253
243
|
when mixing `AND` and `OR`. See the [composition reference](reference.md#sql-fragments-and-joins).
|
|
254
244
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
aggregates, upserts, or other SQL features.
|
|
245
|
+
Use fixed SQL when composition is unnecessary. See the [reference](reference.md) for arrays,
|
|
246
|
+
identifiers, value mappings, CLI options, and error behavior.
|