@kequtech/tenon-orm 0.4.0 → 0.4.2

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2
4
+
5
+ - Rewrite the README and consumer guides around a complete getting-started workflow and task-based navigation.
6
+ - Add guides for SQL queries, schema types and values, connections, and migrations, including joins, pagination, codec configuration, rollback, and migration recovery.
7
+ - Clarify query result modes, write guards, generated types, source checking, and their validation limits. This release changes documentation only; no application migration is required.
8
+
9
+ ## 0.4.1
10
+
11
+ - Add `db.generateTypes({ out, namespace?, check?, onWarning? })`, using the instance's configured codecs.
12
+ - Remove `tenon generate`; run generation and freshness checks from an application script.
13
+ Use the configured database instance, set `check: true` for freshness checks, and close it when
14
+ the script finishes. See the [generation script example](docs/human-guide.md#generate-types).
15
+
3
16
  ## 0.4.0
4
17
 
5
18
  ### Breaking changes
@@ -9,7 +22,7 @@
9
22
  Connections accept a PostgreSQL URI, pgclear `PoolOptions`, or a pgclear `Pool`; translate former
10
23
  pg configuration and supply deployment settings explicitly.
11
24
  PG environment defaults, pgpass/service files, and pg-specific URI options are not read. See
12
- [upgrading from 0.3.x](docs/adoption-guide.md#upgrade-from-tenon-03x).
25
+ [connection configuration](docs/connections.md#configure-a-database-instance).
13
26
  - `db.native` is now a pgclear `Pool`. Use `queryClassic(text, values?)` for finished SQL and
14
27
  acquire/withSession for held sessions; native `query` accepts pgclear's own sql tag only. Prepared
15
28
  statements, streams, and COPY use the public session APIs. Use pgclear `listen` for dedicated
@@ -21,7 +34,9 @@
21
34
  output conversions are opt-in. Undefined, plain objects, and arrays reject; mutation-helper
22
35
  undefined still omits. JSON encoding still rejects bigint values.
23
36
  - Tenon owns schema generation and exports `Database[table]['row' | 'insert' | 'update']`.
24
- Regenerate committed definitions, replace former Rows imports, and review write representations.
37
+ Regenerate committed definitions and review write representations. Replace `Rows[table]`,
38
+ `Rows.Insert[table]`, and `Rows.Update[table]` with `Database[table]['row']`,
39
+ `Database[table]['insert']`, and `Database[table]['update']`.
25
40
  Catalog queries, schema summaries, defaults/domains/identities, and TypeScript rendering live in
26
41
  Tenon; decoder representations come from pgclear. Import generation from Tenon instead of pgclear.
27
42
  Tenon retains migration coordination, atomic file output, and `generate --check` freshness checks.
package/README.md CHANGED
@@ -2,19 +2,20 @@
2
2
 
3
3
  A PostgreSQL ORM designed for coding agents. Built for Node.js and TypeScript.
4
4
 
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 and optional source-aware checking. Tenon owns database declarations and application
8
- TypeScript tooling. Execution, Describe, and value conversion come from the
9
- `@kequtech/pgclear` runtime dependency. Import `sql`, `json`, `param`, and codec helpers from
10
- Tenon for application queries; no separate driver installation is needed.
5
+ Tenon keeps database work in forms coding agents can read, write, and check: explicit SQL, ordinary
6
+ TypeScript functions, and SQL migrations. Queries carry their result types and execution options
7
+ together, so an agent and the person reviewing its work can see what an operation does in one place.
11
8
 
12
9
  ```sh
13
10
  npm install @kequtech/tenon-orm
14
11
  ```
15
12
 
16
- Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript consumers need TypeScript 6+ and Node
17
- development typings.
13
+ Requires Node.js 24+, PostgreSQL 16+, and ESM. TypeScript applications need TypeScript 6+ and Node.js
14
+ typings. The PostgreSQL driver, [pgclear](https://github.com/kequtech/pgclear), is included.
15
+
16
+ ## Query with SQL
17
+
18
+ This example assumes a `public.users` table with integer `id` and text `email` columns:
18
19
 
19
20
  ```ts
20
21
  import { createTenon, sql } from '@kequtech/tenon-orm';
@@ -31,57 +32,89 @@ try {
31
32
  `,
32
33
  { returns: 'oneOrNone' },
33
34
  );
34
- // user is { id: number; email: string } | undefined
35
+ // user: { id: number; email: string } | undefined
35
36
  } finally {
36
37
  await db.close();
37
38
  }
38
39
  ```
39
40
 
40
- The example assumes `public.users` exists. In a server, reuse the instance and close it at shutdown.
41
- `oneOrNone` rejects multiple rows; use `one` for exactly one row, `many` for an array, or omit
42
- `returns` for the driver's result object and metadata.
43
-
44
- Command `rowCount` values are numbers. SQL `count(*)`, bigint/bigserial columns, and bigint array
45
- elements read as exact strings by default. Plain bigint, Date, and supported native/polyfilled
46
- Temporal inputs use server type inference; output conversions to bigint, Date, or Temporal are
47
- explicit codec choices. JSON and PostgreSQL arrays use explicit helpers. See the
48
- [value mappings](docs/reference.md#generated-types-and-checking).
49
-
50
- Joins, relationships, aggregates, and upserts use PostgreSQL SQL. Object-based writes can use
51
- `sql.insertValues` and `sql.assignments` while keeping the surrounding statement explicit.
52
- `db.native` exposes the underlying pgclear `Pool` for prepared statements, streams, COPY, and
53
- other driver features; those calls bypass Tenon's query and transaction guards. Use pgclear's
54
- `listen` for a dedicated notification connection.
55
-
56
- `sql<Row>` declares an expected result type. `tenon check src/users.ts:23` checks a recoverable query
57
- against that type and PostgreSQL Describe without executing the statement. Inconclusive types remain
58
- unproven; unsupported SQL composition is reported as unchecked. `describeQuery` inspects finished SQL
59
- without comparing a source annotation. Keep database tests for actual
60
- rows and write effects. Validation, authorization, and retry policy belong to the application.
61
- Existing migration runners can be retained; Tenon's migrations and type generation are optional.
41
+ `${email}` becomes `$1` in the SQL, with the value passed separately. `sql<Row>` describes one
42
+ returned row. `returns` chooses how Tenon gives those rows back:
43
+
44
+ | `returns` | You receive | Required rows |
45
+ | ------------------ | --------------------------------------------------- | ------------- |
46
+ | `many` | `readonly Row[]` | Any number |
47
+ | `oneOrNone` | `Row \| undefined` | Zero or one |
48
+ | `one` | `Row` | Exactly one |
49
+ | `native` (default) | Driver result with `rows`, `rowCount`, and `fields` | Any number |
50
+
51
+ A row-count mismatch throws. The modes do not add `LIMIT` or change the SQL. In a server, reuse one
52
+ database instance and close it during shutdown. Supply environment variables through your application;
53
+ Tenon does not load `.env` files.
54
+
55
+ ## Designed for coding agents
56
+
57
+ Tenon's API gives coding agents concrete code to work with and explicit contracts to preserve:
58
+
59
+ - **Readable SQL.** Selected columns, joins, filters, and conflict handling are visible in the query.
60
+ An agent can inspect the operation directly and use PostgreSQL's existing vocabulary.
61
+ - **Ordinary TypeScript.** Reusable queries are functions. An agent can follow imports, change a
62
+ function, and pass the same query code a database or transaction client.
63
+ - **Explicit write limits.** A query can require exactly one affected row or set a maximum. Tenon
64
+ rolls back the operation if that bound is violated.
65
+ - **Actionable checks.** Generated table types describe storage contracts; `tenon check` reports
66
+ contradictions between recoverable SQL and declared types. Freshness checks catch stale generated
67
+ declarations, and database tests verify results and effects.
68
+
69
+ The guides and changelog ship with the package, giving an agent documentation for the installed
70
+ version. For work in an existing project, start with the [agent guide](docs/agent-guide.md).
71
+
72
+ ## How Tenon fits together
73
+
74
+ - **SQL defines the operation.** Use PostgreSQL joins, aggregates, constraints, and upserts directly.
75
+ Reuse queries with ordinary functions; use fragments when parts of the SQL need to vary.
76
+ - **TypeScript describes the values.** Declare query projections with `sql<Row>`. Optionally generate
77
+ table read/insert/update types with `db.generateTypes({ out: 'src/generated-schema.d.ts' })`.
78
+ - **Transactions define what commits together.** Use `db.transaction(async (tx) => { ... })` and run
79
+ every participating query through `tx`.
80
+ - **Write guards check affected rows.** `{ affectedRows: { exact: 1 } }` rolls back a write unless
81
+ exactly one row was affected. `{ max: 1 }` allows zero or one.
82
+ - **Migrations change the schema.** Tenon's optional runner applies numbered SQL files. You can keep
83
+ an existing schema and migration runner.
84
+
85
+ For object inputs, `sql.insertValues(input)` and `sql.assignments(patch)` build the column/value
86
+ parts of a statement. The table, conditions, expressions, and conflict behavior stay in your SQL.
87
+ There are no model instances or implicit relationship loading.
88
+
89
+ Query types are declarations. Tenon does not validate returned data against `Row` during execution.
90
+ The optional `tenon check` command compares recoverable source queries with TypeScript types and
91
+ PostgreSQL metadata. Keep database tests for actual results and write effects.
92
+
93
+ Some PostgreSQL values need care: bigint columns and `count(*)` read as exact strings, numeric and
94
+ temporal values also default to strings, and JSON/array inputs use explicit encoding helpers. Command
95
+ `rowCount` is a number or null. See [value mappings](docs/schema-types.md#value-mappings) before
96
+ choosing application types.
62
97
 
63
98
  ## Documentation
64
99
 
65
- Start with this README, then follow the [agent guide](docs/agent-guide.md) when working in an
66
- existing project or the [human guide](docs/human-guide.md) for a first setup. Use the reference for
67
- specific options and the focused guides when a task needs them. Read the docs shipped with the
68
- installed Tenon version; unreleased repository changes may differ.
69
-
70
- - **[Human guide](docs/human-guide.md)** — a table, queries, transactions, and generated types.
71
- - **[Agent guide](docs/agent-guide.md)** — implementation contracts and upgrade workflow.
72
- - **[Reference](docs/reference.md)** — API options, value mappings, CLI, and limitations.
73
- - **[Adoption guide](docs/adoption-guide.md)** — replace an ORM and transfer migration ownership.
74
- - **[Mutation cookbook](docs/mutations.md)** — defaults, patches, upserts, arrays, and batching.
75
- - **[Query inspection](docs/query-inspection.md)** — Describe reports and database tests.
76
- - **[Query checking](docs/query-checking.md)** — project/file/line checking, composition, and limitations.
77
- - **[Changelog](CHANGELOG.md)** — versioned changes and upgrade instructions.
78
-
79
- For common failures, see [troubleshooting](docs/reference.md#troubleshooting). To choose between
80
- source checking, SQL inspection, and generated-file freshness, see
81
- [validation tools](docs/reference.md#choosing-a-validation-tool).
82
-
83
- These documents all ship with the npm package, including the changelog so an agent upgrading a
84
- project can read the relevant release entries locally. Repository maintenance instructions are in
100
+ Start with [Getting started](docs/human-guide.md) for a complete table-to-query example. Then choose
101
+ the guide for your task:
102
+
103
+ | Guide | What it covers |
104
+ | ---------------------------------------------------------- | ---------------------------------------------------------------------------- |
105
+ | [Writing queries](docs/sql.md) | Parameters, reusable functions, joins, aggregates, pagination, and fragments |
106
+ | [Writing data](docs/mutations.md) | Inserts, patches, defaults, upserts, JSON, arrays, and batches |
107
+ | [Schema types and values](docs/schema-types.md) | Generation, read/write types, codecs, and domain validation |
108
+ | [Migrations](docs/migrations.md) | Creating, applying, inspecting, and rolling back SQL migrations |
109
+ | [Connections and driver access](docs/connections.md) | Pool configuration, shutdown, custom codecs, and native features |
110
+ | [Checking query types](docs/query-checking.md) | Source-aware checks and their supported SQL composition |
111
+ | [Inspecting and testing queries](docs/query-inspection.md) | PostgreSQL metadata and tests of real behavior |
112
+ | [Adopting Tenon](docs/adoption-guide.md) | Replacing an ORM and transferring migration ownership |
113
+ | [Agent guide](docs/agent-guide.md) | A short implementation workflow for an existing project |
114
+ | [API and CLI reference](docs/reference.md) | Signatures, options, errors, commands, and troubleshooting |
115
+
116
+ The README, guides, and [changelog](CHANGELOG.md) ship with the npm package. Read the documentation
117
+ for your installed version when upgrading. Library maintenance is covered in
85
118
  [CONTRIBUTING.md](https://github.com/kequtech/tenon-orm/blob/main/CONTRIBUTING.md).
86
119
 
87
120
  [ISC licensed](LICENSE). Maintained by [Kequtech](https://github.com/kequtech).
package/dist/cli.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
3
  import { adoptBaseline, captureBaseline } from './baseline.js';
4
- import { generateDefinitions } from './generate.js';
5
- import { writeDefinitions } from './generation/output.js';
6
4
  import { describeFlags, inspectionSettings, runDescribe } from './inspection/cli.js';
7
5
  import { migrate, migrationStatus, rollback } from './migrations.js';
8
6
  import { createMigration } from './migrations/files.js';
@@ -10,7 +8,6 @@ const help = `tenon migrate|status|rollback [--directory migrations] [--namespac
10
8
  tenon baseline capture|adopt [--directory migrations] [--namespace public]
11
9
  baseline capture: [--schema name ...] (default: whole database schema)
12
10
  tenon new <name> [--directory migrations] (no database required)
13
- tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
14
11
  tenon describe <query.sql> [--role name] [--search-path schemas] [--time-zone zone]
15
12
  tenon check [file.ts[:line]] [--project tsconfig.json] [--json]
16
13
  describe: [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
@@ -41,9 +38,7 @@ async function main() {
41
38
  options: {
42
39
  directory: { type: 'string', default: 'migrations' },
43
40
  namespace: { type: 'string', default: 'public' },
44
- out: { type: 'string', default: 'src/generated-schema.ts' },
45
41
  schema: { type: 'string', multiple: true },
46
- check: { type: 'boolean', default: false },
47
42
  steps: { type: 'string', default: '1' },
48
43
  help: { type: 'boolean', default: false },
49
44
  project: { type: 'string' },
@@ -56,18 +51,17 @@ async function main() {
56
51
  return;
57
52
  }
58
53
  const [command, argument] = positionals;
54
+ if (command === 'generate') {
55
+ throw new Error('The generate command has been removed. Use db.generateTypes({ out, namespace?, check? }) in an application script so generation uses your configured codecs.');
56
+ }
59
57
  // Validate explicitly supplied flags, not defaults. Silently ignoring --check
60
58
  // on migrate, for example, would turn an intended dry run into a schema change.
61
59
  const allowed = new Set(['help']);
62
- if (command !== 'generate' && command !== 'describe' && command !== 'check')
60
+ if (command !== 'describe' && command !== 'check')
63
61
  allowed.add('directory');
64
62
  if (command !== 'new' && command !== 'describe' && command !== 'check') {
65
63
  allowed.add('namespace');
66
64
  }
67
- if (command === 'generate') {
68
- allowed.add('out');
69
- allowed.add('check');
70
- }
71
65
  if (command === 'rollback') {
72
66
  allowed.add('steps');
73
67
  }
@@ -100,10 +94,9 @@ async function main() {
100
94
  ? positionals.length <= 2
101
95
  : baseline || command === 'describe'
102
96
  ? positionals.length === 2
103
- : positionals.length === 1 &&
104
- ['migrate', 'generate', 'status', 'rollback'].includes(command ?? '');
97
+ : positionals.length === 1 && ['migrate', 'status', 'rollback'].includes(command ?? '');
105
98
  if (!valid) {
106
- throw new Error('Choose new, baseline capture|adopt, migrate, generate, describe <query.sql>, check [file.ts[:line]], status, or rollback. Use --help for options.');
99
+ throw new Error('Choose new, baseline capture|adopt, migrate, describe <query.sql>, check [file.ts[:line]], status, or rollback. Use --help for options.');
107
100
  }
108
101
  const connection = process.env.DATABASE_URL;
109
102
  if (!connection) {
@@ -141,15 +134,6 @@ async function main() {
141
134
  return;
142
135
  }
143
136
  switch (command) {
144
- case 'generate': {
145
- const source = await generateDefinitions({
146
- connection,
147
- namespace: values.namespace,
148
- onWarning: warn,
149
- });
150
- await writeDefinitions(source, values.out, values.check);
151
- break;
152
- }
153
137
  case 'status':
154
138
  process.stdout.write(`${JSON.stringify(await migrationStatus(options), null, 2)}\n`);
155
139
  break;
package/dist/database.js CHANGED
@@ -2,6 +2,7 @@ import { createClient } from './client.js';
2
2
  import { createConnection, executeSql } from './connection.js';
3
3
  import { withTransaction } from './transaction.js';
4
4
  import { plainObject } from './validation.js';
5
+ import { generateTypes } from './generate.js';
5
6
  function closeTimeout(options) {
6
7
  const settings = plainObject(options === undefined ? {} : options, 'Close options');
7
8
  for (const key of Object.keys(settings)) {
@@ -42,6 +43,10 @@ export function createTenon(options) {
42
43
  return {
43
44
  ...createClient(query, options.onQuery),
44
45
  native: pool,
46
+ async generateTypes(options) {
47
+ checkOpen();
48
+ await generateTypes(pool, options);
49
+ },
45
50
  async transaction(callback) {
46
51
  checkOpen();
47
52
  return withTransaction(pool, (query) => callback(createClient(query, options.onQuery)));
@@ -4,5 +4,16 @@ export interface GenerateOptions {
4
4
  readonly namespace?: string;
5
5
  readonly onWarning?: (message: string) => void | Promise<void>;
6
6
  }
7
+ export interface GenerateTypesOptions {
8
+ /** Output path, resolved from the working directory. */
9
+ readonly out: string;
10
+ /** Schema to inspect; defaults to public. */
11
+ readonly namespace?: string;
12
+ /** Reject missing or stale output without writing. Defaults to false. */
13
+ readonly check?: boolean;
14
+ readonly onWarning?: (message: string) => void | Promise<void>;
15
+ }
16
+ /** Generate with the owning database's connection and preserve exact file freshness checks. */
17
+ export declare function generateTypes(connection: Connection, options: GenerateTypesOptions): Promise<void>;
7
18
  /** Read one schema snapshot with default catalog codecs and the selected application read mappings. */
8
19
  export declare function generateDefinitions(options: GenerateOptions): Promise<string>;
package/dist/generate.js CHANGED
@@ -4,7 +4,32 @@ import { renderDatabase } from './generation/render.js';
4
4
  import { domainSummaries, schemaSummary, tableSummary } from './generation/schema-summary.js';
5
5
  import { withMigrationLock } from './migrations/connection.js';
6
6
  import { migrationTable } from './migrations/history.js';
7
- import { quote } from './sql.js';
7
+ import { quote, validateName } from './sql.js';
8
+ import { plainObject } from './validation.js';
9
+ import { writeDefinitions } from './generation/output.js';
10
+ /** Generate with the owning database's connection and preserve exact file freshness checks. */
11
+ export async function generateTypes(connection, options) {
12
+ const config = plainObject(options, 'Generation options');
13
+ for (const key of Object.keys(config)) {
14
+ if (!['out', 'namespace', 'check', 'onWarning'].includes(key)) {
15
+ throw new Error(`Unknown generation option: ${key}`);
16
+ }
17
+ }
18
+ if (typeof config.out !== 'string' || !config.out.trim() || config.out.includes('\0')) {
19
+ throw new Error('Generation out must be a nonempty file path.');
20
+ }
21
+ if (config.check !== undefined && typeof config.check !== 'boolean') {
22
+ throw new Error('Generation check must be a boolean.');
23
+ }
24
+ if (options.namespace !== undefined)
25
+ validateName(options.namespace);
26
+ const source = await generateDefinitions({
27
+ connection,
28
+ ...(options.namespace !== undefined && { namespace: options.namespace }),
29
+ ...(options.onWarning !== undefined && { onWarning: options.onWarning }),
30
+ });
31
+ await writeDefinitions(source, config.out, config.check === true);
32
+ }
8
33
  /** Read one schema snapshot with default catalog codecs and the selected application read mappings. */
9
34
  export async function generateDefinitions(options) {
10
35
  if (options.onWarning !== undefined && typeof options.onWarning !== 'function') {
@@ -14,12 +14,11 @@ export async function writeDefinitions(source, filename, check) {
14
14
  if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
15
15
  throw error;
16
16
  }
17
- throw new Error(`Generated definitions file is missing: ${output}\nRun tenon generate with the same options, omitting --check, then commit the result.`);
17
+ throw new Error(`Generated definitions file is missing: ${output}\nRun db.generateTypes with the same options and check: false, then commit the result.`);
18
18
  }
19
19
  if (existing !== source) {
20
- throw new Error(`Generated definitions differ: ${output}\n${definitionDiff(existing, source)}\nRun tenon generate with the same options, omitting --check, then commit the result.`);
20
+ throw new Error(`Generated definitions differ: ${output}\n${definitionDiff(existing, source)}\nRun db.generateTypes with the same options and check: false, then commit the result.`);
21
21
  }
22
- process.stdout.write('Generated definitions are current.\n');
23
22
  return;
24
23
  }
25
24
  await mkdir(dirname(output), { recursive: true });
@@ -31,5 +30,4 @@ export async function writeDefinitions(source, filename, check) {
31
30
  finally {
32
31
  await rm(temporary, { force: true });
33
32
  }
34
- process.stdout.write(`Generated ${output}\n`);
35
33
  }
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export type { DescribeOptions, DescribedColumn, DescribedType, DescriptionIssue,
14
14
  export { adoptBaseline, captureBaseline } from './baseline.js';
15
15
  export type { CaptureBaselineOptions, CapturedBaseline } from './baseline.js';
16
16
  export { generateDefinitions } from './generate.js';
17
- export type { GenerateOptions } from './generate.js';
17
+ export type { GenerateOptions, GenerateTypesOptions } from './generate.js';
18
18
  export { migrate, migrationStatus, rollback } from './migrations.js';
19
19
  export type { MigrationStatus } from './migrations.js';
20
20
  export type { DatabaseToolOptions } from './migrations/connection.js';
package/dist/types.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { Pool } from '@kequtech/pgclear';
2
2
  import type { QueryResult } from './connection.js';
3
3
  import type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
4
4
  import type { SqlQuery } from './query.js';
5
+ import type { GenerateTypesOptions } from './generate.js';
5
6
  export interface QueryClient {
6
7
  query<Row extends object>(query: SqlQuery<Row>, options?: QueryOptions & {
7
8
  readonly returns?: 'native';
@@ -19,6 +20,8 @@ export interface CloseOptions {
19
20
  export type Database = Transaction & {
20
21
  /** The underlying pool-level driver client; native calls bypass Tenon's query and transaction guards. */
21
22
  readonly native: Pool;
23
+ /** Write or check schema declarations using this instance's configured codecs. */
24
+ generateTypes(options: GenerateTypesOptions): Promise<void>;
22
25
  /** Await all work inside the callback. Commit on success; rollback on an escaping error. */
23
26
  transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
24
27
  close(options?: CloseOptions): Promise<void>;