@kequtech/tenon-orm 0.3.1 → 0.4.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 +51 -0
- package/README.md +27 -5
- package/dist/affected-rows.js +0 -1
- package/dist/baseline.js +3 -5
- package/dist/check/contracts.d.ts +6 -0
- package/dist/check/contracts.js +206 -0
- package/dist/check/project.d.ts +18 -0
- package/dist/check/project.js +224 -0
- package/dist/check/source.d.ts +51 -0
- package/dist/check/source.js +629 -0
- package/dist/check/types.d.ts +36 -0
- package/dist/check/types.js +1 -0
- package/dist/check.d.ts +4 -0
- package/dist/check.js +147 -0
- package/dist/cli.js +38 -9
- package/dist/connection.d.ts +5 -12
- package/dist/connection.js +12 -121
- package/dist/database.js +5 -8
- package/dist/describe.js +10 -3
- package/dist/generate.d.ts +2 -3
- package/dist/generate.js +26 -24
- package/dist/generation/catalog.d.ts +5 -6
- package/dist/generation/catalog.js +7 -9
- package/dist/generation/render.d.ts +3 -0
- package/dist/generation/render.js +60 -0
- package/dist/generation/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inspection/catalog.d.ts +3 -6
- package/dist/inspection/catalog.js +9 -18
- package/dist/inspection/cli.d.ts +12 -0
- package/dist/inspection/cli.js +9 -4
- package/dist/inspection/connection.d.ts +2 -3
- package/dist/inspection/connection.js +20 -22
- package/dist/inspection/options.js +0 -6
- package/dist/inspection/result.d.ts +3 -2
- package/dist/inspection/result.js +13 -26
- package/dist/inspection/types.d.ts +6 -4
- package/dist/migrations/connection.d.ts +3 -3
- package/dist/migrations/connection.js +14 -21
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -9
- package/dist/query-options.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +21 -77
- package/dist/types.d.ts +1 -1
- package/docs/adoption-guide.md +54 -12
- package/docs/agent-guide.md +56 -28
- package/docs/human-guide.md +50 -19
- package/docs/mutations.md +22 -9
- package/docs/query-checking.md +200 -0
- package/docs/query-inspection.md +40 -13
- package/docs/reference.md +247 -200
- package/package.json +11 -7
- package/dist/driver/types.d.ts +0 -10
- package/dist/driver/types.js +0 -49
- package/dist/generation/table-types.d.ts +0 -2
- package/dist/generation/table-types.js +0 -96
- package/dist/inspection/protocol.d.ts +0 -7
- package/dist/inspection/protocol.js +0 -45
package/dist/transaction.js
CHANGED
|
@@ -1,81 +1,25 @@
|
|
|
1
|
-
import { executeSql
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const operation = pending.then(async () => {
|
|
18
|
-
connection.check();
|
|
19
|
-
if (recoveryFailure)
|
|
20
|
-
throw recoveryFailure;
|
|
21
|
-
if (!guarded)
|
|
22
|
-
return readResult(await executeSql(client, text, input));
|
|
23
|
-
const savepoint = `tenon_affected_rows_${++savepointId}`;
|
|
24
|
-
await client.query(`SAVEPOINT ${savepoint}`);
|
|
25
|
-
try {
|
|
26
|
-
const result = readResult(await executeSql(client, text, input));
|
|
27
|
-
await client.query(`RELEASE SAVEPOINT ${savepoint}`);
|
|
28
|
-
return result;
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
try {
|
|
32
|
-
await client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
33
|
-
await client.query(`RELEASE SAVEPOINT ${savepoint}`);
|
|
34
|
-
}
|
|
35
|
-
catch (rollbackError) {
|
|
36
|
-
// Even if application code catches this error, the outer transaction
|
|
37
|
-
// must not commit when the rejected write could still be present.
|
|
38
|
-
recoveryFailure = new AggregateError([error, rollbackError], 'Operation and savepoint recovery failed.');
|
|
39
|
-
throw recoveryFailure;
|
|
40
|
-
}
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
pending = operation.then(() => { }, () => { });
|
|
45
|
-
return operation;
|
|
46
|
-
};
|
|
47
|
-
try {
|
|
48
|
-
await client.query('BEGIN');
|
|
49
|
-
began = true;
|
|
50
|
-
const result = await work(query);
|
|
51
|
-
active = false;
|
|
52
|
-
await pending;
|
|
53
|
-
connection.check();
|
|
54
|
-
if (recoveryFailure)
|
|
55
|
-
throw recoveryFailure;
|
|
56
|
-
const committed = await client.query('COMMIT');
|
|
57
|
-
if (committed.command !== 'COMMIT') {
|
|
58
|
-
throw new Error('PostgreSQL rolled back the aborted transaction.');
|
|
59
|
-
}
|
|
60
|
-
return result;
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
active = false;
|
|
64
|
-
await pending;
|
|
65
|
-
if (!began) {
|
|
66
|
-
releaseError = error instanceof Error ? error : new Error('BEGIN failed.');
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
1
|
+
import { executeSql } from './connection.js';
|
|
2
|
+
/** Serialize whole Tenon policies; pgclear owns transaction, savepoint and transport recovery. */
|
|
3
|
+
export function withTransaction(pool, work) {
|
|
4
|
+
return pool.transaction(async (transaction) => {
|
|
5
|
+
let active = true;
|
|
6
|
+
let pending = Promise.resolve();
|
|
7
|
+
const query = (text, input, readResult, guarded) => {
|
|
8
|
+
if (!active)
|
|
9
|
+
return Promise.reject(new Error('The transaction has finished.'));
|
|
10
|
+
const operation = pending.then(() => {
|
|
11
|
+
const execute = async (scope) => readResult(await executeSql(scope, text, input));
|
|
12
|
+
return guarded ? transaction.savepoint(execute) : execute(transaction);
|
|
13
|
+
});
|
|
14
|
+
pending = operation.then(() => { }, () => { });
|
|
15
|
+
return operation;
|
|
16
|
+
};
|
|
69
17
|
try {
|
|
70
|
-
await
|
|
18
|
+
return await work(query);
|
|
71
19
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
20
|
+
finally {
|
|
21
|
+
active = false;
|
|
22
|
+
await pending;
|
|
75
23
|
}
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
connection.release(releaseError);
|
|
80
|
-
}
|
|
24
|
+
});
|
|
81
25
|
}
|
package/dist/types.d.ts
CHANGED
package/docs/adoption-guide.md
CHANGED
|
@@ -4,10 +4,49 @@ Replace application queries independently of migration ownership. Tenon can quer
|
|
|
4
4
|
and generate types while the current migration runner stays in place. For a Tenon version upgrade,
|
|
5
5
|
read the [Changelog](../CHANGELOG.md).
|
|
6
6
|
|
|
7
|
+
## Upgrade from Tenon 0.3.x
|
|
8
|
+
|
|
9
|
+
Tenon 0.4 uses pgclear 0.3.x in place of pg. Keep the existing database, credentials,
|
|
10
|
+
migration history, and application query functions. Pgclear is included as a runtime dependency.
|
|
11
|
+
With npm, update Tenon:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @kequtech/tenon-orm@^0.4.0
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use the project's existing package manager and then review these boundaries:
|
|
18
|
+
|
|
19
|
+
1. Translate pg `PoolConfig` fields to pgclear `PoolOptions`, or replace a borrowed pg Pool with a
|
|
20
|
+
pgclear Pool. A URI remains supported. Supply connection credentials explicitly; the driver does
|
|
21
|
+
not read PG environment variables, pgpass, service files, or pg-specific URI options.
|
|
22
|
+
2. Keep Tenon `sql`, `QueryClient`, result modes, and callback transactions. Update native calls to
|
|
23
|
+
pgclear APIs: use `queryClassic(text, values?)` for finished SQL, release acquired sessions, and
|
|
24
|
+
close dedicated listeners separately. Import `sql`, `json`, `param`, and codec helpers from
|
|
25
|
+
`@kequtech/tenon-orm`. If native driver code also uses Pgclear's tag, alias it as `pgSql`; its query
|
|
26
|
+
objects belong in native `query`, while Tenon's query objects belong in `db.query`.
|
|
27
|
+
3. Review temporal strings, readonly result arrays, Uint8Array bytes, exact string integer/numeric
|
|
28
|
+
arrays, and JSON/array inputs. Use `json(value)`, `param(value, codec)`, or encoded text for JSON
|
|
29
|
+
and arrays. Plain Date, bigint, and supported Temporal inputs are accepted with server inference;
|
|
30
|
+
Date encodes in UTC. They do not change output types. Command rowCount remains a number or null.
|
|
31
|
+
See [value mappings](reference.md#generated-types-and-checking).
|
|
32
|
+
4. Regenerate database declarations against the intended development/test schema. Replace former
|
|
33
|
+
`Rows['users']`, `Rows.Insert['users']`, and `Rows.Update['users']` accesses with
|
|
34
|
+
`Database['users']['row']`, `Database['users']['insert']`, and `Database['users']['update']`.
|
|
35
|
+
Tenon owns generation and uses the configured driver's codec representations. If code imports
|
|
36
|
+
`generateDefinitions` from pgclear, import it from Tenon and use Tenon's options instead. Review
|
|
37
|
+
the generated diff and exclude exact generator output from formatting. Generated helper types
|
|
38
|
+
now import from Tenon; applications do not need a separate Pgclear dependency for them.
|
|
39
|
+
5. Run the project's typecheck and database tests for queries, transactions, JSON/arrays, and native
|
|
40
|
+
integrations. Add optional source checking where composition is recoverable; optional patches
|
|
41
|
+
and runtime-dependent SQL may remain unchecked. See [validation tools](reference.md#choosing-a-validation-tool).
|
|
42
|
+
|
|
43
|
+
If other code still uses pg directly, retain it until those callers are migrated. Updating Tenon
|
|
44
|
+
does not require transferring migration ownership or adopting a new baseline.
|
|
45
|
+
|
|
7
46
|
## Preserve connections and behavior
|
|
8
47
|
|
|
9
48
|
Keep the existing database URI, credentials, TLS requirements, schema, and data. Tenon can add
|
|
10
|
-
driver settings through
|
|
49
|
+
driver settings through pgclear configuration:
|
|
11
50
|
|
|
12
51
|
```ts
|
|
13
52
|
import { createTenon } from '@kequtech/tenon-orm';
|
|
@@ -15,15 +54,15 @@ import { createTenon } from '@kequtech/tenon-orm';
|
|
|
15
54
|
const db = createTenon({
|
|
16
55
|
connection: {
|
|
17
56
|
connectionString: existingUri,
|
|
18
|
-
|
|
57
|
+
connectTimeoutMs: 5000,
|
|
19
58
|
},
|
|
20
59
|
});
|
|
21
60
|
```
|
|
22
61
|
|
|
23
|
-
Translate old driver configuration into
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
62
|
+
Translate old driver configuration into pgclear `PoolOptions`. Pool timeouts use milliseconds.
|
|
63
|
+
Set `startupParameters: { search_path: 'app,public' }` for runtime names. pgclear does not read
|
|
64
|
+
PG environment variables, pgpass, or service files; supply connection settings explicitly. Preserve
|
|
65
|
+
credentials and verified TLS requirements. CLI `--namespace` does not change runtime queries. See
|
|
27
66
|
[connection options](reference.md#installation-and-connection).
|
|
28
67
|
|
|
29
68
|
Before conversion, identify behavior supplied by the old ORM: IDs/defaults, timestamps, validation,
|
|
@@ -42,11 +81,14 @@ application code or SQL where required.
|
|
|
42
81
|
|
|
43
82
|
2. Replace queries with parameterized SQL in ordinary functions accepting `QueryClient` where
|
|
44
83
|
callers may supply `db` or `tx`. Retain projections, ordering, null handling, and authorization.
|
|
45
|
-
3. Review value conversions against [Tenon's mappings](reference.md#generated-types-and-checking)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
84
|
+
3. Review value conversions against [Tenon's mappings](reference.md#generated-types-and-checking).
|
|
85
|
+
Pgclear defaults read int8/bigserial and SQL count(*) as exact strings; command rowCount is a
|
|
86
|
+
number. Bigint, Date, and supported Temporal inputs use server inference. Numeric values,
|
|
87
|
+
dates, arrays, and JSON serialization may also differ from the previous driver. Regenerate
|
|
88
|
+
declarations when mappings change; they do not replace domain validation.
|
|
89
|
+
4. Run the optional [source-aware checker](query-checking.md) for recoverable queries, inspect
|
|
90
|
+
meaningful completed query variants, and run database assertions for actual rows, permissions,
|
|
91
|
+
write effects, and rollback. See [query inspection](query-inspection.md).
|
|
50
92
|
5. Remove unused runtime dependencies and generated code after their callers are gone. Dependencies
|
|
51
93
|
retained by other application code or installed transitively may still be needed.
|
|
52
94
|
|
|
@@ -55,7 +97,7 @@ also rejects zero rows. Use deterministic `ORDER BY` plus `LIMIT 1` when choosin
|
|
|
55
97
|
matches is intended; modes do not add a limit.
|
|
56
98
|
|
|
57
99
|
Use `sql.insertValues` and `sql.assignments` to replace object-to-SQL utilities, checking omission,
|
|
58
|
-
default, null, and empty-input semantics. Generated `
|
|
100
|
+
default, null, and empty-input semantics. Generated `Database[table]['insert']`/`Database[table]['update']` types can replace
|
|
59
101
|
handwritten persistence inputs. See the [mutation cookbook](mutations.md).
|
|
60
102
|
|
|
61
103
|
Use `tx.query` for work that must commit together. Native pool calls do not join Tenon transactions.
|
package/docs/agent-guide.md
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
# Tenon agent guide
|
|
2
2
|
|
|
3
|
-
Tenon executes explicit PostgreSQL SQL through
|
|
3
|
+
Tenon executes explicit PostgreSQL SQL through pgclear. Use ordinary functions for reusable queries. This
|
|
4
4
|
guide covers routine implementation; the [reference](reference.md) defines the full API and the
|
|
5
5
|
[human guide](human-guide.md) gives a step-by-step introduction.
|
|
6
6
|
|
|
7
7
|
## Start with the existing project
|
|
8
8
|
|
|
9
|
+
Read this guide after the README; consult the [reference](reference.md) for specific contracts.
|
|
10
|
+
For a first setup, follow the [human guide](human-guide.md). Prefer documentation from the installed
|
|
11
|
+
package when the repository's main branch describes a newer release.
|
|
12
|
+
|
|
9
13
|
Inspect its connection configuration, migration runner, generated types, query functions, and check
|
|
10
14
|
commands. Preserve those conventions. Tenon queries and type generation do not require Tenon
|
|
11
15
|
migration history. Keep input validation, authorization, domain conversion, and retry policy in the
|
|
12
16
|
application.
|
|
13
17
|
|
|
18
|
+
Start from the existing database module and a nearby query function. Reuse their connection,
|
|
19
|
+
generated-schema imports, namespace, and `QueryClient` convention. Check both resolved Tenon and
|
|
20
|
+
pgclear versions before changing driver calls or value representations.
|
|
21
|
+
|
|
14
22
|
For an upgrade, record the previous resolved Tenon version from the lockfile or installed package
|
|
15
23
|
and read the [Changelog](../CHANGELOG.md) entries through the target version before changing code.
|
|
16
24
|
It ships with the package alongside this guide. Follow its upgrade instructions, regenerate types
|
|
@@ -21,7 +29,9 @@ when mappings change, and run the project's typechecks and relevant database tes
|
|
|
21
29
|
|
|
22
30
|
Requires Node.js 24+, PostgreSQL 16+, ESM, TypeScript 6+, and Node development typings. Install with
|
|
23
31
|
`npm install @kequtech/tenon-orm`; retain the project's package manager and TypeScript
|
|
24
|
-
configuration.
|
|
32
|
+
configuration. Tenon includes pgclear 0.3.x as a runtime dependency. Import `sql`, `json`, `param`,
|
|
33
|
+
and codec helpers from Tenon. See [driver updates](reference.md#driver-updates) for independent
|
|
34
|
+
compatible updates; regenerate changed types and rerun database checks.
|
|
25
35
|
|
|
26
36
|
```ts
|
|
27
37
|
import { createTenon } from '@kequtech/tenon-orm';
|
|
@@ -31,8 +41,8 @@ if (!connection) throw new Error('DATABASE_URL is required');
|
|
|
31
41
|
export const db = createTenon({ connection });
|
|
32
42
|
```
|
|
33
43
|
|
|
34
|
-
`connection` accepts a URI string,
|
|
35
|
-
`
|
|
44
|
+
`connection` accepts a URI string, pgclear `PoolOptions` (including `connectionString`), or an existing
|
|
45
|
+
pgclear `Pool`. Reuse an instance for each database configuration. `db.close()` ends internally created
|
|
36
46
|
pools with a default 5,000 ms grace period; supplied pools remain application-owned. Configure
|
|
37
47
|
shutdown with `db.close({ timeoutMs })`; concurrent callers share its outcome. Finish outstanding
|
|
38
48
|
work before closing. Tenon loads no environment files. Qualify table names or set runtime
|
|
@@ -43,9 +53,9 @@ work before closing. Tenon loads no environment files. Qualify table names or se
|
|
|
43
53
|
|
|
44
54
|
```ts
|
|
45
55
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
46
|
-
import type {
|
|
56
|
+
import type { Database } from './generated-schema.js';
|
|
47
57
|
|
|
48
|
-
type Summary = Pick<
|
|
58
|
+
type Summary = Pick<Database['users']['row'], 'id' | 'email'>;
|
|
49
59
|
|
|
50
60
|
function userById(client: QueryClient, { id }: { id: number }) {
|
|
51
61
|
return client.query(sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`, {
|
|
@@ -62,7 +72,7 @@ aggregates need types for their actual output.
|
|
|
62
72
|
| `returns` | Result | Required returned rows |
|
|
63
73
|
| ------------------ | -------------------------------------------------- | ---------------------- |
|
|
64
74
|
| `native` (default) | Driver object with `.rows`, `.rowCount`, `.fields` | Any |
|
|
65
|
-
| `many` |
|
|
75
|
+
| `many` | `readonly Row[]` | Any |
|
|
66
76
|
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
67
77
|
| `one` | `Row` | Exactly one |
|
|
68
78
|
|
|
@@ -72,9 +82,10 @@ Options are `{ returns?, affectedRows?, label? }`; unknown keys fail. Modes do n
|
|
|
72
82
|
|
|
73
83
|
## Parameters and composition
|
|
74
84
|
|
|
75
|
-
- Interpolate values without quotes. Do not author `$n` placeholders inside templates. `
|
|
76
|
-
and
|
|
77
|
-
|
|
85
|
+
- Interpolate values without quotes. Do not author `$n` placeholders inside templates. `null` binds
|
|
86
|
+
SQL NULL. Plain bigint, Date, and supported native/polyfilled Temporal values encode as text with
|
|
87
|
+
server inference; Date uses UTC. Undefined, plain objects, and arrays require omission or explicit encoding.
|
|
88
|
+
- Use pgclear `param(ids, builtin.int4Array)` with `ANY` for a set of IDs. Cast standalone parameters such as
|
|
78
89
|
`SELECT ${value}::integer AS value` when SQL must establish the type for inspection.
|
|
79
90
|
- `identifier(schema, table)` quotes separate approved names. Strings such as `'DESC'` remain bound
|
|
80
91
|
values; choose SQL syntax through fixed query alternatives or fragments.
|
|
@@ -95,17 +106,20 @@ Selected columns ignore unlisted fields and can discard required inputs. Put exp
|
|
|
95
106
|
policy, and `RETURNING` in the surrounding SQL. Batch in the caller below the query-wide
|
|
96
107
|
65,535-parameter limit.
|
|
97
108
|
|
|
98
|
-
Use generated `
|
|
109
|
+
Use generated `Database['users']['insert']` and `Database['users']['update']` inputs; narrow editable fields with
|
|
99
110
|
`Pick`. These types account for defaults, writable columns, domains, and driver representations.
|
|
100
111
|
They do not validate constraints or JSON contents. See [mutation examples](mutations.md).
|
|
101
112
|
|
|
102
|
-
Use `
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
Use pgclear `json(value)` or encoded text for JSON, and `param(value, codec)` for arrays and
|
|
114
|
+
custom encodings. Bigint scalars and SQL `count(*)` read as exact strings; command `rowCount` uses
|
|
115
|
+
numbers. Plain bigint, Date, and supported Temporal inputs do not change those output defaults.
|
|
116
|
+
Choose explicit codecs for bigint, Date, or Temporal outputs. Decimals and temporal values otherwise
|
|
117
|
+
read as strings. Numeric and bigint arrays preserve exact strings. Bytes use Uint8Array.
|
|
118
|
+
SQL NULL array elements remain null. Tenon generates declarations from the schema and selected
|
|
119
|
+
registry; a custom decoder can declare `text.typescript` so generation and checking agree without
|
|
120
|
+
duplicating its type in `readTypes`. Keep domain validation at the persistence boundary. See
|
|
106
121
|
[value mappings](reference.md#generated-types-and-checking) and the
|
|
107
|
-
[JSON/
|
|
108
|
-
requiring finite expiry, include `isfinite(expires_at)` alongside the SQL expiry comparison.
|
|
122
|
+
[JSON/temporal projection](reference.md#persistence-and-domain-values).
|
|
109
123
|
|
|
110
124
|
## Transactions and native access
|
|
111
125
|
|
|
@@ -128,7 +142,7 @@ provided. Transactions belong to one database instance.
|
|
|
128
142
|
Bounds are nonnegative safe integers. A guarded `db.query` uses a transaction; a guarded `tx.query`
|
|
129
143
|
uses a savepoint. Rejection rolls back that operation. A caught guarded failure permits continued
|
|
130
144
|
work; an unguarded SQL error normally aborts the transaction. Counts exclude extra trigger/cascade
|
|
131
|
-
and CTE effects.
|
|
145
|
+
and CTE effects. pgclear owns savepoint recovery. A lost commit
|
|
132
146
|
acknowledgment can leave the outcome unknown.
|
|
133
147
|
|
|
134
148
|
`db.native` exposes the existing pool for driver features. Native calls bypass Tenon's guards and
|
|
@@ -139,16 +153,30 @@ not establish that the outer transaction committed.
|
|
|
139
153
|
|
|
140
154
|
## Inspect and validate
|
|
141
155
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
Choose the tool for the task: `tenon check` compares source contracts, `tenon describe` reports
|
|
157
|
+
finished SQL metadata, and `tenon generate --check` checks generated-file freshness. None replaces
|
|
158
|
+
TypeScript or database tests. See [validation tools](reference.md#choosing-a-validation-tool).
|
|
159
|
+
|
|
160
|
+
Run tenon check on the TypeScript project, a file, or file:line. It recovers authored SQL and types
|
|
161
|
+
without importing application code, then uses pgclear Describe. Mismatches, unchecked queries and
|
|
162
|
+
zero query sites fail; unproven properties do not. Trust declared types when metadata is inconclusive.
|
|
163
|
+
|
|
164
|
+
For checking, object expansions need an explicit finite shape type and required, defined selected
|
|
165
|
+
properties. Prefer explicit column lists. Fixed fragments, const aliases, fixed joins and finite
|
|
166
|
+
ternaries are supported within 16 variants per query. Dynamic fragments, optional-field SQL
|
|
167
|
+
expansion and unknown batch lengths remain unchecked. See [query checking](query-checking.md).
|
|
168
|
+
|
|
169
|
+
A generated update type usually has optional properties. Even with an explicit column list, those
|
|
170
|
+
properties can change the SQL by omission, so the checker may report an ordinary patch as unchecked.
|
|
171
|
+
Keep that runtime behavior when intended. If selected fields are always supplied, validate/narrow
|
|
172
|
+
the input into an explicit required shape. Do not use casts just to suppress a finding. See
|
|
173
|
+
[responding to findings](query-checking.md#responding-to-findings) and
|
|
174
|
+
[troubleshooting](reference.md#troubleshooting).
|
|
175
|
+
|
|
176
|
+
For metadata alone, use describeQuery or tenon describe query.sql. Match role, search path, timezone,
|
|
177
|
+
and custom codecs. Temporary tables and transaction-local state are not inherited. Retain tests for
|
|
178
|
+
complete rows, meaningful query variants, authorization, effects, and rollback. Describe does not
|
|
179
|
+
prove bound values, JSON members, nullability, cardinality or runtime execution permission.
|
|
152
180
|
|
|
153
181
|
## Schema changes
|
|
154
182
|
|
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 pgclear. 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) for exact contracts and [adoption](adoption-guide.md) when
|
|
@@ -17,8 +17,10 @@ npm install @kequtech/tenon-orm
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Use PostgreSQL 16+ and TypeScript 6+. Keep your existing strict TypeScript configuration and Node
|
|
20
|
-
typings. Supply `DATABASE_URL` through your application's
|
|
21
|
-
`.env` files.
|
|
20
|
+
typings. Tenon includes pgclear 0.3.x. Supply `DATABASE_URL` through your application's
|
|
21
|
+
environment setup; Tenon does not load `.env` files. Import `sql`, `json`, `param`, and codec helpers
|
|
22
|
+
from `@kequtech/tenon-orm` for application queries. See [driver updates](reference.md#driver-updates)
|
|
23
|
+
when updating Pgclear independently.
|
|
22
24
|
|
|
23
25
|
```ts
|
|
24
26
|
// src/db.ts
|
|
@@ -30,7 +32,7 @@ export const db = createTenon({ connection });
|
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
Reuse this instance across the application and call `await db.close()` at shutdown. You can also
|
|
33
|
-
supply an existing `
|
|
35
|
+
supply an existing pgclear `Pool`; your application then owns its shutdown.
|
|
34
36
|
|
|
35
37
|
## Create a table
|
|
36
38
|
|
|
@@ -102,13 +104,22 @@ throws if the query returns more than one row. Use `'one'` when absence should a
|
|
|
102
104
|
`'many'` for an array, or omit `returns` for the native result object with `.rows`, `.rowCount`, and
|
|
103
105
|
`.fields`. These options do not change your SQL.
|
|
104
106
|
|
|
107
|
+
`rowCount` is a number or null when the command has no count. SQL `count(*)` and bigint/bigserial
|
|
108
|
+
column values are exact strings, including values beyond JavaScript's safe integer range. Use
|
|
109
|
+
strings, bigint, or pgclear `param(text, builtin.int8)` for int8 parameters. Plain Date and supported
|
|
110
|
+
Temporal inputs also work; they do not change the default string outputs. Date inputs encode in UTC.
|
|
111
|
+
Opt-in codecs can return Date, bigint, or Temporal values when the application needs them.
|
|
112
|
+
|
|
105
113
|
Generated table types can help describe projections without repeating every field:
|
|
106
114
|
|
|
107
115
|
```ts
|
|
108
|
-
import type {
|
|
109
|
-
type UserSummary = Pick<
|
|
116
|
+
import type { Database } from './generated-schema.js';
|
|
117
|
+
type UserSummary = Pick<Database['users']['row'], 'id' | 'email'>;
|
|
110
118
|
```
|
|
111
119
|
|
|
120
|
+
For frequently used types, define [application aliases](reference.md#application-aliases) such as
|
|
121
|
+
`UserRow`, `UserInsert`, and `UserUpdate` in a handwritten module.
|
|
122
|
+
|
|
112
123
|
You can then write `sql<UserSummary>`. A table type describes the full table; a join or aggregate
|
|
113
124
|
needs a type for what that particular query selects. An annotation is a declaration, not proof that
|
|
114
125
|
the SQL matches. Without one, result fields are `unknown`. Use the inspection and database-test
|
|
@@ -116,7 +127,7 @@ workflow below to check the declared contract against the query's behavior.
|
|
|
116
127
|
|
|
117
128
|
Keep storage types conservative and validate them when projecting domain values. The
|
|
118
129
|
[persistence-to-domain example](reference.md#persistence-and-domain-values) shows validated JSON
|
|
119
|
-
writes and reads,
|
|
130
|
+
writes and reads, temporal strings, and an SQL predicate that rejects infinite session expiry.
|
|
120
131
|
|
|
121
132
|
## Reuse a query with a function
|
|
122
133
|
|
|
@@ -125,9 +136,9 @@ supplied values easy to read:
|
|
|
125
136
|
|
|
126
137
|
```ts
|
|
127
138
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
128
|
-
import type {
|
|
139
|
+
import type { Database } from './generated-schema.js';
|
|
129
140
|
|
|
130
|
-
type UserSummary = Pick<
|
|
141
|
+
type UserSummary = Pick<Database['users']['row'], 'id' | 'email'>;
|
|
131
142
|
|
|
132
143
|
export function userByEmail(client: QueryClient, { email }: { email: string }) {
|
|
133
144
|
return client.query(sql<UserSummary>`SELECT id, email FROM public.users WHERE email = ${email}`, {
|
|
@@ -171,12 +182,12 @@ Explicit `null` stays null. Empty batches and updates throw. Heterogeneous inser
|
|
|
171
182
|
aligned, with `DEFAULT` for missing cells.
|
|
172
183
|
|
|
173
184
|
```ts
|
|
174
|
-
import type {
|
|
185
|
+
import type { Database } from './generated-schema.js';
|
|
175
186
|
|
|
176
|
-
const input:
|
|
187
|
+
const input: Database['users']['insert'] = { email: 'another@example.test' };
|
|
177
188
|
await db.query(sql`INSERT INTO public.users ${sql.insertValues(input, ['email'])}`);
|
|
178
189
|
|
|
179
|
-
const patch: Pick<
|
|
190
|
+
const patch: Pick<Database['users']['update'], 'active'> = { active: false };
|
|
180
191
|
await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
|
|
181
192
|
WHERE email = ${input.email}`);
|
|
182
193
|
```
|
|
@@ -188,14 +199,31 @@ Use `Pick` to limit editable fields. Keep expressions, predicates, conflict hand
|
|
|
188
199
|
|
|
189
200
|
## Check your SQL and result types
|
|
190
201
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
The three database tools answer different questions: `tenon check` compares authored queries with
|
|
203
|
+
their TypeScript contracts, `tenon describe` reports metadata for finished SQL, and
|
|
204
|
+
`tenon generate --check` checks whether saved database declarations are current. See
|
|
205
|
+
[validation tools](reference.md#choosing-a-validation-tool) for their limits.
|
|
206
|
+
|
|
207
|
+
After migrating a development/test database, run the optional source-aware checker:
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
npx tenon check src/queries.ts
|
|
211
|
+
npx tenon check src/queries.ts:23
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A line selects one complete query. Omit the file to check the TypeScript project. The checker reads
|
|
215
|
+
source and types without importing the application or executing SQL. It reports definite
|
|
216
|
+
contradictions and unsupported composition; nullability, JSON members, and other inconclusive
|
|
217
|
+
contracts receive the benefit of the declared type and remain unproven. See
|
|
218
|
+
[query checking](query-checking.md) for supported fragments and explicit object expansion shapes.
|
|
195
219
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
Use describeQuery on finished SQL, or tenon describe query.sql, when you only need metadata and a
|
|
221
|
+
conservative row declaration. Match runtime role/session settings. Retain TypeScript and database
|
|
222
|
+
tests for actual rows, effects, permissions, and rollback. The checker does not replace those tests.
|
|
223
|
+
Generated patch types contain optional fields and can therefore be unchecked even when execution is
|
|
224
|
+
valid. Keep optional patches when needed; see [responding to findings](query-checking.md#responding-to-findings).
|
|
225
|
+
For rejected values, result-count errors, and other common failures, see
|
|
226
|
+
[troubleshooting](reference.md#troubleshooting).
|
|
199
227
|
|
|
200
228
|
## Reuse or assemble parts of SQL when needed
|
|
201
229
|
|
|
@@ -215,6 +243,7 @@ const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
|
|
|
215
243
|
|
|
216
244
|
Execute each query with `db.query`. Validate application-supplied patterns and page sizes before
|
|
217
245
|
building queries. The complete query supplies the result type.
|
|
246
|
+
The SQL aggregate's `count` is a string; the native result's `rowCount` counts returned rows.
|
|
218
247
|
|
|
219
248
|
Use `sql.join` when the number of fragments varies, such as one search condition per word or a set
|
|
220
249
|
of optional filters. It combines fragments with `'AND'`, `'OR'`, or `','` and preserves their bound
|
|
@@ -248,3 +277,5 @@ when mixing `AND` and `OR`. See the [composition reference](reference.md#sql-fra
|
|
|
248
277
|
|
|
249
278
|
Use fixed SQL when composition is unnecessary. See the [reference](reference.md) for arrays,
|
|
250
279
|
identifiers, value mappings, CLI options, and error behavior.
|
|
280
|
+
Runtime loops and dynamic fragment lists work during execution but remain unchecked by `tenon check`;
|
|
281
|
+
use database tests for those queries.
|
package/docs/mutations.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use `sql.insertValues` and `sql.assignments` for object fields while keeping SQL structure visible.
|
|
4
4
|
The [reference](reference.md#object-based-mutation-fragments) defines the complete contract.
|
|
5
|
-
Examples assume an existing `db` and this table, with generated `
|
|
5
|
+
Examples assume an existing `db` and this table, with generated `Database` from the same schema:
|
|
6
6
|
|
|
7
7
|
```sql
|
|
8
8
|
CREATE TABLE profiles (
|
|
@@ -34,9 +34,9 @@ aligned across heterogeneous rows. Empty batches fail; a single default-only obj
|
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
36
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
37
|
-
import type {
|
|
37
|
+
import type { Database } from './generated-schema.js';
|
|
38
38
|
|
|
39
|
-
type ProfileInput = Pick<
|
|
39
|
+
type ProfileInput = Pick<Database['profiles']['insert'], 'name' | 'score'>;
|
|
40
40
|
|
|
41
41
|
function createProfile(client: QueryClient, input: ProfileInput) {
|
|
42
42
|
return client.query(
|
|
@@ -55,7 +55,7 @@ Selected columns choose and order fields, ignoring unlisted keys. They can disca
|
|
|
55
55
|
even when the original object is valid:
|
|
56
56
|
|
|
57
57
|
```ts
|
|
58
|
-
const input:
|
|
58
|
+
const input: Database['profiles']['insert'] = { name: 'Ada' };
|
|
59
59
|
const query = sql`INSERT INTO profiles ${sql.insertValues(input, ['bio', 'score'])}`;
|
|
60
60
|
// Executing this fails: name was discarded and has no default.
|
|
61
61
|
```
|
|
@@ -63,7 +63,7 @@ const query = sql`INSERT INTO profiles ${sql.insertValues(input, ['bio', 'score'
|
|
|
63
63
|
## Patches mixed with expressions
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
|
-
const patch: Pick<
|
|
66
|
+
const patch: Pick<Database['profiles']['update'], 'name' | 'bio'> = { bio: null };
|
|
67
67
|
await db.query(sql`UPDATE profiles SET ${sql.assignments(patch)}, revision = revision + 1
|
|
68
68
|
WHERE id = ${id}`);
|
|
69
69
|
```
|
|
@@ -106,15 +106,28 @@ For a table with `payload jsonb` and `tags text[]`, encode JSON separately from
|
|
|
106
106
|
array:
|
|
107
107
|
|
|
108
108
|
```ts
|
|
109
|
+
import { builtin, json, param } from '@kequtech/tenon-orm';
|
|
110
|
+
|
|
109
111
|
const tags = ['ada', 'lin'];
|
|
110
112
|
await db.query(
|
|
111
|
-
sql`INSERT INTO deliveries ${sql.insertValues({ payload:
|
|
113
|
+
sql`INSERT INTO deliveries ${sql.insertValues({ payload: json(tags), tags: param(tags, builtin.textArray) })}`,
|
|
112
114
|
);
|
|
113
115
|
```
|
|
114
116
|
|
|
115
|
-
Generated JSON writes accept encoded
|
|
116
|
-
strings, and JSON null;
|
|
117
|
-
PostgreSQL
|
|
117
|
+
Generated JSON writes accept pgclear parameters or encoded strings. `json(value)` encodes objects,
|
|
118
|
+
arrays, strings, and JSON null; plain `null` means SQL NULL. Arrays need `param(value, codec)` or
|
|
119
|
+
encoded PostgreSQL text. Plain arrays, objects, and undefined reject as bound values. Tenon passes
|
|
120
|
+
explicit parameters unchanged to pgclear. `json()` still rejects bigint values, including bigint
|
|
121
|
+
nested inside objects; bigint support for ordinary SQL parameters does not change JSON encoding.
|
|
122
|
+
|
|
123
|
+
For bigint/bigserial fields, generated writes accept exact strings, bigint, or explicit parameters;
|
|
124
|
+
reads remain strings by default. Use `param('9007199254740993', builtin.int8)` for an explicit int8
|
|
125
|
+
parameter and `param(['9007199254740993', null], builtin.int8Array)` for an int8 array. The bigint
|
|
126
|
+
output codec and matching array encoder are separate opt-in choices.
|
|
127
|
+
|
|
128
|
+
Date and supported Temporal values can be ordinary scalar inputs, including mutation fields. Date
|
|
129
|
+
encodes in UTC; Temporal calendar dates, wall times, and instants retain their distinct meanings.
|
|
130
|
+
Choose the value appropriate to the PostgreSQL column and retain application validation.
|
|
118
131
|
|
|
119
132
|
## Batches in one transaction
|
|
120
133
|
|