turbine-orm 0.59.2 → 0.60.1
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 +7 -0
- package/dist/cjs/cli/index.d.ts +7 -0
- package/dist/cjs/cli/index.js +28 -0
- package/dist/cjs/cli/mcp.d.ts +1 -1
- package/dist/cjs/cli/mcp.js +1 -1
- package/dist/cjs/generate.d.ts +1 -1
- package/dist/cjs/generate.js +7 -2
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.js +5 -2
- package/dist/cjs/prisma-compat.js +86 -0
- package/dist/cjs/prisma-schema-fingerprint.d.ts +61 -0
- package/dist/cjs/prisma-schema-fingerprint.js +73 -0
- package/dist/cjs/query/warn-registry.d.ts +7 -0
- package/dist/cjs/query/warn-registry.js +7 -0
- package/dist/cjs/query/where.js +1 -1
- package/dist/cjs/schema-metadata.js +1 -1
- package/dist/cjs/schema.d.ts +24 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +29 -1
- package/dist/cli/mcp.d.ts +1 -1
- package/dist/cli/mcp.js +1 -1
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +7 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/prisma-compat.js +53 -0
- package/dist/prisma-schema-fingerprint.d.ts +61 -0
- package/dist/prisma-schema-fingerprint.js +70 -0
- package/dist/query/warn-registry.d.ts +7 -0
- package/dist/query/warn-registry.js +7 -0
- package/dist/query/where.js +1 -1
- package/dist/schema-metadata.js +1 -1
- package/dist/schema.d.ts +24 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1038,6 +1038,13 @@ const prisma = createPrismaCompatClient(db, PRISMA_MAP);
|
|
|
1038
1038
|
const users = await prisma.User.findMany({ include: { posts: { take: 5 } } });
|
|
1039
1039
|
```
|
|
1040
1040
|
|
|
1041
|
+
Because nothing re-runs the generator, the map records a fingerprint of the
|
|
1042
|
+
`schema.prisma` it was built from, and `createPrismaCompatClient` warns once at startup
|
|
1043
|
+
(development only, never in production, silent when the file is absent) if that file has
|
|
1044
|
+
since changed. Put `turbine migrate-from-prisma --if-db` in `postinstall` next to
|
|
1045
|
+
`prisma generate` so it does not depend on anyone remembering; `--if-db` exits 0 when no
|
|
1046
|
+
database is reachable, so a build image with no `DATABASE_URL` still installs.
|
|
1047
|
+
|
|
1041
1048
|
Turbine-only query options (`forceCustomPlan`, `skipGlobalFilters`, `allowFullTableScan`,
|
|
1042
1049
|
`warnOnUnlimited`, `timeout`, `optimisticLock`, `distinctOn`, …) pass through the compat
|
|
1043
1050
|
delegates, and an unrecognized query-level key logs a one-time dev warning naming the
|
package/dist/cjs/cli/index.d.ts
CHANGED
|
@@ -98,6 +98,13 @@ export interface CliArgs {
|
|
|
98
98
|
allowPartial?: boolean;
|
|
99
99
|
/** `migrate-from-prisma --no-db`: parse-only, skip database resolution. */
|
|
100
100
|
noDb?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* `migrate-from-prisma --if-db`: when no connection string can be resolved,
|
|
103
|
+
* print a notice and exit 0 rather than failing. For the postinstall hook,
|
|
104
|
+
* where an `npm ci` in a build image legitimately has no database and must
|
|
105
|
+
* not be turned into a failed install.
|
|
106
|
+
*/
|
|
107
|
+
ifDb?: boolean;
|
|
101
108
|
}
|
|
102
109
|
export declare function parseArgs(argv?: string[]): CliArgs;
|
|
103
110
|
/**
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -84,6 +84,7 @@ const index_stats_js_1 = require("../index-stats.js");
|
|
|
84
84
|
const introspect_js_1 = require("../introspect.js");
|
|
85
85
|
const plan_divergence_js_1 = require("../plan-divergence.js");
|
|
86
86
|
const plan_flip_probe_js_1 = require("../plan-flip-probe.js");
|
|
87
|
+
const prisma_schema_fingerprint_js_1 = require("../prisma-schema-fingerprint.js");
|
|
87
88
|
const schema_js_1 = require("../schema.js");
|
|
88
89
|
const schema_sql_js_1 = require("../schema-sql.js");
|
|
89
90
|
const config_js_1 = require("./config.js");
|
|
@@ -274,6 +275,9 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
274
275
|
case '--no-db':
|
|
275
276
|
result.noDb = true;
|
|
276
277
|
break;
|
|
278
|
+
case '--if-db':
|
|
279
|
+
result.ifDb = true;
|
|
280
|
+
break;
|
|
277
281
|
default:
|
|
278
282
|
if (!arg.startsWith('-')) {
|
|
279
283
|
result.positional.push(arg);
|
|
@@ -1292,6 +1296,17 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1292
1296
|
const origin = variable ? `${(0, ui_js_1.cyan)(variable)} via datasource "${dsName}"` : `datasource "${dsName}" ${key}`;
|
|
1293
1297
|
(0, ui_js_1.info)(`Using the connection string declared by your Prisma schema (${origin}).`);
|
|
1294
1298
|
}
|
|
1299
|
+
if (!resolvedUrl.url && args.ifDb) {
|
|
1300
|
+
// `--if-db`: the caller is a postinstall hook. A build image with no
|
|
1301
|
+
// database is the expected case there, and failing the install over it
|
|
1302
|
+
// would make the hook worse than not having one. Leave every existing
|
|
1303
|
+
// artifact untouched and say plainly that nothing was regenerated, so a
|
|
1304
|
+
// stale map is never mistaken for a fresh one.
|
|
1305
|
+
(0, ui_js_1.newline)();
|
|
1306
|
+
(0, ui_js_1.info)(`No connection string resolved and ${(0, ui_js_1.cyan)('--if-db')} was passed: skipping, nothing regenerated.`);
|
|
1307
|
+
(0, ui_js_1.newline)();
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1295
1310
|
url = resolvedUrl.url ?? requireUrl(config, { datasourceVars: resolvedUrl.missingVariables });
|
|
1296
1311
|
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
1297
1312
|
const spinner = new ui_js_1.Spinner('Introspecting database schema').start();
|
|
@@ -1333,6 +1348,13 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1333
1348
|
(0, node_fs_1.writeFileSync)(reportPath, (0, prisma_report_js_1.formatPrismaReport)(result, { schemaPath: prismaPath, noTimestamp: args.noTimestamp }), 'utf-8');
|
|
1334
1349
|
console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.cyan)(reportPath)} ${(0, ui_js_1.dim)('(report)')}`);
|
|
1335
1350
|
if (!args.noDb && schemaMeta) {
|
|
1351
|
+
// Record WHAT WAS READ, so the runtime adapter can report a stale map
|
|
1352
|
+
// instead of quietly translating names from a schema that has moved on.
|
|
1353
|
+
// The path is stored relative to the directory this ran in: the runtime
|
|
1354
|
+
// resolves it against `process.cwd()`, and an absolute path would both
|
|
1355
|
+
// break for everyone else and commit a home directory to the repo.
|
|
1356
|
+
const sourcePath = (0, node_path_1.relative)(process.cwd(), prismaPath).split(node_path_1.sep).join('/');
|
|
1357
|
+
result.map.source = { path: sourcePath, hash: (0, prisma_schema_fingerprint_js_1.fingerprintPrismaSchema)(source) };
|
|
1336
1358
|
const mapPath = (0, node_path_1.join)(outDir, 'prisma-map.ts');
|
|
1337
1359
|
(0, node_fs_1.writeFileSync)(mapPath, (0, generate_js_1.generatePrismaMap)(result.map, { noTimestamp: args.noTimestamp }), 'utf-8');
|
|
1338
1360
|
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)')}`);
|
|
@@ -3344,6 +3366,7 @@ function showMigrateFromPrismaHelp() {
|
|
|
3344
3366
|
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)')}`);
|
|
3345
3367
|
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)')}`);
|
|
3346
3368
|
console.log(` ${(0, ui_js_1.cyan)('--no-db')} Parse-only: write the report without resolving names`);
|
|
3369
|
+
console.log(` ${(0, ui_js_1.cyan)('--if-db')} Skip (exit 0) when no connection string resolves ${(0, ui_js_1.dim)('(postinstall)')}`);
|
|
3347
3370
|
console.log(` ${(0, ui_js_1.cyan)('--allow-partial')} Exit 0 even when some items are UNRESOLVED`);
|
|
3348
3371
|
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)')}`);
|
|
3349
3372
|
(0, ui_js_1.newline)();
|
|
@@ -3351,6 +3374,11 @@ function showMigrateFromPrismaHelp() {
|
|
|
3351
3374
|
console.log(` ${(0, ui_js_1.dim)('$')} DATABASE_URL=postgres://... npx turbine migrate-from-prisma --schema prisma/schema.prisma`);
|
|
3352
3375
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate-from-prisma --schema prisma/schema.prisma --no-db`);
|
|
3353
3376
|
(0, ui_js_1.newline)();
|
|
3377
|
+
console.log(` ${(0, ui_js_1.bold)('Keeping it current:')}`);
|
|
3378
|
+
console.log(` ${(0, ui_js_1.dim)('Nothing re-runs this for you, and a stale prisma-map.ts fails silently, so')}`);
|
|
3379
|
+
console.log(` ${(0, ui_js_1.dim)('put it next to prisma generate:')}`);
|
|
3380
|
+
console.log(` ${(0, ui_js_1.dim)('"postinstall": "prisma generate && turbine migrate-from-prisma --if-db"')}`);
|
|
3381
|
+
(0, ui_js_1.newline)();
|
|
3354
3382
|
}
|
|
3355
3383
|
function showPushHelp() {
|
|
3356
3384
|
(0, ui_js_1.banner)();
|
package/dist/cjs/cli/mcp.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ interface ForeignKeyRow {
|
|
|
34
34
|
* naming to the SHARED introspection builder (`buildRelationsFromForeignKeys`
|
|
35
35
|
* + `addAutoManyToManyRelations` in ../introspect.ts). MCP previously carried
|
|
36
36
|
* a stale copy of a retired naming scheme, so `turbine mcp` and `turbine
|
|
37
|
-
* generate` derived DIFFERENT relation names from the same database
|
|
37
|
+
* generate` derived DIFFERENT relation names from the same database.
|
|
38
38
|
* Exported for the parity unit test.
|
|
39
39
|
*/
|
|
40
40
|
export declare function buildRelations(tableNames: string[], columnsByTable: Map<string, ColumnMetadata[]>, pkByTable: Map<string, string[]>, rows: ForeignKeyRow[], enums?: Record<string, string[]>): Map<string, Record<string, RelationDef>>;
|
package/dist/cjs/cli/mcp.js
CHANGED
|
@@ -630,7 +630,7 @@ async function loadSchemaMetadata(client, options) {
|
|
|
630
630
|
* naming to the SHARED introspection builder (`buildRelationsFromForeignKeys`
|
|
631
631
|
* + `addAutoManyToManyRelations` in ../introspect.ts). MCP previously carried
|
|
632
632
|
* a stale copy of a retired naming scheme, so `turbine mcp` and `turbine
|
|
633
|
-
* generate` derived DIFFERENT relation names from the same database
|
|
633
|
+
* generate` derived DIFFERENT relation names from the same database.
|
|
634
634
|
* Exported for the parity unit test.
|
|
635
635
|
*/
|
|
636
636
|
function buildRelations(tableNames, columnsByTable, pkByTable, rows, enums = {}) {
|
package/dist/cjs/generate.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface GenerateOptions {
|
|
|
25
25
|
zod?: boolean;
|
|
26
26
|
/**
|
|
27
27
|
* Omit the `Generated at: <ISO timestamp>` line from every generated file
|
|
28
|
-
* header (
|
|
28
|
+
* header (reproducible codegen). With this set, byte-identical
|
|
29
29
|
* schemas regenerate to byte-identical output, so regens produce empty
|
|
30
30
|
* diffs. Default: `false` (timestamp included, unchanged behavior).
|
|
31
31
|
*/
|
package/dist/cjs/generate.js
CHANGED
|
@@ -259,7 +259,7 @@ function stripJsonComments(text) {
|
|
|
259
259
|
// types.ts generator
|
|
260
260
|
// ---------------------------------------------------------------------------
|
|
261
261
|
function generatedFileHeader(options) {
|
|
262
|
-
// `noTimestamp` omits the volatile line entirely
|
|
262
|
+
// `noTimestamp` omits the volatile line entirely so regenerating an
|
|
263
263
|
// unchanged schema produces byte-identical files.
|
|
264
264
|
return [
|
|
265
265
|
'/**',
|
|
@@ -996,6 +996,11 @@ function generatePrismaMap(map, options) {
|
|
|
996
996
|
}
|
|
997
997
|
lines.push(' },');
|
|
998
998
|
lines.push(` enums: ${serializeStringRecord(map.enums)},`);
|
|
999
|
+
if (map.source) {
|
|
1000
|
+
// Provenance, so the runtime adapter can tell you this file is stale rather
|
|
1001
|
+
// than behaving as if the schema never changed.
|
|
1002
|
+
lines.push(` source: { path: '${escSQ(map.source.path)}', hash: '${escSQ(map.source.hash)}' },`);
|
|
1003
|
+
}
|
|
999
1004
|
lines.push('};');
|
|
1000
1005
|
lines.push('');
|
|
1001
1006
|
return lines.join('\n');
|
|
@@ -1044,7 +1049,7 @@ function serializeColumn(col) {
|
|
|
1044
1049
|
];
|
|
1045
1050
|
// Cross-schema type marker, introspection records it only for types living
|
|
1046
1051
|
// outside the introspected schema; it must survive codegen or the runtime
|
|
1047
|
-
// enum-cast guard in query/builder.ts loses the signal
|
|
1052
|
+
// enum-cast guard in query/builder.ts loses the signal.
|
|
1048
1053
|
if (col.pgTypeSchema !== undefined)
|
|
1049
1054
|
parts.push(`pgTypeSchema: '${escSQ(col.pgTypeSchema)}'`);
|
|
1050
1055
|
// Emit isGenerated only when set (server-generated serial/identity), so the
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -43,9 +43,10 @@ export { type IntrospectOptions, introspect } from './introspect.js';
|
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type MetricsFlushRow, type ObserveConfig, type ObserveHandle, type ObserveSink, PgMetricsSink, type PgMetricsSinkOptions, } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
+
export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
46
47
|
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, 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 TemporalInfinityReading, 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
48
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
|
-
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
|
+
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, PrismaSchemaSource, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
50
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
|
|
50
51
|
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
52
|
export { schemaDefToMetadata } from './schema-metadata.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.
|
|
38
|
-
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.DestructivePushRefusal = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.withDbFieldNames = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = void 0;
|
|
37
|
+
exports.validateChannel = exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.fingerprintPrismaSchema = exports.pipelineSupported = exports.executePipeline = exports.PgMetricsSink = exports.HttpJsonSink = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.ReadOnlyError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
|
|
38
|
+
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.DestructivePushRefusal = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.withDbFieldNames = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = void 0;
|
|
39
39
|
var index_js_1 = require("./adapters/index.js");
|
|
40
40
|
Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
|
|
41
41
|
Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
|
|
@@ -93,6 +93,9 @@ Object.defineProperty(exports, "PgMetricsSink", { enumerable: true, get: functio
|
|
|
93
93
|
var pipeline_js_1 = require("./pipeline.js");
|
|
94
94
|
Object.defineProperty(exports, "executePipeline", { enumerable: true, get: function () { return pipeline_js_1.executePipeline; } });
|
|
95
95
|
Object.defineProperty(exports, "pipelineSupported", { enumerable: true, get: function () { return pipeline_js_1.pipelineSupported; } });
|
|
96
|
+
// Prisma-schema fingerprint (provenance on a generated PRISMA_MAP)
|
|
97
|
+
var prisma_schema_fingerprint_js_1 = require("./prisma-schema-fingerprint.js");
|
|
98
|
+
Object.defineProperty(exports, "fingerprintPrismaSchema", { enumerable: true, get: function () { return prisma_schema_fingerprint_js_1.fingerprintPrismaSchema; } });
|
|
96
99
|
// Query builder
|
|
97
100
|
var index_js_2 = require("./query/index.js");
|
|
98
101
|
Object.defineProperty(exports, "AUTO_ASSUMED_ROUND_TRIP_MS", { enumerable: true, get: function () { return index_js_2.AUTO_ASSUMED_ROUND_TRIP_MS; } });
|
|
@@ -110,11 +110,45 @@
|
|
|
110
110
|
* });
|
|
111
111
|
* ```
|
|
112
112
|
*/
|
|
113
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
114
|
+
if (k2 === undefined) k2 = k;
|
|
115
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
116
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
117
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
118
|
+
}
|
|
119
|
+
Object.defineProperty(o, k2, desc);
|
|
120
|
+
}) : (function(o, m, k, k2) {
|
|
121
|
+
if (k2 === undefined) k2 = k;
|
|
122
|
+
o[k2] = m[k];
|
|
123
|
+
}));
|
|
124
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
125
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
126
|
+
}) : function(o, v) {
|
|
127
|
+
o["default"] = v;
|
|
128
|
+
});
|
|
129
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
130
|
+
var ownKeys = function(o) {
|
|
131
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
132
|
+
var ar = [];
|
|
133
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
134
|
+
return ar;
|
|
135
|
+
};
|
|
136
|
+
return ownKeys(o);
|
|
137
|
+
};
|
|
138
|
+
return function (mod) {
|
|
139
|
+
if (mod && mod.__esModule) return mod;
|
|
140
|
+
var result = {};
|
|
141
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
142
|
+
__setModuleDefault(result, mod);
|
|
143
|
+
return result;
|
|
144
|
+
};
|
|
145
|
+
})();
|
|
113
146
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
114
147
|
exports.CLIENT_RESERVED_KEYS = exports.COMPAT_DEFERRED = exports.PRISMA_ARG_KEYS = exports.Prisma = void 0;
|
|
115
148
|
exports.createPrismaCompatClient = createPrismaCompatClient;
|
|
116
149
|
const errors_js_1 = require("./errors.js");
|
|
117
150
|
const nested_write_js_1 = require("./nested-write.js");
|
|
151
|
+
const prisma_schema_fingerprint_js_1 = require("./prisma-schema-fingerprint.js");
|
|
118
152
|
const index_js_1 = require("./query/index.js");
|
|
119
153
|
const utils_js_1 = require("./query/utils.js");
|
|
120
154
|
const warn_registry_js_1 = require("./query/warn-registry.js");
|
|
@@ -442,6 +476,55 @@ function warnUnknownQueryOptions(model, op, args) {
|
|
|
442
476
|
// must never be the reason a query fails.
|
|
443
477
|
}
|
|
444
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Dev-mode notice that the `prisma-map.ts` driving this client no longer matches
|
|
481
|
+
* the Prisma schema it was generated from.
|
|
482
|
+
*
|
|
483
|
+
* Nothing re-runs `turbine migrate-from-prisma`. When the schema moves on, the
|
|
484
|
+
* map does not, and the adapter keeps translating the names it has: a model
|
|
485
|
+
* added last week is simply not on the client, a renamed field is quietly
|
|
486
|
+
* absent from results. Both read as adapter bugs. This turns them into one line
|
|
487
|
+
* naming the file and the command that fixes it.
|
|
488
|
+
*
|
|
489
|
+
* Everything about the check is subordinate to "never break an app that works":
|
|
490
|
+
*
|
|
491
|
+
* - **Async and unawaited.** Client construction does not wait on a file read;
|
|
492
|
+
* the warning lands a tick later or not at all.
|
|
493
|
+
* - **Any failure is silence.** A missing file is the NORMAL deployed state
|
|
494
|
+
* (nobody ships `prisma/` to production) and so are a bundled runtime with no
|
|
495
|
+
* `node:fs` and a read that is refused. None of them is evidence of drift.
|
|
496
|
+
* - **Production is skipped entirely**, before anything is imported or read.
|
|
497
|
+
* - **Once per process per path**, via the shared registry, so a process that
|
|
498
|
+
* builds a client per request says it once.
|
|
499
|
+
*
|
|
500
|
+
* @param map - the map under test; a map with no `source` (hand-written, or
|
|
501
|
+
* emitted before 0.60) is skipped, since absence carries no information.
|
|
502
|
+
*/
|
|
503
|
+
function warnStalePrismaMap(map) {
|
|
504
|
+
const source = map.source;
|
|
505
|
+
if (!source || typeof source.path !== 'string' || typeof source.hash !== 'string')
|
|
506
|
+
return;
|
|
507
|
+
if (typeof process === 'undefined' || process.env?.NODE_ENV === 'production')
|
|
508
|
+
return;
|
|
509
|
+
void (async () => {
|
|
510
|
+
try {
|
|
511
|
+
// Dynamic so a bundle that never reaches this line never needs `node:fs`.
|
|
512
|
+
const { readFile } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
513
|
+
const onDisk = await readFile(source.path, 'utf8');
|
|
514
|
+
if ((0, prisma_schema_fingerprint_js_1.fingerprintPrismaSchema)(onDisk) === source.hash)
|
|
515
|
+
return;
|
|
516
|
+
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.stalePrismaMap, source.path))
|
|
517
|
+
return;
|
|
518
|
+
console.warn(`[turbine] prisma-compat: ${source.path} has changed since prisma-map.ts was generated,` +
|
|
519
|
+
' so any model, field, relation or compound-unique added or renamed since then is missing' +
|
|
520
|
+
' from the compat client. Re-run: turbine migrate-from-prisma');
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// No file, no `node:fs`, no permission. None of these means the map is
|
|
524
|
+
// stale, and a diagnostic must never be the reason a client fails.
|
|
525
|
+
}
|
|
526
|
+
})();
|
|
527
|
+
}
|
|
445
528
|
/**
|
|
446
529
|
* Prisma 6.7+ accepts `limit` on `updateMany` / `deleteMany` to BOUND how many
|
|
447
530
|
* rows a mass mutation touches. Turbine has no row-bounded mass mutation, so
|
|
@@ -1844,6 +1927,9 @@ function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1844
1927
|
// the real client satisfies it structurally (the cast just relaxes the strict
|
|
1845
1928
|
// callback-param variance on `$transaction`).
|
|
1846
1929
|
const db = client;
|
|
1930
|
+
// Fire-and-forget, dev-only, once per process: is this map still describing
|
|
1931
|
+
// the schema on disk? Nothing else in the system ever asks.
|
|
1932
|
+
warnStalePrismaMap(map);
|
|
1847
1933
|
const tableToModel = new Map();
|
|
1848
1934
|
for (const [prismaModel, mm] of Object.entries(map.models))
|
|
1849
1935
|
tableToModel.set(mm.table, prismaModel);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm, fingerprint of the Prisma schema a `PRISMA_MAP` was generated
|
|
3
|
+
* from.
|
|
4
|
+
*
|
|
5
|
+
* ## The failure this exists for
|
|
6
|
+
*
|
|
7
|
+
* `turbine migrate-from-prisma` reads `prisma/schema.prisma` and emits a
|
|
8
|
+
* `prisma-map.ts` that the `turbine-orm/prisma-compat` adapter is driven by.
|
|
9
|
+
* Nothing re-runs it. Add a model, rename a field, change a `@@unique` name, and
|
|
10
|
+
* the map keeps describing the schema as it was: the adapter goes on translating
|
|
11
|
+
* the names it knows and simply has no entry for the new ones. That surfaces as
|
|
12
|
+
* "this model is not on the compat client" or a field silently absent from a
|
|
13
|
+
* result, which reads like an adapter bug rather than a stale artifact, and
|
|
14
|
+
* which is exactly the failure mode a code generator ought to be able to
|
|
15
|
+
* announce about itself.
|
|
16
|
+
*
|
|
17
|
+
* So the generator records what it read, and the adapter checks that record
|
|
18
|
+
* against what is on disk. This is the same shape as the unknown-config-key
|
|
19
|
+
* check in `client.ts` and the parser-overwrite warning in `query/utils.ts`,
|
|
20
|
+
* with a higher severity than either: those two produce visible breakage on
|
|
21
|
+
* their own, and this one produces none at all.
|
|
22
|
+
*
|
|
23
|
+
* ## Why FNV-1a and not a cryptographic digest
|
|
24
|
+
*
|
|
25
|
+
* The question asked is "is this the same file", not "did an adversary craft a
|
|
26
|
+
* collision". A collision costs one missed warning, so a 64-bit non-crypto hash
|
|
27
|
+
* is the right instrument, and it keeps this module pure: no `node:crypto`, no
|
|
28
|
+
* new dependency, and it is importable from the runtime adapter, from the CLI,
|
|
29
|
+
* and from an edge bundle alike.
|
|
30
|
+
*
|
|
31
|
+
* ## Normalization
|
|
32
|
+
*
|
|
33
|
+
* Deliberately minimal, and only for differences that are not edits:
|
|
34
|
+
*
|
|
35
|
+
* - a leading UTF-8 BOM, which some editors add and remove without being asked;
|
|
36
|
+
* - CRLF and lone CR line endings, so a Windows checkout of an unchanged file
|
|
37
|
+
* is not reported as drift (`core.autocrlf` rewrites on checkout);
|
|
38
|
+
* - whitespace at the very END of the file, for the same reason.
|
|
39
|
+
*
|
|
40
|
+
* Note the last one is end-of-FILE only, not per line. Trailing whitespace on an
|
|
41
|
+
* individual line is hashed, and changing it is reported as drift. That is the
|
|
42
|
+
* intended reading: nothing in a checkout rewrites it, so it got there because
|
|
43
|
+
* someone edited the line, and the cost of an unnecessary regeneration is lower
|
|
44
|
+
* than the cost of a missed one.
|
|
45
|
+
*
|
|
46
|
+
* Nothing else is normalized. Comments and blank lines are hashed as-is: a
|
|
47
|
+
* "harmless" edit is still an edit, and reporting one costs a regeneration
|
|
48
|
+
* nobody regrets, while normalizing one away risks silence on a real change
|
|
49
|
+
* whose only visible trace happened to be a comment.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Fingerprint the TEXT of a Prisma schema file. Stable across line-ending and
|
|
53
|
+
* BOM differences that a checkout can introduce (see the module doc), sensitive
|
|
54
|
+
* to every other byte.
|
|
55
|
+
*
|
|
56
|
+
* The returned string is opaque and versioned by its `v1:` prefix: if the
|
|
57
|
+
* normalization ever has to change, old maps carry the old prefix and can be
|
|
58
|
+
* recognized rather than reported as drift against a rule they were not
|
|
59
|
+
* generated under.
|
|
60
|
+
*/
|
|
61
|
+
export declare function fingerprintPrismaSchema(source: string): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm, fingerprint of the Prisma schema a `PRISMA_MAP` was generated
|
|
4
|
+
* from.
|
|
5
|
+
*
|
|
6
|
+
* ## The failure this exists for
|
|
7
|
+
*
|
|
8
|
+
* `turbine migrate-from-prisma` reads `prisma/schema.prisma` and emits a
|
|
9
|
+
* `prisma-map.ts` that the `turbine-orm/prisma-compat` adapter is driven by.
|
|
10
|
+
* Nothing re-runs it. Add a model, rename a field, change a `@@unique` name, and
|
|
11
|
+
* the map keeps describing the schema as it was: the adapter goes on translating
|
|
12
|
+
* the names it knows and simply has no entry for the new ones. That surfaces as
|
|
13
|
+
* "this model is not on the compat client" or a field silently absent from a
|
|
14
|
+
* result, which reads like an adapter bug rather than a stale artifact, and
|
|
15
|
+
* which is exactly the failure mode a code generator ought to be able to
|
|
16
|
+
* announce about itself.
|
|
17
|
+
*
|
|
18
|
+
* So the generator records what it read, and the adapter checks that record
|
|
19
|
+
* against what is on disk. This is the same shape as the unknown-config-key
|
|
20
|
+
* check in `client.ts` and the parser-overwrite warning in `query/utils.ts`,
|
|
21
|
+
* with a higher severity than either: those two produce visible breakage on
|
|
22
|
+
* their own, and this one produces none at all.
|
|
23
|
+
*
|
|
24
|
+
* ## Why FNV-1a and not a cryptographic digest
|
|
25
|
+
*
|
|
26
|
+
* The question asked is "is this the same file", not "did an adversary craft a
|
|
27
|
+
* collision". A collision costs one missed warning, so a 64-bit non-crypto hash
|
|
28
|
+
* is the right instrument, and it keeps this module pure: no `node:crypto`, no
|
|
29
|
+
* new dependency, and it is importable from the runtime adapter, from the CLI,
|
|
30
|
+
* and from an edge bundle alike.
|
|
31
|
+
*
|
|
32
|
+
* ## Normalization
|
|
33
|
+
*
|
|
34
|
+
* Deliberately minimal, and only for differences that are not edits:
|
|
35
|
+
*
|
|
36
|
+
* - a leading UTF-8 BOM, which some editors add and remove without being asked;
|
|
37
|
+
* - CRLF and lone CR line endings, so a Windows checkout of an unchanged file
|
|
38
|
+
* is not reported as drift (`core.autocrlf` rewrites on checkout);
|
|
39
|
+
* - whitespace at the very END of the file, for the same reason.
|
|
40
|
+
*
|
|
41
|
+
* Note the last one is end-of-FILE only, not per line. Trailing whitespace on an
|
|
42
|
+
* individual line is hashed, and changing it is reported as drift. That is the
|
|
43
|
+
* intended reading: nothing in a checkout rewrites it, so it got there because
|
|
44
|
+
* someone edited the line, and the cost of an unnecessary regeneration is lower
|
|
45
|
+
* than the cost of a missed one.
|
|
46
|
+
*
|
|
47
|
+
* Nothing else is normalized. Comments and blank lines are hashed as-is: a
|
|
48
|
+
* "harmless" edit is still an edit, and reporting one costs a regeneration
|
|
49
|
+
* nobody regrets, while normalizing one away risks silence on a real change
|
|
50
|
+
* whose only visible trace happened to be a comment.
|
|
51
|
+
*/
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.fingerprintPrismaSchema = fingerprintPrismaSchema;
|
|
54
|
+
const utils_js_1 = require("./query/utils.js");
|
|
55
|
+
/**
|
|
56
|
+
* Fingerprint the TEXT of a Prisma schema file. Stable across line-ending and
|
|
57
|
+
* BOM differences that a checkout can introduce (see the module doc), sensitive
|
|
58
|
+
* to every other byte.
|
|
59
|
+
*
|
|
60
|
+
* The returned string is opaque and versioned by its `v1:` prefix: if the
|
|
61
|
+
* normalization ever has to change, old maps carry the old prefix and can be
|
|
62
|
+
* recognized rather than reported as drift against a rule they were not
|
|
63
|
+
* generated under.
|
|
64
|
+
*/
|
|
65
|
+
function fingerprintPrismaSchema(source) {
|
|
66
|
+
const normalized = source
|
|
67
|
+
// Written as an escape, not the literal character: a bare BOM in source is
|
|
68
|
+
// invisible in a diff and some tools strip it from the file it lives in.
|
|
69
|
+
.replace(/^\uFEFF/, '')
|
|
70
|
+
.replace(/\r\n?/g, '\n')
|
|
71
|
+
.replace(/\s+$/, '');
|
|
72
|
+
return `v1:${(0, utils_js_1.fnv1a64Hex)(normalized)}`;
|
|
73
|
+
}
|
|
@@ -104,6 +104,13 @@ export declare const WARN_NS: {
|
|
|
104
104
|
* a million executions of one call site is one.
|
|
105
105
|
*/
|
|
106
106
|
readonly unknownQueryOption: "unknownQueryOption";
|
|
107
|
+
/**
|
|
108
|
+
* The `prisma-map.ts` driving a `turbine-orm/prisma-compat` client no longer
|
|
109
|
+
* matches the Prisma schema it was generated from (prisma-compat.ts
|
|
110
|
+
* `warnStalePrismaMap`). Keyed on the schema path, so a process that builds a
|
|
111
|
+
* compat client per request reports it once.
|
|
112
|
+
*/
|
|
113
|
+
readonly stalePrismaMap: "stalePrismaMap";
|
|
107
114
|
/**
|
|
108
115
|
* `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
|
|
109
116
|
* runs no connection setup, so the option is a no-op (client.ts constructor).
|
|
@@ -145,6 +145,13 @@ exports.WARN_NS = {
|
|
|
145
145
|
* a million executions of one call site is one.
|
|
146
146
|
*/
|
|
147
147
|
unknownQueryOption: 'unknownQueryOption',
|
|
148
|
+
/**
|
|
149
|
+
* The `prisma-map.ts` driving a `turbine-orm/prisma-compat` client no longer
|
|
150
|
+
* matches the Prisma schema it was generated from (prisma-compat.ts
|
|
151
|
+
* `warnStalePrismaMap`). Keyed on the schema path, so a process that builds a
|
|
152
|
+
* compat client per request reports it once.
|
|
153
|
+
*/
|
|
154
|
+
stalePrismaMap: 'stalePrismaMap',
|
|
148
155
|
/**
|
|
149
156
|
* `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
|
|
150
157
|
* runs no connection setup, so the option is a no-op (client.ts constructor).
|
package/dist/cjs/query/where.js
CHANGED
|
@@ -1064,7 +1064,7 @@ function enumTypeForColumn(qi, column) {
|
|
|
1064
1064
|
const enums = qi.schema.enums;
|
|
1065
1065
|
if (!enums)
|
|
1066
1066
|
return null;
|
|
1067
|
-
// Cross-schema guard
|
|
1067
|
+
// Cross-schema guard: introspection records pgTypeSchema ONLY when
|
|
1068
1068
|
// the column's type lives OUTSIDE the introspected schema. A same-named
|
|
1069
1069
|
// enum in another schema must not get this schema's cast, search_path
|
|
1070
1070
|
// would resolve `::"status"` to the wrong type. Skipping the cast restores
|
|
@@ -252,7 +252,7 @@ function schemaDefToMetadata(def) {
|
|
|
252
252
|
// local reimplementation had NO collision guard, `posts.user` (text) +
|
|
253
253
|
// `userId references users.id` produced a relation `user` that shadowed the
|
|
254
254
|
// scalar, and two FKs deriving the same name silently clobbered each other
|
|
255
|
-
|
|
255
|
+
//.
|
|
256
256
|
//
|
|
257
257
|
// Constraint names are synthesized in pg's default `<table>_<column>_fkey`
|
|
258
258
|
// form; they only feed the referential-action lookup and composite-FK
|
package/dist/cjs/schema.d.ts
CHANGED
|
@@ -325,6 +325,30 @@ export interface PrismaCompatMap {
|
|
|
325
325
|
models: Record<string, PrismaModelMap>;
|
|
326
326
|
/** Prisma enum name → resolved database enum-type name. */
|
|
327
327
|
enums: Record<string, string>;
|
|
328
|
+
/**
|
|
329
|
+
* The Prisma schema file this map was generated from. Present on maps emitted
|
|
330
|
+
* by `turbine migrate-from-prisma` since 0.60; absent on older ones, and on a
|
|
331
|
+
* map assembled by hand.
|
|
332
|
+
*
|
|
333
|
+
* `turbine-orm/prisma-compat` uses it for a once-per-process, dev-only staleness
|
|
334
|
+
* check: nothing re-runs the generator, so a map can silently fall behind the
|
|
335
|
+
* schema it describes and the adapter has no way to know. See
|
|
336
|
+
* {@link fingerprintPrismaSchema}.
|
|
337
|
+
*/
|
|
338
|
+
source?: PrismaSchemaSource;
|
|
339
|
+
}
|
|
340
|
+
/** Provenance of a generated {@link PrismaCompatMap}. */
|
|
341
|
+
export interface PrismaSchemaSource {
|
|
342
|
+
/**
|
|
343
|
+
* The Prisma schema file, POSIX-separated and relative to the directory the
|
|
344
|
+
* generator ran in (which for the documented workflow is the project root, so
|
|
345
|
+
* it resolves against `process.cwd()` at runtime). Relative rather than
|
|
346
|
+
* absolute so the value is identical for every developer and in CI, and so a
|
|
347
|
+
* generated file never carries someone's home directory into version control.
|
|
348
|
+
*/
|
|
349
|
+
path: string;
|
|
350
|
+
/** {@link fingerprintPrismaSchema} of that file's contents at generation time. */
|
|
351
|
+
hash: string;
|
|
328
352
|
}
|
|
329
353
|
/** One Prisma model's resolved mapping onto a Turbine table + client accessor. */
|
|
330
354
|
export interface PrismaModelMap {
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -98,6 +98,13 @@ export interface CliArgs {
|
|
|
98
98
|
allowPartial?: boolean;
|
|
99
99
|
/** `migrate-from-prisma --no-db`: parse-only, skip database resolution. */
|
|
100
100
|
noDb?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* `migrate-from-prisma --if-db`: when no connection string can be resolved,
|
|
103
|
+
* print a notice and exit 0 rather than failing. For the postinstall hook,
|
|
104
|
+
* where an `npm ci` in a build image legitimately has no database and must
|
|
105
|
+
* not be turned into a failed install.
|
|
106
|
+
*/
|
|
107
|
+
ifDb?: boolean;
|
|
101
108
|
}
|
|
102
109
|
export declare function parseArgs(argv?: string[]): CliArgs;
|
|
103
110
|
/**
|
package/dist/cli/index.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
28
28
|
import { tmpdir } from 'node:os';
|
|
29
|
-
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
29
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
30
30
|
import { pathToFileURL } from 'node:url';
|
|
31
31
|
import { generate, generatePrismaMap } from '../generate.js';
|
|
32
32
|
import { buildCreateIndexSql, buildDropIndexSql, collectDoctorProbeIndexNames, collectRelationProbeColumns, findMissingRelationIndexes, } from '../index-advisor.js';
|
|
@@ -34,6 +34,7 @@ import { auditDoctorIndexes, collectStatsSnapshot, collectTableHeat, findInvalid
|
|
|
34
34
|
import { introspect } from '../introspect.js';
|
|
35
35
|
import { collectDivergenceCandidateColumns, collectDivergenceOrderColumns, findPlanDivergence, PLAN_DIVERGENCE_THRESHOLDS, } from '../plan-divergence.js';
|
|
36
36
|
import { applyFlipVerdicts, emptyFlipProbeResult, needsFlipProbe, probePlanFlips } from '../plan-flip-probe.js';
|
|
37
|
+
import { fingerprintPrismaSchema } from '../prisma-schema-fingerprint.js';
|
|
37
38
|
import { snakeToCamel } from '../schema.js';
|
|
38
39
|
import { DestructivePushRefusal, schemaDiff, schemaPush } from '../schema-sql.js';
|
|
39
40
|
import { configTemplate, DEFAULT_INIT_SEED_FILE, findConfigFile, loadConfigResult, looksLikeSchemaFilePath, resolveConfig, resolveSeedFile, unwrapModuleDefault, } from './config.js';
|
|
@@ -224,6 +225,9 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
224
225
|
case '--no-db':
|
|
225
226
|
result.noDb = true;
|
|
226
227
|
break;
|
|
228
|
+
case '--if-db':
|
|
229
|
+
result.ifDb = true;
|
|
230
|
+
break;
|
|
227
231
|
default:
|
|
228
232
|
if (!arg.startsWith('-')) {
|
|
229
233
|
result.positional.push(arg);
|
|
@@ -1242,6 +1246,17 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1242
1246
|
const origin = variable ? `${cyan(variable)} via datasource "${dsName}"` : `datasource "${dsName}" ${key}`;
|
|
1243
1247
|
info(`Using the connection string declared by your Prisma schema (${origin}).`);
|
|
1244
1248
|
}
|
|
1249
|
+
if (!resolvedUrl.url && args.ifDb) {
|
|
1250
|
+
// `--if-db`: the caller is a postinstall hook. A build image with no
|
|
1251
|
+
// database is the expected case there, and failing the install over it
|
|
1252
|
+
// would make the hook worse than not having one. Leave every existing
|
|
1253
|
+
// artifact untouched and say plainly that nothing was regenerated, so a
|
|
1254
|
+
// stale map is never mistaken for a fresh one.
|
|
1255
|
+
newline();
|
|
1256
|
+
info(`No connection string resolved and ${cyan('--if-db')} was passed: skipping, nothing regenerated.`);
|
|
1257
|
+
newline();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1245
1260
|
url = resolvedUrl.url ?? requireUrl(config, { datasourceVars: resolvedUrl.missingVariables });
|
|
1246
1261
|
label('Database', redactUrl(url));
|
|
1247
1262
|
const spinner = new Spinner('Introspecting database schema').start();
|
|
@@ -1283,6 +1298,13 @@ async function cmdMigrateFromPrisma(args, config) {
|
|
|
1283
1298
|
writeFileSync(reportPath, formatPrismaReport(result, { schemaPath: prismaPath, noTimestamp: args.noTimestamp }), 'utf-8');
|
|
1284
1299
|
console.log(` ${dim(symbols.teeEnd)} ${cyan(reportPath)} ${dim('(report)')}`);
|
|
1285
1300
|
if (!args.noDb && schemaMeta) {
|
|
1301
|
+
// Record WHAT WAS READ, so the runtime adapter can report a stale map
|
|
1302
|
+
// instead of quietly translating names from a schema that has moved on.
|
|
1303
|
+
// The path is stored relative to the directory this ran in: the runtime
|
|
1304
|
+
// resolves it against `process.cwd()`, and an absolute path would both
|
|
1305
|
+
// break for everyone else and commit a home directory to the repo.
|
|
1306
|
+
const sourcePath = relative(process.cwd(), prismaPath).split(sep).join('/');
|
|
1307
|
+
result.map.source = { path: sourcePath, hash: fingerprintPrismaSchema(source) };
|
|
1286
1308
|
const mapPath = join(outDir, 'prisma-map.ts');
|
|
1287
1309
|
writeFileSync(mapPath, generatePrismaMap(result.map, { noTimestamp: args.noTimestamp }), 'utf-8');
|
|
1288
1310
|
console.log(` ${dim(symbols.teeEnd)} ${cyan(mapPath)} ${dim('(typed name map)')}`);
|
|
@@ -3294,6 +3316,7 @@ function showMigrateFromPrismaHelp() {
|
|
|
3294
3316
|
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string ${dim('(unless --no-db)')}`);
|
|
3295
3317
|
console.log(` ${cyan('--out, -o')} ${dim('<dir>')} Output directory ${dim('(default: ./generated/turbine)')}`);
|
|
3296
3318
|
console.log(` ${cyan('--no-db')} Parse-only: write the report without resolving names`);
|
|
3319
|
+
console.log(` ${cyan('--if-db')} Skip (exit 0) when no connection string resolves ${dim('(postinstall)')}`);
|
|
3297
3320
|
console.log(` ${cyan('--allow-partial')} Exit 0 even when some items are UNRESOLVED`);
|
|
3298
3321
|
console.log(` ${cyan('--no-timestamp')} Omit the ${dim('Generated:')} lines ${dim('(reproducible output)')}`);
|
|
3299
3322
|
newline();
|
|
@@ -3301,6 +3324,11 @@ function showMigrateFromPrismaHelp() {
|
|
|
3301
3324
|
console.log(` ${dim('$')} DATABASE_URL=postgres://... npx turbine migrate-from-prisma --schema prisma/schema.prisma`);
|
|
3302
3325
|
console.log(` ${dim('$')} npx turbine migrate-from-prisma --schema prisma/schema.prisma --no-db`);
|
|
3303
3326
|
newline();
|
|
3327
|
+
console.log(` ${bold('Keeping it current:')}`);
|
|
3328
|
+
console.log(` ${dim('Nothing re-runs this for you, and a stale prisma-map.ts fails silently, so')}`);
|
|
3329
|
+
console.log(` ${dim('put it next to prisma generate:')}`);
|
|
3330
|
+
console.log(` ${dim('"postinstall": "prisma generate && turbine migrate-from-prisma --if-db"')}`);
|
|
3331
|
+
newline();
|
|
3304
3332
|
}
|
|
3305
3333
|
function showPushHelp() {
|
|
3306
3334
|
banner();
|
package/dist/cli/mcp.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ interface ForeignKeyRow {
|
|
|
34
34
|
* naming to the SHARED introspection builder (`buildRelationsFromForeignKeys`
|
|
35
35
|
* + `addAutoManyToManyRelations` in ../introspect.ts). MCP previously carried
|
|
36
36
|
* a stale copy of a retired naming scheme, so `turbine mcp` and `turbine
|
|
37
|
-
* generate` derived DIFFERENT relation names from the same database
|
|
37
|
+
* generate` derived DIFFERENT relation names from the same database.
|
|
38
38
|
* Exported for the parity unit test.
|
|
39
39
|
*/
|
|
40
40
|
export declare function buildRelations(tableNames: string[], columnsByTable: Map<string, ColumnMetadata[]>, pkByTable: Map<string, string[]>, rows: ForeignKeyRow[], enums?: Record<string, string[]>): Map<string, Record<string, RelationDef>>;
|
package/dist/cli/mcp.js
CHANGED
|
@@ -622,7 +622,7 @@ async function loadSchemaMetadata(client, options) {
|
|
|
622
622
|
* naming to the SHARED introspection builder (`buildRelationsFromForeignKeys`
|
|
623
623
|
* + `addAutoManyToManyRelations` in ../introspect.ts). MCP previously carried
|
|
624
624
|
* a stale copy of a retired naming scheme, so `turbine mcp` and `turbine
|
|
625
|
-
* generate` derived DIFFERENT relation names from the same database
|
|
625
|
+
* generate` derived DIFFERENT relation names from the same database.
|
|
626
626
|
* Exported for the parity unit test.
|
|
627
627
|
*/
|
|
628
628
|
export function buildRelations(tableNames, columnsByTable, pkByTable, rows, enums = {}) {
|
package/dist/generate.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface GenerateOptions {
|
|
|
25
25
|
zod?: boolean;
|
|
26
26
|
/**
|
|
27
27
|
* Omit the `Generated at: <ISO timestamp>` line from every generated file
|
|
28
|
-
* header (
|
|
28
|
+
* header (reproducible codegen). With this set, byte-identical
|
|
29
29
|
* schemas regenerate to byte-identical output, so regens produce empty
|
|
30
30
|
* diffs. Default: `false` (timestamp included, unchanged behavior).
|
|
31
31
|
*/
|
package/dist/generate.js
CHANGED
|
@@ -247,7 +247,7 @@ export function stripJsonComments(text) {
|
|
|
247
247
|
// types.ts generator
|
|
248
248
|
// ---------------------------------------------------------------------------
|
|
249
249
|
function generatedFileHeader(options) {
|
|
250
|
-
// `noTimestamp` omits the volatile line entirely
|
|
250
|
+
// `noTimestamp` omits the volatile line entirely so regenerating an
|
|
251
251
|
// unchanged schema produces byte-identical files.
|
|
252
252
|
return [
|
|
253
253
|
'/**',
|
|
@@ -984,6 +984,11 @@ export function generatePrismaMap(map, options) {
|
|
|
984
984
|
}
|
|
985
985
|
lines.push(' },');
|
|
986
986
|
lines.push(` enums: ${serializeStringRecord(map.enums)},`);
|
|
987
|
+
if (map.source) {
|
|
988
|
+
// Provenance, so the runtime adapter can tell you this file is stale rather
|
|
989
|
+
// than behaving as if the schema never changed.
|
|
990
|
+
lines.push(` source: { path: '${escSQ(map.source.path)}', hash: '${escSQ(map.source.hash)}' },`);
|
|
991
|
+
}
|
|
987
992
|
lines.push('};');
|
|
988
993
|
lines.push('');
|
|
989
994
|
return lines.join('\n');
|
|
@@ -1032,7 +1037,7 @@ function serializeColumn(col) {
|
|
|
1032
1037
|
];
|
|
1033
1038
|
// Cross-schema type marker, introspection records it only for types living
|
|
1034
1039
|
// outside the introspected schema; it must survive codegen or the runtime
|
|
1035
|
-
// enum-cast guard in query/builder.ts loses the signal
|
|
1040
|
+
// enum-cast guard in query/builder.ts loses the signal.
|
|
1036
1041
|
if (col.pgTypeSchema !== undefined)
|
|
1037
1042
|
parts.push(`pgTypeSchema: '${escSQ(col.pgTypeSchema)}'`);
|
|
1038
1043
|
// Emit isGenerated only when set (server-generated serial/identity), so the
|
package/dist/index.d.ts
CHANGED
|
@@ -43,9 +43,10 @@ export { type IntrospectOptions, introspect } from './introspect.js';
|
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type MetricsFlushRow, type ObserveConfig, type ObserveHandle, type ObserveSink, PgMetricsSink, type PgMetricsSinkOptions, } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
+
export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
46
47
|
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, 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 TemporalInfinityReading, 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
48
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
|
-
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
|
+
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, PrismaSchemaSource, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
50
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
|
|
50
51
|
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
52
|
export { schemaDefToMetadata } from './schema-metadata.js';
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,8 @@ export { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './
|
|
|
48
48
|
export { HttpJsonSink, PgMetricsSink, } from './observe.js';
|
|
49
49
|
// Pipeline
|
|
50
50
|
export { executePipeline, pipelineSupported } from './pipeline.js';
|
|
51
|
+
// Prisma-schema fingerprint (provenance on a generated PRISMA_MAP)
|
|
52
|
+
export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
51
53
|
// Query builder
|
|
52
54
|
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './query/index.js';
|
|
53
55
|
// Realtime, LISTEN/NOTIFY pub/sub
|
package/dist/prisma-compat.js
CHANGED
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
*/
|
|
112
112
|
import { TurbineError, TurbineErrorCode, UnsupportedFeatureError, ValidationError, wrapPgError } from './errors.js';
|
|
113
113
|
import { createManyShapeRuns } from './nested-write.js';
|
|
114
|
+
import { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
114
115
|
import { AGGREGATE_OPTIONS, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './query/index.js';
|
|
115
116
|
import { suggestKey } from './query/utils.js';
|
|
116
117
|
import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
|
|
@@ -438,6 +439,55 @@ function warnUnknownQueryOptions(model, op, args) {
|
|
|
438
439
|
// must never be the reason a query fails.
|
|
439
440
|
}
|
|
440
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Dev-mode notice that the `prisma-map.ts` driving this client no longer matches
|
|
444
|
+
* the Prisma schema it was generated from.
|
|
445
|
+
*
|
|
446
|
+
* Nothing re-runs `turbine migrate-from-prisma`. When the schema moves on, the
|
|
447
|
+
* map does not, and the adapter keeps translating the names it has: a model
|
|
448
|
+
* added last week is simply not on the client, a renamed field is quietly
|
|
449
|
+
* absent from results. Both read as adapter bugs. This turns them into one line
|
|
450
|
+
* naming the file and the command that fixes it.
|
|
451
|
+
*
|
|
452
|
+
* Everything about the check is subordinate to "never break an app that works":
|
|
453
|
+
*
|
|
454
|
+
* - **Async and unawaited.** Client construction does not wait on a file read;
|
|
455
|
+
* the warning lands a tick later or not at all.
|
|
456
|
+
* - **Any failure is silence.** A missing file is the NORMAL deployed state
|
|
457
|
+
* (nobody ships `prisma/` to production) and so are a bundled runtime with no
|
|
458
|
+
* `node:fs` and a read that is refused. None of them is evidence of drift.
|
|
459
|
+
* - **Production is skipped entirely**, before anything is imported or read.
|
|
460
|
+
* - **Once per process per path**, via the shared registry, so a process that
|
|
461
|
+
* builds a client per request says it once.
|
|
462
|
+
*
|
|
463
|
+
* @param map - the map under test; a map with no `source` (hand-written, or
|
|
464
|
+
* emitted before 0.60) is skipped, since absence carries no information.
|
|
465
|
+
*/
|
|
466
|
+
function warnStalePrismaMap(map) {
|
|
467
|
+
const source = map.source;
|
|
468
|
+
if (!source || typeof source.path !== 'string' || typeof source.hash !== 'string')
|
|
469
|
+
return;
|
|
470
|
+
if (typeof process === 'undefined' || process.env?.NODE_ENV === 'production')
|
|
471
|
+
return;
|
|
472
|
+
void (async () => {
|
|
473
|
+
try {
|
|
474
|
+
// Dynamic so a bundle that never reaches this line never needs `node:fs`.
|
|
475
|
+
const { readFile } = await import('node:fs/promises');
|
|
476
|
+
const onDisk = await readFile(source.path, 'utf8');
|
|
477
|
+
if (fingerprintPrismaSchema(onDisk) === source.hash)
|
|
478
|
+
return;
|
|
479
|
+
if (!shouldWarnOnce(WARN_NS.stalePrismaMap, source.path))
|
|
480
|
+
return;
|
|
481
|
+
console.warn(`[turbine] prisma-compat: ${source.path} has changed since prisma-map.ts was generated,` +
|
|
482
|
+
' so any model, field, relation or compound-unique added or renamed since then is missing' +
|
|
483
|
+
' from the compat client. Re-run: turbine migrate-from-prisma');
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// No file, no `node:fs`, no permission. None of these means the map is
|
|
487
|
+
// stale, and a diagnostic must never be the reason a client fails.
|
|
488
|
+
}
|
|
489
|
+
})();
|
|
490
|
+
}
|
|
441
491
|
/**
|
|
442
492
|
* Prisma 6.7+ accepts `limit` on `updateMany` / `deleteMany` to BOUND how many
|
|
443
493
|
* rows a mass mutation touches. Turbine has no row-bounded mass mutation, so
|
|
@@ -1840,6 +1890,9 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1840
1890
|
// the real client satisfies it structurally (the cast just relaxes the strict
|
|
1841
1891
|
// callback-param variance on `$transaction`).
|
|
1842
1892
|
const db = client;
|
|
1893
|
+
// Fire-and-forget, dev-only, once per process: is this map still describing
|
|
1894
|
+
// the schema on disk? Nothing else in the system ever asks.
|
|
1895
|
+
warnStalePrismaMap(map);
|
|
1843
1896
|
const tableToModel = new Map();
|
|
1844
1897
|
for (const [prismaModel, mm] of Object.entries(map.models))
|
|
1845
1898
|
tableToModel.set(mm.table, prismaModel);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm, fingerprint of the Prisma schema a `PRISMA_MAP` was generated
|
|
3
|
+
* from.
|
|
4
|
+
*
|
|
5
|
+
* ## The failure this exists for
|
|
6
|
+
*
|
|
7
|
+
* `turbine migrate-from-prisma` reads `prisma/schema.prisma` and emits a
|
|
8
|
+
* `prisma-map.ts` that the `turbine-orm/prisma-compat` adapter is driven by.
|
|
9
|
+
* Nothing re-runs it. Add a model, rename a field, change a `@@unique` name, and
|
|
10
|
+
* the map keeps describing the schema as it was: the adapter goes on translating
|
|
11
|
+
* the names it knows and simply has no entry for the new ones. That surfaces as
|
|
12
|
+
* "this model is not on the compat client" or a field silently absent from a
|
|
13
|
+
* result, which reads like an adapter bug rather than a stale artifact, and
|
|
14
|
+
* which is exactly the failure mode a code generator ought to be able to
|
|
15
|
+
* announce about itself.
|
|
16
|
+
*
|
|
17
|
+
* So the generator records what it read, and the adapter checks that record
|
|
18
|
+
* against what is on disk. This is the same shape as the unknown-config-key
|
|
19
|
+
* check in `client.ts` and the parser-overwrite warning in `query/utils.ts`,
|
|
20
|
+
* with a higher severity than either: those two produce visible breakage on
|
|
21
|
+
* their own, and this one produces none at all.
|
|
22
|
+
*
|
|
23
|
+
* ## Why FNV-1a and not a cryptographic digest
|
|
24
|
+
*
|
|
25
|
+
* The question asked is "is this the same file", not "did an adversary craft a
|
|
26
|
+
* collision". A collision costs one missed warning, so a 64-bit non-crypto hash
|
|
27
|
+
* is the right instrument, and it keeps this module pure: no `node:crypto`, no
|
|
28
|
+
* new dependency, and it is importable from the runtime adapter, from the CLI,
|
|
29
|
+
* and from an edge bundle alike.
|
|
30
|
+
*
|
|
31
|
+
* ## Normalization
|
|
32
|
+
*
|
|
33
|
+
* Deliberately minimal, and only for differences that are not edits:
|
|
34
|
+
*
|
|
35
|
+
* - a leading UTF-8 BOM, which some editors add and remove without being asked;
|
|
36
|
+
* - CRLF and lone CR line endings, so a Windows checkout of an unchanged file
|
|
37
|
+
* is not reported as drift (`core.autocrlf` rewrites on checkout);
|
|
38
|
+
* - whitespace at the very END of the file, for the same reason.
|
|
39
|
+
*
|
|
40
|
+
* Note the last one is end-of-FILE only, not per line. Trailing whitespace on an
|
|
41
|
+
* individual line is hashed, and changing it is reported as drift. That is the
|
|
42
|
+
* intended reading: nothing in a checkout rewrites it, so it got there because
|
|
43
|
+
* someone edited the line, and the cost of an unnecessary regeneration is lower
|
|
44
|
+
* than the cost of a missed one.
|
|
45
|
+
*
|
|
46
|
+
* Nothing else is normalized. Comments and blank lines are hashed as-is: a
|
|
47
|
+
* "harmless" edit is still an edit, and reporting one costs a regeneration
|
|
48
|
+
* nobody regrets, while normalizing one away risks silence on a real change
|
|
49
|
+
* whose only visible trace happened to be a comment.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Fingerprint the TEXT of a Prisma schema file. Stable across line-ending and
|
|
53
|
+
* BOM differences that a checkout can introduce (see the module doc), sensitive
|
|
54
|
+
* to every other byte.
|
|
55
|
+
*
|
|
56
|
+
* The returned string is opaque and versioned by its `v1:` prefix: if the
|
|
57
|
+
* normalization ever has to change, old maps carry the old prefix and can be
|
|
58
|
+
* recognized rather than reported as drift against a rule they were not
|
|
59
|
+
* generated under.
|
|
60
|
+
*/
|
|
61
|
+
export declare function fingerprintPrismaSchema(source: string): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm, fingerprint of the Prisma schema a `PRISMA_MAP` was generated
|
|
3
|
+
* from.
|
|
4
|
+
*
|
|
5
|
+
* ## The failure this exists for
|
|
6
|
+
*
|
|
7
|
+
* `turbine migrate-from-prisma` reads `prisma/schema.prisma` and emits a
|
|
8
|
+
* `prisma-map.ts` that the `turbine-orm/prisma-compat` adapter is driven by.
|
|
9
|
+
* Nothing re-runs it. Add a model, rename a field, change a `@@unique` name, and
|
|
10
|
+
* the map keeps describing the schema as it was: the adapter goes on translating
|
|
11
|
+
* the names it knows and simply has no entry for the new ones. That surfaces as
|
|
12
|
+
* "this model is not on the compat client" or a field silently absent from a
|
|
13
|
+
* result, which reads like an adapter bug rather than a stale artifact, and
|
|
14
|
+
* which is exactly the failure mode a code generator ought to be able to
|
|
15
|
+
* announce about itself.
|
|
16
|
+
*
|
|
17
|
+
* So the generator records what it read, and the adapter checks that record
|
|
18
|
+
* against what is on disk. This is the same shape as the unknown-config-key
|
|
19
|
+
* check in `client.ts` and the parser-overwrite warning in `query/utils.ts`,
|
|
20
|
+
* with a higher severity than either: those two produce visible breakage on
|
|
21
|
+
* their own, and this one produces none at all.
|
|
22
|
+
*
|
|
23
|
+
* ## Why FNV-1a and not a cryptographic digest
|
|
24
|
+
*
|
|
25
|
+
* The question asked is "is this the same file", not "did an adversary craft a
|
|
26
|
+
* collision". A collision costs one missed warning, so a 64-bit non-crypto hash
|
|
27
|
+
* is the right instrument, and it keeps this module pure: no `node:crypto`, no
|
|
28
|
+
* new dependency, and it is importable from the runtime adapter, from the CLI,
|
|
29
|
+
* and from an edge bundle alike.
|
|
30
|
+
*
|
|
31
|
+
* ## Normalization
|
|
32
|
+
*
|
|
33
|
+
* Deliberately minimal, and only for differences that are not edits:
|
|
34
|
+
*
|
|
35
|
+
* - a leading UTF-8 BOM, which some editors add and remove without being asked;
|
|
36
|
+
* - CRLF and lone CR line endings, so a Windows checkout of an unchanged file
|
|
37
|
+
* is not reported as drift (`core.autocrlf` rewrites on checkout);
|
|
38
|
+
* - whitespace at the very END of the file, for the same reason.
|
|
39
|
+
*
|
|
40
|
+
* Note the last one is end-of-FILE only, not per line. Trailing whitespace on an
|
|
41
|
+
* individual line is hashed, and changing it is reported as drift. That is the
|
|
42
|
+
* intended reading: nothing in a checkout rewrites it, so it got there because
|
|
43
|
+
* someone edited the line, and the cost of an unnecessary regeneration is lower
|
|
44
|
+
* than the cost of a missed one.
|
|
45
|
+
*
|
|
46
|
+
* Nothing else is normalized. Comments and blank lines are hashed as-is: a
|
|
47
|
+
* "harmless" edit is still an edit, and reporting one costs a regeneration
|
|
48
|
+
* nobody regrets, while normalizing one away risks silence on a real change
|
|
49
|
+
* whose only visible trace happened to be a comment.
|
|
50
|
+
*/
|
|
51
|
+
import { fnv1a64Hex } from './query/utils.js';
|
|
52
|
+
/**
|
|
53
|
+
* Fingerprint the TEXT of a Prisma schema file. Stable across line-ending and
|
|
54
|
+
* BOM differences that a checkout can introduce (see the module doc), sensitive
|
|
55
|
+
* to every other byte.
|
|
56
|
+
*
|
|
57
|
+
* The returned string is opaque and versioned by its `v1:` prefix: if the
|
|
58
|
+
* normalization ever has to change, old maps carry the old prefix and can be
|
|
59
|
+
* recognized rather than reported as drift against a rule they were not
|
|
60
|
+
* generated under.
|
|
61
|
+
*/
|
|
62
|
+
export function fingerprintPrismaSchema(source) {
|
|
63
|
+
const normalized = source
|
|
64
|
+
// Written as an escape, not the literal character: a bare BOM in source is
|
|
65
|
+
// invisible in a diff and some tools strip it from the file it lives in.
|
|
66
|
+
.replace(/^\uFEFF/, '')
|
|
67
|
+
.replace(/\r\n?/g, '\n')
|
|
68
|
+
.replace(/\s+$/, '');
|
|
69
|
+
return `v1:${fnv1a64Hex(normalized)}`;
|
|
70
|
+
}
|
|
@@ -104,6 +104,13 @@ export declare const WARN_NS: {
|
|
|
104
104
|
* a million executions of one call site is one.
|
|
105
105
|
*/
|
|
106
106
|
readonly unknownQueryOption: "unknownQueryOption";
|
|
107
|
+
/**
|
|
108
|
+
* The `prisma-map.ts` driving a `turbine-orm/prisma-compat` client no longer
|
|
109
|
+
* matches the Prisma schema it was generated from (prisma-compat.ts
|
|
110
|
+
* `warnStalePrismaMap`). Keyed on the schema path, so a process that builds a
|
|
111
|
+
* compat client per request reports it once.
|
|
112
|
+
*/
|
|
113
|
+
readonly stalePrismaMap: "stalePrismaMap";
|
|
107
114
|
/**
|
|
108
115
|
* `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
|
|
109
116
|
* runs no connection setup, so the option is a no-op (client.ts constructor).
|
|
@@ -139,6 +139,13 @@ export const WARN_NS = {
|
|
|
139
139
|
* a million executions of one call site is one.
|
|
140
140
|
*/
|
|
141
141
|
unknownQueryOption: 'unknownQueryOption',
|
|
142
|
+
/**
|
|
143
|
+
* The `prisma-map.ts` driving a `turbine-orm/prisma-compat` client no longer
|
|
144
|
+
* matches the Prisma schema it was generated from (prisma-compat.ts
|
|
145
|
+
* `warnStalePrismaMap`). Keyed on the schema path, so a process that builds a
|
|
146
|
+
* compat client per request reports it once.
|
|
147
|
+
*/
|
|
148
|
+
stalePrismaMap: 'stalePrismaMap',
|
|
142
149
|
/**
|
|
143
150
|
* `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
|
|
144
151
|
* runs no connection setup, so the option is a no-op (client.ts constructor).
|
package/dist/query/where.js
CHANGED
|
@@ -999,7 +999,7 @@ export function enumTypeForColumn(qi, column) {
|
|
|
999
999
|
const enums = qi.schema.enums;
|
|
1000
1000
|
if (!enums)
|
|
1001
1001
|
return null;
|
|
1002
|
-
// Cross-schema guard
|
|
1002
|
+
// Cross-schema guard: introspection records pgTypeSchema ONLY when
|
|
1003
1003
|
// the column's type lives OUTSIDE the introspected schema. A same-named
|
|
1004
1004
|
// enum in another schema must not get this schema's cast, search_path
|
|
1005
1005
|
// would resolve `::"status"` to the wrong type. Skipping the cast restores
|
package/dist/schema-metadata.js
CHANGED
|
@@ -249,7 +249,7 @@ export function schemaDefToMetadata(def) {
|
|
|
249
249
|
// local reimplementation had NO collision guard, `posts.user` (text) +
|
|
250
250
|
// `userId references users.id` produced a relation `user` that shadowed the
|
|
251
251
|
// scalar, and two FKs deriving the same name silently clobbered each other
|
|
252
|
-
|
|
252
|
+
//.
|
|
253
253
|
//
|
|
254
254
|
// Constraint names are synthesized in pg's default `<table>_<column>_fkey`
|
|
255
255
|
// form; they only feed the referential-action lookup and composite-FK
|
package/dist/schema.d.ts
CHANGED
|
@@ -325,6 +325,30 @@ export interface PrismaCompatMap {
|
|
|
325
325
|
models: Record<string, PrismaModelMap>;
|
|
326
326
|
/** Prisma enum name → resolved database enum-type name. */
|
|
327
327
|
enums: Record<string, string>;
|
|
328
|
+
/**
|
|
329
|
+
* The Prisma schema file this map was generated from. Present on maps emitted
|
|
330
|
+
* by `turbine migrate-from-prisma` since 0.60; absent on older ones, and on a
|
|
331
|
+
* map assembled by hand.
|
|
332
|
+
*
|
|
333
|
+
* `turbine-orm/prisma-compat` uses it for a once-per-process, dev-only staleness
|
|
334
|
+
* check: nothing re-runs the generator, so a map can silently fall behind the
|
|
335
|
+
* schema it describes and the adapter has no way to know. See
|
|
336
|
+
* {@link fingerprintPrismaSchema}.
|
|
337
|
+
*/
|
|
338
|
+
source?: PrismaSchemaSource;
|
|
339
|
+
}
|
|
340
|
+
/** Provenance of a generated {@link PrismaCompatMap}. */
|
|
341
|
+
export interface PrismaSchemaSource {
|
|
342
|
+
/**
|
|
343
|
+
* The Prisma schema file, POSIX-separated and relative to the directory the
|
|
344
|
+
* generator ran in (which for the documented workflow is the project root, so
|
|
345
|
+
* it resolves against `process.cwd()` at runtime). Relative rather than
|
|
346
|
+
* absolute so the value is identical for every developer and in CI, and so a
|
|
347
|
+
* generated file never carries someone's home directory into version control.
|
|
348
|
+
*/
|
|
349
|
+
path: string;
|
|
350
|
+
/** {@link fingerprintPrismaSchema} of that file's contents at generation time. */
|
|
351
|
+
hash: string;
|
|
328
352
|
}
|
|
329
353
|
/** One Prisma model's resolved mapping onto a Turbine table + client accessor. */
|
|
330
354
|
export interface PrismaModelMap {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.1",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
|