@kequtech/tenon-orm 0.1.0 → 0.1.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/README.md +17 -9
- package/dist/generate.js +2 -2
- package/dist/generation/catalog.d.ts +3 -0
- package/dist/generation/catalog.js +5 -2
- package/dist/generation/table-types.d.ts +2 -0
- package/dist/generation/table-types.js +146 -0
- package/dist/mutations.d.ts +11 -0
- package/dist/mutations.js +87 -0
- package/dist/query.d.ts +7 -0
- package/dist/query.js +22 -3
- package/dist/safeql/composition.d.ts +1 -4
- package/dist/safeql/composition.js +19 -45
- package/dist/safeql/eslint.js +16 -11
- package/dist/safeql/mutations.d.ts +3 -0
- package/dist/safeql/mutations.js +74 -0
- package/dist/safeql/static.d.ts +10 -0
- package/dist/safeql/static.js +59 -0
- package/docs/adoption-guide.md +97 -4
- package/docs/agent-guide.md +39 -36
- package/docs/human-guide.md +33 -10
- package/docs/mutations.md +157 -0
- package/docs/reference.md +206 -51
- 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
|
@@ -131,12 +131,105 @@ const query = sql<{ id: number }>`SELECT id FROM public."User" WHERE ${condition
|
|
|
131
131
|
Each fragment use receives correctly ordered parameters, including repeated and nested uses. SQL
|
|
132
132
|
strings and comments containing literal `$1` text are left intact.
|
|
133
133
|
|
|
134
|
-
Static checking supports constant fragments, literal join arrays,
|
|
135
|
-
separators. Runtime-built lists
|
|
136
|
-
|
|
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.
|
|
137
138
|
Otherwise keep an explicit result annotation, use a narrow explained lint suppression, and test
|
|
138
139
|
every variant.
|
|
139
140
|
|
|
141
|
+
## Object-based mutation fragments
|
|
142
|
+
|
|
143
|
+
`sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep the
|
|
144
|
+
operation, table, authorization predicate, conflict policy, expressions, and `RETURNING` in SQL:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const created = await db.query(
|
|
148
|
+
sql<{ id: number }>`
|
|
149
|
+
INSERT INTO profiles ${sql.insertValues([
|
|
150
|
+
{ name: 'Ada', bio: null },
|
|
151
|
+
{ name: 'Lin', score: 4 },
|
|
152
|
+
])}
|
|
153
|
+
ON CONFLICT (name) DO NOTHING
|
|
154
|
+
RETURNING id
|
|
155
|
+
`,
|
|
156
|
+
{ returns: 'many' },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await db.query(sql`
|
|
160
|
+
UPDATE profiles SET ${sql.assignments({ name: input.name, bio: input.bio })},
|
|
161
|
+
revision = revision + 1
|
|
162
|
+
WHERE id = ${id} AND owner_id = ${viewer}
|
|
163
|
+
`);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Both helpers accept plain objects (including null-prototype objects), quote each column as one
|
|
167
|
+
identifier, and bind cell values as data. Inputs are read when the fragment is created; replacing a
|
|
168
|
+
field afterward does not change its captured value. Nested values retain the driver's usual
|
|
169
|
+
serialization behavior. Only own data properties are read; accessors are rejected.
|
|
170
|
+
|
|
171
|
+
| Input | Insert behavior | Assignment behavior |
|
|
172
|
+
| -------------------------------------------- | ---------------------------------- | --------------------------------------- |
|
|
173
|
+
| Missing or `undefined` field | PostgreSQL `DEFAULT` for that cell | Omitted; the stored column is unchanged |
|
|
174
|
+
| Explicit `null` | Bound SQL null | Bound SQL null |
|
|
175
|
+
| Other value | Bound parameter | Bound parameter |
|
|
176
|
+
| SQL fragment, identifier, or query as a cell | Rejected | Rejected |
|
|
177
|
+
|
|
178
|
+
Without `columns`, inserts infer the union of enumerable keys with defined values across all rows,
|
|
179
|
+
in first-seen order. Heterogeneous rows stay aligned; missing cells use `DEFAULT`. One object or a
|
|
180
|
+
one-row batch with no defined fields emits `DEFAULT VALUES`. An empty batch throws. A multi-row
|
|
181
|
+
batch with no inferred columns throws; supply explicit columns to emit multiple default rows.
|
|
182
|
+
|
|
183
|
+
Assignments infer enumerable keys and omit undefined values. An empty object or a selection with
|
|
184
|
+
no defined values throws, so the helper never emits an empty `SET` clause. This also applies when
|
|
185
|
+
the surrounding statement contains arithmetic or other assignments: branch explicitly when the
|
|
186
|
+
intended update consists only of those expressions.
|
|
187
|
+
|
|
188
|
+
An optional nonempty column list selects and orders fields. It does not validate that the input
|
|
189
|
+
object has no additional keys. Unlisted fields are ignored; missing insert cells use `DEFAULT`,
|
|
190
|
+
while missing update fields are omitted. Duplicate or invalid column
|
|
191
|
+
names throw. TypeScript checks selected names against the input's known keys. The list is also the
|
|
192
|
+
way to make runtime objects and batches available to static SQL checking:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const rows: { name: string; bio?: string | null }[] = buildRows();
|
|
196
|
+
const insert = sql`INSERT INTO profiles ${sql.insertValues(rows, ['name', 'bio'])} RETURNING id`;
|
|
197
|
+
|
|
198
|
+
const patch: { name?: string; bio?: string | null } = readPatch();
|
|
199
|
+
const update = sql`UPDATE profiles SET ${sql.assignments(patch, ['name', 'bio'])} WHERE id = ${id}`;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The helpers have no runtime schema metadata. Use [generated input types](#insert-and-update-inputs)
|
|
203
|
+
or application-owned contracts to check JavaScript values; PostgreSQL enforces identity,
|
|
204
|
+
generation, required-column, and constraint rules. Retain application validation.
|
|
205
|
+
To reset an update field to its default, write `column = DEFAULT` in the
|
|
206
|
+
surrounding SQL. Put arithmetic and `excluded.column` expressions there too; these helpers accept
|
|
207
|
+
values only, with no expression/default sentinel.
|
|
208
|
+
|
|
209
|
+
A compiled query, including surrounding interpolations, is limited to 65,535 bound parameters.
|
|
210
|
+
Tenon rejects larger queries and does not split them automatically. Choose batch sizes and
|
|
211
|
+
transaction boundaries in the caller. Helper validation errors identify invalid rows/columns
|
|
212
|
+
without printing cell values.
|
|
213
|
+
|
|
214
|
+
### Checking mutation fragments
|
|
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.
|
|
232
|
+
|
|
140
233
|
## Transactions, guarded writes, and errors
|
|
141
234
|
|
|
142
235
|
```ts
|
|
@@ -184,42 +277,84 @@ transaction has committed.
|
|
|
184
277
|
|
|
185
278
|
`generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
|
|
186
279
|
connected schema. It does not need migration files or history. Table type generation is optional.
|
|
187
|
-
SafeQL reads the database directly and does not require these generated files; they
|
|
188
|
-
|
|
189
|
-
file without rewriting it:
|
|
280
|
+
SafeQL reads the database directly and does not require these generated files; they provide
|
|
281
|
+
reusable row and mutation input types. The CLI writes that source atomically. This command checks
|
|
282
|
+
the existing file without rewriting it:
|
|
190
283
|
|
|
191
284
|
```sh
|
|
192
|
-
npx tenon generate --out src/generated-schema.ts --check
|
|
285
|
+
npx tenon generate --out src/generated-schema.d.ts --check
|
|
193
286
|
```
|
|
194
287
|
|
|
195
288
|
`--check` compares the generated representation with the current database. Pending migration files
|
|
196
289
|
are a separate concern. The representation includes table columns, constraints, indexes, and
|
|
197
290
|
referenced domains, not every PostgreSQL object.
|
|
198
291
|
|
|
199
|
-
Generation reads a consistent snapshot of ordinary tables and partitioned roots.
|
|
200
|
-
|
|
201
|
-
`
|
|
202
|
-
|
|
203
|
-
use query-specific types. A missing namespace
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
|
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
|
|
|
213
|
-
|
|
|
214
|
-
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
292
|
+
Generation reads a consistent snapshot of ordinary tables and partitioned roots. `Rows['User']`
|
|
293
|
+
describes one returned row; the generated `User` alias means the same thing. Use `sql<User>` for a
|
|
294
|
+
full row or `Pick<User, 'id' | 'email'>` for a projection. Table keys preserve exact SQL names;
|
|
295
|
+
`type AuditRow = Rows['audit-log']` works for names that cannot be exported as identifiers.
|
|
296
|
+
Views, foreign tables, and attached partitions use query-specific types. A missing namespace
|
|
297
|
+
fails; an existing empty schema is valid.
|
|
298
|
+
|
|
299
|
+
| PostgreSQL type | Returned value | Generated write input |
|
|
300
|
+
| ---------------------------------------------- | --------------------------------------------- | ------------------------------------- |
|
|
301
|
+
| smallint, integer, oid, real, double precision | `number` | `number` |
|
|
302
|
+
| bigint | `string` | `string \| bigint` |
|
|
303
|
+
| numeric | `string` | `string \| number` |
|
|
304
|
+
| boolean | `boolean` | `boolean` |
|
|
305
|
+
| text, name, varchar, char, uuid, time, timetz | `string` | `string` |
|
|
306
|
+
| date, timestamp, timestamptz | `Date \| number` (numbers include infinities) | `Date \| string` |
|
|
307
|
+
| enum | String literal union | Same union; `never` for an empty enum |
|
|
308
|
+
| json, jsonb | `unknown` | Serialized JSON text (`string`) |
|
|
309
|
+
| bytea | `Buffer` | `Buffer` |
|
|
310
|
+
|
|
311
|
+
For returned rows, nullable columns add null. Domains use verified underlying mappings and the
|
|
312
|
+
column's own nullability. Unverified types warn and become `unknown`. Verified arrays use
|
|
218
313
|
`PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
|
|
219
314
|
contain numbers and can lose precision; cast to text[] for exact values. Enum arrays and arrays of
|
|
220
315
|
domains are unverified. Finite dates and timestamps without time zone use Node's local timezone.
|
|
221
316
|
Custom pg parsers need matching application types.
|
|
222
317
|
|
|
318
|
+
### Insert and update inputs
|
|
319
|
+
|
|
320
|
+
The same file provides `Rows.Insert['table']` and `Rows.Update['table']` for bound write inputs.
|
|
321
|
+
These are ordinary TypeScript types. `--out` accepts `.ts` or `.d.ts`; the examples use `.d.ts`
|
|
322
|
+
because the file contains only declarations.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import type { Rows } from './generated-schema.js';
|
|
326
|
+
|
|
327
|
+
type NewProfile = Rows.Insert['profiles'];
|
|
328
|
+
type ProfilePatch = Pick<Rows.Update['profiles'], 'name' | 'bio'>;
|
|
329
|
+
|
|
330
|
+
const profile: NewProfile = { name: 'Ada' };
|
|
331
|
+
const patch: ProfilePatch = { name: undefined, bio: null };
|
|
332
|
+
sql.insertValues(profile, ['name', 'bio', 'score']);
|
|
333
|
+
sql.assignments(patch, ['name', 'bio']);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Insert fields are required when nonnullable and without a default. Other writable insert fields
|
|
337
|
+
are optional; every writable update field is optional. `undefined` requests a default on insert
|
|
338
|
+
and omits an assignment on update. Explicit `null` is accepted only when the column and its domain
|
|
339
|
+
chain permit it. Domain defaults count; column defaults take precedence.
|
|
340
|
+
|
|
341
|
+
Computed columns and `GENERATED ALWAYS` identity columns cannot receive values. Serial and
|
|
342
|
+
`GENERATED BY DEFAULT` identity columns are optional on insert and writable on update. Other
|
|
343
|
+
primary keys remain writable. Use `Pick` or `Omit` for application-specific restrictions and keep
|
|
344
|
+
expressions, `DEFAULT`, and special identity overrides in SQL.
|
|
345
|
+
|
|
346
|
+
Write representations in the table above also apply to verified array elements. Serialize JSON
|
|
347
|
+
with `JSON.stringify`, including JSON arrays; a JSON string containing `null` differs from SQL null.
|
|
348
|
+
Use timestamp strings for infinities. Unverified non-null write values use `never` and warn;
|
|
349
|
+
nullable fields still accept null. Supply an application-owned replacement when needed, for example
|
|
350
|
+
`Omit<Rows.Insert['events'], 'delay'> & { delay: string }` for a reviewed interval-text input.
|
|
351
|
+
|
|
352
|
+
Apply these types through annotations, `satisfies`, or helper generic arguments such as
|
|
353
|
+
`sql.insertValues<NewProfile>(input)`. They check shapes and representations, not authorization,
|
|
354
|
+
constraints, JSON contents, or nonempty inputs. Optional fields accept explicit `undefined`,
|
|
355
|
+
including with `exactOptionalPropertyTypes`. Selected columns can discard required fields; see
|
|
356
|
+
the [cookbook example](mutations.md#input-types-sql-checking-and-runtime-validation).
|
|
357
|
+
|
|
223
358
|
## SQL checking with SafeQL
|
|
224
359
|
|
|
225
360
|
SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
|
|
@@ -230,8 +365,8 @@ Configure [SafeQL normally](https://safeql.dev/api/). Tenon's optional `safeql`
|
|
|
230
365
|
two objects:
|
|
231
366
|
|
|
232
367
|
- `safeqlPlugin` is a SafeQL plugin descriptor. Add it to each Tenon connection's `plugins` list. It
|
|
233
|
-
translates constant fragments and identifiers, preserves
|
|
234
|
-
pg result mappings. SafeQL owns database analysis and annotation fixes.
|
|
368
|
+
translates constant fragments, official mutation helpers, and identifiers, preserves parameter
|
|
369
|
+
behavior, and supplies pg result mappings. SafeQL owns database analysis and annotation fixes.
|
|
235
370
|
- `eslintPlugin` supplies `tenon/check-composition`. Register it in ESLint and enable the rule to
|
|
236
371
|
report queries that the adapter cannot resolve, including runtime-built fragments and escaped
|
|
237
372
|
outer templates.
|
|
@@ -248,11 +383,8 @@ existing TypeScript installation:
|
|
|
248
383
|
npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
|
|
249
384
|
```
|
|
250
385
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
`typescript-eslint` is an application-owned parser dependency, not a Tenon peer. Your ESLint
|
|
254
|
-
configuration imports SafeQL and the parser explicitly. Use a TypeScript version supported by that
|
|
255
|
-
lint toolchain. The `pg` driver remains Tenon's required runtime peer.
|
|
386
|
+
These are development dependencies. Reuse your existing type-aware parser configuration and a
|
|
387
|
+
TypeScript version supported by the lint toolchain. Tenon's database runtime does not load them.
|
|
256
388
|
|
|
257
389
|
For a project with `src/**/*.ts` and a `tsconfig.json`:
|
|
258
390
|
|
|
@@ -289,9 +421,6 @@ configuration. `projectService` uses the nearest TypeScript project for each fil
|
|
|
289
421
|
`DATABASE_URL` through your application's environment setup and point it at a development/test
|
|
290
422
|
database with migrations applied. This example does not load `.env` files.
|
|
291
423
|
|
|
292
|
-
The `.mjs` example avoids a current declaration mismatch between SafeQL's configuration helper and
|
|
293
|
-
ESLint's `defineConfig`. This affects configuration typechecking, not query result types.
|
|
294
|
-
|
|
295
424
|
`targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
|
|
296
425
|
single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
|
|
297
426
|
that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
|
|
@@ -311,6 +440,9 @@ They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
|
|
|
311
440
|
|
|
312
441
|
### Generate and check annotations
|
|
313
442
|
|
|
443
|
+
For an existing persistence layer, start with the [incremental audit workflow](adoption-guide.md#adopt-sql-checking-incrementally).
|
|
444
|
+
It covers a separate SQL lint command, fix previews, and application refinements.
|
|
445
|
+
|
|
314
446
|
Write a query without a result annotation:
|
|
315
447
|
|
|
316
448
|
```ts
|
|
@@ -372,25 +504,48 @@ file group, each extending its own `safeql.configs.connections({ ... })`. Use no
|
|
|
372
504
|
`files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
|
|
373
505
|
coexist; add Tenon's adapter only to connections checking Tenon queries.
|
|
374
506
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
when the connection's `search_path` selects the correct table at execution. Explicit schema names
|
|
378
|
-
avoid that ambiguity.
|
|
507
|
+
Qualify same-named tables with their schemas, for example `SELECT id FROM billing.accounts`, so
|
|
508
|
+
the intended relation is explicit in both checking and execution.
|
|
379
509
|
|
|
380
510
|
### Supported SQL and limitations
|
|
381
511
|
|
|
382
512
|
Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
|
|
383
513
|
aliases, namespace imports, and object properties that retain its TypeScript type. It checks
|
|
384
|
-
constant fragments and `sql.join` with literal arrays, including nested fragments.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
514
|
+
constant fragments and `sql.join` with nonempty literal arrays, including nested fragments.
|
|
515
|
+
Separators and `identifier()` names must resolve to literal strings or const string initializers.
|
|
516
|
+
Values use the same parameter placeholders in static checking and execution, inside or outside
|
|
517
|
+
fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
|
|
518
|
+
JavaScript types.
|
|
519
|
+
|
|
520
|
+
The checker follows const fragment initializers, including imported constants with inspectable
|
|
521
|
+
source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
|
|
522
|
+
JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
|
|
523
|
+
unsupported even when a particular helper appears simple or a condition is a literal. Mutable
|
|
524
|
+
references, runtime filter lists, and dynamic identifiers are also unsupported. Ordinary value
|
|
525
|
+
expressions remain bound parameters; calls that produce data for an official mutation helper can
|
|
526
|
+
use its explicit column-list contract.
|
|
527
|
+
|
|
528
|
+
`tenon/check-composition` points to the first unsupported interpolation in each query and explains
|
|
529
|
+
why it cannot be checked. For example, opaque mutation inputs need an inline column list, while
|
|
530
|
+
fragment-returning functions remain outside static analysis. Errors reached through shared constants
|
|
531
|
+
are reported at the consuming query. ESLint suppressions must cover the reported expression's line;
|
|
532
|
+
a scoped disable/enable can cover one multiline query.
|
|
533
|
+
|
|
534
|
+
Reusable authorization and other application policy can still live in fragment-returning functions
|
|
535
|
+
at runtime. Keep that policy centralized and cover its behavior with database tests. Suppress
|
|
536
|
+
`tenon/check-composition` only on the particular query, with a reason, when its variants are tested.
|
|
537
|
+
Do not duplicate policy merely to obtain static coverage. SQL and annotation errors in checkable
|
|
538
|
+
queries continue to come from `@ts-safeql/check-sql`.
|
|
539
|
+
|
|
540
|
+
Ordinary functions containing complete `sql` templates remain checkable. Each template is analyzed
|
|
541
|
+
where it is written, including constant fragments defined in that function:
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
function findUsers(email: string) {
|
|
545
|
+
const predicate = sql.fragment`email = ${email}`;
|
|
546
|
+
return sql`SELECT id FROM users WHERE ${predicate}`;
|
|
547
|
+
}
|
|
548
|
+
```
|
|
394
549
|
|
|
395
550
|
Use literal line breaks in `sql` templates and bind string values with `${value}`. The composition
|
|
396
551
|
rule rejects JavaScript escapes such as `\n` or `\'` in the outer template: SafeQL reads raw
|
|
@@ -436,7 +591,7 @@ tenon new <name> [--directory migrations]
|
|
|
436
591
|
tenon migrate [--directory migrations] [--namespace public]
|
|
437
592
|
tenon status [--directory migrations] [--namespace public]
|
|
438
593
|
tenon rollback [--directory migrations] [--namespace public] [--steps 1]
|
|
439
|
-
tenon generate [--namespace public] [--out
|
|
594
|
+
tenon generate [--namespace public] [--out path] [--check]
|
|
440
595
|
tenon baseline capture|adopt [--directory migrations] [--namespace public]
|
|
441
596
|
```
|
|
442
597
|
|
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
|
-
}
|