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/query/filters.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* and execution rather than filter-shape bookkeeping.
|
|
7
7
|
*/
|
|
8
8
|
import { ValidationError } from '../errors.js';
|
|
9
|
+
import { isColumnRef, isPlainColumnRefShape, plainColumnRefMessage } from './column-ref.js';
|
|
9
10
|
import { assertDirectionToken, assertOrderDirection } from './types.js';
|
|
10
11
|
import { OPERATOR_KEYS, resolveColumnName } from './utils.js';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -24,19 +25,40 @@ export function isWhereOperator(value) {
|
|
|
24
25
|
return keys.length > 0 && keys.every((k) => OPERATOR_KEYS.has(k));
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* Class instances
|
|
30
|
-
*
|
|
28
|
+
* THE plain-object test for argument trees: an object literal (or a
|
|
29
|
+
* prototype-less object), never an array, Date, Buffer, typed array or other
|
|
30
|
+
* class instance. Class instances are values a driver binds (a Decimal, an
|
|
31
|
+
* interval wrapper), so no walker over caller arguments may descend into one.
|
|
31
32
|
*/
|
|
32
|
-
export function
|
|
33
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)
|
|
34
|
-
return false;
|
|
35
|
-
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
|
|
33
|
+
export function isPlainRecord(value) {
|
|
34
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
36
35
|
return false;
|
|
37
36
|
const proto = Object.getPrototypeOf(value);
|
|
38
37
|
return proto === Object.prototype || proto === null;
|
|
39
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* `target[key] = value`, always as an OWN property. A plain assignment of the
|
|
41
|
+
* key `__proto__` replaces the object's prototype instead, so a caller key
|
|
42
|
+
* spelled that way (`JSON.parse` produces it as an ordinary own key) would
|
|
43
|
+
* vanish from a rebuilt argument object rather than reach the check that
|
|
44
|
+
* refuses it, and a filter would silently stop filtering.
|
|
45
|
+
*/
|
|
46
|
+
export function setOwn(target, key, value) {
|
|
47
|
+
if (key === '__proto__') {
|
|
48
|
+
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
target[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* {@link isPlainRecord} read at an equality fallthrough: a plain object that
|
|
56
|
+
* matched no known filter shape is a misspelled operator, while a class
|
|
57
|
+
* instance (Buffer for bytea, a Decimal wrapper) is a legitimate bind value.
|
|
58
|
+
*/
|
|
59
|
+
export function isUnmatchedPlainObject(value) {
|
|
60
|
+
return isPlainRecord(value);
|
|
61
|
+
}
|
|
40
62
|
/**
|
|
41
63
|
* Operator keys that accept a {@link ColumnRef} (`{ col: 'otherField' }`)
|
|
42
64
|
* value for column-to-column comparison. `in`/`notIn` and the LIKE operators
|
|
@@ -47,54 +69,409 @@ export const COLUMN_REF_OPERATORS = new Set(['equals', 'not', 'gt', 'gte', 'lt',
|
|
|
47
69
|
* THE relation-filter wrappers: the keys whose body is a clause against the
|
|
48
70
|
* relation's TARGET table rather than against this one.
|
|
49
71
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `RELATION_QUANTIFIERS`
|
|
57
|
-
* `k === 'is' || k === 'isNot'` tests, which is the copy most likely to drift
|
|
58
|
-
* because half of it does not read as a list and a grep for the list does not
|
|
59
|
-
* find it.
|
|
60
|
-
*
|
|
61
|
-
* The failure mode is not cosmetic: a wrapper the SQL compiler treats as a
|
|
62
|
-
* relation filter but a WALKER does not is a wrapper whose body reaches the
|
|
63
|
-
* builder unwalked. That is precisely the operand-position channel this
|
|
64
|
-
* release closed elsewhere.
|
|
72
|
+
* Everything that walks a where clause has to agree about them: a wrapper the
|
|
73
|
+
* SQL compiler treats as a relation filter but a walker does not is a wrapper
|
|
74
|
+
* whose body reaches the builder unwalked. The SQL walk (`walkWhere` in
|
|
75
|
+
* where-compile.ts), the bare to-one normalization (`normalizeRelationFilter`
|
|
76
|
+
* in where.ts) and the PII predicate guard (`cli/pii-predicate-guard.ts`) all
|
|
77
|
+
* read this list. `prisma-compat.ts` still carries its own copy
|
|
78
|
+
* (`RELATION_QUANTIFIERS` plus inline `is` / `isNot` tests).
|
|
65
79
|
*
|
|
66
80
|
* `query/` is the right home rather than `cli/` because the direction of the
|
|
67
81
|
* dependency is fixed: `cli/` and the prisma-compat shim may import from the
|
|
68
82
|
* query path, and the query path may never import from `cli/`
|
|
69
83
|
* (`scripts/check-import-cycles.mjs`).
|
|
70
|
-
*
|
|
71
|
-
* ## STATUS: a landing spot, not yet a deduplication. Read this before trusting it.
|
|
72
|
-
*
|
|
73
|
-
* NOTHING IMPORTS THIS YET. The three copies listed above are all still in
|
|
74
|
-
* place and still hand-synced; declaring the canonical home did not by itself
|
|
75
|
-
* move any of them onto it. It is kept, rather than deleted as unused, because
|
|
76
|
-
* `cli/pii-predicate-guard.ts` names this module as where its own copy wants to
|
|
77
|
-
* go, and deleting the destination is the one change that makes converging
|
|
78
|
-
* harder. Wiring the copies up is a separate change: each one sits on a
|
|
79
|
-
* different walk, so each has to be re-tested on its own.
|
|
80
|
-
*
|
|
81
|
-
* So do not read this constant as evidence that the walkers agree. The comment
|
|
82
|
-
* on each copy is still the only thing holding them in step.
|
|
83
84
|
*/
|
|
84
85
|
export const RELATION_FILTER_WRAPPERS = ['some', 'none', 'every', 'is', 'isNot'];
|
|
85
86
|
/** {@link RELATION_FILTER_WRAPPERS} as a membership set, for the walkers. */
|
|
86
87
|
export const RELATION_FILTER_WRAPPER_SET = new Set(RELATION_FILTER_WRAPPERS);
|
|
88
|
+
// The column-reference check lives with its constructor; re-exported so the
|
|
89
|
+
// where modules keep one import site for every operand-shape guard.
|
|
90
|
+
export { isColumnRef } from './column-ref.js';
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// `undefined` means absent
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
/**
|
|
95
|
+
* Recursion bound for nested `not` operator objects. Past it the operand walk
|
|
96
|
+
* refuses the filter by name instead of recursing further.
|
|
97
|
+
*/
|
|
98
|
+
const OPERATOR_MAX_DEPTH = 64;
|
|
87
99
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
100
|
+
* `value` without its `undefined`-valued keys, one level deep. Returns the
|
|
101
|
+
* input BY REFERENCE when there are none, so the common path allocates
|
|
102
|
+
* nothing. Symbol-keyed properties (internal brands) survive a copy.
|
|
103
|
+
*
|
|
104
|
+
* This is the where walkers' backstop only. The args seam
|
|
105
|
+
* (`pruneUndefinedArgs` in normalize-args.ts) is the authority on `undefined`:
|
|
106
|
+
* it removes undefined keys at every where position and objects that became
|
|
107
|
+
* empty because of them. The walkers see what the seam missed (internal
|
|
108
|
+
* callers below it), skip a key whose value is `undefined`, and never look
|
|
109
|
+
* inside a value, so a JSON operand is data however it is shaped.
|
|
110
|
+
*/
|
|
111
|
+
export function dropUndefinedKeys(value) {
|
|
112
|
+
const keys = Object.keys(value);
|
|
113
|
+
for (const key of keys) {
|
|
114
|
+
if (value[key] !== undefined)
|
|
115
|
+
continue;
|
|
116
|
+
const out = copySymbols(value);
|
|
117
|
+
for (const k of keys)
|
|
118
|
+
if (value[k] !== undefined)
|
|
119
|
+
setOwn(out, k, value[k]);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
/** A fresh object carrying `source`'s own symbol-keyed properties (brands). */
|
|
125
|
+
function copySymbols(source) {
|
|
126
|
+
const out = Object.getPrototypeOf(source) === null ? Object.create(null) : {};
|
|
127
|
+
for (const sym of Object.getOwnPropertySymbols(source)) {
|
|
128
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
|
|
129
|
+
if (descriptor)
|
|
130
|
+
Object.defineProperty(out, sym, descriptor);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** {@link OperandColumnKind} for a column type token (case-insensitive). */
|
|
135
|
+
export function operandColumnKind(columnType) {
|
|
136
|
+
if (columnType === undefined)
|
|
137
|
+
return 'scalar';
|
|
138
|
+
const t = columnType.toLowerCase();
|
|
139
|
+
if (t === 'json' || t === 'jsonb')
|
|
140
|
+
return 'json';
|
|
141
|
+
if (t.startsWith('_'))
|
|
142
|
+
return 'array';
|
|
143
|
+
if (t === 'vector')
|
|
144
|
+
return 'vector';
|
|
145
|
+
return 'scalar';
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Built-in containers whose instances serialize to `{}` (or nothing useful)
|
|
149
|
+
* when a driver JSON-encodes them, so binding one compares against `'{}'`.
|
|
92
150
|
*/
|
|
93
|
-
|
|
94
|
-
|
|
151
|
+
const NON_SCALAR_BUILTINS = [
|
|
152
|
+
Map,
|
|
153
|
+
Set,
|
|
154
|
+
WeakMap,
|
|
155
|
+
WeakSet,
|
|
156
|
+
Promise,
|
|
157
|
+
RegExp,
|
|
158
|
+
Error,
|
|
159
|
+
];
|
|
160
|
+
/**
|
|
161
|
+
* A class instance the driver can bind as one value: one that says how to
|
|
162
|
+
* render itself (`toPostgres`, `toJSON`, or its own `toString`, as a Decimal
|
|
163
|
+
* or an interval wrapper does), and is not a built-in container.
|
|
164
|
+
*/
|
|
165
|
+
function isBindableInstance(value) {
|
|
166
|
+
if (NON_SCALAR_BUILTINS.some((ctor) => value instanceof ctor))
|
|
167
|
+
return false;
|
|
168
|
+
const v = value;
|
|
169
|
+
return (typeof v.toPostgres === 'function' ||
|
|
170
|
+
typeof v.toJSON === 'function' ||
|
|
171
|
+
(typeof v.toString === 'function' && v.toString !== Object.prototype.toString));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A value the driver binds as ONE scalar: a primitive, a Date, a Buffer or
|
|
175
|
+
* typed array, or a class instance that renders itself (see
|
|
176
|
+
* {@link isBindableInstance}). A plain object, an array, a Map or Set is not,
|
|
177
|
+
* and neither is a function, a symbol or `undefined`.
|
|
178
|
+
*/
|
|
179
|
+
function isBindableScalar(value) {
|
|
180
|
+
switch (typeof value) {
|
|
181
|
+
case 'string':
|
|
182
|
+
case 'number':
|
|
183
|
+
case 'bigint':
|
|
184
|
+
case 'boolean':
|
|
185
|
+
return true;
|
|
186
|
+
case 'object':
|
|
187
|
+
break;
|
|
188
|
+
default:
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (value === null || Array.isArray(value))
|
|
192
|
+
return false;
|
|
193
|
+
// An Invalid Date has no SQL value: drivers send it as `NaN` text, a server
|
|
194
|
+
// error, a thrown RangeError or, inside a JSON-encoded list, `null`, which
|
|
195
|
+
// matched nothing and so negated to every row.
|
|
196
|
+
if (value instanceof Date)
|
|
197
|
+
return !Number.isNaN(value.getTime());
|
|
198
|
+
if (ArrayBuffer.isView(value))
|
|
199
|
+
return true;
|
|
200
|
+
if (isColumnRef(value) || isPlainRecord(value))
|
|
201
|
+
return false;
|
|
202
|
+
return isBindableInstance(value);
|
|
203
|
+
}
|
|
204
|
+
/** A non-null value operand that fits a column of `kind`. */
|
|
205
|
+
function fitsColumn(value, kind) {
|
|
206
|
+
if (isBindableScalar(value))
|
|
207
|
+
return true;
|
|
208
|
+
if (kind === 'json') {
|
|
209
|
+
if (value === undefined || typeof value === 'function' || typeof value === 'symbol')
|
|
210
|
+
return false;
|
|
211
|
+
if (value instanceof Date)
|
|
212
|
+
return false;
|
|
213
|
+
return !(typeof value === 'object' && value !== null && NON_SCALAR_BUILTINS.some((c) => value instanceof c));
|
|
214
|
+
}
|
|
215
|
+
if (kind === 'array' || kind === 'vector') {
|
|
216
|
+
return Array.isArray(value) && value.every((el) => el === null || isBindableScalar(el) || Array.isArray(el));
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
/** Short, value-free description of an operand for an error message. */
|
|
221
|
+
export function describeOperand(value) {
|
|
222
|
+
if (value === null)
|
|
223
|
+
return 'null';
|
|
224
|
+
if (value === undefined)
|
|
225
|
+
return 'undefined';
|
|
226
|
+
if (Array.isArray(value))
|
|
227
|
+
return 'an array';
|
|
228
|
+
if (isColumnRef(value))
|
|
229
|
+
return 'a column reference';
|
|
230
|
+
if (value instanceof Date && Number.isNaN(value.getTime()))
|
|
231
|
+
return 'an Invalid Date';
|
|
232
|
+
if (isPlainRecord(value)) {
|
|
233
|
+
const keys = Object.keys(value);
|
|
234
|
+
return keys.length === 0 ? 'an empty object' : `an object with keys ${keys.map((k) => `"${k}"`).join(', ')}`;
|
|
235
|
+
}
|
|
236
|
+
if (typeof value === 'object') {
|
|
237
|
+
const name = value.constructor?.name;
|
|
238
|
+
return typeof name === 'string' && name.length > 0 ? `a ${name} instance` : 'an object';
|
|
239
|
+
}
|
|
240
|
+
return `a ${typeof value}`;
|
|
241
|
+
}
|
|
242
|
+
const SCALAR_WORDS = 'a string, number, bigint, boolean, Date or Buffer';
|
|
243
|
+
function acceptedFor(kind) {
|
|
244
|
+
if (kind === 'array')
|
|
245
|
+
return `an array (or ${SCALAR_WORDS})`;
|
|
246
|
+
if (kind === 'vector')
|
|
247
|
+
return 'an array of numbers';
|
|
248
|
+
if (kind === 'json')
|
|
249
|
+
return 'a JSON value';
|
|
250
|
+
return SCALAR_WORDS;
|
|
251
|
+
}
|
|
252
|
+
/** `on "<column>" for table "<table>"`, for refusal messages. Built only on the throw path. */
|
|
253
|
+
function operandSite(ctx) {
|
|
254
|
+
return `on "${ctx.column}" for table "${ctx.table}"`;
|
|
255
|
+
}
|
|
256
|
+
function refuseOperand(key, value, ctx, accepted) {
|
|
257
|
+
throw new ValidationError(`Invalid operand for '${key}' ${operandSite(ctx)}: received ${describeOperand(value)}. '${key}' takes ${accepted}.`);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Validate every operand of an operator object against its column, and throw
|
|
261
|
+
* {@link ValidationError} (E003) naming the column, the operator, what was
|
|
262
|
+
* received and what is accepted.
|
|
263
|
+
*
|
|
264
|
+
* An operator object must state at least one comparison. One made only of
|
|
265
|
+
* `mode` (or of operators whose value is `undefined`) compares nothing, so it
|
|
266
|
+
* is refused rather than compiled to no predicate: inside a relation filter
|
|
267
|
+
* that silence turned `deleteMany` into a delete of every row. The args seam
|
|
268
|
+
* removes the object when `undefined` emptied it, so Prisma's optional-search
|
|
269
|
+
* idiom (`{ contains: q, mode: 'insensitive' }` with `q` undefined) still
|
|
270
|
+
* reads as "no filter".
|
|
271
|
+
*
|
|
272
|
+
* Called by the SQL build AND the cache-hit param collect with the same
|
|
273
|
+
* arguments, so a warmed template can never bind an operand the cold build
|
|
274
|
+
* refuses. Recurses into a `not` operator object, bounded by `depth`. Runs on
|
|
275
|
+
* every operator object of every query, so it reads only the keys present and
|
|
276
|
+
* builds a message only when it throws.
|
|
277
|
+
*/
|
|
278
|
+
export function assertOperatorOperands(op, ctx, depth = 0) {
|
|
279
|
+
if (depth > OPERATOR_MAX_DEPTH) {
|
|
280
|
+
throw new ValidationError(`\`not\` operators on "${ctx.column}" nest too deeply; flatten the filter.`);
|
|
281
|
+
}
|
|
282
|
+
const mode = op.mode;
|
|
283
|
+
if (mode !== undefined && mode !== 'default' && mode !== 'insensitive') {
|
|
284
|
+
throw new ValidationError(`Invalid mode ${typeof mode === 'string' ? JSON.stringify(mode) : describeOperand(mode)} ${operandSite(ctx)}. ` +
|
|
285
|
+
"Use 'insensitive' or 'default'.");
|
|
286
|
+
}
|
|
287
|
+
// The classifier sends only operator keys here; `mode` was checked above.
|
|
288
|
+
let comparisons = 0;
|
|
289
|
+
for (const key of Object.keys(op)) {
|
|
290
|
+
const value = op[key];
|
|
291
|
+
if (value === undefined)
|
|
292
|
+
continue;
|
|
293
|
+
switch (key) {
|
|
294
|
+
case 'equals':
|
|
295
|
+
case 'not':
|
|
296
|
+
comparisons++;
|
|
297
|
+
if (value !== null && typeof value === 'object')
|
|
298
|
+
assertObjectEqualityOperand(key, value, ctx, depth);
|
|
299
|
+
else if (value !== null && !fitsColumn(value, ctx.kind)) {
|
|
300
|
+
refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)}, null, or col('field')`);
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
case 'gt':
|
|
304
|
+
case 'gte':
|
|
305
|
+
case 'lt':
|
|
306
|
+
case 'lte':
|
|
307
|
+
comparisons++;
|
|
308
|
+
if (value === null) {
|
|
309
|
+
refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)} or col('field'); a comparison with null matches no row, use equals: null or not: null`);
|
|
310
|
+
}
|
|
311
|
+
if (isColumnRef(value))
|
|
312
|
+
break;
|
|
313
|
+
if (ctx.kind !== 'json' && isPlainColumnRefShape(value)) {
|
|
314
|
+
throw new ValidationError(plainColumnRefMessage(value, key, ctx.column, ctx.table));
|
|
315
|
+
}
|
|
316
|
+
if (!fitsColumn(value, ctx.kind))
|
|
317
|
+
refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)} or col('field')`);
|
|
318
|
+
break;
|
|
319
|
+
case 'in':
|
|
320
|
+
case 'notIn':
|
|
321
|
+
comparisons++;
|
|
322
|
+
assertListOperand(key, value, ctx);
|
|
323
|
+
break;
|
|
324
|
+
case 'contains':
|
|
325
|
+
case 'startsWith':
|
|
326
|
+
case 'endsWith':
|
|
327
|
+
comparisons++;
|
|
328
|
+
if (typeof value !== 'string')
|
|
329
|
+
refuseOperand(key, value, ctx, 'a string');
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (comparisons === 0) {
|
|
334
|
+
throw new ValidationError(`The ${depth > 0 ? '`not` operator object' : 'filter'} ${operandSite(ctx)} names no operator: received ` +
|
|
335
|
+
`${describeOperand(op)}. Add one (for example { contains: 'x', mode: 'insensitive' }), or leave ` +
|
|
336
|
+
`"${ctx.column}" out of the filter to apply no condition to it.`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** An object at `equals` / `not`: a column reference, or a `not` operator object to negate. */
|
|
340
|
+
function assertObjectEqualityOperand(key, value, ctx, depth) {
|
|
341
|
+
if (isColumnRef(value)) {
|
|
342
|
+
if (ctx.kind === 'json') {
|
|
343
|
+
refuseOperand(key, value, ctx, `${acceptedFor('json')}; column references are not supported on JSON columns`);
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (ctx.kind === 'json') {
|
|
348
|
+
if (key === 'not')
|
|
349
|
+
assertJsonNotOperand(value, ctx);
|
|
350
|
+
if (!fitsColumn(value, 'json'))
|
|
351
|
+
refuseOperand(key, value, ctx, `${acceptedFor('json')} or null`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (isPlainColumnRefShape(value)) {
|
|
355
|
+
throw new ValidationError(plainColumnRefMessage(value, key, ctx.column, ctx.table));
|
|
356
|
+
}
|
|
357
|
+
if (key === 'not' && isPlainRecord(value)) {
|
|
358
|
+
if (Object.keys(value).length === 0) {
|
|
359
|
+
refuseOperand(key, value, ctx, `a value, null, a column reference or an operator object such as { contains: 'x' }`);
|
|
360
|
+
}
|
|
361
|
+
if (!isWhereOperator(value)) {
|
|
362
|
+
refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)}, null, col('field'), or an operator object`);
|
|
363
|
+
}
|
|
364
|
+
assertOperatorOperands(value, ctx, depth + 1);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (!fitsColumn(value, ctx.kind)) {
|
|
368
|
+
refuseOperand(key, value, ctx, `${acceptedFor(ctx.kind)}, null, or col('field')` +
|
|
369
|
+
(isPlainRecord(value) ? ' (an object is a JSON value, accepted only on json/jsonb columns)' : ''));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* On a JSON column `not` takes a JSON VALUE (`not: { a: 1 }` compares
|
|
374
|
+
* documents), so `not: { contains: {...} }` was compared as the literal document
|
|
375
|
+
* `{"contains": ...}` and matched nearly every row, while the same spelling on a
|
|
376
|
+
* text column negates the filter. An object made only of filter-operator keys
|
|
377
|
+
* is therefore refused, except `not: { equals: X }`, which negates the
|
|
378
|
+
* whole-document equality. A literal document with such keys stays expressible.
|
|
379
|
+
* Every engine runs it (PowDB too), so the spelling means one thing everywhere.
|
|
380
|
+
*/
|
|
381
|
+
export function assertJsonNotOperand(value, ctx) {
|
|
382
|
+
if (!isPlainRecord(value))
|
|
383
|
+
return;
|
|
384
|
+
const keys = Object.keys(value).filter((k) => value[k] !== undefined);
|
|
385
|
+
if (keys.length === 0 || (keys.length === 1 && keys[0] === 'equals'))
|
|
386
|
+
return;
|
|
387
|
+
if (!keys.every((k) => JSON_FILTER_KEYS.has(k) || OPERATOR_KEYS.has(k)))
|
|
388
|
+
return;
|
|
389
|
+
const named = keys.map((k) => `"${k}"`).join(', ');
|
|
390
|
+
throw new ValidationError(`Invalid \`not\` ${operandSite(ctx)}, a JSON column: an object whose keys are all filter operators (${named}) ` +
|
|
391
|
+
'would be compared as a literal JSON document and match nearly every row. To negate the filter, write ' +
|
|
392
|
+
`\`NOT: { ${ctx.column}: { ${keys[0]}: ... } }\`. To compare against a document that has these keys, ` +
|
|
393
|
+
'pass it as `not: jsonValue({ ... })` or `not: { equals: { ... } }`.');
|
|
394
|
+
}
|
|
395
|
+
/** `in` / `notIn`: an array whose elements fit the column (or are null). */
|
|
396
|
+
function assertListOperand(key, value, ctx) {
|
|
397
|
+
if (!Array.isArray(value))
|
|
398
|
+
refuseOperand(key, value, ctx, 'an array of values');
|
|
399
|
+
const elementKind = ctx.kind === 'array' ? 'array' : ctx.kind;
|
|
400
|
+
for (const element of value) {
|
|
401
|
+
if (element === null || fitsColumn(element, elementKind))
|
|
402
|
+
continue;
|
|
403
|
+
throw new ValidationError(`Invalid element in '${key}' ${operandSite(ctx)}: received ${describeOperand(element)}. ` +
|
|
404
|
+
`Every element must be ${acceptedFor(elementKind)} (or null).`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Validate a BARE value (implicit equality, `{ id: 5 }`) against its column.
|
|
409
|
+
* Shared by the build and collect paths of every where scope.
|
|
410
|
+
*/
|
|
411
|
+
export function assertEqualityOperand(value, ctx) {
|
|
412
|
+
if (value === null)
|
|
413
|
+
return;
|
|
414
|
+
const where = `on "${ctx.column}" for table "${ctx.table}"`;
|
|
415
|
+
if (isColumnRef(value)) {
|
|
416
|
+
throw new ValidationError(`A column reference ${where} needs an operator: write { equals: col(${JSON.stringify(value.col)}) }` +
|
|
417
|
+
(ctx.kind === 'json' ? ', on a non-JSON column (references are not supported on JSON columns).' : '.'));
|
|
418
|
+
}
|
|
419
|
+
if (ctx.kind === 'json') {
|
|
420
|
+
if (typeof value === 'function' || typeof value === 'symbol') {
|
|
421
|
+
throw new ValidationError(`Invalid value ${where}: received ${describeOperand(value)}. Pass a JSON value.`);
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (isPlainColumnRefShape(value)) {
|
|
426
|
+
throw new ValidationError(plainColumnRefMessage(value, 'equals', ctx.column, ctx.table));
|
|
427
|
+
}
|
|
428
|
+
if (isPlainRecord(value)) {
|
|
429
|
+
const keys = Object.keys(value);
|
|
430
|
+
throw new ValidationError(keys.length === 0
|
|
431
|
+
? `Empty filter object ${where}. Provide a value or an operator like { gt: 1 }.`
|
|
432
|
+
: `Unknown operator${keys.length > 1 ? 's' : ''} ${keys.map((k) => `"${k}"`).join(', ')} ${where}. ` +
|
|
433
|
+
`Supported operators: ${[...OPERATOR_KEYS].join(', ')}.`);
|
|
434
|
+
}
|
|
435
|
+
if (fitsColumn(value, ctx.kind))
|
|
436
|
+
return;
|
|
437
|
+
throw new ValidationError(`Invalid value ${where}: received ${describeOperand(value)}. A bare value takes ${acceptedFor(ctx.kind)}` +
|
|
438
|
+
(Array.isArray(value) ? '; to match any of several values use { in: [...] }.' : '.'));
|
|
439
|
+
}
|
|
440
|
+
/** `{ equals: v }` written as an object literal with that one key. */
|
|
441
|
+
function isEqualsWrapper(value) {
|
|
442
|
+
if (!isPlainRecord(value))
|
|
95
443
|
return false;
|
|
96
444
|
const keys = Object.keys(value);
|
|
97
|
-
return keys.length === 1 && keys[0] === '
|
|
445
|
+
return keys.length === 1 && keys[0] === 'equals';
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* One `cursor` field's value, checked and unwrapped. A cursor value is the
|
|
449
|
+
* value the last row holds in that column, so it takes the bare-equality rule
|
|
450
|
+
* (`{ equals: v }` is the same as `v`); an operator object, a list on a scalar
|
|
451
|
+
* column or a column reference is refused (E003) in terms of cursors, not
|
|
452
|
+
* filters. NULL is a row's value only in a column that can hold it: in a NOT
|
|
453
|
+
* NULL column (the primary key included) no row has it, so it is refused
|
|
454
|
+
* rather than placing the page nowhere. Shared by every engine's cursor path.
|
|
455
|
+
*/
|
|
456
|
+
export function cursorFieldValue(field, written, ctx) {
|
|
457
|
+
const value = isEqualsWrapper(written) ? written.equals : written;
|
|
458
|
+
const site = `\`cursor\` field "${field}" on table "${ctx.table}"`;
|
|
459
|
+
if (value === null) {
|
|
460
|
+
if (ctx.nullable)
|
|
461
|
+
return null;
|
|
462
|
+
throw new ValidationError(`Invalid ${site}: null. "${ctx.column}" is NOT NULL, so no row holds null there and the cursor names no ` +
|
|
463
|
+
"position. Pass the last row's value, or leave `cursor` out for the first page.");
|
|
464
|
+
}
|
|
465
|
+
const columnRef = isColumnRef(value) || isPlainColumnRefShape(value);
|
|
466
|
+
const operatorShaped = isPlainRecord(value) && ctx.kind !== 'json';
|
|
467
|
+
const list = Array.isArray(value) && ctx.kind === 'scalar';
|
|
468
|
+
if (columnRef || operatorShaped || list) {
|
|
469
|
+
throw new ValidationError(`Invalid ${site}: received ${describeOperand(value)}. A cursor value is the value the last row holds in ` +
|
|
470
|
+
`"${field}" (for example \`cursor: { ${field}: 42 }\`), written bare or as \`{ equals: 42 }\`. ` +
|
|
471
|
+
'To filter rows, use `where`.');
|
|
472
|
+
}
|
|
473
|
+
assertEqualityOperand(value, ctx);
|
|
474
|
+
return value;
|
|
98
475
|
}
|
|
99
476
|
/**
|
|
100
477
|
* Fingerprint the SHAPE of a where-operator object. Null-valued `equals` /
|
|
@@ -103,43 +480,35 @@ export function isColumnRef(value) {
|
|
|
103
480
|
* warmed by `{ not: 5 }` would serve `{ not: null }` with a desynced param list.
|
|
104
481
|
*
|
|
105
482
|
* Column references ({@link ColumnRef}) compile the referenced column into the
|
|
106
|
-
* SQL TEXT (no param bound), so the referenced field name is part of the
|
|
107
|
-
|
|
108
|
-
*
|
|
483
|
+
* SQL TEXT (no param bound), so the referenced field name is part of the
|
|
484
|
+
* shape: `{ equals: col('a') }` and `{ equals: col('b') }` must never share a
|
|
485
|
+
* cache entry. The name is JSON-encoded so exotic field names cannot collide
|
|
109
486
|
* with other fingerprint tokens.
|
|
487
|
+
*
|
|
488
|
+
* A `not` operator object compiles to `NOT (<inner>)`, so its whole inner shape
|
|
489
|
+
* is part of the token, with the inner object's EFFECTIVE mode (its own, else
|
|
490
|
+
* the inherited one). Keys holding `undefined` are skipped, as the builders skip them.
|
|
110
491
|
*/
|
|
111
|
-
export function fingerprintOperatorShape(value) {
|
|
492
|
+
export function fingerprintOperatorShape(value, inheritedMode, depth = 0) {
|
|
112
493
|
const obj = value;
|
|
494
|
+
const effectiveMode = obj.mode !== undefined ? obj.mode : inheritedMode;
|
|
113
495
|
const opKeys = Object.keys(obj)
|
|
114
|
-
.filter((k) => k !== 'mode')
|
|
496
|
+
.filter((k) => k !== 'mode' && obj[k] !== undefined)
|
|
115
497
|
.map((k) => {
|
|
116
498
|
const v = obj[k];
|
|
117
499
|
if ((k === 'equals' || k === 'not') && v === null)
|
|
118
500
|
return `${k}:null`;
|
|
119
501
|
if (COLUMN_REF_OPERATORS.has(k) && isColumnRef(v))
|
|
120
502
|
return `${k}:col(${JSON.stringify(v.col)})`;
|
|
503
|
+
if (k === 'not' && depth < OPERATOR_MAX_DEPTH && isWhereOperator(v)) {
|
|
504
|
+
return `not:${fingerprintOperatorShape(v, effectiveMode, depth + 1)}`;
|
|
505
|
+
}
|
|
121
506
|
return k;
|
|
122
507
|
})
|
|
123
508
|
.sort();
|
|
124
|
-
const modeStr =
|
|
509
|
+
const modeStr = effectiveMode === 'insensitive' ? ':i' : '';
|
|
125
510
|
return `op(${opKeys.join(',')}${modeStr})`;
|
|
126
511
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Guard for the value of an `equals` operator reaching the plain-equality
|
|
129
|
-
* operator path. A plain object literal can only legitimately be an equality
|
|
130
|
-
* value on a json/jsonb column, and those route to the JSONB filter branch
|
|
131
|
-
* BEFORE the operator branch, so any plain object that reaches here is a
|
|
132
|
-
* mistake (e.g. `{ equals: { foo: 1 } }` on a text column). Shared by the
|
|
133
|
-
* SQL-build path and the cache-hit param-collect path so a warmed cache can
|
|
134
|
-
* never skip the check.
|
|
135
|
-
*/
|
|
136
|
-
export function assertBindableEqualsOperand(value, column) {
|
|
137
|
-
if (!isUnmatchedPlainObject(value))
|
|
138
|
-
return;
|
|
139
|
-
throw new ValidationError(`Plain-object value for operator 'equals' on ${column}: ` +
|
|
140
|
-
`objects are only valid 'equals' values on JSON (json/jsonb) columns, ` +
|
|
141
|
-
`where 'equals' is the JSONB containment filter.`);
|
|
142
|
-
}
|
|
143
512
|
/**
|
|
144
513
|
* Object keys in sorted order, mirroring the canonical order used by every
|
|
145
514
|
* cache fingerprint. The SQL-build and cache-hit param-collect paths MUST
|
|
@@ -296,7 +665,9 @@ export const ARRAY_UNIQUE_KEYS = new Set(['has', 'hasEvery', 'hasSome', 'isEmpty
|
|
|
296
665
|
* concrete value is part of the shape; the other operators are value-invariant.
|
|
297
666
|
*/
|
|
298
667
|
export function fingerprintArrayFilterShape(filter) {
|
|
299
|
-
const keys = Object.keys(filter)
|
|
668
|
+
const keys = Object.keys(filter)
|
|
669
|
+
.filter((k) => filter[k] !== undefined)
|
|
670
|
+
.sort();
|
|
300
671
|
const suffix = filter.isEmpty === undefined ? '' : `:empty=${filter.isEmpty ? 'true' : 'false'}`;
|
|
301
672
|
return `${keys.join(',')}${suffix}`;
|
|
302
673
|
}
|
|
@@ -640,24 +1011,34 @@ function assertDroppedDirection(key, value, table) {
|
|
|
640
1011
|
* has a meaning that degrades as the list grows (every added column makes the
|
|
641
1012
|
* result strictly less distinct, converging on the plain query), so a long list
|
|
642
1013
|
* is self-limiting in a way a long sort is not.
|
|
1014
|
+
*
|
|
1015
|
+
* Every element must be a field name: anything else (an `undefined` hole, a
|
|
1016
|
+
* number) is E003 here, the first place either caller reads the list.
|
|
643
1017
|
*/
|
|
644
|
-
export function dedupeColumnList(meta, columns) {
|
|
1018
|
+
export function dedupeColumnList(meta, columns, label = 'distinct') {
|
|
1019
|
+
for (let i = 0; i < columns.length; i++) {
|
|
1020
|
+
const key = columns[i];
|
|
1021
|
+
if (typeof key === 'string' && key.length > 0)
|
|
1022
|
+
continue;
|
|
1023
|
+
throw new ValidationError(`\`${label}\`${meta?.name ? ` on table "${meta.name}"` : ''} takes field names, but element ${i} is ` +
|
|
1024
|
+
`${key === undefined ? 'undefined' : describeOperand(key)}. Pass each field as a string, or leave the ` +
|
|
1025
|
+
`list out to apply no ${label}.`);
|
|
1026
|
+
}
|
|
645
1027
|
if (columns.length < 2)
|
|
646
1028
|
return null;
|
|
647
1029
|
const seen = new Map();
|
|
648
1030
|
let dropped;
|
|
649
1031
|
const kept = [];
|
|
650
1032
|
for (const key of columns) {
|
|
651
|
-
const column =
|
|
652
|
-
const first =
|
|
653
|
-
if (first !== undefined
|
|
1033
|
+
const column = resolvedOrderColumn(meta, key);
|
|
1034
|
+
const first = seen.get(column);
|
|
1035
|
+
if (first !== undefined) {
|
|
654
1036
|
if (dropped === undefined)
|
|
655
1037
|
dropped = [];
|
|
656
1038
|
dropped.push({ key, first, resolved: column });
|
|
657
1039
|
continue;
|
|
658
1040
|
}
|
|
659
|
-
|
|
660
|
-
seen.set(column, key);
|
|
1041
|
+
seen.set(column, key);
|
|
661
1042
|
kept.push(key);
|
|
662
1043
|
}
|
|
663
1044
|
return dropped ? { columns: kept, dropped } : null;
|
|
@@ -688,3 +1069,147 @@ export function isEmptyOrderBy(orderBy) {
|
|
|
688
1069
|
}
|
|
689
1070
|
return orderBy === undefined || orderBy === null;
|
|
690
1071
|
}
|
|
1072
|
+
// ---------------------------------------------------------------------------
|
|
1073
|
+
// JSON and array filter keys, and `undefined` inside a filter
|
|
1074
|
+
// ---------------------------------------------------------------------------
|
|
1075
|
+
/**
|
|
1076
|
+
* Refuse a {@link JsonFilter} carrying a key that is not a JSON operator, and
|
|
1077
|
+
* refuse a filter that selects a `path` but never compares it.
|
|
1078
|
+
*
|
|
1079
|
+
* Both shapes used to compile to NOTHING. `buildJsonFilterClauses` only ever
|
|
1080
|
+
* emitted a clause for a key it recognized, so `{ path: ['title'],
|
|
1081
|
+
* string_contains: 'x' }`, the Prisma spelling, and an easy typo besides -
|
|
1082
|
+
* produced an empty clause list, the predicate vanished, and the query
|
|
1083
|
+
* returned EVERY row. Inside an `AND` it silently dropped that conjunct, so a
|
|
1084
|
+
* tenant scope written this way widened to the whole table. The equivalent
|
|
1085
|
+
* typo on a scalar column has always thrown; this closes the inconsistency.
|
|
1086
|
+
*
|
|
1087
|
+
* Called from both the SQL-build path and the cache-hit param-collect path, so
|
|
1088
|
+
* a warmed SQL cache cannot skip the check.
|
|
1089
|
+
*/
|
|
1090
|
+
export function assertJsonFilterKeys(filter, column) {
|
|
1091
|
+
const obj = filter;
|
|
1092
|
+
const present = Object.keys(obj).filter((k) => obj[k] !== undefined);
|
|
1093
|
+
for (const key of present) {
|
|
1094
|
+
if (isColumnRef(obj[key])) {
|
|
1095
|
+
throw new ValidationError(`A column reference cannot be the '${key}' operand of a JSON filter on ${column}: on a json/jsonb ` +
|
|
1096
|
+
'column the operand is a JSON value. Compare the column with col() only on non-JSON columns.');
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
for (const key of present) {
|
|
1100
|
+
if (JSON_FILTER_KEYS.has(key))
|
|
1101
|
+
continue;
|
|
1102
|
+
const suggestion = JSON_PRISMA_SPELLINGS[key];
|
|
1103
|
+
throw new ValidationError(`Unknown JSON filter operator "${key}" on ${column}.` +
|
|
1104
|
+
(suggestion ? ` Did you mean \`${suggestion}\`?` : '') +
|
|
1105
|
+
` Supported operators: ${[...JSON_FILTER_KEYS].sort().join(', ')}.`);
|
|
1106
|
+
}
|
|
1107
|
+
// `mode` and `path` are modifiers, not comparisons: a filter made only of
|
|
1108
|
+
// those (or of keys holding `undefined`) compares nothing, which is exactly
|
|
1109
|
+
// the silent no-op shape above.
|
|
1110
|
+
if (present.length === 0) {
|
|
1111
|
+
throw new ValidationError(`JSON filter on ${column} names no comparison. Add one of: ${[...JSON_FILTER_KEYS]
|
|
1112
|
+
.filter((k) => k !== 'path' && k !== 'mode')
|
|
1113
|
+
.sort()
|
|
1114
|
+
.join(', ')}.`);
|
|
1115
|
+
}
|
|
1116
|
+
if (present.every((k) => k === 'path' || k === 'mode')) {
|
|
1117
|
+
throw new ValidationError(`JSON filter on ${column} selects a \`path\` but has no comparison. ` +
|
|
1118
|
+
`Add one of: ${[...JSON_FILTER_KEYS]
|
|
1119
|
+
.filter((k) => k !== 'path' && k !== 'mode')
|
|
1120
|
+
.sort()
|
|
1121
|
+
.join(', ')}.`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* The Prisma spelling of each JSON operator, so a migrator who writes the
|
|
1126
|
+
* name they already know gets told the turbine one instead of a bare list.
|
|
1127
|
+
*/
|
|
1128
|
+
const JSON_PRISMA_SPELLINGS = {
|
|
1129
|
+
string_contains: 'stringContains',
|
|
1130
|
+
string_starts_with: 'stringStartsWith',
|
|
1131
|
+
string_ends_with: 'stringEndsWith',
|
|
1132
|
+
array_contains: 'contains',
|
|
1133
|
+
startsWith: 'stringStartsWith',
|
|
1134
|
+
endsWith: 'stringEndsWith',
|
|
1135
|
+
};
|
|
1136
|
+
/**
|
|
1137
|
+
* Whether {@link findUndefinedPath} would find anything, without building the
|
|
1138
|
+
* path strings. Same walk, same leaves, same plain-object rule, same depth bound.
|
|
1139
|
+
*/
|
|
1140
|
+
export function holdsUndefinedValue(value, depth) {
|
|
1141
|
+
if (value === undefined)
|
|
1142
|
+
return true;
|
|
1143
|
+
if (typeof value !== 'object' || value === null || depth > 64)
|
|
1144
|
+
return false;
|
|
1145
|
+
if (Array.isArray(value)) {
|
|
1146
|
+
for (let i = 0; i < value.length; i++)
|
|
1147
|
+
if (holdsUndefinedValue(value[i], depth + 1))
|
|
1148
|
+
return true;
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
const proto = Object.getPrototypeOf(value);
|
|
1152
|
+
if (proto !== Object.prototype && proto !== null)
|
|
1153
|
+
return false;
|
|
1154
|
+
for (const key of Object.keys(value)) {
|
|
1155
|
+
if (holdsUndefinedValue(value[key], depth + 1))
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* The key path of the first `undefined` inside a where object (a global filter),
|
|
1162
|
+
* or `null`.
|
|
1163
|
+
* Walks plain objects and arrays; values of any other kind (Dates, Buffers,
|
|
1164
|
+
* column references) are leaves. Key NAMES only, never values, so the path is
|
|
1165
|
+
* safe in an error message.
|
|
1166
|
+
*/
|
|
1167
|
+
export function findUndefinedPath(value, path, depth) {
|
|
1168
|
+
if (value === undefined)
|
|
1169
|
+
return path || '(root)';
|
|
1170
|
+
if (typeof value !== 'object' || value === null || depth > 64)
|
|
1171
|
+
return null;
|
|
1172
|
+
if (Array.isArray(value)) {
|
|
1173
|
+
for (let i = 0; i < value.length; i++) {
|
|
1174
|
+
const hit = findUndefinedPath(value[i], `${path}[${i}]`, depth + 1);
|
|
1175
|
+
if (hit !== null)
|
|
1176
|
+
return hit;
|
|
1177
|
+
}
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
const proto = Object.getPrototypeOf(value);
|
|
1181
|
+
if (proto !== Object.prototype && proto !== null)
|
|
1182
|
+
return null;
|
|
1183
|
+
for (const key of Object.keys(value)) {
|
|
1184
|
+
const hit = findUndefinedPath(value[key], path ? `${path}.${key}` : key, depth + 1);
|
|
1185
|
+
if (hit !== null)
|
|
1186
|
+
return hit;
|
|
1187
|
+
}
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Refuse an {@link ArrayFilter} that would compile to less than it says: a key
|
|
1192
|
+
* that is not an array operator (it would be dropped, so `{ has: 'a', hsa: 'b' }`
|
|
1193
|
+
* filtered on `has` alone), an `isEmpty` that is not a boolean (it compiled to
|
|
1194
|
+
* nothing), or no comparison at all. Called by the SQL build and the cache-hit
|
|
1195
|
+
* param collect alike.
|
|
1196
|
+
*/
|
|
1197
|
+
export function assertArrayFilter(filter, column) {
|
|
1198
|
+
const obj = filter;
|
|
1199
|
+
let comparisons = 0;
|
|
1200
|
+
for (const key of Object.keys(obj)) {
|
|
1201
|
+
const value = obj[key];
|
|
1202
|
+
if (value === undefined)
|
|
1203
|
+
continue;
|
|
1204
|
+
if (!ARRAY_OPERATOR_KEYS.has(key)) {
|
|
1205
|
+
throw new ValidationError(`Unknown array filter operator "${key}" on ${column}. Supported operators: ${[...ARRAY_OPERATOR_KEYS].join(', ')}.`);
|
|
1206
|
+
}
|
|
1207
|
+
if (key === 'isEmpty' && typeof value !== 'boolean') {
|
|
1208
|
+
throw new ValidationError(`\`isEmpty\` on ${column} takes true or false, received ${describeOperand(value)}.`);
|
|
1209
|
+
}
|
|
1210
|
+
comparisons++;
|
|
1211
|
+
}
|
|
1212
|
+
if (comparisons === 0) {
|
|
1213
|
+
throw new ValidationError(`Array filter on ${column} names no comparison. Add one of: ${[...ARRAY_OPERATOR_KEYS].join(', ')}.`);
|
|
1214
|
+
}
|
|
1215
|
+
}
|