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
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Output goes to the specified directory (default: ./generated/turbine/).
10
10
  */
11
- import { type SchemaMetadata } from './schema.js';
11
+ import { type PrismaCompatMap, type SchemaMetadata } from './schema.js';
12
12
  export interface GenerateOptions {
13
13
  /** The introspected schema to generate from */
14
14
  schema: SchemaMetadata;
@@ -30,16 +30,85 @@ export interface GenerateOptions {
30
30
  * diffs. Default: `false` (timestamp included, unchanged behavior).
31
31
  */
32
32
  noTimestamp?: boolean;
33
+ /**
34
+ * Which extension the generated `index.ts` uses on its sibling import
35
+ * specifiers (`./types`, `./metadata`). F3.
36
+ *
37
+ * - `'js'`: always `./types.js` (required by NodeNext `tsc` and by
38
+ * tsc-compiled ESM run on Node; the pre-0.41 behavior).
39
+ * - `'none'`: always `./types` (correct for bundlers / `moduleResolution
40
+ * bundler | node10`: webpack, Next.js/SWC, Vite/esbuild).
41
+ * - `'auto'` (default): walk up from `outDir` to the nearest `tsconfig.json`
42
+ * (extends chains are NOT followed) and emit `'.js'` when its `module` /
43
+ * `moduleResolution` is `node16`/`nodenext`, `''` when both are present and
44
+ * neither is, and fall back to `'.js'` when the tsconfig is missing,
45
+ * unparseable, or ambiguous (so NodeNext consumers can never regress).
46
+ */
47
+ importExtension?: 'js' | 'none' | 'auto';
48
+ /**
49
+ * Rewrite generated column FIELD names to the raw database column names
50
+ * (snake_case) instead of camelCase (F4). Opt-in; when unset the output is
51
+ * byte-identical to before. Implemented as the pure generate-time
52
+ * {@link withDbFieldNames} transform (identity `columnMap`/`reverseColumnMap`,
53
+ * zero runtime changes). Relation names, table accessors, and entity type
54
+ * names are unaffected.
55
+ */
56
+ keepColumnNames?: boolean;
33
57
  }
34
58
  /** Per-file generator options (subset of {@link GenerateOptions} the emitters need). */
35
59
  export interface GenerateFileOptions {
36
60
  /** Omit the `Generated at:` header line for reproducible output. */
37
61
  noTimestamp?: boolean;
62
+ /**
63
+ * Resolved sibling-import extension for `generateIndex` (`'.js'` or `''`).
64
+ * Defaults to `'.js'` when unset so direct callers stay byte-stable.
65
+ */
66
+ importExt?: string;
67
+ /** Human-readable resolved import mode, recorded as a comment in `index.ts`. */
68
+ importMode?: string;
38
69
  }
39
70
  export declare function generate(options: GenerateOptions): {
40
71
  outDir: string;
41
72
  files: string[];
42
73
  };
74
+ /**
75
+ * Resolve the requested {@link GenerateOptions.importExtension} to the concrete
76
+ * extension string used on `index.ts`'s sibling imports plus a human-readable
77
+ * mode label for the generated comment.
78
+ *
79
+ * - `'js'` → `.js`
80
+ * - `'none'` → `''`
81
+ * - `'auto'` → tsconfig-driven ({@link detectTsconfigExtension}); falls back
82
+ * to `.js` (the pre-0.41 default) when detection is uncertain, so NodeNext
83
+ * consumers can never regress.
84
+ */
85
+ export declare function resolveImportExtension(outDir: string, requested: 'js' | 'none' | 'auto'): {
86
+ ext: string;
87
+ mode: string;
88
+ };
89
+ /**
90
+ * Walk up from `startDir` to the nearest `tsconfig.json` and classify its module
91
+ * resolution. Does NOT follow `extends` chains (a monorepo whose base config
92
+ * sets `moduleResolution` needs the explicit flag). Returns:
93
+ * - `'js'`: `module`/`moduleResolution` is `node16`/`nodenext`;
94
+ * - `'none'`: both fields present and neither is node16/nodenext (bundler,
95
+ * node, node10, classic, esnext, commonjs, preserve);
96
+ * - `null`: no tsconfig found, unparseable, or the fields are absent
97
+ * (possibly hidden behind `extends`) → caller falls back to `.js`.
98
+ */
99
+ export declare function detectTsconfigExtension(startDir: string): 'js' | 'none' | null;
100
+ /**
101
+ * Classify a tsconfig's `compilerOptions.module` / `moduleResolution` (see
102
+ * {@link detectTsconfigExtension}). Tolerant of `//` and block comments and
103
+ * trailing commas (real-world tsconfigs use JSONC).
104
+ */
105
+ export declare function classifyTsconfig(text: string): 'js' | 'none' | null;
106
+ /**
107
+ * Strip `//` line comments and block comments from JSONC text, leaving anything
108
+ * inside a double-quoted string untouched. Trailing commas are then removed so
109
+ * `JSON.parse` accepts the result.
110
+ */
111
+ export declare function stripJsonComments(text: string): string;
43
112
  /**
44
113
  * Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
45
114
  * and *Relations brand-field interfaces). Exported so tests can pin the
@@ -56,3 +125,13 @@ export declare function generateTypes(schema: SchemaMetadata, options?: Generate
56
125
  export declare function generateZod(schema: SchemaMetadata, options?: GenerateFileOptions): string;
57
126
  export declare function generateMetadata(schema: SchemaMetadata, options?: GenerateFileOptions): string;
58
127
  export declare function generateIndex(schema: SchemaMetadata, options?: GenerateFileOptions): string;
128
+ /**
129
+ * Serialize a resolved {@link PrismaCompatMap} into a `prisma-map.ts` module
130
+ * that exports `export const PRISMA_MAP: PrismaCompatMap = {...}`. Written next
131
+ * to the generated client by `turbine migrate-from-prisma`; consumed later by
132
+ * the phase-2 `turbine-orm/prisma-compat` runtime adapter.
133
+ *
134
+ * Deterministic: uses the same reproducible header as the other emitters, so a
135
+ * stable input regenerates byte-identical output under `noTimestamp`.
136
+ */
137
+ export declare function generatePrismaMap(map: PrismaCompatMap, options?: GenerateFileOptions): string;
package/dist/generate.js CHANGED
@@ -8,9 +8,9 @@
8
8
  *
9
9
  * Output goes to the specified directory (default: ./generated/turbine/).
10
10
  */
11
- import { mkdirSync, writeFileSync } from 'node:fs';
12
- import { join, relative, resolve } from 'node:path';
13
- import { pgTypeToTs, singularize, snakeToPascal, } from './schema.js';
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join, relative, resolve } from 'node:path';
13
+ import { pgTypeToTs, singularize, snakeToPascal, withDbFieldNames, } from './schema.js';
14
14
  /** Get the TypeScript type name for a table (singularized PascalCase) */
15
15
  function entityName(tableName) {
16
16
  return snakeToPascal(singularize(tableName));
@@ -49,29 +49,177 @@ export function generate(options) {
49
49
  throw new Error(`Output directory must be within the project root. Got: ${outDir}`);
50
50
  }
51
51
  mkdirSync(outDir, { recursive: true });
52
+ // F4: keep raw DB column names as field names (opt-in). Pure transform: when
53
+ // the flag is off `withDbFieldNames` is never called and output is unchanged.
54
+ const schema = options.keepColumnNames ? withDbFieldNames(options.schema) : options.schema;
55
+ // F3: resolve which extension sibling imports get in index.ts.
56
+ const { ext: importExt, mode: importMode } = resolveImportExtension(outDir, options.importExtension ?? 'auto');
52
57
  const files = [];
53
- const fileOptions = { noTimestamp: options.noTimestamp };
58
+ const fileOptions = { noTimestamp: options.noTimestamp, importExt, importMode };
54
59
  // Generate types.ts
55
- const typesContent = generateTypes(options.schema, fileOptions);
60
+ const typesContent = generateTypes(schema, fileOptions);
56
61
  writeFileSync(join(outDir, 'types.ts'), typesContent, 'utf-8');
57
62
  files.push('types.ts');
58
63
  // Generate metadata.ts
59
- const metadataContent = generateMetadata(options.schema, fileOptions);
64
+ const metadataContent = generateMetadata(schema, fileOptions);
60
65
  writeFileSync(join(outDir, 'metadata.ts'), metadataContent, 'utf-8');
61
66
  files.push('metadata.ts');
62
67
  // Generate index.ts (configured client)
63
- const indexContent = generateIndex(options.schema, fileOptions);
68
+ const indexContent = generateIndex(schema, fileOptions);
64
69
  writeFileSync(join(outDir, 'index.ts'), indexContent, 'utf-8');
65
70
  files.push('index.ts');
66
71
  // Generate zod.ts (optional — --zod flag)
67
72
  if (options.zod) {
68
- const zodContent = generateZod(options.schema, fileOptions);
73
+ const zodContent = generateZod(schema, fileOptions);
69
74
  writeFileSync(join(outDir, 'zod.ts'), zodContent, 'utf-8');
70
75
  files.push('zod.ts');
71
76
  }
72
77
  return { outDir, files };
73
78
  }
74
79
  // ---------------------------------------------------------------------------
80
+ // Import-extension resolution (F3)
81
+ // ---------------------------------------------------------------------------
82
+ /**
83
+ * Resolve the requested {@link GenerateOptions.importExtension} to the concrete
84
+ * extension string used on `index.ts`'s sibling imports plus a human-readable
85
+ * mode label for the generated comment.
86
+ *
87
+ * - `'js'` → `.js`
88
+ * - `'none'` → `''`
89
+ * - `'auto'` → tsconfig-driven ({@link detectTsconfigExtension}); falls back
90
+ * to `.js` (the pre-0.41 default) when detection is uncertain, so NodeNext
91
+ * consumers can never regress.
92
+ */
93
+ export function resolveImportExtension(outDir, requested) {
94
+ if (requested === 'js')
95
+ return { ext: '.js', mode: 'js' };
96
+ if (requested === 'none')
97
+ return { ext: '', mode: 'none' };
98
+ const detected = detectTsconfigExtension(resolve(outDir));
99
+ if (detected === 'js')
100
+ return { ext: '.js', mode: 'auto (nodenext → .js)' };
101
+ if (detected === 'none')
102
+ return { ext: '', mode: 'auto (bundler → no extension)' };
103
+ return { ext: '.js', mode: 'auto (fallback → .js)' };
104
+ }
105
+ /**
106
+ * Walk up from `startDir` to the nearest `tsconfig.json` and classify its module
107
+ * resolution. Does NOT follow `extends` chains (a monorepo whose base config
108
+ * sets `moduleResolution` needs the explicit flag). Returns:
109
+ * - `'js'`: `module`/`moduleResolution` is `node16`/`nodenext`;
110
+ * - `'none'`: both fields present and neither is node16/nodenext (bundler,
111
+ * node, node10, classic, esnext, commonjs, preserve);
112
+ * - `null`: no tsconfig found, unparseable, or the fields are absent
113
+ * (possibly hidden behind `extends`) → caller falls back to `.js`.
114
+ */
115
+ export function detectTsconfigExtension(startDir) {
116
+ let dir = startDir;
117
+ for (;;) {
118
+ const candidate = join(dir, 'tsconfig.json');
119
+ if (existsSync(candidate)) {
120
+ try {
121
+ return classifyTsconfig(readFileSync(candidate, 'utf-8'));
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ const parent = dirname(dir);
128
+ if (parent === dir)
129
+ return null; // reached the filesystem root
130
+ dir = parent;
131
+ }
132
+ }
133
+ /**
134
+ * Classify a tsconfig's `compilerOptions.module` / `moduleResolution` (see
135
+ * {@link detectTsconfigExtension}). Tolerant of `//` and block comments and
136
+ * trailing commas (real-world tsconfigs use JSONC).
137
+ */
138
+ export function classifyTsconfig(text) {
139
+ let parsed;
140
+ try {
141
+ parsed = JSON.parse(stripJsonComments(text));
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ if (typeof parsed !== 'object' || parsed === null)
147
+ return null;
148
+ const co = parsed.compilerOptions;
149
+ if (typeof co !== 'object' || co === null)
150
+ return null;
151
+ const opts = co;
152
+ const mod = typeof opts.module === 'string' ? opts.module.toLowerCase() : undefined;
153
+ const modRes = typeof opts.moduleResolution === 'string' ? opts.moduleResolution.toLowerCase() : undefined;
154
+ const isNodeNext = (v) => v === 'node16' || v === 'nodenext';
155
+ if (isNodeNext(mod) || isNodeNext(modRes))
156
+ return 'js';
157
+ if (mod !== undefined && modRes !== undefined)
158
+ return 'none';
159
+ return null;
160
+ }
161
+ /**
162
+ * Strip `//` line comments and block comments from JSONC text, leaving anything
163
+ * inside a double-quoted string untouched. Trailing commas are then removed so
164
+ * `JSON.parse` accepts the result.
165
+ */
166
+ export function stripJsonComments(text) {
167
+ let out = '';
168
+ let inString = false;
169
+ let inLine = false;
170
+ let inBlock = false;
171
+ for (let i = 0; i < text.length; i++) {
172
+ const ch = text[i];
173
+ const next = text[i + 1];
174
+ if (inLine) {
175
+ if (ch === '\n') {
176
+ inLine = false;
177
+ out += ch;
178
+ }
179
+ continue;
180
+ }
181
+ if (inBlock) {
182
+ if (ch === '*' && next === '/') {
183
+ inBlock = false;
184
+ i++;
185
+ }
186
+ continue;
187
+ }
188
+ if (inString) {
189
+ out += ch;
190
+ if (ch === '\\') {
191
+ // Preserve the escaped character verbatim.
192
+ if (next !== undefined) {
193
+ out += next;
194
+ i++;
195
+ }
196
+ }
197
+ else if (ch === '"') {
198
+ inString = false;
199
+ }
200
+ continue;
201
+ }
202
+ if (ch === '"') {
203
+ inString = true;
204
+ out += ch;
205
+ continue;
206
+ }
207
+ if (ch === '/' && next === '/') {
208
+ inLine = true;
209
+ i++;
210
+ continue;
211
+ }
212
+ if (ch === '/' && next === '*') {
213
+ inBlock = true;
214
+ i++;
215
+ continue;
216
+ }
217
+ out += ch;
218
+ }
219
+ // Drop trailing commas before } or ].
220
+ return out.replace(/,(\s*[}\]])/g, '$1');
221
+ }
222
+ // ---------------------------------------------------------------------------
75
223
  // types.ts generator
76
224
  // ---------------------------------------------------------------------------
77
225
  function generatedFileHeader(options) {
@@ -167,7 +315,7 @@ export function generateTypes(schema, options) {
167
315
  const piiNote = col.pii ? ' (PII: absent unless selected or includePii)' : '';
168
316
  const optional = col.pii ? '?' : '';
169
317
  lines.push(` /** Column: ${col.name}, ${col.pgType}${pkNote}${nullNote}${piiNote} */`);
170
- lines.push(` ${col.field}${optional}: ${columnTsType(col, schema.enums)};`);
318
+ lines.push(` ${quoteIfNeeded(col.field)}${optional}: ${columnTsType(col, schema.enums)};`);
171
319
  }
172
320
  lines.push('}');
173
321
  lines.push('');
@@ -185,10 +333,10 @@ export function generateTypes(schema, options) {
185
333
  if (isOptional) {
186
334
  const reason = isPk ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
187
335
  lines.push(` /** Optional: ${reason} */`);
188
- lines.push(` ${col.field}?: ${columnTsType(col, schema.enums)};`);
336
+ lines.push(` ${quoteIfNeeded(col.field)}?: ${columnTsType(col, schema.enums)};`);
189
337
  }
190
338
  else {
191
- lines.push(` ${col.field}: ${columnTsType(col, schema.enums)};`);
339
+ lines.push(` ${quoteIfNeeded(col.field)}: ${columnTsType(col, schema.enums)};`);
192
340
  }
193
341
  }
194
342
  lines.push('};');
@@ -200,7 +348,7 @@ export function generateTypes(schema, options) {
200
348
  lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
201
349
  lines.push(`export type ${typeName}Update = {`);
202
350
  for (const col of nonPkCols) {
203
- lines.push(` ${col.field}?: ${updateFieldType(columnTsType(col, schema.enums))};`);
351
+ lines.push(` ${quoteIfNeeded(col.field)}?: ${updateFieldType(columnTsType(col, schema.enums))};`);
204
352
  }
205
353
  lines.push('};');
206
354
  lines.push('');
@@ -269,15 +417,54 @@ export function generateTypes(schema, options) {
269
417
  }
270
418
  }
271
419
  if (uniqueSets.length > 0) {
272
- const branches = uniqueSets.map((cols) => {
420
+ const memberType = (colName) => {
421
+ const col = table.columns.find((c) => c.name === colName);
422
+ return { field: col?.field ?? colName, tsType: col?.tsType ?? 'unknown' };
423
+ };
424
+ // Flat branches: one object per unique constraint carrying its columns.
425
+ const flatBranches = uniqueSets.map((cols) => {
273
426
  const fields = cols.map((colName) => {
274
- const col = table.columns.find((c) => c.name === colName);
275
- const field = col?.field ?? colName;
276
- const tsType = col?.tsType ?? 'unknown';
277
- return `${field}: ${tsType}`;
427
+ const m = memberType(colName);
428
+ return `${quoteIfNeeded(m.field)}: ${m.tsType}`;
278
429
  });
279
430
  return `{ ${fields.join('; ')} }`;
280
431
  });
432
+ // Prisma-style compound-unique SELECTOR branches: a synthetic key
433
+ // (`orgId_userId`) whose value holds the member columns, emitted for every
434
+ // COMPOSITE unique (PK, composite UNIQUE constraint, or composite UNIQUE
435
+ // index). Runtime expansion lives in query/compound-unique.ts.
436
+ const compoundSeen = new Set();
437
+ const compoundSets = [];
438
+ const addCompound = (cols) => {
439
+ if (cols.length < 2)
440
+ return;
441
+ const key = cols.join(',');
442
+ if (compoundSeen.has(key))
443
+ return;
444
+ compoundSeen.add(key);
445
+ compoundSets.push(cols);
446
+ };
447
+ addCompound(table.primaryKey);
448
+ for (const uc of table.uniqueColumns)
449
+ addCompound(uc);
450
+ for (const idx of table.indexes) {
451
+ if (idx.unique && !idx.docPath)
452
+ addCompound(idx.columns);
453
+ }
454
+ const selectorEntries = compoundSets.map((cols) => {
455
+ const members = cols.map(memberType);
456
+ return {
457
+ selectorName: members.map((m) => m.field).join('_'),
458
+ memberType: `{ ${members.map((m) => `${quoteIfNeeded(m.field)}: ${m.tsType}`).join('; ')} }`,
459
+ };
460
+ });
461
+ // Named helper type for annotating a compound selector by hand (Prisma parity).
462
+ if (selectorEntries.length > 0) {
463
+ const cuFields = selectorEntries.map((e) => `${e.selectorName}: ${e.memberType}`);
464
+ lines.push(`export type ${typeName}CompoundUniques = { ${cuFields.join('; ')} };`);
465
+ }
466
+ const selectorBranches = selectorEntries.map((e) => `{ ${e.selectorName}: ${e.memberType} }`);
467
+ const branches = [...flatBranches, ...selectorBranches];
281
468
  lines.push(`export type ${typeName}WhereUnique = ${branches.join(' | ')};`);
282
469
  lines.push('');
283
470
  }
@@ -407,7 +594,7 @@ export function generateZod(schema, options) {
407
594
  let expr = zodBaseType(col, schema.enums);
408
595
  if (col.nullable)
409
596
  expr += '.nullable()';
410
- lines.push(` ${col.field}: ${expr},`);
597
+ lines.push(` ${quoteIfNeeded(col.field)}: ${expr},`);
411
598
  }
412
599
  lines.push('});');
413
600
  lines.push('');
@@ -424,7 +611,7 @@ export function generateZod(schema, options) {
424
611
  expr += '.nullable()';
425
612
  if (col.hasDefault || col.nullable || isPk)
426
613
  expr += '.optional()';
427
- lines.push(` ${col.field}: ${expr},`);
614
+ lines.push(` ${quoteIfNeeded(col.field)}: ${expr},`);
428
615
  }
429
616
  lines.push('});');
430
617
  lines.push('');
@@ -440,7 +627,7 @@ export function generateZod(schema, options) {
440
627
  if (col.nullable)
441
628
  expr += '.nullable()';
442
629
  expr += '.optional()';
443
- lines.push(` ${col.field}: ${expr},`);
630
+ lines.push(` ${quoteIfNeeded(col.field)}: ${expr},`);
444
631
  }
445
632
  lines.push('});');
446
633
  lines.push('');
@@ -470,7 +657,7 @@ export function generateMetadata(schema, options) {
470
657
  // columnMap
471
658
  lines.push(' columnMap: {');
472
659
  for (const [field, col] of Object.entries(table.columnMap)) {
473
- lines.push(` ${field}: '${escSQ(col)}',`);
660
+ lines.push(` ${quoteIfNeeded(field)}: '${escSQ(col)}',`);
474
661
  }
475
662
  lines.push(' },');
476
663
  // reverseColumnMap
@@ -570,11 +757,17 @@ export function generateIndex(schema, options) {
570
757
  const hasSafeRelations = new Map();
571
758
  for (const t of tableEntries)
572
759
  hasSafeRelations.set(t.name, typeSafeRelations(t, false).length > 0);
760
+ // F3: sibling-import extension. Defaults to '.js' for direct callers so their
761
+ // output stays byte-stable; `generate()` resolves it (auto / js / none).
762
+ const ext = options?.importExt ?? '.js';
573
763
  const lines = [
574
764
  ...generatedFileHeader(options),
765
+ // Record the resolved import mode for debuggability (only when generate()
766
+ // drove it, so direct generateIndex() callers stay byte-identical).
767
+ ...(options?.importMode ? [`// Sibling imports resolved with importExtension: ${options.importMode}.`, ''] : []),
575
768
  "import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
576
769
  "import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults } from 'turbine-orm';",
577
- "import { SCHEMA } from './metadata.js';",
770
+ `import { SCHEMA } from './metadata${ext}';`,
578
771
  ];
579
772
  // Import all entity types and relations maps
580
773
  const typeImports = [];
@@ -584,7 +777,7 @@ export function generateIndex(schema, options) {
584
777
  typeImports.push(`${entityName(t.name)}Relations`);
585
778
  }
586
779
  }
587
- lines.push(`import type { ${typeImports.join(', ')} } from './types.js';`);
780
+ lines.push(`import type { ${typeImports.join(', ')} } from './types${ext}';`);
588
781
  lines.push('');
589
782
  // -------------------------------------------------------------------------
590
783
  // TypedTransactionClient — same typed table accessors as TurbineClient,
@@ -695,11 +888,70 @@ export function generateIndex(schema, options) {
695
888
  lines.push('}');
696
889
  lines.push('');
697
890
  // Re-export everything
698
- lines.push("export * from './types.js';");
699
- lines.push("export { SCHEMA } from './metadata.js';");
891
+ lines.push(`export * from './types${ext}';`);
892
+ lines.push(`export { SCHEMA } from './metadata${ext}';`);
893
+ lines.push('');
894
+ return lines.join('\n');
895
+ }
896
+ // ---------------------------------------------------------------------------
897
+ // prisma-map.ts generator
898
+ // ---------------------------------------------------------------------------
899
+ /**
900
+ * Serialize a resolved {@link PrismaCompatMap} into a `prisma-map.ts` module
901
+ * that exports `export const PRISMA_MAP: PrismaCompatMap = {...}`. Written next
902
+ * to the generated client by `turbine migrate-from-prisma`; consumed later by
903
+ * the phase-2 `turbine-orm/prisma-compat` runtime adapter.
904
+ *
905
+ * Deterministic: uses the same reproducible header as the other emitters, so a
906
+ * stable input regenerates byte-identical output under `noTimestamp`.
907
+ */
908
+ export function generatePrismaMap(map, options) {
909
+ const lines = [
910
+ ...generatedFileHeader(options),
911
+ "import type { PrismaCompatMap } from 'turbine-orm';",
912
+ '',
913
+ '/**',
914
+ ' * Name map from your Prisma schema onto this Turbine client. Every entry was',
915
+ ' * resolved against the live database; unresolved items are omitted (see the',
916
+ ' * migration report). Pass this to the phase-2 prisma-compat adapter, or read',
917
+ ' * it directly for hand-written compatibility wrappers.',
918
+ ' */',
919
+ 'export const PRISMA_MAP: PrismaCompatMap = {',
920
+ ' models: {',
921
+ ];
922
+ for (const [modelName, model] of Object.entries(map.models)) {
923
+ lines.push(` ${quoteIfNeeded(modelName)}: {`);
924
+ lines.push(` table: '${escSQ(model.table)}',`);
925
+ lines.push(` accessor: '${escSQ(model.accessor)}',`);
926
+ lines.push(` fields: ${serializeStringRecord(model.fields)},`);
927
+ lines.push(' relations: {');
928
+ for (const [rel, r] of Object.entries(model.relations)) {
929
+ lines.push(` ${quoteIfNeeded(rel)}: { name: '${escSQ(r.name)}', cardinality: '${r.cardinality}' },`);
930
+ }
931
+ lines.push(' },');
932
+ lines.push(` compoundUniques: ${serializeStringArrayRecord(model.compoundUniques)},`);
933
+ lines.push(' },');
934
+ }
935
+ lines.push(' },');
936
+ lines.push(` enums: ${serializeStringRecord(map.enums)},`);
937
+ lines.push('};');
700
938
  lines.push('');
701
939
  return lines.join('\n');
702
940
  }
941
+ /** Serialize a `Record<string, string>` as an inline object literal. */
942
+ function serializeStringRecord(rec) {
943
+ const entries = Object.entries(rec);
944
+ if (entries.length === 0)
945
+ return '{}';
946
+ return `{ ${entries.map(([k, v]) => `${quoteIfNeeded(k)}: '${escSQ(v)}'`).join(', ')} }`;
947
+ }
948
+ /** Serialize a `Record<string, string[]>` as an inline object literal. */
949
+ function serializeStringArrayRecord(rec) {
950
+ const entries = Object.entries(rec);
951
+ if (entries.length === 0)
952
+ return '{}';
953
+ return `{ ${entries.map(([k, v]) => `${quoteIfNeeded(k)}: [${v.map((s) => `'${escSQ(s)}'`).join(', ')}]`).join(', ')} }`;
954
+ }
703
955
  // ---------------------------------------------------------------------------
704
956
  // Helpers
705
957
  // ---------------------------------------------------------------------------
package/dist/index.d.ts CHANGED
@@ -45,8 +45,8 @@ export type { ObserveConfig, ObserveHandle } from './observe.js';
45
45
  export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
46
46
  export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
47
47
  export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
48
- export type { CheckMetadata, ColumnMetadata, IndexMetadata, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
49
- export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
48
+ export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
49
+ export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
50
50
  export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnIndexDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, type DocFieldIndexDef, defineSchema, isDocFieldIndexDef, type ManyToManyDef, type ReferenceDef, type SchemaDef, type SchemaIndexDef, type TableDef, table, } from './schema-builder.js';
51
51
  export { schemaDefToMetadata } from './schema-metadata.js';
52
52
  export { type AlterColumnDef, type AlterDef, DestructivePushRefusal, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ export { QueryInterface, } from './query/index.js';
51
51
  // Realtime — LISTEN/NOTIFY pub/sub
52
52
  export { validateChannel } from './realtime.js';
53
53
  // Schema utilities
54
- export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
54
+ export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
55
55
  // Schema builder — define schemas in TypeScript
56
56
  export { applyManyToManyRelations, ColumnBuilder, column, defineSchema, isDocFieldIndexDef,
57
57
  // Legacy compat (deprecated — use object format with defineSchema)