@kequtech/tenon-orm 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +16 -20
- 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 +4 -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 +4 -4
- package/docs/adoption-guide.md +39 -76
- package/docs/agent-guide.md +82 -111
- package/docs/human-guide.md +42 -56
- package/docs/mutations.md +13 -13
- package/docs/query-inspection.md +128 -0
- package/docs/reference.md +81 -294
- 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,31 +1,28 @@
|
|
|
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
19
|
`createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, and
|
|
21
|
-
`close`. The required `connection` accepts a URL,
|
|
22
|
-
explicit `{}` selects
|
|
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.
|
|
@@ -47,17 +44,18 @@ automatically describe joins or aggregates.
|
|
|
47
44
|
`query` accepts `{ returns?, affectedRows?, label? }`. Unknown options are rejected. `label` must be
|
|
48
45
|
a nonempty string when supplied and is used for diagnostics.
|
|
49
46
|
|
|
50
|
-
| `returns` | Result
|
|
51
|
-
| ------------------ |
|
|
52
|
-
| `native` (default) |
|
|
53
|
-
| `many` | `Row[]`
|
|
54
|
-
| `oneOrNone` | `Row \| undefined`
|
|
55
|
-
| `one` | `Row`
|
|
47
|
+
| `returns` | Result | Returned rows allowed |
|
|
48
|
+
| ------------------ | ------------------------------------ | --------------------- |
|
|
49
|
+
| `native` (default) | Postgres.js `QueryResult<Row>` array | Any |
|
|
50
|
+
| `many` | `Row[]` | Any, including zero |
|
|
51
|
+
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
52
|
+
| `one` | `Row` | Exactly one |
|
|
56
53
|
|
|
57
|
-
Native results
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
Native results are arrays of rows with `command`, `count`, `columns`, `statement`, and `state`
|
|
55
|
+
metadata. `count` is the affected-row count, independent of `RETURNING`; it can be null for commands
|
|
56
|
+
without a count. `many` returns a plain row array. Result modes never add `LIMIT` or `RETURNING`.
|
|
57
|
+
A returned-row mismatch does not undo an autocommitted write. Use `affectedRows` for write limits
|
|
58
|
+
that roll back on failure.
|
|
61
59
|
|
|
62
60
|
Reusable queries are ordinary functions. Accept a `QueryClient` and named object inputs when useful;
|
|
63
61
|
both `Database` and `Transaction` implement `QueryClient`. Keep result annotations on the SQL and
|
|
@@ -70,15 +68,19 @@ The `sql` tag binds ordinary interpolations as `$1`, `$2`, etc. Never quote inte
|
|
|
70
68
|
manual parameter placeholders inside templates. Every public query executes one statement, including
|
|
71
69
|
parameterless queries.
|
|
72
70
|
|
|
73
|
-
Values use
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
types
|
|
71
|
+
Values use Postgres.js's native conversions. An undefined bound parameter is rejected. Objects and
|
|
72
|
+
arrays destined for JSON columns are serialized by the driver; passing pre-serialized JSON text
|
|
73
|
+
stores a JSON string. Arrays bound to PostgreSQL array columns retain null elements. SQL context
|
|
74
|
+
establishes array and JSON parameter types; cast standalone parameters to the intended SQL type.
|
|
75
|
+
Tenon uses the driver's raw-query path with the extended protocol so every public call executes
|
|
76
|
+
one statement, including calls without parameters. Application queries explicitly disable prepared
|
|
77
|
+
statement caching, including when the supplied driver client has `prepare: true`. Parameters remain
|
|
78
|
+
bound separately from SQL text.
|
|
77
79
|
|
|
78
80
|
Use an array parameter for a set of values:
|
|
79
81
|
|
|
80
82
|
```ts
|
|
81
|
-
const query = sql<{ id: number }>`SELECT id FROM public.
|
|
83
|
+
const query = sql<{ id: number }>`SELECT id FROM public.users
|
|
82
84
|
WHERE id = ANY(${ids}::integer[])`;
|
|
83
85
|
```
|
|
84
86
|
|
|
@@ -114,7 +116,7 @@ const conditions = [sql.fragment`active = ${true}`];
|
|
|
114
116
|
if (email !== undefined) conditions.push(sql.fragment`email = ${email}`);
|
|
115
117
|
if (afterId !== undefined) conditions.push(sql.fragment`id > ${afterId}`);
|
|
116
118
|
|
|
117
|
-
const query = sql<{ id: number }>`SELECT id FROM public.
|
|
119
|
+
const query = sql<{ id: number }>`SELECT id FROM public.users
|
|
118
120
|
WHERE ${sql.join(conditions, 'AND')} ORDER BY id`;
|
|
119
121
|
```
|
|
120
122
|
|
|
@@ -192,20 +194,20 @@ To reset an update field to its default, write `column = DEFAULT` in the
|
|
|
192
194
|
surrounding SQL. Put arithmetic and `excluded.column` expressions there too; these helpers accept
|
|
193
195
|
values only, with no expression/default sentinel.
|
|
194
196
|
|
|
195
|
-
A compiled query
|
|
197
|
+
A compiled query accepts at most 65,533 bound parameters, including surrounding interpolations.
|
|
196
198
|
Tenon rejects larger queries and does not split them automatically. Choose batch sizes and
|
|
197
199
|
transaction boundaries in the caller. Helper validation errors identify invalid rows/columns
|
|
198
200
|
without printing cell values.
|
|
199
201
|
|
|
200
202
|
See the [mutation cookbook](mutations.md) for common patterns and
|
|
201
|
-
[
|
|
203
|
+
[query inspection](#query-inspection) for checking completed statements.
|
|
202
204
|
|
|
203
205
|
## Transactions, guarded writes, and errors
|
|
204
206
|
|
|
205
207
|
```ts
|
|
206
208
|
await db.transaction(async (tx) => {
|
|
207
209
|
const id = 42;
|
|
208
|
-
await tx.query(sql`UPDATE public.
|
|
210
|
+
await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${id}`, {
|
|
209
211
|
affectedRows: { exact: 1 },
|
|
210
212
|
});
|
|
211
213
|
// Use tx for every query that belongs to this transaction.
|
|
@@ -232,7 +234,7 @@ before data queries and `SET LOCAL statement_timeout = '5s'`. Manual savepoints
|
|
|
232
234
|
through SQL; reserve `tenon_affected_rows_` names for Tenon. Direct BEGIN/COMMIT/ROLLBACK boundaries
|
|
233
235
|
are owned by `transaction`.
|
|
234
236
|
|
|
235
|
-
SQL errors retain
|
|
237
|
+
SQL errors retain Postgres.js's codes and details. Invalid inputs and returned-row mismatches throw ordinary
|
|
236
238
|
`Error` objects. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
|
|
237
239
|
`TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. When both work and cleanup
|
|
238
240
|
fail, `AggregateError` preserves both failures. A lost commit acknowledgment can leave the outcome
|
|
@@ -247,8 +249,8 @@ transaction has committed.
|
|
|
247
249
|
|
|
248
250
|
`generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
|
|
249
251
|
connected schema. It does not need migration files or history. Table type generation is optional.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
Generated files provide reusable row and mutation input types. The CLI writes that source
|
|
253
|
+
atomically. This command checks
|
|
252
254
|
the existing file without rewriting it:
|
|
253
255
|
|
|
254
256
|
```sh
|
|
@@ -260,31 +262,41 @@ current database. It never rewrites the file. Pending migration files
|
|
|
260
262
|
are a separate concern. The representation includes table columns, constraints, indexes, and
|
|
261
263
|
referenced domains, not every PostgreSQL object.
|
|
262
264
|
|
|
263
|
-
Generation reads a consistent snapshot of ordinary tables and partitioned roots. `Rows['
|
|
264
|
-
describes one returned row
|
|
265
|
-
|
|
265
|
+
Generation reads a consistent snapshot of ordinary tables and partitioned roots. `Rows['users']`
|
|
266
|
+
describes one returned row. Application type names can use PascalCase:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import type { Rows } from './generated-schema.js';
|
|
270
|
+
|
|
271
|
+
type User = Rows['users'];
|
|
272
|
+
type UserSummary = Pick<User, 'id' | 'email'>;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Use `sql<User>` for a full row or `sql<UserSummary>` for that projection. Generated table keys and
|
|
276
|
+
named aliases preserve exact SQL names; the generated `users` alias also means `Rows['users']`.
|
|
266
277
|
`type AuditRow = Rows['audit-log']` works for names that cannot be exported as identifiers.
|
|
267
278
|
Views, foreign tables, and attached partitions use query-specific types. A missing namespace
|
|
268
279
|
fails; an existing empty schema is valid.
|
|
269
280
|
|
|
270
|
-
| PostgreSQL type | Returned value
|
|
271
|
-
| ---------------------------------------------- |
|
|
272
|
-
| smallint, integer, oid, real, double precision | `number`
|
|
273
|
-
| bigint | `string`
|
|
274
|
-
| numeric | `string`
|
|
275
|
-
| boolean | `boolean`
|
|
276
|
-
| text, name, varchar, char, uuid, time, timetz | `string`
|
|
277
|
-
| date, timestamp, timestamptz | `Date
|
|
278
|
-
| enum | String literal union
|
|
279
|
-
| json, jsonb | `unknown`
|
|
280
|
-
| bytea | `Buffer`
|
|
281
|
+
| PostgreSQL type | Returned value | Generated write input |
|
|
282
|
+
| ---------------------------------------------- | ---------------------------------------- | --------------------------------------- |
|
|
283
|
+
| smallint, integer, oid, real, double precision | `number` | `number` |
|
|
284
|
+
| bigint | `string` | `string \| bigint` |
|
|
285
|
+
| numeric | `string` | `string \| number` |
|
|
286
|
+
| boolean | `boolean` | `boolean` |
|
|
287
|
+
| text, name, varchar, char, uuid, time, timetz | `string` | `string` |
|
|
288
|
+
| date, timestamp, timestamptz | `Date` (infinities become invalid dates) | `Date \| string` |
|
|
289
|
+
| enum | String literal union | Same union; `never` for an empty enum |
|
|
290
|
+
| json, jsonb | `unknown` | `Exclude<JSONValue, null>` (from Tenon) |
|
|
291
|
+
| bytea | `Buffer` | `Buffer` |
|
|
281
292
|
|
|
282
293
|
For returned rows, nullable columns add null. Domains use verified underlying mappings and the
|
|
283
294
|
column's own nullability. Unverified types warn and become `unknown`. Verified arrays use
|
|
284
295
|
`PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
|
|
285
|
-
contain
|
|
286
|
-
domains are unverified.
|
|
287
|
-
|
|
296
|
+
contain exact strings, like numeric scalars. Enum arrays and arrays of
|
|
297
|
+
domains are unverified. Date-only values use the driver's UTC date parsing; timestamps without time zone use Node's local timezone.
|
|
298
|
+
Infinite timestamps remain invalid `Date` objects: check validity before using date methods.
|
|
299
|
+
Custom Postgres.js parsers need matching application types.
|
|
288
300
|
|
|
289
301
|
### Insert and update inputs
|
|
290
302
|
|
|
@@ -314,9 +326,10 @@ Computed columns and `GENERATED ALWAYS` identity columns cannot receive values.
|
|
|
314
326
|
primary keys remain writable. Use `Pick` or `Omit` for application-specific restrictions and keep
|
|
315
327
|
expressions, `DEFAULT`, and special identity overrides in SQL.
|
|
316
328
|
|
|
317
|
-
Write representations in the table above also apply to verified array elements.
|
|
318
|
-
|
|
319
|
-
|
|
329
|
+
Write representations in the table above also apply to verified array elements. Pass JavaScript JSON
|
|
330
|
+
values directly; do not pre-serialize them. A bound null is SQL null. To construct JSON null explicitly,
|
|
331
|
+
use SQL such as `'null'::jsonb`. Native temporal serialization requires finite dates; strings such as
|
|
332
|
+
`'infinity'` are not valid bound date values. Use SQL literals for these PostgreSQL values. Unverified non-null write values use `never` and warn;
|
|
320
333
|
nullable fields still accept null. Supply an application-owned replacement when needed, for example
|
|
321
334
|
`Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for a reviewed interval-text input.
|
|
322
335
|
|
|
@@ -324,252 +337,26 @@ Apply these types through annotations, `satisfies`, or helper generic arguments
|
|
|
324
337
|
`sql.insertValues<NewProfile>(input)`. They check shapes and representations, not authorization,
|
|
325
338
|
constraints, JSON contents, or nonempty inputs. Optional fields accept explicit `undefined`,
|
|
326
339
|
including with `exactOptionalPropertyTypes`. Selected columns can discard required fields; see
|
|
327
|
-
the [cookbook example](mutations.md#input-types-
|
|
328
|
-
|
|
329
|
-
## SQL checking with SafeQL
|
|
330
|
-
|
|
331
|
-
SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
|
|
332
|
-
on your source files. Tenon runs the queries in the application; it does not import or run either
|
|
333
|
-
tool during query execution.
|
|
334
|
-
|
|
335
|
-
Configure [SafeQL normally](https://safeql.dev/api/). Tenon's optional `safeql` entry point exports
|
|
336
|
-
two objects:
|
|
337
|
-
|
|
338
|
-
- `safeqlPlugin` is a SafeQL plugin descriptor. Add it to each Tenon connection's `plugins` list. It
|
|
339
|
-
translates constant fragments, official mutation helpers, and identifiers, preserves parameter
|
|
340
|
-
behavior, and supplies pg result mappings. SafeQL owns database analysis and annotation fixes.
|
|
341
|
-
- `eslintPlugin` supplies `tenon/check-composition`. Register it in ESLint and enable the rule to
|
|
342
|
-
report queries that the adapter cannot resolve, including runtime-built fragments and escaped
|
|
343
|
-
outer templates.
|
|
340
|
+
the [cookbook example](mutations.md#input-types-and-runtime-validation).
|
|
344
341
|
|
|
345
|
-
|
|
346
|
-
as ordinary, individually suppressible lint errors. Neither replaces or wraps SafeQL's rule.
|
|
342
|
+
## Query inspection
|
|
347
343
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
```sh
|
|
354
|
-
npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
These are development dependencies. Reuse your existing type-aware parser configuration and a
|
|
358
|
-
TypeScript version supported by the lint toolchain. Tenon's database runtime does not load them.
|
|
359
|
-
|
|
360
|
-
For a project with `src/**/*.ts` and a `tsconfig.json`:
|
|
361
|
-
|
|
362
|
-
```js
|
|
363
|
-
// eslint.config.mjs
|
|
364
|
-
import { defineConfig } from 'eslint/config';
|
|
365
|
-
import safeql from '@ts-safeql/eslint-plugin/config';
|
|
366
|
-
import tseslint from 'typescript-eslint';
|
|
367
|
-
import { safeqlPlugin, eslintPlugin } from '@kequtech/tenon-orm/safeql';
|
|
368
|
-
|
|
369
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
370
|
-
if (!databaseUrl) throw new Error('DATABASE_URL is required for SQL checking');
|
|
371
|
-
|
|
372
|
-
export default defineConfig({
|
|
373
|
-
files: ['src/**/*.ts'],
|
|
374
|
-
languageOptions: {
|
|
375
|
-
parser: tseslint.parser,
|
|
376
|
-
parserOptions: { projectService: true },
|
|
377
|
-
},
|
|
378
|
-
plugins: { tenon: eslintPlugin },
|
|
379
|
-
rules: { 'tenon/check-composition': 'error' },
|
|
380
|
-
extends: [
|
|
381
|
-
safeql.configs.connections({
|
|
382
|
-
databaseUrl,
|
|
383
|
-
plugins: [safeqlPlugin],
|
|
384
|
-
targets: [{ tag: 'sql' }],
|
|
385
|
-
}),
|
|
386
|
-
],
|
|
387
|
-
});
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
Add these settings to your existing ESLint setup if you have one; reuse its type-aware parser
|
|
391
|
-
configuration. `projectService` uses the nearest TypeScript project for each file. Supply
|
|
392
|
-
`DATABASE_URL` through your application's environment setup and point it at a development/test
|
|
393
|
-
database with migrations applied. This example does not load `.env` files.
|
|
394
|
-
|
|
395
|
-
`targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
|
|
396
|
-
single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
|
|
397
|
-
that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
|
|
398
|
-
the rule executes application code.
|
|
399
|
-
|
|
400
|
-
Reuse the application's lint scripts. If none exist, add these, adjusting file selection as needed.
|
|
401
|
-
They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
|
|
402
|
-
|
|
403
|
-
```json
|
|
404
|
-
{
|
|
405
|
-
"scripts": {
|
|
406
|
-
"lint": "eslint \"src/**/*.ts\"",
|
|
407
|
-
"lint:fix": "eslint \"src/**/*.ts\" --fix"
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
```
|
|
344
|
+
`describeQuery({ connection, query, session?, timeouts? })` accepts a PostgreSQL connection URI
|
|
345
|
+
and a finished SQL string or `{ text }` query object. It returns parameter/result type metadata,
|
|
346
|
+
source constraints, conservative row declaration, effective session context, and coded findings.
|
|
347
|
+
It does not bind query values, execute the inspected statement, or verify `sql<Row>`.
|
|
411
348
|
|
|
412
|
-
|
|
349
|
+
`tenon describe <query.sql>` prints that report as JSON using `DATABASE_URL`. Session flags are
|
|
350
|
+
`--role`, `--search-path`, and `--time-zone`. Timeout flags are `--connect-timeout-ms`,
|
|
351
|
+
`--statement-timeout-ms`, and `--lock-timeout-ms`; defaults are 3000, 5000, and 1000 ms.
|
|
413
352
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
```ts
|
|
420
|
-
const query = sql`SELECT id, email FROM public."User" WHERE id = ${id}`;
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
Run `npm run lint:fix`. SafeQL can write `sql<{ id: number; email: string }>` using the connected
|
|
424
|
-
schema. It can also repair an incorrect annotation. Review the changes, run TypeScript and database
|
|
425
|
-
tests, and commit the result. Without an annotation, Tenon's result fields stay `unknown`.
|
|
426
|
-
|
|
427
|
-
Run `npm run lint` in CI without `--fix`. Missing/incorrect annotations, invalid SQL, and
|
|
428
|
-
unsupported composition fail the check without rewriting files. After schema changes, run the
|
|
429
|
-
development [`db:migrate` script](human-guide.md#create-a-table) before checking queries. Inspect
|
|
430
|
-
SQL mistakes before accepting types generated from an unintended query. ESLint's fix command may
|
|
431
|
-
also apply other enabled rules.
|
|
432
|
-
|
|
433
|
-
### Configuration choices and multiple databases
|
|
434
|
-
|
|
435
|
-
All connection, target, and override options belong to SafeQL. There is no Tenon-specific
|
|
436
|
-
configuration wrapper or separate database setting. Keep `safeqlPlugin` on each Tenon connection:
|
|
437
|
-
without it, SafeQL does not resolve Tenon fragments and can infer different parameter types from
|
|
438
|
-
those returned at runtime.
|
|
439
|
-
|
|
440
|
-
For multiple databases, pass a connection array to `safeql.configs.connections`. With
|
|
441
|
-
`usersDatabaseUrl` and `billingDatabaseUrl` set to your development/test URLs, replace the single
|
|
442
|
-
connection in the setup above with:
|
|
443
|
-
|
|
444
|
-
```js
|
|
445
|
-
safeql.configs.connections([
|
|
446
|
-
{
|
|
447
|
-
databaseUrl: usersDatabaseUrl,
|
|
448
|
-
plugins: [safeqlPlugin],
|
|
449
|
-
targets: [{ tag: 'usersSql' }],
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
databaseUrl: billingDatabaseUrl,
|
|
453
|
-
plugins: [safeqlPlugin],
|
|
454
|
-
targets: [{ tag: 'billingSql' }],
|
|
455
|
-
},
|
|
456
|
-
]);
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
Queries can then share a file:
|
|
460
|
-
|
|
461
|
-
```ts
|
|
462
|
-
import { sql as usersSql, sql as billingSql } from '@kequtech/tenon-orm';
|
|
463
|
-
|
|
464
|
-
const users = usersSql`SELECT id, email FROM public.users`;
|
|
465
|
-
const invoices = billingSql`SELECT id, total FROM public.invoices`;
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
The aliases only select a checker connection; they are the same runtime `sql` function. Execute each
|
|
469
|
-
query with the correct `createTenon` instance. Neither SafeQL nor Tenon's composition rule follows
|
|
470
|
-
`db.query` calls to infer which database will execute a query. Keep connection targets distinct so a
|
|
471
|
-
query is checked against its intended database.
|
|
472
|
-
|
|
473
|
-
If queries are already separated by file, keep the `sql` tag and use an ESLint config object per
|
|
474
|
-
file group, each extending its own `safeql.configs.connections({ ... })`. Use non-overlapping
|
|
475
|
-
`files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
|
|
476
|
-
coexist; add Tenon's adapter only to connections checking Tenon queries.
|
|
477
|
-
|
|
478
|
-
Qualify same-named tables with their schemas, for example `SELECT id FROM billing.accounts`, so
|
|
479
|
-
the intended relation is explicit in both checking and execution.
|
|
480
|
-
|
|
481
|
-
### Supported SQL and limitations
|
|
482
|
-
|
|
483
|
-
Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
|
|
484
|
-
aliases, namespace imports, and object properties that retain its TypeScript type. It checks
|
|
485
|
-
constant fragments and `sql.join` with inline nonempty array literals, including nested fragments.
|
|
486
|
-
Separators and `identifier()` names must resolve to literal strings or const string initializers.
|
|
487
|
-
Values use the same parameter placeholders in static checking and execution, inside or outside
|
|
488
|
-
fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
|
|
489
|
-
JavaScript types.
|
|
490
|
-
|
|
491
|
-
The checker follows const fragment initializers, including imported constants with inspectable
|
|
492
|
-
source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
|
|
493
|
-
JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
|
|
494
|
-
unsupported even when a particular helper appears simple or a condition is a literal. Mutable
|
|
495
|
-
fragment references and dynamic identifiers are also unsupported. Arrays stored in variables or
|
|
496
|
-
assembled in loops are valid `sql.join` inputs at runtime but are not analyzed. Ordinary value
|
|
497
|
-
expressions remain bound parameters; calls that produce data for an official mutation helper can
|
|
498
|
-
use its explicit column-list contract.
|
|
499
|
-
|
|
500
|
-
`tenon/check-composition` points to the first unsupported interpolation in each query and explains
|
|
501
|
-
why it cannot be checked. For example, opaque mutation inputs need an inline column list, while
|
|
502
|
-
fragment-returning functions remain outside static analysis. Errors reached through shared constants
|
|
503
|
-
are reported at the consuming query. ESLint suppressions must cover the reported expression's line;
|
|
504
|
-
a scoped disable/enable can cover one multiline query.
|
|
505
|
-
|
|
506
|
-
Reusable authorization and other application policy can still live in fragment-returning functions
|
|
507
|
-
at runtime. Keep that policy centralized and cover its behavior with database tests. Suppress
|
|
508
|
-
`tenon/check-composition` only on the particular query, with a reason, when its variants are tested.
|
|
509
|
-
Do not duplicate policy merely to obtain static coverage. SQL and annotation errors in checkable
|
|
510
|
-
queries continue to come from `@ts-safeql/check-sql`.
|
|
511
|
-
|
|
512
|
-
Ordinary functions containing complete `sql` templates remain checkable. Each template is analyzed
|
|
513
|
-
where it is written, including constant fragments defined in that function:
|
|
514
|
-
|
|
515
|
-
```ts
|
|
516
|
-
function findUsers(email: string) {
|
|
517
|
-
const predicate = sql.fragment`email = ${email}`;
|
|
518
|
-
return sql`SELECT id FROM users WHERE ${predicate}`;
|
|
519
|
-
}
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
Use literal line breaks in `sql` templates and bind string values with `${value}`. The composition
|
|
523
|
-
rule rejects JavaScript escapes such as `\n` or `\'` in the outer template: SafeQL reads raw
|
|
524
|
-
template text, while Tenon executes decoded JavaScript text. This diagnostic prevents checking
|
|
525
|
-
different SQL from what runs. Escapes inside bound values are fine; constant fragments are decoded
|
|
526
|
-
by Tenon's adapter before checking.
|
|
527
|
-
|
|
528
|
-
This checks SQL structure and returned-row annotations, not JavaScript input compatibility or
|
|
529
|
-
runtime values. For example, a boolean passed to an integer comparison is rejected by PostgreSQL at
|
|
530
|
-
execution, not by this lint rule. Keep typed function parameters, validate external inputs, and test
|
|
531
|
-
database behavior. Explicit SQL casts are checked as part of the SQL.
|
|
532
|
-
|
|
533
|
-
The adapter supplies scalar mappings for bigint/numeric strings, unknown JSON, and `Date | number`
|
|
534
|
-
temporal values (including infinities). For recursive arrays, use column overrides matching
|
|
535
|
-
generated types, for example:
|
|
536
|
-
|
|
537
|
-
```js
|
|
538
|
-
safeql.configs.connections({
|
|
539
|
-
databaseUrl,
|
|
540
|
-
plugins: [safeqlPlugin],
|
|
541
|
-
targets: [{ tag: 'sql' }],
|
|
542
|
-
overrides: {
|
|
543
|
-
columns: { 'User.tags': 'PgArray<string>', 'User.amounts': 'PgArray<number>' },
|
|
544
|
-
},
|
|
545
|
-
});
|
|
546
|
-
```
|
|
353
|
+
Inspection owns a separate connection and uses Postgres.js mappings. Match the application's role
|
|
354
|
+
and settings; temporary tables, uncommitted changes, transaction-local settings, and custom parsers
|
|
355
|
+
are not inherited. Result nullability remains unknown. CLI exit 0 means description succeeded,
|
|
356
|
+
including reports with unresolved issues; execution and type annotations remain unchecked.
|
|
547
357
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
elements, custom parsers, and unverified PostgreSQL types need appropriate overrides and database
|
|
551
|
-
tests; a checker cannot infer your parser policy. Column keys are `Table.column`, so duplicate table
|
|
552
|
-
names across schemas need care. Type overrides merge with the adapter's defaults. `unsafeSql` is
|
|
553
|
-
outside static checking, and arbitrary JSON still needs application validation.
|
|
554
|
-
|
|
555
|
-
### Checking mutation fragments
|
|
556
|
-
|
|
557
|
-
SafeQL recognizes these official helpers directly through their Tenon function declarations,
|
|
558
|
-
including aliases. It never analyzes their implementation or executes application data builders.
|
|
559
|
-
Without an explicit column list, checking accepts inline object literals and inline insert arrays
|
|
560
|
-
of object literals. Keys must be literal names; object spreads, computed keys, methods, accessors,
|
|
561
|
-
and objects stored in variables require an explicit inline column list. Existing constant string
|
|
562
|
-
resolution works for entries of that list; column arrays stored in variables are unsupported.
|
|
563
|
-
|
|
564
|
-
For runtime inputs with explicit columns, the checker analyzes a representative nonempty insert
|
|
565
|
-
row or assignment list containing **all selected columns**. This checks column references, SQL
|
|
566
|
-
composition, conflict clauses, and `RETURNING` annotations. Runtime omission/default decisions and
|
|
567
|
-
batch length do not require interpreting JavaScript. The check is conservative: an optional
|
|
568
|
-
selected column is checked even when a particular runtime call omits it. It does not establish
|
|
569
|
-
nonempty input, valid runtime values, successful constraints, or every default's behavior; runtime
|
|
570
|
-
validation and database tests cover those contracts. Unknown inputs without explicit columns
|
|
571
|
-
produce `tenon/check-composition` diagnostics. See the [mutation cookbook](mutations.md) for
|
|
572
|
-
empty patches, conditional upserts, arrays, and batching.
|
|
358
|
+
See [query inspection](query-inspection.md) for public types, finding codes, session behavior, and
|
|
359
|
+
the workflow combining inspection with persistent application database tests.
|
|
573
360
|
|
|
574
361
|
## Command reference
|
|
575
362
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kequtech/tenon-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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;
|