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.
- package/README.md +22 -4
- package/dist/cjs/cli/config.js +3 -0
- package/dist/cjs/cli/index.js +179 -0
- package/dist/cjs/cli/prisma-report.js +216 -0
- package/dist/cjs/cli/prisma-resolve.js +335 -0
- package/dist/cjs/cli/prisma-schema.js +484 -0
- package/dist/cjs/client.js +1 -0
- package/dist/cjs/generate.js +279 -22
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/introspect.js +203 -26
- package/dist/cjs/mssql.js +9 -10
- package/dist/cjs/mysql.js +3 -9
- package/dist/cjs/powdb-introspect.js +5 -10
- package/dist/cjs/powql.js +13 -0
- package/dist/cjs/prisma-compat.js +1147 -0
- package/dist/cjs/query/aggregates.js +67 -7
- package/dist/cjs/query/builder.js +388 -17
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/relations.js +7 -5
- package/dist/cjs/query/warn-registry.js +98 -0
- package/dist/cjs/query/writes.js +13 -5
- package/dist/cjs/schema.js +47 -0
- package/dist/cjs/sqlite.js +4 -9
- package/dist/cli/config.d.ts +26 -0
- package/dist/cli/config.js +3 -0
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.js +180 -1
- package/dist/cli/prisma-report.d.ts +19 -0
- package/dist/cli/prisma-report.js +211 -0
- package/dist/cli/prisma-resolve.d.ts +87 -0
- package/dist/cli/prisma-resolve.js +330 -0
- package/dist/cli/prisma-schema.d.ts +116 -0
- package/dist/cli/prisma-schema.js +479 -0
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +18 -2
- package/dist/client.js +1 -0
- package/dist/generate.d.ts +80 -1
- package/dist/generate.js +277 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +92 -2
- package/dist/introspect.js +198 -26
- package/dist/mssql.js +10 -11
- package/dist/mysql.js +4 -10
- package/dist/powdb-introspect.js +5 -10
- package/dist/powql.js +13 -0
- package/dist/prisma-compat.d.ts +281 -0
- package/dist/prisma-compat.js +1143 -0
- package/dist/query/aggregates.js +67 -7
- package/dist/query/builder.d.ts +77 -4
- package/dist/query/builder.js +390 -19
- package/dist/query/compound-unique.d.ts +49 -0
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/deferred.d.ts +18 -0
- package/dist/query/relations.js +7 -5
- package/dist/query/types.d.ts +70 -9
- package/dist/query/warn-registry.d.ts +57 -0
- package/dist/query/warn-registry.js +92 -0
- package/dist/query/writes.js +13 -5
- package/dist/schema.d.ts +75 -0
- package/dist/schema.js +46 -0
- package/dist/sqlite.js +5 -10
- package/package.json +6 -1
package/dist/generate.d.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
275
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
699
|
-
lines.push(
|
|
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)
|