@kequtech/tenon-orm 0.4.0 → 0.4.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 +17 -2
- package/README.md +84 -51
- package/dist/cli.js +6 -22
- package/dist/database.js +5 -0
- package/dist/generate.d.ts +11 -0
- package/dist/generate.js +26 -1
- package/dist/generation/output.js +2 -4
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +3 -0
- package/docs/adoption-guide.md +88 -153
- package/docs/agent-guide.md +91 -172
- package/docs/connections.md +185 -0
- package/docs/human-guide.md +132 -176
- package/docs/migrations.md +133 -0
- package/docs/mutations.md +104 -67
- package/docs/query-checking.md +119 -120
- package/docs/query-inspection.md +78 -74
- package/docs/reference.md +222 -474
- package/docs/schema-types.md +263 -0
- package/docs/sql.md +212 -0
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Connections and driver access
|
|
2
|
+
|
|
3
|
+
`createTenon` uses a pgclear connection pool. Tenon includes the driver; a separate installation is
|
|
4
|
+
needed only when your application imports native pgclear APIs directly.
|
|
5
|
+
|
|
6
|
+
## Configure a database instance
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createTenon } from '@kequtech/tenon-orm';
|
|
10
|
+
|
|
11
|
+
const connection = process.env.DATABASE_URL;
|
|
12
|
+
if (!connection) throw new Error('DATABASE_URL is required');
|
|
13
|
+
|
|
14
|
+
export const db = createTenon({
|
|
15
|
+
connection: {
|
|
16
|
+
connectionString: connection,
|
|
17
|
+
maxConnections: 10,
|
|
18
|
+
connectTimeoutMs: 5000,
|
|
19
|
+
acquireTimeoutMs: 5000,
|
|
20
|
+
idleTimeoutMs: 30_000,
|
|
21
|
+
startupParameters: { search_path: 'app,public', TimeZone: 'UTC' },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`connection` accepts a PostgreSQL URI, pgclear `PoolOptions`, or an existing pgclear `Pool`.
|
|
27
|
+
`ConnectionOptions` is Tenon's alias for `PoolOptions`. Object fields override URI settings.
|
|
28
|
+
Reuse an instance for each database configuration.
|
|
29
|
+
|
|
30
|
+
The driver does not load environment variables, `.env`, pgpass, or service files. Tenon CLI commands
|
|
31
|
+
read `DATABASE_URL`; application code passes its configuration explicitly. An empty options object
|
|
32
|
+
uses localhost:5432, the current OS user and a database with that user's name.
|
|
33
|
+
|
|
34
|
+
Use `tls: {}` for TLS with certificate and hostname verification using system trust. Supply the
|
|
35
|
+
provider's CA or other required TLS settings through pgclear's `tls` options. Preserve the
|
|
36
|
+
connection requirements of your deployment. The complete option list is in the installed driver's
|
|
37
|
+
`docs/public-api.md`; unknown options reject.
|
|
38
|
+
|
|
39
|
+
Qualify table names in SQL or set runtime `search_path`. Tooling `namespace` options select the
|
|
40
|
+
schema to migrate or generate; they do not configure your application's queries. The example above
|
|
41
|
+
assumes an `app` schema already exists.
|
|
42
|
+
|
|
43
|
+
## Close resources
|
|
44
|
+
|
|
45
|
+
For a short-lived script, use `try/finally`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
try {
|
|
49
|
+
// Await database work here.
|
|
50
|
+
} finally {
|
|
51
|
+
await db.close();
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For a server, stop accepting work and finish requests before closing the database. A close call
|
|
56
|
+
immediately rejects new Tenon work and gives an owned pool a default 5,000 ms grace period. Set
|
|
57
|
+
`db.close({ timeoutMs: 1000 })` to change it; `0` requests immediate shutdown. Remaining driver work
|
|
58
|
+
and checkouts expire at the deadline. This bounds database resources, not arbitrary JavaScript
|
|
59
|
+
callbacks or proof that a remote rollback completed.
|
|
60
|
+
|
|
61
|
+
The first valid close call fixes the timeout. Concurrent and later calls share its promise and
|
|
62
|
+
outcome. Timeout must be an integer from 0 to 2,147,483,647 ms; invalid initial options reject
|
|
63
|
+
without closing the instance.
|
|
64
|
+
|
|
65
|
+
### Use an application-owned pool
|
|
66
|
+
|
|
67
|
+
When another part of the application already owns a pgclear pool:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { createPool } from '@kequtech/pgclear';
|
|
71
|
+
import { createTenon } from '@kequtech/tenon-orm';
|
|
72
|
+
|
|
73
|
+
const pool = createPool(connection);
|
|
74
|
+
const db = createTenon({ connection: pool });
|
|
75
|
+
// db.native === pool
|
|
76
|
+
|
|
77
|
+
await db.close(); // Disables this wrapper; pool remains open.
|
|
78
|
+
await pool.close(); // The application owns the pool's shutdown.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Tenon borrows the supplied pool by identity. Closing the wrapper does not cancel that pool's
|
|
82
|
+
outstanding work. Tool APIs that accept `Connection` follow the same ownership rule.
|
|
83
|
+
|
|
84
|
+
## Choose output conversions
|
|
85
|
+
|
|
86
|
+
Outputs follow the pool's codec registry. For example, to read int8 as JavaScript bigint and
|
|
87
|
+
`timestamptz` as Date:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { builtin, createTenon, defaultCodecs } from '@kequtech/tenon-orm';
|
|
91
|
+
|
|
92
|
+
const codecs = defaultCodecs.with(builtin.int8BigInt).with(builtin.timestamptzDate);
|
|
93
|
+
const db = createTenon({ connection: { connectionString: connection, codecs } });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
These choices apply across the database instance, including callback transactions. Tenon's query
|
|
97
|
+
options have no per-query codec override. Input encoders remain separate: a `param(value, codec)`
|
|
98
|
+
carries its own encoder, and passing a Date does not select a Date output decoder.
|
|
99
|
+
|
|
100
|
+
Use `db.generateTypes` with this instance to inherit its read mappings. Pass the same registry
|
|
101
|
+
explicitly to `describeQuery` and `checkQueries`; their CLI forms use default codecs. See
|
|
102
|
+
[schema types and values](schema-types.md#custom-codecs) for custom representations and Temporal.
|
|
103
|
+
|
|
104
|
+
## Observe completed queries
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const db = createTenon({
|
|
108
|
+
connection,
|
|
109
|
+
onQuery(event) {
|
|
110
|
+
console.log(event.label, event.durationMs, event.outcome, event.rowCount);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Set `{ label: 'user-by-email' }` on a query to identify it. `onQuery` receives duration, outcome,
|
|
116
|
+
and available command/count/error-code metadata, with no SQL text or bound values. Validation
|
|
117
|
+
failures before execution are not observed. Native driver calls and tooling are outside this hook.
|
|
118
|
+
|
|
119
|
+
Observers are not awaited. A thrown error or rejected observer promise emits a warning and leaves
|
|
120
|
+
the query outcome unchanged. A successful query event inside a transaction does not mean the outer
|
|
121
|
+
transaction committed. Guarded-call duration includes its commit or rollback work.
|
|
122
|
+
|
|
123
|
+
## Use native driver features
|
|
124
|
+
|
|
125
|
+
`db.native` is the actual pgclear pool. Use it for prepared statements, streams, COPY, or held
|
|
126
|
+
sessions. For a finished SQL string:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const result = await db.native.queryClassic('SELECT $1::integer AS value', [42]);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Native calls bypass Tenon's result modes, affected-row guards, SQL boundary checks, and `onQuery`.
|
|
133
|
+
They do not join a Tenon callback transaction. A callback's `tx` exposes only `query`.
|
|
134
|
+
|
|
135
|
+
Use the public driver session APIs and release held leases. For example:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
await db.native.withSession(async (session) => {
|
|
139
|
+
const prepared = await session.prepare('SELECT $1::integer AS value');
|
|
140
|
+
try {
|
|
141
|
+
await prepared.query<{ value: number }>([42]);
|
|
142
|
+
} finally {
|
|
143
|
+
await prepared.close();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Native `query` accepts pgclear's own SQL tag. Tenon's `sql` and `unsafeSql` results belong in
|
|
149
|
+
`db.query`; use `queryClassic` for text to avoid needing a second tag. When both tags are used,
|
|
150
|
+
import the native one as `pgSql`.
|
|
151
|
+
|
|
152
|
+
### Listen for notifications
|
|
153
|
+
|
|
154
|
+
Import `listen` directly from pgclear for a dedicated connection:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { listen } from '@kequtech/pgclear';
|
|
158
|
+
|
|
159
|
+
const listener = await listen(connection, {
|
|
160
|
+
channels: ['users_changed'],
|
|
161
|
+
onNotification: ({ payload }) => console.log(payload),
|
|
162
|
+
reconnect: true,
|
|
163
|
+
});
|
|
164
|
+
// At shutdown:
|
|
165
|
+
await listener.close();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A listener has a separate lifetime and is not closed by `db.close()`. Notifications are not replayed
|
|
169
|
+
after a disconnection; the application owns its recovery policy.
|
|
170
|
+
|
|
171
|
+
## Update the driver
|
|
172
|
+
|
|
173
|
+
Tenon depends on `@kequtech/pgclear: ^0.3.0`, which permits compatible 0.3.x patch updates. With npm:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
npm update @kequtech/pgclear
|
|
177
|
+
npm ls @kequtech/pgclear
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Review the resolved version and lockfile, regenerate affected declarations, and run application
|
|
181
|
+
checks before committing. A lockfile keeps its chosen version until updated. Pgclear 0.4 is outside
|
|
182
|
+
this range and requires an explicit Tenon compatibility update.
|
|
183
|
+
|
|
184
|
+
If the application imports pgclear directly, declare it as a dependency with a compatible version
|
|
185
|
+
so the package manager can share a copy. See the [changelog](../CHANGELOG.md) when updating Tenon.
|
package/docs/human-guide.md
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Getting started
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This guide creates a table, inserts and reads a user, reuses a query in a transaction, and generates
|
|
4
|
+
TypeScript types. You need an existing PostgreSQL database. If your application already has tables
|
|
5
|
+
and a migration runner, keep them and start at [Write and read a row](#write-and-read-a-row).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
replacing an existing ORM. Code blocks build on the connection and table introduced below;
|
|
9
|
-
query-function examples belong in their own module.
|
|
7
|
+
## Install and connect
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
In an existing Node.js 24+ TypeScript project using ESM:
|
|
9
|
+
Use Node.js 24+, PostgreSQL 16+, ESM, and TypeScript 6+ with Node.js development typings. In an
|
|
10
|
+
existing TypeScript project:
|
|
14
11
|
|
|
15
12
|
```sh
|
|
16
13
|
npm install @kequtech/tenon-orm
|
|
17
14
|
```
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
Keep the project's TypeScript configuration; a typical Node.js application uses `module: "NodeNext"`
|
|
17
|
+
and `strict: true`. Tenon includes its PostgreSQL driver, pgclear.
|
|
18
|
+
|
|
19
|
+
Supply `DATABASE_URL` through your environment setup. For a local example database:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/your_database
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Tenon does not create the database or load `.env` files. Create one shared database module:
|
|
24
26
|
|
|
25
27
|
```ts
|
|
26
28
|
// src/db.ts
|
|
@@ -31,19 +33,18 @@ if (!connection) throw new Error('DATABASE_URL is required');
|
|
|
31
33
|
export const db = createTenon({ connection });
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
Reuse
|
|
35
|
-
|
|
36
|
+
Reuse `db` across requests. Call `await db.close()` when a standalone script finishes or a server
|
|
37
|
+
shuts down. See [connections](connections.md) for pool options and application-owned pools.
|
|
36
38
|
|
|
37
|
-
## Create a
|
|
39
|
+
## Create and apply a migration
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
migration:
|
|
41
|
+
In a fresh migrations directory:
|
|
41
42
|
|
|
42
43
|
```sh
|
|
43
44
|
npx tenon new create_users
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
This creates two empty files. Put the table definition in the up file:
|
|
47
48
|
|
|
48
49
|
```sql
|
|
49
50
|
-- migrations/0001_create_users.up.sql
|
|
@@ -54,91 +55,74 @@ CREATE TABLE public.users (
|
|
|
54
55
|
);
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
Put the reverse operation in the down file:
|
|
59
|
+
|
|
57
60
|
```sql
|
|
58
61
|
-- migrations/0001_create_users.down.sql
|
|
59
62
|
DROP TABLE public.users;
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```json
|
|
65
|
-
{
|
|
66
|
-
"scripts": {
|
|
67
|
-
"db:migrate": "tenon migrate && tenon generate --out src/generated-schema.d.ts"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
```
|
|
65
|
+
Apply it to the database selected by `DATABASE_URL`:
|
|
71
66
|
|
|
72
67
|
```sh
|
|
73
|
-
|
|
68
|
+
npx tenon migrate
|
|
69
|
+
npx tenon status
|
|
74
70
|
```
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
If a change cannot be reversed, omit its down file. A down migration is not a backup.
|
|
80
|
-
|
|
81
|
-
## Write a query
|
|
72
|
+
For later changes, create a new migration. Applied files are checked against recorded checksums and
|
|
73
|
+
must stay unchanged. If a migration cannot be reversed, remove its down file before applying it.
|
|
74
|
+
See [migrations](migrations.md) for rollback and deployment behavior.
|
|
82
75
|
|
|
83
|
-
|
|
76
|
+
## Write and read a row
|
|
84
77
|
|
|
85
78
|
```ts
|
|
79
|
+
// src/example.ts
|
|
86
80
|
import { sql } from '@kequtech/tenon-orm';
|
|
87
81
|
import { db } from './db.js';
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`rowCount` is a number or null when the command has no count. SQL `count(*)` and bigint/bigserial
|
|
108
|
-
column values are exact strings, including values beyond JavaScript's safe integer range. Use
|
|
109
|
-
strings, bigint, or pgclear `param(text, builtin.int8)` for int8 parameters. Plain Date and supported
|
|
110
|
-
Temporal inputs also work; they do not change the default string outputs. Date inputs encode in UTC.
|
|
111
|
-
Opt-in codecs can return Date, bigint, or Temporal values when the application needs them.
|
|
112
|
-
|
|
113
|
-
Generated table types can help describe projections without repeating every field:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
import type { Database } from './generated-schema.js';
|
|
117
|
-
type UserSummary = Pick<Database['users']['row'], 'id' | 'email'>;
|
|
83
|
+
try {
|
|
84
|
+
const email = 'reader@example.test';
|
|
85
|
+
const created = await db.query(
|
|
86
|
+
sql<{ id: number }>`INSERT INTO public.users (email) VALUES (${email}) RETURNING id`,
|
|
87
|
+
{ returns: 'one' },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const user = await db.query(
|
|
91
|
+
sql<{ id: number; email: string; active: boolean }>`
|
|
92
|
+
SELECT id, email, active FROM public.users WHERE id = ${created.id}
|
|
93
|
+
`,
|
|
94
|
+
{ returns: 'oneOrNone' },
|
|
95
|
+
);
|
|
96
|
+
console.log(user);
|
|
97
|
+
} finally {
|
|
98
|
+
await db.close();
|
|
99
|
+
}
|
|
118
100
|
```
|
|
119
101
|
|
|
120
|
-
|
|
121
|
-
|
|
102
|
+
This inserts a new email; rerunning it with the same email will fail the unique constraint. Run it
|
|
103
|
+
through your project's TypeScript runner or compile it with your application. The `.js` imports
|
|
104
|
+
follow NodeNext's convention for compiled TypeScript modules.
|
|
122
105
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
106
|
+
The template binds `${email}` separately from the SQL. Do not put quotes around an interpolation.
|
|
107
|
+
`sql<{ id: number }>` describes one returned row; `RETURNING id` asks PostgreSQL to return it.
|
|
108
|
+
`returns: 'one'` requires exactly one row, while `oneOrNone` permits absence as `undefined`.
|
|
109
|
+
Use `many` for an array or omit `returns` for the driver result with `.rows` and `.rowCount`.
|
|
127
110
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
111
|
+
The annotation does not check SQL or validate the row at runtime. Without an annotation, field values
|
|
112
|
+
are `unknown`. Bigint columns and SQL `count(*)` return strings by default; the integer ID in this
|
|
113
|
+
example returns a number. See [value mappings](schema-types.md#value-mappings) for JSON, arrays,
|
|
114
|
+
decimals, and dates.
|
|
131
115
|
|
|
132
|
-
## Reuse a query
|
|
116
|
+
## Reuse a query
|
|
133
117
|
|
|
134
|
-
|
|
135
|
-
|
|
118
|
+
Put reusable queries in ordinary functions. Accept `QueryClient` when a caller may supply either
|
|
119
|
+
`db` or a transaction's `tx`:
|
|
136
120
|
|
|
137
121
|
```ts
|
|
122
|
+
// src/queries.ts
|
|
138
123
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
139
|
-
import type { Database } from './generated-schema.js';
|
|
140
124
|
|
|
141
|
-
type UserSummary =
|
|
125
|
+
type UserSummary = { id: number; email: string };
|
|
142
126
|
|
|
143
127
|
export function userByEmail(client: QueryClient, { email }: { email: string }) {
|
|
144
128
|
return client.query(sql<UserSummary>`SELECT id, email FROM public.users WHERE email = ${email}`, {
|
|
@@ -147,12 +131,15 @@ export function userByEmail(client: QueryClient, { email }: { email: string }) {
|
|
|
147
131
|
}
|
|
148
132
|
```
|
|
149
133
|
|
|
150
|
-
Call `userByEmail(db, { email: 'reader@example.test' })
|
|
151
|
-
|
|
134
|
+
Call `userByEmail(db, { email: 'reader@example.test' })`. Keep the SQL and result options together in
|
|
135
|
+
the function. Joins and aggregates work the same way; see [writing queries](sql.md).
|
|
152
136
|
|
|
153
|
-
##
|
|
137
|
+
## Use a transaction and a write guard
|
|
138
|
+
|
|
139
|
+
In code using the shared `db`:
|
|
154
140
|
|
|
155
141
|
```ts
|
|
142
|
+
import { sql } from '@kequtech/tenon-orm';
|
|
156
143
|
import { userByEmail } from './queries.js';
|
|
157
144
|
|
|
158
145
|
await db.transaction(async (tx) => {
|
|
@@ -165,117 +152,86 @@ await db.transaction(async (tx) => {
|
|
|
165
152
|
});
|
|
166
153
|
```
|
|
167
154
|
|
|
168
|
-
|
|
169
|
-
|
|
155
|
+
Run all participating queries through `tx` and await them. The callback commits on success and rolls
|
|
156
|
+
back when an error escapes.
|
|
170
157
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
and application authorization. Checking returned rows with `returns` alone does not undo an
|
|
175
|
-
autocommitted write.
|
|
158
|
+
The guard requires exactly one affected row and rolls back the update if the count differs. Use
|
|
159
|
+
`{ max: 1 }` to allow zero or one. Guards also work with `db.query` outside a callback. Result modes
|
|
160
|
+
such as `one` check returned rows; by themselves they do not undo an autocommitted write.
|
|
176
161
|
|
|
177
|
-
|
|
162
|
+
You still author the `WHERE` clause and application authorization. The
|
|
163
|
+
[transaction reference](reference.md#transactions-guarded-writes-and-errors) covers caught errors,
|
|
164
|
+
savepoints, and retry limits. For object-based inserts and patches, see [writing data](mutations.md).
|
|
178
165
|
|
|
179
|
-
|
|
180
|
-
`SET`. Inserts use database defaults for missing/undefined fields; updates omit those fields.
|
|
181
|
-
Explicit `null` stays null. Empty batches and updates throw. Heterogeneous insert rows remain
|
|
182
|
-
aligned, with `DEFAULT` for missing cells.
|
|
166
|
+
## Generate types
|
|
183
167
|
|
|
184
|
-
|
|
185
|
-
|
|
168
|
+
Generation is optional. It reads the migrated database and writes table read/insert/update types.
|
|
169
|
+
Run it through the configured instance so the types use the same value conversions as your queries:
|
|
186
170
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
171
|
+
```ts
|
|
172
|
+
// scripts/generate-types.ts
|
|
173
|
+
import { parseArgs } from 'node:util';
|
|
174
|
+
import { db } from '../src/db.ts';
|
|
175
|
+
|
|
176
|
+
const { values } = parseArgs({ options: { check: { type: 'boolean', default: false } } });
|
|
177
|
+
try {
|
|
178
|
+
await db.generateTypes({ out: 'src/generated-schema.d.ts', check: values.check });
|
|
179
|
+
} finally {
|
|
180
|
+
await db.close();
|
|
181
|
+
}
|
|
193
182
|
```
|
|
194
183
|
|
|
195
|
-
|
|
196
|
-
`
|
|
197
|
-
|
|
198
|
-
|
|
184
|
+
This script runs directly on Node.js 24+ using its TypeScript support. For this execution path,
|
|
185
|
+
`src/db.ts` must be runnable without build-time transformations or extension rewriting. If your
|
|
186
|
+
project compiles scripts, use its normal runner and import conventions instead. To typecheck `.ts`
|
|
187
|
+
imports with `noEmit`, enable `allowImportingTsExtensions`; emitting projects can use
|
|
188
|
+
`rewriteRelativeImportExtensions`.
|
|
199
189
|
|
|
200
|
-
|
|
190
|
+
Add these scripts to your existing `package.json`:
|
|
201
191
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
npx tenon check src/queries.ts
|
|
211
|
-
npx tenon check src/queries.ts:23
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"scripts": {
|
|
195
|
+
"db:generate": "node scripts/generate-types.ts",
|
|
196
|
+
"db:check-types": "npm run db:generate -- --check",
|
|
197
|
+
"db:migrate": "tenon migrate && npm run db:generate"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
212
200
|
```
|
|
213
201
|
|
|
214
|
-
|
|
215
|
-
source and types without importing the application or executing SQL. It reports definite
|
|
216
|
-
contradictions and unsupported composition; nullability, JSON members, and other inconclusive
|
|
217
|
-
contracts receive the benefit of the declared type and remain unproven. See
|
|
218
|
-
[query checking](query-checking.md) for supported fragments and explicit object expansion shapes.
|
|
219
|
-
|
|
220
|
-
Use describeQuery on finished SQL, or tenon describe query.sql, when you only need metadata and a
|
|
221
|
-
conservative row declaration. Match runtime role/session settings. Retain TypeScript and database
|
|
222
|
-
tests for actual rows, effects, permissions, and rollback. The checker does not replace those tests.
|
|
223
|
-
Generated patch types contain optional fields and can therefore be unchecked even when execution is
|
|
224
|
-
valid. Keep optional patches when needed; see [responding to findings](query-checking.md#responding-to-findings).
|
|
225
|
-
For rejected values, result-count errors, and other common failures, see
|
|
226
|
-
[troubleshooting](reference.md#troubleshooting).
|
|
227
|
-
|
|
228
|
-
## Reuse or assemble parts of SQL when needed
|
|
229
|
-
|
|
230
|
-
A fragment is a piece of SQL that carries its bound values with it. It is useful when multiple
|
|
231
|
-
queries share a condition. For example, a search can use the same predicate for its result page and
|
|
232
|
-
its count:
|
|
202
|
+
Run `npm run db:generate` now. The output exports a `Database` type:
|
|
233
203
|
|
|
234
204
|
```ts
|
|
235
|
-
|
|
236
|
-
const limit = 20;
|
|
237
|
-
const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
|
|
238
|
-
const page = sql<UserSummary>`SELECT id, email FROM public.users
|
|
239
|
-
WHERE ${search} ORDER BY id LIMIT ${limit}`;
|
|
240
|
-
const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
|
|
241
|
-
WHERE ${search}`;
|
|
242
|
-
```
|
|
205
|
+
import type { Database } from './generated-schema.js';
|
|
243
206
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
207
|
+
type UserRow = Database['users']['row'];
|
|
208
|
+
type UserSummary = Pick<UserRow, 'id' | 'email'>;
|
|
209
|
+
type UserInsert = Database['users']['insert'];
|
|
210
|
+
type UserUpdate = Database['users']['update'];
|
|
211
|
+
```
|
|
247
212
|
|
|
248
|
-
Use `
|
|
249
|
-
|
|
250
|
-
|
|
213
|
+
Use `UserSummary` with `sql<UserSummary>` or `UserInsert` to type an insert input. These types
|
|
214
|
+
follow table columns, defaults, nullability, and codec representations. They do not validate
|
|
215
|
+
application inputs. Keep aliases in handwritten files and leave generated output unchanged.
|
|
216
|
+
See [schema types](schema-types.md) for an output example and mapping details.
|
|
251
217
|
|
|
252
|
-
|
|
253
|
-
const terms = ['reader', 'example'];
|
|
254
|
-
const filters = [sql.fragment`active = ${true}`];
|
|
255
|
-
for (const term of terms) {
|
|
256
|
-
filters.push(sql.fragment`email ILIKE ${'%' + term + '%'}`);
|
|
257
|
-
}
|
|
218
|
+
## Keep schema and code in step
|
|
258
219
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
```
|
|
220
|
+
During development, run `npm run db:migrate`, review the generated diff, and commit migrations and
|
|
221
|
+
generated declarations together. Exclude the generated file from automatic formatting.
|
|
262
222
|
|
|
263
|
-
|
|
264
|
-
applies. For an entirely optional set of filters, omit the whole `WHERE` clause when the list is
|
|
265
|
-
empty:
|
|
223
|
+
In CI, migrate a dedicated test database and check committed output without rewriting it:
|
|
266
224
|
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
: sql<UserSummary>`SELECT id, email FROM public.users ORDER BY id`;
|
|
225
|
+
```sh
|
|
226
|
+
npx tenon migrate
|
|
227
|
+
npm run db:check-types
|
|
228
|
+
npx tsc --noEmit
|
|
272
229
|
```
|
|
273
230
|
|
|
274
|
-
`
|
|
275
|
-
|
|
276
|
-
when mixing `AND` and `OR`. See the [composition reference](reference.md#sql-fragments-and-joins).
|
|
231
|
+
Do not run the regenerating `db:migrate` script before the freshness check: it would replace the file
|
|
232
|
+
you meant to verify. Production runs migrations and deploys the already-built application.
|
|
277
233
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
234
|
+
Optionally run `npx tenon check src/queries.ts` against the migrated database to compare source query
|
|
235
|
+
types with PostgreSQL metadata. Keep database tests for rows, permissions, and rollback. See
|
|
236
|
+
[choosing a validation tool](reference.md#choosing-a-validation-tool) and
|
|
237
|
+
[troubleshooting](reference.md#troubleshooting) as needed.
|