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/README.md
CHANGED
|
@@ -360,10 +360,12 @@ A few client options tune how `with` relations are loaded and encoded. All are o
|
|
|
360
360
|
```typescript
|
|
361
361
|
const db = turbine({
|
|
362
362
|
connectionString: process.env.DATABASE_URL,
|
|
363
|
-
// How with-clause relations resolve: '
|
|
364
|
-
// statement
|
|
365
|
-
//
|
|
366
|
-
|
|
363
|
+
// How with-clause relations resolve: 'auto' (default since 0.41.0: the
|
|
364
|
+
// single-statement join plan, with a per-relation batched fallback when the
|
|
365
|
+
// correlation column has no covering index), 'join' (always one correlated-
|
|
366
|
+
// subquery statement), or 'batched' (base query + one flat follow-up per
|
|
367
|
+
// relation). Override per query on findMany/findFirst/findUnique.
|
|
368
|
+
relationLoadStrategy: 'auto',
|
|
367
369
|
// 'positional' (Postgres-only) drops repeated JSON keys from relation
|
|
368
370
|
// subqueries — ~39% fewer wire bytes on wide relations, byte-identical output.
|
|
369
371
|
// Default 'object'.
|
|
@@ -887,6 +889,22 @@ const db = await turbinePowDB({ embedded: './data', syncMode: 'normal' }, schema
|
|
|
887
889
|
// const db = await turbinePowDB('powdb://127.0.0.1:7070', schema);
|
|
888
890
|
```
|
|
889
891
|
|
|
892
|
+
Migrating from Prisma? `turbine migrate-from-prisma` emits a typed `PRISMA_MAP`, and the
|
|
893
|
+
`turbine-orm/prisma-compat` subpath wraps a `TurbineClient` in Prisma's `db.Model.*`
|
|
894
|
+
surface (no new dependencies):
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
import { TurbineClient } from 'turbine-orm';
|
|
898
|
+
import { createPrismaCompatClient } from 'turbine-orm/prisma-compat';
|
|
899
|
+
import { SCHEMA } from './generated/turbine/metadata.js';
|
|
900
|
+
import { PRISMA_MAP } from './generated/turbine/prisma-map.js';
|
|
901
|
+
|
|
902
|
+
const db = new TurbineClient({ connectionString: process.env.DATABASE_URL }, SCHEMA);
|
|
903
|
+
const prisma = createPrismaCompatClient(db, PRISMA_MAP);
|
|
904
|
+
|
|
905
|
+
const users = await prisma.User.findMany({ include: { posts: { take: 5 } } });
|
|
906
|
+
```
|
|
907
|
+
|
|
890
908
|
### Capability matrix
|
|
891
909
|
|
|
892
910
|
Everything is honest about what ports and what doesn't. Features marked **PG-only** throw a typed `UnsupportedFeatureError` (`TURBINE_E017`) on other engines rather than silently degrading.
|
package/dist/cjs/cli/config.js
CHANGED
|
@@ -185,6 +185,9 @@ function resolveConfig(fileConfig, overrides) {
|
|
|
185
185
|
migrationsDir: fileConfig.migrationsDir ?? './turbine/migrations',
|
|
186
186
|
seedFile: fileConfig.seed ?? fileConfig.seedFile,
|
|
187
187
|
schemaFile: fileConfig.schemaFile ?? './turbine/schema.ts',
|
|
188
|
+
importExtension: overrides.importExtension ?? fileConfig.importExtension ?? 'auto',
|
|
189
|
+
keepColumnNames: overrides.keepColumnNames ?? fileConfig.keepColumnNames ?? false,
|
|
190
|
+
legacyToManyUniques: overrides.legacyToManyUniques ?? fileConfig.legacyToManyUniques ?? false,
|
|
188
191
|
};
|
|
189
192
|
}
|
|
190
193
|
/**
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Commands:
|
|
7
7
|
* turbine init — Initialize a Turbine project
|
|
8
8
|
* turbine generate | pull — Introspect database and generate TypeScript types
|
|
9
|
+
* turbine migrate-from-prisma - Parse a schema.prisma and emit a Prisma->Turbine name map + report
|
|
9
10
|
* turbine push - Apply schema-builder definitions to database (destructive ops gated)
|
|
10
11
|
* turbine migrate create <name> - Create a new SQL migration file (--auto | --from-diff | --recipe <name>)
|
|
11
12
|
* turbine migrate up — Apply pending migrations
|
|
@@ -80,6 +81,9 @@ const loader_js_1 = require("./loader.js");
|
|
|
80
81
|
const mcp_js_1 = require("./mcp.js");
|
|
81
82
|
const migrate_js_1 = require("./migrate.js");
|
|
82
83
|
const observe_js_1 = require("./observe.js");
|
|
84
|
+
const prisma_report_js_1 = require("./prisma-report.js");
|
|
85
|
+
const prisma_resolve_js_1 = require("./prisma-resolve.js");
|
|
86
|
+
const prisma_schema_js_1 = require("./prisma-schema.js");
|
|
83
87
|
const studio_js_1 = require("./studio.js");
|
|
84
88
|
const ui_js_1 = require("./ui.js");
|
|
85
89
|
function parseArgs(argv = process.argv.slice(2)) {
|
|
@@ -169,6 +173,21 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
169
173
|
case '--no-timestamp':
|
|
170
174
|
result.noTimestamp = true;
|
|
171
175
|
break;
|
|
176
|
+
case '--import-ext':
|
|
177
|
+
case '--import-extension':
|
|
178
|
+
if (next !== 'js' && next !== 'none' && next !== 'auto') {
|
|
179
|
+
console.error(`--import-ext requires one of: js, none, auto (got ${next ?? '(nothing)'})`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
result.importExtension = next;
|
|
183
|
+
i++;
|
|
184
|
+
break;
|
|
185
|
+
case '--keep-column-names':
|
|
186
|
+
result.keepColumnNames = true;
|
|
187
|
+
break;
|
|
188
|
+
case '--legacy-to-many-uniques':
|
|
189
|
+
result.legacyToManyUniques = true;
|
|
190
|
+
break;
|
|
172
191
|
case '--allow-destructive':
|
|
173
192
|
result.allowDestructive = true;
|
|
174
193
|
break;
|
|
@@ -215,6 +234,12 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
215
234
|
case '--demo':
|
|
216
235
|
result.demo = true;
|
|
217
236
|
break;
|
|
237
|
+
case '--allow-partial':
|
|
238
|
+
result.allowPartial = true;
|
|
239
|
+
break;
|
|
240
|
+
case '--no-db':
|
|
241
|
+
result.noDb = true;
|
|
242
|
+
break;
|
|
218
243
|
default:
|
|
219
244
|
if (!arg.startsWith('-')) {
|
|
220
245
|
result.positional.push(arg);
|
|
@@ -930,17 +955,25 @@ async function cmdGenerate(args, config) {
|
|
|
930
955
|
(0, ui_js_1.newline)();
|
|
931
956
|
// Introspect
|
|
932
957
|
const spinner = new ui_js_1.Spinner('Introspecting database schema').start();
|
|
958
|
+
const skippedInternalTables = [];
|
|
933
959
|
const schema = await (0, introspect_js_1.introspect)({
|
|
934
960
|
connectionString: url,
|
|
935
961
|
schema: config.schema,
|
|
936
962
|
include: config.include.length ? config.include : undefined,
|
|
937
963
|
exclude: config.exclude.length ? config.exclude : undefined,
|
|
938
964
|
includeViews: args.includeViews,
|
|
965
|
+
legacyToManyUniques: config.legacyToManyUniques,
|
|
966
|
+
onDefaultTableExclusion: (tables) => skippedInternalTables.push(...tables),
|
|
939
967
|
});
|
|
940
968
|
const tableNames = Object.keys(schema.tables);
|
|
941
969
|
const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
|
|
942
970
|
const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
|
|
943
971
|
spinner.succeed(`Found ${(0, ui_js_1.bold)(String(tableNames.length))} tables, ${(0, ui_js_1.bold)(String(totalColumns))} columns, ${(0, ui_js_1.bold)(String(totalRelations))} relations`);
|
|
972
|
+
// F12: make the default bookkeeping-table exclusions discoverable rather than
|
|
973
|
+
// silent. `include` re-adds any of them byte-for-byte.
|
|
974
|
+
for (const t of skippedInternalTables) {
|
|
975
|
+
console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.teeEnd} skipped internal table ${t} (add it to include to keep it)`)}`);
|
|
976
|
+
}
|
|
944
977
|
// Guard: zero tables means the generated client would be empty. That is almost
|
|
945
978
|
// always a misconfiguration (wrong `schema`, an include/exclude that filtered
|
|
946
979
|
// everything, or a database with no tables yet) rather than intent. Fail loudly
|
|
@@ -980,6 +1013,8 @@ async function cmdGenerate(args, config) {
|
|
|
980
1013
|
connectionString: url,
|
|
981
1014
|
zod: args.zod,
|
|
982
1015
|
noTimestamp: args.noTimestamp,
|
|
1016
|
+
importExtension: config.importExtension,
|
|
1017
|
+
keepColumnNames: config.keepColumnNames,
|
|
983
1018
|
});
|
|
984
1019
|
genSpinner.succeed(`Generated ${(0, ui_js_1.bold)(String(result.files.length))} files in ${(0, ui_js_1.elapsed)(startTime)}`);
|
|
985
1020
|
// List files
|
|
@@ -998,6 +1033,112 @@ async function cmdGenerate(args, config) {
|
|
|
998
1033
|
(0, ui_js_1.newline)();
|
|
999
1034
|
}
|
|
1000
1035
|
// ---------------------------------------------------------------------------
|
|
1036
|
+
// migrate-from-prisma
|
|
1037
|
+
// ---------------------------------------------------------------------------
|
|
1038
|
+
/**
|
|
1039
|
+
* `turbine migrate-from-prisma --schema prisma/schema.prisma` parses a Prisma
|
|
1040
|
+
* schema, resolve its models/fields/relations/compound-uniques against the live
|
|
1041
|
+
* database (unless `--no-db`), and emit (a) a Markdown resolution report and
|
|
1042
|
+
* (b) a typed `prisma-map.ts` name map next to the generated client.
|
|
1043
|
+
*
|
|
1044
|
+
* NOTE: within THIS command `--schema` names the Prisma schema FILE (not the
|
|
1045
|
+
* Postgres namespace, which the rest of the CLI's `--schema` means). The
|
|
1046
|
+
* Postgres namespace is `public` here; multi-schema (`@@schema`) is unsupported
|
|
1047
|
+
* in v1 and listed as a parser note in the report.
|
|
1048
|
+
*/
|
|
1049
|
+
async function cmdMigrateFromPrisma(args, config) {
|
|
1050
|
+
(0, ui_js_1.banner)();
|
|
1051
|
+
// `--schema` is the Prisma schema file path in this command.
|
|
1052
|
+
const prismaPath = (0, node_path_1.resolve)(args.schema ?? 'prisma/schema.prisma');
|
|
1053
|
+
if (!(0, node_fs_1.existsSync)(prismaPath)) {
|
|
1054
|
+
(0, ui_js_1.error)(`Prisma schema file not found: ${(0, ui_js_1.cyan)(prismaPath)}`);
|
|
1055
|
+
(0, ui_js_1.newline)();
|
|
1056
|
+
console.log(` ${(0, ui_js_1.dim)('Point at it with')} ${(0, ui_js_1.cyan)('--schema <path/to/schema.prisma>')} ${(0, ui_js_1.dim)('(default: prisma/schema.prisma).')}`);
|
|
1057
|
+
(0, ui_js_1.newline)();
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
}
|
|
1060
|
+
// Parse (fatal only on a construct we must understand).
|
|
1061
|
+
const source = (0, node_fs_1.readFileSync)(prismaPath, 'utf-8');
|
|
1062
|
+
let ast;
|
|
1063
|
+
try {
|
|
1064
|
+
ast = (0, prisma_schema_js_1.parsePrismaSchema)(source);
|
|
1065
|
+
}
|
|
1066
|
+
catch (err) {
|
|
1067
|
+
if (err instanceof prisma_schema_js_1.PrismaParseError) {
|
|
1068
|
+
(0, ui_js_1.newline)();
|
|
1069
|
+
(0, ui_js_1.error)(`Could not parse ${(0, ui_js_1.cyan)(prismaPath)}`);
|
|
1070
|
+
console.log(` ${(0, ui_js_1.red)(err.message)}`);
|
|
1071
|
+
(0, ui_js_1.newline)();
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
throw err;
|
|
1075
|
+
}
|
|
1076
|
+
(0, ui_js_1.label)('Prisma schema', prismaPath);
|
|
1077
|
+
(0, ui_js_1.label)('Models', String(ast.models.length));
|
|
1078
|
+
(0, ui_js_1.label)('Enums', String(ast.enums.length));
|
|
1079
|
+
// Resolve against the live database, unless --no-db (parse-only).
|
|
1080
|
+
let schemaMeta = null;
|
|
1081
|
+
if (args.noDb) {
|
|
1082
|
+
(0, ui_js_1.info)('Parse-only mode (--no-db): names will not be resolved.');
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
const url = requireUrl(config);
|
|
1086
|
+
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
1087
|
+
const spinner = new ui_js_1.Spinner('Introspecting database schema').start();
|
|
1088
|
+
schemaMeta = await (0, introspect_js_1.introspect)({
|
|
1089
|
+
connectionString: url,
|
|
1090
|
+
// The Postgres NAMESPACE is fixed to `public` here (`--schema` names the
|
|
1091
|
+
// Prisma file, not the namespace).
|
|
1092
|
+
schema: 'public',
|
|
1093
|
+
// Prisma `view` models resolve against introspected views.
|
|
1094
|
+
includeViews: true,
|
|
1095
|
+
// Inherit the shared bookkeeping-table exclusions (Turbine + Prisma).
|
|
1096
|
+
exclude: [...new Set([...config.exclude, ...prisma_resolve_js_1.DEFAULT_EXCLUDED_TABLES])],
|
|
1097
|
+
});
|
|
1098
|
+
spinner.succeed(`Introspected ${(0, ui_js_1.bold)(String(Object.keys(schemaMeta.tables).length))} tables`);
|
|
1099
|
+
}
|
|
1100
|
+
(0, ui_js_1.newline)();
|
|
1101
|
+
const result = (0, prisma_resolve_js_1.resolvePrismaSchema)(ast, schemaMeta);
|
|
1102
|
+
// Console summary.
|
|
1103
|
+
(0, ui_js_1.header)('Resolution');
|
|
1104
|
+
for (const line of (0, prisma_report_js_1.summaryLines)(result)) {
|
|
1105
|
+
const marker = line.includes('[UNRESOLVED]') ? (0, ui_js_1.red)(ui_js_1.symbols.cross) : (0, ui_js_1.green)(ui_js_1.symbols.check);
|
|
1106
|
+
console.log(` ${marker} ${line}`);
|
|
1107
|
+
}
|
|
1108
|
+
(0, ui_js_1.newline)();
|
|
1109
|
+
// Write outputs into the generate outDir.
|
|
1110
|
+
const outDir = (0, node_path_1.resolve)(config.out);
|
|
1111
|
+
const rel = (0, node_path_1.relative)(process.cwd(), outDir);
|
|
1112
|
+
if (rel.startsWith('..') || (0, node_path_1.resolve)(rel) !== outDir) {
|
|
1113
|
+
(0, ui_js_1.error)(`Output directory must be within the project root. Got: ${config.out}`);
|
|
1114
|
+
(0, ui_js_1.newline)();
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
1118
|
+
const reportPath = (0, node_path_1.join)(outDir, 'prisma-migration-report.md');
|
|
1119
|
+
(0, node_fs_1.writeFileSync)(reportPath, (0, prisma_report_js_1.formatPrismaReport)(result, { schemaPath: prismaPath, noTimestamp: args.noTimestamp }), 'utf-8');
|
|
1120
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.cyan)(reportPath)} ${(0, ui_js_1.dim)('(report)')}`);
|
|
1121
|
+
if (!args.noDb) {
|
|
1122
|
+
const mapPath = (0, node_path_1.join)(outDir, 'prisma-map.ts');
|
|
1123
|
+
(0, node_fs_1.writeFileSync)(mapPath, (0, generate_js_1.generatePrismaMap)(result.map, { noTimestamp: args.noTimestamp }), 'utf-8');
|
|
1124
|
+
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.cyan)(mapPath)} ${(0, ui_js_1.dim)('(typed name map)')}`);
|
|
1125
|
+
}
|
|
1126
|
+
(0, ui_js_1.newline)();
|
|
1127
|
+
// Exit non-zero when anything is UNRESOLVED, unless --allow-partial.
|
|
1128
|
+
if (result.hasUnresolved && !args.allowPartial) {
|
|
1129
|
+
(0, ui_js_1.warn)('Some items could not be resolved (see the report). Re-run with --allow-partial to accept a partial map.');
|
|
1130
|
+
(0, ui_js_1.newline)();
|
|
1131
|
+
process.exit(1);
|
|
1132
|
+
}
|
|
1133
|
+
if (args.noDb) {
|
|
1134
|
+
(0, ui_js_1.info)(`Parse-only report written. Re-run without ${(0, ui_js_1.cyan)('--no-db')} against your database to resolve names.`);
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
(0, ui_js_1.success)('Prisma name map generated.');
|
|
1138
|
+
}
|
|
1139
|
+
(0, ui_js_1.newline)();
|
|
1140
|
+
}
|
|
1141
|
+
// ---------------------------------------------------------------------------
|
|
1001
1142
|
// Command: push
|
|
1002
1143
|
// ---------------------------------------------------------------------------
|
|
1003
1144
|
async function cmdPush(args, config) {
|
|
@@ -2164,6 +2305,7 @@ function showSubcommandHelp(command) {
|
|
|
2164
2305
|
init: showInitHelp,
|
|
2165
2306
|
generate: showGenerateHelp,
|
|
2166
2307
|
pull: showGenerateHelp,
|
|
2308
|
+
'migrate-from-prisma': showMigrateFromPrismaHelp,
|
|
2167
2309
|
push: showPushHelp,
|
|
2168
2310
|
migrate: showMigrateHelp,
|
|
2169
2311
|
migration: showMigrateHelp,
|
|
@@ -2225,9 +2367,39 @@ function showGenerateHelp() {
|
|
|
2225
2367
|
console.log(` ${(0, ui_js_1.cyan)('--zod')} Also emit ${(0, ui_js_1.cyan)('zod.ts')} validation schemas ${(0, ui_js_1.dim)('(needs the zod dep)')}`);
|
|
2226
2368
|
console.log(` ${(0, ui_js_1.cyan)('--include-views')} Include views + materialized views as read-only entities`);
|
|
2227
2369
|
console.log(` ${(0, ui_js_1.cyan)('--no-timestamp')} Omit the ${(0, ui_js_1.dim)('Generated at:')} header line ${(0, ui_js_1.dim)('(reproducible, diff-stable output)')}`);
|
|
2370
|
+
console.log(` ${(0, ui_js_1.cyan)('--import-ext')} ${(0, ui_js_1.dim)('<mode>')} Sibling-import extension: ${(0, ui_js_1.cyan)('js')} / ${(0, ui_js_1.cyan)('none')} / ${(0, ui_js_1.cyan)('auto')} ${(0, ui_js_1.dim)('(default: auto)')}`);
|
|
2371
|
+
console.log(` ${(0, ui_js_1.cyan)('--keep-column-names')} Keep raw DB column names as field names ${(0, ui_js_1.dim)('(snake_case, not camelCase)')}`);
|
|
2372
|
+
console.log(` ${(0, ui_js_1.cyan)('--legacy-to-many-uniques')} Emit ${(0, ui_js_1.cyan)('hasMany')} for unique-FK children ${(0, ui_js_1.dim)('(pre-0.41 shape; default flips to hasOne)')}`);
|
|
2228
2373
|
console.log(` ${(0, ui_js_1.cyan)('--allow-empty')} Generate even when introspection matches 0 tables`);
|
|
2229
2374
|
(0, ui_js_1.newline)();
|
|
2230
2375
|
}
|
|
2376
|
+
function showMigrateFromPrismaHelp() {
|
|
2377
|
+
(0, ui_js_1.banner)();
|
|
2378
|
+
console.log(` ${(0, ui_js_1.bold)('turbine migrate-from-prisma')} - Map a Prisma schema onto a Turbine client`);
|
|
2379
|
+
(0, ui_js_1.newline)();
|
|
2380
|
+
console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
|
|
2381
|
+
console.log(` npx turbine migrate-from-prisma ${(0, ui_js_1.dim)('--schema prisma/schema.prisma [options]')}`);
|
|
2382
|
+
(0, ui_js_1.newline)();
|
|
2383
|
+
console.log(` Parses your ${(0, ui_js_1.cyan)('schema.prisma')}, resolves models/fields/relations/compound`);
|
|
2384
|
+
console.log(` uniques against the live database, and writes into the output directory:`);
|
|
2385
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('prisma-migration-report.md')} - per-model resolution + unresolved items`);
|
|
2386
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('prisma-map.ts')} - typed PRISMA_MAP name map`);
|
|
2387
|
+
(0, ui_js_1.newline)();
|
|
2388
|
+
console.log(` ${(0, ui_js_1.dim)('Note:')} here ${(0, ui_js_1.cyan)('--schema')} names the Prisma FILE (not the Postgres namespace).`);
|
|
2389
|
+
(0, ui_js_1.newline)();
|
|
2390
|
+
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
2391
|
+
console.log(` ${(0, ui_js_1.cyan)('--schema')} ${(0, ui_js_1.dim)('<file>')} Path to schema.prisma ${(0, ui_js_1.dim)('(default: prisma/schema.prisma)')}`);
|
|
2392
|
+
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string ${(0, ui_js_1.dim)('(unless --no-db)')}`);
|
|
2393
|
+
console.log(` ${(0, ui_js_1.cyan)('--out, -o')} ${(0, ui_js_1.dim)('<dir>')} Output directory ${(0, ui_js_1.dim)('(default: ./generated/turbine)')}`);
|
|
2394
|
+
console.log(` ${(0, ui_js_1.cyan)('--no-db')} Parse-only: write the report without resolving names`);
|
|
2395
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-partial')} Exit 0 even when some items are UNRESOLVED`);
|
|
2396
|
+
console.log(` ${(0, ui_js_1.cyan)('--no-timestamp')} Omit the ${(0, ui_js_1.dim)('Generated:')} lines ${(0, ui_js_1.dim)('(reproducible output)')}`);
|
|
2397
|
+
(0, ui_js_1.newline)();
|
|
2398
|
+
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
|
2399
|
+
console.log(` ${(0, ui_js_1.dim)('$')} DATABASE_URL=postgres://... npx turbine migrate-from-prisma --schema prisma/schema.prisma`);
|
|
2400
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate-from-prisma --schema prisma/schema.prisma --no-db`);
|
|
2401
|
+
(0, ui_js_1.newline)();
|
|
2402
|
+
}
|
|
2231
2403
|
function showPushHelp() {
|
|
2232
2404
|
(0, ui_js_1.banner)();
|
|
2233
2405
|
console.log(` ${(0, ui_js_1.bold)('turbine push')} — Apply schema-builder definitions to database`);
|
|
@@ -2340,6 +2512,7 @@ function showHelp() {
|
|
|
2340
2512
|
console.log(` ${(0, ui_js_1.bold)('Commands:')}`);
|
|
2341
2513
|
console.log(` ${(0, ui_js_1.cyan)('init')} Initialize a Turbine project`);
|
|
2342
2514
|
console.log(` ${(0, ui_js_1.cyan)('generate')} ${(0, ui_js_1.dim)('| pull')} Introspect database ${ui_js_1.symbols.arrow} generate types`);
|
|
2515
|
+
console.log(` ${(0, ui_js_1.cyan)('migrate-from-prisma')} Map a schema.prisma onto Turbine ${(0, ui_js_1.dim)('(report + typed name map)')}`);
|
|
2343
2516
|
console.log(` ${(0, ui_js_1.cyan)('push')} Apply schema definitions to database`);
|
|
2344
2517
|
console.log(` ${(0, ui_js_1.cyan)('migrate')} ${(0, ui_js_1.dim)('<sub>')} SQL migration management`);
|
|
2345
2518
|
console.log(` ${(0, ui_js_1.dim)('create <name>')} Create a new migration file`);
|
|
@@ -2500,6 +2673,9 @@ async function main() {
|
|
|
2500
2673
|
schema: args.schema,
|
|
2501
2674
|
include: args.include,
|
|
2502
2675
|
exclude: args.exclude,
|
|
2676
|
+
importExtension: args.importExtension,
|
|
2677
|
+
keepColumnNames: args.keepColumnNames,
|
|
2678
|
+
legacyToManyUniques: args.legacyToManyUniques,
|
|
2503
2679
|
};
|
|
2504
2680
|
const config = (0, config_js_1.resolveConfig)(fileConfig, overrides);
|
|
2505
2681
|
// Warn (don't change precedence) when an .env-sourced DATABASE_URL is silently
|
|
@@ -2526,6 +2702,9 @@ async function main() {
|
|
|
2526
2702
|
case 'pull':
|
|
2527
2703
|
await cmdGenerate(args, config);
|
|
2528
2704
|
break;
|
|
2705
|
+
case 'migrate-from-prisma':
|
|
2706
|
+
await cmdMigrateFromPrisma(args, config);
|
|
2707
|
+
break;
|
|
2529
2708
|
case 'push':
|
|
2530
2709
|
await cmdPush(args, config);
|
|
2531
2710
|
break;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Render a {@link ResolutionResult} into the `prisma-migration-report.md`
|
|
4
|
+
* artifact and a short console summary. Pure leaf - string in, string out.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.formatPrismaReport = formatPrismaReport;
|
|
8
|
+
exports.collectUnresolved = collectUnresolved;
|
|
9
|
+
exports.summaryLines = summaryLines;
|
|
10
|
+
const CHECK = 'OK';
|
|
11
|
+
const CROSS = 'UNRESOLVED';
|
|
12
|
+
function modelDisplayStatus(m) {
|
|
13
|
+
if (m.status === 'parsed')
|
|
14
|
+
return 'parsed';
|
|
15
|
+
if (m.status === 'unresolved')
|
|
16
|
+
return CROSS;
|
|
17
|
+
return m.viaMap ? `${CHECK} (@@map)` : CHECK;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the full Markdown migration report.
|
|
21
|
+
*/
|
|
22
|
+
function formatPrismaReport(result, options = {}) {
|
|
23
|
+
const L = [];
|
|
24
|
+
L.push('# Prisma to Turbine migration report');
|
|
25
|
+
L.push('');
|
|
26
|
+
if (options.schemaPath)
|
|
27
|
+
L.push(`Source: \`${options.schemaPath}\``);
|
|
28
|
+
if (!options.noTimestamp)
|
|
29
|
+
L.push(`Generated: ${new Date().toISOString()}`);
|
|
30
|
+
L.push(`Mode: ${result.noDb ? 'parse-only (--no-db, no database resolution)' : 'resolved against live database'}`);
|
|
31
|
+
L.push('');
|
|
32
|
+
// ---- Summary ----------------------------------------------------------
|
|
33
|
+
const modelCount = result.models.length;
|
|
34
|
+
const resolvedModels = result.models.filter((m) => m.status === 'resolved').length;
|
|
35
|
+
const unresolvedModels = result.models.filter((m) => m.status === 'unresolved').length;
|
|
36
|
+
L.push('## Summary');
|
|
37
|
+
L.push('');
|
|
38
|
+
if (result.noDb) {
|
|
39
|
+
L.push(`- Parsed ${modelCount} model(s), ${result.enums.length} enum(s).`);
|
|
40
|
+
L.push('- No database URL provided - names were not resolved. Re-run without `--no-db` to resolve.');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
L.push(`- Models: ${resolvedModels}/${modelCount} resolved${unresolvedModels ? `, ${unresolvedModels} UNRESOLVED` : ''}.`);
|
|
44
|
+
L.push(`- Enums: ${result.enums.filter((e) => e.status === 'resolved').length}/${result.enums.length} resolved.`);
|
|
45
|
+
L.push(`- Overall: ${result.hasUnresolved ? 'INCOMPLETE (some items unresolved)' : 'complete (all items resolved)'}.`);
|
|
46
|
+
}
|
|
47
|
+
L.push('');
|
|
48
|
+
// ---- Model resolution table ------------------------------------------
|
|
49
|
+
L.push('## Models');
|
|
50
|
+
L.push('');
|
|
51
|
+
L.push('| Prisma model | Turbine accessor | Table | Status |');
|
|
52
|
+
L.push('| --- | --- | --- | --- |');
|
|
53
|
+
for (const m of result.models) {
|
|
54
|
+
L.push(`| ${m.prismaName} | ${m.accessor ?? '-'} | ${m.table ?? '-'} | ${modelDisplayStatus(m)} |`);
|
|
55
|
+
}
|
|
56
|
+
L.push('');
|
|
57
|
+
// ---- Per-model detail -------------------------------------------------
|
|
58
|
+
for (const m of result.models) {
|
|
59
|
+
L.push(`### ${m.prismaName}`);
|
|
60
|
+
L.push('');
|
|
61
|
+
if (m.status === 'unresolved') {
|
|
62
|
+
L.push(`> UNRESOLVED: ${m.reason ?? 'no matching table'}`);
|
|
63
|
+
L.push('');
|
|
64
|
+
}
|
|
65
|
+
if (m.fields.length > 0) {
|
|
66
|
+
L.push('Fields:');
|
|
67
|
+
L.push('');
|
|
68
|
+
for (const f of m.fields) {
|
|
69
|
+
if (f.status === 'resolved') {
|
|
70
|
+
L.push(`- \`${f.prismaName}\` -> \`${f.turbineField}\` (column \`${f.column}\`)`);
|
|
71
|
+
}
|
|
72
|
+
else if (f.status === 'parsed') {
|
|
73
|
+
L.push(`- \`${f.prismaName}\` (parsed)`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
L.push(`- \`${f.prismaName}\` UNRESOLVED: ${f.reason}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
L.push('');
|
|
80
|
+
}
|
|
81
|
+
if (m.relations.length > 0) {
|
|
82
|
+
L.push('Relations:');
|
|
83
|
+
L.push('');
|
|
84
|
+
for (const r of m.relations) {
|
|
85
|
+
if (r.status === 'resolved') {
|
|
86
|
+
const j = r.junction ? `, junction \`${r.junction}\`` : '';
|
|
87
|
+
L.push(`- \`${r.prismaName}\` -> \`${r.turbineName}\` (${r.cardinality}${j})`);
|
|
88
|
+
}
|
|
89
|
+
else if (r.status === 'parsed') {
|
|
90
|
+
L.push(`- \`${r.prismaName}\` -> ${r.targetModel} (parsed)`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
L.push(`- \`${r.prismaName}\` -> ${r.targetModel} UNRESOLVED: ${r.reason}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
L.push('');
|
|
97
|
+
}
|
|
98
|
+
if (m.compoundUniques.length > 0) {
|
|
99
|
+
L.push('Compound unique / id selectors:');
|
|
100
|
+
L.push('');
|
|
101
|
+
for (const c of m.compoundUniques) {
|
|
102
|
+
if (c.status === 'resolved') {
|
|
103
|
+
L.push(`- \`${c.selector}\` (@@${c.kind}) -> [${c.turbineFields.join(', ')}]`);
|
|
104
|
+
}
|
|
105
|
+
else if (c.status === 'parsed') {
|
|
106
|
+
L.push(`- \`${c.selector}\` (@@${c.kind}, parsed): [${c.prismaFields.join(', ')}]`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
L.push(`- \`${c.selector}\` (@@${c.kind}) UNRESOLVED: ${c.reason}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
L.push('');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ---- Junction tables --------------------------------------------------
|
|
116
|
+
const junctions = new Set();
|
|
117
|
+
for (const m of result.models) {
|
|
118
|
+
for (const r of m.relations)
|
|
119
|
+
if (r.junction)
|
|
120
|
+
junctions.add(r.junction);
|
|
121
|
+
}
|
|
122
|
+
if (junctions.size > 0) {
|
|
123
|
+
L.push('## Junction tables (implicit m2m)');
|
|
124
|
+
L.push('');
|
|
125
|
+
for (const j of [...junctions].sort())
|
|
126
|
+
L.push(`- \`${j}\``);
|
|
127
|
+
L.push('');
|
|
128
|
+
}
|
|
129
|
+
// ---- Enums ------------------------------------------------------------
|
|
130
|
+
if (result.enums.length > 0) {
|
|
131
|
+
L.push('## Enums');
|
|
132
|
+
L.push('');
|
|
133
|
+
for (const e of result.enums) {
|
|
134
|
+
if (e.status === 'resolved')
|
|
135
|
+
L.push(`- \`${e.prismaName}\` -> \`${e.turbineName}\``);
|
|
136
|
+
else if (e.status === 'parsed')
|
|
137
|
+
L.push(`- \`${e.prismaName}\` (parsed)`);
|
|
138
|
+
else
|
|
139
|
+
L.push(`- \`${e.prismaName}\` UNRESOLVED: ${e.reason}`);
|
|
140
|
+
}
|
|
141
|
+
L.push('');
|
|
142
|
+
}
|
|
143
|
+
// ---- Unresolved roll-up ----------------------------------------------
|
|
144
|
+
const unresolved = collectUnresolved(result);
|
|
145
|
+
if (unresolved.length > 0) {
|
|
146
|
+
L.push('## Unresolved items');
|
|
147
|
+
L.push('');
|
|
148
|
+
for (const u of unresolved)
|
|
149
|
+
L.push(`- ${u}`);
|
|
150
|
+
L.push('');
|
|
151
|
+
}
|
|
152
|
+
// ---- Parser warnings --------------------------------------------------
|
|
153
|
+
if (result.parseWarnings.length > 0) {
|
|
154
|
+
L.push('## Parser notes');
|
|
155
|
+
L.push('');
|
|
156
|
+
for (const w of result.parseWarnings)
|
|
157
|
+
L.push(`- ${w}`);
|
|
158
|
+
L.push('');
|
|
159
|
+
}
|
|
160
|
+
// ---- Fixed semantic-divergence section --------------------------------
|
|
161
|
+
L.push(SEMANTIC_DIVERGENCE);
|
|
162
|
+
return `${L.join('\n')}\n`;
|
|
163
|
+
}
|
|
164
|
+
/** Flat list of unresolved item descriptions across the whole result. */
|
|
165
|
+
function collectUnresolved(result) {
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const m of result.models) {
|
|
168
|
+
if (m.status === 'unresolved')
|
|
169
|
+
out.push(`Model ${m.prismaName}: ${m.reason ?? 'no matching table'}`);
|
|
170
|
+
for (const f of m.fields) {
|
|
171
|
+
if (f.status === 'unresolved')
|
|
172
|
+
out.push(`${m.prismaName}.${f.prismaName} (field): ${f.reason}`);
|
|
173
|
+
}
|
|
174
|
+
for (const r of m.relations) {
|
|
175
|
+
if (r.status === 'unresolved')
|
|
176
|
+
out.push(`${m.prismaName}.${r.prismaName} (relation): ${r.reason}`);
|
|
177
|
+
}
|
|
178
|
+
for (const c of m.compoundUniques) {
|
|
179
|
+
if (c.status === 'unresolved')
|
|
180
|
+
out.push(`${m.prismaName}.${c.selector} (@@${c.kind}): ${c.reason}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const e of result.enums) {
|
|
184
|
+
if (e.status === 'unresolved')
|
|
185
|
+
out.push(`Enum ${e.prismaName}: ${e.reason}`);
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
/** Static section documenting known Prisma-vs-Turbine behavior differences. */
|
|
190
|
+
const SEMANTIC_DIVERGENCE = `## Behavior notes (Prisma vs Turbine)
|
|
191
|
+
|
|
192
|
+
These are deliberate semantic differences to keep in mind when porting queries.
|
|
193
|
+
Phase 1 ships no runtime; it produces this report plus a typed name map. The
|
|
194
|
+
phase-2 \`turbine-orm/prisma-compat\` adapter handles most of these translations.
|
|
195
|
+
|
|
196
|
+
- Cursor pagination. Turbine cursors are EXCLUSIVE and the comparison direction
|
|
197
|
+
follows the \`orderBy\` entry for the cursor field. Prisma cursors are
|
|
198
|
+
INCLUSIVE and idiomatically paired with \`skip: 1\`. Port \`{ cursor, skip: n }\`
|
|
199
|
+
(n >= 1) to a Turbine cursor plus \`offset: n - 1\`.
|
|
200
|
+
- Aggregate / groupBy \`_count\`. Prisma returns \`_count\` as a record
|
|
201
|
+
(\`{ _all: n }\` / per-field counts). Turbine's scalar \`_count: true\` returns a
|
|
202
|
+
number. Reshape as needed (the phase-2 adapter does this both directions).
|
|
203
|
+
- Relation-array order. Without an \`orderBy\` on a \`with\`/\`include\` clause, the
|
|
204
|
+
order of a to-many relation array is unspecified in Turbine (\`json_agg\` order).
|
|
205
|
+
Add an explicit \`orderBy\` where order matters.
|
|
206
|
+
- Connection URL. Prefer an explicit \`sslmode\` in the connection URL (or the
|
|
207
|
+
future-proof \`uselibpqcompat\` form) to avoid a per-boot pg SSL security
|
|
208
|
+
warning.`;
|
|
209
|
+
/** A one-line-per-model console summary for the CLI. */
|
|
210
|
+
function summaryLines(result) {
|
|
211
|
+
return result.models.map((m) => {
|
|
212
|
+
const status = modelDisplayStatus(m);
|
|
213
|
+
const target = m.accessor ? `${m.accessor} (${m.table})` : '-';
|
|
214
|
+
return `${m.prismaName} -> ${target} [${status}]`;
|
|
215
|
+
});
|
|
216
|
+
}
|