@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/docs/agent-guide.md
CHANGED
|
@@ -1,35 +1,26 @@
|
|
|
1
1
|
# Tenon agent guide
|
|
2
2
|
|
|
3
|
-
Tenon executes PostgreSQL SQL through Postgres.js
|
|
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 Postgres.js. Use ordinary functions for reusable
|
|
4
|
+
queries. This guide covers routine implementation; the [reference](reference.md) defines the full
|
|
5
|
+
API and the [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 and read the
|
|
15
|
+
[Changelog](../CHANGELOG.md) entries through the target version before changing code. It ships with
|
|
16
|
+
the package alongside this guide. Follow its upgrade instructions, regenerate types when mappings
|
|
17
|
+
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 configuration.
|
|
33
24
|
|
|
34
25
|
```ts
|
|
35
26
|
import { createTenon } from '@kequtech/tenon-orm';
|
|
@@ -39,19 +30,15 @@ if (!connection) throw new Error('DATABASE_URL is required');
|
|
|
39
30
|
export const db = createTenon({ connection });
|
|
40
31
|
```
|
|
41
32
|
|
|
42
|
-
`connection` accepts a
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
`connection` accepts a URI string, `{ url, options? }`, Postgres.js options, or an existing driver
|
|
34
|
+
client. Reuse an instance for each database configuration. `db.close()` ends internally created
|
|
35
|
+
pools with a default 5,000 ms grace period; supplied clients remain application-owned. Configure
|
|
36
|
+
shutdown with `db.close({ timeoutMs })`; concurrent callers share its outcome. Finish outstanding
|
|
37
|
+
work before closing. Tenon
|
|
38
|
+
loads no environment files. Qualify table names or set runtime `search_path`; CLI `--namespace`
|
|
39
|
+
selects tooling scope, not runtime query names. See [connection options](reference.md#installation-and-connection).
|
|
47
40
|
|
|
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.
|
|
53
|
-
|
|
54
|
-
## Query contracts
|
|
41
|
+
## Query and result contracts
|
|
55
42
|
|
|
56
43
|
```ts
|
|
57
44
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
@@ -59,211 +46,115 @@ import type { Rows } from './generated-schema.js';
|
|
|
59
46
|
|
|
60
47
|
type Summary = Pick<Rows['users'], 'id' | 'email'>;
|
|
61
48
|
|
|
62
|
-
function
|
|
63
|
-
return sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
function userById(client: QueryClient, input: { id: number }) {
|
|
67
|
-
return client.query(userByIdQuery(input), { returns: 'oneOrNone' });
|
|
49
|
+
function userById(client: QueryClient, { id }: { id: number }) {
|
|
50
|
+
return client.query(sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`, {
|
|
51
|
+
returns: 'oneOrNone',
|
|
52
|
+
});
|
|
68
53
|
}
|
|
69
|
-
|
|
70
|
-
const user = await userById(db, { id: 42 });
|
|
71
|
-
// In a transaction, call userById(tx, { id: 42 }).
|
|
72
54
|
```
|
|
73
55
|
|
|
74
|
-
`
|
|
75
|
-
|
|
76
|
-
|
|
56
|
+
`QueryClient` accepts either `db` or a transaction's `tx`. Keep SQL and execution options together.
|
|
57
|
+
`sql<Row>` declares one selected row; it does not verify SQL or validate data. Unannotated fields
|
|
58
|
+
are `unknown`. Generated table types can describe full rows or `Pick` projections; joins and
|
|
59
|
+
aggregates need types for their actual output.
|
|
77
60
|
|
|
78
|
-
| `returns` | Result
|
|
79
|
-
| ------------------ |
|
|
80
|
-
| `native` (default) |
|
|
81
|
-
| `many` | `Row[]`
|
|
82
|
-
| `oneOrNone` | `Row \| undefined`
|
|
83
|
-
| `one` | `Row`
|
|
61
|
+
| `returns` | Result | Required returned rows |
|
|
62
|
+
| ------------------ | -------------------------------------------------- | ---------------------- |
|
|
63
|
+
| `native` (default) | Driver array with `.count`, `.command`, `.columns` | Any |
|
|
64
|
+
| `many` | Plain `Row[]` | Any |
|
|
65
|
+
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
66
|
+
| `one` | `Row` | Exactly one |
|
|
84
67
|
|
|
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.
|
|
68
|
+
Options are `{ returns?, affectedRows?, label? }`; unknown keys fail. Modes do not add SQL limits
|
|
69
|
+
or `RETURNING`. A returned-row mismatch throws `CardinalityError` with code `TENON_CARDINALITY_MISMATCH`;
|
|
70
|
+
it alone does not undo an autocommitted write.
|
|
94
71
|
|
|
95
72
|
## Parameters and composition
|
|
96
73
|
|
|
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:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
|
|
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
|
-
```
|
|
122
|
-
|
|
123
|
-
`pattern` and `limit` are validated application inputs. See the
|
|
124
|
-
[composition reference](reference.md#sql-fragments-and-joins) for grouping and list examples.
|
|
125
|
-
|
|
126
|
-
## Object-based writes
|
|
74
|
+
- Interpolate values without quotes. Do not author `$n` placeholders inside templates.
|
|
75
|
+
`undefined` bound values fail; `null` means SQL null.
|
|
76
|
+
- Use `id = ANY(${ids}::integer[])` for a set of IDs. Cast standalone parameters such as
|
|
77
|
+
`SELECT ${value}::integer AS value` when SQL must establish the type for inspection.
|
|
78
|
+
- `identifier(schema, table)` quotes separate approved names. Strings such as `'DESC'` remain
|
|
79
|
+
bound values; choose SQL syntax through fixed query alternatives or fragments.
|
|
80
|
+
- `sql.fragment` creates partial SQL with bound values; `sql.join(parts, 'AND' | 'OR' | ',')`
|
|
81
|
+
combines a nonempty list. Blank fragments fail. Group mixed conditions with parentheses.
|
|
82
|
+
- Full `sql` or `unsafeSql` queries cannot be nested. `unsafeSql<Row>(text, values)` is for trusted
|
|
83
|
+
external SQL with an explicit values array. Each Tenon query executes one statement.
|
|
127
84
|
|
|
128
|
-
Use
|
|
129
|
-
|
|
130
|
-
expressions, conflict behavior and `RETURNING` visible in SQL. Insert missing/undefined fields use
|
|
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.
|
|
85
|
+
Use fragments for actual reuse or variable composition. Keep fixed structure in SQL, and retain
|
|
86
|
+
mandatory authorization predicates when optional filters are absent.
|
|
135
87
|
|
|
136
|
-
|
|
137
|
-
narrow editable fields with `Pick`. They account for defaults, nullability, domains, and generated
|
|
138
|
-
columns. Write mappings differ from returned rows: timestamps use `Date | string`, bigint uses
|
|
139
|
-
`string | bigint`, and JSON accepts native JavaScript values through Tenon's exported `JSONValue` type. See the [input types](reference.md#insert-and-update-inputs).
|
|
88
|
+
## Writes and generated values
|
|
140
89
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
90
|
+
`sql.insertValues(rowOrRows, columns?)` builds column/value fragments; `sql.assignments(patch,
|
|
91
|
+
columns?)` builds assignments. Missing/undefined insert fields use defaults; update fields are
|
|
92
|
+
omitted. Explicit null stays null. Empty batches and empty assignments fail. Selected columns
|
|
93
|
+
ignore unlisted fields and can discard required inputs. Put expressions, conflict policy, and
|
|
94
|
+
`RETURNING` in the surrounding SQL. Batch in the caller below the query-wide 65,533-parameter limit.
|
|
144
95
|
|
|
145
|
-
|
|
96
|
+
Use generated `Rows.Insert['users']` and `Rows.Update['users']` inputs; narrow editable fields with
|
|
97
|
+
`Pick`. These types account for defaults, writable columns, domains, and driver representations.
|
|
98
|
+
They do not validate constraints or JSON contents. See [mutation examples](mutations.md).
|
|
146
99
|
|
|
147
|
-
|
|
100
|
+
Pass native JSON objects/arrays rather than pre-serialized text. Bigint and numeric reads are
|
|
101
|
+
strings; bigint writes accept `string | bigint`. Dates read as `Date`, including invalid dates for
|
|
102
|
+
infinities; validate before using date methods. SQL NULL array elements are JavaScript null.
|
|
103
|
+
Keep domain validation at the persistence boundary. See [value mappings](reference.md#generated-types-and-checking).
|
|
148
104
|
|
|
149
|
-
|
|
150
|
-
import { describeQuery } from '@kequtech/tenon-orm';
|
|
151
|
-
|
|
152
|
-
const query = userByIdQuery({ id: 42 });
|
|
153
|
-
const description = await describeQuery({ connection, query });
|
|
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
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Supply `DATABASE_URL` through the application's existing environment setup. Match its role,
|
|
167
|
-
`search_path`, and timezone through the connection URI or optional `session` settings. Review
|
|
168
|
-
the returned `context`; this is a separate connection with Postgres.js read mappings. It does not
|
|
169
|
-
inherit custom parsers, temporary tables, or transaction-local state.
|
|
105
|
+
## Transactions and native access
|
|
170
106
|
|
|
171
|
-
|
|
172
|
-
is unknown, source `NOT NULL` is informational, and JSON remains unknown. CLI success means the
|
|
173
|
-
statement was described, not that `sql<Row>`, values, permissions, or business behavior were verified.
|
|
174
|
-
|
|
175
|
-
With a test fixture containing user 42, check the database projection and the actual value separately:
|
|
107
|
+
For an existing user with id 42:
|
|
176
108
|
|
|
177
109
|
```ts
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
['id', 'number'],
|
|
184
|
-
['email', 'string'],
|
|
185
|
-
],
|
|
186
|
-
);
|
|
187
|
-
const expected = { id: 42, email: 'reader@example.test' } satisfies Summary;
|
|
188
|
-
assert.deepEqual(await userById(db, { id: expected.id }), expected);
|
|
110
|
+
await db.transaction(async (tx) => {
|
|
111
|
+
await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${42}`, {
|
|
112
|
+
affectedRows: { exact: 1 },
|
|
113
|
+
});
|
|
114
|
+
});
|
|
189
115
|
```
|
|
190
116
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
117
|
+
Use `tx` for every operation in the callback and await the work. Tenon commits on success and rolls
|
|
118
|
+
back on an escaping error; the callback client expires when the callback finishes. Direct
|
|
119
|
+
BEGIN/COMMIT/ROLLBACK boundaries, nested callback transactions, and automatic retries are not
|
|
120
|
+
provided. Transactions belong to one database instance.
|
|
201
121
|
|
|
202
|
-
|
|
122
|
+
`affectedRows: { exact: n }` or `{ max: n }` checks the final INSERT/UPDATE/DELETE/MERGE count.
|
|
123
|
+
Bounds are nonnegative safe integers. A guarded `db.query` uses a transaction; a guarded `tx.query`
|
|
124
|
+
uses a savepoint. Rejection rolls back that operation. A caught guarded failure permits continued
|
|
125
|
+
work; an unguarded SQL error normally aborts the transaction. Counts exclude extra trigger/cascade
|
|
126
|
+
and CTE effects. Reserve `tenon_affected_rows_` savepoint names for Tenon. A lost commit
|
|
127
|
+
acknowledgment can leave the outcome unknown.
|
|
203
128
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
129
|
+
`db.native` exposes the existing pool for driver features. Native calls bypass Tenon's guards and
|
|
130
|
+
`onQuery`; they do not join a Tenon transaction. Close owned clients through `db.close()`. Follow
|
|
131
|
+
the [native access contract](reference.md#native-driver-access) for parser initialization and LISTEN
|
|
132
|
+
limitations. Optional `onQuery` reports completion metadata without SQL or values; success inside
|
|
133
|
+
a callback does not establish that the outer transaction committed.
|
|
208
134
|
|
|
209
|
-
|
|
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_`.
|
|
135
|
+
## Inspect and validate
|
|
214
136
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
137
|
+
Use `describeQuery({ connection, query })` on completed SQL or `tenon describe query.sql` on a file.
|
|
138
|
+
Inspection owns a separate connection. Match the runtime role, search path, and timezone; custom
|
|
139
|
+
parsers, temporary tables, and transaction-local state are not inherited. Result nullability is
|
|
140
|
+
unknown and JSON structure is unchecked. A successful description does not verify `sql<Row>`,
|
|
141
|
+
bound values, execution permission, or effects.
|
|
220
142
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
143
|
+
Pair inspection with database assertions for complete relevant rows, meaningful query variants,
|
|
144
|
+
and rollback. A small query-building function can share the same statement with inspection and
|
|
145
|
+
execution; keep simple queries inline when that is sufficient. See [query inspection](query-inspection.md).
|
|
224
146
|
|
|
225
147
|
## Schema changes
|
|
226
148
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
npx tenon new create_users
|
|
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.
|
|
149
|
+
With Tenon's runner, development can use `tenon migrate && tenon generate --out
|
|
150
|
+
src/generated-schema.d.ts`. Commit migrations and exact generated output together; exclude the
|
|
151
|
+
generated file from automatic formatting. Production runs migrations and deploys committed types.
|
|
152
|
+
`generate --check` detects representation drift, not pending migrations or every schema change.
|
|
153
|
+
|
|
154
|
+
All database CLI commands require `DATABASE_URL`. `tenon new <name>` creates empty up/down files
|
|
155
|
+
offline; fill them before migrating. Migration commands use `--directory migrations` and
|
|
156
|
+
`--namespace public` by default; generation also accepts `--namespace`. Pass a custom namespace to
|
|
157
|
+
both migration and generation. Omit the down file for an irreversible migration; never edit applied
|
|
158
|
+
history. Each file commits independently with its history entry. Tools use session advisory locks,
|
|
159
|
+
so use a direct or session-pooled connection. Nontransactional DDL needs a separate procedure.
|
|
160
|
+
See the [command reference](reference.md#command-reference).
|
package/docs/human-guide.md
CHANGED
|
@@ -4,9 +4,9 @@ Tenon lets you use PostgreSQL directly from TypeScript. You write SQL; Tenon bin
|
|
|
4
4
|
executes it through Postgres.js. 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,12 @@ 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
|
-
databases, create an instance for each connection. You can also supply an existing Postgres.js client; your
|
|
34
|
-
application then closes that pool.
|
|
32
|
+
Reuse this instance across the application and call `await db.close()` at shutdown. You can also supply an existing Postgres.js client; your application then owns its shutdown.
|
|
35
33
|
|
|
36
34
|
## Create a table
|
|
37
35
|
|
|
38
|
-
If you already have a schema and migration runner,
|
|
39
|
-
|
|
40
|
-
Create a migration:
|
|
36
|
+
If you already have a schema and migration runner, keep them. For a fresh example database,
|
|
37
|
+
create a migration:
|
|
41
38
|
|
|
42
39
|
```sh
|
|
43
40
|
npx tenon new create_users
|
|
@@ -179,50 +176,31 @@ await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
|
|
|
179
176
|
WHERE email = ${input.email}`);
|
|
180
177
|
```
|
|
181
178
|
|
|
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.
|
|
179
|
+
Use `Pick` to limit editable fields. Keep expressions, predicates, conflict handling, and
|
|
180
|
+
`RETURNING` in SQL. Selected columns choose fields; they do not validate the complete object.
|
|
181
|
+
See [input types](reference.md#insert-and-update-inputs) for mappings and the
|
|
182
|
+
[mutation cookbook](mutations.md) for defaults, batches, and upserts.
|
|
189
183
|
|
|
190
184
|
## Check your SQL and result types
|
|
191
185
|
|
|
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.
|
|
201
|
-
|
|
202
|
-
1. Run `npm run db:migrate` against the development/test database.
|
|
203
|
-
2. Inspect the completed SQL and review the reported types and unresolved findings.
|
|
204
|
-
3. Run TypeScript and database tests. Keep these checks in routine CI validation.
|
|
186
|
+
Use `describeQuery({ connection, query })` on completed SQL, or `npx tenon describe query.sql` on a
|
|
187
|
+
file. It reports PostgreSQL metadata and a conservative `rowType` declaration; it does not update
|
|
188
|
+
or verify the application's `sql<Row>` annotation. Inspection uses a separate session, so match
|
|
189
|
+
the runtime role and settings. See [query inspection](query-inspection.md) for a worked test.
|
|
205
190
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
191
|
+
After migrating the development/test database, inspect meaningful query variants, run TypeScript,
|
|
192
|
+
and test actual rows and effects. Keep those checks in CI. Execution assertions cover values,
|
|
193
|
+
nulls, JSON, authorization, and rollback that description alone cannot establish.
|
|
209
194
|
|
|
210
195
|
## Reuse or assemble parts of SQL when needed
|
|
211
196
|
|
|
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
197
|
A fragment is a piece of SQL that carries its bound values with it. It is useful when multiple
|
|
222
198
|
queries share a condition. For example, a search can use the same predicate for its result page and
|
|
223
199
|
its count:
|
|
224
200
|
|
|
225
201
|
```ts
|
|
202
|
+
const pattern = '%example%';
|
|
203
|
+
const limit = 20;
|
|
226
204
|
const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
|
|
227
205
|
const page = sql<UserSummary>`SELECT id, email FROM public.users
|
|
228
206
|
WHERE ${search} ORDER BY id LIMIT ${limit}`;
|
|
@@ -230,8 +208,8 @@ const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
|
|
|
230
208
|
WHERE ${search}`;
|
|
231
209
|
```
|
|
232
210
|
|
|
233
|
-
|
|
234
|
-
|
|
211
|
+
Execute each query with `db.query`. Validate application-supplied patterns and page sizes before
|
|
212
|
+
building queries. The complete query supplies the result type.
|
|
235
213
|
|
|
236
214
|
Use `sql.join` when the number of fragments varies, such as one search condition per word or a set
|
|
237
215
|
of optional filters. It combines fragments with `'AND'`, `'OR'`, or `','` and preserves their bound
|
|
@@ -249,9 +227,19 @@ const query = sql<UserSummary>`SELECT id, email FROM public.users
|
|
|
249
227
|
```
|
|
250
228
|
|
|
251
229
|
This matches active users whose email contains every term. With no terms, the active condition
|
|
252
|
-
still applies.
|
|
230
|
+
still applies. For an entirely optional set of filters, omit the whole `WHERE` clause when the list
|
|
231
|
+
is empty:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const optionalFilters = terms.map((term) => sql.fragment`email ILIKE ${'%' + term + '%'}`);
|
|
235
|
+
const unfilteredOrMatching = optionalFilters.length
|
|
236
|
+
? sql<UserSummary>`SELECT id, email FROM public.users WHERE ${sql.join(optionalFilters, 'AND')} ORDER BY id`
|
|
237
|
+
: sql<UserSummary>`SELECT id, email FROM public.users ORDER BY id`;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`sql.join` and fragments require nonempty input. Keep mandatory authorization predicates in the
|
|
241
|
+
initial list when applicable; an empty optional-filter list must not remove them. Use parentheses
|
|
253
242
|
when mixing `AND` and `OR`. See the [composition reference](reference.md#sql-fragments-and-joins).
|
|
254
243
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
aggregates, upserts, or other SQL features.
|
|
244
|
+
Use fixed SQL when composition is unnecessary. See the [reference](reference.md) for arrays,
|
|
245
|
+
identifiers, value mappings, CLI options, and error behavior.
|