turbine-orm 0.40.1 → 0.41.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 (63) hide show
  1. package/README.md +22 -4
  2. package/dist/cjs/cli/config.js +3 -0
  3. package/dist/cjs/cli/index.js +179 -0
  4. package/dist/cjs/cli/prisma-report.js +216 -0
  5. package/dist/cjs/cli/prisma-resolve.js +335 -0
  6. package/dist/cjs/cli/prisma-schema.js +484 -0
  7. package/dist/cjs/client.js +1 -0
  8. package/dist/cjs/generate.js +279 -22
  9. package/dist/cjs/index.js +3 -2
  10. package/dist/cjs/introspect.js +203 -26
  11. package/dist/cjs/mssql.js +9 -10
  12. package/dist/cjs/mysql.js +3 -9
  13. package/dist/cjs/powdb-introspect.js +5 -10
  14. package/dist/cjs/powql.js +13 -0
  15. package/dist/cjs/prisma-compat.js +1147 -0
  16. package/dist/cjs/query/aggregates.js +67 -7
  17. package/dist/cjs/query/builder.js +388 -17
  18. package/dist/cjs/query/compound-unique.js +0 -0
  19. package/dist/cjs/query/relations.js +7 -5
  20. package/dist/cjs/query/warn-registry.js +98 -0
  21. package/dist/cjs/query/writes.js +13 -5
  22. package/dist/cjs/schema.js +47 -0
  23. package/dist/cjs/sqlite.js +4 -9
  24. package/dist/cli/config.d.ts +26 -0
  25. package/dist/cli/config.js +3 -0
  26. package/dist/cli/index.d.ts +11 -0
  27. package/dist/cli/index.js +180 -1
  28. package/dist/cli/prisma-report.d.ts +19 -0
  29. package/dist/cli/prisma-report.js +211 -0
  30. package/dist/cli/prisma-resolve.d.ts +87 -0
  31. package/dist/cli/prisma-resolve.js +330 -0
  32. package/dist/cli/prisma-schema.d.ts +116 -0
  33. package/dist/cli/prisma-schema.js +479 -0
  34. package/dist/cli/ui.d.ts +1 -1
  35. package/dist/client.d.ts +18 -2
  36. package/dist/client.js +1 -0
  37. package/dist/generate.d.ts +80 -1
  38. package/dist/generate.js +277 -25
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.js +1 -1
  41. package/dist/introspect.d.ts +92 -2
  42. package/dist/introspect.js +198 -26
  43. package/dist/mssql.js +10 -11
  44. package/dist/mysql.js +4 -10
  45. package/dist/powdb-introspect.js +5 -10
  46. package/dist/powql.js +13 -0
  47. package/dist/prisma-compat.d.ts +281 -0
  48. package/dist/prisma-compat.js +1143 -0
  49. package/dist/query/aggregates.js +67 -7
  50. package/dist/query/builder.d.ts +77 -4
  51. package/dist/query/builder.js +390 -19
  52. package/dist/query/compound-unique.d.ts +49 -0
  53. package/dist/query/compound-unique.js +0 -0
  54. package/dist/query/deferred.d.ts +18 -0
  55. package/dist/query/relations.js +7 -5
  56. package/dist/query/types.d.ts +70 -9
  57. package/dist/query/warn-registry.d.ts +57 -0
  58. package/dist/query/warn-registry.js +92 -0
  59. package/dist/query/writes.js +13 -5
  60. package/dist/schema.d.ts +75 -0
  61. package/dist/schema.js +46 -0
  62. package/dist/sqlite.js +5 -10
  63. package/package.json +6 -1
@@ -0,0 +1,484 @@
1
+ "use strict";
2
+ /**
3
+ * Hand-rolled `schema.prisma` subset parser (zero dependencies).
4
+ *
5
+ * Powers `turbine migrate-from-prisma`. It parses ONLY the constructs the
6
+ * name-mapper needs (models, enums, views, fields, `@map`/`@@map`, relations
7
+ * including implicit m2m junctions, `@@unique` including named selectors, and
8
+ * `@@id`) and is deliberately LENIENT everywhere else: any attribute, block, or
9
+ * token it does not recognize is skipped and recorded as a warning, never a
10
+ * fatal error. The live DATABASE is the authority for resolution, so a partial
11
+ * parse is still useful.
12
+ *
13
+ * It is a pure leaf like `cli/destructive.ts` - it reads a string and returns
14
+ * data, touches no filesystem, database, or process state, and imports nothing
15
+ * from the rest of the package.
16
+ *
17
+ * Where it MUST understand a construct (an unterminated block/string, a broken
18
+ * `@@id`/`@@unique`/`@@map`/`@relation`) it throws {@link PrismaParseError} with
19
+ * a 1-based line number.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.PrismaParseError = void 0;
23
+ exports.parsePrismaSchema = parsePrismaSchema;
24
+ // ---------------------------------------------------------------------------
25
+ // Error
26
+ // ---------------------------------------------------------------------------
27
+ /** Thrown for a malformed construct the parser must understand. Carries a line number. */
28
+ class PrismaParseError extends Error {
29
+ line;
30
+ constructor(message, line) {
31
+ super(`schema.prisma line ${line}: ${message}`);
32
+ this.name = 'PrismaParseError';
33
+ this.line = line;
34
+ }
35
+ }
36
+ exports.PrismaParseError = PrismaParseError;
37
+ // ---------------------------------------------------------------------------
38
+ // Comment stripping (string-aware, offset-preserving)
39
+ // ---------------------------------------------------------------------------
40
+ /**
41
+ * Blank out `//` line comments (including `///` doc comments) with spaces,
42
+ * preserving every newline and byte offset so line numbers stay exact. A `//`
43
+ * inside a double-quoted string literal is left intact (e.g.
44
+ * `@default("http://x")`). Prisma has no block-comment syntax.
45
+ */
46
+ function stripComments(src) {
47
+ let out = '';
48
+ let i = 0;
49
+ let inString = false;
50
+ while (i < src.length) {
51
+ const ch = src[i];
52
+ if (inString) {
53
+ out += ch;
54
+ if (ch === '\\' && i + 1 < src.length) {
55
+ out += src[i + 1];
56
+ i += 2;
57
+ continue;
58
+ }
59
+ if (ch === '"')
60
+ inString = false;
61
+ i++;
62
+ continue;
63
+ }
64
+ if (ch === '"') {
65
+ inString = true;
66
+ out += ch;
67
+ i++;
68
+ continue;
69
+ }
70
+ if (ch === '/' && src[i + 1] === '/') {
71
+ // Blank to end of line, keeping the newline.
72
+ while (i < src.length && src[i] !== '\n') {
73
+ out += ' ';
74
+ i++;
75
+ }
76
+ continue;
77
+ }
78
+ out += ch;
79
+ i++;
80
+ }
81
+ return out;
82
+ }
83
+ /** 1-based line number for a character offset. */
84
+ function lineAt(src, offset) {
85
+ let line = 1;
86
+ for (let i = 0; i < offset && i < src.length; i++) {
87
+ if (src[i] === '\n')
88
+ line++;
89
+ }
90
+ return line;
91
+ }
92
+ // ---------------------------------------------------------------------------
93
+ // Attribute-argument tokenizer
94
+ // ---------------------------------------------------------------------------
95
+ /** Unquote a `"..."` literal, resolving the escapes Prisma supports. */
96
+ function unquote(raw) {
97
+ const s = raw.trim();
98
+ if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
99
+ return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\').replace(/\\n/g, '\n').replace(/\\t/g, '\t');
100
+ }
101
+ return s;
102
+ }
103
+ /**
104
+ * Split a balanced attribute-argument body on top-level commas, respecting
105
+ * nested `(...)`, `[...]`, and `"..."`. Returns the raw comma-separated pieces.
106
+ */
107
+ function splitTopLevel(body) {
108
+ const parts = [];
109
+ let depth = 0;
110
+ let inString = false;
111
+ let cur = '';
112
+ for (let i = 0; i < body.length; i++) {
113
+ const ch = body[i];
114
+ if (inString) {
115
+ cur += ch;
116
+ if (ch === '\\' && i + 1 < body.length) {
117
+ cur += body[i + 1];
118
+ i++;
119
+ continue;
120
+ }
121
+ if (ch === '"')
122
+ inString = false;
123
+ continue;
124
+ }
125
+ if (ch === '"') {
126
+ inString = true;
127
+ cur += ch;
128
+ continue;
129
+ }
130
+ if (ch === '(' || ch === '[' || ch === '{')
131
+ depth++;
132
+ else if (ch === ')' || ch === ']' || ch === '}')
133
+ depth--;
134
+ if (ch === ',' && depth === 0) {
135
+ parts.push(cur);
136
+ cur = '';
137
+ continue;
138
+ }
139
+ cur += ch;
140
+ }
141
+ if (cur.trim() !== '')
142
+ parts.push(cur);
143
+ return parts;
144
+ }
145
+ /** Index of the first top-level `:` (outside strings/brackets), or -1. */
146
+ function topLevelColon(s) {
147
+ let depth = 0;
148
+ let inString = false;
149
+ for (let i = 0; i < s.length; i++) {
150
+ const ch = s[i];
151
+ if (inString) {
152
+ if (ch === '\\') {
153
+ i++;
154
+ continue;
155
+ }
156
+ if (ch === '"')
157
+ inString = false;
158
+ continue;
159
+ }
160
+ if (ch === '"')
161
+ inString = true;
162
+ else if (ch === '(' || ch === '[' || ch === '{')
163
+ depth++;
164
+ else if (ch === ')' || ch === ']' || ch === '}')
165
+ depth--;
166
+ else if (ch === ':' && depth === 0)
167
+ return i;
168
+ }
169
+ return -1;
170
+ }
171
+ /** Parse one argument piece into a {@link PrismaAttrArg}. */
172
+ function parseArg(piece) {
173
+ const trimmed = piece.trim();
174
+ // Named arg? `key: value` where the colon is at top level (not inside a
175
+ // string). A bare `http://x` string literal is already inside quotes, so a
176
+ // top-level colon before a quote/bracket is a named key.
177
+ let key;
178
+ let rest = trimmed;
179
+ const colonIdx = topLevelColon(trimmed);
180
+ if (colonIdx !== -1) {
181
+ const maybeKey = trimmed.slice(0, colonIdx).trim();
182
+ if (/^[a-zA-Z_]\w*$/.test(maybeKey)) {
183
+ key = maybeKey;
184
+ rest = trimmed.slice(colonIdx + 1).trim();
185
+ }
186
+ }
187
+ if (rest.startsWith('[')) {
188
+ const inner = rest.slice(1, rest.lastIndexOf(']'));
189
+ const items = splitTopLevel(inner)
190
+ .map((el) => unquote(el.trim()))
191
+ .filter((el) => el !== '');
192
+ return { key, kind: 'array', items };
193
+ }
194
+ if (rest.startsWith('"')) {
195
+ return { key, kind: 'string', value: unquote(rest) };
196
+ }
197
+ return { key, kind: 'raw', value: rest };
198
+ }
199
+ /** Find the index of the `)` matching the `(` at `open`, respecting strings/nesting. */
200
+ function matchParen(s, open) {
201
+ let depth = 0;
202
+ let inString = false;
203
+ for (let i = open; i < s.length; i++) {
204
+ const ch = s[i];
205
+ if (inString) {
206
+ if (ch === '\\') {
207
+ i++;
208
+ continue;
209
+ }
210
+ if (ch === '"')
211
+ inString = false;
212
+ continue;
213
+ }
214
+ if (ch === '"')
215
+ inString = true;
216
+ else if (ch === '(')
217
+ depth++;
218
+ else if (ch === ')') {
219
+ depth--;
220
+ if (depth === 0)
221
+ return i;
222
+ }
223
+ }
224
+ return -1;
225
+ }
226
+ /**
227
+ * Scan a fragment (the tail of a field line, or a block-attribute line) for
228
+ * attributes. Starts at each `@`, reads the attribute name, and, when followed
229
+ * by `(`, captures the balanced parenthesized body.
230
+ */
231
+ function parseAttributes(fragment, line) {
232
+ const attrs = [];
233
+ let i = 0;
234
+ while (i < fragment.length) {
235
+ if (fragment[i] !== '@') {
236
+ i++;
237
+ continue;
238
+ }
239
+ const block = fragment[i + 1] === '@';
240
+ let j = i + (block ? 2 : 1);
241
+ const nameStart = j;
242
+ while (j < fragment.length && /[\w.]/.test(fragment[j]))
243
+ j++;
244
+ const rawName = fragment.slice(nameStart, j);
245
+ if (rawName === '') {
246
+ i = j + 1;
247
+ continue;
248
+ }
249
+ let args = [];
250
+ // Skip spaces between the name and an optional '('.
251
+ let k = j;
252
+ while (k < fragment.length && (fragment[k] === ' ' || fragment[k] === '\t'))
253
+ k++;
254
+ if (fragment[k] === '(') {
255
+ const close = matchParen(fragment, k);
256
+ if (close === -1) {
257
+ throw new PrismaParseError(`unterminated "(" in attribute @${block ? '@' : ''}${rawName}`, line);
258
+ }
259
+ const body = fragment.slice(k + 1, close);
260
+ args = splitTopLevel(body).map(parseArg);
261
+ j = close + 1;
262
+ }
263
+ else {
264
+ j = k;
265
+ }
266
+ // `@db.VarChar(255)` etc. - keep only the head so `db` is the recorded name.
267
+ attrs.push({ name: rawName.split('.')[0], args, block, line });
268
+ i = j;
269
+ }
270
+ return attrs;
271
+ }
272
+ const BLOCK_KEYWORDS = new Set(['model', 'view', 'type', 'enum', 'datasource', 'generator']);
273
+ /** Find the `}` matching the `{` at `open`, respecting strings. */
274
+ function matchBrace(s, open) {
275
+ let depth = 0;
276
+ let inString = false;
277
+ for (let i = open; i < s.length; i++) {
278
+ const ch = s[i];
279
+ if (inString) {
280
+ if (ch === '\\') {
281
+ i++;
282
+ continue;
283
+ }
284
+ if (ch === '"')
285
+ inString = false;
286
+ continue;
287
+ }
288
+ if (ch === '"')
289
+ inString = true;
290
+ else if (ch === '{')
291
+ depth++;
292
+ else if (ch === '}') {
293
+ depth--;
294
+ if (depth === 0)
295
+ return i;
296
+ }
297
+ }
298
+ return -1;
299
+ }
300
+ /** Scan the top level for `keyword Name { ... }` blocks via brace matching. */
301
+ function scanBlocks(src) {
302
+ const blocks = [];
303
+ const headerRe = /(^|\n)[ \t]*([a-zA-Z]+)[ \t]+([A-Za-z_]\w*)[ \t]*\{/g;
304
+ let m;
305
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
306
+ while ((m = headerRe.exec(src)) !== null) {
307
+ const keyword = m[2];
308
+ if (!BLOCK_KEYWORDS.has(keyword))
309
+ continue;
310
+ const braceOpen = src.indexOf('{', m.index);
311
+ const close = matchBrace(src, braceOpen);
312
+ const headerLine = lineAt(src, m.index + m[1].length);
313
+ if (close === -1) {
314
+ throw new PrismaParseError(`unterminated "{" for ${keyword} ${m[3]}`, headerLine);
315
+ }
316
+ blocks.push({
317
+ keyword,
318
+ name: m[3],
319
+ body: src.slice(braceOpen + 1, close),
320
+ headerLine,
321
+ bodyOffset: braceOpen + 1,
322
+ });
323
+ headerRe.lastIndex = close + 1;
324
+ }
325
+ return blocks;
326
+ }
327
+ // ---------------------------------------------------------------------------
328
+ // Body parsing
329
+ // ---------------------------------------------------------------------------
330
+ /**
331
+ * Split a block body into logical lines, keeping each line's absolute source
332
+ * offset so we can report exact line numbers. Prisma fields and block
333
+ * attributes are single-line.
334
+ */
335
+ function bodyLines(body, bodyOffset, src) {
336
+ const out = [];
337
+ let offset = 0;
338
+ for (const rawLine of body.split('\n')) {
339
+ const text = rawLine.trim();
340
+ if (text !== '')
341
+ out.push({ text, line: lineAt(src, bodyOffset + offset) });
342
+ offset += rawLine.length + 1; // + newline
343
+ }
344
+ return out;
345
+ }
346
+ /** Turn a `@@id` / `@@unique` attribute into a {@link PrismaCompoundKey}. */
347
+ function parseCompoundKey(attr, line) {
348
+ const kind = attr.name === 'id' ? 'id' : 'unique';
349
+ // Field list is the first array-kind arg (positional `[a, b]`) or a
350
+ // `fields: [a, b]` named arg.
351
+ const fieldsArg = attr.args.find((a) => a.kind === 'array' && (a.key === undefined || a.key === 'fields'));
352
+ if (!fieldsArg?.items || fieldsArg.items.length === 0) {
353
+ throw new PrismaParseError(`@@${attr.name} requires a field list, e.g. @@${attr.name}([a, b])`, line);
354
+ }
355
+ const nameArg = attr.args.find((a) => a.key === 'name');
356
+ const mapArg = attr.args.find((a) => a.key === 'map');
357
+ return {
358
+ fields: fieldsArg.items,
359
+ name: nameArg?.kind === 'string' ? nameArg.value : undefined,
360
+ map: mapArg?.kind === 'string' ? mapArg.value : undefined,
361
+ kind,
362
+ line,
363
+ };
364
+ }
365
+ function truncate(s, n = 60) {
366
+ return s.length > n ? `${s.slice(0, n)}...` : s;
367
+ }
368
+ /** Parse a single field declaration line. Returns null for a non-field line. */
369
+ function parseFieldLine(text, line, warnings) {
370
+ // First token = field name, second token = type. Both are simple words; the
371
+ // type may carry a trailing `[]` and/or `?`.
372
+ const m = text.match(/^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)(\[\])?(\?)?/);
373
+ if (!m) {
374
+ // Not a field (e.g. a stray token); skip leniently.
375
+ warnings.push(`Skipped unrecognized line ${line}: "${truncate(text)}"`);
376
+ return null;
377
+ }
378
+ const name = m[1];
379
+ const type = m[2];
380
+ const isList = m[3] === '[]';
381
+ const optional = m[4] === '?';
382
+ const rest = text.slice(m[0].length);
383
+ const attrs = parseAttributes(rest, line);
384
+ return { name, type, optional, isList, attrs, line };
385
+ }
386
+ function parseModelBody(block, kind, src, warnings) {
387
+ const model = {
388
+ name: block.name,
389
+ kind,
390
+ fields: [],
391
+ compoundKeys: [],
392
+ blockAttrs: [],
393
+ line: block.headerLine,
394
+ };
395
+ for (const { text, line } of bodyLines(block.body, block.bodyOffset, src)) {
396
+ if (text.startsWith('@@')) {
397
+ const attrs = parseAttributes(text, line);
398
+ for (const attr of attrs) {
399
+ model.blockAttrs.push(attr);
400
+ if (attr.name === 'map') {
401
+ const arg = attr.args.find((a) => a.key === undefined || a.key === 'name');
402
+ if (arg?.kind !== 'string' || !arg.value) {
403
+ throw new PrismaParseError(`@@map requires a quoted table name`, line);
404
+ }
405
+ model.map = arg.value;
406
+ }
407
+ else if (attr.name === 'id' || attr.name === 'unique') {
408
+ model.compoundKeys.push(parseCompoundKey(attr, line));
409
+ }
410
+ // @@index, @@schema, and anything else: recorded in blockAttrs, unused.
411
+ }
412
+ continue;
413
+ }
414
+ // A field line: `name Type[modifiers] @attr @attr(...)`.
415
+ const field = parseFieldLine(text, line, warnings);
416
+ if (field)
417
+ model.fields.push(field);
418
+ }
419
+ return model;
420
+ }
421
+ function parseEnumBody(block, src) {
422
+ const en = { name: block.name, values: [], line: block.headerLine };
423
+ for (const { text, line } of bodyLines(block.body, block.bodyOffset, src)) {
424
+ if (text.startsWith('@@')) {
425
+ for (const attr of parseAttributes(text, line)) {
426
+ if (attr.name === 'map') {
427
+ const arg = attr.args.find((a) => a.key === undefined);
428
+ if (arg?.kind === 'string' && arg.value)
429
+ en.map = arg.value;
430
+ }
431
+ }
432
+ continue;
433
+ }
434
+ const m = text.match(/^([A-Za-z_]\w*)/);
435
+ if (m)
436
+ en.values.push(m[1]);
437
+ }
438
+ return en;
439
+ }
440
+ // ---------------------------------------------------------------------------
441
+ // Entry point
442
+ // ---------------------------------------------------------------------------
443
+ /**
444
+ * Parse a `schema.prisma` source string into a {@link PrismaSchemaAst}.
445
+ *
446
+ * Understands: model / view / type / enum blocks; field lines with `@map`,
447
+ * `@id`, `@unique`, `@default`, `@updatedAt`, `@ignore`, `@relation`; and block
448
+ * attributes `@@map`, `@@id`, `@@unique`, `@@index`, `@@schema`. Unknown
449
+ * attributes and blocks are skipped into {@link PrismaSchemaAst.warnings}.
450
+ *
451
+ * @throws {@link PrismaParseError} on an unterminated block/paren/string or a
452
+ * structurally broken `@@id` / `@@unique` / `@@map`.
453
+ */
454
+ function parsePrismaSchema(source) {
455
+ const src = stripComments(source);
456
+ const ast = { models: [], enums: [], warnings: [] };
457
+ for (const block of scanBlocks(src)) {
458
+ switch (block.keyword) {
459
+ case 'model':
460
+ ast.models.push(parseModelBody(block, 'model', src, ast.warnings));
461
+ break;
462
+ case 'view':
463
+ ast.models.push(parseModelBody(block, 'view', src, ast.warnings));
464
+ break;
465
+ case 'type':
466
+ // Composite/embedded types (MongoDB) are not tables. Parse leniently so
467
+ // relation fields typed as such a model still resolve, but record a note.
468
+ ast.models.push(parseModelBody(block, 'type', src, ast.warnings));
469
+ ast.warnings.push(`Block "type ${block.name}" parsed but not resolved (composite types are not tables).`);
470
+ break;
471
+ case 'enum':
472
+ ast.enums.push(parseEnumBody(block, src));
473
+ break;
474
+ case 'datasource':
475
+ case 'generator':
476
+ // Configuration blocks - irrelevant to name mapping.
477
+ break;
478
+ default:
479
+ ast.warnings.push(`Skipped unsupported block "${block.keyword} ${block.name}".`);
480
+ break;
481
+ }
482
+ }
483
+ return ast;
484
+ }
@@ -370,6 +370,7 @@ class TurbineClient {
370
370
  warnOnUnlimited: config.warnOnUnlimited,
371
371
  utcTimestamps: config.utcTimestamps,
372
372
  relationLoadStrategy: config.relationLoadStrategy,
373
+ stableRelationOrder: config.stableRelationOrder,
373
374
  jsonEncoding: config.jsonEncoding,
374
375
  globalFilters: config.globalFilters,
375
376
  preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),