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
@@ -16,6 +16,8 @@ const node_crypto_1 = require("node:crypto");
16
16
  const node_http_1 = require("node:http");
17
17
  const pg_1 = __importDefault(require("pg"));
18
18
  const connection_guard_js_1 = require("../connection-guard.js");
19
+ const local_server_guard_js_1 = require("./local-server-guard.js");
20
+ const local_server_output_js_1 = require("./local-server-output.js");
19
21
  const observe_ui_js_1 = require("./observe-ui.js");
20
22
  const rate_limit_js_1 = require("./rate-limit.js");
21
23
  const ui_js_1 = require("./ui.js");
@@ -34,31 +36,41 @@ async function startObserve(options) {
34
36
  console.error(`[turbine] observe pool error: ${(0, ui_js_1.redactUrl)(err.message)}`);
35
37
  });
36
38
  (0, connection_guard_js_1.absorbCheckedOutErrors)(pool);
37
- const probe = await pool.connect();
38
- try {
39
- await probe.query('SELECT 1');
40
- }
41
- finally {
42
- probe.release();
43
- }
44
39
  const authToken = (0, node_crypto_1.randomBytes)(24).toString('hex');
40
+ const ticketGate = (0, local_server_guard_js_1.createTicketGate)();
45
41
  const rateLimiter = new Map();
46
42
  const server = (0, node_http_1.createServer)((req, res) => {
47
- handleRequest(req, res, pool, options, authToken, rateLimiter).catch((err) => {
48
- sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
43
+ handleRequest(req, res, pool, options, authToken, rateLimiter, ticketGate).catch((err) => {
44
+ sendJson(res, 500, { error: (0, local_server_output_js_1.responseErrorText)(err) });
49
45
  });
50
46
  });
51
- await new Promise((resolve, reject) => {
52
- server.once('error', reject);
53
- server.listen(options.port, options.host, () => {
54
- server.off('error', reject);
55
- resolve();
47
+ try {
48
+ const probe = await pool.connect();
49
+ try {
50
+ await probe.query('SELECT 1');
51
+ }
52
+ finally {
53
+ probe.release();
54
+ }
55
+ await new Promise((resolve, reject) => {
56
+ server.once('error', reject);
57
+ server.listen(options.port, options.host, () => {
58
+ server.off('error', reject);
59
+ resolve();
60
+ });
56
61
  });
57
- });
62
+ }
63
+ catch (err) {
64
+ // A start that fails (probe refused, port taken) must not leave the probe
65
+ // connection idling in a pool nobody holds: it would keep a database session
66
+ // open and the process alive until the idle timeout.
67
+ await pool.end().catch(() => { });
68
+ throw err;
69
+ }
58
70
  const hostPart = options.host.includes(':') && !options.host.startsWith('[') ? `[${options.host}]` : options.host;
59
- const url = `http://${hostPart}:${options.port}/?token=${authToken}`;
71
+ const url = `http://${hostPart}:${options.port}/?token=${ticketGate.token}`;
60
72
  if (options.openBrowser) {
61
- openUrl(url);
73
+ (0, local_server_output_js_1.openInBrowser)(url);
62
74
  }
63
75
  return {
64
76
  authToken,
@@ -72,15 +84,31 @@ async function startObserve(options) {
72
84
  // ---------------------------------------------------------------------------
73
85
  // Request routing
74
86
  // ---------------------------------------------------------------------------
75
- async function handleRequest(req, res, pool, options, authToken, rateLimiter) {
87
+ async function handleRequest(req, res, pool, options, authToken, rateLimiter,
88
+ /** The one-time login ticket. Absent on a hand-built call: the ticket is then `authToken` itself. */
89
+ ticketGate = gateFor(authToken)) {
76
90
  const hostPart = options.host.includes(':') && !options.host.startsWith('[') ? `[${options.host}]` : options.host;
77
91
  const expectedOrigin = `http://${hostPart}:${options.port}`;
92
+ // DNS rebinding: a page on another domain that re-resolves to this port still
93
+ // sends its own hostname in `Host`. Checked before any route or token logic.
94
+ if (!(0, local_server_guard_js_1.isAllowedHostHeader)(req.headers.host, options.host, options.port)) {
95
+ sendJson(res, 403, { error: 'host not allowed: open Observe at the address printed in the terminal' });
96
+ return;
97
+ }
78
98
  const origin = req.headers.origin;
79
99
  if (origin && origin !== expectedOrigin) {
80
100
  sendJson(res, 403, { error: 'cross-origin requests not allowed' });
81
101
  return;
82
102
  }
83
- const url = new URL(req.url ?? '/', expectedOrigin);
103
+ // A request target that is not a URL (`GET //`) is a client error, not a 500.
104
+ let url;
105
+ try {
106
+ url = new URL(req.url ?? '/', expectedOrigin);
107
+ }
108
+ catch {
109
+ sendJson(res, 400, { error: 'malformed request target' });
110
+ return;
111
+ }
84
112
  const pathname = url.pathname;
85
113
  if (pathname === '/' || pathname === '/index.html') {
86
114
  if (req.method !== 'GET' && req.method !== 'HEAD') {
@@ -88,13 +116,28 @@ async function handleRequest(req, res, pool, options, authToken, rateLimiter) {
88
116
  return;
89
117
  }
90
118
  const queryToken = url.searchParams.get('token');
91
- if (queryToken && constantTimeEqual(queryToken, authToken)) {
92
- res.writeHead(302, {
93
- Location: '/',
94
- 'Set-Cookie': `turbine_observe_token=${authToken}; Path=/; HttpOnly; SameSite=Strict`,
95
- });
96
- res.end();
97
- return;
119
+ if (queryToken) {
120
+ // The printed URL's token is a ONE-TIME ticket, exchanged here for the
121
+ // session cookie by the first GET that carries it. HEAD never spends it.
122
+ const outcome = req.method === 'GET' ? (0, local_server_guard_js_1.redeemTicket)(ticketGate, queryToken) : 'invalid';
123
+ if (outcome === 'granted') {
124
+ res.writeHead(302, {
125
+ Location: '/',
126
+ 'Set-Cookie': `turbine_observe_token=${authToken}; Path=/; HttpOnly; SameSite=Strict`,
127
+ });
128
+ res.end();
129
+ return;
130
+ }
131
+ if (outcome === 'used') {
132
+ if (isAuthorized(req, authToken)) {
133
+ res.writeHead(302, { Location: '/' });
134
+ res.end();
135
+ }
136
+ else {
137
+ sendText(res, 403, 'This login link was already used. Restart Observe to get a new one.');
138
+ }
139
+ return;
140
+ }
98
141
  }
99
142
  sendHtml(res, 200, observe_ui_js_1.OBSERVE_HTML, cspNonce());
100
143
  return;
@@ -127,7 +170,7 @@ async function handleRequest(req, res, pool, options, authToken, rateLimiter) {
127
170
  // ---------------------------------------------------------------------------
128
171
  function isAuthorized(req, expectedToken) {
129
172
  const headerToken = req.headers['x-turbine-token'];
130
- if (typeof headerToken === 'string' && constantTimeEqual(headerToken, expectedToken)) {
173
+ if (typeof headerToken === 'string' && (0, local_server_guard_js_1.constantTimeEqual)(headerToken, expectedToken)) {
131
174
  return true;
132
175
  }
133
176
  const cookieHeader = req.headers.cookie ?? '';
@@ -135,18 +178,23 @@ function isAuthorized(req, expectedToken) {
135
178
  // ENDS with this one (`x_turbine_observe_token=...`) matched first and the real
136
179
  // token was never compared, denying the legitimate session.
137
180
  const match = /(?:^|;\s*)turbine_observe_token=([a-f0-9]+)/.exec(cookieHeader);
138
- if (match?.[1] && constantTimeEqual(match[1], expectedToken)) {
181
+ if (match?.[1] && (0, local_server_guard_js_1.constantTimeEqual)(match[1], expectedToken)) {
139
182
  return true;
140
183
  }
141
184
  return false;
142
185
  }
143
- function constantTimeEqual(a, b) {
144
- // Hash both inputs to fixed-length 32-byte SHA-256 digests before comparing.
145
- // This makes the comparison constant-length (timingSafeEqual never throws on a
146
- // length mismatch) and leaks neither length nor content via timing.
147
- const ah = (0, node_crypto_1.createHash)('sha256').update(a).digest();
148
- const bh = (0, node_crypto_1.createHash)('sha256').update(b).digest();
149
- return (0, node_crypto_1.timingSafeEqual)(ah, bh);
186
+ /**
187
+ * The ticket for a caller that passed none (a hand-built dispatch): `authToken`
188
+ * itself, remembered per token so it is still spent exactly once.
189
+ */
190
+ const FALLBACK_GATES = new Map();
191
+ function gateFor(authToken) {
192
+ let gate = FALLBACK_GATES.get(authToken);
193
+ if (!gate) {
194
+ gate = (0, local_server_guard_js_1.createTicketGate)(authToken);
195
+ FALLBACK_GATES.set(authToken, gate);
196
+ }
197
+ return gate;
150
198
  }
151
199
  // ---------------------------------------------------------------------------
152
200
  // API handlers
@@ -195,7 +243,7 @@ async function apiLatency(res, pool, params) {
195
243
  // this server writes no log of its own, so the HTTP response is the only
196
244
  // channel either error could reach. Same shape as cli/studio.ts.
197
245
  }
198
- sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
246
+ sendJson(res, 500, { error: (0, local_server_output_js_1.responseErrorText)(err) });
199
247
  }
200
248
  finally {
201
249
  client.release();
@@ -236,7 +284,7 @@ async function apiModels(res, pool, params) {
236
284
  // this server writes no log of its own, so the HTTP response is the only
237
285
  // channel either error could reach. Same shape as cli/studio.ts.
238
286
  }
239
- sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
287
+ sendJson(res, 500, { error: (0, local_server_output_js_1.responseErrorText)(err) });
240
288
  }
241
289
  finally {
242
290
  client.release();
@@ -284,21 +332,3 @@ function sendText(res, status, text) {
284
332
  res.writeHead(status, { ...SECURITY_HEADERS, 'Content-Security-Policy': JSON_CSP, 'Content-Type': 'text/plain' });
285
333
  res.end(text);
286
334
  }
287
- // ---------------------------------------------------------------------------
288
- // Browser open
289
- // ---------------------------------------------------------------------------
290
- function openUrl(url) {
291
- const { platform: os } = process;
292
- const { spawn } = require('node:child_process');
293
- try {
294
- if (os === 'darwin')
295
- spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
296
- else if (os === 'win32')
297
- spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();
298
- else
299
- spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
300
- }
301
- catch {
302
- // Best effort
303
- }
304
- }
@@ -62,32 +62,15 @@
62
62
  * name-carrying position that is not a string is refused as a SHAPE here,
63
63
  * never skipped: skipping is the branch that leaks.
64
64
  */
65
+ import { RELATION_FILTER_WRAPPERS } from '../query/filters.js';
65
66
  import type { SchemaMetadata, TableMetadata } from '../schema.js';
66
67
  /**
67
- * Relation-filter wrappers whose body is a clause against the relation's target.
68
- *
69
- * THREE copies of this list exist in the tree, and they have to agree: a wrapper
70
- * the compiler understands and this guard does not is a wrapper that reaches the
71
- * builder unguarded.
72
- *
73
- * 1. this one, the only NAMED definition;
74
- * 2. `query/where-compile.ts`, inlined as `'some' in x || 'every' in x || …`;
75
- * 3. `prisma-compat.ts`, SPLIT across `RELATION_QUANTIFIERS`
76
- * (`some`/`every`/`none`) and two inline `k === 'is' || k === 'isNot'`
77
- * tests, which is the copy most likely to drift because half of it does
78
- * not look like a list at all.
79
- *
80
- * Copy 2 belongs to the SQL compiler and cannot import this one (a query-path
81
- * module must not depend on `cli/`), so the deduplication has to go the other
82
- * way: the list wants to live in `query/filters.ts` and be imported here, the
83
- * way {@link COLUMN_REF_OPERATORS} above now is.
84
- *
85
- * That destination now EXISTS (`RELATION_FILTER_WRAPPERS` in `query/filters.ts`)
86
- * and nothing imports it, including this file, so it is a fourth copy rather
87
- * than a deduplication and this comment is still the only thing holding them in
88
- * step. Moving each walk onto it is a change per walk, each with its own tests.
68
+ * Relation-filter wrappers whose body is a clause against the relation's target:
69
+ * the SQL compiler's own list (`query/filters.ts`), imported so a wrapper the
70
+ * compiler understands can never reach the builder unguarded. Re-exported for
71
+ * callers that still import it from here.
89
72
  */
90
- export declare const RELATION_FILTER_WRAPPERS: readonly ["some", "none", "every", "is", "isNot"];
73
+ export { RELATION_FILTER_WRAPPERS };
91
74
  /**
92
75
  * Recursion bound for the guard walk.
93
76
  *
@@ -101,6 +84,43 @@ export declare const RELATION_FILTER_WRAPPERS: readonly ["some", "none", "every"
101
84
  * nothing the builder would accept is refused here for depth alone.
102
85
  */
103
86
  export declare const PII_GUARD_MAX_DEPTH = 32;
87
+ /**
88
+ * Find a `lock` option anywhere a query level can carry one: the args
89
+ * themselves, or the options of any `with` entry at any depth. Returns the
90
+ * dotted path to the shallowest hit (`lock`, `with.posts.lock`), or `null`.
91
+ *
92
+ * WHY THIS IS NOT PART OF {@link assertNoPiiPredicates}. A locking read is not
93
+ * a hidden-column question, and the PII walk is conditional by design: Studio
94
+ * skips it under `--show-pii`, `explain_query` hands it an allowlisted subset
95
+ * of its arguments, and `compile_query` hands it only `where` for an aggregate.
96
+ * A read-only refusal has to hold in every one of those, so each surface calls
97
+ * this directly and unconditionally. It lives in this module because both
98
+ * surfaces already import it, which is what keeps the rule and its wording in
99
+ * one place.
100
+ *
101
+ * ANY VALUE other than `undefined` counts, `null` and `false` included. The
102
+ * builder treats exactly `undefined` as "no lock" and refuses every other
103
+ * non-lock value itself (E003), so answering "present" for all of them means
104
+ * the verdict here never depends on what the builder would have made of it.
105
+ *
106
+ * A `with` MAP is keyed by relation names, so a relation that happens to be
107
+ * called `lock` is a level to descend into and never a hit; only the option
108
+ * position on a level is read. A table column called `lock` is likewise safe:
109
+ * columns live under `where` / `orderBy` / `select`, which are not searched.
110
+ *
111
+ * Iterative and breadth-first, so a deeply nested payload costs time and no
112
+ * stack, and the shallowest hit is the one reported. The visited set makes it
113
+ * terminate on a cyclic object, which parsed JSON cannot produce but a caller
114
+ * of a pure helper can.
115
+ */
116
+ export declare function findRowLockOption(args: Record<string, unknown>): string | null;
117
+ /**
118
+ * The refusal text for a `lock` found by {@link findRowLockOption}, shared so
119
+ * the two surfaces cannot word the same rule two ways. `path` is interpolated
120
+ * as given: it can carry a caller-chosen relation name, so each surface passes
121
+ * it through its own printable-name escaping first.
122
+ */
123
+ export declare function rowLockRefusalMessage(path: string, tool: string): string;
104
124
  /**
105
125
  * The two things Studio and the MCP server genuinely do differently, plus the
106
126
  * schema the walk resolves relation targets against.
@@ -65,35 +65,13 @@
65
65
  */
66
66
  Object.defineProperty(exports, "__esModule", { value: true });
67
67
  exports.PII_GUARD_MAX_DEPTH = exports.RELATION_FILTER_WRAPPERS = void 0;
68
+ exports.findRowLockOption = findRowLockOption;
69
+ exports.rowLockRefusalMessage = rowLockRefusalMessage;
68
70
  exports.assertNoPiiPredicates = assertNoPiiPredicates;
71
+ const column_ref_js_1 = require("../query/column-ref.js");
69
72
  const filters_js_1 = require("../query/filters.js");
73
+ Object.defineProperty(exports, "RELATION_FILTER_WRAPPERS", { enumerable: true, get: function () { return filters_js_1.RELATION_FILTER_WRAPPERS; } });
70
74
  const utils_js_1 = require("../query/utils.js");
71
- /**
72
- * Relation-filter wrappers whose body is a clause against the relation's target.
73
- *
74
- * THREE copies of this list exist in the tree, and they have to agree: a wrapper
75
- * the compiler understands and this guard does not is a wrapper that reaches the
76
- * builder unguarded.
77
- *
78
- * 1. this one, the only NAMED definition;
79
- * 2. `query/where-compile.ts`, inlined as `'some' in x || 'every' in x || …`;
80
- * 3. `prisma-compat.ts`, SPLIT across `RELATION_QUANTIFIERS`
81
- * (`some`/`every`/`none`) and two inline `k === 'is' || k === 'isNot'`
82
- * tests, which is the copy most likely to drift because half of it does
83
- * not look like a list at all.
84
- *
85
- * Copy 2 belongs to the SQL compiler and cannot import this one (a query-path
86
- * module must not depend on `cli/`), so the deduplication has to go the other
87
- * way: the list wants to live in `query/filters.ts` and be imported here, the
88
- * way {@link COLUMN_REF_OPERATORS} above now is.
89
- *
90
- * That destination now EXISTS (`RELATION_FILTER_WRAPPERS` in `query/filters.ts`)
91
- * and nothing imports it, including this file, so it is a fourth copy rather
92
- * than a deduplication and this comment is still the only thing holding them in
93
- * step. Moving each walk onto it is a change per walk, each with its own tests.
94
- */
95
- exports.RELATION_FILTER_WRAPPERS = ['some', 'none', 'every', 'is', 'isNot'];
96
- const RELATION_FILTER_WRAPPER_SET = new Set(exports.RELATION_FILTER_WRAPPERS);
97
75
  /**
98
76
  * Recursion bound for the guard walk.
99
77
  *
@@ -133,6 +111,78 @@ const LEVEL_OBJECT_KEYS = new Set([
133
111
  * which is what the builder itself branches on.
134
112
  */
135
113
  const RELATION_PICK_KEYS = new Set(['pick', 'by', 'direction', 'nulls', 'plan']);
114
+ /**
115
+ * Find a `lock` option anywhere a query level can carry one: the args
116
+ * themselves, or the options of any `with` entry at any depth. Returns the
117
+ * dotted path to the shallowest hit (`lock`, `with.posts.lock`), or `null`.
118
+ *
119
+ * WHY THIS IS NOT PART OF {@link assertNoPiiPredicates}. A locking read is not
120
+ * a hidden-column question, and the PII walk is conditional by design: Studio
121
+ * skips it under `--show-pii`, `explain_query` hands it an allowlisted subset
122
+ * of its arguments, and `compile_query` hands it only `where` for an aggregate.
123
+ * A read-only refusal has to hold in every one of those, so each surface calls
124
+ * this directly and unconditionally. It lives in this module because both
125
+ * surfaces already import it, which is what keeps the rule and its wording in
126
+ * one place.
127
+ *
128
+ * ANY VALUE other than `undefined` counts, `null` and `false` included. The
129
+ * builder treats exactly `undefined` as "no lock" and refuses every other
130
+ * non-lock value itself (E003), so answering "present" for all of them means
131
+ * the verdict here never depends on what the builder would have made of it.
132
+ *
133
+ * A `with` MAP is keyed by relation names, so a relation that happens to be
134
+ * called `lock` is a level to descend into and never a hit; only the option
135
+ * position on a level is read. A table column called `lock` is likewise safe:
136
+ * columns live under `where` / `orderBy` / `select`, which are not searched.
137
+ *
138
+ * Iterative and breadth-first, so a deeply nested payload costs time and no
139
+ * stack, and the shallowest hit is the one reported. The visited set makes it
140
+ * terminate on a cyclic object, which parsed JSON cannot produce but a caller
141
+ * of a pure helper can.
142
+ */
143
+ function findRowLockOption(args) {
144
+ const queue = [{ level: args, parent: null, relation: '' }];
145
+ const seen = new Set();
146
+ for (let next = 0; next < queue.length; next++) {
147
+ const frame = queue[next];
148
+ if (seen.has(frame.level))
149
+ continue;
150
+ seen.add(frame.level);
151
+ if (frame.level.lock !== undefined)
152
+ return lockPath(frame);
153
+ const withClause = frame.level.with;
154
+ if (!isPlainObject(withClause))
155
+ continue;
156
+ for (const [relation, spec] of Object.entries(withClause)) {
157
+ if (isPlainObject(spec))
158
+ queue.push({ level: spec, parent: frame, relation });
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+ /** `lock`, `with.posts.lock`, `with.posts.with.author.lock`: the path from the root to a hit. */
164
+ function lockPath(frame) {
165
+ const relations = [];
166
+ for (let at = frame; at !== null && at.parent !== null; at = at.parent) {
167
+ relations.push(at.relation);
168
+ }
169
+ const parts = [];
170
+ for (let i = relations.length - 1; i >= 0; i--)
171
+ parts.push('with', relations[i]);
172
+ parts.push('lock');
173
+ return parts.join('.');
174
+ }
175
+ /**
176
+ * The refusal text for a `lock` found by {@link findRowLockOption}, shared so
177
+ * the two surfaces cannot word the same rule two ways. `path` is interpolated
178
+ * as given: it can carry a caller-chosen relation name, so each surface passes
179
+ * it through its own printable-name escaping first.
180
+ */
181
+ function rowLockRefusalMessage(path, tool) {
182
+ return (`${tool} does not accept \`${path}\`: it is read-only, and a locking read takes row locks, which needs a ` +
183
+ 'write-capable transaction in your application. Remove it from the query here (nothing else about the query ' +
184
+ 'changes), and run the locking read inside db.$transaction(async (tx) => ...) in your code.');
185
+ }
136
186
  /** An object or array value, i.e. something that can carry a nested name. */
137
187
  function isObjectLike(value) {
138
188
  return value !== null && typeof value === 'object';
@@ -228,23 +278,34 @@ function assertNoPiiPredicates(args, rootTable, host) {
228
278
  /**
229
279
  * A column-keyed operator object (`{ gt: 5 }`, `{ sort, nulls }`, a JSON path,
230
280
  * a vector distance) holds values, not names, with ONE exception: the
231
- * comparison operators also accept `{ col: 'otherField' }`, which compiles to
232
- * a column-to-column comparison in the same table. Scan for that.
281
+ * comparison operators also accept a column reference (`col('otherField')`),
282
+ * which compiles to a column-to-column comparison in the same table. Scan for
283
+ * that, including inside a `not` operator object, which compiles to
284
+ * `NOT (<inner>)` with the same operand positions.
285
+ *
286
+ * A reference carries a symbol brand that parsed JSON cannot produce, so the
287
+ * request data these tools compile never holds one, and the builder refuses
288
+ * the plain `{ col }` spelling. That spelling is refused HERE as well, as a
289
+ * shape: the guard must not depend on the builder to close a channel it is
290
+ * responsible for.
233
291
  *
234
292
  * The operator list is IMPORTED from `query/filters.ts` rather than copied:
235
293
  * that is the set the SQL compiler itself branches on, and a guard that
236
294
  * disagreed with it would be guarding a different query than the one that
237
- * runs. A copy that drifted short by one operator reopens the operand-position
238
- * channel this scan exists to close, silently.
295
+ * runs.
239
296
  */
240
- const checkColumnRefs = (table, operatorNode) => {
297
+ const checkColumnRefs = (table, operatorNode, depth) => {
298
+ assertWithinDepth(depth);
241
299
  for (const opKey of filters_js_1.COLUMN_REF_OPERATORS) {
242
300
  const operand = (0, utils_js_1.ownLookup)(operatorNode, opKey);
243
- if (!isPlainObject(operand))
301
+ if ((0, filters_js_1.isColumnRef)(operand)) {
302
+ checkColumnName(table, operand.col);
244
303
  continue;
245
- const ref = operand.col;
246
- if (typeof ref === 'string')
247
- checkColumnName(table, ref);
304
+ }
305
+ if ((0, column_ref_js_1.isPlainColumnRefShape)(operand))
306
+ host.refuseShape(table, `${opKey}: { col }`);
307
+ if (opKey === 'not' && isPlainObject(operand))
308
+ checkColumnRefs(table, operand, depth + 1);
248
309
  }
249
310
  };
250
311
  /**
@@ -271,7 +332,7 @@ function assertNoPiiPredicates(args, rootTable, host) {
271
332
  // body is a clause against the SAME target. Handing the wrapper to the
272
333
  // column walker instead walked `is` as if it were a column of the target,
273
334
  // so the inner clause was never visited at all.
274
- if (scope === 'relation' && RELATION_FILTER_WRAPPER_SET.has(key)) {
335
+ if (scope === 'relation' && filters_js_1.RELATION_FILTER_WRAPPER_SET.has(key)) {
275
336
  visitClause(value, table, depth + 1);
276
337
  return;
277
338
  }
@@ -299,7 +360,7 @@ function assertNoPiiPredicates(args, rootTable, host) {
299
360
  if (scope === 'relation' && !table.allColumns.includes(column))
300
361
  host.refuseShape(table, key);
301
362
  if (isPlainObject(value))
302
- checkColumnRefs(table, value);
363
+ checkColumnRefs(table, value, depth + 1);
303
364
  };
304
365
  /** A where / orderBy / cursor object (or a Prisma-style array of them). */
305
366
  const visitClause = (node, table, depth) => {
@@ -16,9 +16,11 @@
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.DEFAULT_EXCLUDED_TABLES = void 0;
18
18
  exports.resolvePrismaSchema = resolvePrismaSchema;
19
+ const introspect_js_1 = require("../introspect.js");
20
+ const utils_js_1 = require("../query/utils.js");
19
21
  const schema_js_1 = require("../schema.js");
20
- var introspect_js_1 = require("../introspect.js");
21
- Object.defineProperty(exports, "DEFAULT_EXCLUDED_TABLES", { enumerable: true, get: function () { return introspect_js_1.DEFAULT_EXCLUDED_TABLES; } });
22
+ var introspect_js_2 = require("../introspect.js");
23
+ Object.defineProperty(exports, "DEFAULT_EXCLUDED_TABLES", { enumerable: true, get: function () { return introspect_js_2.DEFAULT_EXCLUDED_TABLES; } });
22
24
  // ---------------------------------------------------------------------------
23
25
  // Name-candidate helpers
24
26
  // ---------------------------------------------------------------------------
@@ -106,7 +108,9 @@ function resolvePrismaSchema(ast, schema, options = {}) {
106
108
  const result = {
107
109
  models: [],
108
110
  enums: [],
109
- map: { models: {}, enums: {} },
111
+ // Prototype-less: models, fields, relations and enums are keyed by names the
112
+ // Prisma schema chose, and `__proto__` / `constructor` are legal names there.
113
+ map: { models: (0, introspect_js_1.nullRecord)(), enums: (0, introspect_js_1.nullRecord)() },
110
114
  hasUnresolved: false,
111
115
  parseWarnings: ast.warnings,
112
116
  noDb,
@@ -115,7 +119,7 @@ function resolvePrismaSchema(ast, schema, options = {}) {
115
119
  for (const model of ast.models) {
116
120
  const rt = modelTable.get(model.name);
117
121
  const table = rt.table;
118
- const tableMeta = table && resolvedSchema ? resolvedSchema.tables[table] : undefined;
122
+ const tableMeta = table && resolvedSchema ? (0, utils_js_1.ownLookup)(resolvedSchema.tables, table) : undefined;
119
123
  const accessor = table ? (0, schema_js_1.snakeToCamel)(table) : null;
120
124
  const status = noDb ? 'parsed' : table ? 'resolved' : 'unresolved';
121
125
  const resolved = {
@@ -148,23 +152,23 @@ function resolvePrismaSchema(ast, schema, options = {}) {
148
152
  result.models.push(resolved);
149
153
  // Build the verified map entry (skip in --no-db and for unresolved tables).
150
154
  if (!noDb && table && accessor) {
151
- const fields = {};
155
+ const fields = (0, introspect_js_1.nullRecord)();
152
156
  for (const f of resolved.fields) {
153
157
  if (f.status === 'resolved' && f.turbineField)
154
158
  fields[f.prismaName] = f.turbineField;
155
159
  }
156
- const relations = {};
160
+ const relations = (0, introspect_js_1.nullRecord)();
157
161
  for (const r of resolved.relations) {
158
162
  if (r.status === 'resolved' && r.turbineName && r.cardinality) {
159
163
  relations[r.prismaName] = { name: r.turbineName, cardinality: r.cardinality };
160
164
  }
161
165
  }
162
- const compoundUniques = {};
166
+ const compoundUniques = (0, introspect_js_1.nullRecord)();
163
167
  for (const c of resolved.compoundUniques) {
164
168
  if (c.status === 'resolved' && c.turbineFields)
165
169
  compoundUniques[c.selector] = c.turbineFields;
166
170
  }
167
- const clientDefaults = collectClientDefaults(model, fields, resolvedSchema?.tables[table]);
171
+ const clientDefaults = collectClientDefaults(model, fields, (0, utils_js_1.ownLookup)(resolvedSchema?.tables ?? {}, table));
168
172
  result.map.models[model.name] = {
169
173
  table,
170
174
  accessor,
@@ -264,7 +268,7 @@ function resolveScalarField(model, fieldName, tableMeta, noDb) {
264
268
  if (noDb || !tableMeta) {
265
269
  return { prismaName: fieldName, turbineField: null, column: null, status: noDb ? 'parsed' : 'unresolved' };
266
270
  }
267
- const turbineField = tableMeta.reverseColumnMap[column];
271
+ const turbineField = (0, utils_js_1.ownLookup)(tableMeta.reverseColumnMap, column);
268
272
  if (turbineField) {
269
273
  return { prismaName: fieldName, turbineField, column, status: 'resolved' };
270
274
  }
@@ -310,9 +314,9 @@ function relationFkColumns(model, field) {
310
314
  function collectClientDefaults(model, resolvedFields, tableMeta) {
311
315
  if (!tableMeta)
312
316
  return undefined;
313
- const out = {};
317
+ const out = (0, introspect_js_1.nullRecord)();
314
318
  for (const field of model.fields) {
315
- const turbineField = resolvedFields[field.name];
319
+ const turbineField = (0, utils_js_1.ownLookup)(resolvedFields, field.name);
316
320
  if (!turbineField)
317
321
  continue;
318
322
  const col = tableMeta.columns.find((c) => c.field === turbineField || c.name === turbineField);
@@ -472,11 +476,11 @@ function resolveCompoundUnique(model, key, tableMeta, noDb) {
472
476
  if (noDb || !tableMeta)
473
477
  return base;
474
478
  const columns = key.fields.map((f) => fieldColumn(model, f));
475
- const missing = columns.filter((c) => !tableMeta.reverseColumnMap[c]);
479
+ const missing = columns.filter((c) => !(0, utils_js_1.ownLookup)(tableMeta.reverseColumnMap, c));
476
480
  if (missing.length > 0) {
477
481
  return { ...base, reason: `column(s) not found: ${missing.join(', ')}` };
478
482
  }
479
- const turbineFields = columns.map((c) => tableMeta.reverseColumnMap[c]);
483
+ const turbineFields = columns.map((c) => (0, utils_js_1.ownLookup)(tableMeta.reverseColumnMap, c));
480
484
  const want = [...columns].sort().join(',');
481
485
  const matches = (setList) => setList.some((s) => [...s].sort().join(',') === want);
482
486
  if (key.kind === 'id') {
@@ -93,11 +93,19 @@ function lineAt(src, offset) {
93
93
  // ---------------------------------------------------------------------------
94
94
  // Attribute-argument tokenizer
95
95
  // ---------------------------------------------------------------------------
96
- /** Unquote a `"..."` literal, resolving the escapes Prisma supports. */
96
+ const QUOTED_ESCAPES = { '"': '"', '\\': '\\', n: '\n', t: '\t' };
97
+ /**
98
+ * Unquote a `"..."` literal, resolving the escapes Prisma supports.
99
+ *
100
+ * ONE left-to-right pass, because the escapes are not independent: after `\\`
101
+ * has become a backslash, that backslash must not be read again as the start of
102
+ * `\n` or `\t`. A chain of `.replace` calls does exactly that, so the literal
103
+ * `"C:\\new"` came out as `C:` + newline + `ew`.
104
+ */
97
105
  function unquote(raw) {
98
106
  const s = raw.trim();
99
107
  if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
100
- return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\').replace(/\\n/g, '\n').replace(/\\t/g, '\t');
108
+ return s.slice(1, -1).replace(/\\(["\\nt])/g, (_, escaped) => QUOTED_ESCAPES[escaped]);
101
109
  }
102
110
  return s;
103
111
  }
@@ -429,7 +437,7 @@ function parseFieldLine(text, line, warnings) {
429
437
  `Turbine generates the column from the database, so it IS present on the generated client.`);
430
438
  }
431
439
  for (const attr of attrs) {
432
- const note = NOTED_FIELD_ATTRS[attr.name];
440
+ const note = Object.hasOwn(NOTED_FIELD_ATTRS, attr.name) ? NOTED_FIELD_ATTRS[attr.name] : undefined;
433
441
  if (note)
434
442
  warnings.push(`Line ${line}: field "${name}": ${note}`);
435
443
  }
@@ -463,7 +471,7 @@ function parseModelBody(block, kind, src, warnings) {
463
471
  // @@index and anything else: recorded in blockAttrs, unused. The
464
472
  // few that change what the model MEANS get a warning rather than
465
473
  // silence (see NOTED_BLOCK_ATTRS).
466
- const note = NOTED_BLOCK_ATTRS[attr.name];
474
+ const note = Object.hasOwn(NOTED_BLOCK_ATTRS, attr.name) ? NOTED_BLOCK_ATTRS[attr.name] : undefined;
467
475
  if (note)
468
476
  warnings.push(`Line ${line}: model "${block.name}": ${note}`);
469
477
  }