turbine-orm 0.81.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/generate.js
CHANGED
|
@@ -11,19 +11,29 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
13
13
|
import { ValidationError } from './errors.js';
|
|
14
|
-
import { assertDistinctColumnFields } from './introspect.js';
|
|
15
|
-
import {
|
|
14
|
+
import { assertDistinctColumnFields, printableCatalogName } from './introspect.js';
|
|
15
|
+
import { consoleLogger, logTo } from './logger.js';
|
|
16
|
+
import { closestName } from './query/utils.js';
|
|
17
|
+
import { pgTypeToTs, pgTypeToTsInput, singularize, snakeToPascal, timeOfDayKind, withDbFieldNames, } from './schema.js';
|
|
18
|
+
import { tsTypeTextProblem } from './schema-builder.js';
|
|
16
19
|
/** Get the TypeScript type name for a table (singularized PascalCase) */
|
|
17
20
|
function entityName(tableName) {
|
|
18
21
|
return snakeToPascal(singularize(tableName));
|
|
19
22
|
}
|
|
23
|
+
/** The override for one column, or `undefined`. */
|
|
24
|
+
function overrideFor(overrides, table, column) {
|
|
25
|
+
return overrides?.get(table)?.get(column);
|
|
26
|
+
}
|
|
20
27
|
/**
|
|
21
28
|
* Resolve the TypeScript type for a column, mapping enum-typed columns to their
|
|
22
29
|
* generated string-literal union (PascalCase enum name) instead of the
|
|
23
|
-
* `unknown` that {@link pgTypeToTs} yields for user-defined types.
|
|
24
|
-
* the
|
|
30
|
+
* `unknown` that {@link pgTypeToTs} yields for user-defined types. A
|
|
31
|
+
* `columnTypes` override replaces the type outright (nullability is still
|
|
32
|
+
* added). Falls back to the introspected `col.tsType` for everything else.
|
|
25
33
|
*/
|
|
26
|
-
function columnTsType(col, enums) {
|
|
34
|
+
export function columnTsType(col, enums, override) {
|
|
35
|
+
if (override !== undefined)
|
|
36
|
+
return col.nullable ? `${override} | null` : override;
|
|
27
37
|
const dt = col.dialectType ?? col.pgType;
|
|
28
38
|
const isArray = col.isArray || dt.startsWith('_');
|
|
29
39
|
const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
|
|
@@ -37,27 +47,78 @@ function columnTsType(col, enums) {
|
|
|
37
47
|
}
|
|
38
48
|
/**
|
|
39
49
|
* Resolve the TypeScript type a column accepts on the WRITE path (the `*Create`
|
|
40
|
-
* / `*Update` input types)
|
|
41
|
-
*
|
|
50
|
+
* / `*Update` input types). It differs from the row type where the runtime
|
|
51
|
+
* accepts more than one spelling or cannot take the parsed read value back
|
|
52
|
+
* ({@link pgTypeToTsInput}), and for time-of-day columns.
|
|
42
53
|
*
|
|
43
54
|
* A `time` / `timetz` column reads back as a `string` (`'09:00:00'`), so the
|
|
44
55
|
* row type stays `string`. On write the runtime also accepts a JS `Date` and
|
|
45
56
|
* narrows it to its UTC time of day (see `coerceWriteValue` in
|
|
46
57
|
* query/writes.ts), matching Prisma, which types a `DateTime @db.Time(6)`
|
|
47
|
-
* field as a `Date`.
|
|
48
|
-
* type has to admit them. Every other column is unchanged.
|
|
58
|
+
* field as a `Date`.
|
|
49
59
|
*/
|
|
50
|
-
function writeColumnTsType(col, enums) {
|
|
51
|
-
const tsType = columnTsType(col, enums);
|
|
60
|
+
function writeColumnTsType(col, enums, override) {
|
|
61
|
+
const tsType = columnTsType(col, enums, override);
|
|
62
|
+
if (override !== undefined)
|
|
63
|
+
return tsType;
|
|
52
64
|
const dt = col.dialectType ?? col.pgType;
|
|
53
65
|
const isArray = col.isArray || dt.startsWith('_');
|
|
54
66
|
const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
|
|
55
|
-
if (
|
|
67
|
+
if (Object.hasOwn(enums, base))
|
|
56
68
|
return tsType;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
if (timeOfDayKind(base)) {
|
|
70
|
+
// A `time[]` column accepts a Date per element (the bind rewrite runs
|
|
71
|
+
// element-wise, see coerceTemporalValue in query/utils.ts).
|
|
72
|
+
const widened = isArray ? '(string | Date)[]' : 'string | Date';
|
|
73
|
+
return col.nullable ? `${widened} | null` : widened;
|
|
74
|
+
}
|
|
75
|
+
// Only rewrite a type the read map produced: hand-built or engine metadata
|
|
76
|
+
// carrying its own tsType keeps it.
|
|
77
|
+
const pgType = isArray && !dt.startsWith('_') ? `_${dt}` : dt;
|
|
78
|
+
const read = pgTypeToTs(pgType, col.nullable);
|
|
79
|
+
const input = pgTypeToTsInput(pgType, col.nullable);
|
|
80
|
+
return input !== read && tsType === read ? input : tsType;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate {@link GenerateOptions.columnTypes} against the schema and index it.
|
|
84
|
+
* A key naming no table or column is a `ValidationError`, not a silent no-op,
|
|
85
|
+
* since an override that applies to nothing leaves the column typed `unknown`.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveColumnTypeOverrides(schema, columnTypes) {
|
|
88
|
+
if (columnTypes === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
const out = new Map();
|
|
91
|
+
for (const [key, tsType] of Object.entries(columnTypes)) {
|
|
92
|
+
const dot = key.lastIndexOf('.');
|
|
93
|
+
const tableName = dot > 0 ? key.slice(0, dot) : '';
|
|
94
|
+
const columnName = dot > 0 ? key.slice(dot + 1) : '';
|
|
95
|
+
if (!tableName || !columnName) {
|
|
96
|
+
throw new ValidationError(`columnTypes key ${printableCatalogName(JSON.stringify(key))} must be "<table>.<column>", using database names, ` +
|
|
97
|
+
'for example "users.settings".');
|
|
98
|
+
}
|
|
99
|
+
const table = Object.hasOwn(schema.tables, tableName) ? schema.tables[tableName] : undefined;
|
|
100
|
+
if (!table) {
|
|
101
|
+
const hint = closestName(tableName, Object.keys(schema.tables));
|
|
102
|
+
throw new ValidationError(`columnTypes key ${printableCatalogName(JSON.stringify(key))} names no table${hint ? `. Did you mean "${printableCatalogName(hint)}"?` : '.'}`);
|
|
103
|
+
}
|
|
104
|
+
if (!table.columns.some((c) => c.name === columnName)) {
|
|
105
|
+
const hint = closestName(columnName, table.columns.map((c) => c.name));
|
|
106
|
+
throw new ValidationError(`columnTypes key ${printableCatalogName(JSON.stringify(key))} names no column of "${printableCatalogName(tableName)}"` +
|
|
107
|
+
`${hint ? `. Did you mean "${printableCatalogName(`${tableName}.${hint}`)}"?` : '.'}`);
|
|
108
|
+
}
|
|
109
|
+
const problem = typeof tsType === 'string' ? tsTypeTextProblem(tsType) : 'it is not a string';
|
|
110
|
+
if (problem !== null) {
|
|
111
|
+
throw new ValidationError(`columnTypes[${printableCatalogName(JSON.stringify(key))}] must be one TypeScript type on one line, for example ` +
|
|
112
|
+
`"import('../src/types.js').Settings", but ${problem}: ${printableCatalogName(JSON.stringify(tsType))}.`);
|
|
113
|
+
}
|
|
114
|
+
let perTable = out.get(tableName);
|
|
115
|
+
if (!perTable) {
|
|
116
|
+
perTable = new Map();
|
|
117
|
+
out.set(tableName, perTable);
|
|
118
|
+
}
|
|
119
|
+
perTable.set(columnName, tsType.trim());
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
61
122
|
}
|
|
62
123
|
/**
|
|
63
124
|
* Characters that must never reach generated source verbatim: the two that end
|
|
@@ -131,7 +192,13 @@ export function generate(options) {
|
|
|
131
192
|
// F3: resolve which extension sibling imports get in index.ts.
|
|
132
193
|
const { ext: importExt, mode: importMode } = resolveImportExtension(outDir, options.importExtension ?? 'auto');
|
|
133
194
|
const files = [];
|
|
134
|
-
const fileOptions = {
|
|
195
|
+
const fileOptions = {
|
|
196
|
+
noTimestamp: options.noTimestamp,
|
|
197
|
+
importExt,
|
|
198
|
+
importMode,
|
|
199
|
+
...(options.columnTypes !== undefined ? { columnTypes: options.columnTypes } : {}),
|
|
200
|
+
...(options.logger !== undefined ? { logger: options.logger } : {}),
|
|
201
|
+
};
|
|
135
202
|
// Generate types.ts
|
|
136
203
|
const typesContent = generateTypes(schema, fileOptions);
|
|
137
204
|
writeFileSync(join(outDir, 'types.ts'), typesContent, 'utf-8');
|
|
@@ -161,6 +228,7 @@ export function generate(options) {
|
|
|
161
228
|
* mode label for the generated comment.
|
|
162
229
|
*
|
|
163
230
|
* - `'js'` → `.js`
|
|
231
|
+
* - `'ts'` → `.ts`
|
|
164
232
|
* - `'none'` → `''`
|
|
165
233
|
* - `'auto'` → tsconfig-driven ({@link detectTsconfigExtension}); falls back
|
|
166
234
|
* to `.js` (the pre-0.41 default) when detection is uncertain, so NodeNext
|
|
@@ -169,6 +237,8 @@ export function generate(options) {
|
|
|
169
237
|
export function resolveImportExtension(outDir, requested) {
|
|
170
238
|
if (requested === 'js')
|
|
171
239
|
return { ext: '.js', mode: 'js' };
|
|
240
|
+
if (requested === 'ts')
|
|
241
|
+
return { ext: '.ts', mode: 'ts' };
|
|
172
242
|
if (requested === 'none')
|
|
173
243
|
return { ext: '', mode: 'none' };
|
|
174
244
|
const detected = detectTsconfigExtension(resolve(outDir));
|
|
@@ -321,14 +391,14 @@ function generatedFileHeader(options) {
|
|
|
321
391
|
* skip those relations here with a warning instead of emitting broken types.
|
|
322
392
|
* The runtime metadata (metadata.ts) still carries every relation.
|
|
323
393
|
*/
|
|
324
|
-
function typeSafeRelations(table,
|
|
394
|
+
function typeSafeRelations(table, logger) {
|
|
325
395
|
const columnFields = new Set(table.columns.map((c) => c.field));
|
|
326
396
|
const usable = [];
|
|
327
397
|
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
328
398
|
if (columnFields.has(relName)) {
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
399
|
+
if (logger) {
|
|
400
|
+
logTo(logger, 'warn', printableCatalogName(`Relation "${relName}" on table "${table.name}" shadows a column field of the same name, ` +
|
|
401
|
+
'omitting it from the generated types. Rename the relation (or the column) to expose it.'), { code: 'generate-relation-shadows-column', table: table.name, relation: relName });
|
|
332
402
|
}
|
|
333
403
|
continue;
|
|
334
404
|
}
|
|
@@ -364,10 +434,42 @@ export function isEmittableIdentifier(name) {
|
|
|
364
434
|
function requireEmittable(derived, subject, role) {
|
|
365
435
|
if (isEmittableIdentifier(derived))
|
|
366
436
|
return;
|
|
367
|
-
throw new ValidationError(`Cannot generate code for ${subject}: it produces ${JSON.stringify(derived)} as ${role}, ` +
|
|
437
|
+
throw new ValidationError(`Cannot generate code for ${printableCatalogName(subject)}: it produces ${printableCatalogName(JSON.stringify(derived))} as ${role}, ` +
|
|
368
438
|
`which is not a valid TypeScript identifier. Rename the database object, or exclude it from generation ` +
|
|
369
439
|
`(introspect \`exclude\`).`);
|
|
370
440
|
}
|
|
441
|
+
/** Table accessors the generated `TurbineClient` class cannot declare as members. */
|
|
442
|
+
const RESERVED_ACCESSORS = new Set(['constructor', '__proto__']);
|
|
443
|
+
/** The type a defineSchema file declared for a column (bare, no `| null`), if any. */
|
|
444
|
+
function declaredTsTypeOf(col) {
|
|
445
|
+
return col.declaredTsType;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* The metadata `types.ts` is written from. A column whose schema file declared a
|
|
449
|
+
* TypeScript type takes it, with `| null` for a nullable column, in place of the
|
|
450
|
+
* type mapped from its database type. The runtime keeps the mapped type
|
|
451
|
+
* (`metadata.ts` is written from the input), because the engines branch on it.
|
|
452
|
+
* Returns `schema` itself when no column declared one.
|
|
453
|
+
*/
|
|
454
|
+
function withDeclaredTypes(schema) {
|
|
455
|
+
let declaredAny = false;
|
|
456
|
+
const tables = Object.create(null);
|
|
457
|
+
for (const [name, table] of Object.entries(schema.tables)) {
|
|
458
|
+
if (!table.columns.some((c) => declaredTsTypeOf(c) !== undefined)) {
|
|
459
|
+
tables[name] = table;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
declaredAny = true;
|
|
463
|
+
tables[name] = {
|
|
464
|
+
...table,
|
|
465
|
+
columns: table.columns.map((col) => {
|
|
466
|
+
const declared = declaredTsTypeOf(col);
|
|
467
|
+
return declared === undefined ? col : { ...col, tsType: col.nullable ? `${declared} | null` : declared };
|
|
468
|
+
}),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return declaredAny ? { ...schema, tables } : schema;
|
|
472
|
+
}
|
|
371
473
|
/**
|
|
372
474
|
* Refuse a schema whose names cannot be emitted as TypeScript identifiers.
|
|
373
475
|
*
|
|
@@ -389,11 +491,29 @@ export function assertEmittableSchema(schema) {
|
|
|
389
491
|
for (const table of Object.values(schema.tables)) {
|
|
390
492
|
requireEmittable(entityName(table.name), `table "${table.name}"`, 'the generated entity type name');
|
|
391
493
|
requireEmittable(snakeToCamelStr(table.name), `table "${table.name}"`, 'the generated client accessor');
|
|
494
|
+
if (RESERVED_ACCESSORS.has(snakeToCamelStr(table.name))) {
|
|
495
|
+
throw new ValidationError(`Cannot generate code for table "${printableCatalogName(table.name)}": its client accessor ` +
|
|
496
|
+
`"${snakeToCamelStr(table.name)}" is a reserved class member name, so the generated class could not declare it. ` +
|
|
497
|
+
'Rename the table, or exclude it from generation (introspect `exclude`).');
|
|
498
|
+
}
|
|
392
499
|
// Two columns on one field would emit a duplicate interface member (TS2300
|
|
393
500
|
// in the consumer's build) and a `columnMap` missing a column. The rule is
|
|
394
501
|
// introspect.ts's; re-asserting it here covers a hand-built or
|
|
395
502
|
// engine-introspected schema that never went through that path.
|
|
396
503
|
assertDistinctColumnFields(table.name, table.columns);
|
|
504
|
+
// A column's type is emitted verbatim. Introspection only produces mapped
|
|
505
|
+
// types, but hand-built or code-first metadata (`tsType` on defineSchema)
|
|
506
|
+
// can carry anything, and a line break or a stray bracket would end the type
|
|
507
|
+
// and start a statement in the generated file.
|
|
508
|
+
for (const col of table.columns) {
|
|
509
|
+
for (const text of [col.tsType, declaredTsTypeOf(col)]) {
|
|
510
|
+
const problem = text === undefined ? null : tsTypeTextProblem(text);
|
|
511
|
+
if (problem !== null) {
|
|
512
|
+
throw new ValidationError(`Cannot generate code for column "${printableCatalogName(col.name)}" on table "${printableCatalogName(table.name)}": ` +
|
|
513
|
+
`its TypeScript type ${printableCatalogName(JSON.stringify(text))} cannot be emitted, because ${problem}. Give one type on one line.`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
397
517
|
// Only the relations that reach the TYPE layer: a relation shadowing a
|
|
398
518
|
// column field is already dropped from types.ts, so refusing on its name
|
|
399
519
|
// would refuse a schema that generates fine.
|
|
@@ -405,38 +525,33 @@ export function assertEmittableSchema(schema) {
|
|
|
405
525
|
}
|
|
406
526
|
/**
|
|
407
527
|
* Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
|
|
408
|
-
*
|
|
409
|
-
* generator output without writing files to disk.
|
|
528
|
+
* *Relations brand-field interfaces, and the write input types). Exported so
|
|
529
|
+
* tests can pin the generator output without writing files to disk.
|
|
410
530
|
*/
|
|
411
|
-
export function generateTypes(
|
|
412
|
-
assertEmittableSchema(
|
|
531
|
+
export function generateTypes(input, options) {
|
|
532
|
+
assertEmittableSchema(input);
|
|
533
|
+
const schema = withDeclaredTypes(input);
|
|
534
|
+
const overrides = resolveColumnTypeOverrides(schema, options?.columnTypes);
|
|
413
535
|
const lines = [...generatedFileHeader(options)];
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
// recurse through nested `with` clauses at any depth. The generator emits
|
|
420
|
-
// each `*Relations` member as a `RelationDescriptor<Target, Cardinality,
|
|
421
|
-
// TargetRelations>` so users get full deep `with`-clause type inference
|
|
422
|
-
// out of the box (TASK-2.1).
|
|
423
|
-
lines.push("import type { RelationDescriptor, UpdateOperatorInput } from 'turbine-orm';");
|
|
536
|
+
// UpdateOperatorInput, SqlExpr and ColumnRef let the generated *Update
|
|
537
|
+
// types express atomic operators, `expr.*` update expressions and `col()`.
|
|
538
|
+
// RelationDescriptor is the brand that lets `WithResult` recurse through
|
|
539
|
+
// nested `with` clauses at any depth.
|
|
540
|
+
lines.push("import type { ColumnRef, RelationDescriptor, SqlExpr, UpdateOperatorInput } from 'turbine-orm';");
|
|
424
541
|
lines.push('');
|
|
425
|
-
// Pre-compute which tables have relations so we know whether to thread
|
|
426
|
-
// `${TargetType}Relations` (for deep inference) or `{}` (the no-relations
|
|
427
|
-
// default) into each `RelationDescriptor`. Built once up-front because
|
|
428
|
-
// relations can point at tables we haven't iterated to yet.
|
|
429
542
|
// Relations that can be surfaced in the type layer, computed once per table
|
|
430
543
|
// (relations that would shadow a scalar column field are excluded + warned).
|
|
431
544
|
const safeRelationsByTable = new Map();
|
|
432
545
|
for (const t of Object.values(schema.tables)) {
|
|
433
|
-
safeRelationsByTable.set(t.name, typeSafeRelations(t));
|
|
546
|
+
safeRelationsByTable.set(t.name, typeSafeRelations(t, options?.logger ?? consoleLogger));
|
|
434
547
|
}
|
|
435
548
|
const tablesWithRelations = new Set();
|
|
436
549
|
for (const t of Object.values(schema.tables)) {
|
|
437
550
|
if ((safeRelationsByTable.get(t.name) ?? []).length > 0)
|
|
438
551
|
tablesWithRelations.add(t.name);
|
|
439
552
|
}
|
|
553
|
+
const rowType = (table, col) => columnTsType(col, schema.enums, overrideFor(overrides, table.name, col.name));
|
|
554
|
+
const writeType = (table, col) => writeColumnTsType(col, schema.enums, overrideFor(overrides, table.name, col.name));
|
|
440
555
|
// Generate enum types
|
|
441
556
|
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
442
557
|
const typeName = snakeToPascal(enumName);
|
|
@@ -454,62 +569,57 @@ export function generateTypes(schema, options) {
|
|
|
454
569
|
const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
|
|
455
570
|
const nullNote = col.nullable ? ' (nullable)' : '';
|
|
456
571
|
// PII columns are excluded from default projections, so the field is
|
|
457
|
-
// absent unless the
|
|
458
|
-
//
|
|
459
|
-
|
|
572
|
+
// absent unless the read passes `includePii: UNSAFE` (a `select` naming
|
|
573
|
+
// it without the flag throws, and a write never returns it). The emitted
|
|
574
|
+
// type marks it optional so it tells the truth about absence.
|
|
575
|
+
const piiNote = col.pii ? ' (PII: absent unless the read passes includePii: UNSAFE)' : '';
|
|
460
576
|
const optional = col.pii ? '?' : '';
|
|
461
577
|
lines.push(` /** Column: ${docSafe(col.name)}, ${docSafe(col.pgType)}${pkNote}${nullNote}${piiNote} */`);
|
|
462
|
-
lines.push(` ${quoteIfNeeded(col.field)}${optional}: ${
|
|
578
|
+
lines.push(` ${quoteIfNeeded(col.field)}${optional}: ${rowType(table, col)};`);
|
|
463
579
|
}
|
|
464
580
|
lines.push('}');
|
|
465
581
|
lines.push('');
|
|
466
582
|
// --- Create input type ---
|
|
467
583
|
// Optional: server-generated columns (serial / identity), columns with a
|
|
468
584
|
// default, and nullable columns (default to NULL). Everything else is
|
|
469
|
-
// required, PRIMARY KEY MEMBERSHIP INCLUDED
|
|
585
|
+
// required, PRIMARY KEY MEMBERSHIP INCLUDED: a text natural key, a
|
|
470
586
|
// client-supplied uuid with no default, or a composite key of plain
|
|
471
|
-
// integers must be supplied,
|
|
472
|
-
// (E010). Marking every PK column optional let `create({ data: {} })` on
|
|
473
|
-
// a junction table typecheck and fail at runtime, which is the opposite of
|
|
474
|
-
// what a generated type is for.
|
|
587
|
+
// integers must be supplied, or the database rejects the row (E010).
|
|
475
588
|
lines.push(`/** Input type for creating a row in \`${docSafe(table.name)}\` */`);
|
|
476
589
|
lines.push(`export type ${typeName}Create = {`);
|
|
477
590
|
for (const col of table.columns) {
|
|
478
591
|
// STORED generated columns are computed by the database, never writable.
|
|
479
592
|
if (col.isGeneratedStored)
|
|
480
593
|
continue;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (isOptional) {
|
|
484
|
-
const reason = isGenerated ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
|
|
594
|
+
if (isOptionalOnCreate(col)) {
|
|
595
|
+
const reason = col.isGenerated === true ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
|
|
485
596
|
lines.push(` /** Optional: ${reason} */`);
|
|
486
|
-
lines.push(` ${quoteIfNeeded(col.field)}?: ${
|
|
597
|
+
lines.push(` ${quoteIfNeeded(col.field)}?: ${writeType(table, col)} | undefined;`);
|
|
487
598
|
}
|
|
488
599
|
else {
|
|
489
|
-
lines.push(` ${quoteIfNeeded(col.field)}: ${
|
|
600
|
+
lines.push(` ${quoteIfNeeded(col.field)}: ${writeType(table, col)};`);
|
|
490
601
|
}
|
|
491
602
|
}
|
|
492
603
|
lines.push('};');
|
|
493
604
|
lines.push('');
|
|
494
605
|
// --- Update input type (all fields optional except PK) ---
|
|
495
|
-
//
|
|
496
|
-
//
|
|
606
|
+
// Every field also takes `{ set: value }`, and numeric fields the atomic
|
|
607
|
+
// operators (`{ viewCount: { increment: 1 } }`).
|
|
497
608
|
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name) && !c.isGeneratedStored);
|
|
498
609
|
lines.push(`/** Input type for updating a row in \`${docSafe(table.name)}\` */`);
|
|
499
610
|
lines.push(`export type ${typeName}Update = {`);
|
|
500
611
|
for (const col of nonPkCols) {
|
|
501
|
-
lines.push(` ${quoteIfNeeded(col.field)}?: ${updateFieldType(
|
|
612
|
+
lines.push(` ${quoteIfNeeded(col.field)}?: ${updateFieldType(writeType(table, col))} | undefined;`);
|
|
502
613
|
}
|
|
503
614
|
lines.push('};');
|
|
504
615
|
lines.push('');
|
|
505
616
|
// --- Relations map (for type-safe `with` clauses) ---
|
|
506
617
|
//
|
|
507
|
-
// Each relation is
|
|
508
|
-
// TargetRelations
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
// no relations of its own, the descriptor uses `{}` (the default).
|
|
618
|
+
// Each relation is a `RelationDescriptor<Target, Cardinality,
|
|
619
|
+
// TargetRelations, Kind, Nullable>`. `WithResult` reads the third
|
|
620
|
+
// parameter to recurse into nested `with` clauses and the fifth to type a
|
|
621
|
+
// to-one relation whose foreign key is NOT NULL as never null; the write
|
|
622
|
+
// input types read the kind.
|
|
513
623
|
const safeRelations = safeRelationsByTable.get(table.name) ?? [];
|
|
514
624
|
const hasRelations = safeRelations.length > 0;
|
|
515
625
|
if (hasRelations) {
|
|
@@ -518,168 +628,367 @@ export function generateTypes(schema, options) {
|
|
|
518
628
|
for (const [relName, rel] of safeRelations) {
|
|
519
629
|
const targetType = entityName(rel.to);
|
|
520
630
|
// manyToMany is a collection too → 'many' cardinality (same as hasMany).
|
|
521
|
-
const cardinality = rel
|
|
631
|
+
const cardinality = isToMany(rel) ? "'many'" : "'one'";
|
|
522
632
|
const targetRelations = tablesWithRelations.has(rel.to) ? `${targetType}Relations` : '{}';
|
|
523
|
-
|
|
633
|
+
const notNull = isRequiredBelongsTo(table, rel) ? ', false' : '';
|
|
634
|
+
lines.push(` ${quoteIfNeeded(relName)}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}, '${escSQ(rel.type)}'${notNull}>;`);
|
|
524
635
|
}
|
|
525
636
|
lines.push('}');
|
|
526
637
|
lines.push('');
|
|
527
638
|
// --- Legacy per-relation interfaces (kept for backward compatibility) ---
|
|
528
639
|
for (const [relName, rel] of safeRelations) {
|
|
529
640
|
const targetType = entityName(rel.to);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
lines.push(` ${quoteIfNeeded(relName)}: ${targetType} | null;`);
|
|
540
|
-
lines.push('}');
|
|
541
|
-
}
|
|
641
|
+
const loaded = isToMany(rel)
|
|
642
|
+
? `${targetType}[]`
|
|
643
|
+
: isRequiredBelongsTo(table, rel)
|
|
644
|
+
? targetType
|
|
645
|
+
: `${targetType} | null`;
|
|
646
|
+
lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
|
|
647
|
+
lines.push(`export interface ${typeName}With${snakeToPascal(relName)} extends ${typeName} {`);
|
|
648
|
+
lines.push(` ${quoteIfNeeded(relName)}: ${loaded};`);
|
|
649
|
+
lines.push('}');
|
|
542
650
|
lines.push('');
|
|
543
651
|
}
|
|
544
652
|
}
|
|
545
653
|
}
|
|
546
654
|
// ---------------------------------------------------------------------------
|
|
547
|
-
//
|
|
548
|
-
//
|
|
655
|
+
// Write input types: WhereUnique, CreateInput, UpdateInput, the nested
|
|
656
|
+
// relation write inputs, and the per-table Inputs map the client accessor
|
|
657
|
+
// takes.
|
|
549
658
|
// ---------------------------------------------------------------------------
|
|
550
659
|
for (const table of Object.values(schema.tables)) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
660
|
+
emitWhereUnique(lines, table, (col) => rowType(table, { ...col, nullable: false, tsType: stripNull(col.tsType) }));
|
|
661
|
+
}
|
|
662
|
+
const variantsByTable = createVariantsByTable(schema, safeRelationsByTable);
|
|
663
|
+
for (const table of Object.values(schema.tables)) {
|
|
664
|
+
emitWriteInputs(lines, schema, table, safeRelationsByTable.get(table.name) ?? [], variantsByTable, (col) => writeType(table, col));
|
|
665
|
+
}
|
|
666
|
+
return lines.join('\n');
|
|
667
|
+
}
|
|
668
|
+
/** A to-many relation (an array on the row). */
|
|
669
|
+
function isToMany(rel) {
|
|
670
|
+
return rel.type === 'hasMany' || rel.type === 'manyToMany';
|
|
671
|
+
}
|
|
672
|
+
/** Whether a column may be omitted on create. */
|
|
673
|
+
function isOptionalOnCreate(col) {
|
|
674
|
+
return col.isGenerated === true || col.hasDefault || col.nullable;
|
|
675
|
+
}
|
|
676
|
+
/** `T | null` -> `T`. */
|
|
677
|
+
function stripNull(tsType) {
|
|
678
|
+
return tsType.replace(/\s*\|\s*null$/, '');
|
|
679
|
+
}
|
|
680
|
+
/** The foreign-key columns of a relation, as columns of `table` (the side that holds them). */
|
|
681
|
+
function fkColumns(table, rel) {
|
|
682
|
+
const out = [];
|
|
683
|
+
for (const name of Array.isArray(rel.foreignKey) ? rel.foreignKey : [rel.foreignKey]) {
|
|
684
|
+
const col = table.columns.find((c) => c.name === name);
|
|
685
|
+
if (col)
|
|
686
|
+
out.push(col);
|
|
687
|
+
}
|
|
688
|
+
return out;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* A belongsTo relation whose foreign key is NOT NULL, so a row always points
|
|
692
|
+
* at a target row and reading the relation never yields null (absent a
|
|
693
|
+
* per-relation `where` or a global filter on the target, see
|
|
694
|
+
* `RelationDescriptor`).
|
|
695
|
+
*/
|
|
696
|
+
function isRequiredBelongsTo(table, rel) {
|
|
697
|
+
if (rel.type !== 'belongsTo')
|
|
698
|
+
return false;
|
|
699
|
+
const fks = fkColumns(table, rel);
|
|
700
|
+
return fks.length > 0 && fks.length === normalizeFk(rel).length && fks.every((c) => !c.nullable);
|
|
701
|
+
}
|
|
702
|
+
function normalizeFk(rel) {
|
|
703
|
+
return Array.isArray(rel.foreignKey) ? rel.foreignKey : [rel.foreignKey];
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* The keys a nested create through `rel` (a hasMany / hasOne on `parent`) must
|
|
707
|
+
* NOT require from the child: the child's foreign-key fields, which the
|
|
708
|
+
* runtime fills from the parent row, and the child's matching belongsTo
|
|
709
|
+
* relation, which would point the row at a second parent.
|
|
710
|
+
*/
|
|
711
|
+
function suppliedByParent(schema, rel) {
|
|
712
|
+
if (rel.type !== 'hasMany' && rel.type !== 'hasOne')
|
|
713
|
+
return [];
|
|
714
|
+
const child = Object.hasOwn(schema.tables, rel.to) ? schema.tables[rel.to] : undefined;
|
|
715
|
+
if (!child)
|
|
716
|
+
return [];
|
|
717
|
+
const fks = normalizeFk(rel);
|
|
718
|
+
const keys = fkColumns(child, rel).map((c) => c.field);
|
|
719
|
+
const fkKey = [...fks].sort().join(',');
|
|
720
|
+
for (const [name, back] of Object.entries(child.relations)) {
|
|
721
|
+
if (back.type === 'belongsTo' && back.to === rel.from && [...normalizeFk(back)].sort().join(',') === fkKey) {
|
|
722
|
+
keys.push(name);
|
|
568
723
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
return {
|
|
610
|
-
// The selector NAME is the underscore-join of the member FIELD names.
|
|
611
|
-
// It is normally a valid identifier (`orgId_userId`), but a
|
|
612
|
-
// junction-style column that is not a valid identifier (e.g. a quoted
|
|
613
|
-
// uppercase `"A"` / `"B"`) would join into a broken object key. Emit
|
|
614
|
-
// any non-identifier name as ONE quoted string-literal key so the
|
|
615
|
-
// generated types.ts always parses; the runtime selector map in
|
|
616
|
-
// query/compound-unique.ts registers the same name spelling.
|
|
617
|
-
selectorName: quoteIfNeeded(members.map((m) => m.field).join('_')),
|
|
618
|
-
memberType: `{ ${members.map((m) => `${quoteIfNeeded(m.field)}: ${m.tsType}`).join('; ')} }`,
|
|
619
|
-
};
|
|
620
|
-
});
|
|
621
|
-
// Named helper type for annotating a compound selector by hand (Prisma parity).
|
|
622
|
-
if (selectorEntries.length > 0) {
|
|
623
|
-
const cuFields = selectorEntries.map((e) => `${e.selectorName}: ${e.memberType}`);
|
|
624
|
-
lines.push(`export type ${typeName}CompoundUniques = { ${cuFields.join('; ')} };`);
|
|
625
|
-
}
|
|
626
|
-
const selectorBranches = selectorEntries.map((e) => `{ ${e.selectorName}: ${e.memberType} }`);
|
|
627
|
-
const branches = [...flatBranches, ...selectorBranches];
|
|
628
|
-
lines.push(`export type ${typeName}WhereUnique = ${branches.join(' | ')};`);
|
|
629
|
-
lines.push('');
|
|
724
|
+
}
|
|
725
|
+
return keys;
|
|
726
|
+
}
|
|
727
|
+
const FULL_CREATE_VARIANT = { omitted: new Set(), suffix: '' };
|
|
728
|
+
function variantKey(omitted) {
|
|
729
|
+
return JSON.stringify([...omitted].sort());
|
|
730
|
+
}
|
|
731
|
+
/** A PascalCase identifier fragment for a type name, or `''`. */
|
|
732
|
+
function identifierPart(name) {
|
|
733
|
+
return snakeToPascal(name).replace(/[^A-Za-z0-9_$]/g, '');
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Every create shape each table needs: the full one, plus one per distinct
|
|
737
|
+
* set of parent-supplied keys among the hasMany / hasOne relations pointing at
|
|
738
|
+
* it. Keyed by table name, then by {@link variantKey}.
|
|
739
|
+
*/
|
|
740
|
+
function createVariantsByTable(schema, relationsByTable) {
|
|
741
|
+
const out = new Map();
|
|
742
|
+
for (const table of Object.values(schema.tables)) {
|
|
743
|
+
out.set(table.name, new Map([[variantKey([]), FULL_CREATE_VARIANT]]));
|
|
744
|
+
}
|
|
745
|
+
for (const table of Object.values(schema.tables)) {
|
|
746
|
+
for (const [, rel] of relationsByTable.get(table.name) ?? []) {
|
|
747
|
+
const omitted = suppliedByParent(schema, rel);
|
|
748
|
+
const variants = out.get(rel.to);
|
|
749
|
+
if (omitted.length === 0 || !variants)
|
|
750
|
+
continue;
|
|
751
|
+
const key = variantKey(omitted);
|
|
752
|
+
if (variants.has(key))
|
|
753
|
+
continue;
|
|
754
|
+
const child = schema.tables[rel.to];
|
|
755
|
+
// Name the variant after the child's relation back to the parent when
|
|
756
|
+
// there is one (`PostCreateWithoutAuthorInput`), else after the keys.
|
|
757
|
+
const back = omitted.find((k) => child !== undefined && Object.hasOwn(child.relations, k));
|
|
758
|
+
const base = `Without${identifierPart(back ?? omitted.join('_')) || 'Parent'}`;
|
|
759
|
+
const taken = new Set([...variants.values()].map((v) => v.suffix));
|
|
760
|
+
let suffix = base;
|
|
761
|
+
for (let n = 2; taken.has(suffix); n++)
|
|
762
|
+
suffix = `${base}${n}`;
|
|
763
|
+
variants.set(key, { omitted: new Set(omitted), suffix });
|
|
630
764
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
765
|
+
}
|
|
766
|
+
return out;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Required belongsTo relations beyond this many keep their foreign keys
|
|
770
|
+
* optional on create instead of becoming an either-or union: TypeScript
|
|
771
|
+
* multiplies an intersection of unions out, so N either-or pairs are 2^N
|
|
772
|
+
* object types for every create on the table.
|
|
773
|
+
*/
|
|
774
|
+
const MAX_FK_CHOICES = 4;
|
|
775
|
+
/** `XWhereUnique`: one object per unique key, plus compound selector branches. */
|
|
776
|
+
function emitWhereUnique(lines, table, memberTs) {
|
|
777
|
+
const typeName = entityName(table.name);
|
|
778
|
+
// Union of unique constraint shapes, deduplicating PK
|
|
779
|
+
const seen = new Set();
|
|
780
|
+
const uniqueSets = [];
|
|
781
|
+
const pkKey = table.primaryKey.join(',');
|
|
782
|
+
seen.add(pkKey);
|
|
783
|
+
uniqueSets.push(table.primaryKey);
|
|
784
|
+
for (const uc of table.uniqueColumns) {
|
|
785
|
+
const ucKey = uc.join(',');
|
|
786
|
+
if (!seen.has(ucKey)) {
|
|
787
|
+
seen.add(ucKey);
|
|
788
|
+
uniqueSets.push(uc);
|
|
652
789
|
}
|
|
653
790
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
791
|
+
const memberType = (colName) => {
|
|
792
|
+
const col = table.columns.find((c) => c.name === colName);
|
|
793
|
+
return { field: col?.field ?? colName, tsType: col ? memberTs(col) : 'unknown' };
|
|
794
|
+
};
|
|
795
|
+
// Flat branches: one object per unique constraint carrying its columns.
|
|
796
|
+
const flatBranches = uniqueSets.map((cols) => {
|
|
797
|
+
const fields = cols.map((colName) => {
|
|
798
|
+
const m = memberType(colName);
|
|
799
|
+
return `${quoteIfNeeded(m.field)}: ${m.tsType}`;
|
|
800
|
+
});
|
|
801
|
+
return `{ ${fields.join('; ')} }`;
|
|
802
|
+
});
|
|
803
|
+
// Prisma-style compound-unique SELECTOR branches: a synthetic key
|
|
804
|
+
// (`orgId_userId`) whose value holds the member columns, emitted for every
|
|
805
|
+
// COMPOSITE unique (PK, composite UNIQUE constraint, or composite UNIQUE
|
|
806
|
+
// index). Runtime expansion lives in query/compound-unique.ts.
|
|
807
|
+
const compoundSeen = new Set();
|
|
808
|
+
const compoundSets = [];
|
|
809
|
+
const addCompound = (cols) => {
|
|
810
|
+
if (cols.length < 2)
|
|
811
|
+
return;
|
|
812
|
+
const key = cols.join(',');
|
|
813
|
+
if (compoundSeen.has(key))
|
|
814
|
+
return;
|
|
815
|
+
compoundSeen.add(key);
|
|
816
|
+
compoundSets.push(cols);
|
|
817
|
+
};
|
|
818
|
+
addCompound(table.primaryKey);
|
|
819
|
+
for (const uc of table.uniqueColumns)
|
|
820
|
+
addCompound(uc);
|
|
821
|
+
for (const idx of table.indexes) {
|
|
822
|
+
// A partial unique index does not guarantee table-wide row uniqueness,
|
|
823
|
+
// so it must not become a compound-unique selector (matches the runtime
|
|
824
|
+
// exclusion in query/compound-unique.ts).
|
|
825
|
+
if (idx.unique && !idx.docPath && !idx.partial)
|
|
826
|
+
addCompound(idx.columns);
|
|
827
|
+
}
|
|
828
|
+
const selectorEntries = compoundSets.map((cols) => {
|
|
829
|
+
const members = cols.map(memberType);
|
|
830
|
+
return {
|
|
831
|
+
// The selector NAME is the underscore-join of the member FIELD names.
|
|
832
|
+
// A junction-style column that is not a valid identifier would join
|
|
833
|
+
// into a broken object key, so a non-identifier name is emitted as ONE
|
|
834
|
+
// quoted key; query/compound-unique.ts registers the same spelling.
|
|
835
|
+
selectorName: quoteIfNeeded(members.map((m) => m.field).join('_')),
|
|
836
|
+
memberType: `{ ${members.map((m) => `${quoteIfNeeded(m.field)}: ${m.tsType}`).join('; ')} }`,
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
// Named helper type for annotating a compound selector by hand (Prisma parity).
|
|
840
|
+
if (selectorEntries.length > 0) {
|
|
841
|
+
const cuFields = selectorEntries.map((e) => `${e.selectorName}: ${e.memberType}`);
|
|
842
|
+
lines.push(`export type ${typeName}CompoundUniques = { ${cuFields.join('; ')} };`);
|
|
843
|
+
}
|
|
844
|
+
const selectorBranches = selectorEntries.map((e) => `{ ${e.selectorName}: ${e.memberType} }`);
|
|
845
|
+
const branches = [...flatBranches, ...selectorBranches];
|
|
846
|
+
lines.push(`export type ${typeName}WhereUnique = ${branches.join(' | ')};`);
|
|
847
|
+
lines.push('');
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* The write inputs for one table:
|
|
851
|
+
*
|
|
852
|
+
* - `XCreateInput`: the create data. A required belongsTo is an either-or:
|
|
853
|
+
* give the foreign key, or the relation (`connect` / `create` /
|
|
854
|
+
* `connectOrCreate`).
|
|
855
|
+
* - `XCreateWithout<Relation>Input`: the same data for a row created through
|
|
856
|
+
* a parent's hasMany / hasOne, without the keys that parent supplies (see
|
|
857
|
+
* {@link CreateVariant}).
|
|
858
|
+
* - `XUpdateInput`: update data, scalars plus nested relation writes.
|
|
859
|
+
* - `XNestedCreateInput` / `XNestedUpdateInput` / `XConnectOrCreate` (and
|
|
860
|
+
* their `Without<Relation>` forms): the ops a to-many (or hasOne) relation
|
|
861
|
+
* pointing at X takes.
|
|
862
|
+
* - `XNestedToOneCreateInput` / `XNestedToOneUpdateInput`: the ops a
|
|
863
|
+
* belongsTo relation pointing at X takes. Single objects; `disconnect` and
|
|
864
|
+
* `delete` take `true` for the connected row.
|
|
865
|
+
* - `XInputs`: the map the generated client hands to `QueryInterface`.
|
|
866
|
+
*/
|
|
867
|
+
function emitWriteInputs(lines, schema, table, relations, variantsByTable, writeTs) {
|
|
868
|
+
const typeName = entityName(table.name);
|
|
869
|
+
const relationField = (rel, update) => {
|
|
870
|
+
const target = entityName(rel.to);
|
|
871
|
+
if (rel.type === 'belongsTo')
|
|
872
|
+
return `${target}NestedToOne${update ? 'Update' : 'Create'}Input`;
|
|
873
|
+
const variant = variantsByTable.get(rel.to)?.get(variantKey(suppliedByParent(schema, rel))) ?? FULL_CREATE_VARIANT;
|
|
874
|
+
return `${target}Nested${update ? 'Update' : 'Create'}${variant.suffix}Input`;
|
|
875
|
+
};
|
|
876
|
+
// Required belongsTo relations whose foreign keys are themselves required on
|
|
877
|
+
// create become either-or choices.
|
|
878
|
+
const choices = [];
|
|
879
|
+
const relaxed = [];
|
|
880
|
+
for (const [relName, rel] of relations) {
|
|
881
|
+
if (!isRequiredBelongsTo(table, rel))
|
|
882
|
+
continue;
|
|
883
|
+
const fks = fkColumns(table, rel).filter((c) => !c.isGeneratedStored && !isOptionalOnCreate(c));
|
|
884
|
+
if (fks.length === 0)
|
|
885
|
+
continue;
|
|
886
|
+
if (choices.length < MAX_FK_CHOICES)
|
|
887
|
+
choices.push({ relName, rel, fks });
|
|
888
|
+
else
|
|
889
|
+
relaxed.push(...fks);
|
|
890
|
+
}
|
|
891
|
+
const choiceFields = new Set(choices.flatMap((c) => c.fks).map((c) => c.field));
|
|
892
|
+
const relaxedFields = new Set(relaxed.map((c) => c.field));
|
|
893
|
+
const where = `${typeName}WhereUnique`;
|
|
894
|
+
const filter = `${typeName}WhereUnique | Partial<${typeName}>`;
|
|
895
|
+
const variants = [...(variantsByTable.get(table.name)?.values() ?? [FULL_CREATE_VARIANT])];
|
|
896
|
+
for (const { omitted, suffix } of variants) {
|
|
897
|
+
// --- CreateInput (one concrete type per variant) ---
|
|
898
|
+
const members = [];
|
|
899
|
+
for (const col of table.columns) {
|
|
900
|
+
if (col.isGeneratedStored || choiceFields.has(col.field) || omitted.has(col.field))
|
|
901
|
+
continue;
|
|
902
|
+
const optional = isOptionalOnCreate(col) || relaxedFields.has(col.field);
|
|
903
|
+
members.push(optional
|
|
904
|
+
? `${quoteIfNeeded(col.field)}?: ${writeTs(col)} | undefined`
|
|
905
|
+
: `${quoteIfNeeded(col.field)}: ${writeTs(col)}`);
|
|
906
|
+
}
|
|
907
|
+
for (const [relName, rel] of relations) {
|
|
908
|
+
if (omitted.has(relName))
|
|
909
|
+
continue;
|
|
910
|
+
members.push(`${quoteIfNeeded(relName)}?: ${relationField(rel, false)} | undefined`);
|
|
911
|
+
}
|
|
912
|
+
const parts = [members.length > 0 ? `{ ${members.join('; ')} }` : 'Record<string, never>'];
|
|
913
|
+
for (const { relName, rel, fks } of choices) {
|
|
914
|
+
if (omitted.has(relName) || fks.every((c) => omitted.has(c.field)))
|
|
915
|
+
continue;
|
|
916
|
+
const viaKey = fks.map((c) => `${quoteIfNeeded(c.field)}: ${writeTs(c)}`).join('; ');
|
|
917
|
+
const viaRelation = [
|
|
918
|
+
...fks.map((c) => `${quoteIfNeeded(c.field)}?: ${writeTs(c)} | undefined`),
|
|
919
|
+
`${quoteIfNeeded(relName)}: ${relationField(rel, false)}`,
|
|
920
|
+
].join('; ');
|
|
921
|
+
parts.push(`({ ${viaKey} } | { ${viaRelation} })`);
|
|
922
|
+
}
|
|
923
|
+
const supplied = [...omitted].map((k) => `\`${docSafe(k)}\``).join(', ');
|
|
924
|
+
lines.push(suffix === ''
|
|
925
|
+
? `/** Data for creating a \`${docSafe(table.name)}\` row */`
|
|
926
|
+
: `/** Data for creating a \`${docSafe(table.name)}\` row through a parent relation, which supplies ${supplied} */`);
|
|
927
|
+
lines.push(`export type ${typeName}Create${suffix}Input = ${parts.join(' & ')};`);
|
|
928
|
+
lines.push('');
|
|
929
|
+
// --- Ops a to-many (or hasOne) relation pointing AT this table takes ---
|
|
930
|
+
const create = `${typeName}Create${suffix}Input`;
|
|
931
|
+
const connectOrCreate = `${typeName}ConnectOrCreate${suffix}`;
|
|
932
|
+
lines.push(`export interface ${typeName}NestedCreate${suffix}Input {`);
|
|
933
|
+
lines.push(` create?: ${create} | ${create}[] | undefined;`);
|
|
934
|
+
lines.push(` connect?: ${where} | ${where}[] | undefined;`);
|
|
935
|
+
lines.push(` connectOrCreate?: ${connectOrCreate} | ${connectOrCreate}[] | undefined;`);
|
|
665
936
|
lines.push('}');
|
|
666
937
|
lines.push('');
|
|
667
|
-
lines.push(`export interface ${typeName}
|
|
668
|
-
lines.push(`
|
|
669
|
-
lines.push(`
|
|
670
|
-
lines.push(`
|
|
671
|
-
|
|
672
|
-
lines.push(`
|
|
673
|
-
|
|
938
|
+
lines.push(`export interface ${typeName}NestedUpdate${suffix}Input extends ${typeName}NestedCreate${suffix}Input {`);
|
|
939
|
+
lines.push(` disconnect?: ${filter} | (${filter})[] | undefined;`);
|
|
940
|
+
lines.push(` set?: ${where}[] | undefined;`);
|
|
941
|
+
lines.push(` delete?: ${filter} | (${filter})[] | undefined;`);
|
|
942
|
+
const updateItem = `{ where: ${filter}; data: ${typeName}UpdateInput }`;
|
|
943
|
+
lines.push(` update?: ${updateItem} | ${updateItem}[] | undefined;`);
|
|
944
|
+
const upsertItem = `{ where: ${filter}; create: ${create}; update: ${typeName}UpdateInput }`;
|
|
945
|
+
lines.push(` upsert?: ${upsertItem} | ${upsertItem}[] | undefined;`);
|
|
674
946
|
lines.push('}');
|
|
675
947
|
lines.push('');
|
|
676
|
-
lines.push(`export interface ${
|
|
677
|
-
lines.push(` where: ${
|
|
678
|
-
lines.push(` create: ${
|
|
948
|
+
lines.push(`export interface ${connectOrCreate} {`);
|
|
949
|
+
lines.push(` where: ${where};`);
|
|
950
|
+
lines.push(` create: ${create};`);
|
|
679
951
|
lines.push('}');
|
|
680
952
|
lines.push('');
|
|
681
953
|
}
|
|
682
|
-
|
|
954
|
+
// --- UpdateInput ---
|
|
955
|
+
if (relations.length > 0) {
|
|
956
|
+
lines.push(`export type ${typeName}UpdateInput = ${typeName}Update & {`);
|
|
957
|
+
for (const [relName, rel] of relations) {
|
|
958
|
+
lines.push(` ${quoteIfNeeded(relName)}?: ${relationField(rel, true)} | undefined;`);
|
|
959
|
+
}
|
|
960
|
+
lines.push('};');
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
lines.push(`export type ${typeName}UpdateInput = ${typeName}Update;`);
|
|
964
|
+
}
|
|
965
|
+
lines.push('');
|
|
966
|
+
// A belongsTo points at ONE row: single objects, no `set`, and `true` (or
|
|
967
|
+
// `{}`) for "the connected row" on disconnect / delete.
|
|
968
|
+
const connected = `boolean | Record<string, never> | ${where}`;
|
|
969
|
+
lines.push(`export interface ${typeName}NestedToOneCreateInput {`);
|
|
970
|
+
lines.push(` create?: ${typeName}CreateInput | undefined;`);
|
|
971
|
+
lines.push(` connect?: ${where} | undefined;`);
|
|
972
|
+
lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | undefined;`);
|
|
973
|
+
lines.push('}');
|
|
974
|
+
lines.push('');
|
|
975
|
+
lines.push(`export interface ${typeName}NestedToOneUpdateInput extends ${typeName}NestedToOneCreateInput {`);
|
|
976
|
+
lines.push(` disconnect?: ${connected} | undefined;`);
|
|
977
|
+
lines.push(` delete?: ${connected} | undefined;`);
|
|
978
|
+
lines.push(` update?: { data: ${typeName}UpdateInput } | undefined;`);
|
|
979
|
+
lines.push(` upsert?: { where: ${filter}; create: ${typeName}CreateInput; update: ${typeName}UpdateInput } | undefined;`);
|
|
980
|
+
lines.push('}');
|
|
981
|
+
lines.push('');
|
|
982
|
+
// --- The accessor's input map ---
|
|
983
|
+
lines.push(`/** Write and lookup input types for the \`${docSafe(table.name)}\` client accessor */`);
|
|
984
|
+
lines.push(`export interface ${typeName}Inputs {`);
|
|
985
|
+
lines.push(` create: ${typeName}CreateInput;`);
|
|
986
|
+
lines.push(` createMany: ${typeName}Create;`);
|
|
987
|
+
lines.push(` update: ${typeName}UpdateInput;`);
|
|
988
|
+
lines.push(` updateMany: ${typeName}Update;`);
|
|
989
|
+
lines.push(` whereUnique: ${where};`);
|
|
990
|
+
lines.push('}');
|
|
991
|
+
lines.push('');
|
|
683
992
|
}
|
|
684
993
|
// ---------------------------------------------------------------------------
|
|
685
994
|
// zod.ts generator (H1, `turbine generate --zod`)
|
|
@@ -707,6 +1016,19 @@ function zodScalar(ts) {
|
|
|
707
1016
|
case 'number[]':
|
|
708
1017
|
// pgvector, `pgTypeToTs('vector')` yields `number[]`.
|
|
709
1018
|
return 'z.array(z.number())';
|
|
1019
|
+
case 'number | string | bigint':
|
|
1020
|
+
// int8 write input.
|
|
1021
|
+
return 'z.union([z.number(), z.string(), z.bigint()])';
|
|
1022
|
+
case 'string | number':
|
|
1023
|
+
// numeric write input.
|
|
1024
|
+
return 'z.union([z.string(), z.number()])';
|
|
1025
|
+
case '{ x: number; y: number }':
|
|
1026
|
+
return 'z.object({ x: z.number(), y: z.number() })';
|
|
1027
|
+
case '{ x: number; y: number; radius: number }':
|
|
1028
|
+
return 'z.object({ x: z.number(), y: z.number(), radius: z.number() })';
|
|
1029
|
+
case '{ years?: number; months?: number; days?: number; hours?: number; minutes?: number; seconds?: number; milliseconds?: number }':
|
|
1030
|
+
// interval: the driver's parsed object, zero fields absent.
|
|
1031
|
+
return `z.object({ ${['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'].map((f) => `${f}: z.number().optional()`).join(', ')} })`;
|
|
710
1032
|
default:
|
|
711
1033
|
// json/jsonb and any unmapped user-defined type.
|
|
712
1034
|
return 'z.unknown()';
|
|
@@ -735,7 +1057,14 @@ function zodBaseType(col, enums, forWrite = false) {
|
|
|
735
1057
|
expr = 'z.union([z.string(), z.date()])';
|
|
736
1058
|
}
|
|
737
1059
|
else {
|
|
738
|
-
|
|
1060
|
+
// The read (or write) map decides the element type, which for an array
|
|
1061
|
+
// is not always the scalar's (`bigint[]` reads as strings).
|
|
1062
|
+
const full = (forWrite ? pgTypeToTsInput : pgTypeToTs)(isArray ? `_${base}` : base, false);
|
|
1063
|
+
const element = isArray && full.endsWith('[]') ? full.slice(0, -2).replace(/^\((.*)\)$/, '$1') : full;
|
|
1064
|
+
expr = zodScalar(element);
|
|
1065
|
+
if (isArray)
|
|
1066
|
+
expr += '.array()';
|
|
1067
|
+
return expr;
|
|
739
1068
|
}
|
|
740
1069
|
if (isArray)
|
|
741
1070
|
expr += '.array()';
|
|
@@ -764,7 +1093,7 @@ export function generateZod(schema, options) {
|
|
|
764
1093
|
let expr = zodBaseType(col, schema.enums);
|
|
765
1094
|
if (col.nullable)
|
|
766
1095
|
expr += '.nullable()';
|
|
767
|
-
lines.push(` ${
|
|
1096
|
+
lines.push(` ${objectKey(col.field)}: ${expr},`);
|
|
768
1097
|
}
|
|
769
1098
|
lines.push('});');
|
|
770
1099
|
lines.push('');
|
|
@@ -781,7 +1110,7 @@ export function generateZod(schema, options) {
|
|
|
781
1110
|
expr += '.nullable()';
|
|
782
1111
|
if (col.isGenerated === true || col.hasDefault || col.nullable)
|
|
783
1112
|
expr += '.optional()';
|
|
784
|
-
lines.push(` ${
|
|
1113
|
+
lines.push(` ${objectKey(col.field)}: ${expr},`);
|
|
785
1114
|
}
|
|
786
1115
|
lines.push('});');
|
|
787
1116
|
lines.push('');
|
|
@@ -797,7 +1126,7 @@ export function generateZod(schema, options) {
|
|
|
797
1126
|
if (col.nullable)
|
|
798
1127
|
expr += '.nullable()';
|
|
799
1128
|
expr += '.optional()';
|
|
800
|
-
lines.push(` ${
|
|
1129
|
+
lines.push(` ${objectKey(col.field)}: ${expr},`);
|
|
801
1130
|
}
|
|
802
1131
|
lines.push('});');
|
|
803
1132
|
lines.push('');
|
|
@@ -821,7 +1150,7 @@ export function generateMetadata(schema, options) {
|
|
|
821
1150
|
' tables: {',
|
|
822
1151
|
];
|
|
823
1152
|
for (const table of Object.values(schema.tables)) {
|
|
824
|
-
lines.push(` ${
|
|
1153
|
+
lines.push(` ${objectKey(table.name)}: {`);
|
|
825
1154
|
lines.push(` name: '${escSQ(table.name)}',`);
|
|
826
1155
|
// columns
|
|
827
1156
|
lines.push(' columns: [');
|
|
@@ -832,13 +1161,13 @@ export function generateMetadata(schema, options) {
|
|
|
832
1161
|
// columnMap
|
|
833
1162
|
lines.push(' columnMap: {');
|
|
834
1163
|
for (const [field, col] of Object.entries(table.columnMap)) {
|
|
835
|
-
lines.push(` ${
|
|
1164
|
+
lines.push(` ${objectKey(field)}: '${escSQ(col)}',`);
|
|
836
1165
|
}
|
|
837
1166
|
lines.push(' },');
|
|
838
1167
|
// reverseColumnMap
|
|
839
1168
|
lines.push(' reverseColumnMap: {');
|
|
840
1169
|
for (const [col, field] of Object.entries(table.reverseColumnMap)) {
|
|
841
|
-
lines.push(` ${
|
|
1170
|
+
lines.push(` ${objectKey(col)}: '${escSQ(field)}',`);
|
|
842
1171
|
}
|
|
843
1172
|
lines.push(' },');
|
|
844
1173
|
// dateColumns
|
|
@@ -848,12 +1177,12 @@ export function generateMetadata(schema, options) {
|
|
|
848
1177
|
const dialectTypes = table.dialectTypes ?? table.pgTypes;
|
|
849
1178
|
lines.push(' dialectTypes: {');
|
|
850
1179
|
for (const [col, dialectType] of Object.entries(dialectTypes)) {
|
|
851
|
-
lines.push(` ${
|
|
1180
|
+
lines.push(` ${objectKey(col)}: '${escSQ(dialectType)}',`);
|
|
852
1181
|
}
|
|
853
1182
|
lines.push(' },');
|
|
854
1183
|
lines.push(' pgTypes: {');
|
|
855
1184
|
for (const [col, pgType] of Object.entries(table.pgTypes)) {
|
|
856
|
-
lines.push(` ${
|
|
1185
|
+
lines.push(` ${objectKey(col)}: '${escSQ(pgType)}',`);
|
|
857
1186
|
}
|
|
858
1187
|
lines.push(' },');
|
|
859
1188
|
// allColumns
|
|
@@ -882,7 +1211,7 @@ export function generateMetadata(schema, options) {
|
|
|
882
1211
|
`sourceKey: ${keyLiteral(rel.through.sourceKey)}, ` +
|
|
883
1212
|
`targetKey: ${keyLiteral(rel.through.targetKey)} }`;
|
|
884
1213
|
}
|
|
885
|
-
lines.push(` ${
|
|
1214
|
+
lines.push(` ${objectKey(relName)}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral}${throughLiteral} },`);
|
|
886
1215
|
}
|
|
887
1216
|
lines.push(' },');
|
|
888
1217
|
// indexes
|
|
@@ -915,7 +1244,7 @@ export function generateMetadata(schema, options) {
|
|
|
915
1244
|
// enums
|
|
916
1245
|
lines.push(' enums: {');
|
|
917
1246
|
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
918
|
-
lines.push(` ${
|
|
1247
|
+
lines.push(` ${objectKey(enumName)}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
|
|
919
1248
|
}
|
|
920
1249
|
lines.push(' },');
|
|
921
1250
|
lines.push('};');
|
|
@@ -946,7 +1275,7 @@ export function generateIndex(schema, options) {
|
|
|
946
1275
|
// drove it, so direct generateIndex() callers stay byte-identical).
|
|
947
1276
|
...(options?.importMode ? [`// Sibling imports resolved with importExtension: ${options.importMode}.`, ''] : []),
|
|
948
1277
|
"import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
|
|
949
|
-
"import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults } from 'turbine-orm';",
|
|
1278
|
+
"import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults, WhereClause } from 'turbine-orm';",
|
|
950
1279
|
`import { SCHEMA } from './metadata${ext}';`,
|
|
951
1280
|
];
|
|
952
1281
|
// Import all entity types and relations maps
|
|
@@ -956,9 +1285,36 @@ export function generateIndex(schema, options) {
|
|
|
956
1285
|
if (hasSafeRelations.get(t.name)) {
|
|
957
1286
|
typeImports.push(`${entityName(t.name)}Relations`);
|
|
958
1287
|
}
|
|
1288
|
+
typeImports.push(`${entityName(t.name)}Inputs`);
|
|
959
1289
|
}
|
|
960
1290
|
lines.push(`import type { ${typeImports.join(', ')} } from './types${ext}';`);
|
|
961
1291
|
lines.push('');
|
|
1292
|
+
// Global filters keyed by table, each checked against its own row type, so a
|
|
1293
|
+
// misspelled table or column is a compile error. A table is keyed by its
|
|
1294
|
+
// client accessor, and also by its database name when the two differ, the
|
|
1295
|
+
// two spellings the runtime accepts.
|
|
1296
|
+
lines.push('/** `globalFilters` for this schema: a where clause (or a function returning one) per table. */');
|
|
1297
|
+
lines.push('export interface TurbineGlobalFilters {');
|
|
1298
|
+
for (const table of tableEntries) {
|
|
1299
|
+
const typeName = entityName(table.name);
|
|
1300
|
+
const where = `WhereClause<${typeName}${hasSafeRelations.get(table.name) ? `, ${typeName}Relations` : ''}>`;
|
|
1301
|
+
// `null` (or a function returning it) applies no filter on purpose.
|
|
1302
|
+
const filter = `${where} | null | (() => ${where} | null)`;
|
|
1303
|
+
const accessor = snakeToCamelStr(table.name);
|
|
1304
|
+
lines.push(` ${quoteIfNeeded(accessor)}?: ${filter} | undefined;`);
|
|
1305
|
+
if (accessor !== table.name)
|
|
1306
|
+
lines.push(` ${quoteIfNeeded(table.name)}?: ${filter} | undefined;`);
|
|
1307
|
+
}
|
|
1308
|
+
lines.push('}');
|
|
1309
|
+
lines.push('');
|
|
1310
|
+
// Each field also admits an explicit `undefined` (read as unset), so
|
|
1311
|
+
// `turbine({ connectionString: process.env.DATABASE_URL })` compiles under
|
|
1312
|
+
// exactOptionalPropertyTypes.
|
|
1313
|
+
lines.push('/** Client configuration with `globalFilters` typed against this schema. */');
|
|
1314
|
+
lines.push('export type TypedTurbineConfig = {');
|
|
1315
|
+
lines.push(" [K in Exclude<keyof TurbineConfig, 'globalFilters'>]?: TurbineConfig[K] | undefined;");
|
|
1316
|
+
lines.push('} & { globalFilters?: TurbineGlobalFilters | undefined };');
|
|
1317
|
+
lines.push('');
|
|
962
1318
|
// -------------------------------------------------------------------------
|
|
963
1319
|
// TypedTransactionClient, same typed table accessors as TurbineClient,
|
|
964
1320
|
// but scoped to a single transaction connection. The runtime instance is
|
|
@@ -976,22 +1332,23 @@ export function generateIndex(schema, options) {
|
|
|
976
1332
|
const typeName = entityName(table.name);
|
|
977
1333
|
const accessor = snakeToCamelStr(table.name);
|
|
978
1334
|
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
979
|
-
const genericArgs =
|
|
1335
|
+
const genericArgs = `${typeName}, ${hasRelations ? `${typeName}Relations` : '{}'}, ${typeName}Inputs`;
|
|
980
1336
|
lines.push(` /** Query interface for the \`${docSafe(table.name)}\` table (transaction-scoped) */`);
|
|
981
1337
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
982
1338
|
}
|
|
983
|
-
|
|
1339
|
+
// The nested `$transaction` (SAVEPOINT) hands its callback a typed client.
|
|
1340
|
+
// Declared as a real overriding method rather than through a same-named
|
|
1341
|
+
// interface (declaration merging), which strict lint configs reject
|
|
1342
|
+
// (no-unsafe-declaration-merging). It only narrows the callback's type:
|
|
1343
|
+
// the body is the base method.
|
|
984
1344
|
lines.push('');
|
|
985
|
-
// Augment the class with a typed `$transaction` overload via interface
|
|
986
|
-
// merging. This adds an additional callable signature whose callback
|
|
987
|
-
// parameter is narrowed to `TypedTransactionClient`, while the base
|
|
988
|
-
// signature (callback parameter `BaseTransactionClient`) remains valid.
|
|
989
|
-
lines.push('export interface TypedTransactionClient {');
|
|
990
1345
|
lines.push(' /**');
|
|
991
1346
|
lines.push(' * Nested transaction via SAVEPOINT. The callback receives a typed');
|
|
992
1347
|
lines.push(' * `TypedTransactionClient` so all table accessors auto-complete.');
|
|
993
1348
|
lines.push(' */');
|
|
994
|
-
lines.push(' $transaction<R>(fn: (tx: TypedTransactionClient) => Promise<R>): Promise<R
|
|
1349
|
+
lines.push(' override $transaction<R>(fn: (tx: TypedTransactionClient) => Promise<R>): Promise<R> {');
|
|
1350
|
+
lines.push(' return super.$transaction(fn as (tx: BaseTransactionClient) => Promise<R>);');
|
|
1351
|
+
lines.push(' }');
|
|
995
1352
|
lines.push('}');
|
|
996
1353
|
lines.push('');
|
|
997
1354
|
// Generate the client class with JSDoc
|
|
@@ -1018,37 +1375,30 @@ export function generateIndex(schema, options) {
|
|
|
1018
1375
|
const typeName = entityName(table.name);
|
|
1019
1376
|
const accessor = snakeToCamelStr(table.name);
|
|
1020
1377
|
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
1021
|
-
const genericArgs =
|
|
1378
|
+
const genericArgs = `${typeName}, ${hasRelations ? `${typeName}Relations` : '{}'}, ${typeName}Inputs`;
|
|
1022
1379
|
lines.push(` /** Query interface for the \`${docSafe(table.name)}\` table */`);
|
|
1023
1380
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
1024
1381
|
}
|
|
1025
1382
|
lines.push('');
|
|
1026
|
-
lines.push(' constructor(config?:
|
|
1027
|
-
lines.push(' super(config, SCHEMA);');
|
|
1383
|
+
lines.push(' constructor(config?: TypedTurbineConfig) {');
|
|
1384
|
+
lines.push(' super(config as TurbineConfig | undefined, SCHEMA);');
|
|
1028
1385
|
lines.push(' }');
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
// `tx.posts`, etc. $withSession is the RLS shorthand for
|
|
1035
|
-
// `$transaction(fn, { sessionContext })`; it used to be left out, so its
|
|
1036
|
-
// callback stayed the untyped base `TransactionClient` and the documented
|
|
1037
|
-
// `tx.<table>.findMany()` did not compile on a generated client.
|
|
1386
|
+
// Typed `$transaction` / `$withSession`: the callback parameter is narrowed
|
|
1387
|
+
// to `TypedTransactionClient`, so `tx.users` etc. autocomplete. Real
|
|
1388
|
+
// overriding methods whose bodies are the base methods, NOT a same-named
|
|
1389
|
+
// interface merged onto the class: strict lint configs reject that merge
|
|
1390
|
+
// (no-unsafe-declaration-merging), and generated code must pass them.
|
|
1038
1391
|
//
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1044
|
-
// $withSession overload mirrors the base parameter list exactly for the same
|
|
1045
|
-
// reason; src/test/generate-typecheck.test.ts compiles the result.
|
|
1046
|
-
lines.push('export interface TurbineClient {');
|
|
1392
|
+
// Each overload must stay compatible with the base class's (TS2416): the
|
|
1393
|
+
// base `$transaction` has a callback form AND a batch-array form, so both
|
|
1394
|
+
// are redeclared, and `$withSession` mirrors the base parameter list.
|
|
1395
|
+
// src/test/generate-typecheck.test.ts compiles the result.
|
|
1396
|
+
lines.push('');
|
|
1047
1397
|
lines.push(' /**');
|
|
1048
1398
|
lines.push(' * Run a callback inside a transaction. The callback receives a typed');
|
|
1049
1399
|
lines.push(' * `TypedTransactionClient` with autocompletion for every table accessor.');
|
|
1050
1400
|
lines.push(' */');
|
|
1051
|
-
lines.push(' $transaction<R>(');
|
|
1401
|
+
lines.push(' override $transaction<R>(');
|
|
1052
1402
|
lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
|
|
1053
1403
|
lines.push(' options?: TransactionOptions,');
|
|
1054
1404
|
lines.push(' ): Promise<R>;');
|
|
@@ -1056,18 +1406,28 @@ export function generateIndex(schema, options) {
|
|
|
1056
1406
|
lines.push(' * Batch form: run several deferred queries in one transaction and get');
|
|
1057
1407
|
lines.push(' * their results as a tuple (same as the base client).');
|
|
1058
1408
|
lines.push(' */');
|
|
1059
|
-
lines.push(' $transaction<T extends readonly DeferredQuery<unknown>[]>(');
|
|
1409
|
+
lines.push(' override $transaction<T extends readonly DeferredQuery<unknown>[]>(');
|
|
1060
1410
|
lines.push(' queries: readonly [...T],');
|
|
1411
|
+
lines.push(' options?: TransactionOptions,');
|
|
1061
1412
|
lines.push(' ): Promise<PipelineResults<T>>;');
|
|
1413
|
+
lines.push(' override $transaction(');
|
|
1414
|
+
lines.push(' fnOrQueries: ((tx: TypedTransactionClient) => Promise<unknown>) | readonly DeferredQuery<unknown>[],');
|
|
1415
|
+
lines.push(' options?: TransactionOptions,');
|
|
1416
|
+
lines.push(' ): Promise<unknown> {');
|
|
1417
|
+
lines.push(" return super.$transaction(fnOrQueries as Parameters<BaseTurbineClient['$transaction']>[0], options);");
|
|
1418
|
+
lines.push(' }');
|
|
1419
|
+
lines.push('');
|
|
1062
1420
|
lines.push(' /**');
|
|
1063
1421
|
lines.push(' * Run a callback inside a transaction with the given session GUCs applied');
|
|
1064
1422
|
lines.push(' * via `set_config(..., true)` (the RLS / multi-tenant shorthand). The');
|
|
1065
1423
|
lines.push(' * callback receives a typed `TypedTransactionClient`, same as `$transaction`.');
|
|
1066
1424
|
lines.push(' */');
|
|
1067
|
-
lines.push(' $withSession<R>(');
|
|
1425
|
+
lines.push(' override $withSession<R>(');
|
|
1068
1426
|
lines.push(' context: Record<string, string | number | boolean>,');
|
|
1069
1427
|
lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
|
|
1070
|
-
lines.push(' ): Promise<R
|
|
1428
|
+
lines.push(' ): Promise<R> {');
|
|
1429
|
+
lines.push(' return super.$withSession(context, fn as (tx: BaseTransactionClient) => Promise<R>);');
|
|
1430
|
+
lines.push(' }');
|
|
1071
1431
|
lines.push('}');
|
|
1072
1432
|
lines.push('');
|
|
1073
1433
|
// Factory function with JSDoc
|
|
@@ -1078,7 +1438,7 @@ export function generateIndex(schema, options) {
|
|
|
1078
1438
|
lines.push(' * fields) to fall back to the `DATABASE_URL` environment variable.');
|
|
1079
1439
|
lines.push(' * @returns A fully-typed TurbineClient with table accessors.');
|
|
1080
1440
|
lines.push(' */');
|
|
1081
|
-
lines.push('export function turbine(config?:
|
|
1441
|
+
lines.push('export function turbine(config?: TypedTurbineConfig): TurbineClient {');
|
|
1082
1442
|
lines.push(' return new TurbineClient(config);');
|
|
1083
1443
|
lines.push('}');
|
|
1084
1444
|
lines.push('');
|
|
@@ -1115,13 +1475,13 @@ export function generatePrismaMap(map, options) {
|
|
|
1115
1475
|
' models: {',
|
|
1116
1476
|
];
|
|
1117
1477
|
for (const [modelName, model] of Object.entries(map.models)) {
|
|
1118
|
-
lines.push(` ${
|
|
1478
|
+
lines.push(` ${objectKey(modelName)}: {`);
|
|
1119
1479
|
lines.push(` table: '${escSQ(model.table)}',`);
|
|
1120
1480
|
lines.push(` accessor: '${escSQ(model.accessor)}',`);
|
|
1121
1481
|
lines.push(` fields: ${serializeStringRecord(model.fields)},`);
|
|
1122
1482
|
lines.push(' relations: {');
|
|
1123
1483
|
for (const [rel, r] of Object.entries(model.relations)) {
|
|
1124
|
-
lines.push(` ${
|
|
1484
|
+
lines.push(` ${objectKey(rel)}: { name: '${escSQ(r.name)}', cardinality: '${r.cardinality}' },`);
|
|
1125
1485
|
}
|
|
1126
1486
|
lines.push(' },');
|
|
1127
1487
|
lines.push(` compoundUniques: ${serializeStringArrayRecord(model.compoundUniques)},`);
|
|
@@ -1146,14 +1506,14 @@ function serializeStringRecord(rec) {
|
|
|
1146
1506
|
const entries = Object.entries(rec);
|
|
1147
1507
|
if (entries.length === 0)
|
|
1148
1508
|
return '{}';
|
|
1149
|
-
return `{ ${entries.map(([k, v]) => `${
|
|
1509
|
+
return `{ ${entries.map(([k, v]) => `${objectKey(k)}: '${escSQ(v)}'`).join(', ')} }`;
|
|
1150
1510
|
}
|
|
1151
1511
|
/** Serialize a `Record<string, string[]>` as an inline object literal. */
|
|
1152
1512
|
function serializeStringArrayRecord(rec) {
|
|
1153
1513
|
const entries = Object.entries(rec);
|
|
1154
1514
|
if (entries.length === 0)
|
|
1155
1515
|
return '{}';
|
|
1156
|
-
return `{ ${entries.map(([k, v]) => `${
|
|
1516
|
+
return `{ ${entries.map(([k, v]) => `${objectKey(k)}: [${v.map((s) => `'${escSQ(s)}'`).join(', ')}]`).join(', ')} }`;
|
|
1157
1517
|
}
|
|
1158
1518
|
// ---------------------------------------------------------------------------
|
|
1159
1519
|
// Helpers
|
|
@@ -1207,6 +1567,11 @@ function serializeColumn(col) {
|
|
|
1207
1567
|
parts.push(`updatedAt: true`);
|
|
1208
1568
|
if (col.maxLength !== undefined)
|
|
1209
1569
|
parts.push(`maxLength: ${col.maxLength}`);
|
|
1570
|
+
// The default expression, so an engine without column defaults (PowDB) can
|
|
1571
|
+
// apply it on create. Arbitrary SQL text, so written as a JSON string
|
|
1572
|
+
// literal, which escapes every quote, backslash and line break.
|
|
1573
|
+
if (col.defaultValue != null)
|
|
1574
|
+
parts.push(`defaultValue: ${JSON.stringify(col.defaultValue)}`);
|
|
1210
1575
|
return `{ ${parts.join(', ')} }`;
|
|
1211
1576
|
}
|
|
1212
1577
|
/**
|
|
@@ -1231,44 +1596,33 @@ const BARE_KEY_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
|
1231
1596
|
* return `'${s}'` verbatim, so a name containing a quote closed the key and
|
|
1232
1597
|
* reopened in expression position.
|
|
1233
1598
|
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
* adding a property, because the object-literal special case applies to the
|
|
1237
|
-
* quoted spelling too. Quoting is not a fix for it, so there is no branch for
|
|
1238
|
-
* it; the consequence is a metadata map that silently omits that one entry, not
|
|
1239
|
-
* code execution.
|
|
1599
|
+
* A name of `__proto__` is safe in an interface but not in an object literal;
|
|
1600
|
+
* see {@link objectKey}.
|
|
1240
1601
|
*/
|
|
1241
1602
|
function quoteIfNeeded(s) {
|
|
1242
1603
|
return BARE_KEY_RE.test(s) ? s : `'${escSQ(s)}'`;
|
|
1243
1604
|
}
|
|
1605
|
+
/**
|
|
1606
|
+
* {@link quoteIfNeeded} for a key in an OBJECT LITERAL that is evaluated at
|
|
1607
|
+
* runtime (metadata.ts, the Prisma map). `{ '__proto__': v }` sets the literal's
|
|
1608
|
+
* prototype instead of defining a property, quoted or not, so that one name is
|
|
1609
|
+
* emitted as a computed key, whose string literal is just as inert and which does
|
|
1610
|
+
* define the property. Interface members (types.ts) have no such rule and keep
|
|
1611
|
+
* using {@link quoteIfNeeded}.
|
|
1612
|
+
*/
|
|
1613
|
+
function objectKey(s) {
|
|
1614
|
+
return s === '__proto__' ? "['__proto__']" : quoteIfNeeded(s);
|
|
1615
|
+
}
|
|
1244
1616
|
function snakeToCamelStr(s) {
|
|
1245
1617
|
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
1246
1618
|
}
|
|
1247
1619
|
/**
|
|
1248
|
-
* Build the update-input field type for a column
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1253
|
-
* `'number'` (e.g. `number`, `number | null`), it's eligible. Other
|
|
1254
|
-
* scalar types (`string`, `boolean`, `Date`, `unknown`, `Buffer`,
|
|
1255
|
-
* `Date | null`, etc.) pass through unchanged.
|
|
1620
|
+
* Build the update-input field type for a column: the plain value, the
|
|
1621
|
+
* {@link UpdateOperatorInput} object (`{ set }` on every column, plus
|
|
1622
|
+
* `increment` / `decrement` / `multiply` / `divide` when the type includes
|
|
1623
|
+
* `number`), an `expr.*` update expression or a `col()` reference, matching
|
|
1624
|
+
* what `update()` accepts at runtime and the core `UpdateData` type.
|
|
1256
1625
|
*/
|
|
1257
1626
|
function updateFieldType(tsType) {
|
|
1258
|
-
|
|
1259
|
-
if (containsNumberType(tsType)) {
|
|
1260
|
-
return `${tsType} | UpdateOperatorInput<number>`;
|
|
1261
|
-
}
|
|
1262
|
-
return tsType;
|
|
1263
|
-
}
|
|
1264
|
-
/**
|
|
1265
|
-
* Detect whether a TypeScript type expression contains the `number` primitive
|
|
1266
|
-
* as a top-level union member. Conservative on purpose, only matches
|
|
1267
|
-
* `number`, `number | null`, `null | number`, etc., not `number[]` or
|
|
1268
|
-
* `Record<string, number>`.
|
|
1269
|
-
*/
|
|
1270
|
-
function containsNumberType(tsType) {
|
|
1271
|
-
// Tokenize on `|` and check each member.
|
|
1272
|
-
const parts = tsType.split('|').map((p) => p.trim());
|
|
1273
|
-
return parts.some((p) => p === 'number');
|
|
1627
|
+
return `${tsType} | UpdateOperatorInput<${tsType}> | SqlExpr<${tsType}> | ColumnRef`;
|
|
1274
1628
|
}
|