@kequtech/tenon-orm 0.1.1 → 0.1.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/README.md +12 -10
- package/dist/cli.js +4 -2
- package/dist/generate.js +2 -2
- package/dist/generation/catalog.d.ts +3 -0
- package/dist/generation/catalog.js +5 -2
- package/dist/generation/diff.d.ts +2 -0
- package/dist/generation/diff.js +30 -0
- package/dist/generation/table-types.d.ts +2 -0
- package/dist/generation/table-types.js +146 -0
- package/dist/mutations.d.ts +1 -1
- package/dist/query.d.ts +2 -2
- package/dist/query.js +1 -1
- package/dist/safeql/composition.js +9 -0
- package/dist/safeql/eslint.js +16 -11
- package/docs/adoption-guide.md +97 -4
- package/docs/agent-guide.md +38 -56
- package/docs/human-guide.md +37 -37
- package/docs/mutations.md +49 -12
- package/docs/reference.md +128 -94
- package/package.json +1 -1
- package/dist/generation/row-types.d.ts +0 -2
- package/dist/generation/row-types.js +0 -115
package/docs/reference.md
CHANGED
|
@@ -86,22 +86,22 @@ const query = sql<{ id: number }>`SELECT id FROM public."User"
|
|
|
86
86
|
NUL, and fit PostgreSQL's 63-byte identifier limit. Choose names from application-approved
|
|
87
87
|
alternatives; quoting does not authorize access. Ordinary strings such as `'DESC'` are bound values,
|
|
88
88
|
not SQL syntax. For ordering, use complete query alternatives or application-selected fragments
|
|
89
|
-
containing `ASC` or `DESC
|
|
89
|
+
containing `ASC` or `DESC`.
|
|
90
90
|
|
|
91
91
|
`unsafeSql<Row>(text, values)` accepts externally authored, trusted SQL and an explicit values
|
|
92
|
-
array, including `[]` when empty.
|
|
93
|
-
|
|
92
|
+
array, including `[]` when empty. Never concatenate request input into its text.
|
|
93
|
+
Full `sql` or `unsafeSql` queries cannot be inserted into other queries; use
|
|
94
94
|
fragments for partial SQL.
|
|
95
95
|
|
|
96
96
|
## SQL fragments and joins
|
|
97
97
|
|
|
98
98
|
`sql.fragment` creates an immutable piece of SQL with bound values. It has no row type and cannot
|
|
99
|
-
execute alone. Insert it into a complete `sql<Row>` query.
|
|
100
|
-
|
|
101
|
-
directly in the query.
|
|
99
|
+
execute alone. Insert it into a complete `sql<Row>` query. Use fragments to share predicates or
|
|
100
|
+
other SQL expressions between queries. Blank fragments throw.
|
|
102
101
|
|
|
103
|
-
`sql.join(fragments, separator)` combines
|
|
104
|
-
|
|
102
|
+
`sql.join(fragments, separator)` combines fragments into a new fragment. Use it when the number of
|
|
103
|
+
parts varies: optional filters, a condition for each search term, or a list of SQL expressions.
|
|
104
|
+
Write fixed structure directly in SQL. Supported separators are:
|
|
105
105
|
|
|
106
106
|
| Argument | Inserted SQL | Typical use |
|
|
107
107
|
| -------- | ------------ | -------------------------- |
|
|
@@ -109,35 +109,22 @@ Separators are exactly:
|
|
|
109
109
|
| `'OR'` | `OR` | Any condition may hold |
|
|
110
110
|
| `','` | `, ` | Projections or assignments |
|
|
111
111
|
|
|
112
|
-
No default, arbitrary string, or fragment separator is accepted. Runtime validation also protects
|
|
113
|
-
JavaScript callers. A single item is returned as a new fragment without extra separators. Empty
|
|
114
|
-
lists and blank fragments throw; choose empty-list behavior explicitly in application code.
|
|
115
|
-
|
|
116
|
-
Joins do not add parentheses or alter SQL precedence. Group alternatives before combining them with
|
|
117
|
-
other conditions:
|
|
118
|
-
|
|
119
112
|
```ts
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
'AND',
|
|
127
|
-
);
|
|
128
|
-
const query = sql<{ id: number }>`SELECT id FROM public."User" WHERE ${conditions}`;
|
|
113
|
+
const conditions = [sql.fragment`active = ${true}`];
|
|
114
|
+
if (email !== undefined) conditions.push(sql.fragment`email = ${email}`);
|
|
115
|
+
if (afterId !== undefined) conditions.push(sql.fragment`id > ${afterId}`);
|
|
116
|
+
|
|
117
|
+
const query = sql<{ id: number }>`SELECT id FROM public."User"
|
|
118
|
+
WHERE ${sql.join(conditions, 'AND')} ORDER BY id`;
|
|
129
119
|
```
|
|
130
120
|
|
|
121
|
+
The list must be nonempty; the example keeps the active condition even when both optional filters
|
|
122
|
+
are absent. Joins insert separators without adding parentheses or changing SQL precedence. Group
|
|
123
|
+
mixed AND/OR expressions with parentheses, as in ordinary SQL.
|
|
124
|
+
|
|
131
125
|
Each fragment use receives correctly ordered parameters, including repeated and nested uses. SQL
|
|
132
126
|
strings and comments containing literal `$1` text are left intact.
|
|
133
127
|
|
|
134
|
-
Static checking supports constant fragments, literal join arrays, literal or const-string
|
|
135
|
-
separators, and [official mutation helpers](#object-based-mutation-fragments). Runtime-built lists
|
|
136
|
-
and selected fragments execute safely with respect to parameter binding, but the checker cannot
|
|
137
|
-
verify their SQL or result types. Prefer fixed SQL when it fits.
|
|
138
|
-
Otherwise keep an explicit result annotation, use a narrow explained lint suppression, and test
|
|
139
|
-
every variant.
|
|
140
|
-
|
|
141
128
|
## Object-based mutation fragments
|
|
142
129
|
|
|
143
130
|
`sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep the
|
|
@@ -188,8 +175,7 @@ intended update consists only of those expressions.
|
|
|
188
175
|
An optional nonempty column list selects and orders fields. It does not validate that the input
|
|
189
176
|
object has no additional keys. Unlisted fields are ignored; missing insert cells use `DEFAULT`,
|
|
190
177
|
while missing update fields are omitted. Duplicate or invalid column
|
|
191
|
-
names throw. TypeScript checks selected names against the input's known keys
|
|
192
|
-
way to make runtime objects and batches available to static SQL checking:
|
|
178
|
+
names throw. TypeScript checks selected names against the input's known keys:
|
|
193
179
|
|
|
194
180
|
```ts
|
|
195
181
|
const rows: { name: string; bio?: string | null }[] = buildRows();
|
|
@@ -199,10 +185,10 @@ const patch: { name?: string; bio?: string | null } = readPatch();
|
|
|
199
185
|
const update = sql`UPDATE profiles SET ${sql.assignments(patch, ['name', 'bio'])} WHERE id = ${id}`;
|
|
200
186
|
```
|
|
201
187
|
|
|
202
|
-
The helpers have no schema metadata.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
188
|
+
The helpers have no runtime schema metadata. Use [generated input types](#insert-and-update-inputs)
|
|
189
|
+
or application-owned contracts to check JavaScript values; PostgreSQL enforces identity,
|
|
190
|
+
generation, required-column, and constraint rules. Retain application validation.
|
|
191
|
+
To reset an update field to its default, write `column = DEFAULT` in the
|
|
206
192
|
surrounding SQL. Put arithmetic and `excluded.column` expressions there too; these helpers accept
|
|
207
193
|
values only, with no expression/default sentinel.
|
|
208
194
|
|
|
@@ -211,24 +197,8 @@ Tenon rejects larger queries and does not split them automatically. Choose batch
|
|
|
211
197
|
transaction boundaries in the caller. Helper validation errors identify invalid rows/columns
|
|
212
198
|
without printing cell values.
|
|
213
199
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
SafeQL recognizes these official helpers directly through their Tenon function declarations,
|
|
217
|
-
including aliases. It never analyzes their implementation or executes application data builders.
|
|
218
|
-
Without an explicit column list, checking accepts inline object literals and inline insert arrays
|
|
219
|
-
of object literals. Keys must be literal names; object spreads, computed keys, methods, accessors,
|
|
220
|
-
and objects stored in variables require an explicit inline column list. Existing constant string
|
|
221
|
-
resolution works for entries of that list; column arrays stored in variables are unsupported.
|
|
222
|
-
|
|
223
|
-
For runtime inputs with explicit columns, the checker analyzes a representative nonempty insert
|
|
224
|
-
row or assignment list containing **all selected columns**. This checks column references, SQL
|
|
225
|
-
composition, conflict clauses, and `RETURNING` annotations. Runtime omission/default decisions and
|
|
226
|
-
batch length do not require interpreting JavaScript. The check is conservative: an optional
|
|
227
|
-
selected column is checked even when a particular runtime call omits it. It does not establish
|
|
228
|
-
nonempty input, valid runtime values, successful constraints, or every default's behavior; runtime
|
|
229
|
-
validation and database tests cover those contracts. Unknown inputs without explicit columns
|
|
230
|
-
produce `tenon/check-composition` diagnostics. See the [mutation cookbook](mutations.md) for
|
|
231
|
-
empty patches, conditional upserts, arrays, and batching.
|
|
200
|
+
See the [mutation cookbook](mutations.md) for common patterns and
|
|
201
|
+
[checking mutation fragments](#checking-mutation-fragments) for optional SQL checking.
|
|
232
202
|
|
|
233
203
|
## Transactions, guarded writes, and errors
|
|
234
204
|
|
|
@@ -277,42 +247,85 @@ transaction has committed.
|
|
|
277
247
|
|
|
278
248
|
`generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
|
|
279
249
|
connected schema. It does not need migration files or history. Table type generation is optional.
|
|
280
|
-
SafeQL reads the database directly and does not require these generated files; they
|
|
281
|
-
|
|
282
|
-
file without rewriting it:
|
|
250
|
+
SafeQL reads the database directly and does not require these generated files; they provide
|
|
251
|
+
reusable row and mutation input types. The CLI writes that source atomically. This command checks
|
|
252
|
+
the existing file without rewriting it:
|
|
283
253
|
|
|
284
254
|
```sh
|
|
285
|
-
npx tenon generate --out src/generated-schema.ts --check
|
|
255
|
+
npx tenon generate --out src/generated-schema.d.ts --check
|
|
286
256
|
```
|
|
287
257
|
|
|
288
|
-
`--check`
|
|
258
|
+
`--check` reports a missing file or shows a short text diff when the saved output differs from the
|
|
259
|
+
current database. It never rewrites the file. Pending migration files
|
|
289
260
|
are a separate concern. The representation includes table columns, constraints, indexes, and
|
|
290
261
|
referenced domains, not every PostgreSQL object.
|
|
291
262
|
|
|
292
|
-
Generation reads a consistent snapshot of ordinary tables and partitioned roots.
|
|
293
|
-
|
|
294
|
-
`
|
|
295
|
-
|
|
296
|
-
use query-specific types. A missing namespace
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
|
|
|
306
|
-
|
|
|
307
|
-
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
263
|
+
Generation reads a consistent snapshot of ordinary tables and partitioned roots. `Rows['User']`
|
|
264
|
+
describes one returned row; the generated `User` alias means the same thing. Use `sql<User>` for a
|
|
265
|
+
full row or `Pick<User, 'id' | 'email'>` for a projection. Table keys preserve exact SQL names;
|
|
266
|
+
`type AuditRow = Rows['audit-log']` works for names that cannot be exported as identifiers.
|
|
267
|
+
Views, foreign tables, and attached partitions use query-specific types. A missing namespace
|
|
268
|
+
fails; an existing empty schema is valid.
|
|
269
|
+
|
|
270
|
+
| PostgreSQL type | Returned value | Generated write input |
|
|
271
|
+
| ---------------------------------------------- | --------------------------------------------- | ------------------------------------- |
|
|
272
|
+
| smallint, integer, oid, real, double precision | `number` | `number` |
|
|
273
|
+
| bigint | `string` | `string \| bigint` |
|
|
274
|
+
| numeric | `string` | `string \| number` |
|
|
275
|
+
| boolean | `boolean` | `boolean` |
|
|
276
|
+
| text, name, varchar, char, uuid, time, timetz | `string` | `string` |
|
|
277
|
+
| date, timestamp, timestamptz | `Date \| number` (numbers include infinities) | `Date \| string` |
|
|
278
|
+
| enum | String literal union | Same union; `never` for an empty enum |
|
|
279
|
+
| json, jsonb | `unknown` | Serialized JSON text (`string`) |
|
|
280
|
+
| bytea | `Buffer` | `Buffer` |
|
|
281
|
+
|
|
282
|
+
For returned rows, nullable columns add null. Domains use verified underlying mappings and the
|
|
283
|
+
column's own nullability. Unverified types warn and become `unknown`. Verified arrays use
|
|
311
284
|
`PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
|
|
312
285
|
contain numbers and can lose precision; cast to text[] for exact values. Enum arrays and arrays of
|
|
313
286
|
domains are unverified. Finite dates and timestamps without time zone use Node's local timezone.
|
|
314
287
|
Custom pg parsers need matching application types.
|
|
315
288
|
|
|
289
|
+
### Insert and update inputs
|
|
290
|
+
|
|
291
|
+
The same file provides `Rows.Insert['table']` and `Rows.Update['table']` for bound write inputs.
|
|
292
|
+
These are ordinary TypeScript types. `--out` accepts `.ts` or `.d.ts`; the examples use `.d.ts`
|
|
293
|
+
because the file contains only declarations.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import type { Rows } from './generated-schema.js';
|
|
297
|
+
|
|
298
|
+
type NewProfile = Rows.Insert['profiles'];
|
|
299
|
+
type ProfilePatch = Pick<Rows.Update['profiles'], 'name' | 'bio'>;
|
|
300
|
+
|
|
301
|
+
const profile: NewProfile = { name: 'Ada' };
|
|
302
|
+
const patch: ProfilePatch = { name: undefined, bio: null };
|
|
303
|
+
sql.insertValues(profile, ['name', 'bio', 'score']);
|
|
304
|
+
sql.assignments(patch, ['name', 'bio']);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Insert fields are required when nonnullable and without a default. Other writable insert fields
|
|
308
|
+
are optional; every writable update field is optional. `undefined` requests a default on insert
|
|
309
|
+
and omits an assignment on update. Explicit `null` is accepted only when the column and its domain
|
|
310
|
+
chain permit it. Domain defaults count; column defaults take precedence.
|
|
311
|
+
|
|
312
|
+
Computed columns and `GENERATED ALWAYS` identity columns cannot receive values. Serial and
|
|
313
|
+
`GENERATED BY DEFAULT` identity columns are optional on insert and writable on update. Other
|
|
314
|
+
primary keys remain writable. Use `Pick` or `Omit` for application-specific restrictions and keep
|
|
315
|
+
expressions, `DEFAULT`, and special identity overrides in SQL.
|
|
316
|
+
|
|
317
|
+
Write representations in the table above also apply to verified array elements. Serialize JSON
|
|
318
|
+
with `JSON.stringify`, including JSON arrays; a JSON string containing `null` differs from SQL null.
|
|
319
|
+
Use timestamp strings for infinities. Unverified non-null write values use `never` and warn;
|
|
320
|
+
nullable fields still accept null. Supply an application-owned replacement when needed, for example
|
|
321
|
+
`Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for a reviewed interval-text input.
|
|
322
|
+
|
|
323
|
+
Apply these types through annotations, `satisfies`, or helper generic arguments such as
|
|
324
|
+
`sql.insertValues<NewProfile>(input)`. They check shapes and representations, not authorization,
|
|
325
|
+
constraints, JSON contents, or nonempty inputs. Optional fields accept explicit `undefined`,
|
|
326
|
+
including with `exactOptionalPropertyTypes`. Selected columns can discard required fields; see
|
|
327
|
+
the [cookbook example](mutations.md#input-types-sql-checking-and-runtime-validation).
|
|
328
|
+
|
|
316
329
|
## SQL checking with SafeQL
|
|
317
330
|
|
|
318
331
|
SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
|
|
@@ -341,11 +354,8 @@ existing TypeScript installation:
|
|
|
341
354
|
npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
|
|
342
355
|
```
|
|
343
356
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
`typescript-eslint` is an application-owned parser dependency, not a Tenon peer. Your ESLint
|
|
347
|
-
configuration imports SafeQL and the parser explicitly. Use a TypeScript version supported by that
|
|
348
|
-
lint toolchain. The `pg` driver remains Tenon's required runtime peer.
|
|
357
|
+
These are development dependencies. Reuse your existing type-aware parser configuration and a
|
|
358
|
+
TypeScript version supported by the lint toolchain. Tenon's database runtime does not load them.
|
|
349
359
|
|
|
350
360
|
For a project with `src/**/*.ts` and a `tsconfig.json`:
|
|
351
361
|
|
|
@@ -382,9 +392,6 @@ configuration. `projectService` uses the nearest TypeScript project for each fil
|
|
|
382
392
|
`DATABASE_URL` through your application's environment setup and point it at a development/test
|
|
383
393
|
database with migrations applied. This example does not load `.env` files.
|
|
384
394
|
|
|
385
|
-
The `.mjs` example avoids a current declaration mismatch between SafeQL's configuration helper and
|
|
386
|
-
ESLint's `defineConfig`. This affects configuration typechecking, not query result types.
|
|
387
|
-
|
|
388
395
|
`targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
|
|
389
396
|
single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
|
|
390
397
|
that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
|
|
@@ -404,6 +411,9 @@ They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
|
|
|
404
411
|
|
|
405
412
|
### Generate and check annotations
|
|
406
413
|
|
|
414
|
+
For an existing persistence layer, start with the [incremental audit workflow](adoption-guide.md#adopt-sql-checking-incrementally).
|
|
415
|
+
It covers a separate SQL lint command, fix previews, and application refinements.
|
|
416
|
+
|
|
407
417
|
Write a query without a result annotation:
|
|
408
418
|
|
|
409
419
|
```ts
|
|
@@ -465,16 +475,14 @@ file group, each extending its own `safeql.configs.connections({ ... })`. Use no
|
|
|
465
475
|
`files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
|
|
466
476
|
coexist; add Tenon's adapter only to connections checking Tenon queries.
|
|
467
477
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
when the connection's `search_path` selects the correct table at execution. Explicit schema names
|
|
471
|
-
avoid that ambiguity.
|
|
478
|
+
Qualify same-named tables with their schemas, for example `SELECT id FROM billing.accounts`, so
|
|
479
|
+
the intended relation is explicit in both checking and execution.
|
|
472
480
|
|
|
473
481
|
### Supported SQL and limitations
|
|
474
482
|
|
|
475
483
|
Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
|
|
476
484
|
aliases, namespace imports, and object properties that retain its TypeScript type. It checks
|
|
477
|
-
constant fragments and `sql.join` with nonempty
|
|
485
|
+
constant fragments and `sql.join` with inline nonempty array literals, including nested fragments.
|
|
478
486
|
Separators and `identifier()` names must resolve to literal strings or const string initializers.
|
|
479
487
|
Values use the same parameter placeholders in static checking and execution, inside or outside
|
|
480
488
|
fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
|
|
@@ -484,10 +492,17 @@ The checker follows const fragment initializers, including imported constants wi
|
|
|
484
492
|
source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
|
|
485
493
|
JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
|
|
486
494
|
unsupported even when a particular helper appears simple or a condition is a literal. Mutable
|
|
487
|
-
references
|
|
495
|
+
fragment references and dynamic identifiers are also unsupported. Arrays stored in variables or
|
|
496
|
+
assembled in loops are valid `sql.join` inputs at runtime but are not analyzed. Ordinary value
|
|
488
497
|
expressions remain bound parameters; calls that produce data for an official mutation helper can
|
|
489
498
|
use its explicit column-list contract.
|
|
490
499
|
|
|
500
|
+
`tenon/check-composition` points to the first unsupported interpolation in each query and explains
|
|
501
|
+
why it cannot be checked. For example, opaque mutation inputs need an inline column list, while
|
|
502
|
+
fragment-returning functions remain outside static analysis. Errors reached through shared constants
|
|
503
|
+
are reported at the consuming query. ESLint suppressions must cover the reported expression's line;
|
|
504
|
+
a scoped disable/enable can cover one multiline query.
|
|
505
|
+
|
|
491
506
|
Reusable authorization and other application policy can still live in fragment-returning functions
|
|
492
507
|
at runtime. Keep that policy centralized and cover its behavior with database tests. Suppress
|
|
493
508
|
`tenon/check-composition` only on the particular query, with a reason, when its variants are tested.
|
|
@@ -537,6 +552,25 @@ tests; a checker cannot infer your parser policy. Column keys are `Table.column`
|
|
|
537
552
|
names across schemas need care. Type overrides merge with the adapter's defaults. `unsafeSql` is
|
|
538
553
|
outside static checking, and arbitrary JSON still needs application validation.
|
|
539
554
|
|
|
555
|
+
### Checking mutation fragments
|
|
556
|
+
|
|
557
|
+
SafeQL recognizes these official helpers directly through their Tenon function declarations,
|
|
558
|
+
including aliases. It never analyzes their implementation or executes application data builders.
|
|
559
|
+
Without an explicit column list, checking accepts inline object literals and inline insert arrays
|
|
560
|
+
of object literals. Keys must be literal names; object spreads, computed keys, methods, accessors,
|
|
561
|
+
and objects stored in variables require an explicit inline column list. Existing constant string
|
|
562
|
+
resolution works for entries of that list; column arrays stored in variables are unsupported.
|
|
563
|
+
|
|
564
|
+
For runtime inputs with explicit columns, the checker analyzes a representative nonempty insert
|
|
565
|
+
row or assignment list containing **all selected columns**. This checks column references, SQL
|
|
566
|
+
composition, conflict clauses, and `RETURNING` annotations. Runtime omission/default decisions and
|
|
567
|
+
batch length do not require interpreting JavaScript. The check is conservative: an optional
|
|
568
|
+
selected column is checked even when a particular runtime call omits it. It does not establish
|
|
569
|
+
nonempty input, valid runtime values, successful constraints, or every default's behavior; runtime
|
|
570
|
+
validation and database tests cover those contracts. Unknown inputs without explicit columns
|
|
571
|
+
produce `tenon/check-composition` diagnostics. See the [mutation cookbook](mutations.md) for
|
|
572
|
+
empty patches, conditional upserts, arrays, and batching.
|
|
573
|
+
|
|
540
574
|
## Command reference
|
|
541
575
|
|
|
542
576
|
All database commands use `DATABASE_URL`; environment files are not loaded automatically. For
|
|
@@ -548,7 +582,7 @@ tenon new <name> [--directory migrations]
|
|
|
548
582
|
tenon migrate [--directory migrations] [--namespace public]
|
|
549
583
|
tenon status [--directory migrations] [--namespace public]
|
|
550
584
|
tenon rollback [--directory migrations] [--namespace public] [--steps 1]
|
|
551
|
-
tenon generate [--namespace public] [--out
|
|
585
|
+
tenon generate [--namespace public] [--out path] [--check]
|
|
552
586
|
tenon baseline capture|adopt [--directory migrations] [--namespace public]
|
|
553
587
|
```
|
|
554
588
|
|
package/package.json
CHANGED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// Every generated table remains accessible through Rows["exact SQL name"].
|
|
2
|
-
const reservedTypeNames = new Set(`
|
|
3
|
-
as await break case catch class const continue debugger default delete do else enum
|
|
4
|
-
export extends false finally for function if implements import in instanceof
|
|
5
|
-
interface let new null package private protected public return static super
|
|
6
|
-
switch this throw true try typeof var void while with yield any bigint boolean
|
|
7
|
-
intrinsic never number object string symbol undefined unknown
|
|
8
|
-
Rows globalThis PgArray
|
|
9
|
-
`
|
|
10
|
-
.trim()
|
|
11
|
-
.split(/\s+/));
|
|
12
|
-
function canExportRowName(name) {
|
|
13
|
-
return (/^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u.test(name) && !reservedTypeNames.has(name));
|
|
14
|
-
}
|
|
15
|
-
// Exact built-in OIDs with verified default pg text parsers. Array parsers do
|
|
16
|
-
// not necessarily use their scalar type's representation (notably numeric[]).
|
|
17
|
-
const arrayTypes = {
|
|
18
|
-
'1000': 'boolean',
|
|
19
|
-
'1001': 'import("node:buffer").Buffer',
|
|
20
|
-
'1005': 'number',
|
|
21
|
-
'1007': 'number',
|
|
22
|
-
'1021': 'number',
|
|
23
|
-
'1022': 'number',
|
|
24
|
-
'1028': 'number',
|
|
25
|
-
'1016': 'string',
|
|
26
|
-
'1231': 'number',
|
|
27
|
-
'1009': 'string',
|
|
28
|
-
'1014': 'string',
|
|
29
|
-
'1015': 'string',
|
|
30
|
-
'1183': 'string',
|
|
31
|
-
'1270': 'string',
|
|
32
|
-
'2951': 'string',
|
|
33
|
-
'1182': 'globalThis.Date | number',
|
|
34
|
-
'1115': 'globalThis.Date | number',
|
|
35
|
-
'1185': 'globalThis.Date | number',
|
|
36
|
-
'199': 'unknown',
|
|
37
|
-
'3807': 'unknown',
|
|
38
|
-
};
|
|
39
|
-
const scalarTypes = {
|
|
40
|
-
int2: 'number',
|
|
41
|
-
int4: 'number',
|
|
42
|
-
oid: 'number',
|
|
43
|
-
float4: 'number',
|
|
44
|
-
float8: 'number',
|
|
45
|
-
bool: 'boolean',
|
|
46
|
-
text: 'string',
|
|
47
|
-
varchar: 'string',
|
|
48
|
-
bpchar: 'string',
|
|
49
|
-
time: 'string',
|
|
50
|
-
timetz: 'string',
|
|
51
|
-
uuid: 'string',
|
|
52
|
-
// pg returns +/-Infinity as numbers. TypeScript has no infinity literal type.
|
|
53
|
-
date: 'globalThis.Date | number',
|
|
54
|
-
timestamp: 'globalThis.Date | number',
|
|
55
|
-
timestamptz: 'globalThis.Date | number',
|
|
56
|
-
int8: 'string',
|
|
57
|
-
numeric: 'string',
|
|
58
|
-
json: 'unknown',
|
|
59
|
-
jsonb: 'unknown',
|
|
60
|
-
bytea: 'import("node:buffer").Buffer',
|
|
61
|
-
};
|
|
62
|
-
function isSupportedArray(value) {
|
|
63
|
-
return Object.hasOwn(arrayTypes, value.oid);
|
|
64
|
-
}
|
|
65
|
-
function columnType(value, column, onWarning) {
|
|
66
|
-
let scalar;
|
|
67
|
-
if (isSupportedArray(value)) {
|
|
68
|
-
scalar = `PgArray<${arrayTypes[value.oid]}>`;
|
|
69
|
-
}
|
|
70
|
-
else if (value.typeKind === 'e') {
|
|
71
|
-
scalar = value.choices?.map((choice) => JSON.stringify(choice)).join(' | ') || 'never';
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
const type = value.typeSchema === 'pg_catalog' && Object.hasOwn(scalarTypes, value.type)
|
|
75
|
-
? scalarTypes[value.type]
|
|
76
|
-
: undefined;
|
|
77
|
-
if (type) {
|
|
78
|
-
scalar = type;
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
const underlying = value.declaredOid === value.oid
|
|
82
|
-
? ''
|
|
83
|
-
: ` with underlying type ${JSON.stringify(value.typeSchema)}.${JSON.stringify(value.type)} (OID ${value.oid})`;
|
|
84
|
-
onWarning(`Column ${column} uses PostgreSQL type ${JSON.stringify(value.declaredSchema)}.${JSON.stringify(value.declaredType)} (OID ${value.declaredOid})${underlying} without a verified pg mapping; generated as unknown.`);
|
|
85
|
-
scalar = 'unknown';
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// unknown already includes null, including for nullable JSON and unmapped types.
|
|
89
|
-
return `${scalar}${value.nullable && scalar !== 'unknown' ? ' | null' : ''}`;
|
|
90
|
-
}
|
|
91
|
-
export function renderRowTypes(tables, namespace, onWarning) {
|
|
92
|
-
const rows = tables.map((table) => {
|
|
93
|
-
const tableName = JSON.stringify(table.name);
|
|
94
|
-
const columns = table.columns.map((column) => {
|
|
95
|
-
const columnName = JSON.stringify(column.name);
|
|
96
|
-
const path = `${JSON.stringify(namespace)}.${tableName}.${columnName}`;
|
|
97
|
-
return ` ${columnName}: ${columnType(column, path, onWarning)};`;
|
|
98
|
-
});
|
|
99
|
-
return ` ${tableName}: {
|
|
100
|
-
${columns.join('\n')}
|
|
101
|
-
};`;
|
|
102
|
-
});
|
|
103
|
-
const aliases = tables
|
|
104
|
-
.filter((table) => canExportRowName(table.name))
|
|
105
|
-
.map((table) => `export type ${table.name} = Rows[${JSON.stringify(table.name)}];`);
|
|
106
|
-
const arrays = tables.some((table) => table.columns.some(isSupportedArray))
|
|
107
|
-
? 'export type PgArray<T> = (T | null | PgArray<T>)[];\n\n'
|
|
108
|
-
: '';
|
|
109
|
-
return `${arrays}// Full database rows. Compose query types with Pick, indexed access, and intersections.
|
|
110
|
-
export type Rows = {
|
|
111
|
-
${rows.join('\n')}
|
|
112
|
-
};
|
|
113
|
-
${aliases.join('\n')}
|
|
114
|
-
`;
|
|
115
|
-
}
|