@kequtech/tenon-orm 0.1.4 → 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 +15 -19
- package/dist/affected-rows.d.ts +1 -1
- package/dist/affected-rows.js +6 -6
- package/dist/baseline.d.ts +1 -1
- package/dist/baseline.js +5 -5
- package/dist/cli.js +18 -39
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +18 -13
- package/dist/connection.js +14 -43
- package/dist/database.js +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 +76 -107
- package/docs/human-guide.md +20 -34
- package/docs/mutations.md +13 -13
- package/docs/query-inspection.md +128 -0
- package/docs/reference.md +66 -288
- 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/dist/transaction.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { reserveConnection } from './driver/session.js';
|
|
2
|
+
import { executeSql } from './connection.js';
|
|
2
3
|
/** The callback owns one connection and expires before commit or rollback begins. */
|
|
3
4
|
export async function withTransaction(pool, work) {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
5
|
+
const connection = await reserveConnection(pool, 'BEGIN');
|
|
6
|
+
const client = connection.sql;
|
|
6
7
|
let active = true;
|
|
7
|
-
let
|
|
8
|
+
let discard = false;
|
|
8
9
|
let savepointId = 0;
|
|
9
10
|
let pending = Promise.resolve();
|
|
10
11
|
let recoveryFailure;
|
|
@@ -20,16 +21,16 @@ export async function withTransaction(pool, work) {
|
|
|
20
21
|
if (!guarded)
|
|
21
22
|
return readResult(await executeSql(client, text, input));
|
|
22
23
|
const savepoint = `tenon_affected_rows_${++savepointId}`;
|
|
23
|
-
await client.
|
|
24
|
+
await client.unsafe(`SAVEPOINT ${savepoint}`);
|
|
24
25
|
try {
|
|
25
26
|
const result = readResult(await executeSql(client, text, input));
|
|
26
|
-
await client.
|
|
27
|
+
await client.unsafe(`RELEASE SAVEPOINT ${savepoint}`);
|
|
27
28
|
return result;
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
31
|
try {
|
|
31
|
-
await client.
|
|
32
|
-
await client.
|
|
32
|
+
await client.unsafe(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
33
|
+
await client.unsafe(`RELEASE SAVEPOINT ${savepoint}`);
|
|
33
34
|
}
|
|
34
35
|
catch (rollbackError) {
|
|
35
36
|
// Even if application code catches this error, the outer transaction
|
|
@@ -44,14 +45,13 @@ export async function withTransaction(pool, work) {
|
|
|
44
45
|
return operation;
|
|
45
46
|
};
|
|
46
47
|
try {
|
|
47
|
-
await client.query('BEGIN');
|
|
48
48
|
const result = await work(query);
|
|
49
49
|
active = false;
|
|
50
50
|
await pending;
|
|
51
51
|
connection.check();
|
|
52
52
|
if (recoveryFailure)
|
|
53
53
|
throw recoveryFailure;
|
|
54
|
-
const committed = await client.
|
|
54
|
+
const committed = await client.unsafe('COMMIT');
|
|
55
55
|
if (committed.command !== 'COMMIT') {
|
|
56
56
|
throw new Error('PostgreSQL rolled back the aborted transaction.');
|
|
57
57
|
}
|
|
@@ -61,15 +61,16 @@ export async function withTransaction(pool, work) {
|
|
|
61
61
|
active = false;
|
|
62
62
|
await pending;
|
|
63
63
|
try {
|
|
64
|
-
|
|
64
|
+
connection.check();
|
|
65
|
+
await client.unsafe('ROLLBACK');
|
|
65
66
|
}
|
|
66
67
|
catch (rollbackError) {
|
|
67
|
-
|
|
68
|
+
discard = true;
|
|
68
69
|
throw new AggregateError([error, rollbackError], 'Transaction and rollback failed.');
|
|
69
70
|
}
|
|
70
71
|
throw error;
|
|
71
72
|
}
|
|
72
73
|
finally {
|
|
73
|
-
connection.release(
|
|
74
|
+
await connection.release(discard);
|
|
74
75
|
}
|
|
75
76
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type { QueryResult
|
|
1
|
+
import type { QueryResult } from './connection.js';
|
|
2
2
|
import type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
|
|
3
3
|
import type { SqlQuery } from './query.js';
|
|
4
4
|
export interface QueryClient {
|
|
5
|
-
query<Row extends
|
|
5
|
+
query<Row extends object>(query: SqlQuery<Row>, options?: QueryOptions & {
|
|
6
6
|
readonly returns?: 'native';
|
|
7
7
|
}): NoInfer<Promise<QueryResult<Row>>>;
|
|
8
|
-
query<Row extends
|
|
8
|
+
query<Row extends object, Mode extends QueryReturnMode>(query: SqlQuery<Row>, options: QueryOptions & {
|
|
9
9
|
readonly returns: Mode;
|
|
10
10
|
}): NoInfer<Promise<QueryOutput<Row, Mode>>>;
|
|
11
|
-
query<Row extends
|
|
11
|
+
query<Row extends object>(query: SqlQuery<Row>, options: QueryOptions | undefined): NoInfer<Promise<QueryOutput<Row, QueryReturnMode>>>;
|
|
12
12
|
}
|
|
13
13
|
export type Transaction = QueryClient;
|
|
14
14
|
export type Database = Transaction & {
|
package/docs/adoption-guide.md
CHANGED
|
@@ -22,12 +22,11 @@ queries or generate types; keep the existing migration runner if that is the int
|
|
|
22
22
|
|
|
23
23
|
3. Replace queries with parameterized SQL in ordinary functions with named object inputs where
|
|
24
24
|
useful. Preserve each caller's projection, null handling, ordering, and transaction behavior.
|
|
25
|
-
Generated types describe
|
|
25
|
+
Generated types describe Postgres.js values: check bigint/numeric strings, dates, JSON, and arrays when
|
|
26
26
|
the old ORM transformed them.
|
|
27
|
-
4.
|
|
28
|
-
[
|
|
29
|
-
|
|
30
|
-
annotations. Run tests against a dedicated test database. Verify
|
|
27
|
+
4. Inspect completed queries and test their behavior using the
|
|
28
|
+
[inspection workflow](#inspect-queries-during-adoption) below. Declare result types explicitly
|
|
29
|
+
and run tests against a dedicated test database. Verify
|
|
31
30
|
writes and rollback, relationships, validation, and any hooks you moved into application code or
|
|
32
31
|
SQL. A schema-preserving first release makes application rollback easier.
|
|
33
32
|
5. Remove the old runtime dependency and unused generated code after its callers are gone. Keep the
|
|
@@ -50,81 +49,25 @@ in SQL and remove the local utilities after their callers move. Generated `Rows.
|
|
|
50
49
|
`Rows.Update` types can replace handwritten table inputs; retain application-specific refinements
|
|
51
50
|
and validation. Run the existing write and rollback workflows. See the [mutation cookbook](mutations.md).
|
|
52
51
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
For an existing application, including Next.js, SQL checking can run as a separate audit while the
|
|
56
|
-
normal lint/build commands keep their existing database requirements. Extend the existing flat
|
|
57
|
-
ESLint configuration rather than replacing it. After installing the
|
|
58
|
-
[checking dependencies](reference.md#install-and-configure), a single-database setup can use:
|
|
59
|
-
|
|
60
|
-
```js
|
|
61
|
-
// eslint.sql.config.mjs
|
|
62
|
-
import { defineConfig } from 'eslint/config';
|
|
63
|
-
import baseConfig from './eslint.config.mjs';
|
|
64
|
-
import safeql from '@ts-safeql/eslint-plugin/config';
|
|
65
|
-
import tseslint from 'typescript-eslint';
|
|
66
|
-
import { safeqlPlugin, eslintPlugin } from '@kequtech/tenon-orm/safeql';
|
|
67
|
-
|
|
68
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
69
|
-
if (!databaseUrl) throw new Error('DATABASE_URL is required for SQL checking');
|
|
70
|
-
|
|
71
|
-
export default defineConfig(baseConfig, {
|
|
72
|
-
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
73
|
-
languageOptions: {
|
|
74
|
-
parser: tseslint.parser,
|
|
75
|
-
parserOptions: { projectService: true },
|
|
76
|
-
},
|
|
77
|
-
plugins: { tenon: eslintPlugin },
|
|
78
|
-
rules: { 'tenon/check-composition': 'error' },
|
|
79
|
-
extends: [
|
|
80
|
-
safeql.configs.connections({
|
|
81
|
-
databaseUrl,
|
|
82
|
-
plugins: [safeqlPlugin],
|
|
83
|
-
targets: [{ tag: { regex: '.*' } }],
|
|
84
|
-
}),
|
|
85
|
-
],
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Preserve existing type-aware parser settings where configured. The broad target above lets Tenon
|
|
90
|
-
recognize aliases; use distinct targets for [multiple databases](reference.md#configuration-choices-and-multiple-databases).
|
|
91
|
-
The example does not load environment files. Use the application's existing environment loader
|
|
92
|
-
and URL selection to supply its migrated development/test database through `DATABASE_URL`.
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"scripts": {
|
|
97
|
-
"lint:sql": "eslint --config eslint.sql.config.mjs \"src/**/*.{ts,tsx}\""
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Start with `npm run lint:sql`. Keep both checking rules enabled and distinguish the findings:
|
|
52
|
+
## Inspect queries during adoption
|
|
103
53
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
mutation inputs where useful. Keep reusable authorization policy centralized when its fragments
|
|
109
|
-
remain unsupported, with database tests for runtime variants.
|
|
54
|
+
Use `describeQuery` from `@kequtech/tenon-orm` on the completed query object, or
|
|
55
|
+
`npx tenon describe query.sql` on a SQL file. Inspection is available with the normal Tenon
|
|
56
|
+
installation and uses no TypeScript compiler API. It works with helpers and runtime-built
|
|
57
|
+
fragments because it receives the final SQL. See [query inspection](query-inspection.md).
|
|
110
58
|
|
|
111
|
-
|
|
112
|
-
|
|
59
|
+
Reuse the application's connection URI and session configuration; compare the reported role,
|
|
60
|
+
search path, and timezone with execution. Inspection uses a separate connection and Postgres.js
|
|
61
|
+
read mappings. Custom parsers and transaction-local state remain application responsibilities.
|
|
113
62
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
63
|
+
Pair descriptions with durable database tests of actual rows and effects. For each converted
|
|
64
|
+
query, cover the meaningful filter/ownership branches, null relationships, JSON structure, empty
|
|
65
|
+
results, ordering/cursors, and transaction failure/replay behavior as applicable. Describe's success
|
|
66
|
+
does not verify a TypeScript annotation or prove that execution is permitted or succeeds. Preserve
|
|
67
|
+
existing authorization policy and transaction boundaries while making queries available to tests.
|
|
117
68
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
application. For example, require a finite `Date` before calling `.getTime()`, validate structured
|
|
121
|
-
JSON from `unknown`, and justify CHECK-constrained string refinements with matching schema checks
|
|
122
|
-
or a reviewed column override. Do not globally map timestamps to `Date`: pg can return numeric
|
|
123
|
-
infinities. See [checker mappings](reference.md#supported-sql-and-limitations).
|
|
124
|
-
|
|
125
|
-
Unsupported queries have not had their SQL verified. Keep database tests for their runtime variants
|
|
126
|
-
and use explained suppressions only where appropriate. Once the intended scope passes, make SQL
|
|
127
|
-
checking a required CI step. Fix commands can also apply other enabled ESLint rules.
|
|
69
|
+
Keep these tests in routine validation. A described query instance does not cover untested source
|
|
70
|
+
queries or detect stale annotations elsewhere.
|
|
128
71
|
|
|
129
72
|
## Move migration ownership to Tenon
|
|
130
73
|
|
|
@@ -144,6 +87,26 @@ server's major. Default scope is the whole database schema. Repeat `--schema nam
|
|
|
144
87
|
explicitly, accounting for dependencies, extensions, and required roles. Tenon's selected history
|
|
145
88
|
table is excluded; old ORM metadata is preserved for deliberate cleanup later.
|
|
146
89
|
|
|
90
|
+
The baseline recreates application schema, not a complete deployment environment. It omits
|
|
91
|
+
ownership, grants/revokes, and default privileges. PostgreSQL creates replayed objects under the
|
|
92
|
+
migration role. Role definitions and memberships also remain outside the dump. Functions,
|
|
93
|
+
policies, or other object definitions can still refer to roles; provision any referenced roles
|
|
94
|
+
before replay. See the [PostgreSQL pg_dump reference](https://www.postgresql.org/docs/16/app-pgdump.html).
|
|
95
|
+
|
|
96
|
+
For `SECURITY DEFINER` functions, the owner determines execution authority, and newly created
|
|
97
|
+
functions ordinarily permit `PUBLIC` execution. A captured function body alone does not preserve
|
|
98
|
+
its original security boundary. Keep fixed application policy, such as revoking `PUBLIC` execution,
|
|
99
|
+
in reviewed SQL in the baseline before its first adoption or in a subsequent application migration.
|
|
100
|
+
Deployment provisioning assigns the intended owners, schema access, and grants to runtime roles.
|
|
101
|
+
Keeping all security setup in provisioning is also supported, provided it runs before application access.
|
|
102
|
+
|
|
103
|
+
On fresh environments, provision required roles, apply migrations, then apply the deployment's
|
|
104
|
+
ownership/grant policy before starting the application. Verify both allowed and denied operations,
|
|
105
|
+
including execution of sensitive functions and access to their underlying tables. For a shared
|
|
106
|
+
database, apply only the selected application's policy. On existing environments, confirm the same
|
|
107
|
+
provisioning procedure is repeatable. After adoption, use subsequent migrations rather than editing
|
|
108
|
+
checksummed history.
|
|
109
|
+
|
|
147
110
|
Capture changes no source data, migration history, or sequence positions. Review and commit the SQL.
|
|
148
111
|
Keep the capture header: it identifies native dump SQL that bypasses the authored-migration scanner.
|
|
149
112
|
It is a convention, not proof of provenance; do not use it on handwritten migrations. Normal
|
package/docs/agent-guide.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tenon agent guide
|
|
2
2
|
|
|
3
|
-
Tenon executes PostgreSQL SQL through
|
|
3
|
+
Tenon executes PostgreSQL SQL through Postgres.js, with parameter binding, transactions, optional write
|
|
4
4
|
limits, SQL migrations, and generated row and mutation input types. Use ordinary functions for reusable queries.
|
|
5
5
|
This guide contains the setup and contracts needed for routine implementation; the
|
|
6
6
|
[reference](reference.md) covers additional options.
|
|
@@ -16,8 +16,9 @@ This guide contains the setup and contracts needed for routine implementation; t
|
|
|
16
16
|
composition.
|
|
17
17
|
3. Put reusable queries in functions. Accept a `QueryClient` and named object inputs when useful;
|
|
18
18
|
keep SQL and query options together. Call the same function with `db` or `tx`.
|
|
19
|
-
4. After SQL changes,
|
|
20
|
-
|
|
19
|
+
4. After SQL changes, inspect the completed queries and run typechecking and relevant database
|
|
20
|
+
assertions. Cover meaningful runtime variants and verify declared result contracts against
|
|
21
|
+
complete relevant rows.
|
|
21
22
|
5. Keep validation, authorization, retries, and parser configuration in the application. A quoted
|
|
22
23
|
identifier or correct row count does not prove authorization.
|
|
23
24
|
|
|
@@ -27,8 +28,7 @@ Requires Node.js 24+, PostgreSQL 16+, ESM, TypeScript 6+, and Node development t
|
|
|
27
28
|
application's strict TypeScript configuration.
|
|
28
29
|
|
|
29
30
|
```sh
|
|
30
|
-
npm install @kequtech/tenon-orm
|
|
31
|
-
npm install --save-dev @types/pg
|
|
31
|
+
npm install @kequtech/tenon-orm
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
```ts
|
|
@@ -39,9 +39,9 @@ if (!connection) throw new Error('DATABASE_URL is required');
|
|
|
39
39
|
export const db = createTenon({ connection });
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
`connection` accepts a URL,
|
|
42
|
+
`connection` accepts a URL, Postgres.js options, or an existing Postgres.js client. An explicit `{}` selects the driver's
|
|
43
43
|
environment defaults. Reuse one instance per database connection. `close()` closes owned pools;
|
|
44
|
-
supplied
|
|
44
|
+
supplied clients remain application-owned. The driver retains its native connection callbacks. Await
|
|
45
45
|
outstanding work before closing. Environment files are not loaded automatically. Qualify SQL names
|
|
46
46
|
or configure `search_path`; CLI `--namespace` does not change runtime query names.
|
|
47
47
|
|
|
@@ -53,10 +53,12 @@ import type { Rows } from './generated-schema.js';
|
|
|
53
53
|
|
|
54
54
|
type Summary = Pick<Rows['users'], 'id' | 'email'>;
|
|
55
55
|
|
|
56
|
-
function
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
function userByIdQuery({ id }: { id: number }) {
|
|
57
|
+
return sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function userById(client: QueryClient, input: { id: number }) {
|
|
61
|
+
return client.query(userByIdQuery(input), { returns: 'oneOrNone' });
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
const user = await userById(db, { id: 42 });
|
|
@@ -67,23 +69,29 @@ const user = await userById(db, { id: 42 });
|
|
|
67
69
|
Unannotated fields stay `unknown`. Generated types describe full table rows; joins, aliases,
|
|
68
70
|
aggregates, and expressions need projection-specific types.
|
|
69
71
|
|
|
70
|
-
| `returns` | Result
|
|
71
|
-
| ------------------ |
|
|
72
|
-
| `native` (default) |
|
|
73
|
-
| `many` | `Row[]`
|
|
74
|
-
| `oneOrNone` | `Row \| undefined`
|
|
75
|
-
| `one` | `Row`
|
|
72
|
+
| `returns` | Result | Row-count contract |
|
|
73
|
+
| ------------------ | ------------------------------------ | ------------------ |
|
|
74
|
+
| `native` (default) | Postgres.js `QueryResult<Row>` array | Any |
|
|
75
|
+
| `many` | `Row[]` | Any |
|
|
76
|
+
| `oneOrNone` | `Row \| undefined` | At most one |
|
|
77
|
+
| `one` | `Row` | Exactly one |
|
|
76
78
|
|
|
77
79
|
`query` options are `{ returns?, affectedRows?, label? }`; unknown options fail. Return modes never
|
|
78
80
|
insert `LIMIT` or `RETURNING`. Returned-row checks alone do not undo autocommitted writes. `label`
|
|
79
|
-
is an optional nonempty diagnostic string.
|
|
81
|
+
is an optional nonempty diagnostic string. Native results are row arrays with `.count`, `.command`,
|
|
82
|
+
and `.columns`; `many` returns a plain row array.
|
|
83
|
+
|
|
84
|
+
The Postgres.js driver is included. Undefined bound parameters fail. Pass JSON values directly,
|
|
85
|
+
including arrays, rather than calling `JSON.stringify`. Numeric and bigint scalars and arrays read
|
|
86
|
+
as strings. Dates read as `Date`, including invalid dates for timestamp infinities; check validity
|
|
87
|
+
before using them. SQL NULL array elements read as JavaScript null.
|
|
80
88
|
|
|
81
89
|
## Parameters and composition
|
|
82
90
|
|
|
83
91
|
- Ordinary interpolations bind values. Do not quote them or author manual `$n` placeholders inside
|
|
84
|
-
templates. `undefined`
|
|
92
|
+
templates. `undefined` is rejected; use `null` for SQL null.
|
|
85
93
|
- PostgreSQL infers parameter types from SQL. Cast standalone projections, e.g.
|
|
86
|
-
`SELECT ${value}::float8 AS value
|
|
94
|
+
`SELECT ${value}::float8 AS value`, so inspection and execution use the same type.
|
|
87
95
|
- Use `id = ANY(${ids}::integer[])` for an array of IDs. Do not build a comma-separated string of
|
|
88
96
|
values.
|
|
89
97
|
- `identifier(schema, table)` quotes separate application-approved components. Ordinary strings such
|
|
@@ -116,115 +124,74 @@ instead of application helpers that enumerate object keys. Keep tables, authoriz
|
|
|
116
124
|
expressions, conflict behavior and `RETURNING` visible in SQL. Insert missing/undefined fields use
|
|
117
125
|
PostgreSQL defaults; update missing/undefined fields are omitted. Explicit null stays null. Empty
|
|
118
126
|
batches/updates throw; a single default-only insert works. Omit generated columns and batch large
|
|
119
|
-
writes explicitly below the query-wide limit of 65,
|
|
127
|
+
writes explicitly below the query-wide limit of 65,533 parameters. Expressions and queries are
|
|
120
128
|
rejected as cell values; write arithmetic, `DEFAULT`, and `excluded.column` in the surrounding SQL.
|
|
121
129
|
|
|
122
130
|
Use generated `Rows.Insert['table']` and `Rows.Update['table']` for mutation inputs when available;
|
|
123
131
|
narrow editable fields with `Pick`. They account for defaults, nullability, domains, and generated
|
|
124
132
|
columns. Write mappings differ from returned rows: timestamps use `Date | string`, bigint uses
|
|
125
|
-
`string | bigint`, and JSON
|
|
133
|
+
`string | bigint`, and JSON accepts native JavaScript values through Tenon's exported `JSONValue` type. See the [input types](reference.md#insert-and-update-inputs).
|
|
126
134
|
|
|
127
135
|
Selected columns choose and order fields, e.g. `sql.insertValues(rows, ['name', 'score'])`. Unlisted
|
|
128
136
|
fields are ignored. See the [mutation contract](reference.md#object-based-mutation-fragments) and
|
|
129
137
|
[cookbook](mutations.md).
|
|
130
138
|
|
|
131
|
-
##
|
|
132
|
-
|
|
133
|
-
SafeQL checks SQL against the development/test schema and generates or verifies result annotations.
|
|
134
|
-
Configure it normally and add both exports from `@kequtech/tenon-orm/safeql`:
|
|
135
|
-
|
|
136
|
-
- `safeqlPlugin` goes in each SafeQL connection's `plugins`. It translates Tenon SQL and constant
|
|
137
|
-
fragments and supplies pg type mappings. SafeQL performs result inference.
|
|
138
|
-
- `eslintPlugin` goes in ESLint's `plugins`, with `tenon/check-composition` enabled. It reports
|
|
139
|
-
unsupported forms that the adapter skips. It does not wrap SafeQL's rule.
|
|
139
|
+
## Inspect and test completed queries
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
use a TypeScript version supported by the lint toolchain. Generated table types are optional.
|
|
141
|
+
Inspection ships with Tenon and needs no additional setup:
|
|
143
142
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
```sh
|
|
147
|
-
npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
|
|
148
|
-
```
|
|
143
|
+
```ts
|
|
144
|
+
import { describeQuery } from '@kequtech/tenon-orm';
|
|
149
145
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
import { defineConfig } from 'eslint/config';
|
|
153
|
-
import safeql from '@ts-safeql/eslint-plugin/config';
|
|
154
|
-
import tseslint from 'typescript-eslint';
|
|
155
|
-
import { safeqlPlugin, eslintPlugin } from '@kequtech/tenon-orm/safeql';
|
|
156
|
-
|
|
157
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
158
|
-
if (!databaseUrl) throw new Error('DATABASE_URL is required for SQL checking');
|
|
159
|
-
|
|
160
|
-
export default defineConfig({
|
|
161
|
-
files: ['src/**/*.ts'],
|
|
162
|
-
languageOptions: {
|
|
163
|
-
parser: tseslint.parser,
|
|
164
|
-
parserOptions: { projectService: true },
|
|
165
|
-
},
|
|
166
|
-
plugins: { tenon: eslintPlugin },
|
|
167
|
-
rules: { 'tenon/check-composition': 'error' },
|
|
168
|
-
extends: [
|
|
169
|
-
safeql.configs.connections({
|
|
170
|
-
databaseUrl,
|
|
171
|
-
plugins: [safeqlPlugin],
|
|
172
|
-
targets: [{ tag: 'sql' }],
|
|
173
|
-
}),
|
|
174
|
-
],
|
|
175
|
-
});
|
|
146
|
+
const query = userByIdQuery({ id: 42 });
|
|
147
|
+
const description = await describeQuery({ connection, query });
|
|
176
148
|
```
|
|
177
149
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
150
|
+
Pass the actual query object that will go to `db.query`, including fragments and conditional
|
|
151
|
+
predicates. A small ordinary query-building function can make the same statement available to
|
|
152
|
+
inspection and execution tests. Keep authorization policy inside the builder or shared fragments.
|
|
181
153
|
|
|
182
|
-
|
|
183
|
-
database, `{ tag: { regex: '.*' } }` includes all Tenon aliases and typed object properties; the
|
|
184
|
-
adapter excludes unrelated tags. The composition rule checks every Tenon query in its ESLint file
|
|
185
|
-
scope, independently of SafeQL's targets.
|
|
154
|
+
For a SQL file with PostgreSQL `$n` placeholders:
|
|
186
155
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
```json
|
|
190
|
-
{
|
|
191
|
-
"scripts": {
|
|
192
|
-
"lint": "eslint \"src/**/*.ts\"",
|
|
193
|
-
"lint:fix": "eslint \"src/**/*.ts\" --fix"
|
|
194
|
-
}
|
|
195
|
-
}
|
|
156
|
+
```sh
|
|
157
|
+
npx tenon describe query.sql
|
|
196
158
|
```
|
|
197
159
|
|
|
198
|
-
|
|
199
|
-
`
|
|
160
|
+
Supply `DATABASE_URL` through the application's existing environment setup. Match its role,
|
|
161
|
+
`search_path`, and timezone through the connection URI or optional `session` settings. Review
|
|
162
|
+
the returned `context`; this is a separate connection with Postgres.js read mappings. It does not
|
|
163
|
+
inherit custom parsers, temporary tables, or transaction-local state.
|
|
200
164
|
|
|
201
|
-
The
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
only on the particular query with a reason.
|
|
165
|
+
The report contains parameter/column types and findings with stable issue codes. Result nullability
|
|
166
|
+
is unknown, source `NOT NULL` is informational, and JSON remains unknown. CLI success means the
|
|
167
|
+
statement was described, not that `sql<Row>`, values, permissions, or business behavior were verified.
|
|
205
168
|
|
|
206
|
-
|
|
207
|
-
data-builder results need inline selected columns, e.g. `sql.insertValues(rows, ['name', 'score'])`.
|
|
208
|
-
The check uses all selected columns; it does not validate runtime input values or omission decisions.
|
|
169
|
+
With a test fixture containing user 42, check the database projection and the actual value separately:
|
|
209
170
|
|
|
210
|
-
|
|
211
|
-
|
|
171
|
+
```ts
|
|
172
|
+
import assert from 'node:assert/strict';
|
|
212
173
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
174
|
+
assert.deepEqual(
|
|
175
|
+
description.columns.map(({ name, readType }) => [name, readType]),
|
|
176
|
+
[
|
|
177
|
+
['id', 'number'],
|
|
178
|
+
['email', 'string'],
|
|
179
|
+
],
|
|
180
|
+
);
|
|
181
|
+
const expected = { id: 42, email: 'reader@example.test' } satisfies Summary;
|
|
182
|
+
assert.deepEqual(await userById(db, { id: expected.id }), expected);
|
|
183
|
+
```
|
|
219
184
|
|
|
220
|
-
|
|
221
|
-
|
|
185
|
+
The builder keeps inspection and application SQL together; the query function keeps execution options
|
|
186
|
+
and its `db`/`tx` contract together. Keep simple queries inline when they do not need a shared builder.
|
|
187
|
+
The typed fixture checks the declared shape, the description checks PostgreSQL's projection, and
|
|
188
|
+
execution checks the actual row. None of these proves coverage of other inputs or authorization rules.
|
|
222
189
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
190
|
+
Run the same query through the real application connection against its test database. Assert full
|
|
191
|
+
relevant rows/effects and exercise meaningful branches: ownership, optional filters, absent joins,
|
|
192
|
+
empty aggregates, pagination ties, JSON validation, and failed/replayed/rolled-back writes as
|
|
193
|
+
applicable. Keep these tests alongside the query; describing a single instance covers only that
|
|
194
|
+
instance. See [query inspection](query-inspection.md) for session options, limits, and issue codes.
|
|
228
195
|
|
|
229
196
|
## Transactions and writes
|
|
230
197
|
|
|
@@ -241,7 +208,7 @@ savepoint names beginning with `tenon_affected_rows_`.
|
|
|
241
208
|
|
|
242
209
|
Affected counts cover the final command, not extra trigger/cascade/CTE effects. They cannot detect
|
|
243
210
|
the wrong single row. Sequence advances and external effects are not reversible. A lost commit
|
|
244
|
-
acknowledgment may leave the outcome unknown; retry only according to application policy. Native
|
|
211
|
+
acknowledgment may leave the outcome unknown; retry only according to application policy. Native Postgres.js
|
|
245
212
|
errors retain their details; `AffectedRowsError` has codes `TENON_AFFECTED_ROWS_MISMATCH` and
|
|
246
213
|
`TENON_AFFECTED_ROWS_UNSUPPORTED`. Cleanup failures can produce `AggregateError`.
|
|
247
214
|
|
|
@@ -290,5 +257,7 @@ partitioned roots; views/foreign tables need authored query types.
|
|
|
290
257
|
When replacing another ORM, preserve its schema and behavior. If transferring migration ownership,
|
|
291
258
|
capture one reviewed baseline with `tenon baseline capture`, record it only on matching existing
|
|
292
259
|
databases with `tenon baseline adopt`, and use `tenon migrate` on fresh databases. Adoption does not
|
|
293
|
-
compare schemas or run baseline SQL.
|
|
294
|
-
|
|
260
|
+
compare schemas or run baseline SQL. Fresh replay omits ownership and grants; apply deployment
|
|
261
|
+
security provisioning before application access, especially for `SECURITY DEFINER` functions.
|
|
262
|
+
See [adoption](adoption-guide.md) for the full transition procedure. A down migration or schema-only
|
|
263
|
+
baseline is not a data backup.
|
package/docs/human-guide.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Using Tenon: a human guide
|
|
2
2
|
|
|
3
3
|
Tenon lets you use PostgreSQL directly from TypeScript. You write SQL; Tenon binds its inputs and
|
|
4
|
-
executes it through
|
|
4
|
+
executes it through Postgres.js. This guide follows a small application from its first table to reusable
|
|
5
5
|
queries, transactions, and result checking.
|
|
6
6
|
|
|
7
7
|
Use the [reference](reference.md) when you need exact options or unusual cases. The
|
|
@@ -13,8 +13,7 @@ Use the [reference](reference.md) when you need exact options or unusual cases.
|
|
|
13
13
|
In an existing Node.js 24+ TypeScript project using ESM:
|
|
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
|
Use PostgreSQL 16+ and TypeScript 6+. Keep your existing strict TypeScript configuration and Node
|
|
@@ -31,7 +30,7 @@ export const db = createTenon({ connection });
|
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
Reuse this instance across the application and call `await db.close()` at shutdown. For multiple
|
|
34
|
-
databases, create an instance for each connection. You can also supply an existing
|
|
33
|
+
databases, create an instance for each connection. You can also supply an existing Postgres.js client; your
|
|
35
34
|
application then closes that pool.
|
|
36
35
|
|
|
37
36
|
## Create a table
|
|
@@ -102,7 +101,7 @@ separately from the SQL text, even when the value contains SQL.
|
|
|
102
101
|
|
|
103
102
|
The type describes one selected row. `returns: 'oneOrNone'` gives you that row or `undefined`; it
|
|
104
103
|
throws if the query returns more than one row. Use `'one'` when absence should also be an error,
|
|
105
|
-
`'many'` for an array, or omit `returns` for the
|
|
104
|
+
`'many'` for an array, or omit `returns` for the native result array with `.count` and `.columns`.
|
|
106
105
|
These options do not change your SQL.
|
|
107
106
|
|
|
108
107
|
Generated table types can help describe projections without repeating every field:
|
|
@@ -114,8 +113,8 @@ type UserSummary = Pick<Rows['users'], 'id' | 'email'>;
|
|
|
114
113
|
|
|
115
114
|
You can then write `sql<UserSummary>`. A table type describes the full table; a join or aggregate
|
|
116
115
|
needs a type for what that particular query selects. An annotation is a declaration, not proof that
|
|
117
|
-
the SQL matches. Without one, result fields are `unknown`.
|
|
118
|
-
|
|
116
|
+
the SQL matches. Without one, result fields are `unknown`. Use the inspection and database-test
|
|
117
|
+
workflow below to check the declared contract against the query's behavior.
|
|
119
118
|
|
|
120
119
|
## Reuse a query with a function
|
|
121
120
|
|
|
@@ -190,36 +189,23 @@ choose which fields to write; they do not validate the complete object. See the
|
|
|
190
189
|
|
|
191
190
|
## Check your SQL and result types
|
|
192
191
|
|
|
193
|
-
|
|
192
|
+
Use `describeQuery({ connection, query })` on the completed query object, or
|
|
193
|
+
`npx tenon describe query.sql` on a SQL file, to inspect the server's parameter and result types.
|
|
194
|
+
This works with dynamic fragments and needs no additional checking setup. Match the application's
|
|
195
|
+
connection settings and keep database assertions for actual rows, nulls, JSON, permissions, and
|
|
196
|
+
transaction effects. See [query inspection](query-inspection.md).
|
|
194
197
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
SafeQL is optional development tooling run by ESLint. Tenon's adapter supports queries, constant
|
|
200
|
-
fragments, and mutation helpers; `tenon/check-composition` reports queries it cannot inspect.
|
|
201
|
-
Follow the one-time [SQL checking setup](reference.md#sql-checking-with-safeql).
|
|
202
|
-
|
|
203
|
-
Use the application's existing lint commands. If it has none, the setup shows `lint` and `lint:fix`.
|
|
204
|
-
They run ESLint, including all other enabled rules. Once configured, the normal workflow is:
|
|
198
|
+
TypeScript checks the application code and the result types you declare. Describe reports
|
|
199
|
+
PostgreSQL metadata; it does not generate or verify a TypeScript annotation. Use generated table
|
|
200
|
+
types where appropriate, declare projections explicitly, and check complete returned rows in tests.
|
|
205
201
|
|
|
206
202
|
1. Run `npm run db:migrate` against the development/test database.
|
|
207
|
-
2.
|
|
208
|
-
3.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
an incorrect declared result type before the query runs.
|
|
214
|
-
|
|
215
|
-
Input validation and database tests still matter. Checking a query does not prove that a request is
|
|
216
|
-
authorized, that its values are valid, or that returned JSON has an application-specific shape.
|
|
217
|
-
|
|
218
|
-
The checker supports constant fragments and `sql.join` with inline arrays. Runtime-built lists and
|
|
219
|
-
fragment-returning functions remain outside static analysis. Keep result annotations and database
|
|
220
|
-
tests for those queries; when using the checker, suppress `tenon/check-composition` only on the
|
|
221
|
-
tested query with a reason. Runtime-built mutation inputs need inline selected columns for SQL
|
|
222
|
-
checking. See the [checking reference](reference.md#supported-sql-and-limitations) for details.
|
|
203
|
+
2. Inspect the completed SQL and review the reported types and unresolved findings.
|
|
204
|
+
3. Run TypeScript and database tests. Keep these checks in routine CI validation.
|
|
205
|
+
|
|
206
|
+
Input validation and authorization remain application responsibilities. Describe cannot establish
|
|
207
|
+
that a request is authorized, that its values are valid, or that returned JSON has an
|
|
208
|
+
application-specific shape.
|
|
223
209
|
|
|
224
210
|
## Reuse or assemble parts of SQL when needed
|
|
225
211
|
|