turbine-orm 0.81.0 → 0.90.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 +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/prisma-compat.js
CHANGED
|
@@ -67,16 +67,22 @@
|
|
|
67
67
|
* - Fluent relation chaining (`prisma.user.findUnique().posts()`).
|
|
68
68
|
* - Accelerate / Pulse / driver-adapter preview features, the Mongo API, and the
|
|
69
69
|
* `prisma migrate`/`db` CLI family (Turbine ships its own migrations).
|
|
70
|
-
* - **
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* `
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* - **
|
|
79
|
-
*
|
|
70
|
+
* - **Negative `take`** (take-from-end) throws: Turbine has no reverse-take.
|
|
71
|
+
* A **`cursor` on a nested relation include** throws: `with` has no cursor
|
|
72
|
+
* (`take` / `skip` there page each parent's rows, as in Prisma).
|
|
73
|
+
* - **A filtered relation count** (`_count: { select: { posts: { where } } }`)
|
|
74
|
+
* and **`distinct` on a nested include** throw `UnsupportedFeatureError`:
|
|
75
|
+
* core counts and includes a relation whole, so honoring either would mean
|
|
76
|
+
* returning a wrong number or duplicate rows. The message names the
|
|
77
|
+
* alternative (a `count` / `findMany` on the related model).
|
|
78
|
+
* - **Nothing is accepted and dropped.** Every option on an included relation
|
|
79
|
+
* and every key under `_count` is translated, refused by name (the two
|
|
80
|
+
* above, `cursor`), or refused as unknown (`ValidationError`, with a
|
|
81
|
+
* suggestion): a misspelled `wher`, a Turbine spelling like `limit`, a
|
|
82
|
+
* relation `_count` does not know. The same holds for `$transaction` options
|
|
83
|
+
* (`maxWait` is accepted, warned about once, and not applied: the pool's
|
|
84
|
+
* `connectionTimeoutMs` bounds the wait) and for the keys of a nested write's
|
|
85
|
+
* `{ where, data }` / `{ where, create, update }` payloads.
|
|
80
86
|
* - **`limit` on `updateMany` / `deleteMany`** (Prisma 6.7+) throws. Turbine has
|
|
81
87
|
* no row-bounded mass mutation, and dropping a SAFETY BOUND with a warning
|
|
82
88
|
* would turn "change at most 10 rows" into "change every matching row".
|
|
@@ -87,9 +93,10 @@
|
|
|
87
93
|
* either (or any `include` on a write) THROWS, since a write returns its own
|
|
88
94
|
* row only. `omit` on READS is translated to core's own `omit`, see
|
|
89
95
|
* {@link translateOmit}.
|
|
90
|
-
* - **`
|
|
91
|
-
*
|
|
92
|
-
*
|
|
96
|
+
* - **`take` / `skip` / `cursor` on `count` and `aggregate`** throw: Prisma
|
|
97
|
+
* computes those over one page of rows and Turbine has no paged count. An
|
|
98
|
+
* `orderBy` alone changes nothing there and is accepted. `count({ select })`
|
|
99
|
+
* returns Prisma's per-field object (`{ _all, field }`).
|
|
93
100
|
*
|
|
94
101
|
* ## Type dependencies (0.41.0)
|
|
95
102
|
*
|
|
@@ -119,11 +126,15 @@
|
|
|
119
126
|
* });
|
|
120
127
|
* ```
|
|
121
128
|
*/
|
|
129
|
+
import { resolveDiagnostics } from './diagnostics.js';
|
|
122
130
|
import { TurbineError, TurbineErrorCode, UnsupportedFeatureError, ValidationError, wrapPgError } from './errors.js';
|
|
131
|
+
import { CLIENT_LOG_CONTEXT, consoleLogger, logTo } from './logger.js';
|
|
123
132
|
import { createManyShapeRuns } from './nested-write.js';
|
|
124
133
|
import { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
134
|
+
import { isPlainRecord, RELATION_FILTER_WRAPPER_SET, setOwn } from './query/filters.js';
|
|
125
135
|
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, UNSAFE, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './query/index.js';
|
|
126
|
-
import {
|
|
136
|
+
import { CURSOR_INCLUSIVE } from './query/normalize-args.js';
|
|
137
|
+
import { ownLookup, suggestKey } from './query/utils.js';
|
|
127
138
|
import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
|
|
128
139
|
import { assertWhereDepth, MAX_WHERE_DEPTH } from './query/where-compile.js';
|
|
129
140
|
// ---------------------------------------------------------------------------
|
|
@@ -233,19 +244,21 @@ function timeStringToDate(v) {
|
|
|
233
244
|
return new Date(Date.UTC(1970, 0, 1, Number(m[1]), Number(m[2]), Number(m[3]), ms));
|
|
234
245
|
}
|
|
235
246
|
function buildLookups(ctx, mm) {
|
|
236
|
-
|
|
247
|
+
// Keyed by field and relation names, so prototype-less: a field called
|
|
248
|
+
// `constructor` is an own key here, never Object.prototype's member.
|
|
249
|
+
const reverseFields = Object.create(null);
|
|
237
250
|
let identityFields = true;
|
|
238
251
|
for (const [prismaField, turbineField] of Object.entries(mm.fields)) {
|
|
239
252
|
reverseFields[turbineField] = prismaField;
|
|
240
253
|
if (prismaField !== turbineField)
|
|
241
254
|
identityFields = false;
|
|
242
255
|
}
|
|
243
|
-
const reverseRelations =
|
|
256
|
+
const reverseRelations = Object.create(null);
|
|
244
257
|
for (const [prismaRel, rel] of Object.entries(mm.relations)) {
|
|
245
258
|
reverseRelations[rel.name] = { prismaName: prismaRel, cardinality: rel.cardinality };
|
|
246
259
|
}
|
|
247
260
|
const timeFields = new Set();
|
|
248
|
-
for (const col of ctx.schema.tables
|
|
261
|
+
for (const col of ownLookup(ctx.schema.tables, mm.table)?.columns ?? []) {
|
|
249
262
|
if (TIME_PG_TYPES.has(col.pgType))
|
|
250
263
|
timeFields.add(col.field);
|
|
251
264
|
}
|
|
@@ -260,62 +273,18 @@ function lookupsFor(ctx, mm) {
|
|
|
260
273
|
return l;
|
|
261
274
|
}
|
|
262
275
|
/** Resolve a turbine relation's target Prisma model map (for nested translation). */
|
|
263
|
-
/**
|
|
264
|
-
* A `where` fragment no row satisfies, for Prisma's empty `OR`.
|
|
265
|
-
*
|
|
266
|
-
* `{ <key>: { in: [] } }`, which core compiles to `= ANY($n)` with an empty
|
|
267
|
-
* array: false for every row, NULL-valued ones included, and one ordinary bound
|
|
268
|
-
* param rather than a literal spliced into the SQL. The column is the first
|
|
269
|
-
* primary-key column, else the table's first column, so it is always a real
|
|
270
|
-
* column of this table and the predicate stays a plain scalar comparison the
|
|
271
|
-
* cache fingerprints like any other.
|
|
272
|
-
*
|
|
273
|
-
* A table this layer cannot name a column of THROWS. The fallback used to be
|
|
274
|
-
* `{}`, an empty `where` fragment - which is not "no rows" but its exact
|
|
275
|
-
* opposite, EVERY row, and it would have been merged in silently beside the
|
|
276
|
-
* caller's other predicates. A sentinel builder that cannot build a sentinel
|
|
277
|
-
* has not succeeded, and the one shape it exists to compile is the one where
|
|
278
|
-
* answering "all of them" instead of "none of them" is worst.
|
|
279
|
-
*/
|
|
280
|
-
function matchNothing(ctx, mm) {
|
|
281
|
-
const meta = ctx.schema.tables[mm.table];
|
|
282
|
-
const column = meta?.primaryKey?.[0] ?? meta?.allColumns?.[0];
|
|
283
|
-
if (!meta || !column) {
|
|
284
|
-
throw new ValidationError(`An empty \`OR\` on model ${mm.table} means "no rows", and compiling that needs one real column ` +
|
|
285
|
-
`of the table, but the schema metadata for "${mm.table}" lists none. Regenerate the client ` +
|
|
286
|
-
'(`npx turbine generate`) so the table carries its columns, or drop the empty `OR`.');
|
|
287
|
-
}
|
|
288
|
-
return { [meta.reverseColumnMap?.[column] ?? column]: { in: [] } };
|
|
289
|
-
}
|
|
290
276
|
function relTargetModel(ctx, mm, turbineRel) {
|
|
291
|
-
const
|
|
277
|
+
const relations = ownLookup(ctx.schema.tables, mm.table)?.relations;
|
|
278
|
+
const rd = relations ? ownLookup(relations, turbineRel) : undefined;
|
|
292
279
|
if (!rd)
|
|
293
280
|
return undefined;
|
|
294
281
|
const modelName = ctx.tableToModel.get(rd.to);
|
|
295
|
-
return modelName ? ctx.map.models
|
|
282
|
+
return modelName ? ownLookup(ctx.map.models, modelName) : undefined;
|
|
296
283
|
}
|
|
297
284
|
// ---------------------------------------------------------------------------
|
|
298
285
|
// Argument translation
|
|
299
286
|
// ---------------------------------------------------------------------------
|
|
300
287
|
const COMBINATORS = new Set(['AND', 'OR', 'NOT']);
|
|
301
|
-
/**
|
|
302
|
-
* The to-many half of the relation-filter wrapper list.
|
|
303
|
-
*
|
|
304
|
-
* DRIFT RISK, and this is the third copy of that list in the tree. The canonical
|
|
305
|
-
* NAMED one is `RELATION_FILTER_WRAPPERS` in `cli/pii-predicate-guard.ts`
|
|
306
|
-
* (`some`/`none`/`every`/`is`/`isNot`), and a second is inlined in
|
|
307
|
-
* `query/where-compile.ts` as `'some' in x || 'every' in x || …`. This copy is
|
|
308
|
-
* the worst of the three because it is SPLIT: the to-many quantifiers are here,
|
|
309
|
-
* and `is` / `isNot` are two inline `k === …` tests in
|
|
310
|
-
* {@link translateRelationFilter}, so half of it does not read as a list and a
|
|
311
|
-
* grep for the list will not find it. A wrapper the compiler learns and this set
|
|
312
|
-
* does not is a relation body that reaches core untranslated, i.e. with Prisma
|
|
313
|
-
* field names still in it.
|
|
314
|
-
*
|
|
315
|
-
* The fix is an export from `query/filters.ts` that all three import; until that
|
|
316
|
-
* exists, these comments are what hold them in step.
|
|
317
|
-
*/
|
|
318
|
-
const RELATION_QUANTIFIERS = new Set(['some', 'every', 'none']);
|
|
319
288
|
/**
|
|
320
289
|
* Refuse a translation walk that has nested past {@link MAX_WHERE_DEPTH}.
|
|
321
290
|
*
|
|
@@ -355,16 +324,8 @@ function assertTranslateDepth(depth, clause) {
|
|
|
355
324
|
`query needs, and an unbounded walk over caller-supplied nesting is a stack-overflow surface, so it is ` +
|
|
356
325
|
`refused. If this is a generated argument, flatten it: a single array of N entries is one level, not N.`);
|
|
357
326
|
}
|
|
358
|
-
/** Whether a value is a plain object usable as a compound-unique selector. */
|
|
359
|
-
function isPlainObject(v) {
|
|
360
|
-
return (typeof v === 'object' &&
|
|
361
|
-
v !== null &&
|
|
362
|
-
!Array.isArray(v) &&
|
|
363
|
-
!(v instanceof Date) &&
|
|
364
|
-
!(typeof Buffer !== 'undefined' && Buffer.isBuffer(v)));
|
|
365
|
-
}
|
|
366
327
|
function renameField(mm, prismaField) {
|
|
367
|
-
return mm.fields
|
|
328
|
+
return ownLookup(mm.fields, prismaField) ?? prismaField;
|
|
368
329
|
}
|
|
369
330
|
/** The turbine arg surface each operation compiles into. */
|
|
370
331
|
const OPERATION_TABLES = {
|
|
@@ -484,8 +445,8 @@ const KNOWN_KEYS = (() => {
|
|
|
484
445
|
return out;
|
|
485
446
|
})();
|
|
486
447
|
/**
|
|
487
|
-
*
|
|
488
|
-
* no meaning for.
|
|
448
|
+
* Per-query advice for a key on a delegate's args object that this operation
|
|
449
|
+
* has no meaning for.
|
|
489
450
|
*
|
|
490
451
|
* An unrecognized key is silently ignored (JavaScript objects have no schema),
|
|
491
452
|
* which makes a typo, a turbine spelling, and a genuinely missing feature all
|
|
@@ -497,13 +458,14 @@ const KNOWN_KEYS = (() => {
|
|
|
497
458
|
* into a failing one on upgrade over one stray key, and a key from a Prisma
|
|
498
459
|
* major newer than {@link PRISMA_ARG_KEYS} would be exactly that key. The whole
|
|
499
460
|
* body is wrapped so a hostile or exotic args object (a Proxy whose `ownKeys`
|
|
500
|
-
* throws) can never be the reason a query fails.
|
|
501
|
-
* `
|
|
461
|
+
* throws) can never be the reason a query fails. Off when the client's
|
|
462
|
+
* `diagnostics` turn advice off, and once per `model.operation.key` per
|
|
463
|
+
* process, like every other advisory here.
|
|
502
464
|
*/
|
|
503
|
-
function warnUnknownQueryOptions(model, op, args) {
|
|
504
|
-
if (
|
|
465
|
+
function warnUnknownQueryOptions(log, model, op, args) {
|
|
466
|
+
if (!log.diagnostics.advice)
|
|
505
467
|
return;
|
|
506
|
-
if (!
|
|
468
|
+
if (!isPlainRecord(args))
|
|
507
469
|
return;
|
|
508
470
|
try {
|
|
509
471
|
const known = KNOWN_KEYS[op];
|
|
@@ -519,13 +481,19 @@ function warnUnknownQueryOptions(model, op, args) {
|
|
|
519
481
|
if (!shouldWarnOnce(WARN_NS.unknownQueryOption, `${model}.${op}.${key}`))
|
|
520
482
|
continue;
|
|
521
483
|
if (alias) {
|
|
522
|
-
|
|
523
|
-
` prisma-compat takes Prisma's "${alias}". (${model}.${op})
|
|
484
|
+
logTo(log.logger, 'warn', `prisma-compat: "${key}" is Turbine's spelling and is ignored here;` +
|
|
485
|
+
` prisma-compat takes Prisma's "${alias}". (${model}.${op})`, { code: 'prisma-compat-native-spelling', table: model, operation: op, key, prismaKey: alias });
|
|
524
486
|
continue;
|
|
525
487
|
}
|
|
526
488
|
const suggestion = suggestKey(key, known);
|
|
527
|
-
|
|
528
|
-
(suggestion ? ` Did you mean "${suggestion}"?` : '')
|
|
489
|
+
logTo(log.logger, 'warn', `prisma-compat: unknown option "${key}" in ${model}.${op}(), it is ignored.` +
|
|
490
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : ''), {
|
|
491
|
+
code: 'prisma-compat-unknown-option',
|
|
492
|
+
table: model,
|
|
493
|
+
operation: op,
|
|
494
|
+
key,
|
|
495
|
+
...(suggestion ? { suggestion } : {}),
|
|
496
|
+
});
|
|
529
497
|
}
|
|
530
498
|
}
|
|
531
499
|
catch {
|
|
@@ -534,8 +502,8 @@ function warnUnknownQueryOptions(model, op, args) {
|
|
|
534
502
|
}
|
|
535
503
|
}
|
|
536
504
|
/**
|
|
537
|
-
*
|
|
538
|
-
*
|
|
505
|
+
* Notice that the `prisma-map.ts` driving this client no longer matches the
|
|
506
|
+
* Prisma schema it was generated from.
|
|
539
507
|
*
|
|
540
508
|
* Nothing re-runs `turbine migrate-from-prisma`. When the schema moves on, the
|
|
541
509
|
* map does not, and the adapter keeps translating the names it has: a model
|
|
@@ -550,19 +518,26 @@ function warnUnknownQueryOptions(model, op, args) {
|
|
|
550
518
|
* - **Any failure is silence.** A missing file is the NORMAL deployed state
|
|
551
519
|
* (nobody ships `prisma/` to production) and so are a bundled runtime with no
|
|
552
520
|
* `node:fs` and a read that is refused. None of them is evidence of drift.
|
|
553
|
-
* - **
|
|
554
|
-
*
|
|
555
|
-
* builds a client per request
|
|
521
|
+
* - **Always on**, like every construction warning: a stale map loses models
|
|
522
|
+
* in production too. In production mode the file is READ once per process
|
|
523
|
+
* per path, so a process that builds a client per request does not read it
|
|
524
|
+
* per request; in development it is re-read per client, so a schema edited
|
|
525
|
+
* under a running dev server is still noticed.
|
|
526
|
+
* - **Warned once per process per path**, via the shared registry.
|
|
556
527
|
*
|
|
557
528
|
* @param map - the map under test; a map with no `source` (hand-written, or
|
|
558
529
|
* emitted before 0.60) is skipped, since absence carries no information.
|
|
559
530
|
*/
|
|
560
|
-
function warnStalePrismaMap(map) {
|
|
531
|
+
function warnStalePrismaMap(map, log) {
|
|
561
532
|
const source = map.source;
|
|
562
533
|
if (!source || typeof source.path !== 'string' || typeof source.hash !== 'string')
|
|
563
534
|
return;
|
|
564
|
-
|
|
535
|
+
// No `process`, no `node:fs`: an edge runtime has no file to compare.
|
|
536
|
+
if (typeof process === 'undefined')
|
|
565
537
|
return;
|
|
538
|
+
if (log.diagnostics.mode === 'production' && !shouldWarnOnce(WARN_NS.stalePrismaMap, `checked:${source.path}`)) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
566
541
|
void (async () => {
|
|
567
542
|
try {
|
|
568
543
|
// Dynamic so a bundle that never reaches this line never needs `node:fs`.
|
|
@@ -572,9 +547,9 @@ function warnStalePrismaMap(map) {
|
|
|
572
547
|
return;
|
|
573
548
|
if (!shouldWarnOnce(WARN_NS.stalePrismaMap, source.path))
|
|
574
549
|
return;
|
|
575
|
-
|
|
550
|
+
logTo(log.logger, 'warn', `prisma-compat: ${source.path} has changed since prisma-map.ts was generated,` +
|
|
576
551
|
' so any model, field, relation or compound-unique added or renamed since then is missing' +
|
|
577
|
-
' from the compat client. Re-run: turbine migrate-from-prisma');
|
|
552
|
+
' from the compat client. Re-run: turbine migrate-from-prisma', { code: 'prisma-map-stale', path: source.path });
|
|
578
553
|
}
|
|
579
554
|
catch {
|
|
580
555
|
// No file, no `node:fs`, no permission. None of these means the map is
|
|
@@ -590,7 +565,7 @@ function warnStalePrismaMap(map) {
|
|
|
590
565
|
* bound turns "change at most 10 rows" into "change every row".
|
|
591
566
|
*/
|
|
592
567
|
function refuseRowLimit(model, op, args) {
|
|
593
|
-
if (!
|
|
568
|
+
if (!isPlainRecord(args) || args.limit === undefined)
|
|
594
569
|
return;
|
|
595
570
|
throw new UnsupportedFeatureError(`\`limit\` on ${op} (row-bounded mass mutation)`, 'prisma-compat', `Turbine has no row-bounded ${op}; select the rows you mean with \`where\`` +
|
|
596
571
|
` (on ${model}.${op}), or page them and mutate by primary key.`);
|
|
@@ -624,33 +599,11 @@ function mapRelationLoadStrategy(value) {
|
|
|
624
599
|
*/
|
|
625
600
|
function translateWhere(ctx, mm, where, depth = 0) {
|
|
626
601
|
assertTranslateDepth(depth, 'where');
|
|
627
|
-
if (!
|
|
602
|
+
if (!isPlainRecord(where))
|
|
628
603
|
return where;
|
|
629
604
|
const out = {};
|
|
630
|
-
// The empty-`OR` sentinel, held aside rather than merged into `out`. At most
|
|
631
|
-
// one per level, since `OR` is a single object key.
|
|
632
|
-
let matchNone;
|
|
633
605
|
for (const [key, val] of Object.entries(where)) {
|
|
634
606
|
if (COMBINATORS.has(key)) {
|
|
635
|
-
// An EMPTY `OR` is false in Prisma (no branch can match), while an empty
|
|
636
|
-
// `AND` / `NOT` is true. Core drops an empty combinator entirely, so the
|
|
637
|
-
// same call returned EVERY row here: the classic trigger is
|
|
638
|
-
// `OR: ids.map(...)` over a selection that turned out to be empty, i.e.
|
|
639
|
-
// "none of these" answered with "all of them". Compiled to a predicate
|
|
640
|
-
// that matches nothing so this layer keeps Prisma's meaning; core's own
|
|
641
|
-
// semantics are unchanged and documented separately.
|
|
642
|
-
//
|
|
643
|
-
// Held aside, NOT `Object.assign`ed onto `out`: the sentinel is keyed on
|
|
644
|
-
// a real column of the table, so merging it made two predicates on one
|
|
645
|
-
// key collide and JavaScript's key ORDER decided which survived.
|
|
646
|
-
// `{ OR: [], id: 5 }` kept the caller's `id = 5` and dropped the
|
|
647
|
-
// sentinel (returning the row Prisma excludes); `{ id: 5, OR: [] }` kept
|
|
648
|
-
// the sentinel and dropped the caller's `id = 5`. Same query, same
|
|
649
|
-
// meaning, opposite predicate.
|
|
650
|
-
if (key === 'OR' && Array.isArray(val) && val.length === 0) {
|
|
651
|
-
matchNone = matchNothing(ctx, mm);
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
607
|
// An `AND` / `OR` array of N conditions is ONE level, not N: the elements
|
|
655
608
|
// are siblings, so they all translate at the same incremented depth.
|
|
656
609
|
out[key] = Array.isArray(val)
|
|
@@ -659,18 +612,18 @@ function translateWhere(ctx, mm, where, depth = 0) {
|
|
|
659
612
|
continue;
|
|
660
613
|
}
|
|
661
614
|
// Compound-unique selector (custom or default Prisma name).
|
|
662
|
-
const compound = mm.compoundUniques
|
|
663
|
-
if (compound &&
|
|
615
|
+
const compound = ownLookup(mm.compoundUniques, key);
|
|
616
|
+
if (compound && isPlainRecord(val) && !ownLookup(mm.fields, key) && !ownLookup(mm.relations, key)) {
|
|
664
617
|
const inner = {};
|
|
665
618
|
for (const [pk, pv] of Object.entries(val))
|
|
666
|
-
inner
|
|
619
|
+
setOwn(inner, renameField(mm, pk), pv);
|
|
667
620
|
// Turbine's core derives the selector name from the underscore-join of the
|
|
668
621
|
// member FIELD names, so re-key custom names onto that canonical form.
|
|
669
|
-
out
|
|
622
|
+
setOwn(out, compound.join('_'), inner);
|
|
670
623
|
continue;
|
|
671
624
|
}
|
|
672
625
|
// Relation filter.
|
|
673
|
-
const rel = mm.relations
|
|
626
|
+
const rel = ownLookup(mm.relations, key);
|
|
674
627
|
if (rel) {
|
|
675
628
|
// Prisma spells "this to-one relation has no related row" as a bare
|
|
676
629
|
// `null` on the relation key; core spells it `{ is: null }` and answers a
|
|
@@ -680,44 +633,28 @@ function translateWhere(ctx, mm, where, depth = 0) {
|
|
|
680
633
|
// has no bare-null shape there, so core still refuses them by name rather
|
|
681
634
|
// than this layer inventing a meaning.
|
|
682
635
|
if (val === null && rel.cardinality === 'one') {
|
|
683
|
-
out
|
|
636
|
+
setOwn(out, rel.name, { is: null });
|
|
684
637
|
continue;
|
|
685
638
|
}
|
|
686
639
|
const target = relTargetModel(ctx, mm, rel.name);
|
|
687
|
-
out
|
|
640
|
+
setOwn(out, rel.name, translateRelationFilter(ctx, target, val, depth + 1));
|
|
688
641
|
continue;
|
|
689
642
|
}
|
|
690
643
|
// Scalar field, key renamed, value (literal or operator object) passes
|
|
691
644
|
// through unchanged (Prisma operator names match Turbine's).
|
|
692
|
-
out
|
|
645
|
+
setOwn(out, renameField(mm, key), val);
|
|
693
646
|
}
|
|
694
|
-
|
|
695
|
-
return out;
|
|
696
|
-
// Nothing else at this level: the sentinel IS the where, no wrapper needed.
|
|
697
|
-
if (Object.keys(out).length === 0)
|
|
698
|
-
return matchNone;
|
|
699
|
-
// Otherwise conjoin, so neither half can overwrite the other whatever order
|
|
700
|
-
// the caller wrote the keys in. BRANDED, because an `AND` array is the shape
|
|
701
|
-
// that makes a statement unnamed (a caller-written combinator has a
|
|
702
|
-
// caller-sized arity); this one is Turbine's, with a fixed arity of two, and
|
|
703
|
-
// an unbranded wrapper here would quietly take every compat query carrying an
|
|
704
|
-
// empty `OR` off named prepared statements. The caller's own combinators sit
|
|
705
|
-
// INSIDE `out` and are walked at the next level with their own brand check,
|
|
706
|
-
// so they still count.
|
|
707
|
-
return markInternalCombinator({ AND: [out, matchNone] });
|
|
647
|
+
return out;
|
|
708
648
|
}
|
|
709
649
|
function translateRelationFilter(ctx, target, val, depth) {
|
|
710
|
-
if (!
|
|
650
|
+
if (!isPlainRecord(val))
|
|
711
651
|
return val;
|
|
712
652
|
const keys = Object.keys(val);
|
|
713
|
-
const hasQuantifier = keys.some((k) =>
|
|
653
|
+
const hasQuantifier = keys.some((k) => RELATION_FILTER_WRAPPER_SET.has(k));
|
|
714
654
|
if (hasQuantifier) {
|
|
715
655
|
const out = {};
|
|
716
656
|
for (const [k, v] of Object.entries(val)) {
|
|
717
|
-
out
|
|
718
|
-
target && (RELATION_QUANTIFIERS.has(k) || k === 'is' || k === 'isNot')
|
|
719
|
-
? translateWhere(ctx, target, v, depth + 1)
|
|
720
|
-
: v;
|
|
657
|
+
setOwn(out, k, target && RELATION_FILTER_WRAPPER_SET.has(k) ? translateWhere(ctx, target, v, depth + 1) : v);
|
|
721
658
|
}
|
|
722
659
|
return out;
|
|
723
660
|
}
|
|
@@ -741,7 +678,7 @@ function translateOrderBy(ctx, mm, ob, depth = 0) {
|
|
|
741
678
|
assertTranslateDepth(depth, 'orderBy');
|
|
742
679
|
if (Array.isArray(ob))
|
|
743
680
|
return ob.map((o) => translateOrderBy(ctx, mm, o, depth + 1));
|
|
744
|
-
if (!
|
|
681
|
+
if (!isPlainRecord(ob))
|
|
745
682
|
return ob;
|
|
746
683
|
const out = {};
|
|
747
684
|
for (const [key, val] of Object.entries(ob)) {
|
|
@@ -749,20 +686,23 @@ function translateOrderBy(ctx, mm, ob, depth = 0) {
|
|
|
749
686
|
out._count = val;
|
|
750
687
|
continue;
|
|
751
688
|
}
|
|
752
|
-
const rel = mm.relations
|
|
689
|
+
const rel = ownLookup(mm.relations, key);
|
|
753
690
|
if (rel) {
|
|
754
691
|
const target = relTargetModel(ctx, mm, rel.name);
|
|
755
692
|
out[rel.name] =
|
|
756
|
-
|
|
693
|
+
isPlainRecord(val) && !('_count' in val) && target ? translateOrderBy(ctx, target, val, depth + 1) : val;
|
|
757
694
|
continue;
|
|
758
695
|
}
|
|
759
|
-
out
|
|
696
|
+
setOwn(out, renameField(mm, key), val);
|
|
760
697
|
}
|
|
761
698
|
return out;
|
|
762
699
|
}
|
|
763
|
-
/**
|
|
700
|
+
/**
|
|
701
|
+
* Map a Prisma `take` to a Turbine `limit`. Negative take is unsupported. Any
|
|
702
|
+
* other value goes to core as written, which refuses a non-integer.
|
|
703
|
+
*/
|
|
764
704
|
function mapTake(take) {
|
|
765
|
-
if (take < 0) {
|
|
705
|
+
if (typeof take === 'number' && take < 0) {
|
|
766
706
|
throw new UnsupportedFeatureError('negative take (take-from-end pagination)', 'prisma-compat', 'Turbine has no reverse-take; reverse your orderBy and use a positive take instead.');
|
|
767
707
|
}
|
|
768
708
|
return take;
|
|
@@ -787,11 +727,11 @@ function translateOmit(ctx, mm, omit) {
|
|
|
787
727
|
for (const [key, val] of Object.entries(omit)) {
|
|
788
728
|
if (val === false || val == null)
|
|
789
729
|
continue;
|
|
790
|
-
if (mm.relations
|
|
730
|
+
if (ownLookup(mm.relations, key)) {
|
|
791
731
|
throw new ValidationError(`prisma-compat: \`omit\` on model "${modelName(ctx, mm)}" takes scalar fields, but "${key}" ` +
|
|
792
732
|
'is a relation. Leave the relation out of `include` instead.');
|
|
793
733
|
}
|
|
794
|
-
out
|
|
734
|
+
setOwn(out, renameField(mm, key), true);
|
|
795
735
|
any = true;
|
|
796
736
|
}
|
|
797
737
|
return any ? out : undefined;
|
|
@@ -828,15 +768,15 @@ function translateProjection(ctx, mm, args, depth = 0) {
|
|
|
828
768
|
if (val === false || val == null)
|
|
829
769
|
continue;
|
|
830
770
|
if (key === '_count') {
|
|
831
|
-
withClause._count = translateCountSelect(mm, val);
|
|
771
|
+
withClause._count = translateCountSelect(ctx, mm, val);
|
|
832
772
|
hasWith = true;
|
|
833
773
|
continue;
|
|
834
774
|
}
|
|
835
|
-
const rel = mm.relations
|
|
775
|
+
const rel = ownLookup(mm.relations, key);
|
|
836
776
|
if (!rel) {
|
|
837
777
|
throw new ValidationError(`prisma-compat: unknown relation "${key}" in include on model "${modelName(ctx, mm)}".`);
|
|
838
778
|
}
|
|
839
|
-
withClause
|
|
779
|
+
setOwn(withClause, rel.name, translateWithOption(ctx, mm, rel.name, val, depth + 1));
|
|
840
780
|
hasWith = true;
|
|
841
781
|
}
|
|
842
782
|
return { omit, with: hasWith ? withClause : undefined };
|
|
@@ -848,17 +788,17 @@ function translateProjection(ctx, mm, args, depth = 0) {
|
|
|
848
788
|
if (val === false || val == null)
|
|
849
789
|
continue;
|
|
850
790
|
if (key === '_count') {
|
|
851
|
-
withClause._count = translateCountSelect(mm, val);
|
|
791
|
+
withClause._count = translateCountSelect(ctx, mm, val);
|
|
852
792
|
hasWith = true;
|
|
853
793
|
continue;
|
|
854
794
|
}
|
|
855
|
-
const rel = mm.relations
|
|
795
|
+
const rel = ownLookup(mm.relations, key);
|
|
856
796
|
if (rel) {
|
|
857
|
-
withClause
|
|
797
|
+
setOwn(withClause, rel.name, translateWithOption(ctx, mm, rel.name, val, depth + 1));
|
|
858
798
|
hasWith = true;
|
|
859
799
|
continue;
|
|
860
800
|
}
|
|
861
|
-
scalar
|
|
801
|
+
setOwn(scalar, renameField(mm, key), true);
|
|
862
802
|
hasScalar = true;
|
|
863
803
|
}
|
|
864
804
|
return { select: hasScalar ? scalar : undefined, with: hasWith ? withClause : undefined };
|
|
@@ -940,7 +880,7 @@ function resolveWriteProjection(ctx, mm, op, args) {
|
|
|
940
880
|
for (const [key, val] of Object.entries(select)) {
|
|
941
881
|
if (val === false || val == null)
|
|
942
882
|
continue;
|
|
943
|
-
if (key === '_count' || mm.relations
|
|
883
|
+
if (key === '_count' || ownLookup(mm.relations, key)) {
|
|
944
884
|
throw new UnsupportedFeatureError(`select of "${key}" on ${op}`, 'prisma-compat', `"${key}" is a relation on model "${modelName(ctx, mm)}"; a write returns its own row only, so read ` +
|
|
945
885
|
'it back with a separate findUnique inside the same $transaction.');
|
|
946
886
|
}
|
|
@@ -957,7 +897,7 @@ function resolveWriteProjection(ctx, mm, op, args) {
|
|
|
957
897
|
for (const [key, val] of Object.entries(omitArg)) {
|
|
958
898
|
if (val === false || val == null)
|
|
959
899
|
continue;
|
|
960
|
-
if (mm.relations
|
|
900
|
+
if (ownLookup(mm.relations, key)) {
|
|
961
901
|
throw new ValidationError(`prisma-compat: \`omit\` on model "${modelName(ctx, mm)}" takes scalar fields, but "${key}" ` +
|
|
962
902
|
'is a relation.');
|
|
963
903
|
}
|
|
@@ -986,13 +926,13 @@ function coreWriteProjection(mm, proj) {
|
|
|
986
926
|
}
|
|
987
927
|
/** Apply a {@link WriteProjection} to one already-reshaped write result row. */
|
|
988
928
|
function applyWriteProjection(proj, row) {
|
|
989
|
-
if (!
|
|
929
|
+
if (!isPlainRecord(row))
|
|
990
930
|
return row;
|
|
991
931
|
if (proj.pick) {
|
|
992
932
|
const out = {};
|
|
993
933
|
for (const key of proj.pick) {
|
|
994
|
-
if (key
|
|
995
|
-
out
|
|
934
|
+
if (Object.hasOwn(row, key))
|
|
935
|
+
setOwn(out, key, row[key]);
|
|
996
936
|
}
|
|
997
937
|
return out;
|
|
998
938
|
}
|
|
@@ -1004,9 +944,65 @@ function applyWriteProjection(proj, row) {
|
|
|
1004
944
|
}
|
|
1005
945
|
return row;
|
|
1006
946
|
}
|
|
947
|
+
/** What a caller wrote, named for a message: `an array`, `null`, `a string`. */
|
|
948
|
+
function describeValue(value) {
|
|
949
|
+
if (value === undefined)
|
|
950
|
+
return 'undefined';
|
|
951
|
+
if (value === null)
|
|
952
|
+
return 'null';
|
|
953
|
+
if (Array.isArray(value))
|
|
954
|
+
return 'an array';
|
|
955
|
+
return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
|
|
956
|
+
}
|
|
957
|
+
/** The Prisma spelling of a turbine relation name, for a message. */
|
|
958
|
+
function prismaRelationName(ctx, mm, turbineRel) {
|
|
959
|
+
return ownLookup(lookupsFor(ctx, mm).reverseRelations, turbineRel)?.prismaName ?? turbineRel;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* The options Prisma takes on an included relation. `cursor` and `distinct` are
|
|
963
|
+
* here because they are Prisma's, and are refused by name (E017) rather than
|
|
964
|
+
* called unknown: Turbine's `with` clause cannot honor either.
|
|
965
|
+
*/
|
|
966
|
+
const NESTED_RELATION_KEYS = new Set([
|
|
967
|
+
'where',
|
|
968
|
+
'orderBy',
|
|
969
|
+
'take',
|
|
970
|
+
'skip',
|
|
971
|
+
'cursor',
|
|
972
|
+
'distinct',
|
|
973
|
+
'select',
|
|
974
|
+
'include',
|
|
975
|
+
'omit',
|
|
976
|
+
]);
|
|
977
|
+
/**
|
|
978
|
+
* Refuse a key on an included relation's options that this layer does not
|
|
979
|
+
* translate. Every key it does not read used to be dropped, so a misspelled
|
|
980
|
+
* `wher` or a Turbine spelling (`limit`) returned unfiltered, unlimited rows
|
|
981
|
+
* with no signal. An `undefined` value is absent, as everywhere else.
|
|
982
|
+
*/
|
|
983
|
+
function assertNestedRelationKeys(label, opts) {
|
|
984
|
+
for (const key of Object.keys(opts)) {
|
|
985
|
+
if (opts[key] === undefined || NESTED_RELATION_KEYS.has(key))
|
|
986
|
+
continue;
|
|
987
|
+
const alias = ALIAS_HINT[key];
|
|
988
|
+
if (alias) {
|
|
989
|
+
throw new ValidationError(`prisma-compat: "${key}" in the options of relation "${label}" is Turbine's spelling and would be ignored. ` +
|
|
990
|
+
`prisma-compat takes Prisma's "${alias}".`);
|
|
991
|
+
}
|
|
992
|
+
const suggestion = suggestKey(key, NESTED_RELATION_KEYS);
|
|
993
|
+
throw new ValidationError(`prisma-compat: unknown option "${key}" in the options of relation "${label}", it would be ignored.` +
|
|
994
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : '') +
|
|
995
|
+
` Valid options: ${[...NESTED_RELATION_KEYS].join(', ')}.`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
1007
998
|
/**
|
|
1008
999
|
* Translate a Prisma relation include payload into a Turbine `WithOptions`.
|
|
1009
1000
|
*
|
|
1001
|
+
* Every option the payload carries is either translated, refused by name, or
|
|
1002
|
+
* refused as unknown: none is dropped. `cursor` and `distinct` are Prisma's
|
|
1003
|
+
* and have no per-relation form in a `with` clause (E017); any other key is
|
|
1004
|
+
* E003 (see {@link assertNestedRelationKeys}).
|
|
1005
|
+
*
|
|
1010
1006
|
* `depth` is the relation hop count carried down from {@link translateProjection}
|
|
1011
1007
|
* and is charged to the nested `where` / `orderBy` too, so a payload that pads
|
|
1012
1008
|
* one arg with relation hops and then nests the other cannot spend two separate
|
|
@@ -1015,8 +1011,11 @@ function applyWriteProjection(proj, row) {
|
|
|
1015
1011
|
function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
|
|
1016
1012
|
if (val === true)
|
|
1017
1013
|
return true;
|
|
1018
|
-
|
|
1019
|
-
|
|
1014
|
+
const label = `${modelName(ctx, mm)}.${prismaRelationName(ctx, mm, turbineRel)}`;
|
|
1015
|
+
if (!isPlainRecord(val)) {
|
|
1016
|
+
throw new ValidationError(`prisma-compat: the value for relation "${label}" must be true or an options object, received ${describeValue(val)}.`);
|
|
1017
|
+
}
|
|
1018
|
+
assertNestedRelationKeys(label, val);
|
|
1020
1019
|
const target = relTargetModel(ctx, mm, turbineRel);
|
|
1021
1020
|
const opt = {};
|
|
1022
1021
|
if (val.where !== undefined)
|
|
@@ -1025,10 +1024,26 @@ function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
|
|
|
1025
1024
|
opt.orderBy = target ? translateOrderBy(ctx, target, val.orderBy, depth) : val.orderBy;
|
|
1026
1025
|
if (val.take !== undefined)
|
|
1027
1026
|
opt.limit = mapTake(val.take);
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
if (
|
|
1027
|
+
// Per parent, as in Prisma; core refuses it on a to-one relation.
|
|
1028
|
+
if (val.skip !== undefined)
|
|
1029
|
+
opt.offset = val.skip;
|
|
1030
|
+
if (val.cursor !== undefined) {
|
|
1031
|
+
throw new UnsupportedFeatureError('cursor on a nested relation include', 'prisma-compat', "Turbine's `with` clause has no cursor. Page the relation with `take` / `skip`, or with a separate " +
|
|
1032
|
+
'findMany on the related model.');
|
|
1033
|
+
}
|
|
1034
|
+
// `distinct: []` names no field and removes nothing, so it is not a request.
|
|
1035
|
+
if (val.distinct !== undefined && !(Array.isArray(val.distinct) && val.distinct.length === 0)) {
|
|
1036
|
+
throw new UnsupportedFeatureError(`distinct on a nested relation include ("${label}")`, 'prisma-compat', "Turbine's `with` clause has no distinct. Run a separate findMany on the related model with the same " +
|
|
1037
|
+
'where / orderBy / distinct, or de-duplicate the included rows in application code.');
|
|
1038
|
+
}
|
|
1039
|
+
if (val.select !== undefined || val.include !== undefined || val.omit !== undefined) {
|
|
1040
|
+
// Without the target model there are no field names to translate with, and
|
|
1041
|
+
// dropping the projection would return every column of the child rows.
|
|
1042
|
+
if (!target) {
|
|
1043
|
+
throw new ValidationError(`prisma-compat: relation "${label}" points at a table that is not in the compat map, so its ` +
|
|
1044
|
+
'`select` / `include` / `omit` cannot be translated. Regenerate the map with ' +
|
|
1045
|
+
'`turbine migrate-from-prisma`, or drop the projection.');
|
|
1046
|
+
}
|
|
1032
1047
|
const proj = translateProjection(ctx, target, val, depth);
|
|
1033
1048
|
if (proj.select)
|
|
1034
1049
|
opt.select = proj.select;
|
|
@@ -1041,30 +1056,104 @@ function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
|
|
|
1041
1056
|
}
|
|
1042
1057
|
return opt;
|
|
1043
1058
|
}
|
|
1044
|
-
/**
|
|
1045
|
-
function
|
|
1059
|
+
/** A count `where` that filters nothing: absent, or an object with no defined key. */
|
|
1060
|
+
function isUnfilteredCountWhere(where) {
|
|
1061
|
+
return where === undefined || (isPlainRecord(where) && Object.values(where).every((v) => v === undefined));
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Translate a Prisma `_count: { select: { rel: true } }` / `true` into Turbine
|
|
1065
|
+
* `with._count`.
|
|
1066
|
+
*
|
|
1067
|
+
* Core counts a relation whole: `with._count` takes `true` or `{ rel: true }`.
|
|
1068
|
+
* So a Prisma filtered count (`{ rel: { where } }`) is refused (E017) naming the
|
|
1069
|
+
* alternative, a relation that is not on the model is E003, and so is any key
|
|
1070
|
+
* but `select` under `_count` (a misspelled `selec` used to count EVERY relation,
|
|
1071
|
+
* and a misspelled relation counted nothing, both without a word).
|
|
1072
|
+
*/
|
|
1073
|
+
function translateCountSelect(ctx, mm, val) {
|
|
1046
1074
|
if (val === true)
|
|
1047
1075
|
return true;
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1076
|
+
const model = modelName(ctx, mm);
|
|
1077
|
+
if (!isPlainRecord(val)) {
|
|
1078
|
+
throw new ValidationError(`prisma-compat: \`_count\` on model "${model}" takes true or { select: { <relation>: true } }, ` +
|
|
1079
|
+
`received ${describeValue(val)}.`);
|
|
1080
|
+
}
|
|
1081
|
+
for (const key of Object.keys(val)) {
|
|
1082
|
+
if (key === 'select' || val[key] === undefined)
|
|
1083
|
+
continue;
|
|
1084
|
+
const suggestion = suggestKey(key, ['select']);
|
|
1085
|
+
throw new ValidationError(`prisma-compat: unknown key "${key}" in \`_count\` on model "${model}", it would be ignored.` +
|
|
1086
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : '') +
|
|
1087
|
+
' The only key is "select".');
|
|
1088
|
+
}
|
|
1089
|
+
if (val.select === undefined)
|
|
1090
|
+
return true;
|
|
1091
|
+
if (!isPlainRecord(val.select)) {
|
|
1092
|
+
throw new ValidationError(`prisma-compat: \`_count.select\` on model "${model}" takes { <relation>: true }, ` +
|
|
1093
|
+
`received ${describeValue(val.select)}.`);
|
|
1094
|
+
}
|
|
1095
|
+
const out = {};
|
|
1096
|
+
for (const [key, v] of Object.entries(val.select)) {
|
|
1097
|
+
if (v === undefined || v === false || v === null)
|
|
1098
|
+
continue;
|
|
1099
|
+
const rel = ownLookup(mm.relations, key);
|
|
1100
|
+
if (!rel) {
|
|
1101
|
+
const known = Object.keys(mm.relations);
|
|
1102
|
+
const suggestion = suggestKey(key, known);
|
|
1103
|
+
throw new ValidationError(`prisma-compat: unknown relation "${key}" in \`_count.select\` on model "${model}", it would not be counted.` +
|
|
1104
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : '') +
|
|
1105
|
+
` Relations: ${known.join(', ') || '(none)'}.`);
|
|
1106
|
+
}
|
|
1107
|
+
if (v === true) {
|
|
1108
|
+
setOwn(out, rel.name, true);
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
if (!isPlainRecord(v)) {
|
|
1112
|
+
throw new ValidationError(`prisma-compat: \`_count.select.${key}\` on model "${model}" takes true, false or { where }, ` +
|
|
1113
|
+
`received ${describeValue(v)}.`);
|
|
1114
|
+
}
|
|
1115
|
+
for (const k of Object.keys(v)) {
|
|
1116
|
+
if (k === 'where' || v[k] === undefined)
|
|
1052
1117
|
continue;
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1118
|
+
const hint = suggestKey(k, ['where']);
|
|
1119
|
+
throw new ValidationError(`prisma-compat: unknown key "${k}" in \`_count.select.${key}\` on model "${model}", it would be ignored.` +
|
|
1120
|
+
(hint ? ` Did you mean "${hint}"?` : '') +
|
|
1121
|
+
' The only key is "where".');
|
|
1056
1122
|
}
|
|
1057
|
-
|
|
1123
|
+
if (!isUnfilteredCountWhere(v.where)) {
|
|
1124
|
+
const target = relTargetModel(ctx, mm, rel.name);
|
|
1125
|
+
const targetName = target ? modelName(ctx, target) : undefined;
|
|
1126
|
+
const delegate = targetName ? (prismaPropertyAlias(targetName) ?? targetName) : '<model>';
|
|
1127
|
+
throw new UnsupportedFeatureError(`a filtered relation count (\`_count.select.${key}.where\` on model "${model}")`, 'prisma-compat', 'Turbine counts every row of a relation, so the filter would be dropped and the number would be wrong. ' +
|
|
1128
|
+
`Count the filtered rows through the related model instead: prisma.${delegate}.count({ where: ... }) ` +
|
|
1129
|
+
`with the foreign key and your filter, or prisma.${delegate}.groupBy({ by: [<foreign key>], _count: true }) ` +
|
|
1130
|
+
'for many parents at once.');
|
|
1131
|
+
}
|
|
1132
|
+
setOwn(out, rel.name, true);
|
|
1058
1133
|
}
|
|
1059
|
-
return
|
|
1134
|
+
return out;
|
|
1060
1135
|
}
|
|
1061
1136
|
function modelName(ctx, mm) {
|
|
1062
1137
|
return ctx.tableToModel.get(mm.table) ?? mm.table;
|
|
1063
1138
|
}
|
|
1064
1139
|
/**
|
|
1065
|
-
*
|
|
1066
|
-
* `
|
|
1067
|
-
* a
|
|
1140
|
+
* A Prisma field list that is written either as one name or as an array of
|
|
1141
|
+
* names (`distinct`, groupBy `by`), renamed into turbine fields. Anything else
|
|
1142
|
+
* is refused: a bare string used to be dropped (`distinct: 'status'` returned
|
|
1143
|
+
* every row, and `by: 'status'` crashed with a TypeError).
|
|
1144
|
+
*/
|
|
1145
|
+
function translateFieldList(ctx, mm, option, val) {
|
|
1146
|
+
const names = typeof val === 'string' ? [val] : val;
|
|
1147
|
+
if (!Array.isArray(names) || names.some((n) => typeof n !== 'string')) {
|
|
1148
|
+
throw new ValidationError(`prisma-compat: \`${option}\` on model "${modelName(ctx, mm)}" takes a field name or an array of field names, ` +
|
|
1149
|
+
`received ${describeValue(val)}.`);
|
|
1150
|
+
}
|
|
1151
|
+
return names.map((f) => renameField(mm, f));
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Assemble Turbine findMany-family args from Prisma read args. `where` /
|
|
1155
|
+
* `orderBy` are translated first, so the cursor step works in turbine-field
|
|
1156
|
+
* space.
|
|
1068
1157
|
*/
|
|
1069
1158
|
function translateReadArgs(ctx, mm, prismaArgs, kind) {
|
|
1070
1159
|
const t = {};
|
|
@@ -1086,19 +1175,11 @@ function translateReadArgs(ctx, mm, prismaArgs, kind) {
|
|
|
1086
1175
|
t.omit = proj.omit;
|
|
1087
1176
|
if (proj.with)
|
|
1088
1177
|
t.with = proj.with;
|
|
1089
|
-
if (
|
|
1090
|
-
t.distinct =
|
|
1091
|
-
}
|
|
1178
|
+
if (prismaArgs.distinct !== undefined)
|
|
1179
|
+
t.distinct = translateFieldList(ctx, mm, 'distinct', prismaArgs.distinct);
|
|
1092
1180
|
if (prismaArgs.relationLoadStrategy !== undefined) {
|
|
1093
1181
|
t.relationLoadStrategy = mapRelationLoadStrategy(prismaArgs.relationLoadStrategy);
|
|
1094
1182
|
}
|
|
1095
|
-
// ORDER IS LOAD-BEARING: translateCursor MUST run BEFORE applyImplicitPkOrder.
|
|
1096
|
-
// The cursor translation reads `t.orderBy` to decide the seek direction and to
|
|
1097
|
-
// validate that a bare inclusive cursor names the sort key; it must see the
|
|
1098
|
-
// USER's ordering, never an ordering this layer invented. Swap these two lines
|
|
1099
|
-
// and a bare inclusive cursor on a non-PK field silently compiles against the
|
|
1100
|
-
// injected primary-key order instead of throwing. See the guard test
|
|
1101
|
-
// "cursor decisions are made against the user's own orderBy".
|
|
1102
1183
|
translateCursor(ctx, mm, prismaArgs, t);
|
|
1103
1184
|
applyImplicitPkOrder(ctx, mm, t, kind);
|
|
1104
1185
|
return t;
|
|
@@ -1145,15 +1226,9 @@ function translateReadArgs(ctx, mm, prismaArgs, kind) {
|
|
|
1145
1226
|
* with `column "id" does not exist`. Injecting also lifts the `LIMIT` outside
|
|
1146
1227
|
* the `DISTINCT ON`, so the inner scan stops being limited. `distinct` reads
|
|
1147
1228
|
* keep the exact SQL they emitted before this ordering existed.
|
|
1148
|
-
* - `cursor`:
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
* primary-key order. The injection is safe because it runs strictly AFTER
|
|
1152
|
-
* {@link translateCursor}: the cursor comparison direction was already
|
|
1153
|
-
* resolved from the user's own `orderBy` (ascending by default, matching an
|
|
1154
|
-
* ascending primary key), and the bare-inclusive-cursor branch has already
|
|
1155
|
-
* set or demanded an `orderBy`, so only the exclusive `cursor` + `skip`
|
|
1156
|
-
* branch reaches here. It gains an `ORDER BY <pk> ASC` and nothing else.
|
|
1229
|
+
* - `cursor`: not excluded, and harmless either way. Core orders every cursor
|
|
1230
|
+
* page itself (the caller's order plus a primary-key tiebreaker), and with no
|
|
1231
|
+
* `orderBy` that is exactly the `ORDER BY <pk> ASC` written here.
|
|
1157
1232
|
*/
|
|
1158
1233
|
function applyImplicitPkOrder(ctx, mm, t, kind) {
|
|
1159
1234
|
if (kind === 'unique')
|
|
@@ -1164,134 +1239,59 @@ function applyImplicitPkOrder(ctx, mm, t, kind) {
|
|
|
1164
1239
|
return;
|
|
1165
1240
|
if (kind === 'many' && t.limit === undefined && t.offset === undefined)
|
|
1166
1241
|
return;
|
|
1167
|
-
const meta = ctx.schema.tables
|
|
1242
|
+
const meta = ownLookup(ctx.schema.tables, mm.table);
|
|
1168
1243
|
const pk = meta?.primaryKey ?? [];
|
|
1169
1244
|
if (pk.length === 0)
|
|
1170
1245
|
return;
|
|
1171
1246
|
const fields = pk.map((col) => meta?.reverseColumnMap?.[col] ?? col);
|
|
1172
1247
|
t.orderBy = fields.length === 1 ? { [fields[0]]: 'asc' } : fields.map((f) => ({ [f]: 'asc' }));
|
|
1173
1248
|
}
|
|
1174
|
-
/** turbine field names of the model's single-column primary key, if any. */
|
|
1175
|
-
function singleColumnPkField(ctx, mm) {
|
|
1176
|
-
const pk = ctx.schema.tables[mm.table]?.primaryKey;
|
|
1177
|
-
if (pk && pk.length === 1) {
|
|
1178
|
-
const col = pk[0];
|
|
1179
|
-
return ctx.schema.tables[mm.table]?.reverseColumnMap?.[col] ?? col;
|
|
1180
|
-
}
|
|
1181
|
-
return undefined;
|
|
1182
|
-
}
|
|
1183
1249
|
/**
|
|
1184
1250
|
* Translate Prisma cursor + take/skip onto Turbine args (operating on already
|
|
1185
1251
|
* translated `t.where` / `t.orderBy` in turbine-field space).
|
|
1186
1252
|
*
|
|
1187
1253
|
* - **No cursor:** `skip`→`offset`, `take`→`limit`.
|
|
1188
|
-
* - **Cursor
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
* single-column PK). Any other shape THROWS rather than emit a wrong page -
|
|
1196
|
-
* pair the cursor with `skip: 1` for the exact exclusive translation.
|
|
1254
|
+
* - **Cursor:** core's cursor with the {@link CURSOR_INCLUSIVE} flag, plus
|
|
1255
|
+
* `offset: skip`. That is Prisma's definition exactly: the page starts AT the
|
|
1256
|
+
* cursor row, in the query's order (core completes it with a primary-key
|
|
1257
|
+
* tiebreaker), and `skip` counts from there, so the `skip: 1` idiom drops the
|
|
1258
|
+
* cursor row. Core places the page from the row the cursor names by a unique
|
|
1259
|
+
* key, which is what a Prisma cursor always is, so a cursor whose row has
|
|
1260
|
+
* been deleted gives an empty page, as it does in Prisma.
|
|
1197
1261
|
*/
|
|
1198
|
-
function translateCursor(
|
|
1262
|
+
function translateCursor(_ctx, mm, prismaArgs, t) {
|
|
1199
1263
|
const cursor = prismaArgs.cursor;
|
|
1200
1264
|
const skip = prismaArgs.skip;
|
|
1201
1265
|
const take = prismaArgs.take;
|
|
1202
1266
|
if (take !== undefined)
|
|
1203
1267
|
t.limit = mapTake(take);
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1206
|
-
|
|
1268
|
+
if (skip !== undefined)
|
|
1269
|
+
t.offset = skip;
|
|
1270
|
+
if (cursor === undefined)
|
|
1271
|
+
return;
|
|
1272
|
+
// A cursor that is not an object (`null`) reaches core as written, which
|
|
1273
|
+
// refuses it: read as "no cursor" it would return the first page.
|
|
1274
|
+
if (!isPlainRecord(cursor)) {
|
|
1275
|
+
t.cursor = cursor;
|
|
1207
1276
|
return;
|
|
1208
1277
|
}
|
|
1209
1278
|
// Translate cursor field names (and expand a compound-unique selector cursor).
|
|
1279
|
+
// An `undefined` value is carried through rather than dropped, so a cursor
|
|
1280
|
+
// whose every value was `undefined` reaches core's args seam as one and is
|
|
1281
|
+
// refused there (E003), instead of becoming `{}` and paging from the start.
|
|
1210
1282
|
const tcursor = {};
|
|
1211
1283
|
for (const [key, val] of Object.entries(cursor)) {
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const compound = mm.compoundUniques[key];
|
|
1215
|
-
if (compound && isPlainObject(val)) {
|
|
1284
|
+
const compound = ownLookup(mm.compoundUniques, key);
|
|
1285
|
+
if (compound && isPlainRecord(val)) {
|
|
1216
1286
|
for (const [pk, pv] of Object.entries(val))
|
|
1217
|
-
tcursor
|
|
1287
|
+
setOwn(tcursor, renameField(mm, pk), pv);
|
|
1218
1288
|
}
|
|
1219
1289
|
else {
|
|
1220
|
-
tcursor
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
const cursorFields = Object.keys(tcursor);
|
|
1224
|
-
if (skip !== undefined && skip >= 1) {
|
|
1225
|
-
t.cursor = tcursor;
|
|
1226
|
-
t.offset = skip - 1;
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
// Bare inclusive cursor (skip absent or 0).
|
|
1230
|
-
if (cursorFields.length !== 1) {
|
|
1231
|
-
throw new UnsupportedFeatureError('inclusive multi-field cursor without skip', 'prisma-compat', 'Pair the cursor with `skip: 1` (the Prisma idiom) so it maps to an exact exclusive cursor.');
|
|
1232
|
-
}
|
|
1233
|
-
const field = cursorFields[0];
|
|
1234
|
-
const value = tcursor[field];
|
|
1235
|
-
const obEntries = orderByPairs(t.orderBy);
|
|
1236
|
-
let desc = false;
|
|
1237
|
-
if (obEntries.length === 0) {
|
|
1238
|
-
const pkField = singleColumnPkField(ctx, mm);
|
|
1239
|
-
if (!pkField || field !== pkField) {
|
|
1240
|
-
throw new UnsupportedFeatureError('inclusive cursor without orderBy on a non-PK field', 'prisma-compat', `A bare Prisma cursor is inclusive; translating it needs the cursor field "${field}" to be the single-column primary key, or to be the single orderBy field. Pair the cursor with \`skip: 1\`.`);
|
|
1241
|
-
}
|
|
1242
|
-
t.orderBy = { [field]: 'asc' };
|
|
1243
|
-
}
|
|
1244
|
-
else {
|
|
1245
|
-
if (obEntries.length !== 1 || obEntries[0][0] !== field) {
|
|
1246
|
-
throw new UnsupportedFeatureError('inclusive cursor whose field is not the sort key', 'prisma-compat', `A bare Prisma cursor is inclusive; translating it needs the cursor field "${field}" to be the single orderBy field. Order by "${field}", or pair the cursor with \`skip: 1\`.`);
|
|
1290
|
+
setOwn(tcursor, renameField(mm, key), val);
|
|
1247
1291
|
}
|
|
1248
|
-
desc = obEntries[0][1];
|
|
1249
1292
|
}
|
|
1250
|
-
|
|
1251
|
-
t
|
|
1252
|
-
}
|
|
1253
|
-
/**
|
|
1254
|
-
* Flatten a Turbine orderBy (object, or an array of them) into [field, isDesc]
|
|
1255
|
-
* pairs.
|
|
1256
|
-
*
|
|
1257
|
-
* EVERY array element is read. Reading only an array of length one returned
|
|
1258
|
-
* `[]` for a two-element array, and `[]` is also what "no orderBy at all"
|
|
1259
|
-
* returns, so {@link translateCursor} took its no-orderBy branch: it found a
|
|
1260
|
-
* bare cursor on the primary key, wrote `orderBy = { id: 'asc' }` over the
|
|
1261
|
-
* caller's array, and returned the rows following the anchor in PK order
|
|
1262
|
-
* instead of in the caller's order. The single-sort-key check downstream is
|
|
1263
|
-
* what refuses a shape a bare (inclusive) cursor cannot express, and it can
|
|
1264
|
-
* only fire if the pairs it counts are all there.
|
|
1265
|
-
*/
|
|
1266
|
-
function orderByPairs(ob) {
|
|
1267
|
-
const entries = Array.isArray(ob) ? ob : [ob];
|
|
1268
|
-
const out = [];
|
|
1269
|
-
for (const entry of entries) {
|
|
1270
|
-
if (!isPlainObject(entry))
|
|
1271
|
-
continue;
|
|
1272
|
-
for (const [k, v] of Object.entries(entry)) {
|
|
1273
|
-
const dir = isPlainObject(v) ? v.sort : v;
|
|
1274
|
-
out.push([k, dir === 'desc']);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
return out;
|
|
1278
|
-
}
|
|
1279
|
-
/** Merge a `{ field: { gte|lte: value } }` keyset predicate into a where object. */
|
|
1280
|
-
function mergeKeyset(where, field, op, value) {
|
|
1281
|
-
const merged = { ...where };
|
|
1282
|
-
const existing = merged[field];
|
|
1283
|
-
if (isPlainObject(existing)) {
|
|
1284
|
-
merged[field] = { ...existing, [op]: value };
|
|
1285
|
-
}
|
|
1286
|
-
else if (existing !== undefined) {
|
|
1287
|
-
const prevAnd = merged.AND;
|
|
1288
|
-
const andList = Array.isArray(prevAnd) ? prevAnd : prevAnd !== undefined ? [prevAnd] : [];
|
|
1289
|
-
merged.AND = [...andList, { [field]: { [op]: value } }];
|
|
1290
|
-
}
|
|
1291
|
-
else {
|
|
1292
|
-
merged[field] = { [op]: value };
|
|
1293
|
-
}
|
|
1294
|
-
return merged;
|
|
1293
|
+
t.cursor = tcursor;
|
|
1294
|
+
t[CURSOR_INCLUSIVE] = true;
|
|
1295
1295
|
}
|
|
1296
1296
|
// --- write-data translation (nested writes) -------------------------------
|
|
1297
1297
|
const NESTED_WRITE_OPS = new Set([
|
|
@@ -1315,20 +1315,36 @@ const NESTED_WRITE_OPS = new Set([
|
|
|
1315
1315
|
*/
|
|
1316
1316
|
function translateWriteData(ctx, mm, data, depth = 0) {
|
|
1317
1317
|
assertTranslateDepth(depth, 'data');
|
|
1318
|
-
if (!
|
|
1318
|
+
if (!isPlainRecord(data))
|
|
1319
1319
|
return data;
|
|
1320
1320
|
const out = {};
|
|
1321
1321
|
for (const [key, val] of Object.entries(data)) {
|
|
1322
|
-
|
|
1323
|
-
|
|
1322
|
+
if (isExpressionNode(val)) {
|
|
1323
|
+
// The field names inside an expression are Turbine's, and this layer
|
|
1324
|
+
// renames only the keys it can see, so one would be compiled against
|
|
1325
|
+
// the wrong names wherever a Prisma field is mapped.
|
|
1326
|
+
throw new UnsupportedFeatureError(`an update expression (expr / col()) in "${key}" on model "${modelName(ctx, mm)}"`, 'prisma-compat', 'Expressions are a Turbine-native API: run this write through the Turbine client, where field names ' +
|
|
1327
|
+
'are its own, or compute the value in JavaScript.');
|
|
1328
|
+
}
|
|
1329
|
+
const rel = ownLookup(mm.relations, key);
|
|
1330
|
+
if (rel && isPlainRecord(val) && Object.keys(val).some((k) => NESTED_WRITE_OPS.has(k))) {
|
|
1324
1331
|
const target = relTargetModel(ctx, mm, rel.name);
|
|
1325
|
-
out
|
|
1332
|
+
setOwn(out, rel.name, translateNestedWrite(ctx, target, val, depth + 1));
|
|
1326
1333
|
continue;
|
|
1327
1334
|
}
|
|
1328
|
-
out
|
|
1335
|
+
setOwn(out, renameField(mm, key), val);
|
|
1329
1336
|
}
|
|
1330
1337
|
return out;
|
|
1331
1338
|
}
|
|
1339
|
+
/**
|
|
1340
|
+
* An update expression (`expr.*`) or column reference (`col()`), read by the
|
|
1341
|
+
* brands' registry keys so this shim imports neither builder module.
|
|
1342
|
+
*/
|
|
1343
|
+
function isExpressionNode(value) {
|
|
1344
|
+
return (typeof value === 'object' &&
|
|
1345
|
+
value !== null &&
|
|
1346
|
+
(Symbol.for('turbine-orm.sqlExpr') in value || Symbol.for('turbine-orm.columnRef') in value));
|
|
1347
|
+
}
|
|
1332
1348
|
/**
|
|
1333
1349
|
* One relation's nested-write ops. `depth` is threaded into EVERY payload
|
|
1334
1350
|
* translator, the `where` ones included, so the budget is spent on total
|
|
@@ -1356,10 +1372,8 @@ function translateNestedWrite(ctx, target, ops, depth) {
|
|
|
1356
1372
|
break;
|
|
1357
1373
|
case 'deleteMany':
|
|
1358
1374
|
case 'updateMany':
|
|
1359
|
-
out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
|
|
1360
|
-
break;
|
|
1361
1375
|
case 'update':
|
|
1362
|
-
out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
|
|
1376
|
+
out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, op, p, depth + 1));
|
|
1363
1377
|
break;
|
|
1364
1378
|
case 'connectOrCreate':
|
|
1365
1379
|
out[op] = mapMaybeArray(payload, (p) => translateConnectOrCreate(ctx, target, p, depth + 1));
|
|
@@ -1368,7 +1382,7 @@ function translateNestedWrite(ctx, target, ops, depth) {
|
|
|
1368
1382
|
out[op] = mapMaybeArray(payload, (p) => translateUpsertNested(ctx, target, p, depth + 1));
|
|
1369
1383
|
break;
|
|
1370
1384
|
default:
|
|
1371
|
-
out
|
|
1385
|
+
setOwn(out, op, payload);
|
|
1372
1386
|
}
|
|
1373
1387
|
}
|
|
1374
1388
|
return out;
|
|
@@ -1376,11 +1390,27 @@ function translateNestedWrite(ctx, target, ops, depth) {
|
|
|
1376
1390
|
function mapMaybeArray(val, fn) {
|
|
1377
1391
|
return Array.isArray(val) ? val.map(fn) : fn(val);
|
|
1378
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Refuse a key on a nested write's `{ where, data }` / `{ where, create, update }`
|
|
1395
|
+
* payload that this layer does not read: it was dropped, so the part of the
|
|
1396
|
+
* write the caller meant never ran. An `undefined` value is absent.
|
|
1397
|
+
*/
|
|
1398
|
+
function assertNestedPayloadKeys(op, p, allowed) {
|
|
1399
|
+
for (const key of Object.keys(p)) {
|
|
1400
|
+
if (p[key] === undefined || allowed.includes(key))
|
|
1401
|
+
continue;
|
|
1402
|
+
const suggestion = suggestKey(key, allowed);
|
|
1403
|
+
throw new ValidationError(`prisma-compat: unknown key "${key}" in a nested ${op}, it would be ignored.` +
|
|
1404
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : '') +
|
|
1405
|
+
` Valid keys: ${allowed.join(', ')}.`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1379
1408
|
/** A `{ where?, data }` pair (nested update/updateMany), or a bare data object. */
|
|
1380
|
-
function translateWhereDataPair(ctx, target, p, depth) {
|
|
1381
|
-
if (!
|
|
1409
|
+
function translateWhereDataPair(ctx, target, op, p, depth) {
|
|
1410
|
+
if (!isPlainRecord(p))
|
|
1382
1411
|
return p;
|
|
1383
1412
|
if ('data' in p || 'where' in p) {
|
|
1413
|
+
assertNestedPayloadKeys(op, p, ['where', 'data']);
|
|
1384
1414
|
const out = {};
|
|
1385
1415
|
if (p.where !== undefined)
|
|
1386
1416
|
out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
|
|
@@ -1391,8 +1421,9 @@ function translateWhereDataPair(ctx, target, p, depth) {
|
|
|
1391
1421
|
return target ? translateWriteData(ctx, target, p, depth) : p;
|
|
1392
1422
|
}
|
|
1393
1423
|
function translateConnectOrCreate(ctx, target, p, depth) {
|
|
1394
|
-
if (!
|
|
1424
|
+
if (!isPlainRecord(p))
|
|
1395
1425
|
return p;
|
|
1426
|
+
assertNestedPayloadKeys('connectOrCreate', p, ['where', 'create']);
|
|
1396
1427
|
const out = {};
|
|
1397
1428
|
if (p.where !== undefined)
|
|
1398
1429
|
out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
|
|
@@ -1401,8 +1432,9 @@ function translateConnectOrCreate(ctx, target, p, depth) {
|
|
|
1401
1432
|
return out;
|
|
1402
1433
|
}
|
|
1403
1434
|
function translateUpsertNested(ctx, target, p, depth) {
|
|
1404
|
-
if (!
|
|
1435
|
+
if (!isPlainRecord(p))
|
|
1405
1436
|
return p;
|
|
1437
|
+
assertNestedPayloadKeys('upsert', p, ['where', 'create', 'update']);
|
|
1406
1438
|
const out = {};
|
|
1407
1439
|
if (p.where !== undefined)
|
|
1408
1440
|
out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
|
|
@@ -1414,8 +1446,24 @@ function translateUpsertNested(ctx, target, p, depth) {
|
|
|
1414
1446
|
}
|
|
1415
1447
|
// --- aggregate / groupBy translation --------------------------------------
|
|
1416
1448
|
const AGG_FIELD_BLOCKS = ['_sum', '_avg', '_min', '_max'];
|
|
1449
|
+
/**
|
|
1450
|
+
* Prisma's `count` and `aggregate` with `take` / `skip` / `cursor` compute over
|
|
1451
|
+
* that PAGE of rows. Turbine has no paged count or aggregate, and answering
|
|
1452
|
+
* over every matching row instead is a different number, so it is refused.
|
|
1453
|
+
* `orderBy` on its own changes no count or aggregate and is accepted.
|
|
1454
|
+
*/
|
|
1455
|
+
function refusePagedAggregate(op, args) {
|
|
1456
|
+
const paged = ['take', 'skip', 'cursor'].filter((k) => args[k] !== undefined);
|
|
1457
|
+
if (paged.length === 0)
|
|
1458
|
+
return;
|
|
1459
|
+
throw new UnsupportedFeatureError(`${paged.join(' / ')} on ${op}`, 'prisma-compat', `Prisma computes this ${op} over one page of rows, and Turbine has no paged ${op}. Fetch the page with ` +
|
|
1460
|
+
`findMany (same where / orderBy / ${paged.join(' / ')}) and compute it from the rows, or drop ` +
|
|
1461
|
+
`${paged.join(' / ')} to ${op} every matching row.`);
|
|
1462
|
+
}
|
|
1417
1463
|
function translateAggregateArgs(ctx, mm, args, isGroupBy) {
|
|
1418
1464
|
const t = {};
|
|
1465
|
+
if (!isGroupBy)
|
|
1466
|
+
refusePagedAggregate('aggregate', args);
|
|
1419
1467
|
applyNativeOptions(isGroupBy ? GROUP_BY_OPTIONS : AGGREGATE_OPTIONS, args, t);
|
|
1420
1468
|
if (args.where !== undefined)
|
|
1421
1469
|
t.where = translateWhere(ctx, mm, args.where);
|
|
@@ -1426,17 +1474,19 @@ function translateAggregateArgs(ctx, mm, args, isGroupBy) {
|
|
|
1426
1474
|
t[block] = renameAggBlock(mm, args[block], false);
|
|
1427
1475
|
}
|
|
1428
1476
|
if (isGroupBy) {
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1477
|
+
if (args.by === undefined) {
|
|
1478
|
+
throw new ValidationError(`prisma-compat: groupBy on model "${modelName(ctx, mm)}" requires \`by\`, a field name or an array of field names.`);
|
|
1479
|
+
}
|
|
1480
|
+
t.by = translateFieldList(ctx, mm, 'by', args.by);
|
|
1431
1481
|
if (args.orderBy !== undefined)
|
|
1432
1482
|
t.orderBy = translateOrderBy(ctx, mm, args.orderBy);
|
|
1433
1483
|
if (args.having !== undefined)
|
|
1434
1484
|
t.having = renameHaving(mm, args.having);
|
|
1435
1485
|
if (args.distinctOn !== undefined)
|
|
1436
1486
|
t.distinctOn = translateDistinctOn(ctx, mm, args.distinctOn);
|
|
1437
|
-
if (
|
|
1487
|
+
if (args.take !== undefined)
|
|
1438
1488
|
t.limit = mapTake(args.take);
|
|
1439
|
-
if (
|
|
1489
|
+
if (args.skip !== undefined)
|
|
1440
1490
|
t.offset = args.skip;
|
|
1441
1491
|
}
|
|
1442
1492
|
return t;
|
|
@@ -1449,7 +1499,7 @@ function translateAggregateArgs(ctx, mm, args, isGroupBy) {
|
|
|
1449
1499
|
* silently wrong on one with a `@map`.
|
|
1450
1500
|
*/
|
|
1451
1501
|
function translateDistinctOn(ctx, mm, val) {
|
|
1452
|
-
if (!
|
|
1502
|
+
if (!isPlainRecord(val))
|
|
1453
1503
|
return val;
|
|
1454
1504
|
const out = { ...val };
|
|
1455
1505
|
if (Array.isArray(val.columns))
|
|
@@ -1460,7 +1510,7 @@ function translateDistinctOn(ctx, mm, val) {
|
|
|
1460
1510
|
}
|
|
1461
1511
|
/** Rename field keys inside an aggregate block; `_count`'s `_all` passes through. */
|
|
1462
1512
|
function renameAggBlock(mm, block, isCount) {
|
|
1463
|
-
if (block === true || !
|
|
1513
|
+
if (block === true || !isPlainRecord(block))
|
|
1464
1514
|
return block;
|
|
1465
1515
|
const out = {};
|
|
1466
1516
|
for (const [key, val] of Object.entries(block)) {
|
|
@@ -1468,7 +1518,7 @@ function renameAggBlock(mm, block, isCount) {
|
|
|
1468
1518
|
out._all = val;
|
|
1469
1519
|
continue;
|
|
1470
1520
|
}
|
|
1471
|
-
out
|
|
1521
|
+
setOwn(out, renameField(mm, key), val);
|
|
1472
1522
|
}
|
|
1473
1523
|
return out;
|
|
1474
1524
|
}
|
|
@@ -1483,7 +1533,7 @@ function renameAggBlock(mm, block, isCount) {
|
|
|
1483
1533
|
*/
|
|
1484
1534
|
function renameHaving(mm, having, depth = 0) {
|
|
1485
1535
|
assertTranslateDepth(depth, 'having');
|
|
1486
|
-
if (!
|
|
1536
|
+
if (!isPlainRecord(having))
|
|
1487
1537
|
return having;
|
|
1488
1538
|
const out = {};
|
|
1489
1539
|
for (const [key, val] of Object.entries(having)) {
|
|
@@ -1495,7 +1545,7 @@ function renameHaving(mm, having, depth = 0) {
|
|
|
1495
1545
|
out._count = val;
|
|
1496
1546
|
continue;
|
|
1497
1547
|
}
|
|
1498
|
-
out
|
|
1548
|
+
setOwn(out, renameField(mm, key), val);
|
|
1499
1549
|
}
|
|
1500
1550
|
return out;
|
|
1501
1551
|
}
|
|
@@ -1509,7 +1559,7 @@ function reshapeRowOrNull(ctx, mm, row) {
|
|
|
1509
1559
|
return row == null ? null : reshapeRow(ctx, mm, row);
|
|
1510
1560
|
}
|
|
1511
1561
|
function reshapeRow(ctx, mm, row) {
|
|
1512
|
-
if (!
|
|
1562
|
+
if (!isPlainRecord(row))
|
|
1513
1563
|
return row;
|
|
1514
1564
|
const l = lookupsFor(ctx, mm);
|
|
1515
1565
|
const out = {};
|
|
@@ -1518,7 +1568,7 @@ function reshapeRow(ctx, mm, row) {
|
|
|
1518
1568
|
out._count = reshapeCount(l, val);
|
|
1519
1569
|
continue;
|
|
1520
1570
|
}
|
|
1521
|
-
const rel = l.reverseRelations
|
|
1571
|
+
const rel = ownLookup(l.reverseRelations, key);
|
|
1522
1572
|
if (rel) {
|
|
1523
1573
|
const target = relTargetModel(ctx, mm, key);
|
|
1524
1574
|
let rv;
|
|
@@ -1528,25 +1578,25 @@ function reshapeRow(ctx, mm, row) {
|
|
|
1528
1578
|
// map's cardinality is authoritative → first element or null.
|
|
1529
1579
|
rv = rel.cardinality === 'one' ? (mapped.length ? mapped[0] : null) : mapped;
|
|
1530
1580
|
}
|
|
1531
|
-
else if (
|
|
1581
|
+
else if (isPlainRecord(val)) {
|
|
1532
1582
|
rv = target ? reshapeRow(ctx, target, val) : val;
|
|
1533
1583
|
}
|
|
1534
1584
|
else {
|
|
1535
1585
|
rv = val; // null to-one
|
|
1536
1586
|
}
|
|
1537
|
-
out
|
|
1587
|
+
setOwn(out, rel.prismaName, rv);
|
|
1538
1588
|
continue;
|
|
1539
1589
|
}
|
|
1540
1590
|
let sv = val;
|
|
1541
1591
|
if (typeof sv === 'string' && l.timeFields.has(key)) {
|
|
1542
1592
|
sv = timeStringToDate(sv) ?? sv;
|
|
1543
1593
|
}
|
|
1544
|
-
out
|
|
1594
|
+
setOwn(out, l.identityFields ? key : (ownLookup(l.reverseFields, key) ?? key), sv);
|
|
1545
1595
|
}
|
|
1546
1596
|
return out;
|
|
1547
1597
|
}
|
|
1548
1598
|
function reshapeCount(l, count) {
|
|
1549
|
-
if (!
|
|
1599
|
+
if (!isPlainRecord(count))
|
|
1550
1600
|
return count;
|
|
1551
1601
|
const out = {};
|
|
1552
1602
|
for (const [key, val] of Object.entries(count)) {
|
|
@@ -1554,13 +1604,13 @@ function reshapeCount(l, count) {
|
|
|
1554
1604
|
out._all = val;
|
|
1555
1605
|
continue;
|
|
1556
1606
|
}
|
|
1557
|
-
out
|
|
1607
|
+
setOwn(out, ownLookup(l.reverseRelations, key)?.prismaName ?? key, val);
|
|
1558
1608
|
}
|
|
1559
1609
|
return out;
|
|
1560
1610
|
}
|
|
1561
1611
|
/** Reshape an aggregate result: field keys inside blocks → prisma names. */
|
|
1562
1612
|
function reshapeAggregate(ctx, mm, res) {
|
|
1563
|
-
if (!
|
|
1613
|
+
if (!isPlainRecord(res))
|
|
1564
1614
|
return res;
|
|
1565
1615
|
const l = lookupsFor(ctx, mm);
|
|
1566
1616
|
const out = {};
|
|
@@ -1577,12 +1627,12 @@ function reshapeAggregate(ctx, mm, res) {
|
|
|
1577
1627
|
if (typeof sv === 'string' && l.timeFields.has(key)) {
|
|
1578
1628
|
sv = timeStringToDate(sv) ?? sv;
|
|
1579
1629
|
}
|
|
1580
|
-
out
|
|
1630
|
+
setOwn(out, l.identityFields ? key : (ownLookup(l.reverseFields, key) ?? key), sv);
|
|
1581
1631
|
}
|
|
1582
1632
|
return out;
|
|
1583
1633
|
}
|
|
1584
1634
|
function reshapeAggFieldBlock(l, block, isCount) {
|
|
1585
|
-
if (!
|
|
1635
|
+
if (!isPlainRecord(block))
|
|
1586
1636
|
return block;
|
|
1587
1637
|
const out = {};
|
|
1588
1638
|
for (const [key, val] of Object.entries(block)) {
|
|
@@ -1590,13 +1640,13 @@ function reshapeAggFieldBlock(l, block, isCount) {
|
|
|
1590
1640
|
out._all = val;
|
|
1591
1641
|
continue;
|
|
1592
1642
|
}
|
|
1593
|
-
out
|
|
1643
|
+
setOwn(out, ownLookup(l.reverseFields, key) ?? key, val);
|
|
1594
1644
|
}
|
|
1595
1645
|
return out;
|
|
1596
1646
|
}
|
|
1597
1647
|
/** Reshape a groupBy row: by-field keys + aggregate blocks → prisma names. */
|
|
1598
1648
|
function reshapeGroupRow(ctx, mm, row) {
|
|
1599
|
-
if (!
|
|
1649
|
+
if (!isPlainRecord(row))
|
|
1600
1650
|
return row;
|
|
1601
1651
|
const l = lookupsFor(ctx, mm);
|
|
1602
1652
|
const out = {};
|
|
@@ -1613,7 +1663,7 @@ function reshapeGroupRow(ctx, mm, row) {
|
|
|
1613
1663
|
if (typeof sv === 'string' && l.timeFields.has(key)) {
|
|
1614
1664
|
sv = timeStringToDate(sv) ?? sv;
|
|
1615
1665
|
}
|
|
1616
|
-
out
|
|
1666
|
+
setOwn(out, l.identityFields ? key : (ownLookup(l.reverseFields, key) ?? key), sv);
|
|
1617
1667
|
}
|
|
1618
1668
|
return out;
|
|
1619
1669
|
}
|
|
@@ -1654,6 +1704,55 @@ class CompatPromise {
|
|
|
1654
1704
|
function batchableOf(v) {
|
|
1655
1705
|
return v instanceof CompatPromise ? v[COMPAT_DEFERRED] : undefined;
|
|
1656
1706
|
}
|
|
1707
|
+
/** Warn-once namespace for a `$transaction` option this layer accepts and does not apply. */
|
|
1708
|
+
const TX_OPTION_WARN_NS = 'prismaCompatTxOption';
|
|
1709
|
+
/**
|
|
1710
|
+
* Every `$transaction` option this layer understands: Prisma's three, plus the
|
|
1711
|
+
* Turbine-native ones it forwards to core as written.
|
|
1712
|
+
*/
|
|
1713
|
+
const TX_OPTION_KEYS = new Set([
|
|
1714
|
+
'isolationLevel',
|
|
1715
|
+
'timeout',
|
|
1716
|
+
'maxWait',
|
|
1717
|
+
'sessionContext',
|
|
1718
|
+
'allowBuiltinSettings',
|
|
1719
|
+
'readOnly',
|
|
1720
|
+
]);
|
|
1721
|
+
/**
|
|
1722
|
+
* Check a `$transaction` options object and drop the one key core has no use
|
|
1723
|
+
* for. A key that is not an option used to be forwarded and ignored, so a
|
|
1724
|
+
* misspelled `isolationLevl` ran the transaction at the default isolation.
|
|
1725
|
+
* `maxWait` bounds how long Prisma waits for a pooled connection; here the
|
|
1726
|
+
* pool's own `connectionTimeoutMs` does that, so it is accepted, warned about
|
|
1727
|
+
* once, and not forwarded.
|
|
1728
|
+
*/
|
|
1729
|
+
function resolveTxOptions(log, options) {
|
|
1730
|
+
if (options === undefined || options === null)
|
|
1731
|
+
return undefined;
|
|
1732
|
+
if (!isPlainRecord(options)) {
|
|
1733
|
+
throw new ValidationError(`prisma-compat: $transaction options must be an object, received ${describeValue(options)}.`);
|
|
1734
|
+
}
|
|
1735
|
+
const out = {};
|
|
1736
|
+
for (const [key, value] of Object.entries(options)) {
|
|
1737
|
+
if (value === undefined)
|
|
1738
|
+
continue;
|
|
1739
|
+
if (!TX_OPTION_KEYS.has(key)) {
|
|
1740
|
+
const suggestion = suggestKey(key, TX_OPTION_KEYS);
|
|
1741
|
+
throw new ValidationError(`prisma-compat: unknown $transaction option "${key}", it would be ignored.` +
|
|
1742
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : '') +
|
|
1743
|
+
` Valid options: ${[...TX_OPTION_KEYS].join(', ')}.`);
|
|
1744
|
+
}
|
|
1745
|
+
if (key === 'maxWait') {
|
|
1746
|
+
if (log.diagnostics.advice && shouldWarnOnce(TX_OPTION_WARN_NS, key)) {
|
|
1747
|
+
logTo(log.logger, 'warn', 'prisma-compat: the $transaction option "maxWait" has no effect here. Waiting for a pooled connection ' +
|
|
1748
|
+
"is bounded by the client's `connectionTimeoutMs` instead.", { code: 'prisma-compat-unknown-option', operation: '$transaction', key });
|
|
1749
|
+
}
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
out[key] = value;
|
|
1753
|
+
}
|
|
1754
|
+
return out;
|
|
1755
|
+
}
|
|
1657
1756
|
/**
|
|
1658
1757
|
* Prisma's client-property spelling of a model name: first letter lowercased
|
|
1659
1758
|
* (`model User` -> `prisma.user`). Returns null when the spelling is identical
|
|
@@ -1699,7 +1798,7 @@ function clientDefaultValue(kind) {
|
|
|
1699
1798
|
/** Fill missing create-side client defaults (uuid / cuid / now / updatedAt), by Prisma field name. */
|
|
1700
1799
|
function applyCreateDefaults(mm, data) {
|
|
1701
1800
|
const cd = mm.clientDefaults;
|
|
1702
|
-
if (!cd || !
|
|
1801
|
+
if (!cd || !isPlainRecord(data))
|
|
1703
1802
|
return data;
|
|
1704
1803
|
let out = data;
|
|
1705
1804
|
for (const [field, kind] of Object.entries(cd)) {
|
|
@@ -1714,7 +1813,7 @@ function applyCreateDefaults(mm, data) {
|
|
|
1714
1813
|
/** Touch @updatedAt fields on the update side (Prisma sets them on every update). */
|
|
1715
1814
|
function applyUpdateTouch(mm, data) {
|
|
1716
1815
|
const cd = mm.clientDefaults;
|
|
1717
|
-
if (!cd || !
|
|
1816
|
+
if (!cd || !isPlainRecord(data))
|
|
1718
1817
|
return data;
|
|
1719
1818
|
let out = data;
|
|
1720
1819
|
for (const [field, kind] of Object.entries(cd)) {
|
|
@@ -1728,8 +1827,8 @@ function applyUpdateTouch(mm, data) {
|
|
|
1728
1827
|
}
|
|
1729
1828
|
/** Whether TRANSLATED write data carries relation keys (nested-write shapes). */
|
|
1730
1829
|
function hasNestedKeys(ctx, mm, data) {
|
|
1731
|
-
const rels = ctx.schema.tables
|
|
1732
|
-
if (!rels || !
|
|
1830
|
+
const rels = ownLookup(ctx.schema.tables, mm.table)?.relations;
|
|
1831
|
+
if (!rels || !isPlainRecord(data))
|
|
1733
1832
|
return false;
|
|
1734
1833
|
return Object.keys(data).some((k) => {
|
|
1735
1834
|
if (!Object.hasOwn(rels, k))
|
|
@@ -1790,7 +1889,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1790
1889
|
// itself, for the same reason (see the comment above): a compat promise that
|
|
1791
1890
|
// is built and never awaited must produce no output at all.
|
|
1792
1891
|
const translate = () => {
|
|
1793
|
-
warnUnknownQueryOptions(modelName(ctx, mm), op, rawArgs);
|
|
1892
|
+
warnUnknownQueryOptions(ctx.log, modelName(ctx, mm), op, rawArgs);
|
|
1794
1893
|
return translateRaw();
|
|
1795
1894
|
};
|
|
1796
1895
|
const batchable = batch
|
|
@@ -1857,10 +1956,15 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1857
1956
|
});
|
|
1858
1957
|
},
|
|
1859
1958
|
createMany: (args) => defer('createMany', args, () => {
|
|
1959
|
+
// Prisma takes one row object or an array of them. Anything else used
|
|
1960
|
+
// to become zero rows, so the call "succeeded" with `count: 0`.
|
|
1860
1961
|
const data = args.data;
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
:
|
|
1962
|
+
const list = Array.isArray(data) ? data : isPlainRecord(data) ? [data] : undefined;
|
|
1963
|
+
if (!list) {
|
|
1964
|
+
throw new ValidationError(`prisma-compat: createMany on model "${modelName(ctx, mm)}" requires \`data\`, a row object or an ` +
|
|
1965
|
+
`array of them, received ${describeValue(data)}.`);
|
|
1966
|
+
}
|
|
1967
|
+
const rows = list.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)));
|
|
1864
1968
|
const t = { data: rows };
|
|
1865
1969
|
applyNativeOptions(CREATE_MANY_OPTIONS, args, t);
|
|
1866
1970
|
if (args.skipDuplicates)
|
|
@@ -1898,7 +2002,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1898
2002
|
// `optimisticLock.field` is a FIELD NAME, so it is renamed rather than
|
|
1899
2003
|
// copied: a blind passthrough would send the Prisma spelling into core
|
|
1900
2004
|
// and break on any model whose column is `@map`ped.
|
|
1901
|
-
if (
|
|
2005
|
+
if (isPlainRecord(a.optimisticLock)) {
|
|
1902
2006
|
t.optimisticLock = {
|
|
1903
2007
|
...a.optimisticLock,
|
|
1904
2008
|
field: renameField(mm, String(a.optimisticLock.field)),
|
|
@@ -1974,13 +2078,31 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1974
2078
|
execInTx: async (table, t) => shape(await table(mm.table).upsert(t)),
|
|
1975
2079
|
});
|
|
1976
2080
|
},
|
|
1977
|
-
count: (args = {}) =>
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2081
|
+
count: (args = {}) => {
|
|
2082
|
+
const a = args;
|
|
2083
|
+
// `count({ select })` answers per field (`{ _all, title }`, a non-null
|
|
2084
|
+
// count per field), which is core's aggregate `_count` record form.
|
|
2085
|
+
if (isPlainRecord(a.select)) {
|
|
2086
|
+
const countOf = (raw) => reshapeAggregate(ctx, mm, raw)._count;
|
|
2087
|
+
return defer('count', args, () => {
|
|
2088
|
+
refusePagedAggregate('count', a);
|
|
2089
|
+
const t = {};
|
|
2090
|
+
applyNativeOptions(AGGREGATE_OPTIONS, a, t);
|
|
2091
|
+
if (a.where !== undefined)
|
|
2092
|
+
t.where = translateWhere(ctx, mm, a.where);
|
|
2093
|
+
t._count = renameAggBlock(mm, a.select, true);
|
|
2094
|
+
return t;
|
|
2095
|
+
}, (qi, t) => qi.aggregate(t).then(countOf), { build: (qi, t) => qi.buildAggregate(t), reshape: countOf });
|
|
2096
|
+
}
|
|
2097
|
+
return defer('count', args, () => {
|
|
2098
|
+
refusePagedAggregate('count', a);
|
|
2099
|
+
const t = {};
|
|
2100
|
+
applyNativeOptions(COUNT_OPTIONS, a, t);
|
|
2101
|
+
if (a.where !== undefined)
|
|
2102
|
+
t.where = translateWhere(ctx, mm, a.where);
|
|
2103
|
+
return t;
|
|
2104
|
+
}, (qi, t) => qi.count(t), { build: (qi, t) => qi.buildCount(t), reshape: (raw) => raw });
|
|
2105
|
+
},
|
|
1984
2106
|
aggregate: (args) => defer('aggregate', args, () => translateAggregateArgs(ctx, mm, args, false), (qi, t) => qi.aggregate(t).then((r) => reshapeAggregate(ctx, mm, r)), { build: (qi, t) => qi.buildAggregate(t), reshape: (raw) => reshapeAggregate(ctx, mm, raw) }),
|
|
1985
2107
|
groupBy: (args) => defer('groupBy', args, () => translateAggregateArgs(ctx, mm, args, true), (qi, t) => qi.groupBy(t).then((rows) => rows.map((r) => reshapeGroupRow(ctx, mm, r))), {
|
|
1986
2108
|
build: (qi, t) => qi.buildGroupBy(t),
|
|
@@ -2095,8 +2217,9 @@ const JUNCTION_WARN_NS = 'prismaCompatJunction';
|
|
|
2095
2217
|
* `$transaction` must not turn the transaction surface into a delegate).
|
|
2096
2218
|
*
|
|
2097
2219
|
* Every skip except the mapped-table one loses a capability the caller might
|
|
2098
|
-
* expect, so those warn once per junction name
|
|
2099
|
-
* already maps to is not a loss (the model IS
|
|
2220
|
+
* expect, so those warn once per junction name (a construction warning, so
|
|
2221
|
+
* always on); a table that a model already maps to is not a loss (the model IS
|
|
2222
|
+
* that table's accessor).
|
|
2100
2223
|
*/
|
|
2101
2224
|
function junctionModels(ctx, map, tableToModel) {
|
|
2102
2225
|
const out = [];
|
|
@@ -2117,16 +2240,16 @@ function junctionModels(ctx, map, tableToModel) {
|
|
|
2117
2240
|
if (rel.type !== 'manyToMany')
|
|
2118
2241
|
continue;
|
|
2119
2242
|
const name = rel.through?.table;
|
|
2120
|
-
if (!name || !ctx.schema.tables
|
|
2243
|
+
if (!name || !ownLookup(ctx.schema.tables, name) || tableToModel.has(name))
|
|
2121
2244
|
continue;
|
|
2122
2245
|
if (registered.has(name))
|
|
2123
2246
|
continue;
|
|
2124
2247
|
if (seen.has(name)) {
|
|
2125
|
-
if (
|
|
2126
|
-
|
|
2248
|
+
if (shouldWarnOnce(JUNCTION_WARN_NS, name)) {
|
|
2249
|
+
logTo(ctx.log.logger, 'warn', `prisma-compat: the many-to-many junction table "${name}" collides with an ` +
|
|
2127
2250
|
'existing client member of the same name, so no junction accessor was created for it. ' +
|
|
2128
2251
|
'Reach its rows through the owning model relation (a nested connect / disconnect / set on the ' +
|
|
2129
|
-
'related model), or through $queryRaw / $executeRaw.');
|
|
2252
|
+
'related model), or through $queryRaw / $executeRaw.', { code: 'prisma-compat-junction-collision', table: name });
|
|
2130
2253
|
}
|
|
2131
2254
|
continue;
|
|
2132
2255
|
}
|
|
@@ -2261,15 +2384,19 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
2261
2384
|
// the real client satisfies it structurally (the cast just relaxes the strict
|
|
2262
2385
|
// callback-param variance on `$transaction`).
|
|
2263
2386
|
const db = client;
|
|
2264
|
-
//
|
|
2265
|
-
//
|
|
2266
|
-
|
|
2387
|
+
// Log through the wrapped client's logger and diagnostics. A stand-in client
|
|
2388
|
+
// without them (a test stub) gets the console and the environment's mode.
|
|
2389
|
+
const log = client[CLIENT_LOG_CONTEXT] ?? { logger: consoleLogger, diagnostics: resolveDiagnostics() };
|
|
2390
|
+
// Fire-and-forget, once per process: is this map still describing the schema
|
|
2391
|
+
// on disk? Nothing else in the system ever asks.
|
|
2392
|
+
warnStalePrismaMap(map, log);
|
|
2267
2393
|
const tableToModel = new Map();
|
|
2268
2394
|
for (const [prismaModel, mm] of Object.entries(map.models))
|
|
2269
2395
|
tableToModel.set(mm.table, prismaModel);
|
|
2270
2396
|
const ctx = {
|
|
2271
2397
|
map,
|
|
2272
2398
|
schema: db.schema,
|
|
2399
|
+
log,
|
|
2273
2400
|
tableToModel,
|
|
2274
2401
|
lookups: new Map(),
|
|
2275
2402
|
options: {
|
|
@@ -2361,7 +2488,15 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
2361
2488
|
}
|
|
2362
2489
|
const base = {
|
|
2363
2490
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2364
|
-
$transaction: ((arg,
|
|
2491
|
+
$transaction: ((arg, rawTxOptions) => {
|
|
2492
|
+
// Rejected, never thrown: Prisma's $transaction is always thenable.
|
|
2493
|
+
let txOptions;
|
|
2494
|
+
try {
|
|
2495
|
+
txOptions = resolveTxOptions(ctx.log, rawTxOptions);
|
|
2496
|
+
}
|
|
2497
|
+
catch (err) {
|
|
2498
|
+
return Promise.reject(decorate(err, ctx.options.prismaErrorCodes));
|
|
2499
|
+
}
|
|
2365
2500
|
// Array (lazy batch) form. Wrapped so validation/build errors REJECT the
|
|
2366
2501
|
// returned promise (Prisma's $transaction is always thenable) rather than
|
|
2367
2502
|
// throwing synchronously.
|
|
@@ -2388,7 +2523,7 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
2388
2523
|
}, txOptions);
|
|
2389
2524
|
}
|
|
2390
2525
|
const deferreds = batchables.map((b) => b.build());
|
|
2391
|
-
const results = (await db.$transaction(deferreds));
|
|
2526
|
+
const results = (await db.$transaction(deferreds, txOptions));
|
|
2392
2527
|
return results.map((raw, i) => batchables[i].reshape(raw));
|
|
2393
2528
|
}
|
|
2394
2529
|
catch (err) {
|
|
@@ -2401,10 +2536,10 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
2401
2536
|
return db.$transaction((tx) => {
|
|
2402
2537
|
const txDelegates = {};
|
|
2403
2538
|
for (const [prismaModel, mm] of delegateModels) {
|
|
2404
|
-
txDelegates
|
|
2539
|
+
setOwn(txDelegates, prismaModel, extendDelegate(makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n))), prismaModel, exts));
|
|
2405
2540
|
const alias = prismaPropertyAlias(prismaModel);
|
|
2406
|
-
if (alias && !(
|
|
2407
|
-
txDelegates
|
|
2541
|
+
if (alias && !Object.hasOwn(map.models, alias) && !Object.hasOwn(txDelegates, alias)) {
|
|
2542
|
+
setOwn(txDelegates, alias, ownLookup(txDelegates, prismaModel));
|
|
2408
2543
|
}
|
|
2409
2544
|
}
|
|
2410
2545
|
// Raw SQL on the transaction's own connection. Prisma's tx client
|
|
@@ -2431,13 +2566,13 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
2431
2566
|
// key from the map, so no dynamic-access proxy is needed.
|
|
2432
2567
|
const result = { ...base };
|
|
2433
2568
|
for (const [prismaModel, delegate] of delegates)
|
|
2434
|
-
result
|
|
2569
|
+
setOwn(result, prismaModel, delegate);
|
|
2435
2570
|
// Prisma-spelling aliases (`prisma.user` for `model User`). Skipped when the
|
|
2436
2571
|
// lowercased name is itself a model or already taken (never shadow a real key).
|
|
2437
2572
|
for (const [prismaModel, delegate] of delegates) {
|
|
2438
2573
|
const alias = prismaPropertyAlias(prismaModel);
|
|
2439
|
-
if (alias && !(alias
|
|
2440
|
-
result
|
|
2574
|
+
if (alias && !Object.hasOwn(result, alias))
|
|
2575
|
+
setOwn(result, alias, delegate);
|
|
2441
2576
|
}
|
|
2442
2577
|
// Extension `client` members last: every name was checked against the real
|
|
2443
2578
|
// client keys in applyExtension, so this can never overwrite a delegate.
|