turbine-orm 0.49.0 → 0.50.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 (157) hide show
  1. package/README.md +122 -39
  2. package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
  3. package/dist/cjs/adapters/index.d.ts +110 -0
  4. package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
  5. package/dist/cjs/cli/config.d.ts +181 -0
  6. package/dist/cjs/cli/config.js +32 -6
  7. package/dist/cjs/cli/destructive.d.ts +38 -0
  8. package/dist/cjs/cli/index.d.ts +359 -0
  9. package/dist/cjs/cli/index.js +228 -56
  10. package/dist/cjs/cli/loader.d.ts +61 -0
  11. package/dist/cjs/cli/mcp.d.ts +42 -0
  12. package/dist/cjs/cli/migrate.d.ts +356 -0
  13. package/dist/cjs/cli/migrate.js +131 -40
  14. package/dist/cjs/cli/observe-ui.d.ts +1 -0
  15. package/dist/cjs/cli/observe-ui.js +14 -5
  16. package/dist/cjs/cli/observe.d.ts +25 -0
  17. package/dist/cjs/cli/observe.js +49 -12
  18. package/dist/cjs/cli/pii-tags.d.ts +53 -0
  19. package/dist/cjs/cli/prisma-report.d.ts +33 -0
  20. package/dist/cjs/cli/prisma-report.js +73 -0
  21. package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
  22. package/dist/cjs/cli/prisma-resolve.js +1 -0
  23. package/dist/cjs/cli/prisma-schema.d.ts +176 -0
  24. package/dist/cjs/cli/prisma-schema.js +82 -4
  25. package/dist/cjs/cli/rate-limit.d.ts +32 -0
  26. package/dist/cjs/cli/rate-limit.js +45 -0
  27. package/dist/cjs/cli/studio-demo.d.ts +43 -0
  28. package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
  29. package/dist/cjs/cli/studio.d.ts +207 -0
  30. package/dist/cjs/cli/studio.js +136 -71
  31. package/dist/cjs/cli/ui.d.ts +73 -0
  32. package/dist/cjs/cli/ui.js +51 -9
  33. package/dist/cjs/client.d.ts +837 -0
  34. package/dist/cjs/client.js +3 -0
  35. package/dist/cjs/dialect.d.ts +516 -0
  36. package/dist/cjs/dialect.js +37 -12
  37. package/dist/cjs/errors.d.ts +370 -0
  38. package/dist/cjs/generate.d.ts +137 -0
  39. package/dist/cjs/generate.js +39 -6
  40. package/dist/cjs/index-advisor.d.ts +153 -0
  41. package/dist/cjs/index-stats.d.ts +384 -0
  42. package/dist/cjs/index.d.ts +55 -0
  43. package/dist/cjs/index.js +7 -2
  44. package/dist/cjs/introspect.d.ts +269 -0
  45. package/dist/cjs/mssql.d.ts +232 -0
  46. package/dist/cjs/mssql.js +6 -0
  47. package/dist/cjs/mysql.d.ts +173 -0
  48. package/dist/cjs/mysql.js +16 -0
  49. package/dist/cjs/nested-write.d.ts +96 -0
  50. package/dist/cjs/nested-write.js +414 -24
  51. package/dist/cjs/observe.d.ts +115 -0
  52. package/dist/cjs/optional-peer-import.d.cts +72 -0
  53. package/dist/cjs/pipeline-submittable.d.ts +93 -0
  54. package/dist/cjs/pipeline.d.ts +71 -0
  55. package/dist/cjs/powdb-introspect.d.ts +84 -0
  56. package/dist/cjs/powdb.d.ts +931 -0
  57. package/dist/cjs/powdb.js +106 -21
  58. package/dist/cjs/powql.d.ts +592 -0
  59. package/dist/cjs/powql.js +42 -6
  60. package/dist/cjs/prisma-compat.d.ts +283 -0
  61. package/dist/cjs/prisma-compat.js +167 -9
  62. package/dist/cjs/query/aggregates.d.ts +92 -0
  63. package/dist/cjs/query/aggregates.js +7 -3
  64. package/dist/cjs/query/batched-loader.d.ts +193 -0
  65. package/dist/cjs/query/builder.d.ts +849 -0
  66. package/dist/cjs/query/builder.js +571 -65
  67. package/dist/cjs/query/compound-unique.d.ts +51 -0
  68. package/dist/cjs/query/deferred.d.ts +223 -0
  69. package/dist/cjs/query/filters.d.ts +201 -0
  70. package/dist/cjs/query/index.d.ts +14 -0
  71. package/dist/cjs/query/index.js +6 -1
  72. package/dist/cjs/query/relations.d.ts +609 -0
  73. package/dist/cjs/query/relations.js +693 -46
  74. package/dist/cjs/query/types.d.ts +1300 -0
  75. package/dist/cjs/query/utils.d.ts +209 -0
  76. package/dist/cjs/query/utils.js +208 -1
  77. package/dist/cjs/query/warn-registry.d.ts +68 -0
  78. package/dist/cjs/query/warn-registry.js +9 -0
  79. package/dist/cjs/query/where-compile.d.ts +139 -0
  80. package/dist/cjs/query/where.d.ts +548 -0
  81. package/dist/cjs/query/where.js +58 -22
  82. package/dist/cjs/query/writes.d.ts +172 -0
  83. package/dist/cjs/query/writes.js +105 -12
  84. package/dist/cjs/realtime.d.ts +70 -0
  85. package/dist/cjs/schema-builder.d.ts +354 -0
  86. package/dist/cjs/schema-metadata.d.ts +83 -0
  87. package/dist/cjs/schema-sql.d.ts +217 -0
  88. package/dist/cjs/schema-sql.js +23 -5
  89. package/dist/cjs/schema.d.ts +356 -0
  90. package/dist/cjs/schema.js +125 -0
  91. package/dist/cjs/seed.d.ts +15 -0
  92. package/dist/cjs/serverless.d.ts +142 -0
  93. package/dist/cjs/sqlite.d.ts +143 -0
  94. package/dist/cjs/sqlite.js +4 -0
  95. package/dist/cjs/typed-sql.d.ts +102 -0
  96. package/dist/cli/config.d.ts +18 -4
  97. package/dist/cli/config.js +31 -6
  98. package/dist/cli/index.d.ts +123 -0
  99. package/dist/cli/index.js +223 -58
  100. package/dist/cli/migrate.d.ts +59 -10
  101. package/dist/cli/migrate.js +128 -41
  102. package/dist/cli/observe-ui.d.ts +1 -1
  103. package/dist/cli/observe-ui.js +14 -5
  104. package/dist/cli/observe.d.ts +7 -1
  105. package/dist/cli/observe.js +48 -12
  106. package/dist/cli/prisma-report.d.ts +14 -0
  107. package/dist/cli/prisma-report.js +72 -0
  108. package/dist/cli/prisma-resolve.d.ts +6 -0
  109. package/dist/cli/prisma-resolve.js +1 -0
  110. package/dist/cli/prisma-schema.d.ts +62 -2
  111. package/dist/cli/prisma-schema.js +81 -4
  112. package/dist/cli/rate-limit.d.ts +32 -0
  113. package/dist/cli/rate-limit.js +40 -0
  114. package/dist/cli/studio.d.ts +5 -5
  115. package/dist/cli/studio.js +135 -70
  116. package/dist/cli/ui.d.ts +1 -1
  117. package/dist/cli/ui.js +51 -9
  118. package/dist/client.d.ts +40 -0
  119. package/dist/client.js +3 -0
  120. package/dist/dialect.d.ts +17 -1
  121. package/dist/dialect.js +37 -12
  122. package/dist/generate.js +40 -7
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/mssql.js +6 -0
  126. package/dist/mysql.js +16 -0
  127. package/dist/nested-write.d.ts +2 -0
  128. package/dist/nested-write.js +415 -25
  129. package/dist/powdb.d.ts +4 -2
  130. package/dist/powdb.js +106 -21
  131. package/dist/powql.d.ts +5 -0
  132. package/dist/powql.js +42 -6
  133. package/dist/prisma-compat.d.ts +2 -0
  134. package/dist/prisma-compat.js +166 -8
  135. package/dist/query/aggregates.js +7 -3
  136. package/dist/query/builder.d.ts +292 -21
  137. package/dist/query/builder.js +570 -64
  138. package/dist/query/deferred.d.ts +39 -0
  139. package/dist/query/index.d.ts +1 -1
  140. package/dist/query/index.js +1 -1
  141. package/dist/query/relations.d.ts +173 -5
  142. package/dist/query/relations.js +688 -47
  143. package/dist/query/types.d.ts +123 -39
  144. package/dist/query/utils.d.ts +116 -0
  145. package/dist/query/utils.js +198 -0
  146. package/dist/query/warn-registry.d.ts +9 -0
  147. package/dist/query/warn-registry.js +9 -0
  148. package/dist/query/where.d.ts +38 -1
  149. package/dist/query/where.js +58 -23
  150. package/dist/query/writes.d.ts +42 -1
  151. package/dist/query/writes.js +104 -13
  152. package/dist/schema-sql.d.ts +14 -0
  153. package/dist/schema-sql.js +23 -5
  154. package/dist/schema.d.ts +38 -0
  155. package/dist/schema.js +123 -0
  156. package/dist/sqlite.js +4 -0
  157. package/package.json +77 -28
@@ -87,9 +87,11 @@ exports.OBSERVE_HTML = `<!doctype html>
87
87
  <h2>Error rates</h2>
88
88
  <div id="errors-table"></div>
89
89
  </div>
90
- <script>
90
+ <script nonce="__CSP_NONCE__">
91
91
  let currentRange = '1h';
92
- const token = document.cookie.match(/turbine_observe_token=([a-f0-9]+)/)?.[1] || '';
92
+ // Anchored on a cookie boundary so a decoy cookie whose name ends with this
93
+ // one cannot shadow the real token.
94
+ const token = document.cookie.match(/(?:^|;\\s*)turbine_observe_token=([a-f0-9]+)/)?.[1] || '';
93
95
  const headers = { 'x-turbine-token': token };
94
96
 
95
97
  document.querySelector('.controls').addEventListener('click', e => {
@@ -142,13 +144,20 @@ exports.OBSERVE_HTML = `<!doctype html>
142
144
  .replace(/'/g, '&#39;');
143
145
  }
144
146
 
147
+ // Numeric sink: counts are integer aggregates, but they are still values
148
+ // arriving over the wire, so coerce rather than interpolating them raw.
149
+ function num(v) {
150
+ const n = Number(v);
151
+ return Number.isFinite(n) ? String(n) : '0';
152
+ }
153
+
145
154
  function renderModels(data) {
146
155
  const el = document.getElementById('models-table');
147
156
  if (!data || data.length === 0) { el.innerHTML = '<p class="empty">No data yet</p>'; return; }
148
157
  let html = '<table><thead><tr><th>Model</th><th>Action</th><th class="num">Count</th><th class="num">Avg (ms)</th><th class="num">P95 (ms)</th><th class="num">P99 (ms)</th></tr></thead><tbody>';
149
158
  for (const row of data) {
150
159
  html += '<tr><td>' + escapeHtml(row.model) + '</td><td>' + escapeHtml(row.action) + '</td>'
151
- + '<td class="num">' + row.count + '</td>'
160
+ + '<td class="num">' + num(row.count) + '</td>'
152
161
  + '<td class="num">' + row.avg_ms.toFixed(1) + '</td>'
153
162
  + '<td class="num">' + row.p95_ms.toFixed(1) + '</td>'
154
163
  + '<td class="num">' + row.p99_ms.toFixed(1) + '</td></tr>';
@@ -165,8 +174,8 @@ exports.OBSERVE_HTML = `<!doctype html>
165
174
  const rate = row.count > 0 ? (row.error_count / row.count * 100).toFixed(1) : '0.0';
166
175
  const cls = parseFloat(rate) > 5 ? 'error-rate' : 'low-error';
167
176
  html += '<tr><td>' + escapeHtml(row.model) + '</td><td>' + escapeHtml(row.action) + '</td>'
168
- + '<td class="num">' + row.count + '</td>'
169
- + '<td class="num">' + row.error_count + '</td>'
177
+ + '<td class="num">' + num(row.count) + '</td>'
178
+ + '<td class="num">' + num(row.error_count) + '</td>'
170
179
  + '<td class="num ' + cls + '">' + rate + '%</td></tr>';
171
180
  }
172
181
  html += '</tbody></table>';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * turbine-orm CLI: Observe
3
+ *
4
+ * A local, read-only dashboard for viewing query metrics stored in
5
+ * _turbine_metrics. Same security model as Studio: loopback binding,
6
+ * random token, HttpOnly cookie, CSP headers, read-only transactions.
7
+ */
8
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
9
+ import pg from 'pg';
10
+ export interface ObserveOptions {
11
+ url: string;
12
+ port: number;
13
+ host: string;
14
+ openBrowser: boolean;
15
+ }
16
+ export interface ObserveServerHandle {
17
+ dispose: () => Promise<void>;
18
+ authToken: string;
19
+ url: string;
20
+ }
21
+ export declare function startObserve(options: ObserveOptions): Promise<ObserveServerHandle>;
22
+ export declare function handleRequest(req: IncomingMessage, res: ServerResponse, pool: pg.Pool, options: ObserveOptions, authToken: string, rateLimiter: Map<string, {
23
+ count: number;
24
+ resetAt: number;
25
+ }>): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * turbine-orm CLI Observe
3
+ * turbine-orm CLI: Observe
4
4
  *
5
5
  * A local, read-only dashboard for viewing query metrics stored in
6
6
  * _turbine_metrics. Same security model as Studio: loopback binding,
@@ -11,10 +11,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.startObserve = startObserve;
14
+ exports.handleRequest = handleRequest;
14
15
  const node_crypto_1 = require("node:crypto");
15
16
  const node_http_1 = require("node:http");
16
17
  const pg_1 = __importDefault(require("pg"));
17
18
  const observe_ui_js_1 = require("./observe-ui.js");
19
+ const rate_limit_js_1 = require("./rate-limit.js");
18
20
  // ---------------------------------------------------------------------------
19
21
  // Main entry point
20
22
  // ---------------------------------------------------------------------------
@@ -32,8 +34,9 @@ async function startObserve(options) {
32
34
  probe.release();
33
35
  }
34
36
  const authToken = (0, node_crypto_1.randomBytes)(24).toString('hex');
37
+ const rateLimiter = new Map();
35
38
  const server = (0, node_http_1.createServer)((req, res) => {
36
- handleRequest(req, res, pool, options, authToken).catch((err) => {
39
+ handleRequest(req, res, pool, options, authToken, rateLimiter).catch((err) => {
37
40
  sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
38
41
  });
39
42
  });
@@ -61,7 +64,7 @@ async function startObserve(options) {
61
64
  // ---------------------------------------------------------------------------
62
65
  // Request routing
63
66
  // ---------------------------------------------------------------------------
64
- async function handleRequest(req, res, pool, options, authToken) {
67
+ async function handleRequest(req, res, pool, options, authToken, rateLimiter) {
65
68
  const hostPart = options.host.includes(':') && !options.host.startsWith('[') ? `[${options.host}]` : options.host;
66
69
  const expectedOrigin = `http://${hostPart}:${options.port}`;
67
70
  const origin = req.headers.origin;
@@ -85,10 +88,21 @@ async function handleRequest(req, res, pool, options, authToken) {
85
88
  res.end();
86
89
  return;
87
90
  }
88
- sendHtml(res, 200, observe_ui_js_1.OBSERVE_HTML);
91
+ sendHtml(res, 200, observe_ui_js_1.OBSERVE_HTML, cspNonce());
89
92
  return;
90
93
  }
91
- if (!isAuthorized(req, authToken)) {
94
+ // Rate limiting: the same fixed window as Studio (shared implementation), applied
95
+ // before the auth gate so unauthenticated probing is throttled too, and keyed
96
+ // per caller so the two never share a bucket.
97
+ const authorized = isAuthorized(req, authToken);
98
+ const rateLimitResult = (0, rate_limit_js_1.checkRateLimit)(rateLimiter, `${authorized ? 'session' : 'anon'}:${(0, rate_limit_js_1.callerKey)(req)}`);
99
+ if (!rateLimitResult.allowed) {
100
+ const retryAfter = Math.ceil((rateLimitResult.resetAt - Date.now()) / 1000);
101
+ res.setHeader('Retry-After', String(retryAfter));
102
+ sendJson(res, 429, { error: 'Rate limit exceeded', retryAfter });
103
+ return;
104
+ }
105
+ if (!authorized) {
92
106
  sendJson(res, 401, { error: 'unauthorized' });
93
107
  return;
94
108
  }
@@ -109,7 +123,10 @@ function isAuthorized(req, expectedToken) {
109
123
  return true;
110
124
  }
111
125
  const cookieHeader = req.headers.cookie ?? '';
112
- const match = /turbine_observe_token=([a-f0-9]+)/.exec(cookieHeader);
126
+ // Anchored on a cookie boundary: unanchored, a decoy cookie whose name merely
127
+ // ENDS with this one (`x_turbine_observe_token=...`) matched first and the real
128
+ // token was never compared, denying the legitimate session.
129
+ const match = /(?:^|;\s*)turbine_observe_token=([a-f0-9]+)/.exec(cookieHeader);
113
130
  if (match?.[1] && constantTimeEqual(match[1], expectedToken)) {
114
131
  return true;
115
132
  }
@@ -206,19 +223,39 @@ const SECURITY_HEADERS = {
206
223
  'X-Content-Type-Options': 'nosniff',
207
224
  'X-Frame-Options': 'DENY',
208
225
  'Referrer-Policy': 'no-referrer',
209
- 'Content-Security-Policy': "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'",
210
226
  };
227
+ /**
228
+ * A fresh CSP nonce for one HTML response, matching Studio's posture: the value
229
+ * is stamped into both the header and the inline `<script nonce="...">` tag so
230
+ * script-src drops `unsafe-inline`.
231
+ */
232
+ function cspNonce() {
233
+ return (0, node_crypto_1.randomBytes)(16).toString('base64');
234
+ }
235
+ /** Non-document responses render no markup, so no inline script is allowed at all. */
236
+ const JSON_CSP = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'";
211
237
  function sendJson(res, status, body) {
212
238
  const payload = JSON.stringify(body);
213
- res.writeHead(status, { ...SECURITY_HEADERS, 'Content-Type': 'application/json' });
239
+ res.writeHead(status, {
240
+ ...SECURITY_HEADERS,
241
+ 'Content-Security-Policy': JSON_CSP,
242
+ 'Content-Type': 'application/json',
243
+ });
214
244
  res.end(payload);
215
245
  }
216
- function sendHtml(res, status, html) {
217
- res.writeHead(status, { ...SECURITY_HEADERS, 'Content-Type': 'text/html; charset=utf-8' });
218
- res.end(html);
246
+ function sendHtml(res, status, html, nonce) {
247
+ const body = html.replaceAll('__CSP_NONCE__', nonce);
248
+ res.writeHead(status, {
249
+ ...SECURITY_HEADERS,
250
+ // style-src keeps 'unsafe-inline' (the dashboard styles inline, and nonces
251
+ // do not cover style attributes); script-src moves to the per-request nonce.
252
+ 'Content-Security-Policy': `default-src 'none'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; frame-ancestors 'none'`,
253
+ 'Content-Type': 'text/html; charset=utf-8',
254
+ });
255
+ res.end(body);
219
256
  }
220
257
  function sendText(res, status, text) {
221
- res.writeHead(status, { ...SECURITY_HEADERS, 'Content-Type': 'text/plain' });
258
+ res.writeHead(status, { ...SECURITY_HEADERS, 'Content-Security-Policy': JSON_CSP, 'Content-Type': 'text/plain' });
222
259
  res.end(text);
223
260
  }
224
261
  // ---------------------------------------------------------------------------
@@ -0,0 +1,53 @@
1
+ /**
2
+ * PII tags for tools that build their schema from live introspection.
3
+ *
4
+ * `ColumnMetadata.pii` is a CODE-FIRST declaration: it comes from
5
+ * `defineSchema({ pii: true })` or the fluent `.pii()`, and `introspect.ts`
6
+ * never sets it (there is no reliable way to infer "this column holds personal
7
+ * data" from a Postgres catalog, and guessing would be worse than not trying).
8
+ *
9
+ * Studio and the MCP server introspect a live database, so on their own they
10
+ * see NO tags at all and their redaction is inert. This module closes that gap
11
+ * by reading the tags out of the generated `metadata.ts` that `turbine
12
+ * generate` writes, and handing back a table → column-name map the caller
13
+ * layers onto its introspected metadata.
14
+ *
15
+ * The generated file is TypeScript in the user's project, so it cannot simply
16
+ * be imported by a compiled CLI. It is read as TEXT and scanned for the exact
17
+ * shapes `generate.ts` emits (`serializeColumn`: one column object per line,
18
+ * `pii: true` only when tagged). Nothing is executed. A file that does not
19
+ * parse yields no tags, and the caller decides what to say about that.
20
+ */
21
+ /** Table name → the snake_case column names tagged `pii: true`. */
22
+ export type PiiTagMap = Record<string, string[]>;
23
+ export interface PiiTagSource {
24
+ /** Absolute path that was read. */
25
+ path: string;
26
+ tags: PiiTagMap;
27
+ /** Total tagged columns, across tables. */
28
+ count: number;
29
+ }
30
+ /**
31
+ * Scan generated-metadata source text for PII-tagged columns.
32
+ *
33
+ * Exported for testing; callers normally use {@link loadPiiTags}.
34
+ */
35
+ export declare function parsePiiTags(source: string): PiiTagMap;
36
+ /**
37
+ * Read PII tags from the generated metadata in `outDir`, or return `null` when
38
+ * there is no readable generated metadata there. Never throws.
39
+ */
40
+ export declare function loadPiiTags(outDir: string): PiiTagSource | null;
41
+ /**
42
+ * Apply `tags` to introspected metadata, in place. Only columns that exist in
43
+ * the live schema are tagged, so a stale generated file can never invent one.
44
+ * Returns the number of columns actually tagged.
45
+ */
46
+ export declare function applyPiiTags(metadata: {
47
+ tables: Record<string, {
48
+ columns: {
49
+ name: string;
50
+ pii?: boolean;
51
+ }[];
52
+ }>;
53
+ }, tags: PiiTagMap): number;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Render a {@link ResolutionResult} into the `prisma-migration-report.md`
3
+ * artifact and a short console summary. Pure leaf - string in, string out.
4
+ */
5
+ import type { ResolutionResult } from './prisma-resolve.js';
6
+ export interface ReportOptions {
7
+ /** Path the Prisma schema was read from (for the report header). */
8
+ schemaPath?: string;
9
+ /** Omit the volatile `Generated: <ISO>` line for reproducible output. */
10
+ noTimestamp?: boolean;
11
+ }
12
+ /**
13
+ * Build the full Markdown migration report.
14
+ */
15
+ export declare function formatPrismaReport(result: ResolutionResult, options?: ReportOptions): string;
16
+ /** One resolved many-to-many relation, named from both sides. */
17
+ interface ManyToManyCallSite {
18
+ /** `Model.field` as written in schema.prisma (what application code says). */
19
+ prismaPath: string;
20
+ /** The Prisma field name on its own (what to grep for). */
21
+ prismaField: string;
22
+ /** The Turbine relation name (what `generated/metadata.ts` says). */
23
+ turbineName: string;
24
+ /** Junction table, when it could be named. */
25
+ junction?: string;
26
+ }
27
+ /** Every resolved manyToMany relation in the result, in model/field order. */
28
+ export declare function manyToManyCallSites(result: ResolutionResult): ManyToManyCallSite[];
29
+ /** Flat list of unresolved item descriptions across the whole result. */
30
+ export declare function collectUnresolved(result: ResolutionResult): string[];
31
+ /** A one-line-per-model console summary for the CLI. */
32
+ export declare function summaryLines(result: ResolutionResult): string[];
33
+ export {};
@@ -5,6 +5,7 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.formatPrismaReport = formatPrismaReport;
8
+ exports.manyToManyCallSites = manyToManyCallSites;
8
9
  exports.collectUnresolved = collectUnresolved;
9
10
  exports.summaryLines = summaryLines;
10
11
  const CHECK = 'OK';
@@ -112,6 +113,8 @@ function formatPrismaReport(result, options = {}) {
112
113
  L.push('');
113
114
  }
114
115
  }
116
+ // ---- Many-to-many call sites -----------------------------------------
117
+ L.push(...manyToManySection(result));
115
118
  // ---- Junction tables --------------------------------------------------
116
119
  const junctions = new Set();
117
120
  for (const m of result.models) {
@@ -161,6 +164,71 @@ function formatPrismaReport(result, options = {}) {
161
164
  L.push(SEMANTIC_DIVERGENCE);
162
165
  return `${L.join('\n')}\n`;
163
166
  }
167
+ /** Every resolved manyToMany relation in the result, in model/field order. */
168
+ function manyToManyCallSites(result) {
169
+ const out = [];
170
+ for (const m of result.models) {
171
+ for (const r of m.relations) {
172
+ if (r.status !== 'resolved' || !r.turbineName)
173
+ continue;
174
+ if (!r.manyToMany && !r.junction)
175
+ continue;
176
+ out.push({
177
+ prismaPath: `${m.prismaName}.${r.prismaName}`,
178
+ prismaField: r.prismaName,
179
+ turbineName: r.turbineName,
180
+ junction: r.junction,
181
+ });
182
+ }
183
+ }
184
+ return out;
185
+ }
186
+ /**
187
+ * The many-to-many audit section.
188
+ *
189
+ * A migration audit that greps the TURBINE relation names cannot find anything:
190
+ * application code written against the compat client uses the PRISMA field
191
+ * names, and the two are related only through `PRISMA_MAP`. That is true of
192
+ * every compat integration, so the report resolves the pairing itself rather
193
+ * than leaving it to a recipe the reader has to get right.
194
+ */
195
+ function manyToManySection(result) {
196
+ const sites = manyToManyCallSites(result);
197
+ if (sites.length === 0) {
198
+ if (!result.noDb)
199
+ return [];
200
+ return [
201
+ '## Many-to-many relations (audit these call sites)',
202
+ '',
203
+ 'Not determined: many-to-many relations are recognized from the live database.',
204
+ 'Re-run without `--no-db` to get the audit list.',
205
+ '',
206
+ ];
207
+ }
208
+ const L = [];
209
+ L.push('## Many-to-many relations (audit these call sites)');
210
+ L.push('');
211
+ L.push('Turbine and Prisma name these relations differently, and your application code');
212
+ L.push('uses the PRISMA name. Grepping the Turbine relation name (for example');
213
+ L.push('`grep -rn "manyToMany" generated/`, then searching for the names it prints) finds');
214
+ L.push('nothing and silently reports a clean audit. Both names are paired below.');
215
+ L.push('');
216
+ L.push('| Prisma call site | Turbine relation | Junction table |');
217
+ L.push('| --- | --- | --- |');
218
+ for (const s of sites) {
219
+ L.push(`| \`${s.prismaPath}\` | \`${s.turbineName}\` | ${s.junction ? `\`${s.junction}\`` : '-'} |`);
220
+ }
221
+ L.push('');
222
+ L.push('Audit every write whose `data` nests one of the Prisma field names above');
223
+ L.push('(`connect`, `disconnect`, `set`, `connectOrCreate`, `create`, `update`, `upsert`,');
224
+ L.push('`delete`): those are the many-to-many writes in your codebase.');
225
+ L.push('');
226
+ L.push('```bash');
227
+ L.push(`grep -rEn "\\b(${[...new Set(sites.map((s) => s.prismaField))].sort().join('|')})\\b" src`);
228
+ L.push('```');
229
+ L.push('');
230
+ return L;
231
+ }
164
232
  /** Flat list of unresolved item descriptions across the whole result. */
165
233
  function collectUnresolved(result) {
166
234
  const out = [];
@@ -200,6 +268,11 @@ phase-2 \`turbine-orm/prisma-compat\` adapter handles most of these translations
200
268
  - Aggregate / groupBy \`_count\`. Prisma returns \`_count\` as a record
201
269
  (\`{ _all: n }\` / per-field counts). Turbine's scalar \`_count: true\` returns a
202
270
  number. Reshape as needed (the phase-2 adapter does this both directions).
271
+ - Paginated reads. Prisma appends an implicit \`ORDER BY <primary key> ASC\` to a
272
+ \`findMany\` with \`take\`/\`skip\`; core Turbine emits a bare \`LIMIT\`, which is not
273
+ deterministic (a row can appear on two pages or on none). The phase-2
274
+ \`prisma-compat\` adapter restores Prisma's ordering; on the core client, pass an
275
+ explicit \`orderBy\` or set \`implicitPkOrdering: true\`.
203
276
  - Relation-array order. Without an \`orderBy\` on a \`with\`/\`include\` clause, the
204
277
  order of a to-many relation array is unspecified in Turbine (\`json_agg\` order).
205
278
  Add an explicit \`orderBy\` where order matters.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Resolve a parsed Prisma schema against live introspected Turbine metadata.
3
+ *
4
+ * Consumes a {@link PrismaSchemaAst} (from `cli/prisma-schema.ts`) plus a
5
+ * {@link SchemaMetadata} (from `introspect()`), and produces a
6
+ * {@link ResolutionResult}: a per-model/field/relation/compound-unique
7
+ * resolution report AND the typed {@link PrismaCompatMap} that only ever
8
+ * contains VERIFIED mappings. Anything that cannot be matched against the live
9
+ * database is reported UNRESOLVED with a reason and left out of the map - the
10
+ * database is the authority, and an ambiguous guess is worse than an honest gap.
11
+ *
12
+ * Pure leaf: no filesystem, database, or process access. Passing `schema: null`
13
+ * yields a parse-only report (every item `parsed`, no map) for `--no-db`.
14
+ */
15
+ import { type PrismaCompatMap, type SchemaMetadata } from '../schema.js';
16
+ import type { PrismaSchemaAst } from './prisma-schema.js';
17
+ export { DEFAULT_EXCLUDED_TABLES } from '../introspect.js';
18
+ export type ResolveStatus = 'resolved' | 'unresolved' | 'parsed';
19
+ export interface ResolvedField {
20
+ prismaName: string;
21
+ /** Resolved Turbine field (camelCase), or null. */
22
+ turbineField: string | null;
23
+ /** Resolved snake_case database column, or null. */
24
+ column: string | null;
25
+ status: ResolveStatus;
26
+ reason?: string;
27
+ }
28
+ export interface ResolvedRelation {
29
+ prismaName: string;
30
+ /** Turbine relation name, or null. */
31
+ turbineName: string | null;
32
+ cardinality: 'one' | 'many' | null;
33
+ /** Target Prisma model this relation points at. */
34
+ targetModel: string;
35
+ /** Junction table for an m2m relation, if resolved. */
36
+ junction?: string;
37
+ /**
38
+ * True when the resolved Turbine relation is `manyToMany`. Distinct from
39
+ * {@link ResolvedRelation.junction}, which is absent when the junction table
40
+ * itself could not be named.
41
+ */
42
+ manyToMany?: boolean;
43
+ status: ResolveStatus;
44
+ reason?: string;
45
+ }
46
+ export interface ResolvedCompoundUnique {
47
+ /** Prisma selector name (explicit `name:`, else the field-name underscore-join). */
48
+ selector: string;
49
+ /** Prisma field names participating. */
50
+ prismaFields: string[];
51
+ /** Resolved Turbine field names (in order), or null. */
52
+ turbineFields: string[] | null;
53
+ kind: 'id' | 'unique';
54
+ status: ResolveStatus;
55
+ reason?: string;
56
+ }
57
+ export interface ResolvedModel {
58
+ prismaName: string;
59
+ kind: 'model' | 'view' | 'type';
60
+ /** Resolved snake_case table name, or null. */
61
+ table: string | null;
62
+ /** camelCase client accessor, or null. */
63
+ accessor: string | null;
64
+ /** True when the table name came from an explicit `@@map`. */
65
+ viaMap: boolean;
66
+ status: ResolveStatus;
67
+ reason?: string;
68
+ fields: ResolvedField[];
69
+ relations: ResolvedRelation[];
70
+ compoundUniques: ResolvedCompoundUnique[];
71
+ }
72
+ export interface ResolvedEnum {
73
+ prismaName: string;
74
+ turbineName: string | null;
75
+ status: ResolveStatus;
76
+ reason?: string;
77
+ }
78
+ export interface ResolutionResult {
79
+ models: ResolvedModel[];
80
+ enums: ResolvedEnum[];
81
+ /** The verified name map. Empty `models`/`enums` in `--no-db` mode. */
82
+ map: PrismaCompatMap;
83
+ /** True if any model/field/relation/compound-unique/enum is UNRESOLVED. */
84
+ hasUnresolved: boolean;
85
+ /** Non-fatal parser notes (skipped blocks/attributes). */
86
+ parseWarnings: string[];
87
+ /** True when resolution was skipped (`--no-db`): the report is parse-only. */
88
+ noDb: boolean;
89
+ }
90
+ /** Options controlling how names are resolved. */
91
+ export interface ResolveOptions {
92
+ /**
93
+ * Resolve field names against the raw database column names instead of the
94
+ * camelCase default, matching a client generated with `--keep-column-names`.
95
+ * Applied by running the introspected metadata through {@link withDbFieldNames}
96
+ * up front, so every resolved `turbineField` (and compound-unique
97
+ * `turbineFields`) is the DB column spelling and the emitted PRISMA_MAP agrees
98
+ * with the generated client. Table names, accessors, and relations are
99
+ * unaffected (those never carry camelCased column names).
100
+ */
101
+ keepColumnNames?: boolean;
102
+ }
103
+ /**
104
+ * Resolve `ast` against introspected `schema` (or `null` for parse-only).
105
+ */
106
+ export declare function resolvePrismaSchema(ast: PrismaSchemaAst, schema: SchemaMetadata | null, options?: ResolveOptions): ResolutionResult;
@@ -346,6 +346,7 @@ function resolveRelation(model, fieldName, targetModelName, isList, modelTable,
346
346
  turbineName: def.name,
347
347
  cardinality,
348
348
  junction: def.type === 'manyToMany' ? def.through?.table : undefined,
349
+ manyToMany: def.type === 'manyToMany' ? true : undefined,
349
350
  status: 'resolved',
350
351
  };
351
352
  }