@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.
Files changed (65) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +99 -2
  3. package/dist/affected-rows.d.ts +11 -0
  4. package/dist/affected-rows.js +30 -0
  5. package/dist/baseline.d.ts +19 -0
  6. package/dist/baseline.js +100 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +158 -0
  9. package/dist/client.d.ts +4 -0
  10. package/dist/client.js +54 -0
  11. package/dist/connection.d.ts +15 -0
  12. package/dist/connection.js +51 -0
  13. package/dist/database.d.ts +7 -0
  14. package/dist/database.js +44 -0
  15. package/dist/errors.d.ts +9 -0
  16. package/dist/errors.js +18 -0
  17. package/dist/generate.d.ts +9 -0
  18. package/dist/generate.js +34 -0
  19. package/dist/generation/catalog.d.ts +19 -0
  20. package/dist/generation/catalog.js +35 -0
  21. package/dist/generation/row-types.d.ts +2 -0
  22. package/dist/generation/row-types.js +115 -0
  23. package/dist/generation/schema-summary.d.ts +6 -0
  24. package/dist/generation/schema-summary.js +114 -0
  25. package/dist/index.d.ts +18 -0
  26. package/dist/index.js +7 -0
  27. package/dist/migrations/baseline-sql.d.ts +3 -0
  28. package/dist/migrations/baseline-sql.js +38 -0
  29. package/dist/migrations/connection.d.ts +9 -0
  30. package/dist/migrations/connection.js +65 -0
  31. package/dist/migrations/files.d.ts +21 -0
  32. package/dist/migrations/files.js +131 -0
  33. package/dist/migrations/history.d.ts +12 -0
  34. package/dist/migrations/history.js +30 -0
  35. package/dist/migrations/sql.d.ts +2 -0
  36. package/dist/migrations/sql.js +94 -0
  37. package/dist/migrations.d.ts +10 -0
  38. package/dist/migrations.js +68 -0
  39. package/dist/query-options.d.ts +15 -0
  40. package/dist/query-options.js +35 -0
  41. package/dist/query.d.ts +31 -0
  42. package/dist/query.js +110 -0
  43. package/dist/safeql/composition.d.ts +12 -0
  44. package/dist/safeql/composition.js +194 -0
  45. package/dist/safeql/eslint.d.ts +2 -0
  46. package/dist/safeql/eslint.js +45 -0
  47. package/dist/safeql/plugin.d.ts +3 -0
  48. package/dist/safeql/plugin.js +43 -0
  49. package/dist/safeql.d.ts +6 -0
  50. package/dist/safeql.js +7 -0
  51. package/dist/sql.d.ts +5 -0
  52. package/dist/sql.js +28 -0
  53. package/dist/transaction-sql.d.ts +2 -0
  54. package/dist/transaction-sql.js +67 -0
  55. package/dist/transaction.d.ts +4 -0
  56. package/dist/transaction.js +75 -0
  57. package/dist/types.d.ts +28 -0
  58. package/dist/types.js +1 -0
  59. package/dist/validation.d.ts +1 -0
  60. package/dist/validation.js +10 -0
  61. package/docs/adoption-guide.md +91 -0
  62. package/docs/agent-guide.md +289 -0
  63. package/docs/human-guide.md +253 -0
  64. package/docs/reference.md +456 -0
  65. 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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }