@kequtech/tenon-orm 0.2.1 → 0.3.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 +52 -36
- package/README.md +27 -70
- package/dist/affected-rows.js +6 -6
- package/dist/baseline.js +4 -4
- package/dist/client.js +1 -1
- package/dist/connection.d.ts +15 -16
- package/dist/connection.js +124 -14
- package/dist/database.js +36 -17
- package/dist/driver/types.d.ts +1 -1
- package/dist/driver/types.js +11 -11
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -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/schema-summary.d.ts +3 -3
- package/dist/generation/schema-summary.js +11 -11
- package/dist/generation/table-types.js +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -1
- package/dist/inspection/catalog.d.ts +4 -3
- package/dist/inspection/catalog.js +13 -10
- package/dist/inspection/cli.js +3 -3
- package/dist/inspection/connection.d.ts +2 -2
- package/dist/inspection/connection.js +22 -22
- package/dist/inspection/options.d.ts +3 -3
- package/dist/inspection/options.js +9 -1
- package/dist/inspection/protocol.d.ts +7 -0
- package/dist/inspection/protocol.js +45 -0
- package/dist/inspection/result.d.ts +1 -1
- package/dist/inspection/result.js +6 -6
- package/dist/inspection/types.d.ts +10 -10
- package/dist/migrations/connection.d.ts +3 -2
- package/dist/migrations/connection.js +18 -20
- package/dist/migrations/history.d.ts +4 -4
- package/dist/migrations/history.js +5 -5
- package/dist/migrations.js +7 -7
- package/dist/query-options.js +5 -5
- package/dist/query.js +2 -2
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.js +19 -14
- package/dist/types.d.ts +7 -3
- package/docs/adoption-guide.md +109 -122
- package/docs/agent-guide.md +124 -228
- package/docs/human-guide.md +44 -55
- package/docs/mutations.md +58 -76
- package/docs/query-inspection.md +97 -89
- package/docs/reference.md +264 -319
- package/package.json +4 -4
- package/dist/driver/array.d.ts +0 -5
- package/dist/driver/array.js +0 -70
- package/dist/driver/lifecycle.d.ts +0 -15
- package/dist/driver/lifecycle.js +0 -57
- package/dist/driver/session.d.ts +0 -8
- package/dist/driver/session.js +0 -92
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- **Breaking:** `pg@8.23.1` replaces Postgres.js. Use pg `PoolConfig` (`connectionString` for a URI)
|
|
6
|
+
or `pg.Pool`; removed `{ url, options }` and `ConnectionUri`. `db.native` is the actual pool;
|
|
7
|
+
results use `rows`, `rowCount`, and `fields`, and error fields use pg names (`table`,
|
|
8
|
+
`constraint`). Native LISTEN uses checked-out clients with application-owned resubscription.
|
|
9
|
+
- Driver defaults apply, including no acquisition timeout and SQL null for bound `undefined`. The
|
|
10
|
+
parameter ceiling now matches PostgreSQL's 65,535 limit.
|
|
11
|
+
- Regenerate types: temporal reads include numeric infinities, date-only values use Node's local
|
|
12
|
+
timezone, and numeric arrays contain numbers that can lose precision. JSON write inputs are
|
|
13
|
+
encoded strings; use `JSON.stringify`, especially for arrays and strings. Removed `JSONValue`.
|
|
14
|
+
|
|
15
|
+
## 0.2.2
|
|
16
|
+
|
|
17
|
+
- Closing a Tenon-owned client through `db.close()` prevents native LISTEN reconnects from restoring
|
|
18
|
+
connections after shutdown, including pending subscription setup. Borrowed clients retain
|
|
19
|
+
application ownership.
|
|
20
|
+
- Added `connection: { url, options? }` to preserve a deployment URI while configuring the native
|
|
21
|
+
driver. `ConnectionUri` is exported; `ConnectionOptions` includes the pinned driver's constructor
|
|
22
|
+
`onnotify` callback. Internally created clients remain Tenon-owned.
|
|
23
|
+
- Inspection accepts explicit `undefined` for optional session and timeout settings, including in
|
|
24
|
+
strict TypeScript configurations; unknown keys and invalid defined values still fail.
|
|
25
|
+
- `db.close({ timeoutMs? })` now shares one shutdown outcome across concurrent/later callers and
|
|
26
|
+
defaults to a 5,000 ms grace period before requesting owned driver termination; `0` skips the
|
|
27
|
+
wait.
|
|
28
|
+
- Added `CardinalityError` for `one`/`oneOrNone` mismatches, with code `TENON_CARDINALITY_MISMATCH`,
|
|
29
|
+
expected mode, actual count, and optional label.
|
|
30
|
+
|
|
3
31
|
## 0.2.1
|
|
4
32
|
|
|
5
33
|
- Added readonly `db.native` access to Tenon's existing Postgres.js client for LISTEN, cursors,
|
|
@@ -14,29 +42,29 @@ Changes since 0.1.0, including improvements shipped in 0.1.1–0.1.4.
|
|
|
14
42
|
|
|
15
43
|
### Breaking changes
|
|
16
44
|
|
|
17
|
-
- Tenon includes `postgres@3.4.9` as its only runtime dependency. Removed `pg`, `@types/pg`,
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
- Tenon includes `postgres@3.4.9` as its only runtime dependency. Removed `pg`, `@types/pg`, and
|
|
46
|
+
driver peer requirements. Connections accept a PostgreSQL URI, Postgres.js options, or an existing
|
|
47
|
+
Postgres.js client.
|
|
20
48
|
- Native results are row arrays with `count`, `command`, and `columns` metadata. Replace
|
|
21
|
-
`result.rows` with the result itself, `rowCount` with `count`, and `fields` with `columns`.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- Value conversions follow Postgres.js defaults: undefined parameters reject, JSON inputs use
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- Removed the `/safeql` entry point, `safeqlPlugin`, `eslintPlugin`, and their dependencies and
|
|
28
|
-
|
|
49
|
+
`result.rows` with the result itself, `rowCount` with `count`, and `fields` with `columns`. The
|
|
50
|
+
`many`, `oneOrNone`, and `one` result modes remain available. Native error fields use Postgres.js
|
|
51
|
+
names such as `table_name` and `constraint_name`.
|
|
52
|
+
- Value conversions follow Postgres.js defaults: undefined parameters reject, JSON inputs use native
|
|
53
|
+
JavaScript values, numeric arrays return strings, and timestamp infinities return invalid `Date`
|
|
54
|
+
objects. Pre-serialized JSON text becomes a JSON string.
|
|
55
|
+
- Removed the `/safeql` entry point, `safeqlPlugin`, `eslintPlugin`, and their dependencies and peer
|
|
56
|
+
requirements. Tenon no longer checks or fixes SQL result annotations automatically.
|
|
29
57
|
|
|
30
58
|
### Added
|
|
31
59
|
|
|
32
60
|
- `describeQuery({ connection, query, session?, timeouts? })` and `tenon describe <query.sql>`
|
|
33
|
-
report PostgreSQL parameter/result types, source columns, a conservative row declaration,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- `sql.insertValues(input, columns?)` builds inserts with database defaults for missing or
|
|
38
|
-
|
|
39
|
-
|
|
61
|
+
report PostgreSQL parameter/result types, source columns, a conservative row declaration, and
|
|
62
|
+
unresolved findings. Inspection accepts completed dynamic SQL, uses a separate session, and
|
|
63
|
+
supports explicit role, search path, timezone, and timeouts. It does not execute the query or
|
|
64
|
+
verify its TypeScript annotation. See [query inspection](docs/query-inspection.md).
|
|
65
|
+
- `sql.insertValues(input, columns?)` builds inserts with database defaults for missing or undefined
|
|
66
|
+
cells. `sql.assignments(input, columns?)` builds updates that omit undefined fields. Both preserve
|
|
67
|
+
explicit null. See the [mutation cookbook](docs/mutations.md).
|
|
40
68
|
- Generated `Rows.Insert['table']` and `Rows.Update['table']` types account for defaults,
|
|
41
69
|
nullability, domains, generated columns, and driver input representations.
|
|
42
70
|
|
|
@@ -44,21 +72,9 @@ Changes since 0.1.0, including improvements shipped in 0.1.1–0.1.4.
|
|
|
44
72
|
|
|
45
73
|
- SQL NULL array elements remain JavaScript null; quoted `"NULL"`, escapes, nesting, and custom
|
|
46
74
|
parsers retain their meanings.
|
|
47
|
-
- Transactions and migration locks reject work after connection loss. Failed recovery discards
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
- PostgreSQL `name` columns generate string types. `tenon generate --check` reports missing
|
|
54
|
-
files and a short diff for stale definitions without modifying the saved file.
|
|
55
|
-
|
|
56
|
-
### Upgrading
|
|
57
|
-
|
|
58
|
-
1. Remove Tenon's SafeQL imports and plugin configuration, and unused checking dependencies.
|
|
59
|
-
2. Install Tenon and remove `pg`/`@types/pg` when no other code needs them. Replace pg-specific
|
|
60
|
-
connection configuration and pools with Postgres.js equivalents.
|
|
61
|
-
3. Update native result/error access, pass JSON values directly, and regenerate table definitions.
|
|
62
|
-
Review numeric arrays and date validity. Use generated insert/update types for write inputs.
|
|
63
|
-
4. Inspect meaningful query variants and keep database tests for complete rows, permissions,
|
|
64
|
-
ordering, write effects, and rollback. Match inspection settings to the execution session.
|
|
75
|
+
- Transactions and migration locks reject work after connection loss. Failed recovery discards the
|
|
76
|
+
affected session, and accepted pending work finishes before transaction cleanup.
|
|
77
|
+
- Large fragment composition avoids argument-spread failures. Queries reject more than 65,533 bound
|
|
78
|
+
parameters before execution; batching remains application-controlled.
|
|
79
|
+
- PostgreSQL `name` columns generate string types. `tenon generate --check` reports missing files
|
|
80
|
+
and a short diff for stale definitions without modifying the saved file.
|
package/README.md
CHANGED
|
@@ -2,21 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A PostgreSQL ORM designed for coding agents. Built for Node.js and TypeScript.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
transactions,
|
|
7
|
-
|
|
8
|
-
run against the database.
|
|
9
|
-
|
|
10
|
-
Tenon takes the place of an ORM without hiding PostgreSQL behind a query builder. You or your agent
|
|
11
|
-
write the SQL; Tenon handles binding values and returning results. Your application controls the
|
|
12
|
-
connection configuration and validation. Tenon includes its pinned Postgres.js driver.
|
|
5
|
+
Write SQL directly, bind values with a template tag, and reuse queries through ordinary functions.
|
|
6
|
+
Tenon provides transactions, affected-row guards, SQL migrations, generated database types, and
|
|
7
|
+
query inspection. It includes its pinned pg driver.
|
|
13
8
|
|
|
14
9
|
```sh
|
|
15
10
|
npm install @kequtech/tenon-orm
|
|
16
11
|
```
|
|
17
12
|
|
|
18
|
-
Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and
|
|
19
|
-
|
|
13
|
+
Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
|
|
14
|
+
development typings.
|
|
20
15
|
|
|
21
16
|
```ts
|
|
22
17
|
import { createTenon, sql } from '@kequtech/tenon-orm';
|
|
@@ -39,70 +34,32 @@ try {
|
|
|
39
34
|
}
|
|
40
35
|
```
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Use [`db.native`](docs/reference.md#native-driver-access) to access the underlying Postgres.js client
|
|
47
|
-
for LISTEN, cursors, COPY, and other native features without installing another driver. Native calls
|
|
48
|
-
use the pool directly and bypass Tenon's query and transaction guards.
|
|
49
|
-
|
|
50
|
-
Tenon also provides:
|
|
51
|
-
|
|
52
|
-
- Transactions and affected-row limits that roll back rejected writes.
|
|
53
|
-
- SQL migration files and a CLI to apply, inspect, and roll back migrations.
|
|
54
|
-
- TypeScript row, insert, and update types generated from your database.
|
|
55
|
-
- SQL fragments for sharing query parts or assembling variable conditions.
|
|
56
|
-
- Query descriptions from PostgreSQL, including parameter/result types and session context.
|
|
57
|
-
|
|
58
|
-
Joins, relationships, aggregates, and upserts use ordinary PostgreSQL SQL. Tenon does not introduce
|
|
59
|
-
model classes or automatically load related records. Input validation, authorization, and database
|
|
60
|
-
performance remain application concerns. You can keep an existing migration runner; Tenon's
|
|
61
|
-
migrations and table type generation are independent conveniences.
|
|
62
|
-
|
|
63
|
-
## Here's what a coding agent says about it
|
|
37
|
+
The example assumes `public.users` exists. In a server, reuse the instance and close it at shutdown.
|
|
38
|
+
`oneOrNone` rejects multiple rows; use `one` for exactly one row, `many` for an array, or omit
|
|
39
|
+
`returns` for the driver's result object and metadata.
|
|
64
40
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
>
|
|
70
|
-
> — Codex, after working on Tenon. An interested party, to put it mildly.
|
|
41
|
+
Joins, relationships, aggregates, and upserts use PostgreSQL SQL. Object-based writes can use
|
|
42
|
+
`sql.insertValues` and `sql.assignments` while keeping the surrounding statement explicit.
|
|
43
|
+
`db.native` exposes the underlying `pg.Pool` for LISTEN, cursors, COPY, and other driver features;
|
|
44
|
+
those calls bypass Tenon's query and transaction guards.
|
|
71
45
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
support selected columns.
|
|
77
|
-
Use [generated input types](docs/reference.md#insert-and-update-inputs) to check those objects
|
|
78
|
-
against your schema.
|
|
79
|
-
|
|
80
|
-
## Checking query results
|
|
81
|
-
|
|
82
|
-
The type in `sql<{ id: number; email: string }>` describes what you expect back. TypeScript alone
|
|
83
|
-
cannot verify that your SQL actually selects those columns.
|
|
84
|
-
|
|
85
|
-
Use `describeQuery({ connection, query })` to inspect the finished SQL, including dynamic fragments,
|
|
86
|
-
or `npx tenon describe query.sql` for a SQL file. It reports PostgreSQL types, Postgres.js values,
|
|
87
|
-
session context, and unresolved findings without executing the statement. No checking packages or
|
|
88
|
-
compiler API are required. Keep database tests for actual values, nullability, authorization, and
|
|
89
|
-
write effects; descriptions do not verify `sql<Row>`. See [query inspection](docs/query-inspection.md).
|
|
46
|
+
`sql<Row>` declares an expected result type. `describeQuery` inspects the finished SQL through
|
|
47
|
+
PostgreSQL without executing it; it does not verify that annotation. Keep database tests for actual
|
|
48
|
+
rows and write effects. Validation, authorization, and retry policy belong to the application.
|
|
49
|
+
Existing migration runners can be retained; Tenon's migrations and type generation are optional.
|
|
90
50
|
|
|
91
51
|
## Documentation
|
|
92
52
|
|
|
93
|
-
- **[
|
|
94
|
-
- **[
|
|
95
|
-
- **[
|
|
96
|
-
|
|
97
|
-
- **[
|
|
98
|
-
|
|
99
|
-
- **[
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
The complete `docs/` folder ships with the npm package. Maintainer instructions are in
|
|
105
|
-
[CONTRIBUTING.md](https://github.com/kequtech/tenon-orm/blob/main/CONTRIBUTING.md), which is not
|
|
106
|
-
bundled.
|
|
53
|
+
- **[Human guide](docs/human-guide.md)** — a table, queries, transactions, and generated types.
|
|
54
|
+
- **[Agent guide](docs/agent-guide.md)** — implementation contracts and upgrade workflow.
|
|
55
|
+
- **[Reference](docs/reference.md)** — API options, value mappings, CLI, and limitations.
|
|
56
|
+
- **[Adoption guide](docs/adoption-guide.md)** — replace an ORM and transfer migration ownership.
|
|
57
|
+
- **[Mutation cookbook](docs/mutations.md)** — defaults, patches, upserts, arrays, and batching.
|
|
58
|
+
- **[Query inspection](docs/query-inspection.md)** — Describe reports and database tests.
|
|
59
|
+
- **[Changelog](CHANGELOG.md)** — versioned changes and upgrade instructions.
|
|
60
|
+
|
|
61
|
+
These documents all ship with the npm package, including the changelog so an agent upgrading a
|
|
62
|
+
project can read the relevant release entries locally. Repository maintenance instructions are in
|
|
63
|
+
[CONTRIBUTING.md](https://github.com/kequtech/tenon-orm/blob/main/CONTRIBUTING.md).
|
|
107
64
|
|
|
108
65
|
[ISC licensed](LICENSE). Maintained by [Kequtech](https://github.com/kequtech).
|
package/dist/affected-rows.js
CHANGED
|
@@ -17,14 +17,14 @@ export function validateAffectedRows(value) {
|
|
|
17
17
|
}
|
|
18
18
|
export function checkAffectedRows(constraint, result) {
|
|
19
19
|
if (!['INSERT', 'UPDATE', 'DELETE', 'MERGE'].includes(result.command) ||
|
|
20
|
-
result.
|
|
21
|
-
!Number.isSafeInteger(result.
|
|
22
|
-
result.
|
|
23
|
-
throw new AffectedRowsError(constraint, result.
|
|
20
|
+
result.rowCount === null ||
|
|
21
|
+
!Number.isSafeInteger(result.rowCount) ||
|
|
22
|
+
result.rowCount < 0) {
|
|
23
|
+
throw new AffectedRowsError(constraint, result.rowCount, result.command, false);
|
|
24
24
|
}
|
|
25
25
|
const expected = constraint.exact ?? constraint.max;
|
|
26
|
-
const matches = constraint.exact === undefined ? result.
|
|
26
|
+
const matches = constraint.exact === undefined ? result.rowCount <= expected : result.rowCount === expected;
|
|
27
27
|
if (!matches) {
|
|
28
|
-
throw new AffectedRowsError(constraint, result.
|
|
28
|
+
throw new AffectedRowsError(constraint, result.rowCount, result.command, true);
|
|
29
29
|
}
|
|
30
30
|
}
|
package/dist/baseline.js
CHANGED
|
@@ -82,16 +82,16 @@ export async function adoptBaseline(options) {
|
|
|
82
82
|
if (applied.length) {
|
|
83
83
|
return [];
|
|
84
84
|
}
|
|
85
|
-
const schema = await client.
|
|
85
|
+
const schema = await client.query('SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = $1', [
|
|
86
86
|
namespace,
|
|
87
87
|
]);
|
|
88
|
-
if (!schema.length) {
|
|
88
|
+
if (!schema.rows.length) {
|
|
89
89
|
throw new Error('The baseline namespace does not exist. Use migrate to initialize a fresh database.');
|
|
90
90
|
}
|
|
91
|
-
await client.
|
|
91
|
+
await client.query('BEGIN');
|
|
92
92
|
await createHistoryTable(client, namespace);
|
|
93
93
|
await recordMigration(client, namespace, baseline);
|
|
94
|
-
const result = await client.
|
|
94
|
+
const result = await client.query('COMMIT');
|
|
95
95
|
if (result.command !== 'COMMIT') {
|
|
96
96
|
throw new Error('Baseline adoption was rolled back.');
|
|
97
97
|
}
|
package/dist/client.js
CHANGED
|
@@ -16,7 +16,7 @@ export function createClient(query, onQuery) {
|
|
|
16
16
|
try {
|
|
17
17
|
const value = await query(statement.text, statement.values, (result) => {
|
|
18
18
|
command = result.command;
|
|
19
|
-
rowCount = result.
|
|
19
|
+
rowCount = result.rowCount;
|
|
20
20
|
if (options.affectedRows)
|
|
21
21
|
checkAffectedRows(options.affectedRows, result);
|
|
22
22
|
return queryOutput(options, result);
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Pool, type PoolClient, type PoolConfig, type QueryResult as PgResult } from 'pg';
|
|
2
2
|
import type { UnknownRow } from './query.js';
|
|
3
|
-
export type ConnectionOptions =
|
|
4
|
-
export type Connection = string | ConnectionOptions |
|
|
5
|
-
export type QueryResult<Row extends object = UnknownRow> =
|
|
6
|
-
/** The driver operations shared by ordinary and reserved connections. */
|
|
7
|
-
export interface SqlConnection {
|
|
8
|
-
unsafe<Rows extends object[] = UnknownRow[]>(text: string, input?: postgres.ParameterOrJSON<never>[], options?: postgres.UnsafeQueryOptions & {
|
|
9
|
-
simple?: boolean;
|
|
10
|
-
}): Promise<postgres.RowList<Rows>>;
|
|
11
|
-
array: postgres.Sql['array'];
|
|
12
|
-
typed: postgres.Sql['typed'];
|
|
13
|
-
}
|
|
3
|
+
export type ConnectionOptions = PoolConfig;
|
|
4
|
+
export type Connection = string | ConnectionOptions | Pool;
|
|
5
|
+
export type QueryResult<Row extends object = UnknownRow> = PgResult<Row>;
|
|
14
6
|
export type QueryExecutor = <T>(text: string, input: readonly unknown[], readResult: (result: QueryResult) => T, guarded: boolean) => Promise<T>;
|
|
15
|
-
/** Own
|
|
7
|
+
/** Own constructed pools; supplied pools retain their application's ownership and error policy. */
|
|
16
8
|
export declare function createConnection(connection: Connection): {
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
pool: Pool;
|
|
10
|
+
run<T>(work: () => Promise<T>): Promise<T>;
|
|
11
|
+
close(timeoutMs: number): Promise<void>;
|
|
19
12
|
};
|
|
20
|
-
|
|
13
|
+
/** A checked-out pg client remains tied to its session and is discarded after a failure. */
|
|
14
|
+
export declare function watchClient(client: PoolClient): {
|
|
15
|
+
check(): void;
|
|
16
|
+
release(error?: Error): void;
|
|
17
|
+
};
|
|
18
|
+
/** Unnamed extended protocol enforces a single statement, even without parameters. */
|
|
19
|
+
export declare function executeSql(connection: Pool | PoolClient, text: string, input?: readonly unknown[]): Promise<PgResult<UnknownRow>>;
|
package/dist/connection.js
CHANGED
|
@@ -1,22 +1,132 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Pool, } from 'pg';
|
|
2
2
|
import { plainObject } from './validation.js';
|
|
3
|
-
/**
|
|
3
|
+
/** A supplied pool may come from another installed copy of pg. */
|
|
4
|
+
function isPool(connection) {
|
|
5
|
+
return (connection !== null &&
|
|
6
|
+
typeof connection === 'object' &&
|
|
7
|
+
'connect' in connection &&
|
|
8
|
+
typeof connection.connect === 'function' &&
|
|
9
|
+
'query' in connection &&
|
|
10
|
+
typeof connection.query === 'function' &&
|
|
11
|
+
'totalCount' in connection &&
|
|
12
|
+
typeof connection.totalCount === 'number' &&
|
|
13
|
+
'end' in connection &&
|
|
14
|
+
typeof connection.end === 'function');
|
|
15
|
+
}
|
|
16
|
+
/** Own constructed pools; supplied pools retain their application's ownership and error policy. */
|
|
4
17
|
export function createConnection(connection) {
|
|
5
|
-
if (
|
|
6
|
-
return {
|
|
7
|
-
|
|
18
|
+
if (isPool(connection))
|
|
19
|
+
return { pool: connection, run: (work) => work(), close: async () => { } };
|
|
20
|
+
let config;
|
|
8
21
|
if (typeof connection === 'string') {
|
|
9
22
|
if (!connection.trim())
|
|
10
23
|
throw new Error('Connection string must not be empty.');
|
|
11
|
-
|
|
24
|
+
config = { connectionString: connection };
|
|
12
25
|
}
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
else {
|
|
27
|
+
plainObject(connection, 'Connection configuration');
|
|
28
|
+
if ('url' in connection) {
|
|
29
|
+
throw new Error('Use pg connectionString in the connection configuration instead of url/options.');
|
|
30
|
+
}
|
|
31
|
+
config = connection;
|
|
32
|
+
}
|
|
33
|
+
const pool = new Pool(config);
|
|
34
|
+
const clients = new Set();
|
|
35
|
+
const pending = new Map();
|
|
36
|
+
let terminating = false;
|
|
37
|
+
const terminate = (client) => {
|
|
38
|
+
void client.end().catch(() => { });
|
|
39
|
+
};
|
|
40
|
+
// pool.end handles idle clients; the deadline also ends native checkouts.
|
|
41
|
+
pool.on('acquire', (client) => {
|
|
42
|
+
clients.add(client);
|
|
43
|
+
if (terminating)
|
|
44
|
+
terminate(client);
|
|
45
|
+
});
|
|
46
|
+
pool.on('release', (_error, client) => clients.delete(client));
|
|
47
|
+
pool.on('error', (error) => process.emitWarning(error));
|
|
48
|
+
return {
|
|
49
|
+
pool,
|
|
50
|
+
run(work) {
|
|
51
|
+
let expire;
|
|
52
|
+
const task = new Promise((resolve, reject) => {
|
|
53
|
+
expire = reject;
|
|
54
|
+
void Promise.resolve().then(work).then(resolve, reject);
|
|
55
|
+
});
|
|
56
|
+
pending.set(task, expire);
|
|
57
|
+
const forget = () => {
|
|
58
|
+
pending.delete(task);
|
|
59
|
+
};
|
|
60
|
+
void task.then(forget, forget);
|
|
61
|
+
return task;
|
|
62
|
+
},
|
|
63
|
+
async close(timeoutMs) {
|
|
64
|
+
let timer;
|
|
65
|
+
let ending;
|
|
66
|
+
const end = () => (ending ??= pool.end());
|
|
67
|
+
try {
|
|
68
|
+
await Promise.race([
|
|
69
|
+
(async () => {
|
|
70
|
+
await Promise.allSettled([...pending.keys()]);
|
|
71
|
+
await end();
|
|
72
|
+
})(),
|
|
73
|
+
new Promise((resolve) => {
|
|
74
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
75
|
+
}).then(async () => {
|
|
76
|
+
// Native callbacks must reject close on failure, not escape a timer callback.
|
|
77
|
+
terminating = true;
|
|
78
|
+
const error = new Error('The database shutdown deadline expired.');
|
|
79
|
+
for (const reject of pending.values())
|
|
80
|
+
reject(error);
|
|
81
|
+
try {
|
|
82
|
+
void end().catch(() => { });
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
// Attempt every checkout even when one native end call throws.
|
|
86
|
+
await Promise.all([...clients].map(async (client) => terminate(client)));
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** A checked-out pg client remains tied to its session and is discarded after a failure. */
|
|
98
|
+
export function watchClient(client) {
|
|
99
|
+
let failure;
|
|
100
|
+
const onError = (error) => {
|
|
101
|
+
failure ??= error;
|
|
102
|
+
};
|
|
103
|
+
const onEnd = () => {
|
|
104
|
+
failure ??= new Error('The PostgreSQL connection closed.');
|
|
105
|
+
};
|
|
106
|
+
client.on('error', onError);
|
|
107
|
+
client.on('end', onEnd);
|
|
108
|
+
return {
|
|
109
|
+
check() {
|
|
110
|
+
if (failure)
|
|
111
|
+
throw failure;
|
|
112
|
+
},
|
|
113
|
+
release(error) {
|
|
114
|
+
try {
|
|
115
|
+
client.release(error ?? failure);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
client.removeListener('error', onError);
|
|
119
|
+
client.removeListener('end', onEnd);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
15
123
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
124
|
+
/** Unnamed extended protocol enforces a single statement, even without parameters. */
|
|
125
|
+
export function executeSql(connection, text, input = []) {
|
|
126
|
+
const config = {
|
|
127
|
+
text,
|
|
128
|
+
values: [...input],
|
|
129
|
+
queryMode: 'extended',
|
|
130
|
+
};
|
|
131
|
+
return connection.query(config);
|
|
22
132
|
}
|
package/dist/database.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { createClient } from './client.js';
|
|
2
2
|
import { createConnection, executeSql } from './connection.js';
|
|
3
|
-
import { prepareArrays } from './driver/array.js';
|
|
4
3
|
import { withTransaction } from './transaction.js';
|
|
5
4
|
import { plainObject } from './validation.js';
|
|
5
|
+
function closeTimeout(options) {
|
|
6
|
+
const settings = plainObject(options === undefined ? {} : options, 'Close options');
|
|
7
|
+
for (const key of Object.keys(settings)) {
|
|
8
|
+
if (key !== 'timeoutMs')
|
|
9
|
+
throw new Error(`Unknown close option: ${key}`);
|
|
10
|
+
}
|
|
11
|
+
const timeoutMs = settings.timeoutMs === undefined ? 5000 : settings.timeoutMs;
|
|
12
|
+
if (typeof timeoutMs !== 'number' ||
|
|
13
|
+
!Number.isInteger(timeoutMs) ||
|
|
14
|
+
timeoutMs < 0 ||
|
|
15
|
+
timeoutMs > 2147483647) {
|
|
16
|
+
throw new Error('Close timeoutMs must be an integer from 0 to 2147483647 milliseconds.');
|
|
17
|
+
}
|
|
18
|
+
return timeoutMs;
|
|
19
|
+
}
|
|
6
20
|
export function createTenon(options) {
|
|
7
21
|
for (const key of Object.keys(plainObject(options, 'Tenon options'))) {
|
|
8
22
|
if (key !== 'connection' && key !== 'onQuery') {
|
|
@@ -12,35 +26,40 @@ export function createTenon(options) {
|
|
|
12
26
|
if (options.onQuery !== undefined && typeof options.onQuery !== 'function') {
|
|
13
27
|
throw new Error('onQuery must be a function.');
|
|
14
28
|
}
|
|
15
|
-
const {
|
|
16
|
-
let
|
|
29
|
+
const { pool, run, close } = createConnection(options.connection);
|
|
30
|
+
let closing;
|
|
17
31
|
function checkOpen() {
|
|
18
|
-
if (
|
|
32
|
+
if (closing) {
|
|
19
33
|
throw new Error('This database instance is closed.');
|
|
20
34
|
}
|
|
21
35
|
}
|
|
22
|
-
const query =
|
|
36
|
+
const query = (text, input, readResult, guarded) => {
|
|
23
37
|
checkOpen();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
return run(async () => {
|
|
39
|
+
if (guarded) {
|
|
40
|
+
return withTransaction(pool, (query) => query(text, input, readResult, false));
|
|
41
|
+
}
|
|
42
|
+
return readResult(await executeSql(pool, text, input));
|
|
43
|
+
});
|
|
29
44
|
};
|
|
30
45
|
return {
|
|
31
46
|
...createClient(query, options.onQuery),
|
|
32
47
|
native: pool,
|
|
33
48
|
async transaction(callback) {
|
|
34
49
|
checkOpen();
|
|
35
|
-
return withTransaction(pool, (query) => callback(createClient(query, options.onQuery)));
|
|
50
|
+
return run(() => withTransaction(pool, (query) => callback(createClient(query, options.onQuery))));
|
|
36
51
|
},
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
return;
|
|
52
|
+
close(options) {
|
|
53
|
+
if (closing)
|
|
54
|
+
return closing;
|
|
55
|
+
try {
|
|
56
|
+
const timeoutMs = closeTimeout(options);
|
|
57
|
+
// Cache before invoking driver callbacks, so reentrant/concurrent callers share shutdown.
|
|
58
|
+
closing = Promise.resolve().then(() => close(timeoutMs));
|
|
59
|
+
return closing;
|
|
40
60
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await pool.end();
|
|
61
|
+
catch (error) {
|
|
62
|
+
return Promise.reject(error);
|
|
44
63
|
}
|
|
45
64
|
},
|
|
46
65
|
};
|
package/dist/driver/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** pg default representations, shared by generation and query inspection. */
|
|
2
2
|
export type ValueTypes = Readonly<{
|
|
3
3
|
read: string;
|
|
4
4
|
write: string;
|
package/dist/driver/types.js
CHANGED
|
@@ -8,18 +8,18 @@ export const arrayTypes = {
|
|
|
8
8
|
'1022': 'number',
|
|
9
9
|
'1028': 'number',
|
|
10
10
|
'1016': { read: 'string', write: 'string | bigint' },
|
|
11
|
-
'1231': { read: '
|
|
11
|
+
'1231': { read: 'number', write: 'string | number' },
|
|
12
12
|
'1009': 'string',
|
|
13
13
|
'1014': 'string',
|
|
14
14
|
'1015': 'string',
|
|
15
15
|
'1183': 'string',
|
|
16
16
|
'1270': 'string',
|
|
17
17
|
'2951': 'string',
|
|
18
|
-
'1182': { read: 'globalThis.Date', write: 'globalThis.Date | string' },
|
|
19
|
-
'1115': { read: 'globalThis.Date', write: 'globalThis.Date | string' },
|
|
20
|
-
'1185': { read: 'globalThis.Date', write: 'globalThis.Date | string' },
|
|
21
|
-
'199': { read: 'unknown', write: '
|
|
22
|
-
'3807': { read: 'unknown', write: '
|
|
18
|
+
'1182': { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
|
|
19
|
+
'1115': { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
|
|
20
|
+
'1185': { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
|
|
21
|
+
'199': { read: 'unknown', write: 'string' },
|
|
22
|
+
'3807': { read: 'unknown', write: 'string' },
|
|
23
23
|
};
|
|
24
24
|
export const scalarTypes = {
|
|
25
25
|
int2: 'number',
|
|
@@ -35,13 +35,13 @@ export const scalarTypes = {
|
|
|
35
35
|
time: 'string',
|
|
36
36
|
timetz: 'string',
|
|
37
37
|
uuid: 'string',
|
|
38
|
-
date: { read: 'globalThis.Date', write: 'globalThis.Date | string' },
|
|
39
|
-
timestamp: { read: 'globalThis.Date', write: 'globalThis.Date | string' },
|
|
40
|
-
timestamptz: { read: 'globalThis.Date', write: 'globalThis.Date | string' },
|
|
38
|
+
date: { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
|
|
39
|
+
timestamp: { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
|
|
40
|
+
timestamptz: { read: 'globalThis.Date | number', write: 'globalThis.Date | string' },
|
|
41
41
|
int8: { read: 'string', write: 'string | bigint' },
|
|
42
42
|
numeric: { read: 'string', write: 'string | number' },
|
|
43
|
-
json: { read: 'unknown', write: '
|
|
44
|
-
jsonb: { read: 'unknown', write: '
|
|
43
|
+
json: { read: 'unknown', write: 'string' },
|
|
44
|
+
jsonb: { read: 'unknown', write: 'string' },
|
|
45
45
|
bytea: 'import("node:buffer").Buffer',
|
|
46
46
|
};
|
|
47
47
|
export function valueTypes(mapping) {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { AffectedRowsConstraint } from './affected-rows.js';
|
|
2
|
+
export declare class CardinalityError extends Error {
|
|
3
|
+
name: string;
|
|
4
|
+
readonly code = "TENON_CARDINALITY_MISMATCH";
|
|
5
|
+
readonly expected: 'one' | 'oneOrNone';
|
|
6
|
+
readonly actual: number;
|
|
7
|
+
readonly label?: string;
|
|
8
|
+
constructor(expected: 'one' | 'oneOrNone', actual: number, label?: string);
|
|
9
|
+
}
|
|
2
10
|
export declare class AffectedRowsError extends Error {
|
|
3
11
|
name: string;
|
|
4
12
|
readonly code: 'TENON_AFFECTED_ROWS_MISMATCH' | 'TENON_AFFECTED_ROWS_UNSUPPORTED';
|
package/dist/errors.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
export class CardinalityError extends Error {
|
|
2
|
+
name = 'CardinalityError';
|
|
3
|
+
code = 'TENON_CARDINALITY_MISMATCH';
|
|
4
|
+
expected;
|
|
5
|
+
actual;
|
|
6
|
+
label;
|
|
7
|
+
constructor(expected, actual, label) {
|
|
8
|
+
const count = expected === 'one' ? 'exactly one row' : 'at most one row';
|
|
9
|
+
super(`${label ?? 'Query'} expected ${count}, but returned ${actual} rows.`);
|
|
10
|
+
this.expected = expected;
|
|
11
|
+
this.actual = actual;
|
|
12
|
+
if (label !== undefined)
|
|
13
|
+
this.label = label;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
1
16
|
export class AffectedRowsError extends Error {
|
|
2
17
|
name = 'AffectedRowsError';
|
|
3
18
|
code;
|