@kequtech/tenon-orm 0.1.4 → 0.2.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 +64 -0
- package/README.md +19 -19
- package/dist/affected-rows.d.ts +1 -1
- package/dist/affected-rows.js +6 -6
- package/dist/baseline.d.ts +1 -1
- package/dist/baseline.js +5 -5
- package/dist/cli.js +18 -39
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +18 -13
- package/dist/connection.js +14 -43
- package/dist/database.js +5 -2
- package/dist/describe.d.ts +3 -0
- package/dist/describe.js +21 -0
- package/dist/driver/array.d.ts +5 -0
- package/dist/driver/array.js +70 -0
- package/dist/driver/lifecycle.d.ts +15 -0
- package/dist/driver/lifecycle.js +57 -0
- package/dist/driver/session.d.ts +8 -0
- package/dist/driver/session.js +92 -0
- package/dist/driver/types.d.ts +10 -0
- package/dist/driver/types.js +49 -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/output.d.ts +2 -0
- package/dist/generation/output.js +35 -0
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +11 -11
- package/dist/generation/table-types.js +2 -52
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/inspection/catalog.d.ts +25 -0
- package/dist/inspection/catalog.js +29 -0
- package/dist/inspection/cli.d.ts +22 -0
- package/dist/inspection/cli.js +34 -0
- package/dist/inspection/connection.d.ts +4 -0
- package/dist/inspection/connection.js +44 -0
- package/dist/inspection/options.d.ts +15 -0
- package/dist/inspection/options.js +49 -0
- package/dist/inspection/result.d.ts +4 -0
- package/dist/inspection/result.js +90 -0
- package/dist/inspection/types.d.ts +74 -0
- package/dist/inspection/types.js +1 -0
- package/dist/migrations/baseline-sql.d.ts +1 -1
- package/dist/migrations/baseline-sql.js +1 -1
- package/dist/migrations/connection.d.ts +2 -3
- package/dist/migrations/connection.js +15 -13
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -7
- package/dist/mutations.d.ts +0 -5
- package/dist/mutations.js +33 -41
- package/dist/query-options.d.ts +2 -2
- package/dist/query-options.js +4 -4
- package/dist/query.d.ts +3 -4
- package/dist/query.js +2 -2
- package/dist/sql.d.ts +1 -1
- package/dist/sql.js +1 -1
- package/dist/transaction-sql.js +1 -1
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +14 -13
- package/dist/types.d.ts +7 -4
- package/docs/adoption-guide.md +43 -76
- package/docs/agent-guide.md +82 -107
- package/docs/human-guide.md +20 -34
- package/docs/mutations.md +13 -13
- package/docs/query-inspection.md +128 -0
- package/docs/reference.md +98 -289
- package/package.json +10 -31
- package/dist/safeql/composition.d.ts +0 -9
- package/dist/safeql/composition.js +0 -168
- package/dist/safeql/eslint.d.ts +0 -2
- package/dist/safeql/eslint.js +0 -50
- package/dist/safeql/mutations.d.ts +0 -3
- package/dist/safeql/mutations.js +0 -74
- package/dist/safeql/plugin.d.ts +0 -3
- package/dist/safeql/plugin.js +0 -43
- package/dist/safeql/static.d.ts +0 -10
- package/dist/safeql/static.js +0 -59
- package/dist/safeql.d.ts +0 -6
- package/dist/safeql.js +0 -7
package/docs/reference.md
CHANGED
|
@@ -1,35 +1,63 @@
|
|
|
1
1
|
# Tenon reference
|
|
2
2
|
|
|
3
|
-
Use this document to look up exact API behavior,
|
|
3
|
+
Use this document to look up exact API behavior, query inspection, and limitations. The
|
|
4
4
|
[human guide](human-guide.md) teaches the workflow; the [agent guide](agent-guide.md) is a
|
|
5
5
|
self-contained implementation guide. [Adoption](adoption-guide.md) covers replacing an existing ORM
|
|
6
6
|
and transferring migration history.
|
|
7
7
|
|
|
8
8
|
## Installation and connection
|
|
9
9
|
|
|
10
|
-
Runtime requirements: Node.js 24+, PostgreSQL 16+, ESM
|
|
11
|
-
TypeScript 6+ and development typings
|
|
10
|
+
Runtime requirements: Node.js 24+, PostgreSQL 16+, and ESM. The Postgres.js driver is included. TypeScript consumers need
|
|
11
|
+
TypeScript 6+ and Node development typings. Keep your application's strict TypeScript
|
|
12
12
|
setup; Tenon needs no special compiler configuration. `"type": "module"` and TypeScript's
|
|
13
|
-
`"module": "NodeNext"` are a usual Node setup.
|
|
13
|
+
`"module": "NodeNext"` are a usual Node setup.
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
npm install @kequtech/tenon-orm
|
|
17
|
-
npm install --save-dev @types/pg
|
|
16
|
+
npm install @kequtech/tenon-orm
|
|
18
17
|
```
|
|
19
18
|
|
|
20
|
-
`createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, and
|
|
21
|
-
`close`. The required `connection` accepts a URL,
|
|
22
|
-
explicit `{}` selects
|
|
19
|
+
`createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, `native`, and
|
|
20
|
+
`close`. The required `connection` accepts a URL, Postgres.js options, or an existing Postgres.js client. An
|
|
21
|
+
explicit `{}` selects the driver's environment defaults. Tenon does not load environment files
|
|
23
22
|
automatically.
|
|
24
23
|
|
|
25
24
|
Reuse an instance per database connection. `close()` closes pools created by Tenon; supplied pools
|
|
26
|
-
remain application-owned. Finish outstanding work before closing.
|
|
27
|
-
`pool.on('error', handler)` on supplied pools; Tenon reports idle errors on owned pools through
|
|
28
|
-
`process.emitWarning`.
|
|
25
|
+
remain application-owned. Finish outstanding work before closing. Postgres.js handles connection errors through rejected queries and its native connection callbacks.
|
|
29
26
|
|
|
30
27
|
Qualify SQL names or configure the driver's `search_path`. The CLI's `--namespace` selects the
|
|
31
28
|
schema used by tooling, not a runtime query namespace.
|
|
32
29
|
|
|
30
|
+
## Native driver access
|
|
31
|
+
|
|
32
|
+
`db.native` is the underlying Postgres.js client, typed as `postgres.Sql`. The property is readonly
|
|
33
|
+
in TypeScript. It exposes the existing pool without creating another client or requiring a separate
|
|
34
|
+
driver installation. A supplied client is returned by identity. Use native APIs for LISTEN, cursors,
|
|
35
|
+
COPY, and other driver features; their behavior follows the included `postgres@3.4.9`.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const db = createTenon({ connection });
|
|
39
|
+
await db.native.listen('updates', handlePayload, refreshAfterSubscription);
|
|
40
|
+
// At application shutdown, after outstanding work finishes:
|
|
41
|
+
await db.close();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Postgres.js LISTEN creates a dedicated physical connection alongside the query pool. Applications
|
|
45
|
+
own notification parsing, authorization, callback failures, and catch-up after resubscription. See
|
|
46
|
+
the [Postgres.js native API](https://github.com/porsager/postgres/tree/v3.4.9#listen--notify).
|
|
47
|
+
|
|
48
|
+
Native calls bypass Tenon's result contracts, affected-row guards, SQL boundary checks, and
|
|
49
|
+
`onQuery` observation. They use native preparation defaults. `db.native` always refers to the pool,
|
|
50
|
+
including inside `db.transaction`: its queries do not join that transaction. Transaction callbacks
|
|
51
|
+
expose only `tx.query`; use that for work that must commit or roll back together.
|
|
52
|
+
|
|
53
|
+
Native access is immediate and does not initialize Tenon's array correction. The first Tenon query
|
|
54
|
+
or transaction installs that correction on the shared client; already prepared native statements
|
|
55
|
+
can retain earlier parsers. Raw calls otherwise follow native driver behavior.
|
|
56
|
+
|
|
57
|
+
`db.close()` ends a Tenon-owned client and its native listeners. With a borrowed client, it closes
|
|
58
|
+
only the Tenon wrapper: native queries and listeners remain usable until their owner calls
|
|
59
|
+
`db.native.end()`. Calling `end()` directly also ends the client used by Tenon.
|
|
60
|
+
|
|
33
61
|
## Queries and result modes
|
|
34
62
|
|
|
35
63
|
```ts
|
|
@@ -47,17 +75,18 @@ automatically describe joins or aggregates.
|
|
|
47
75
|
`query` accepts `{ returns?, affectedRows?, label? }`. Unknown options are rejected. `label` must be
|
|
48
76
|
a nonempty string when supplied and is used for diagnostics.
|
|
49
77
|
|
|
50
|
-
| `returns` | Result
|
|
51
|
-
| ------------------ |
|
|
52
|
-
| `native` (default) |
|
|
53
|
-
| `many` | `Row[]`
|
|
54
|
-
| `oneOrNone` | `Row \| undefined`
|
|
55
|
-
| `one` | `Row`
|
|
78
|
+
| `returns` | Result | Returned rows allowed |
|
|
79
|
+
| ------------------ | ------------------------------------ | --------------------- |
|
|
80
|
+
| `native` (default) | Postgres.js `QueryResult<Row>` array | Any |
|
|
81
|
+
| `many` | `Row[]` | Any, including zero |
|
|
82
|
+
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
83
|
+
| `one` | `Row` | Exactly one |
|
|
56
84
|
|
|
57
|
-
Native results
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
Native results are arrays of rows with `command`, `count`, `columns`, `statement`, and `state`
|
|
86
|
+
metadata. `count` is the affected-row count, independent of `RETURNING`; it can be null for commands
|
|
87
|
+
without a count. `many` returns a plain row array. Result modes never add `LIMIT` or `RETURNING`.
|
|
88
|
+
A returned-row mismatch does not undo an autocommitted write. Use `affectedRows` for write limits
|
|
89
|
+
that roll back on failure.
|
|
61
90
|
|
|
62
91
|
Reusable queries are ordinary functions. Accept a `QueryClient` and named object inputs when useful;
|
|
63
92
|
both `Database` and `Transaction` implement `QueryClient`. Keep result annotations on the SQL and
|
|
@@ -70,10 +99,14 @@ The `sql` tag binds ordinary interpolations as `$1`, `$2`, etc. Never quote inte
|
|
|
70
99
|
manual parameter placeholders inside templates. Every public query executes one statement, including
|
|
71
100
|
parameterless queries.
|
|
72
101
|
|
|
73
|
-
Values use
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
types
|
|
102
|
+
Values use Postgres.js's native conversions. An undefined bound parameter is rejected. Objects and
|
|
103
|
+
arrays destined for JSON columns are serialized by the driver; passing pre-serialized JSON text
|
|
104
|
+
stores a JSON string. Arrays bound to PostgreSQL array columns retain null elements. SQL context
|
|
105
|
+
establishes array and JSON parameter types; cast standalone parameters to the intended SQL type.
|
|
106
|
+
Tenon uses the driver's raw-query path with the extended protocol so every public call executes
|
|
107
|
+
one statement, including calls without parameters. Application queries explicitly disable prepared
|
|
108
|
+
statement caching, including when the supplied driver client has `prepare: true`. Parameters remain
|
|
109
|
+
bound separately from SQL text.
|
|
77
110
|
|
|
78
111
|
Use an array parameter for a set of values:
|
|
79
112
|
|
|
@@ -192,13 +225,13 @@ To reset an update field to its default, write `column = DEFAULT` in the
|
|
|
192
225
|
surrounding SQL. Put arithmetic and `excluded.column` expressions there too; these helpers accept
|
|
193
226
|
values only, with no expression/default sentinel.
|
|
194
227
|
|
|
195
|
-
A compiled query
|
|
228
|
+
A compiled query accepts at most 65,533 bound parameters, including surrounding interpolations.
|
|
196
229
|
Tenon rejects larger queries and does not split them automatically. Choose batch sizes and
|
|
197
230
|
transaction boundaries in the caller. Helper validation errors identify invalid rows/columns
|
|
198
231
|
without printing cell values.
|
|
199
232
|
|
|
200
233
|
See the [mutation cookbook](mutations.md) for common patterns and
|
|
201
|
-
[
|
|
234
|
+
[query inspection](#query-inspection) for checking completed statements.
|
|
202
235
|
|
|
203
236
|
## Transactions, guarded writes, and errors
|
|
204
237
|
|
|
@@ -232,7 +265,7 @@ before data queries and `SET LOCAL statement_timeout = '5s'`. Manual savepoints
|
|
|
232
265
|
through SQL; reserve `tenon_affected_rows_` names for Tenon. Direct BEGIN/COMMIT/ROLLBACK boundaries
|
|
233
266
|
are owned by `transaction`.
|
|
234
267
|
|
|
235
|
-
SQL errors retain
|
|
268
|
+
SQL errors retain Postgres.js's codes and details. Invalid inputs and returned-row mismatches throw ordinary
|
|
236
269
|
`Error` objects. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
|
|
237
270
|
`TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. When both work and cleanup
|
|
238
271
|
fail, `AggregateError` preserves both failures. A lost commit acknowledgment can leave the outcome
|
|
@@ -247,8 +280,8 @@ transaction has committed.
|
|
|
247
280
|
|
|
248
281
|
`generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
|
|
249
282
|
connected schema. It does not need migration files or history. Table type generation is optional.
|
|
250
|
-
|
|
251
|
-
|
|
283
|
+
Generated files provide reusable row and mutation input types. The CLI writes that source
|
|
284
|
+
atomically. This command checks
|
|
252
285
|
the existing file without rewriting it:
|
|
253
286
|
|
|
254
287
|
```sh
|
|
@@ -276,24 +309,25 @@ named aliases preserve exact SQL names; the generated `users` alias also means `
|
|
|
276
309
|
Views, foreign tables, and attached partitions use query-specific types. A missing namespace
|
|
277
310
|
fails; an existing empty schema is valid.
|
|
278
311
|
|
|
279
|
-
| PostgreSQL type | Returned value
|
|
280
|
-
| ---------------------------------------------- |
|
|
281
|
-
| smallint, integer, oid, real, double precision | `number`
|
|
282
|
-
| bigint | `string`
|
|
283
|
-
| numeric | `string`
|
|
284
|
-
| boolean | `boolean`
|
|
285
|
-
| text, name, varchar, char, uuid, time, timetz | `string`
|
|
286
|
-
| date, timestamp, timestamptz | `Date
|
|
287
|
-
| enum | String literal union
|
|
288
|
-
| json, jsonb | `unknown`
|
|
289
|
-
| bytea | `Buffer`
|
|
312
|
+
| PostgreSQL type | Returned value | Generated write input |
|
|
313
|
+
| ---------------------------------------------- | ---------------------------------------- | --------------------------------------- |
|
|
314
|
+
| smallint, integer, oid, real, double precision | `number` | `number` |
|
|
315
|
+
| bigint | `string` | `string \| bigint` |
|
|
316
|
+
| numeric | `string` | `string \| number` |
|
|
317
|
+
| boolean | `boolean` | `boolean` |
|
|
318
|
+
| text, name, varchar, char, uuid, time, timetz | `string` | `string` |
|
|
319
|
+
| date, timestamp, timestamptz | `Date` (infinities become invalid dates) | `Date \| string` |
|
|
320
|
+
| enum | String literal union | Same union; `never` for an empty enum |
|
|
321
|
+
| json, jsonb | `unknown` | `Exclude<JSONValue, null>` (from Tenon) |
|
|
322
|
+
| bytea | `Buffer` | `Buffer` |
|
|
290
323
|
|
|
291
324
|
For returned rows, nullable columns add null. Domains use verified underlying mappings and the
|
|
292
325
|
column's own nullability. Unverified types warn and become `unknown`. Verified arrays use
|
|
293
326
|
`PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
|
|
294
|
-
contain
|
|
295
|
-
domains are unverified.
|
|
296
|
-
|
|
327
|
+
contain exact strings, like numeric scalars. Enum arrays and arrays of
|
|
328
|
+
domains are unverified. Date-only values use the driver's UTC date parsing; timestamps without time zone use Node's local timezone.
|
|
329
|
+
Infinite timestamps remain invalid `Date` objects: check validity before using date methods.
|
|
330
|
+
Custom Postgres.js parsers need matching application types.
|
|
297
331
|
|
|
298
332
|
### Insert and update inputs
|
|
299
333
|
|
|
@@ -323,9 +357,10 @@ Computed columns and `GENERATED ALWAYS` identity columns cannot receive values.
|
|
|
323
357
|
primary keys remain writable. Use `Pick` or `Omit` for application-specific restrictions and keep
|
|
324
358
|
expressions, `DEFAULT`, and special identity overrides in SQL.
|
|
325
359
|
|
|
326
|
-
Write representations in the table above also apply to verified array elements.
|
|
327
|
-
|
|
328
|
-
|
|
360
|
+
Write representations in the table above also apply to verified array elements. Pass JavaScript JSON
|
|
361
|
+
values directly; do not pre-serialize them. A bound null is SQL null. To construct JSON null explicitly,
|
|
362
|
+
use SQL such as `'null'::jsonb`. Native temporal serialization requires finite dates; strings such as
|
|
363
|
+
`'infinity'` are not valid bound date values. Use SQL literals for these PostgreSQL values. Unverified non-null write values use `never` and warn;
|
|
329
364
|
nullable fields still accept null. Supply an application-owned replacement when needed, for example
|
|
330
365
|
`Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for a reviewed interval-text input.
|
|
331
366
|
|
|
@@ -333,252 +368,26 @@ Apply these types through annotations, `satisfies`, or helper generic arguments
|
|
|
333
368
|
`sql.insertValues<NewProfile>(input)`. They check shapes and representations, not authorization,
|
|
334
369
|
constraints, JSON contents, or nonempty inputs. Optional fields accept explicit `undefined`,
|
|
335
370
|
including with `exactOptionalPropertyTypes`. Selected columns can discard required fields; see
|
|
336
|
-
the [cookbook example](mutations.md#input-types-
|
|
337
|
-
|
|
338
|
-
## SQL checking with SafeQL
|
|
339
|
-
|
|
340
|
-
SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
|
|
341
|
-
on your source files. Tenon runs the queries in the application; it does not import or run either
|
|
342
|
-
tool during query execution.
|
|
343
|
-
|
|
344
|
-
Configure [SafeQL normally](https://safeql.dev/api/). Tenon's optional `safeql` entry point exports
|
|
345
|
-
two objects:
|
|
346
|
-
|
|
347
|
-
- `safeqlPlugin` is a SafeQL plugin descriptor. Add it to each Tenon connection's `plugins` list. It
|
|
348
|
-
translates constant fragments, official mutation helpers, and identifiers, preserves parameter
|
|
349
|
-
behavior, and supplies pg result mappings. SafeQL owns database analysis and annotation fixes.
|
|
350
|
-
- `eslintPlugin` supplies `tenon/check-composition`. Register it in ESLint and enable the rule to
|
|
351
|
-
report queries that the adapter cannot resolve, including runtime-built fragments and escaped
|
|
352
|
-
outer templates.
|
|
353
|
-
|
|
354
|
-
Use both. The adapter skips unsupported queries; the companion rule makes those exclusions visible
|
|
355
|
-
as ordinary, individually suppressible lint errors. Neither replaces or wraps SafeQL's rule.
|
|
356
|
-
|
|
357
|
-
### Install and configure
|
|
358
|
-
|
|
359
|
-
Install the checking tools as application development dependencies, reusing the application's
|
|
360
|
-
existing TypeScript installation:
|
|
361
|
-
|
|
362
|
-
```sh
|
|
363
|
-
npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
These are development dependencies. Reuse your existing type-aware parser configuration and a
|
|
367
|
-
TypeScript version supported by the lint toolchain. Tenon's database runtime does not load them.
|
|
368
|
-
|
|
369
|
-
For a project with `src/**/*.ts` and a `tsconfig.json`:
|
|
370
|
-
|
|
371
|
-
```js
|
|
372
|
-
// eslint.config.mjs
|
|
373
|
-
import { defineConfig } from 'eslint/config';
|
|
374
|
-
import safeql from '@ts-safeql/eslint-plugin/config';
|
|
375
|
-
import tseslint from 'typescript-eslint';
|
|
376
|
-
import { safeqlPlugin, eslintPlugin } from '@kequtech/tenon-orm/safeql';
|
|
377
|
-
|
|
378
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
379
|
-
if (!databaseUrl) throw new Error('DATABASE_URL is required for SQL checking');
|
|
380
|
-
|
|
381
|
-
export default defineConfig({
|
|
382
|
-
files: ['src/**/*.ts'],
|
|
383
|
-
languageOptions: {
|
|
384
|
-
parser: tseslint.parser,
|
|
385
|
-
parserOptions: { projectService: true },
|
|
386
|
-
},
|
|
387
|
-
plugins: { tenon: eslintPlugin },
|
|
388
|
-
rules: { 'tenon/check-composition': 'error' },
|
|
389
|
-
extends: [
|
|
390
|
-
safeql.configs.connections({
|
|
391
|
-
databaseUrl,
|
|
392
|
-
plugins: [safeqlPlugin],
|
|
393
|
-
targets: [{ tag: 'sql' }],
|
|
394
|
-
}),
|
|
395
|
-
],
|
|
396
|
-
});
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
Add these settings to your existing ESLint setup if you have one; reuse its type-aware parser
|
|
400
|
-
configuration. `projectService` uses the nearest TypeScript project for each file. Supply
|
|
401
|
-
`DATABASE_URL` through your application's environment setup and point it at a development/test
|
|
402
|
-
database with migrations applied. This example does not load `.env` files.
|
|
403
|
-
|
|
404
|
-
`targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
|
|
405
|
-
single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
|
|
406
|
-
that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
|
|
407
|
-
the rule executes application code.
|
|
408
|
-
|
|
409
|
-
Reuse the application's lint scripts. If none exist, add these, adjusting file selection as needed.
|
|
410
|
-
They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
|
|
411
|
-
|
|
412
|
-
```json
|
|
413
|
-
{
|
|
414
|
-
"scripts": {
|
|
415
|
-
"lint": "eslint \"src/**/*.ts\"",
|
|
416
|
-
"lint:fix": "eslint \"src/**/*.ts\" --fix"
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### Generate and check annotations
|
|
422
|
-
|
|
423
|
-
For an existing persistence layer, start with the [incremental audit workflow](adoption-guide.md#adopt-sql-checking-incrementally).
|
|
424
|
-
It covers a separate SQL lint command, fix previews, and application refinements.
|
|
425
|
-
|
|
426
|
-
Write a query without a result annotation:
|
|
427
|
-
|
|
428
|
-
```ts
|
|
429
|
-
const query = sql`SELECT id, email FROM public.users WHERE id = ${id}`;
|
|
430
|
-
```
|
|
371
|
+
the [cookbook example](mutations.md#input-types-and-runtime-validation).
|
|
431
372
|
|
|
432
|
-
|
|
433
|
-
schema. It can also repair an incorrect annotation. Review the changes, run TypeScript and database
|
|
434
|
-
tests, and commit the result. Without an annotation, Tenon's result fields stay `unknown`.
|
|
435
|
-
|
|
436
|
-
Run `npm run lint` in CI without `--fix`. Missing/incorrect annotations, invalid SQL, and
|
|
437
|
-
unsupported composition fail the check without rewriting files. After schema changes, run the
|
|
438
|
-
development [`db:migrate` script](human-guide.md#create-a-table) before checking queries. Inspect
|
|
439
|
-
SQL mistakes before accepting types generated from an unintended query. ESLint's fix command may
|
|
440
|
-
also apply other enabled rules.
|
|
441
|
-
|
|
442
|
-
### Configuration choices and multiple databases
|
|
443
|
-
|
|
444
|
-
All connection, target, and override options belong to SafeQL. There is no Tenon-specific
|
|
445
|
-
configuration wrapper or separate database setting. Keep `safeqlPlugin` on each Tenon connection:
|
|
446
|
-
without it, SafeQL does not resolve Tenon fragments and can infer different parameter types from
|
|
447
|
-
those returned at runtime.
|
|
448
|
-
|
|
449
|
-
For multiple databases, pass a connection array to `safeql.configs.connections`. With
|
|
450
|
-
`usersDatabaseUrl` and `billingDatabaseUrl` set to your development/test URLs, replace the single
|
|
451
|
-
connection in the setup above with:
|
|
452
|
-
|
|
453
|
-
```js
|
|
454
|
-
safeql.configs.connections([
|
|
455
|
-
{
|
|
456
|
-
databaseUrl: usersDatabaseUrl,
|
|
457
|
-
plugins: [safeqlPlugin],
|
|
458
|
-
targets: [{ tag: 'usersSql' }],
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
databaseUrl: billingDatabaseUrl,
|
|
462
|
-
plugins: [safeqlPlugin],
|
|
463
|
-
targets: [{ tag: 'billingSql' }],
|
|
464
|
-
},
|
|
465
|
-
]);
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
Queries can then share a file:
|
|
469
|
-
|
|
470
|
-
```ts
|
|
471
|
-
import { sql as usersSql, sql as billingSql } from '@kequtech/tenon-orm';
|
|
472
|
-
|
|
473
|
-
const users = usersSql`SELECT id, email FROM public.users`;
|
|
474
|
-
const invoices = billingSql`SELECT id, total FROM public.invoices`;
|
|
475
|
-
```
|
|
373
|
+
## Query inspection
|
|
476
374
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
query
|
|
481
|
-
|
|
482
|
-
If queries are already separated by file, keep the `sql` tag and use an ESLint config object per
|
|
483
|
-
file group, each extending its own `safeql.configs.connections({ ... })`. Use non-overlapping
|
|
484
|
-
`files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
|
|
485
|
-
coexist; add Tenon's adapter only to connections checking Tenon queries.
|
|
486
|
-
|
|
487
|
-
Qualify same-named tables with their schemas, for example `SELECT id FROM billing.accounts`, so
|
|
488
|
-
the intended relation is explicit in both checking and execution.
|
|
489
|
-
|
|
490
|
-
### Supported SQL and limitations
|
|
491
|
-
|
|
492
|
-
Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
|
|
493
|
-
aliases, namespace imports, and object properties that retain its TypeScript type. It checks
|
|
494
|
-
constant fragments and `sql.join` with inline nonempty array literals, including nested fragments.
|
|
495
|
-
Separators and `identifier()` names must resolve to literal strings or const string initializers.
|
|
496
|
-
Values use the same parameter placeholders in static checking and execution, inside or outside
|
|
497
|
-
fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
|
|
498
|
-
JavaScript types.
|
|
499
|
-
|
|
500
|
-
The checker follows const fragment initializers, including imported constants with inspectable
|
|
501
|
-
source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
|
|
502
|
-
JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
|
|
503
|
-
unsupported even when a particular helper appears simple or a condition is a literal. Mutable
|
|
504
|
-
fragment references and dynamic identifiers are also unsupported. Arrays stored in variables or
|
|
505
|
-
assembled in loops are valid `sql.join` inputs at runtime but are not analyzed. Ordinary value
|
|
506
|
-
expressions remain bound parameters; calls that produce data for an official mutation helper can
|
|
507
|
-
use its explicit column-list contract.
|
|
508
|
-
|
|
509
|
-
`tenon/check-composition` points to the first unsupported interpolation in each query and explains
|
|
510
|
-
why it cannot be checked. For example, opaque mutation inputs need an inline column list, while
|
|
511
|
-
fragment-returning functions remain outside static analysis. Errors reached through shared constants
|
|
512
|
-
are reported at the consuming query. ESLint suppressions must cover the reported expression's line;
|
|
513
|
-
a scoped disable/enable can cover one multiline query.
|
|
514
|
-
|
|
515
|
-
Reusable authorization and other application policy can still live in fragment-returning functions
|
|
516
|
-
at runtime. Keep that policy centralized and cover its behavior with database tests. Suppress
|
|
517
|
-
`tenon/check-composition` only on the particular query, with a reason, when its variants are tested.
|
|
518
|
-
Do not duplicate policy merely to obtain static coverage. SQL and annotation errors in checkable
|
|
519
|
-
queries continue to come from `@ts-safeql/check-sql`.
|
|
520
|
-
|
|
521
|
-
Ordinary functions containing complete `sql` templates remain checkable. Each template is analyzed
|
|
522
|
-
where it is written, including constant fragments defined in that function:
|
|
375
|
+
`describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL connection URI
|
|
376
|
+
and a finished SQL string or `{ text }` query object. It returns parameter/result type metadata,
|
|
377
|
+
source constraints, conservative row declaration, effective session context, and coded findings.
|
|
378
|
+
It does not bind query values, execute the inspected statement, or verify `sql<Row>`.
|
|
523
379
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return sql`SELECT id FROM users WHERE ${predicate}`;
|
|
528
|
-
}
|
|
529
|
-
```
|
|
380
|
+
`tenon describe <query.sql>` prints that report as JSON using `DATABASE_URL`. Session flags are
|
|
381
|
+
`--role`, `--search-path`, and `--time-zone`. Timeout flags are `--connect-timeout-ms`,
|
|
382
|
+
`--statement-timeout-ms`, and `--lock-timeout-ms`; defaults are 3000, 5000, and 1000 ms.
|
|
530
383
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
by Tenon's adapter before checking.
|
|
536
|
-
|
|
537
|
-
This checks SQL structure and returned-row annotations, not JavaScript input compatibility or
|
|
538
|
-
runtime values. For example, a boolean passed to an integer comparison is rejected by PostgreSQL at
|
|
539
|
-
execution, not by this lint rule. Keep typed function parameters, validate external inputs, and test
|
|
540
|
-
database behavior. Explicit SQL casts are checked as part of the SQL.
|
|
541
|
-
|
|
542
|
-
The adapter supplies scalar mappings for bigint/numeric strings, unknown JSON, and `Date | number`
|
|
543
|
-
temporal values (including infinities). For recursive arrays, use column overrides matching
|
|
544
|
-
generated types, for example:
|
|
545
|
-
|
|
546
|
-
```js
|
|
547
|
-
safeql.configs.connections({
|
|
548
|
-
databaseUrl,
|
|
549
|
-
plugins: [safeqlPlugin],
|
|
550
|
-
targets: [{ tag: 'sql' }],
|
|
551
|
-
overrides: {
|
|
552
|
-
columns: { 'users.tags': 'PgArray<string>', 'users.amounts': 'PgArray<number>' },
|
|
553
|
-
},
|
|
554
|
-
});
|
|
555
|
-
```
|
|
384
|
+
Inspection owns a separate connection and uses Postgres.js mappings. Match the application's role
|
|
385
|
+
and settings; temporary tables, uncommitted changes, transaction-local settings, and custom parsers
|
|
386
|
+
are not inherited. Result nullability remains unknown. CLI exit 0 means description succeeded,
|
|
387
|
+
including reports with unresolved issues; execution and type annotations remain unchecked.
|
|
556
388
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
elements, custom parsers, and unverified PostgreSQL types need appropriate overrides and database
|
|
560
|
-
tests; a checker cannot infer your parser policy. Column keys are `Table.column`, so duplicate table
|
|
561
|
-
names across schemas need care. Type overrides merge with the adapter's defaults. `unsafeSql` is
|
|
562
|
-
outside static checking, and arbitrary JSON still needs application validation.
|
|
563
|
-
|
|
564
|
-
### Checking mutation fragments
|
|
565
|
-
|
|
566
|
-
SafeQL recognizes these official helpers directly through their Tenon function declarations,
|
|
567
|
-
including aliases. It never analyzes their implementation or executes application data builders.
|
|
568
|
-
Without an explicit column list, checking accepts inline object literals and inline insert arrays
|
|
569
|
-
of object literals. Keys must be literal names; object spreads, computed keys, methods, accessors,
|
|
570
|
-
and objects stored in variables require an explicit inline column list. Existing constant string
|
|
571
|
-
resolution works for entries of that list; column arrays stored in variables are unsupported.
|
|
572
|
-
|
|
573
|
-
For runtime inputs with explicit columns, the checker analyzes a representative nonempty insert
|
|
574
|
-
row or assignment list containing **all selected columns**. This checks column references, SQL
|
|
575
|
-
composition, conflict clauses, and `RETURNING` annotations. Runtime omission/default decisions and
|
|
576
|
-
batch length do not require interpreting JavaScript. The check is conservative: an optional
|
|
577
|
-
selected column is checked even when a particular runtime call omits it. It does not establish
|
|
578
|
-
nonempty input, valid runtime values, successful constraints, or every default's behavior; runtime
|
|
579
|
-
validation and database tests cover those contracts. Unknown inputs without explicit columns
|
|
580
|
-
produce `tenon/check-composition` diagnostics. See the [mutation cookbook](mutations.md) for
|
|
581
|
-
empty patches, conditional upserts, arrays, and batching.
|
|
389
|
+
See [query inspection](query-inspection.md) for public types, finding codes, session behavior, and
|
|
390
|
+
the workflow combining inspection with persistent application database tests.
|
|
582
391
|
|
|
583
392
|
## Command reference
|
|
584
393
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kequtech/tenon-orm",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -28,16 +28,13 @@
|
|
|
28
28
|
".": {
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"import": "./dist/index.js"
|
|
31
|
-
},
|
|
32
|
-
"./safeql": {
|
|
33
|
-
"types": "./dist/safeql.d.ts",
|
|
34
|
-
"import": "./dist/safeql.js"
|
|
35
31
|
}
|
|
36
32
|
},
|
|
37
33
|
"files": [
|
|
38
34
|
"dist/**/*.js",
|
|
39
35
|
"dist/**/*.d.ts",
|
|
40
|
-
"docs/"
|
|
36
|
+
"docs/",
|
|
37
|
+
"CHANGELOG.md"
|
|
41
38
|
],
|
|
42
39
|
"engines": {
|
|
43
40
|
"node": ">=24"
|
|
@@ -48,41 +45,23 @@
|
|
|
48
45
|
"typecheck": "node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json",
|
|
49
46
|
"test": "npm run build && node node_modules/typescript/bin/tsc -p config/tsconfig.test.json && node .test-dist/test/check-database.js && node --test .test-dist/test/*.test.js",
|
|
50
47
|
"test:consumer": "npm run build && node node_modules/typescript/bin/tsc -p config/tsconfig.test.json && node --test .test-dist/test/consumer.test.js",
|
|
48
|
+
"test:describe": "node node_modules/typescript/bin/tsc -p config/tsconfig.test.json && node --test .test-dist/test/describe*.test.js",
|
|
51
49
|
"check": "npm run format:check && npm run typecheck && npm test",
|
|
52
50
|
"prepack": "npm run build",
|
|
53
51
|
"prepublishOnly": "npm run check",
|
|
54
52
|
"format": "prettier --write .",
|
|
55
53
|
"format:check": "prettier --check .",
|
|
56
|
-
"benchmark": "npm run build && node scripts/benchmark.mjs"
|
|
54
|
+
"benchmark": "npm run build && node scripts/benchmark.mjs",
|
|
55
|
+
"test:pilots": "npm run build && node test/pilots/run.mjs"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"postgres": "3.4.9"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
59
61
|
"@types/node": "^24.19.2",
|
|
60
|
-
"@types/pg": "^8.23.1",
|
|
61
|
-
"pg": "^8.23.1",
|
|
62
62
|
"prettier": "^3.9.10",
|
|
63
63
|
"typescript": "~6.0.2",
|
|
64
|
-
"typescript-next": "npm:typescript@^7.0.2"
|
|
65
|
-
"@ts-safeql/eslint-plugin": "^5.4.2",
|
|
66
|
-
"typescript-eslint": "^8.58.0",
|
|
67
|
-
"eslint": "^10.0.0",
|
|
68
|
-
"@ts-safeql/plugin-utils": "^5.3.1"
|
|
69
|
-
},
|
|
70
|
-
"peerDependencies": {
|
|
71
|
-
"pg": "^8.23.1",
|
|
72
|
-
"typescript": ">=6",
|
|
73
|
-
"@ts-safeql/eslint-plugin": "^5.4.2",
|
|
74
|
-
"eslint": "^10.0.0"
|
|
75
|
-
},
|
|
76
|
-
"peerDependenciesMeta": {
|
|
77
|
-
"typescript": {
|
|
78
|
-
"optional": true
|
|
79
|
-
},
|
|
80
|
-
"@ts-safeql/eslint-plugin": {
|
|
81
|
-
"optional": true
|
|
82
|
-
},
|
|
83
|
-
"eslint": {
|
|
84
|
-
"optional": true
|
|
85
|
-
}
|
|
64
|
+
"typescript-next": "npm:typescript@^7.0.2"
|
|
86
65
|
},
|
|
87
66
|
"bin": {
|
|
88
67
|
"tenon": "./dist/cli.js"
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript';
|
|
2
|
-
export { CompositionError } from './static.js';
|
|
3
|
-
export declare function isExport(node: ts.Node, checker: ts.TypeChecker, name: string): boolean;
|
|
4
|
-
export declare function isFragment(type: ts.Type): boolean;
|
|
5
|
-
/** Reconstruct only immutable fragments and literal join lists. Never execute application code. */
|
|
6
|
-
export declare function renderFragment(node: ts.Node, checker: ts.TypeChecker, seen?: Set<ts.Node>): string;
|
|
7
|
-
export declare function renderIdentifier(node: ts.Node, checker: ts.TypeChecker, seen?: Set<ts.Node>): string;
|
|
8
|
-
export declare function isIdentifierType(type: ts.Type): boolean;
|
|
9
|
-
export declare function renderValue(node: ts.Node, checker: ts.TypeChecker, seen?: Set<ts.Node>): string;
|