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.
Files changed (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. 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 { pgTypeToTs, singularize, snakeToPascal, timeOfDayKind, withDbFieldNames, } from './schema.js';
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. Falls back to
24
- * the introspected `col.tsType` for every non-enum column.
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), which is wider than the row type for time-of-day
41
- * columns.
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`. Consumers porting from Prisma pass Dates, so the input
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 (!timeOfDayKind(base))
67
+ if (Object.hasOwn(enums, base))
56
68
  return tsType;
57
- // A `time[]` column accepts a Date per element (the bind rewrite runs
58
- // element-wise, see coerceTemporalValue in query/utils.ts).
59
- const widened = isArray ? '(string | Date)[]' : 'string | Date';
60
- return col.nullable ? `${widened} | null` : widened;
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 = { noTimestamp: options.noTimestamp, importExt, importMode };
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, warn = true) {
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 (warn) {
330
- console.warn(`[turbine] Relation "${relName}" on table "${table.name}" shadows a column field of the same name, ` +
331
- `omitting it from the generated types. Rename the relation (or the column) to expose it.`);
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
- * and *Relations brand-field interfaces). Exported so tests can pin the
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(schema, options) {
412
- assertEmittableSchema(schema);
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
- // We import UpdateOperatorInput so generated *Update types can express
415
- // atomic increment / decrement / multiply / divide / set operators on
416
- // numeric columns (TASK-3.4).
417
- //
418
- // RelationDescriptor is the brand-field interface that lets `WithResult`
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 query names it in `select` or passes `includePii`.
458
- // The emitted type marks it optional so it tells the truth about absence.
459
- const piiNote = col.pii ? ' (PII: absent unless selected or includePii)' : '';
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}: ${columnTsType(col, schema.enums)};`);
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. A text natural key, a
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, and the database rejects the row otherwise
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
- const isGenerated = col.isGenerated === true;
482
- const isOptional = isGenerated || col.hasDefault || col.nullable;
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)}?: ${writeColumnTsType(col, schema.enums)};`);
597
+ lines.push(` ${quoteIfNeeded(col.field)}?: ${writeType(table, col)} | undefined;`);
487
598
  }
488
599
  else {
489
- lines.push(` ${quoteIfNeeded(col.field)}: ${writeColumnTsType(col, schema.enums)};`);
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
- // Numeric columns additionally accept `UpdateOperatorInput<number>` so
496
- // users can write `{ viewCount: { increment: 1 } }` without an `as any`.
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(writeColumnTsType(col, schema.enums))};`);
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 emitted as a `RelationDescriptor<Target, Cardinality,
508
- // TargetRelations>` brand-field interface. This is what enables the
509
- // recursive `WithResult` type to walk through nested `with` clauses at
510
- // any depth, `RelationRelations<R[K]>` reads the third type parameter
511
- // and threads it into the next recursion step. If the target table has
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.type === 'hasMany' || rel.type === 'manyToMany' ? "'many'" : "'one'";
631
+ const cardinality = isToMany(rel) ? "'many'" : "'one'";
522
632
  const targetRelations = tablesWithRelations.has(rel.to) ? `${targetType}Relations` : '{}';
523
- lines.push(` ${quoteIfNeeded(relName)}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
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
- if (rel.type === 'hasMany' || rel.type === 'manyToMany') {
531
- lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
532
- lines.push(`export interface ${typeName}With${snakeToPascal(relName)} extends ${typeName} {`);
533
- lines.push(` ${quoteIfNeeded(relName)}: ${targetType}[];`);
534
- lines.push('}');
535
- }
536
- else {
537
- lines.push(`/** ${typeName} with \`${docSafe(relName)}\` relation loaded (${rel.type}: ${docSafe(rel.to)}) */`);
538
- lines.push(`export interface ${typeName}With${snakeToPascal(relName)} extends ${typeName} {`);
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
- // Nested write types (WhereUnique, NestedCreateInput, NestedUpdateInput,
548
- // ConnectOrCreate, CreateInput, UpdateInput)
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
- const typeName = entityName(table.name);
552
- const safeRelations = safeRelationsByTable.get(table.name) ?? [];
553
- const hasRels = safeRelations.length > 0;
554
- // WhereUnique, union of unique constraint shapes, deduplicating PK
555
- const seen = new Set();
556
- const uniqueSets = [];
557
- // Always include the primary key first
558
- const pkKey = table.primaryKey.join(',');
559
- seen.add(pkKey);
560
- uniqueSets.push(table.primaryKey);
561
- // Add unique indexes that aren't duplicates of the PK
562
- for (const uc of table.uniqueColumns) {
563
- const ucKey = uc.join(',');
564
- if (!seen.has(ucKey)) {
565
- seen.add(ucKey);
566
- uniqueSets.push(uc);
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
- if (uniqueSets.length > 0) {
570
- const memberType = (colName) => {
571
- const col = table.columns.find((c) => c.name === colName);
572
- return { field: col?.field ?? colName, tsType: col?.tsType ?? 'unknown' };
573
- };
574
- // Flat branches: one object per unique constraint carrying its columns.
575
- const flatBranches = uniqueSets.map((cols) => {
576
- const fields = cols.map((colName) => {
577
- const m = memberType(colName);
578
- return `${quoteIfNeeded(m.field)}: ${m.tsType}`;
579
- });
580
- return `{ ${fields.join('; ')} }`;
581
- });
582
- // Prisma-style compound-unique SELECTOR branches: a synthetic key
583
- // (`orgId_userId`) whose value holds the member columns, emitted for every
584
- // COMPOSITE unique (PK, composite UNIQUE constraint, or composite UNIQUE
585
- // index). Runtime expansion lives in query/compound-unique.ts.
586
- const compoundSeen = new Set();
587
- const compoundSets = [];
588
- const addCompound = (cols) => {
589
- if (cols.length < 2)
590
- return;
591
- const key = cols.join(',');
592
- if (compoundSeen.has(key))
593
- return;
594
- compoundSeen.add(key);
595
- compoundSets.push(cols);
596
- };
597
- addCompound(table.primaryKey);
598
- for (const uc of table.uniqueColumns)
599
- addCompound(uc);
600
- for (const idx of table.indexes) {
601
- // A partial unique index does not guarantee table-wide row uniqueness,
602
- // so it must not become a compound-unique selector (matches the runtime
603
- // exclusion in query/compound-unique.ts).
604
- if (idx.unique && !idx.docPath && !idx.partial)
605
- addCompound(idx.columns);
606
- }
607
- const selectorEntries = compoundSets.map((cols) => {
608
- const members = cols.map(memberType);
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
- // CreateInput / UpdateInput, extends base type with optional relation fields
632
- if (hasRels) {
633
- lines.push(`export type ${typeName}CreateInput = ${typeName}Create & {`);
634
- for (const [relName, rel] of safeRelations) {
635
- const targetType = entityName(rel.to);
636
- lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedCreateInput;`);
637
- }
638
- lines.push('};');
639
- lines.push('');
640
- lines.push(`export type ${typeName}UpdateInput = ${typeName}Update & {`);
641
- for (const [relName, rel] of safeRelations) {
642
- const targetType = entityName(rel.to);
643
- if (rel.type === 'hasMany') {
644
- lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedUpdateInput;`);
645
- }
646
- else {
647
- lines.push(` ${quoteIfNeeded(relName)}?: ${targetType}NestedCreateInput;`);
648
- }
649
- }
650
- lines.push('};');
651
- lines.push('');
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
- // Emit NestedCreateInput, NestedUpdateInput, ConnectOrCreate for every table
655
- for (const table of Object.values(schema.tables)) {
656
- const typeName = entityName(table.name);
657
- const hasRels = (safeRelationsByTable.get(table.name) ?? []).length > 0;
658
- // NestedCreateInput uses *CreateInput (which includes relation fields) when
659
- // the table has relations, otherwise falls back to the plain *Create type.
660
- const createRefType = hasRels ? `${typeName}CreateInput` : `${typeName}Create`;
661
- lines.push(`export interface ${typeName}NestedCreateInput {`);
662
- lines.push(` create?: ${createRefType} | ${createRefType}[];`);
663
- lines.push(` connect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
664
- lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | ${typeName}ConnectOrCreate[];`);
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}NestedUpdateInput {`);
668
- lines.push(` create?: ${createRefType} | ${createRefType}[];`);
669
- lines.push(` connect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
670
- lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | ${typeName}ConnectOrCreate[];`);
671
- lines.push(` disconnect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
672
- lines.push(` set?: ${typeName}WhereUnique[];`);
673
- lines.push(` delete?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
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 ${typeName}ConnectOrCreate {`);
677
- lines.push(` where: ${typeName}WhereUnique;`);
678
- lines.push(` create: ${createRefType};`);
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
- return lines.join('\n');
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
- expr = zodScalar(pgTypeToTs(base, false));
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(` ${quoteIfNeeded(col.field)}: ${expr},`);
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(` ${quoteIfNeeded(col.field)}: ${expr},`);
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(` ${quoteIfNeeded(col.field)}: ${expr},`);
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(` ${quoteIfNeeded(table.name)}: {`);
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(` ${quoteIfNeeded(field)}: '${escSQ(col)}',`);
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(` ${quoteIfNeeded(col)}: '${escSQ(field)}',`);
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(` ${quoteIfNeeded(col)}: '${escSQ(dialectType)}',`);
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(` ${quoteIfNeeded(col)}: '${escSQ(pgType)}',`);
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(` ${quoteIfNeeded(relName)}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral}${throughLiteral} },`);
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(` ${quoteIfNeeded(enumName)}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
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 = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
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
- lines.push('}');
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 = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
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?: TurbineConfig) {');
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
- lines.push('}');
1030
- lines.push('');
1031
- // Augment TurbineClient via interface merging with typed $transaction and
1032
- // $withSession overloads. The callback parameter is narrowed to
1033
- // `TypedTransactionClient` so users get autocomplete on `tx.users`,
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
- // IMPORTANT: each merged member must be compatible with the base class's
1040
- // member ON ITS OWN (TS2415), since v0.26 the base $transaction also has a
1041
- // batch-array overload (`$transaction([...queries])`), so the merged
1042
- // interface must redeclare BOTH signatures. Emitting only the callback form
1043
- // makes every generated client fail `tsc` with "incorrectly extends". The
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?: TurbineConfig): TurbineClient {');
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(` ${quoteIfNeeded(modelName)}: {`);
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(` ${quoteIfNeeded(rel)}: { name: '${escSQ(r.name)}', cardinality: '${r.cardinality}' },`);
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]) => `${quoteIfNeeded(k)}: '${escSQ(v)}'`).join(', ')} }`;
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]) => `${quoteIfNeeded(k)}: [${v.map((s) => `'${escSQ(s)}'`).join(', ')}]`).join(', ')} }`;
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
- * KNOWN LIMIT, deliberately not handled here: a database object named
1235
- * `__proto__` still sets the emitted object literal's PROTOTYPE rather than
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. Numeric columns become
1249
- * `T | UpdateOperatorInput<number> | null?` so users can write atomic
1250
- * operators (`{ increment: 1 }`, `{ multiply: 2 }`, etc.) without casts.
1251
- *
1252
- * The check is purely structural, if the column's TS type contains
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
- // Strip parens for the regex check; preserve the original string in the output.
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
  }