@kequtech/tenon-orm 0.4.0 → 0.4.3
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 +22 -2
- package/README.md +93 -51
- package/dist/check.js +4 -2
- package/dist/cli.js +97 -40
- package/dist/config/init.d.ts +2 -0
- package/dist/config/init.js +34 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.js +159 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +1 -0
- package/dist/connection.d.ts +5 -0
- package/dist/describe.js +3 -1
- package/dist/generation/output.js +0 -2
- package/dist/index.d.ts +2 -1
- package/dist/inspection/cli.d.ts +3 -4
- package/dist/inspection/cli.js +3 -13
- package/dist/inspection/connection.js +23 -19
- package/dist/inspection/options.d.ts +5 -7
- package/dist/inspection/options.js +49 -22
- package/dist/inspection/types.d.ts +3 -2
- package/dist/migrations/history.d.ts +1 -1
- package/dist/migrations/history.js +3 -2
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +6 -0
- package/docs/adoption-guide.md +94 -153
- package/docs/agent-guide.md +93 -176
- package/docs/configuration.md +146 -0
- package/docs/connections.md +187 -0
- package/docs/human-guide.md +150 -179
- package/docs/migrations.md +140 -0
- package/docs/mutations.md +104 -67
- package/docs/query-checking.md +122 -120
- package/docs/query-inspection.md +83 -76
- package/docs/reference.md +234 -474
- package/docs/schema-types.md +266 -0
- package/docs/sql.md +212 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.3
|
|
4
|
+
|
|
5
|
+
- Add shared runtime/CLI configuration and offline `tenon init`. Restore `tenon generate`, share migration defaults, and require config for generation/checking/inspection.
|
|
6
|
+
- Remove `db.generateTypes()`. Add native baseline connection/schema defaults and fix inspection timeout overrides and migration-history lookup under custom search paths.
|
|
7
|
+
|
|
8
|
+
## 0.4.2
|
|
9
|
+
|
|
10
|
+
- Rewrite the README and consumer guides around a complete getting-started workflow and task-based navigation.
|
|
11
|
+
- Add guides for SQL queries, schema types and values, connections, and migrations, including joins, pagination, codec configuration, rollback, and migration recovery.
|
|
12
|
+
- Clarify query result modes, write guards, generated types, source checking, and their validation limits. This release changes documentation only; no application migration is required.
|
|
13
|
+
|
|
14
|
+
## 0.4.1
|
|
15
|
+
|
|
16
|
+
- Add `db.generateTypes({ out, namespace?, check?, onWarning? })`, using the instance's configured codecs.
|
|
17
|
+
- Remove `tenon generate`; run generation and freshness checks from an application script.
|
|
18
|
+
Use the configured database instance, set `check: true` for freshness checks, and close it when
|
|
19
|
+
the script finishes. See the [generation script example](docs/human-guide.md#generate-types).
|
|
20
|
+
|
|
3
21
|
## 0.4.0
|
|
4
22
|
|
|
5
23
|
### Breaking changes
|
|
@@ -9,7 +27,7 @@
|
|
|
9
27
|
Connections accept a PostgreSQL URI, pgclear `PoolOptions`, or a pgclear `Pool`; translate former
|
|
10
28
|
pg configuration and supply deployment settings explicitly.
|
|
11
29
|
PG environment defaults, pgpass/service files, and pg-specific URI options are not read. See
|
|
12
|
-
[
|
|
30
|
+
[connection configuration](docs/connections.md#configure-a-database-instance).
|
|
13
31
|
- `db.native` is now a pgclear `Pool`. Use `queryClassic(text, values?)` for finished SQL and
|
|
14
32
|
acquire/withSession for held sessions; native `query` accepts pgclear's own sql tag only. Prepared
|
|
15
33
|
statements, streams, and COPY use the public session APIs. Use pgclear `listen` for dedicated
|
|
@@ -21,7 +39,9 @@
|
|
|
21
39
|
output conversions are opt-in. Undefined, plain objects, and arrays reject; mutation-helper
|
|
22
40
|
undefined still omits. JSON encoding still rejects bigint values.
|
|
23
41
|
- Tenon owns schema generation and exports `Database[table]['row' | 'insert' | 'update']`.
|
|
24
|
-
Regenerate committed definitions
|
|
42
|
+
Regenerate committed definitions and review write representations. Replace `Rows[table]`,
|
|
43
|
+
`Rows.Insert[table]`, and `Rows.Update[table]` with `Database[table]['row']`,
|
|
44
|
+
`Database[table]['insert']`, and `Database[table]['update']`.
|
|
25
45
|
Catalog queries, schema summaries, defaults/domains/identities, and TypeScript rendering live in
|
|
26
46
|
Tenon; decoder representations come from pgclear. Import generation from Tenon instead of pgclear.
|
|
27
47
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
17
|
-
|
|
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,98 @@ try {
|
|
|
31
32
|
`,
|
|
32
33
|
{ returns: 'oneOrNone' },
|
|
33
34
|
);
|
|
34
|
-
// user
|
|
35
|
+
// user: { id: number; email: string } | undefined
|
|
35
36
|
} finally {
|
|
36
37
|
await db.close();
|
|
37
38
|
}
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 `tenon generate`.
|
|
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.
|
|
97
|
+
|
|
98
|
+
## Share configuration with the CLI
|
|
99
|
+
|
|
100
|
+
Run `tenon init` to create `tenon.config.ts`, put connection settings and codecs there, and use its
|
|
101
|
+
`connection` in `createTenon`.
|
|
102
|
+
Then run `tenon generate`, `tenon generate --check`, `tenon check`, and `tenon describe query.sql`
|
|
103
|
+
without custom scripts. Migration commands also use the shared connection and migration defaults.
|
|
104
|
+
See [shared configuration](docs/configuration.md) for the complete setup.
|
|
62
105
|
|
|
63
106
|
## Documentation
|
|
64
107
|
|
|
65
|
-
Start with
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
project can read the relevant release entries locally. Repository maintenance instructions are in
|
|
108
|
+
Start with [Getting started](docs/human-guide.md) for a complete table-to-query example. Then choose
|
|
109
|
+
the guide for your task:
|
|
110
|
+
|
|
111
|
+
| Guide | What it covers |
|
|
112
|
+
| ---------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
113
|
+
| [Shared configuration](docs/configuration.md) | Runtime/CLI connection settings, codecs, command defaults, and overrides |
|
|
114
|
+
| [Writing queries](docs/sql.md) | Parameters, reusable functions, joins, aggregates, pagination, and fragments |
|
|
115
|
+
| [Writing data](docs/mutations.md) | Inserts, patches, defaults, upserts, JSON, arrays, and batches |
|
|
116
|
+
| [Schema types and values](docs/schema-types.md) | Generation, read/write types, codecs, and domain validation |
|
|
117
|
+
| [Migrations](docs/migrations.md) | Creating, applying, inspecting, and rolling back SQL migrations |
|
|
118
|
+
| [Connections and driver access](docs/connections.md) | Pool configuration, shutdown, custom codecs, and native features |
|
|
119
|
+
| [Checking query types](docs/query-checking.md) | Source-aware checks and their supported SQL composition |
|
|
120
|
+
| [Inspecting and testing queries](docs/query-inspection.md) | PostgreSQL metadata and tests of real behavior |
|
|
121
|
+
| [Adopting Tenon](docs/adoption-guide.md) | Replacing an ORM and transferring migration ownership |
|
|
122
|
+
| [Agent guide](docs/agent-guide.md) | A short implementation workflow for an existing project |
|
|
123
|
+
| [API and CLI reference](docs/reference.md) | Signatures, options, errors, commands, and troubleshooting |
|
|
124
|
+
|
|
125
|
+
The README, guides, and [changelog](CHANGELOG.md) ship with the npm package. Read the documentation
|
|
126
|
+
for your installed version when upgrading. Library maintenance is covered in
|
|
85
127
|
[CONTRIBUTING.md](https://github.com/kequtech/tenon-orm/blob/main/CONTRIBUTING.md).
|
|
86
128
|
|
|
87
129
|
[ISC licensed](LICENSE). Maintained by [Kequtech](https://github.com/kequtech).
|
package/dist/check.js
CHANGED
|
@@ -17,7 +17,10 @@ export async function checkQueries(options) {
|
|
|
17
17
|
if (!['connection', 'target', 'project', 'session', 'timeouts', 'codecs', 'readTypes'].includes(name))
|
|
18
18
|
throw new Error(`Unknown check option: ${name}`);
|
|
19
19
|
}
|
|
20
|
-
const
|
|
20
|
+
const codecs = options.codecs ??
|
|
21
|
+
(typeof options.connection === 'object' ? options.connection?.codecs : undefined) ??
|
|
22
|
+
defaultCodecs;
|
|
23
|
+
const project = loadProject({ ...options, codecs });
|
|
21
24
|
const libraries = librarySymbols(project);
|
|
22
25
|
const diagnostics = [...project.diagnostics];
|
|
23
26
|
const queries = project.selected.flatMap((source) => new SourceAnalysis(project, libraries).discover(source));
|
|
@@ -52,7 +55,6 @@ export async function checkQueries(options) {
|
|
|
52
55
|
return { query, variants, diagnostics: analysis.issues };
|
|
53
56
|
});
|
|
54
57
|
const reports = [];
|
|
55
|
-
const codecs = options.codecs ?? defaultCodecs;
|
|
56
58
|
const readTypes = { ...options.readTypes };
|
|
57
59
|
for (const oid of Object.keys(readTypes)) {
|
|
58
60
|
if (codecs.typescript(Number(oid)) !== undefined)
|
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { defaultCodecs } from '@kequtech/pgclear';
|
|
3
6
|
import { adoptBaseline, captureBaseline } from './baseline.js';
|
|
7
|
+
import { describeFlags, inspectionSettings } from './inspection/cli.js';
|
|
8
|
+
import { loadConfig } from './config/load.js';
|
|
9
|
+
import { initConfig } from './config/init.js';
|
|
4
10
|
import { generateDefinitions } from './generate.js';
|
|
5
11
|
import { writeDefinitions } from './generation/output.js';
|
|
6
|
-
import {
|
|
12
|
+
import { validatePath } from './validation.js';
|
|
13
|
+
import { describeQuery } from './describe.js';
|
|
7
14
|
import { migrate, migrationStatus, rollback } from './migrations.js';
|
|
8
15
|
import { createMigration } from './migrations/files.js';
|
|
9
|
-
const help = `tenon
|
|
16
|
+
const help = `tenon init [--config path] (create config without a database)
|
|
17
|
+
tenon migrate|status|rollback [--directory migrations] [--namespace public]
|
|
10
18
|
tenon baseline capture|adopt [--directory migrations] [--namespace public]
|
|
11
|
-
baseline capture: [--schema name ...] (default: whole database schema)
|
|
19
|
+
baseline capture: [--schema name ...] [--connection native-uri] (default: whole database schema)
|
|
12
20
|
tenon new <name> [--directory migrations] (no database required)
|
|
13
|
-
tenon generate [--
|
|
21
|
+
tenon generate [--out path] [--namespace public] [--check] [--config path]
|
|
14
22
|
tenon describe <query.sql> [--role name] [--search-path schemas] [--time-zone zone]
|
|
15
23
|
tenon check [file.ts[:line]] [--project tsconfig.json] [--json]
|
|
24
|
+
generate/check/describe require tenon.config.ts, .mts, .js or .mjs, or --config path.
|
|
16
25
|
describe: [--connect-timeout-ms 3000] [--statement-timeout-ms 5000] [--lock-timeout-ms 1000]
|
|
17
26
|
describe emits JSON metadata; it does not execute the query or verify result annotations.
|
|
18
27
|
check compares source contracts; mismatches, unchecked sites and no sites fail. Unproven types are notes.
|
|
19
28
|
rollback: --steps 1
|
|
20
|
-
|
|
29
|
+
All commands accept --config path. Migration commands fall back to DATABASE_URL only without config.
|
|
21
30
|
`;
|
|
22
31
|
function warn(message) {
|
|
23
32
|
process.stderr.write(`Warning: ${message}\n`);
|
|
@@ -25,7 +34,14 @@ function warn(message) {
|
|
|
25
34
|
async function runBaseline(action, options) {
|
|
26
35
|
warn("Capture one shared baseline at the old ORM's final schema checkpoint. Each existing database must be at that checkpoint before adoption. Tenon does not compare schemas or remove the old ORM.");
|
|
27
36
|
if (action === 'capture') {
|
|
28
|
-
|
|
37
|
+
if (typeof options.connection !== 'string') {
|
|
38
|
+
throw new Error('Baseline capture needs a native libpq URI. Pass --connection or use a string config connection.');
|
|
39
|
+
}
|
|
40
|
+
const result = await captureBaseline({
|
|
41
|
+
...options,
|
|
42
|
+
connection: options.connection,
|
|
43
|
+
onWarning: warn,
|
|
44
|
+
});
|
|
29
45
|
process.stdout.write(`Captured ${result.upPath}. Source history and data were not changed. Commit this file and generated definitions.\n`);
|
|
30
46
|
}
|
|
31
47
|
else {
|
|
@@ -39,11 +55,13 @@ async function main() {
|
|
|
39
55
|
allowPositionals: true,
|
|
40
56
|
tokens: true,
|
|
41
57
|
options: {
|
|
42
|
-
directory: { type: 'string'
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
directory: { type: 'string' },
|
|
59
|
+
connection: { type: 'string' },
|
|
60
|
+
namespace: { type: 'string' },
|
|
61
|
+
config: { type: 'string' },
|
|
62
|
+
out: { type: 'string' },
|
|
46
63
|
check: { type: 'boolean', default: false },
|
|
64
|
+
schema: { type: 'string', multiple: true },
|
|
47
65
|
steps: { type: 'string', default: '1' },
|
|
48
66
|
help: { type: 'boolean', default: false },
|
|
49
67
|
project: { type: 'string' },
|
|
@@ -56,23 +74,29 @@ async function main() {
|
|
|
56
74
|
return;
|
|
57
75
|
}
|
|
58
76
|
const [command, argument] = positionals;
|
|
77
|
+
for (const name of ['out', 'directory', 'project']) {
|
|
78
|
+
if (values[name] !== undefined)
|
|
79
|
+
validatePath(values[name], `--${name}`);
|
|
80
|
+
}
|
|
59
81
|
// Validate explicitly supplied flags, not defaults. Silently ignoring --check
|
|
60
82
|
// on migrate, for example, would turn an intended dry run into a schema change.
|
|
61
83
|
const allowed = new Set(['help']);
|
|
62
|
-
if (
|
|
84
|
+
if (!['describe', 'check', 'generate', 'init'].includes(command ?? ''))
|
|
63
85
|
allowed.add('directory');
|
|
64
|
-
if (
|
|
86
|
+
if (!['new', 'describe', 'check', 'init'].includes(command ?? '')) {
|
|
65
87
|
allowed.add('namespace');
|
|
66
88
|
}
|
|
89
|
+
if (command === 'rollback') {
|
|
90
|
+
allowed.add('steps');
|
|
91
|
+
}
|
|
67
92
|
if (command === 'generate') {
|
|
68
93
|
allowed.add('out');
|
|
69
94
|
allowed.add('check');
|
|
70
95
|
}
|
|
71
|
-
|
|
72
|
-
allowed.add('steps');
|
|
73
|
-
}
|
|
96
|
+
allowed.add('config');
|
|
74
97
|
if (command === 'baseline' && argument === 'capture') {
|
|
75
98
|
allowed.add('schema');
|
|
99
|
+
allowed.add('connection');
|
|
76
100
|
}
|
|
77
101
|
if (command === 'check') {
|
|
78
102
|
allowed.add('project');
|
|
@@ -87,11 +111,22 @@ async function main() {
|
|
|
87
111
|
throw new Error(`--${token.name} is not supported by ${command ?? 'this command'}.`);
|
|
88
112
|
}
|
|
89
113
|
}
|
|
114
|
+
if (command === 'init') {
|
|
115
|
+
if (positionals.length !== 1)
|
|
116
|
+
throw new Error('Use init [--config path] to create configuration.');
|
|
117
|
+
const path = await initConfig(values.config);
|
|
118
|
+
process.stdout.write(`Created ${path}\nSet DATABASE_URL before running database commands.\n`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
90
121
|
if (command === 'new') {
|
|
91
122
|
if (positionals.length !== 2) {
|
|
92
123
|
throw new Error('Use new <name> to create a migration pair.');
|
|
93
124
|
}
|
|
94
|
-
const
|
|
125
|
+
const config = await loadConfig(values.config, false);
|
|
126
|
+
const files = await createMigration({
|
|
127
|
+
name: argument,
|
|
128
|
+
directory: values.directory ?? config?.migrations?.directory ?? 'migrations',
|
|
129
|
+
});
|
|
95
130
|
process.stdout.write(`Created ${files.upPath}\nCreated ${files.downPath}\n`);
|
|
96
131
|
return;
|
|
97
132
|
}
|
|
@@ -103,23 +138,41 @@ async function main() {
|
|
|
103
138
|
: positionals.length === 1 &&
|
|
104
139
|
['migrate', 'generate', 'status', 'rollback'].includes(command ?? '');
|
|
105
140
|
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.');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
throw new Error('Choose init, new, baseline capture|adopt, migrate, generate, describe <query.sql>, check [file.ts[:line]], status, or rollback. Use --help for options.');
|
|
142
|
+
}
|
|
143
|
+
if (command === 'generate' || command === 'check' || command === 'describe') {
|
|
144
|
+
const config = await loadConfig(values.config);
|
|
145
|
+
if (command === 'generate') {
|
|
146
|
+
const out = values.out === undefined ? config.generate?.out : resolve(values.out);
|
|
147
|
+
if (out === undefined)
|
|
148
|
+
throw new Error('Configure generate.out or pass --out.');
|
|
149
|
+
const namespace = values.namespace ?? config.generate?.namespace ?? config.migrations?.namespace;
|
|
150
|
+
const source = await generateDefinitions({
|
|
151
|
+
connection: config.connection,
|
|
152
|
+
onWarning: warn,
|
|
153
|
+
...(namespace !== undefined && { namespace }),
|
|
154
|
+
});
|
|
155
|
+
await writeDefinitions(source, out, values.check);
|
|
156
|
+
process.stdout.write(values.check ? 'Generated definitions are current.\n' : `Generated ${out}\n`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const flags = inspectionSettings(values);
|
|
160
|
+
const settings = {
|
|
161
|
+
connection: config.connection,
|
|
162
|
+
session: { ...config.session, ...flags.session },
|
|
163
|
+
timeouts: { ...config.timeouts, ...flags.timeouts },
|
|
164
|
+
};
|
|
165
|
+
if (command === 'describe') {
|
|
166
|
+
const result = await describeQuery({ ...settings, query: await readFile(argument, 'utf8') });
|
|
167
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
117
170
|
const { checkQueries } = await import('./check.js');
|
|
118
171
|
const report = await checkQueries({
|
|
119
|
-
|
|
172
|
+
...settings,
|
|
173
|
+
...config.check,
|
|
120
174
|
...(argument === undefined ? {} : { target: argument }),
|
|
121
175
|
...(values.project === undefined ? {} : { project: values.project }),
|
|
122
|
-
...inspectionSettings(values),
|
|
123
176
|
});
|
|
124
177
|
if (values.json)
|
|
125
178
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
@@ -132,24 +185,28 @@ async function main() {
|
|
|
132
185
|
process.exitCode = 1;
|
|
133
186
|
return;
|
|
134
187
|
}
|
|
135
|
-
const
|
|
188
|
+
const config = await loadConfig(values.config, false);
|
|
189
|
+
const capture = command === 'baseline' && argument === 'capture';
|
|
190
|
+
const connection = values.connection ??
|
|
191
|
+
(capture ? config?.baseline?.connection : undefined) ??
|
|
192
|
+
config?.connection ??
|
|
193
|
+
process.env.DATABASE_URL;
|
|
194
|
+
if (!connection)
|
|
195
|
+
throw new Error('DATABASE_URL is required.');
|
|
196
|
+
const options = {
|
|
197
|
+
connection: typeof connection === 'string' ? connection : { ...connection, codecs: defaultCodecs },
|
|
198
|
+
directory: values.directory ?? config?.migrations?.directory ?? 'migrations',
|
|
199
|
+
namespace: values.namespace ?? config?.migrations?.namespace ?? 'public',
|
|
200
|
+
};
|
|
136
201
|
if (baseline) {
|
|
202
|
+
const schemas = values.schema ?? (capture ? config?.baseline?.schemas : undefined);
|
|
137
203
|
await runBaseline(argument, {
|
|
138
204
|
...options,
|
|
139
|
-
...(
|
|
205
|
+
...(schemas !== undefined && { schemas }),
|
|
140
206
|
});
|
|
141
207
|
return;
|
|
142
208
|
}
|
|
143
209
|
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
210
|
case 'status':
|
|
154
211
|
process.stdout.write(`${JSON.stringify(await migrationStatus(options), null, 2)}\n`);
|
|
155
212
|
break;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, extname, resolve } from 'node:path';
|
|
3
|
+
import { validatePath } from '../validation.js';
|
|
4
|
+
import { configNames, findConfig } from './load.js';
|
|
5
|
+
/** Scaffold one value-only config, without importing existing application code or opening a pool. */
|
|
6
|
+
export async function initConfig(filename) {
|
|
7
|
+
if (filename !== undefined)
|
|
8
|
+
validatePath(filename, 'Config filename');
|
|
9
|
+
const output = resolve(filename ?? 'tenon.config.ts');
|
|
10
|
+
const extension = extname(output);
|
|
11
|
+
if (!['.ts', '.mts', '.js', '.mjs'].includes(extension)) {
|
|
12
|
+
throw new Error('Config filename must end in .ts, .mts, .js, or .mjs.');
|
|
13
|
+
}
|
|
14
|
+
if (filename === undefined || configNames.includes(basename(output))) {
|
|
15
|
+
const existing = await findConfig(dirname(output));
|
|
16
|
+
if (existing && (filename === undefined || dirname(existing) === dirname(output))) {
|
|
17
|
+
throw new Error(`Tenon configuration already exists: ${existing}. Use or edit that file.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const typed = extension === '.ts' || extension === '.mts';
|
|
21
|
+
const source = `${typed ? "import type { TenonConfig } from '@kequtech/tenon-orm';\n\n" : ''}const connectionString = process.env.DATABASE_URL;
|
|
22
|
+
if (!connectionString) throw new Error('DATABASE_URL is required');
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
connection: { connectionString },
|
|
26
|
+
migrations: { directory: 'migrations', namespace: 'public' },
|
|
27
|
+
generate: { out: 'src/generated-schema.d.ts' },
|
|
28
|
+
check: { project: 'tsconfig.json' },
|
|
29
|
+
}${typed ? ' satisfies TenonConfig' : ''};
|
|
30
|
+
`;
|
|
31
|
+
await mkdir(dirname(output), { recursive: true });
|
|
32
|
+
await writeFile(output, source, { encoding: 'utf8', flag: 'wx' });
|
|
33
|
+
return output;
|
|
34
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TenonConfig } from '../config.js';
|
|
2
|
+
export declare const configNames: string[];
|
|
3
|
+
/** Find configuration without importing it, including for offline initialization. */
|
|
4
|
+
export declare function findConfig(directory?: string): Promise<string | undefined>;
|
|
5
|
+
/** Import only the selected trusted configuration module; never discover application entry points. */
|
|
6
|
+
export declare function loadConfig(filename?: string): Promise<TenonConfig>;
|
|
7
|
+
export declare function loadConfig(filename: string | undefined, required: false): Promise<TenonConfig | undefined>;
|