@kequtech/tenon-orm 0.0.0-stage → 0.1.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/LICENSE +15 -0
- package/README.md +99 -2
- package/dist/affected-rows.d.ts +11 -0
- package/dist/affected-rows.js +30 -0
- package/dist/baseline.d.ts +19 -0
- package/dist/baseline.js +100 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +158 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +54 -0
- package/dist/connection.d.ts +15 -0
- package/dist/connection.js +51 -0
- package/dist/database.d.ts +7 -0
- package/dist/database.js +44 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +18 -0
- package/dist/generate.d.ts +9 -0
- package/dist/generate.js +34 -0
- package/dist/generation/catalog.d.ts +19 -0
- package/dist/generation/catalog.js +35 -0
- package/dist/generation/row-types.d.ts +2 -0
- package/dist/generation/row-types.js +115 -0
- package/dist/generation/schema-summary.d.ts +6 -0
- package/dist/generation/schema-summary.js +114 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +7 -0
- package/dist/migrations/baseline-sql.d.ts +3 -0
- package/dist/migrations/baseline-sql.js +38 -0
- package/dist/migrations/connection.d.ts +9 -0
- package/dist/migrations/connection.js +65 -0
- package/dist/migrations/files.d.ts +21 -0
- package/dist/migrations/files.js +131 -0
- package/dist/migrations/history.d.ts +12 -0
- package/dist/migrations/history.js +30 -0
- package/dist/migrations/sql.d.ts +2 -0
- package/dist/migrations/sql.js +94 -0
- package/dist/migrations.d.ts +10 -0
- package/dist/migrations.js +68 -0
- package/dist/query-options.d.ts +15 -0
- package/dist/query-options.js +35 -0
- package/dist/query.d.ts +31 -0
- package/dist/query.js +110 -0
- package/dist/safeql/composition.d.ts +12 -0
- package/dist/safeql/composition.js +194 -0
- package/dist/safeql/eslint.d.ts +2 -0
- package/dist/safeql/eslint.js +45 -0
- package/dist/safeql/plugin.d.ts +3 -0
- package/dist/safeql/plugin.js +43 -0
- package/dist/safeql.d.ts +6 -0
- package/dist/safeql.js +7 -0
- package/dist/sql.d.ts +5 -0
- package/dist/sql.js +28 -0
- package/dist/transaction-sql.d.ts +2 -0
- package/dist/transaction-sql.js +67 -0
- package/dist/transaction.d.ts +4 -0
- package/dist/transaction.js +75 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +1 -0
- package/dist/validation.js +10 -0
- package/docs/adoption-guide.md +91 -0
- package/docs/agent-guide.md +289 -0
- package/docs/human-guide.md +253 -0
- package/docs/reference.md +456 -0
- package/package.json +92 -4
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# Tenon reference
|
|
2
|
+
|
|
3
|
+
Use this document to look up exact API behavior, checker setup, and limitations. The
|
|
4
|
+
[human guide](human-guide.md) teaches the workflow; the [agent guide](agent-guide.md) is a
|
|
5
|
+
self-contained implementation guide. [Adoption](adoption-guide.md) covers replacing an existing ORM
|
|
6
|
+
and transferring migration history.
|
|
7
|
+
|
|
8
|
+
## Installation and connection
|
|
9
|
+
|
|
10
|
+
Runtime requirements: Node.js 24+, PostgreSQL 16+, ESM, and `pg`. TypeScript consumers need
|
|
11
|
+
TypeScript 6+ and development typings for Node and `pg`. Keep your application's strict TypeScript
|
|
12
|
+
setup; Tenon needs no special compiler configuration. `"type": "module"` and TypeScript's
|
|
13
|
+
`"module": "NodeNext"` are a usual Node setup. Checker compatibility is listed separately below.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @kequtech/tenon-orm pg
|
|
17
|
+
npm install --save-dev @types/pg
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, and
|
|
21
|
+
`close`. The required `connection` accepts a URL, `pg.PoolConfig`, or existing `pg.Pool`. An
|
|
22
|
+
explicit `{}` selects pg's environment defaults. Tenon does not load environment files
|
|
23
|
+
automatically.
|
|
24
|
+
|
|
25
|
+
Reuse an instance per database connection. `close()` closes pools created by Tenon; supplied pools
|
|
26
|
+
remain application-owned. Finish outstanding work before closing. Install your own
|
|
27
|
+
`pool.on('error', handler)` on supplied pools; Tenon reports idle errors on owned pools through
|
|
28
|
+
`process.emitWarning`.
|
|
29
|
+
|
|
30
|
+
Qualify SQL names or configure the driver's `search_path`. The CLI's `--namespace` selects the
|
|
31
|
+
schema used by tooling, not a runtime query namespace.
|
|
32
|
+
|
|
33
|
+
## Queries and result modes
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const result = await db.query(sql<Row>`SELECT ...`, {
|
|
37
|
+
returns: 'many',
|
|
38
|
+
label: 'list-users',
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`sql<Row>` produces a `SqlQuery<Row>` with SQL `text` and ordered `values`. It does not execute.
|
|
43
|
+
`Row` describes one selected row; fields are `unknown` when no annotation is supplied. Type
|
|
44
|
+
annotations do not verify SQL or validate JSON. Full table types from generation do not
|
|
45
|
+
automatically describe joins or aggregates.
|
|
46
|
+
|
|
47
|
+
`query` accepts `{ returns?, affectedRows?, label? }`. Unknown options are rejected. `label` must be
|
|
48
|
+
a nonempty string when supplied and is used for diagnostics.
|
|
49
|
+
|
|
50
|
+
| `returns` | Result | Returned rows allowed |
|
|
51
|
+
| ------------------ | --------------------- | --------------------- |
|
|
52
|
+
| `native` (default) | pg `QueryResult<Row>` | Any |
|
|
53
|
+
| `many` | `Row[]` | Any, including zero |
|
|
54
|
+
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
55
|
+
| `one` | `Row` | Exactly one |
|
|
56
|
+
|
|
57
|
+
Native results retain `command`, `rowCount`, `fields`, and `rows`; a null row count means none was
|
|
58
|
+
reported. Result modes never add `LIMIT` or `RETURNING`. A count mismatch throws; returned-row
|
|
59
|
+
checks alone do not undo an autocommitted write. Use `affectedRows` for write limits that roll back
|
|
60
|
+
on failure.
|
|
61
|
+
|
|
62
|
+
Reusable queries are ordinary functions. Accept a `QueryClient` and named object inputs when useful;
|
|
63
|
+
both `Database` and `Transaction` implement `QueryClient`. Keep result annotations on the SQL and
|
|
64
|
+
execution options at the query call. Public types include `QueryOptions`, `QueryReturnMode`, and
|
|
65
|
+
`QueryOutput<Row, Mode>`.
|
|
66
|
+
|
|
67
|
+
## SQL parameters and identifiers
|
|
68
|
+
|
|
69
|
+
The `sql` tag binds ordinary interpolations as `$1`, `$2`, etc. Never quote interpolations or author
|
|
70
|
+
manual parameter placeholders inside templates. Every public query executes one statement, including
|
|
71
|
+
parameterless queries.
|
|
72
|
+
|
|
73
|
+
Values use pg's conversions, including arrays, JSON, dates, and `undefined` becoming SQL null. Cast
|
|
74
|
+
standalone parameter projections to the intended SQL type: `SELECT ${1.5} AS value` returns text,
|
|
75
|
+
while `SELECT ${1.5}::float8 AS value` returns a JavaScript number. PostgreSQL infers parameter
|
|
76
|
+
types from SQL context.
|
|
77
|
+
|
|
78
|
+
Use an array parameter for a set of values:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const query = sql<{ id: number }>`SELECT id FROM public."User"
|
|
82
|
+
WHERE id = ANY(${ids}::integer[])`;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`identifier(schema, table)` quotes each component separately. Names must be nonempty, contain no
|
|
86
|
+
NUL, and fit PostgreSQL's 63-byte identifier limit. Choose names from application-approved
|
|
87
|
+
alternatives; quoting does not authorize access. Ordinary strings such as `'DESC'` are bound values,
|
|
88
|
+
not SQL syntax. For ordering, use complete query alternatives or application-selected fragments
|
|
89
|
+
containing `ASC` or `DESC`; runtime-selected fragments need checker exclusions and tests.
|
|
90
|
+
|
|
91
|
+
`unsafeSql<Row>(text, values)` accepts externally authored, trusted SQL and an explicit values
|
|
92
|
+
array, including `[]` when empty. It bypasses static SQL checking. Never concatenate request input
|
|
93
|
+
into its text. Full `sql` or `unsafeSql` queries cannot be inserted into other queries; use
|
|
94
|
+
fragments for partial SQL.
|
|
95
|
+
|
|
96
|
+
## SQL fragments and joins
|
|
97
|
+
|
|
98
|
+
`sql.fragment` creates an immutable piece of SQL with bound values. It has no row type and cannot
|
|
99
|
+
execute alone. Insert it into a complete `sql<Row>` query. Constant fragments can share a predicate
|
|
100
|
+
between, for example, a list query and a count query. Ordinary fixed conditions are usually clearer
|
|
101
|
+
directly in the query.
|
|
102
|
+
|
|
103
|
+
`sql.join(fragments, separator)` combines a nonempty array of fragments into a new fragment.
|
|
104
|
+
Separators are exactly:
|
|
105
|
+
|
|
106
|
+
| Argument | Inserted SQL | Typical use |
|
|
107
|
+
| -------- | ------------ | -------------------------- |
|
|
108
|
+
| `'AND'` | `AND` | All conditions must hold |
|
|
109
|
+
| `'OR'` | `OR` | Any condition may hold |
|
|
110
|
+
| `','` | `, ` | Projections or assignments |
|
|
111
|
+
|
|
112
|
+
No default, arbitrary string, or fragment separator is accepted. Runtime validation also protects
|
|
113
|
+
JavaScript callers. A single item is returned as a new fragment without extra separators. Empty
|
|
114
|
+
lists and blank fragments throw; choose empty-list behavior explicitly in application code.
|
|
115
|
+
|
|
116
|
+
Joins do not add parentheses or alter SQL precedence. Group alternatives before combining them with
|
|
117
|
+
other conditions:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const alternatives = sql.join(
|
|
121
|
+
[sql.fragment`email = ${primary}`, sql.fragment`email = ${secondary}`],
|
|
122
|
+
'OR',
|
|
123
|
+
);
|
|
124
|
+
const conditions = sql.join(
|
|
125
|
+
[sql.fragment`(${alternatives})`, sql.fragment`active = ${true}`],
|
|
126
|
+
'AND',
|
|
127
|
+
);
|
|
128
|
+
const query = sql<{ id: number }>`SELECT id FROM public."User" WHERE ${conditions}`;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Each fragment use receives correctly ordered parameters, including repeated and nested uses. SQL
|
|
132
|
+
strings and comments containing literal `$1` text are left intact.
|
|
133
|
+
|
|
134
|
+
Static checking supports constant fragments, literal join arrays, and literal or const-string
|
|
135
|
+
separators. Runtime-built lists and selected fragments execute safely with respect to parameter
|
|
136
|
+
binding, but the checker cannot verify their SQL or result types. Prefer fixed SQL when it fits.
|
|
137
|
+
Otherwise keep an explicit result annotation, use a narrow explained lint suppression, and test
|
|
138
|
+
every variant.
|
|
139
|
+
|
|
140
|
+
## Transactions, guarded writes, and errors
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
await db.transaction(async (tx) => {
|
|
144
|
+
const id = 42;
|
|
145
|
+
await tx.query(sql`UPDATE public."User" SET active = false WHERE id = ${id}`, {
|
|
146
|
+
affectedRows: { exact: 1 },
|
|
147
|
+
});
|
|
148
|
+
// Use tx for every query that belongs to this transaction.
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Transactions belong to one database instance; they do not coordinate writes across databases. The
|
|
153
|
+
callback commits on success and rolls back when an error escapes. Await all work before returning;
|
|
154
|
+
the callback client expires afterward. A helper accepting `QueryClient` works with either `db` or
|
|
155
|
+
`tx`. Nested callback transactions and automatic retries are not provided.
|
|
156
|
+
|
|
157
|
+
`affectedRows: { exact: n }` requires n affected rows; `{ max: n }` permits up to n. Both require
|
|
158
|
+
nonnegative safe integers. Checks apply to INSERT, UPDATE, DELETE, and MERGE, independent of
|
|
159
|
+
`RETURNING`. A root call uses a transaction; a call on `tx` uses a savepoint. A mismatch rolls back
|
|
160
|
+
before throwing. You may catch a guarded failure and continue the outer transaction. Unguarded
|
|
161
|
+
PostgreSQL errors normally abort that transaction, even when caught.
|
|
162
|
+
|
|
163
|
+
The count covers the final command, excluding additional trigger, cascade, or CTE effects. It cannot
|
|
164
|
+
identify the wrong single row. Returned-row checks alone do not undo autocommitted writes. Rollback
|
|
165
|
+
cannot undo sequence advances or external side effects of database functions.
|
|
166
|
+
|
|
167
|
+
Set isolation and timeouts with SQL, for example `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`
|
|
168
|
+
before data queries and `SET LOCAL statement_timeout = '5s'`. Manual savepoints are available
|
|
169
|
+
through SQL; reserve `tenon_affected_rows_` names for Tenon. Direct BEGIN/COMMIT/ROLLBACK boundaries
|
|
170
|
+
are owned by `transaction`.
|
|
171
|
+
|
|
172
|
+
SQL errors retain pg's codes and details. Invalid inputs and returned-row mismatches throw ordinary
|
|
173
|
+
`Error` objects. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
|
|
174
|
+
`TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. When both work and cleanup
|
|
175
|
+
fail, `AggregateError` preserves both failures. A lost commit acknowledgment can leave the outcome
|
|
176
|
+
unknown; retry only when the application's operation makes that safe.
|
|
177
|
+
|
|
178
|
+
Optional `createTenon({ connection, onQuery })` observes completed calls with `label`, `durationMs`,
|
|
179
|
+
`outcome`, and available `command`, `rowCount`, `errorCode`. It receives no SQL or values. Observers
|
|
180
|
+
are not awaited; failures emit warnings. A successful callback query does not mean its outer
|
|
181
|
+
transaction has committed.
|
|
182
|
+
|
|
183
|
+
## Generated types and checking
|
|
184
|
+
|
|
185
|
+
`generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
|
|
186
|
+
connected schema. It does not need migration files or history. Table type generation is optional.
|
|
187
|
+
SafeQL reads the database directly and does not require these generated files; they are useful when
|
|
188
|
+
reusing table row shapes. The CLI writes that source atomically. This command checks the existing
|
|
189
|
+
file without rewriting it:
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
npx tenon generate --out src/generated-schema.ts --check
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`--check` compares the generated representation with the current database. Pending migration files
|
|
196
|
+
are a separate concern. The representation includes table columns, constraints, indexes, and
|
|
197
|
+
referenced domains, not every PostgreSQL object.
|
|
198
|
+
|
|
199
|
+
Generation reads a consistent snapshot of ordinary tables and partitioned roots. Use the generated
|
|
200
|
+
named type in queries, for example `sql<User>`. For other table names, define an alias such as
|
|
201
|
+
`type AuditRow = Rows['audit-log']` and use `sql<AuditRow>`; SafeQL resolves named aliases but not
|
|
202
|
+
indexed access directly in `sql<Rows['audit-log']>`. Views, foreign tables, and attached partitions
|
|
203
|
+
use query-specific types. A missing namespace fails; an existing empty schema is valid.
|
|
204
|
+
|
|
205
|
+
| PostgreSQL type | Default pg value |
|
|
206
|
+
| ---------------------------------------------- | ----------------------------------------------- |
|
|
207
|
+
| smallint, integer, oid, real, double precision | `number` |
|
|
208
|
+
| bigint, numeric | `string` |
|
|
209
|
+
| boolean | `boolean` |
|
|
210
|
+
| text, varchar, char, uuid, time, timetz | `string` |
|
|
211
|
+
| date, timestamp, timestamptz | `Date \| number` (numbers represent infinities) |
|
|
212
|
+
| enum | String literal union; `never` for an empty enum |
|
|
213
|
+
| json, jsonb | `unknown` |
|
|
214
|
+
| bytea | `Buffer` |
|
|
215
|
+
|
|
216
|
+
Nullable columns add null. Domains use verified underlying mappings and the column's own
|
|
217
|
+
nullability. Unverified types warn and become `unknown`. Verified arrays use
|
|
218
|
+
`PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
|
|
219
|
+
contain numbers and can lose precision; cast to text[] for exact values. Enum arrays and arrays of
|
|
220
|
+
domains are unverified. Finite dates and timestamps without time zone use Node's local timezone.
|
|
221
|
+
Custom pg parsers need matching application types.
|
|
222
|
+
|
|
223
|
+
## SQL checking with SafeQL
|
|
224
|
+
|
|
225
|
+
SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
|
|
226
|
+
on your source files. Tenon runs the queries in the application; it does not import or run either
|
|
227
|
+
tool during query execution.
|
|
228
|
+
|
|
229
|
+
Configure [SafeQL normally](https://safeql.dev/api/). Tenon's optional `safeql` entry point exports
|
|
230
|
+
two objects:
|
|
231
|
+
|
|
232
|
+
- `safeqlPlugin` is a SafeQL plugin descriptor. Add it to each Tenon connection's `plugins` list. It
|
|
233
|
+
translates constant fragments and identifiers, preserves runtime parameter behavior, and supplies
|
|
234
|
+
pg result mappings. SafeQL owns database analysis and annotation fixes.
|
|
235
|
+
- `eslintPlugin` supplies `tenon/check-composition`. Register it in ESLint and enable the rule to
|
|
236
|
+
report queries that the adapter cannot resolve, including runtime-built fragments and escaped
|
|
237
|
+
outer templates.
|
|
238
|
+
|
|
239
|
+
Use both. The adapter skips unsupported queries; the companion rule makes those exclusions visible
|
|
240
|
+
as ordinary, individually suppressible lint errors. Neither replaces or wraps SafeQL's rule.
|
|
241
|
+
|
|
242
|
+
### Install and configure
|
|
243
|
+
|
|
244
|
+
Install the checking tools as application development dependencies, reusing the application's
|
|
245
|
+
existing TypeScript installation:
|
|
246
|
+
|
|
247
|
+
```sh
|
|
248
|
+
npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
SafeQL and ESLint are the integration's hosts, and the adapter imports TypeScript to inspect query
|
|
252
|
+
expressions. They are optional peers because Tenon's database runtime does not need them.
|
|
253
|
+
`typescript-eslint` is an application-owned parser dependency, not a Tenon peer. Your ESLint
|
|
254
|
+
configuration imports SafeQL and the parser explicitly. Use a TypeScript version supported by that
|
|
255
|
+
lint toolchain. The `pg` driver remains Tenon's required runtime peer.
|
|
256
|
+
|
|
257
|
+
For a project with `src/**/*.ts` and a `tsconfig.json`:
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
// eslint.config.mjs
|
|
261
|
+
import { defineConfig } from 'eslint/config';
|
|
262
|
+
import safeql from '@ts-safeql/eslint-plugin/config';
|
|
263
|
+
import tseslint from 'typescript-eslint';
|
|
264
|
+
import { safeqlPlugin, eslintPlugin } from '@kequtech/tenon-orm/safeql';
|
|
265
|
+
|
|
266
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
267
|
+
if (!databaseUrl) throw new Error('DATABASE_URL is required for SQL checking');
|
|
268
|
+
|
|
269
|
+
export default defineConfig({
|
|
270
|
+
files: ['src/**/*.ts'],
|
|
271
|
+
languageOptions: {
|
|
272
|
+
parser: tseslint.parser,
|
|
273
|
+
parserOptions: { projectService: true },
|
|
274
|
+
},
|
|
275
|
+
plugins: { tenon: eslintPlugin },
|
|
276
|
+
rules: { 'tenon/check-composition': 'error' },
|
|
277
|
+
extends: [
|
|
278
|
+
safeql.configs.connections({
|
|
279
|
+
databaseUrl,
|
|
280
|
+
plugins: [safeqlPlugin],
|
|
281
|
+
targets: [{ tag: 'sql' }],
|
|
282
|
+
}),
|
|
283
|
+
],
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Add these settings to your existing ESLint setup if you have one; reuse its type-aware parser
|
|
288
|
+
configuration. `projectService` uses the nearest TypeScript project for each file. Supply
|
|
289
|
+
`DATABASE_URL` through your application's environment setup and point it at a development/test
|
|
290
|
+
database with migrations applied. This example does not load `.env` files.
|
|
291
|
+
|
|
292
|
+
The `.mjs` example avoids a current declaration mismatch between SafeQL's configuration helper and
|
|
293
|
+
ESLint's `defineConfig`. This affects configuration typechecking, not query result types.
|
|
294
|
+
|
|
295
|
+
`targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
|
|
296
|
+
single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
|
|
297
|
+
that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
|
|
298
|
+
the rule executes application code.
|
|
299
|
+
|
|
300
|
+
Reuse the application's lint scripts. If none exist, add these, adjusting file selection as needed.
|
|
301
|
+
They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"scripts": {
|
|
306
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
307
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Generate and check annotations
|
|
313
|
+
|
|
314
|
+
Write a query without a result annotation:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const query = sql`SELECT id, email FROM public."User" WHERE id = ${id}`;
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Run `npm run lint:fix`. SafeQL can write `sql<{ id: number; email: string }>` using the connected
|
|
321
|
+
schema. It can also repair an incorrect annotation. Review the changes, run TypeScript and database
|
|
322
|
+
tests, and commit the result. Without an annotation, Tenon's result fields stay `unknown`.
|
|
323
|
+
|
|
324
|
+
Run `npm run lint` in CI without `--fix`. Missing/incorrect annotations, invalid SQL, and
|
|
325
|
+
unsupported composition fail the check without rewriting files. After schema changes, run the
|
|
326
|
+
development [`db:migrate` script](human-guide.md#create-a-table) before checking queries. Inspect
|
|
327
|
+
SQL mistakes before accepting types generated from an unintended query. ESLint's fix command may
|
|
328
|
+
also apply other enabled rules.
|
|
329
|
+
|
|
330
|
+
### Configuration choices and multiple databases
|
|
331
|
+
|
|
332
|
+
All connection, target, and override options belong to SafeQL. There is no Tenon-specific
|
|
333
|
+
configuration wrapper or separate database setting. Keep `safeqlPlugin` on each Tenon connection:
|
|
334
|
+
without it, SafeQL does not resolve Tenon fragments and can infer different parameter types from
|
|
335
|
+
those returned at runtime.
|
|
336
|
+
|
|
337
|
+
For multiple databases, pass a connection array to `safeql.configs.connections`. With
|
|
338
|
+
`usersDatabaseUrl` and `billingDatabaseUrl` set to your development/test URLs, replace the single
|
|
339
|
+
connection in the setup above with:
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
safeql.configs.connections([
|
|
343
|
+
{
|
|
344
|
+
databaseUrl: usersDatabaseUrl,
|
|
345
|
+
plugins: [safeqlPlugin],
|
|
346
|
+
targets: [{ tag: 'usersSql' }],
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
databaseUrl: billingDatabaseUrl,
|
|
350
|
+
plugins: [safeqlPlugin],
|
|
351
|
+
targets: [{ tag: 'billingSql' }],
|
|
352
|
+
},
|
|
353
|
+
]);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Queries can then share a file:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { sql as usersSql, sql as billingSql } from '@kequtech/tenon-orm';
|
|
360
|
+
|
|
361
|
+
const users = usersSql`SELECT id, email FROM public.users`;
|
|
362
|
+
const invoices = billingSql`SELECT id, total FROM public.invoices`;
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The aliases only select a checker connection; they are the same runtime `sql` function. Execute each
|
|
366
|
+
query with the correct `createTenon` instance. Neither SafeQL nor Tenon's composition rule follows
|
|
367
|
+
`db.query` calls to infer which database will execute a query. Keep connection targets distinct so a
|
|
368
|
+
query is checked against its intended database.
|
|
369
|
+
|
|
370
|
+
If queries are already separated by file, keep the `sql` tag and use an ESLint config object per
|
|
371
|
+
file group, each extending its own `safeql.configs.connections({ ... })`. Use non-overlapping
|
|
372
|
+
`files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
|
|
373
|
+
coexist; add Tenon's adapter only to connections checking Tenon queries.
|
|
374
|
+
|
|
375
|
+
If a database has same-named tables in different schemas, qualify them in SQL, for example
|
|
376
|
+
`SELECT id FROM billing.accounts`. SafeQL can infer the wrong type for an unqualified table even
|
|
377
|
+
when the connection's `search_path` selects the correct table at execution. Explicit schema names
|
|
378
|
+
avoid that ambiguity.
|
|
379
|
+
|
|
380
|
+
### Supported SQL and limitations
|
|
381
|
+
|
|
382
|
+
Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
|
|
383
|
+
aliases, namespace imports, and object properties that retain its TypeScript type. It checks
|
|
384
|
+
constant fragments and `sql.join` with literal arrays, including nested fragments. Separators must
|
|
385
|
+
be literal strings or const strings. Literal `identifier()` names and const string names are
|
|
386
|
+
supported. Values use the same parameter placeholders in static checking and execution, inside or
|
|
387
|
+
outside fragments. PostgreSQL infers their types from the SQL context; Tenon does not add casts
|
|
388
|
+
based on JavaScript types. Mutable references, runtime lists, conditional fragments,
|
|
389
|
+
function-generated fragments, and dynamic identifiers produce `tenon/check-composition` diagnostics.
|
|
390
|
+
Suppress only that rule on the particular query when runtime variants have database tests. SQL and
|
|
391
|
+
annotation errors still come from `@ts-safeql/check-sql`. An imported fragment must have an
|
|
392
|
+
inspectable source initializer; declarations alone cannot describe its SQL. It does not execute
|
|
393
|
+
application code to discover SQL.
|
|
394
|
+
|
|
395
|
+
Use literal line breaks in `sql` templates and bind string values with `${value}`. The composition
|
|
396
|
+
rule rejects JavaScript escapes such as `\n` or `\'` in the outer template: SafeQL reads raw
|
|
397
|
+
template text, while Tenon executes decoded JavaScript text. This diagnostic prevents checking
|
|
398
|
+
different SQL from what runs. Escapes inside bound values are fine; constant fragments are decoded
|
|
399
|
+
by Tenon's adapter before checking.
|
|
400
|
+
|
|
401
|
+
This checks SQL structure and returned-row annotations, not JavaScript input compatibility or
|
|
402
|
+
runtime values. For example, a boolean passed to an integer comparison is rejected by PostgreSQL at
|
|
403
|
+
execution, not by this lint rule. Keep typed function parameters, validate external inputs, and test
|
|
404
|
+
database behavior. Explicit SQL casts are checked as part of the SQL.
|
|
405
|
+
|
|
406
|
+
The adapter supplies scalar mappings for bigint/numeric strings, unknown JSON, and `Date | number`
|
|
407
|
+
temporal values (including infinities). For recursive arrays, use column overrides matching
|
|
408
|
+
generated types, for example:
|
|
409
|
+
|
|
410
|
+
```js
|
|
411
|
+
safeql.configs.connections({
|
|
412
|
+
databaseUrl,
|
|
413
|
+
plugins: [safeqlPlugin],
|
|
414
|
+
targets: [{ tag: 'sql' }],
|
|
415
|
+
overrides: {
|
|
416
|
+
columns: { 'User.tags': 'PgArray<string>', 'User.amounts': 'PgArray<number>' },
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Import `PgArray` from generated definitions in files using that annotation. Numeric arrays use
|
|
422
|
+
numbers with pg's default parser, even though numeric scalars use strings. Array dimensions/null
|
|
423
|
+
elements, custom parsers, and unverified PostgreSQL types need appropriate overrides and database
|
|
424
|
+
tests; a checker cannot infer your parser policy. Column keys are `Table.column`, so duplicate table
|
|
425
|
+
names across schemas need care. Type overrides merge with the adapter's defaults. `unsafeSql` is
|
|
426
|
+
outside static checking, and arbitrary JSON still needs application validation.
|
|
427
|
+
|
|
428
|
+
## Command reference
|
|
429
|
+
|
|
430
|
+
All database commands use `DATABASE_URL`; environment files are not loaded automatically. For
|
|
431
|
+
multiple databases, run commands with the intended database's URL and migration directory, and use
|
|
432
|
+
distinct generated output files if generating table types.
|
|
433
|
+
|
|
434
|
+
```text
|
|
435
|
+
tenon new <name> [--directory migrations]
|
|
436
|
+
tenon migrate [--directory migrations] [--namespace public]
|
|
437
|
+
tenon status [--directory migrations] [--namespace public]
|
|
438
|
+
tenon rollback [--directory migrations] [--namespace public] [--steps 1]
|
|
439
|
+
tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
|
|
440
|
+
tenon baseline capture|adopt [--directory migrations] [--namespace public]
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Migration filenames are `<numeric-id>_<name>.up.sql` and optional matching down files. IDs sort
|
|
444
|
+
numerically. Each migration and its history entry commit together; earlier files remain applied if a
|
|
445
|
+
later file fails. Status reports applied/pending IDs. Rollback checks that every selected file has
|
|
446
|
+
down SQL before executing any. Use dollar-quoted function bodies and explicit `E'...'` for backslash
|
|
447
|
+
escapes. Nontransactional DDL such as `CREATE INDEX CONCURRENTLY` needs a separate procedure.
|
|
448
|
+
|
|
449
|
+
Tools coordinate through session advisory locks per database/namespace. Use direct connections or
|
|
450
|
+
session pooling, with PostgreSQL timeouts to bound waiting.
|
|
451
|
+
|
|
452
|
+
Programmatic `migrate`, `migrationStatus`, and `rollback` accept
|
|
453
|
+
`{ connection, directory?, namespace? }`; rollback also accepts `steps`.
|
|
454
|
+
`createMigration({ name, directory? })` scaffolds files; `readMigrations(directory?)` reads
|
|
455
|
+
validated SQL and checksums. Baseline capture/adoption are covered in the
|
|
456
|
+
[adoption guide](adoption-guide.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,94 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kequtech/tenon-orm",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "ISC",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "A PostgreSQL ORM designed for coding agents, with explicit SQL, typed queries, and migrations",
|
|
7
|
+
"homepage": "https://github.com/kequtech/tenon-orm#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+ssh://git@github.com/kequtech/tenon-orm.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kequtech/tenon-orm/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"orm",
|
|
17
|
+
"postgresql",
|
|
18
|
+
"sql",
|
|
19
|
+
"typescript",
|
|
20
|
+
"migrations",
|
|
21
|
+
"agents"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org/"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./safeql": {
|
|
33
|
+
"types": "./dist/safeql.d.ts",
|
|
34
|
+
"import": "./dist/safeql.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/**/*.js",
|
|
39
|
+
"dist/**/*.d.ts",
|
|
40
|
+
"docs/"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=24"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"clean": "node scripts/clean.mjs",
|
|
47
|
+
"build": "node scripts/build.mjs",
|
|
48
|
+
"typecheck": "node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json",
|
|
49
|
+
"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
|
+
"test:consumer": "npm run build && node node_modules/typescript/bin/tsc -p config/tsconfig.test.json && node --test .test-dist/test/consumer.test.js",
|
|
51
|
+
"check": "npm run format:check && npm run typecheck && npm test",
|
|
52
|
+
"prepack": "npm run build",
|
|
53
|
+
"prepublishOnly": "npm run check",
|
|
54
|
+
"format": "prettier --write .",
|
|
55
|
+
"format:check": "prettier --check .",
|
|
56
|
+
"benchmark": "npm run build && node scripts/benchmark.mjs"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^24.19.2",
|
|
60
|
+
"@types/pg": "^8.23.1",
|
|
61
|
+
"pg": "^8.23.1",
|
|
62
|
+
"prettier": "^3.9.10",
|
|
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
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"bin": {
|
|
88
|
+
"tenon": "./dist/cli.js"
|
|
89
|
+
},
|
|
90
|
+
"prettier": {
|
|
91
|
+
"singleQuote": true,
|
|
92
|
+
"printWidth": 100
|
|
93
|
+
}
|
|
94
|
+
}
|