@cerbos/orm-prisma 4.0.0 → 4.1.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 +168 -12
- package/lib/index.d.ts +74 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +344 -90
- package/lib/index.js.map +1 -1
- package/package.json +10 -7
- package/src/adversarial.test.ts +948 -253
- package/src/corpus.ts +414 -0
- package/src/generated/prisma-adversarial/browser.ts +10 -0
- package/src/generated/prisma-adversarial/client.ts +10 -0
- package/src/generated/prisma-adversarial/internal/class.ts +24 -4
- package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +178 -2
- package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +27 -1
- package/src/generated/prisma-adversarial/models/AdversarialInner.ts +1369 -0
- package/src/generated/prisma-adversarial/models/AdversarialParent.ts +1474 -0
- package/src/generated/prisma-adversarial/models/AdversarialResource.ts +124 -0
- package/src/generated/prisma-adversarial/models.ts +2 -0
- package/src/generated/{prisma → prisma-adversarial-pg}/browser.ts +14 -19
- package/src/generated/{prisma → prisma-adversarial-pg}/client.ts +16 -21
- package/src/generated/{prisma → prisma-adversarial-pg}/commonInputTypes.ts +153 -71
- package/src/generated/prisma-adversarial-pg/internal/class.ts +264 -0
- package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespace.ts +339 -371
- package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespaceBrowser.ts +56 -48
- package/src/generated/prisma-adversarial-pg/models/AdversarialCategory.ts +1404 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +1371 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialLabel.ts +1275 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +1476 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +1791 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialSubCategory.ts +1404 -0
- package/src/generated/prisma-adversarial-pg/models/AdversarialTag.ts +1310 -0
- package/src/generated/prisma-adversarial-pg/models.ts +18 -0
- package/src/generated/prisma-adversarial-pg-v6/index.d.ts +11830 -0
- package/src/generated/prisma-adversarial-v6/index.d.ts +3173 -44
- package/src/index.ts +508 -119
- package/src/test-setup.adversarial-url.ts +18 -0
- package/src/test-setup.adversarial.pg.v6.ts +13 -0
- package/src/test-setup.adversarial.pg.v7.ts +9 -0
- package/src/translator.test.ts +1586 -0
- package/src/generated/prisma/internal/class.ts +0 -274
- package/src/generated/prisma/models/Category.ts +0 -1412
- package/src/generated/prisma/models/Label.ts +0 -1276
- package/src/generated/prisma/models/NestedResource.ts +0 -1562
- package/src/generated/prisma/models/NextLevelNestedResource.ts +0 -1337
- package/src/generated/prisma/models/Resource.ts +0 -2196
- package/src/generated/prisma/models/SubCategory.ts +0 -1412
- package/src/generated/prisma/models/Tag.ts +0 -1276
- package/src/generated/prisma/models/User.ts +0 -1503
- package/src/generated/prisma/models.ts +0 -19
- package/src/generated/prisma-v6/index.d.ts +0 -13726
- package/src/index.test.ts +0 -8885
- package/src/test-setup.v6.ts +0 -5
- package/src/test-setup.v7.ts +0 -7
- /package/src/generated/{prisma → prisma-adversarial-pg}/enums.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/client.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/default.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/edge.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/runtime/index-browser.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/runtime/library.d.ts +0 -0
- /package/src/generated/{prisma-v6 → prisma-adversarial-pg-v6}/wasm.d.ts +0 -0
package/lib/index.js
CHANGED
|
@@ -240,32 +240,16 @@ function getFilterEntry(filter) {
|
|
|
240
240
|
}
|
|
241
241
|
return entry;
|
|
242
242
|
}
|
|
243
|
+
/** Whether this reference is one the caller sends as an explicit `null` when the column is NULL. */
|
|
244
|
+
function isExplicitNullReference(fieldRef) {
|
|
245
|
+
return fieldRef.nullAttributeRepresentation === "explicit";
|
|
246
|
+
}
|
|
243
247
|
function isResolvedFieldReference(operand) {
|
|
244
248
|
return "path" in operand;
|
|
245
249
|
}
|
|
246
250
|
function isResolvedValue(operand) {
|
|
247
251
|
return "value" in operand;
|
|
248
252
|
}
|
|
249
|
-
function getNamedOperand(operands, message) {
|
|
250
|
-
const operand = operands.find(isNamedOperand);
|
|
251
|
-
if (!operand) {
|
|
252
|
-
throw new Error(message);
|
|
253
|
-
}
|
|
254
|
-
return operand;
|
|
255
|
-
}
|
|
256
|
-
function getValueOperand(operands, message) {
|
|
257
|
-
const operand = operands.find(isValueOperand);
|
|
258
|
-
if (!operand) {
|
|
259
|
-
throw new Error(message);
|
|
260
|
-
}
|
|
261
|
-
return operand;
|
|
262
|
-
}
|
|
263
|
-
function requireResolvedFieldReference(operand, message) {
|
|
264
|
-
if (!isResolvedFieldReference(operand)) {
|
|
265
|
-
throw new Error(message);
|
|
266
|
-
}
|
|
267
|
-
return operand;
|
|
268
|
-
}
|
|
269
253
|
function requireResolvedValue(operand, message) {
|
|
270
254
|
if (!isResolvedValue(operand)) {
|
|
271
255
|
throw new Error(message);
|
|
@@ -282,20 +266,50 @@ function buildFieldEqualsFilter(fieldRef, value) {
|
|
|
282
266
|
const fieldName = getLeafField(fieldRef.path);
|
|
283
267
|
return wrapRelations(fieldRef.relations, { [fieldName]: { equals: value } });
|
|
284
268
|
}
|
|
285
|
-
function
|
|
269
|
+
function buildDirectOrInLeaf(fieldRef, values) {
|
|
286
270
|
const fieldName = getLeafField(fieldRef.path);
|
|
287
|
-
|
|
271
|
+
return values.length === 1
|
|
288
272
|
? { [fieldName]: values[0] }
|
|
289
273
|
: { [fieldName]: { in: values } };
|
|
290
|
-
|
|
274
|
+
}
|
|
275
|
+
function buildFieldDirectOrInFilter(fieldRef, values) {
|
|
276
|
+
return wrapRelations(fieldRef.relations, buildDirectOrInLeaf(fieldRef, values));
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* `field IS NOT NULL AND <leaf>`, so a NULL column makes the whole thing definitely FALSE rather
|
|
280
|
+
* than merely unmatched — which is what lets an enclosing `NOT` include the row.
|
|
281
|
+
*
|
|
282
|
+
* Prisma has no three-valued logic to expose: a record either matches a `where` or it does not.
|
|
283
|
+
* That is exactly why the guard has to be explicit — `{ NOT: { field: { equals: "x" } } }` leans on
|
|
284
|
+
* the generated SQL's UNKNOWN, which excludes the record under both polarities, and an
|
|
285
|
+
* explicit-null attribute needs the negation to include it.
|
|
286
|
+
*
|
|
287
|
+
* The guard is ANDed with the leaf BEFORE the relation wrapping, so both halves are evaluated
|
|
288
|
+
* against the same element rather than against two independently-matched ones.
|
|
289
|
+
*/
|
|
290
|
+
function guardedFieldFilter(fieldRef, leaf) {
|
|
291
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
292
|
+
return wrapRelations(fieldRef.relations, {
|
|
293
|
+
AND: [{ [fieldName]: { not: null } }, leaf],
|
|
294
|
+
});
|
|
291
295
|
}
|
|
292
296
|
function buildMembershipFilter(fieldRef, values) {
|
|
293
297
|
const nonNullValues = values.filter((value) => value !== null);
|
|
298
|
+
const carriesNull = nonNullValues.length !== values.length;
|
|
299
|
+
if (carriesNull) {
|
|
300
|
+
assertNullOperandTranslatable("a null element in an `in` list", fieldRef.nullAttributeRepresentation);
|
|
301
|
+
}
|
|
294
302
|
const filters = [];
|
|
295
303
|
if (nonNullValues.length > 0 || values.length === 0) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
// Without a null element nothing has made the membership definite yet: a NULL column is
|
|
305
|
+
// dropped by `{ in: [...] }` under BOTH polarities, while CEL compares a null VALUE against
|
|
306
|
+
// each element and gets a definite false, so its negation is TRUE. With a null element the
|
|
307
|
+
// `[null]` disjunct below already settles it.
|
|
308
|
+
filters.push(carriesNull || !isExplicitNullReference(fieldRef)
|
|
309
|
+
? buildFieldDirectOrInFilter(fieldRef, nonNullValues)
|
|
310
|
+
: guardedFieldFilter(fieldRef, buildDirectOrInLeaf(fieldRef, nonNullValues)));
|
|
311
|
+
}
|
|
312
|
+
if (carriesNull) {
|
|
299
313
|
filters.push(buildFieldDirectOrInFilter(fieldRef, [null]));
|
|
300
314
|
}
|
|
301
315
|
if (filters.length === 1) {
|
|
@@ -308,10 +322,32 @@ function buildAlwaysTrueFilter() {
|
|
|
308
322
|
}
|
|
309
323
|
let lambdaScopes = [];
|
|
310
324
|
let rootModelName;
|
|
325
|
+
let nullRepresentation = "explicit";
|
|
326
|
+
/**
|
|
327
|
+
* Guards every site that would emit a NULL-selecting predicate out of a `null` comparison
|
|
328
|
+
* operand.
|
|
329
|
+
*
|
|
330
|
+
* Under the `"omitted"` representation a NULL column carries no attribute, so CEL raises a
|
|
331
|
+
* missing-attribute error and `check()` denies the row — `IS NULL` would return exactly the rows
|
|
332
|
+
* the PDP refuses. The rejection is deliberately wider than the over-granting shapes: `ne(x,
|
|
333
|
+
* null)` on its own is aligned, but Prisma applies negation by wrapping (`{ NOT: ... }`) rather
|
|
334
|
+
* than by pushing it into the leaf, so a leaf cannot tell whether an enclosing `not` will flip
|
|
335
|
+
* `IS NOT NULL` back into a NULL-selecting predicate. Rejecting every null operand is correct
|
|
336
|
+
* under any nesting; narrowing it requires negation-parity tracking.
|
|
337
|
+
*/
|
|
338
|
+
function assertNullOperandTranslatable(context, declared) {
|
|
339
|
+
if ((declared ?? nullRepresentation) === "omitted") {
|
|
340
|
+
throw new Error(`Cannot translate ${context} under nullAttributeRepresentation "omitted": a NULL column ` +
|
|
341
|
+
"sends no attribute, so Cerbos evaluates the comparison as a missing-attribute error " +
|
|
342
|
+
"(deny) while a NULL-selecting filter would return those rows. Send NULL columns as " +
|
|
343
|
+
'explicit nulls and use "explicit", or keep this shape out of the policy.');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
311
346
|
/**
|
|
312
347
|
* Converts a Cerbos query plan to a Prisma filter.
|
|
313
348
|
*/
|
|
314
|
-
function queryPlanToPrisma({ queryPlan, mapper = {}, model, }) {
|
|
349
|
+
function queryPlanToPrisma({ queryPlan, mapper = {}, model, nullAttributeRepresentation = "explicit", }) {
|
|
350
|
+
nullRepresentation = nullAttributeRepresentation;
|
|
315
351
|
switch (queryPlan.kind) {
|
|
316
352
|
case core_1.PlanKind.ALWAYS_ALLOWED:
|
|
317
353
|
return { kind: core_1.PlanKind.ALWAYS_ALLOWED };
|
|
@@ -331,6 +367,16 @@ function queryPlanToPrisma({ queryPlan, mapper = {}, model, }) {
|
|
|
331
367
|
// are ignored.
|
|
332
368
|
throw new Error("A constant-false conditional predicate must be folded by the Cerbos planner");
|
|
333
369
|
}
|
|
370
|
+
// A `map()` at the ROOT of the condition returns a list, not a boolean. Translating it
|
|
371
|
+
// produces a projection filter that is not a valid Prisma `where` clause — the shape
|
|
372
|
+
// must be refused HERE rather than left for findMany to reject at query time, because
|
|
373
|
+
// a projection that happened to coerce would be a silently-wrong filter. (`filter()`
|
|
374
|
+
// as a condition is already rejected inside handleCollectionOperator with its own
|
|
375
|
+
// message; nested inside a comparison or size() both remain translatable.)
|
|
376
|
+
if (isOperatorOperand(condition) && condition.operator === "map") {
|
|
377
|
+
throw new Error("map() returns a list, not a boolean, so it cannot be a condition on its own; " +
|
|
378
|
+
"only comparisons or size() over its result have a boolean meaning");
|
|
379
|
+
}
|
|
334
380
|
return {
|
|
335
381
|
kind: core_1.PlanKind.CONDITIONAL,
|
|
336
382
|
filters: buildPrismaFilterFromCerbosExpression(condition, mapper),
|
|
@@ -677,6 +723,7 @@ function resolveFieldReference(reference, mapper) {
|
|
|
677
723
|
model: activeConfig.relation.model,
|
|
678
724
|
field: activeConfig.relation.field,
|
|
679
725
|
nestedMapper: fields,
|
|
726
|
+
subqueryFilter: activeConfig.relation.subqueryFilter,
|
|
680
727
|
},
|
|
681
728
|
];
|
|
682
729
|
// Process nested relations
|
|
@@ -699,6 +746,7 @@ function resolveFieldReference(reference, mapper) {
|
|
|
699
746
|
model: nextConfig.relation.model,
|
|
700
747
|
field: nextConfig.relation.field,
|
|
701
748
|
nestedMapper: nextConfig.relation.fields,
|
|
749
|
+
subqueryFilter: nextConfig.relation.subqueryFilter,
|
|
702
750
|
});
|
|
703
751
|
currentMapper = nextConfig.relation.fields || {};
|
|
704
752
|
currentParts = currentParts.slice(1);
|
|
@@ -717,12 +765,14 @@ function resolveFieldReference(reference, mapper) {
|
|
|
717
765
|
path: field ? [field] : remainingParts,
|
|
718
766
|
relations,
|
|
719
767
|
valueType: activeConfig.valueType,
|
|
768
|
+
nullAttributeRepresentation: activeConfig.nullAttributeRepresentation,
|
|
720
769
|
};
|
|
721
770
|
}
|
|
722
771
|
// Simple field mapping
|
|
723
772
|
return {
|
|
724
773
|
path: [activeConfig?.field || reference],
|
|
725
774
|
valueType: activeConfig?.valueType,
|
|
775
|
+
nullAttributeRepresentation: activeConfig?.nullAttributeRepresentation,
|
|
726
776
|
};
|
|
727
777
|
}
|
|
728
778
|
/**
|
|
@@ -731,9 +781,137 @@ function resolveFieldReference(reference, mapper) {
|
|
|
731
781
|
function getPrismaRelationOperator(relation) {
|
|
732
782
|
return relation.type === "one" ? "is" : "some";
|
|
733
783
|
}
|
|
784
|
+
/**
|
|
785
|
+
* One relation-scoped filter (`{ tags: { some: … } }`), narrowed by the store-side predicate the
|
|
786
|
+
* caller declared on the mapping.
|
|
787
|
+
*
|
|
788
|
+
* Every nested relation filter the adapter emits goes through here, so a declared
|
|
789
|
+
* `subqueryFilter` reaches the collection macros, the membership shapes, the emptiness checks and
|
|
790
|
+
* the hop-existence guard alike. Adding a new relation shape means routing it through here, or
|
|
791
|
+
* the declaration silently stops applying to it.
|
|
792
|
+
*
|
|
793
|
+
* `every` is the one operator that cannot simply absorb the predicate. `{ every: AND(W, P) }`
|
|
794
|
+
* would REQUIRE every record to satisfy W, which is the opposite of "ignore the records W hides",
|
|
795
|
+
* so it is rewritten to `{ none: AND(W, NOT P) }` — no visible record violates P. Both agree with
|
|
796
|
+
* `every` when nothing is declared, and both stay vacuously true over an empty relation.
|
|
797
|
+
*/
|
|
798
|
+
function relationFilter(relation, operator, inner) {
|
|
799
|
+
const declared = relation.subqueryFilter;
|
|
800
|
+
if (declared === undefined) {
|
|
801
|
+
return { [relation.name]: { [operator]: inner } };
|
|
802
|
+
}
|
|
803
|
+
const narrow = (filter) => Object.keys(filter).length === 0 ? declared : { AND: [declared, filter] };
|
|
804
|
+
if (operator === "every") {
|
|
805
|
+
return Object.keys(inner).length === 0
|
|
806
|
+
? { [relation.name]: { every: inner } }
|
|
807
|
+
: { [relation.name]: { none: { AND: [declared, { NOT: inner }] } } };
|
|
808
|
+
}
|
|
809
|
+
return { [relation.name]: { [operator]: narrow(inner) } };
|
|
810
|
+
}
|
|
734
811
|
/**
|
|
735
812
|
* Builds a nested relation filter for Prisma queries.
|
|
736
813
|
*/
|
|
814
|
+
/**
|
|
815
|
+
* "Every intermediate hop of a dotted path exists", as a Prisma filter.
|
|
816
|
+
*
|
|
817
|
+
* CEL cannot dot through a list, so each intermediate segment of `a.b.c` is a to-ONE
|
|
818
|
+
* parent: absent, the caller sends no attribute and CEL raises a missing-path error, which
|
|
819
|
+
* denies. `NOT { categories: { some: { subCategories: { some: P } } } }` is TRUE for a row
|
|
820
|
+
* with no category at all, so the negation returns rows the PDP denies — the parent must be
|
|
821
|
+
* required separately (cerbos/query-plan-adapters#309).
|
|
822
|
+
*
|
|
823
|
+
* Returns undefined when the reference has no intermediate hop, so a direct relation keeps
|
|
824
|
+
* its empty-collection semantics (`!tags.exists(...)` over zero tags is still TRUE).
|
|
825
|
+
*/
|
|
826
|
+
function buildLeadingHopsExistFilter(head, restRelations) {
|
|
827
|
+
if (restRelations.length === 0) {
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
// The last entry is the collection being iterated; everything before it is a hop.
|
|
831
|
+
return buildHopsExistFilter([head, ...restRelations.slice(0, -1)]);
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* "Every one of these relations exists", as a nested Prisma filter with an empty innermost
|
|
835
|
+
* predicate — `{ parent: { is: { inner: { is: {} } } } }`.
|
|
836
|
+
*/
|
|
837
|
+
function buildHopsExistFilter(hops) {
|
|
838
|
+
if (hops.length === 0) {
|
|
839
|
+
return undefined;
|
|
840
|
+
}
|
|
841
|
+
let inner = {};
|
|
842
|
+
for (let i = hops.length - 1; i >= 0; i--) {
|
|
843
|
+
const relation = assertDefined(hops[i], "Relation mapping is missing");
|
|
844
|
+
inner = relationFilter(relation, getPrismaRelationOperator(relation), inner);
|
|
845
|
+
}
|
|
846
|
+
return inner;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* "Every to-one hop this expression dots through exists", as a Prisma filter, or undefined
|
|
850
|
+
* when it dots through none.
|
|
851
|
+
*
|
|
852
|
+
* Prisma has no UNKNOWN: a chained relation filter is exact under the POSITIVE polarity —
|
|
853
|
+
* `categories: { some: { subCategories: { some: P } } }` cannot hold without the category —
|
|
854
|
+
* but `NOT` inverts the hop requirement along with the predicate, so every operator built on
|
|
855
|
+
* a chain readmits the parentless rows once negated. #309 fixed that inside the collection
|
|
856
|
+
* macros, which left the sibling operators reached through the same chain unguarded:
|
|
857
|
+
* membership and `hasIntersection` (cerbos/query-plan-adapters#315), and the negated count
|
|
858
|
+
* spelling `!(size(chain) > 0)` (#316), all of which the macros' guard never saw.
|
|
859
|
+
*
|
|
860
|
+
* Requiring the hops OUTSIDE the negation is what fixes all of them at once, and it is
|
|
861
|
+
* unconditionally faithful to CEL rather than a per-operator patch: a missing hop is a
|
|
862
|
+
* missing-path error, which denies under BOTH polarities, so the requirement never depends
|
|
863
|
+
* on which operator sits above the chain.
|
|
864
|
+
*
|
|
865
|
+
* Lambda bodies are deliberately not walked — their references resolve against a scoped
|
|
866
|
+
* mapper, and a negated macro carries its own guard through buildNegatedCollectionFilter.
|
|
867
|
+
*/
|
|
868
|
+
function buildExpressionHopsExistFilter(operand, mapper) {
|
|
869
|
+
const hops = [];
|
|
870
|
+
const seen = new Set();
|
|
871
|
+
const visit = (expr) => {
|
|
872
|
+
if (isNamedOperand(expr)) {
|
|
873
|
+
if (seen.has(expr.name)) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
seen.add(expr.name);
|
|
877
|
+
const { relations } = resolveFieldReference(expr.name, mapper);
|
|
878
|
+
if (!relations || relations.length === 0) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
// Every to-ONE relation on the path is a hop that must EXIST — including a TRAILING one,
|
|
882
|
+
// which is the case a chain of to-many relations never produces. `parent.aBool` resolves
|
|
883
|
+
// to a single to-one relation, and `NOT { parent: { is: { aBool: true } } }` is TRUE for
|
|
884
|
+
// a row with no parent at all, so the negation returns rows the PDP denies. A trailing
|
|
885
|
+
// to-MANY relation is the collection being iterated rather than a hop, and keeps its
|
|
886
|
+
// empty-collection semantics (`!tags.exists(...)` over zero tags is still TRUE).
|
|
887
|
+
const last = assertDefined(relations[relations.length - 1], "Relation mapping is missing");
|
|
888
|
+
const hopFilter = buildHopsExistFilter(last.type === "one" ? relations : relations.slice(0, -1));
|
|
889
|
+
if (hopFilter !== undefined) {
|
|
890
|
+
hops.push(hopFilter);
|
|
891
|
+
}
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (!isOperatorOperand(expr) || expr.operator === "lambda") {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
expr.operands.forEach(visit);
|
|
898
|
+
};
|
|
899
|
+
visit(operand);
|
|
900
|
+
if (hops.length === 0) {
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
return hops.length === 1
|
|
904
|
+
? assertDefined(hops[0], "Leading-hops filter is missing")
|
|
905
|
+
: { AND: hops };
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* `{ NOT: filter }`, with every to-one hop `operand` dots through required OUTSIDE the
|
|
909
|
+
* negation so an absent parent stays denied (see buildExpressionHopsExistFilter).
|
|
910
|
+
*/
|
|
911
|
+
function negateRequiringHops(operand, filter, mapper) {
|
|
912
|
+
const hops = buildExpressionHopsExistFilter(operand, mapper);
|
|
913
|
+
return hops === undefined ? { NOT: filter } : { AND: [hops, { NOT: filter }] };
|
|
914
|
+
}
|
|
737
915
|
function buildNestedRelationFilter(relations, fieldFilter) {
|
|
738
916
|
if (relations.length === 0)
|
|
739
917
|
return fieldFilter;
|
|
@@ -755,7 +933,7 @@ function buildNestedRelationFilter(relations, fieldFilter) {
|
|
|
755
933
|
currentFilter = { [relation.field]: filterValue };
|
|
756
934
|
}
|
|
757
935
|
}
|
|
758
|
-
currentFilter =
|
|
936
|
+
currentFilter = relationFilter(relation, relationOperator, currentFilter);
|
|
759
937
|
}
|
|
760
938
|
return currentFilter;
|
|
761
939
|
}
|
|
@@ -931,9 +1109,6 @@ function buildPrismaFilterFromCerbosExpression(expression, mapper) {
|
|
|
931
1109
|
case "endsWith": {
|
|
932
1110
|
return handleStringOperator(operator, operands, mapper);
|
|
933
1111
|
}
|
|
934
|
-
case "isSet": {
|
|
935
|
-
return handleIsSetOperator(operands, mapper);
|
|
936
|
-
}
|
|
937
1112
|
case "hasIntersection": {
|
|
938
1113
|
return handleHasIntersectionOperator(operands, mapper);
|
|
939
1114
|
}
|
|
@@ -972,6 +1147,20 @@ function containsCollectionOperator(expr) {
|
|
|
972
1147
|
}
|
|
973
1148
|
return expr.operands.some(containsCollectionOperator);
|
|
974
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Whether `expr` dots through at least one intermediate to-one hop, so negating it needs the
|
|
1152
|
+
* hop requirement of buildExpressionHopsExistFilter.
|
|
1153
|
+
*/
|
|
1154
|
+
function referencesChainedRelation(expr, mapper) {
|
|
1155
|
+
if (isNamedOperand(expr)) {
|
|
1156
|
+
const { relations } = resolveFieldReference(expr.name, mapper);
|
|
1157
|
+
return relations !== undefined && relations.length > 1;
|
|
1158
|
+
}
|
|
1159
|
+
if (!isOperatorOperand(expr) || expr.operator === "lambda") {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
return expr.operands.some((operand) => referencesChainedRelation(operand, mapper));
|
|
1163
|
+
}
|
|
975
1164
|
/**
|
|
976
1165
|
* Builds the filter for `!expr`. Plain field predicates negate correctly with Prisma's NOT
|
|
977
1166
|
* (SQL three-valued logic keeps NULL rows excluded under both polarities), but relation
|
|
@@ -993,11 +1182,16 @@ function buildNegatedFilter(operand, mapper) {
|
|
|
993
1182
|
if (!relations || relations.length === 0) {
|
|
994
1183
|
return buildFieldEqualsFilter(fieldRef, false);
|
|
995
1184
|
}
|
|
996
|
-
return
|
|
997
|
-
NOT: buildPrismaFilterFromCerbosExpression(operand, mapper),
|
|
998
|
-
};
|
|
1185
|
+
return negateRequiringHops(operand, buildPrismaFilterFromCerbosExpression(operand, mapper), mapper);
|
|
999
1186
|
}
|
|
1000
|
-
|
|
1187
|
+
// `and`/`or` must be pushed through with De Morgan whenever a branch can be UNKNOWN, so
|
|
1188
|
+
// CEL's error absorption survives: `!(A && B)` with A erroring and B false is TRUE in CEL,
|
|
1189
|
+
// and `OR[!A, !B]` reproduces that where a single outer NOT over the conjunction — with the
|
|
1190
|
+
// hop requirement ANDed outside it — would deny. A chained relation is the second source of
|
|
1191
|
+
// UNKNOWN besides the collection macros, so it opens the same push-down.
|
|
1192
|
+
if (isOperatorOperand(operand) &&
|
|
1193
|
+
(containsCollectionOperator(operand) ||
|
|
1194
|
+
referencesChainedRelation(operand, mapper))) {
|
|
1001
1195
|
switch (operand.operator) {
|
|
1002
1196
|
case "and":
|
|
1003
1197
|
return {
|
|
@@ -1017,9 +1211,7 @@ function buildNegatedFilter(operand, mapper) {
|
|
|
1017
1211
|
return buildNegatedCollectionFilter(operand.operator, operand.operands, mapper);
|
|
1018
1212
|
}
|
|
1019
1213
|
}
|
|
1020
|
-
return
|
|
1021
|
-
NOT: buildPrismaFilterFromCerbosExpression(operand, mapper),
|
|
1022
|
-
};
|
|
1214
|
+
return negateRequiringHops(operand, buildPrismaFilterFromCerbosExpression(operand, mapper), mapper);
|
|
1023
1215
|
}
|
|
1024
1216
|
function getTernaryOperands(operands) {
|
|
1025
1217
|
if (operands.length !== 3) {
|
|
@@ -1052,25 +1244,23 @@ function buildBooleanBranchFilter(branch, mapper) {
|
|
|
1052
1244
|
}
|
|
1053
1245
|
return { kind: "constant", value: branch.value };
|
|
1054
1246
|
}
|
|
1247
|
+
/**
|
|
1248
|
+
* "The ternary's condition is definitively FALSE", which is exactly buildNegatedFilter's
|
|
1249
|
+
* contract — three-valued, so a condition that is UNKNOWN (a NULL column, an erroring lambda
|
|
1250
|
+
* body, an absent to-one parent) selects NEITHER branch.
|
|
1251
|
+
*
|
|
1252
|
+
* It delegates rather than spelling a second negation. A private copy is how the ternary
|
|
1253
|
+
* missed the hop requirement #315/#316 added: a bare `NOT` over a chain filter is TRUE when
|
|
1254
|
+
* the to-one parent is absent, so the else-branch was selected for rows CEL denies outright —
|
|
1255
|
+
* the missing path is an evaluation error, not a false condition
|
|
1256
|
+
* (cerbos/query-plan-adapters#334). Delegating also gives the ternary the De Morgan push-down,
|
|
1257
|
+
* which a chained `and`/`or` condition needs for CEL's error absorption to survive.
|
|
1258
|
+
*/
|
|
1055
1259
|
function buildFalseConditionFilter(condition, mapper) {
|
|
1056
1260
|
if (isValueOperand(condition)) {
|
|
1057
1261
|
throw new Error("Constant ternary conditions must be folded before use");
|
|
1058
1262
|
}
|
|
1059
|
-
|
|
1060
|
-
const fieldRef = resolveFieldReference(condition.name, mapper);
|
|
1061
|
-
if (!fieldRef.relations || fieldRef.relations.length === 0) {
|
|
1062
|
-
return buildFieldEqualsFilter(fieldRef, false);
|
|
1063
|
-
}
|
|
1064
|
-
return {
|
|
1065
|
-
NOT: buildFieldEqualsFilter(fieldRef, true),
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
if (isOperatorOperand(condition) && condition.operator === "not") {
|
|
1069
|
-
return buildPrismaFilterFromCerbosExpression(assertDefined(condition.operands[0], "not operator requires an operand"), mapper);
|
|
1070
|
-
}
|
|
1071
|
-
return {
|
|
1072
|
-
NOT: buildPrismaFilterFromCerbosExpression(condition, mapper),
|
|
1073
|
-
};
|
|
1263
|
+
return buildNegatedFilter(condition, mapper);
|
|
1074
1264
|
}
|
|
1075
1265
|
function buildGuardedTernaryFilter({ condition, thenFilter, elseFilter, mapper, }) {
|
|
1076
1266
|
const conditionTrue = buildPrismaFilterFromCerbosExpression(condition, mapper);
|
|
@@ -1291,7 +1481,7 @@ function handleSizeComparison(operator, sizeOperand, valueOperand, mapper) {
|
|
|
1291
1481
|
throw new Error("size operator requires a relation mapping");
|
|
1292
1482
|
}
|
|
1293
1483
|
const prismaOp = isNonEmpty ? "some" : "none";
|
|
1294
|
-
const leafFilter =
|
|
1484
|
+
const leafFilter = relationFilter(deepest, prismaOp, {});
|
|
1295
1485
|
if (relations.length === 1) {
|
|
1296
1486
|
return leafFilter;
|
|
1297
1487
|
}
|
|
@@ -1360,6 +1550,26 @@ function handleRelationalOperator(operator, operands, mapper) {
|
|
|
1360
1550
|
*/
|
|
1361
1551
|
function buildComparisonFilter(fieldRef, operator, value) {
|
|
1362
1552
|
const prismaOperator = assertDefined(CERBOS_TO_PRISMA_OPERATOR[operator], `Unsupported operator: ${operator}`);
|
|
1553
|
+
if (value === null) {
|
|
1554
|
+
assertNullOperandTranslatable(`\`${operator}\` against a null operand`, fieldRef.nullAttributeRepresentation);
|
|
1555
|
+
}
|
|
1556
|
+
// An attribute the caller sends as an explicit null holds a null VALUE in CEL, so equality
|
|
1557
|
+
// against a non-null operand is a definite FALSE and inequality a definite TRUE. Prisma leans
|
|
1558
|
+
// on SQL's UNKNOWN for both, which drops the row under either polarity, so the presence of the
|
|
1559
|
+
// column has to become part of the predicate. Confined to the equality family: an ordering
|
|
1560
|
+
// comparison against a null receiver is a no-overload error in CEL, which denies exactly as the
|
|
1561
|
+
// dropped row does.
|
|
1562
|
+
if (value !== null && isExplicitNullReference(fieldRef)) {
|
|
1563
|
+
const fieldName = getLeafField(fieldRef.path);
|
|
1564
|
+
if (operator === "eq") {
|
|
1565
|
+
return guardedFieldFilter(fieldRef, { [fieldName]: { equals: value } });
|
|
1566
|
+
}
|
|
1567
|
+
if (operator === "ne") {
|
|
1568
|
+
return {
|
|
1569
|
+
NOT: guardedFieldFilter(fieldRef, { [fieldName]: { equals: value } }),
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1363
1573
|
if (typeof value === "number" && !Number.isInteger(value)) {
|
|
1364
1574
|
const fieldName = getLeafField(fieldRef.path);
|
|
1365
1575
|
const lower = Math.floor(value);
|
|
@@ -1446,11 +1656,49 @@ function buildFieldToFieldFilter(operator, leftOperand, left, rightOperand, righ
|
|
|
1446
1656
|
}
|
|
1447
1657
|
const leftField = getLeafField(left.path);
|
|
1448
1658
|
const rightField = getLeafField(right.path);
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1659
|
+
const fieldReference = { _ref: rightField, _container: container };
|
|
1660
|
+
// Two explicit nulls are EQUAL in CEL, and a null against a value is definitely NOT equal.
|
|
1661
|
+
// A bare field reference gives UNKNOWN for both, which drops the row under either polarity.
|
|
1662
|
+
const leftExplicit = isExplicitNullReference(left);
|
|
1663
|
+
const rightExplicit = isExplicitNullReference(right);
|
|
1664
|
+
// Mixing the two conventions across one comparison has no faithful rendering. The declared side
|
|
1665
|
+
// needs a definite answer for its NULL (CEL holds a null VALUE); the undeclared side needs
|
|
1666
|
+
// UNKNOWN for its NULL (a missing attribute, which CEL denies under both polarities). A definite
|
|
1667
|
+
// predicate returns rows the PDP refuses; a plain one drops rows the PDP allows. Refuse it
|
|
1668
|
+
// rather than pick a direction — declare both attributes, or neither.
|
|
1669
|
+
if (leftExplicit !== rightExplicit && (operator === "eq" || operator === "ne")) {
|
|
1670
|
+
throw new Error(`Cannot translate \`${operator}\` between two columns under mixed null conventions: ` +
|
|
1671
|
+
"cannot compare an attribute declared explicit-null with one on the omitted convention: the omitted side is UNKNOWN for a NULL column while the declared side is definite, and no single predicate is both. Declare nullAttributeRepresentation on both mapper entries, or on neither.");
|
|
1672
|
+
}
|
|
1673
|
+
if (leftExplicit && rightExplicit && (operator === "eq" || operator === "ne")) {
|
|
1674
|
+
const present = [];
|
|
1675
|
+
if (leftExplicit) {
|
|
1676
|
+
present.push({ [leftField]: { not: null } });
|
|
1677
|
+
}
|
|
1678
|
+
if (rightExplicit) {
|
|
1679
|
+
present.push({ [rightField]: { not: null } });
|
|
1680
|
+
}
|
|
1681
|
+
const equality = leftExplicit && rightExplicit
|
|
1682
|
+
? {
|
|
1683
|
+
OR: [
|
|
1684
|
+
{ AND: [{ [leftField]: null }, { [rightField]: null }] },
|
|
1685
|
+
{ AND: [...present, { [leftField]: { equals: fieldReference } }] },
|
|
1686
|
+
],
|
|
1687
|
+
}
|
|
1688
|
+
: { AND: [...present, { [leftField]: { equals: fieldReference } }] };
|
|
1689
|
+
return operator === "eq" ? equality : { NOT: equality };
|
|
1690
|
+
}
|
|
1691
|
+
// `ne` is the one operator whose Prisma spelling cannot carry a field reference: `not` nests a
|
|
1692
|
+
// NestedStringFilter, which has no `_ref` form, so `{ col: { not: { _ref } } }` is rejected by
|
|
1693
|
+
// the client rather than executed. Hoisting the negation to the top-level `NOT` combinator says
|
|
1694
|
+
// the same thing with a shape that does accept one — the same rewrite the explicit-null branch
|
|
1695
|
+
// above already performs. Three-valued logic is unchanged by the hoist: a NULL on either side
|
|
1696
|
+
// makes the inner equality UNKNOWN, and `NOT UNKNOWN` is still UNKNOWN, so the row stays out
|
|
1697
|
+
// under both polarities, which is what a missing attribute does on the check side.
|
|
1698
|
+
if (operator === "ne") {
|
|
1699
|
+
return { NOT: { [leftField]: { equals: fieldReference } } };
|
|
1700
|
+
}
|
|
1701
|
+
return { [leftField]: { [prismaOperator]: fieldReference } };
|
|
1454
1702
|
}
|
|
1455
1703
|
/**
|
|
1456
1704
|
* Helper function to handle "in" operator
|
|
@@ -1500,9 +1748,17 @@ function handleStringOperator(operator, operands, mapper) {
|
|
|
1500
1748
|
// Prisma emits LIKE without an ESCAPE clause and does not escape wildcard characters,
|
|
1501
1749
|
// so a needle containing % or _ would match as a pattern instead of literally (e.g.
|
|
1502
1750
|
// startsWith("100%") wrongly matches "100xdone"). Fail loudly rather than filter wrong.
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1751
|
+
//
|
|
1752
|
+
// A backslash is the same hazard reached from the other direction, and only an executed
|
|
1753
|
+
// PostgreSQL leg surfaces it (cerbos/query-plan-adapters#320): SQLite's LIKE has no escape
|
|
1754
|
+
// character at all, so `\` is literal there, while PostgreSQL and MySQL treat it as the
|
|
1755
|
+
// DEFAULT escape character even with no ESCAPE clause. `endsWith("\\")` is then a pattern
|
|
1756
|
+
// ending in an escape character — a hard error on PostgreSQL — and a `\x` anywhere inside
|
|
1757
|
+
// silently drops the backslash, so `contains("a\\b")` matches "ab", a row the PDP denies.
|
|
1758
|
+
if (/[%_\\]/.test(value)) {
|
|
1759
|
+
throw new Error(`Cannot translate ${operator} with a needle containing LIKE metacharacters (%, _ or \\): ` +
|
|
1760
|
+
"Prisma does not escape wildcards in string filters, and \\ is the default LIKE " +
|
|
1761
|
+
"escape character on PostgreSQL and MySQL");
|
|
1506
1762
|
}
|
|
1507
1763
|
const fieldName = getLeafField(receiver.path);
|
|
1508
1764
|
return wrapRelations(receiver.relations, {
|
|
@@ -1552,19 +1808,6 @@ function handleStringOperator(operator, operands, mapper) {
|
|
|
1552
1808
|
}
|
|
1553
1809
|
throw new Error(`${operator} between two constants must be folded by the Cerbos planner`);
|
|
1554
1810
|
}
|
|
1555
|
-
/**
|
|
1556
|
-
* Helper function to handle "isSet" operator
|
|
1557
|
-
*/
|
|
1558
|
-
function handleIsSetOperator(operands, mapper) {
|
|
1559
|
-
const nameOperand = getNamedOperand(operands, "Name operand is undefined");
|
|
1560
|
-
const valueOperand = getValueOperand(operands, "Value operand is undefined");
|
|
1561
|
-
const fieldRef = requireResolvedFieldReference(resolveOperand(nameOperand, mapper), "Name operand must resolve to a field reference");
|
|
1562
|
-
const resolvedValue = requireResolvedValue(resolveOperand(valueOperand, mapper), "Value operand must resolve to a value");
|
|
1563
|
-
const fieldName = getLeafField(fieldRef.path);
|
|
1564
|
-
return wrapRelations(fieldRef.relations, {
|
|
1565
|
-
[fieldName]: resolvedValue.value ? { not: null } : { equals: null },
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
1811
|
/**
|
|
1569
1812
|
* Helper function to handle "hasIntersection" operator
|
|
1570
1813
|
*/
|
|
@@ -1775,7 +2018,7 @@ function wrapCollectionElementFilter(parts, elementFilter) {
|
|
|
1775
2018
|
const inner = parts.restRelations.length > 0
|
|
1776
2019
|
? buildNestedRelationFilter(parts.restRelations, elementFilter)
|
|
1777
2020
|
: elementFilter;
|
|
1778
|
-
return
|
|
2021
|
+
return relationFilter(parts.head, "some", inner);
|
|
1779
2022
|
}
|
|
1780
2023
|
function throwUnsupportedCollectionOperator(operator) {
|
|
1781
2024
|
if (operator === "exists_one") {
|
|
@@ -1802,7 +2045,7 @@ function handleCollectionOperator(operator, operands, mapper) {
|
|
|
1802
2045
|
case "exists": {
|
|
1803
2046
|
// A NULL element keeps the per-element predicate UNKNOWN, so it can never create a
|
|
1804
2047
|
// false positive here; a CEL error (deny) coincides with "no match" — no guard needed.
|
|
1805
|
-
const filter =
|
|
2048
|
+
const filter = relationFilter(head, "some", filterValue);
|
|
1806
2049
|
const unknownElement = buildUnknownElementFilter(parts);
|
|
1807
2050
|
if (unknownElement !== undefined) {
|
|
1808
2051
|
recordNestedCollectionUnknownFilter({
|
|
@@ -1815,12 +2058,12 @@ function handleCollectionOperator(operator, operands, mapper) {
|
|
|
1815
2058
|
return filter;
|
|
1816
2059
|
}
|
|
1817
2060
|
case "except":
|
|
1818
|
-
return
|
|
2061
|
+
return relationFilter(head, "some", { NOT: filterValue });
|
|
1819
2062
|
case "all": {
|
|
1820
2063
|
if (parts.restRelations.length > 0) {
|
|
1821
2064
|
throw new Error("all() over a multi-hop relation chain is not supported");
|
|
1822
2065
|
}
|
|
1823
|
-
const base =
|
|
2066
|
+
const base = relationFilter(head, "every", filterValue);
|
|
1824
2067
|
if (nullableFields.size === 0) {
|
|
1825
2068
|
return base;
|
|
1826
2069
|
}
|
|
@@ -1831,7 +2074,7 @@ function handleCollectionOperator(operator, operands, mapper) {
|
|
|
1831
2074
|
return {
|
|
1832
2075
|
AND: [
|
|
1833
2076
|
base,
|
|
1834
|
-
|
|
2077
|
+
relationFilter(head, "none", buildNullWitnessFilter(nullableFields)),
|
|
1835
2078
|
],
|
|
1836
2079
|
};
|
|
1837
2080
|
}
|
|
@@ -1853,9 +2096,17 @@ function buildNegatedCollectionFilter(operator, operands, mapper) {
|
|
|
1853
2096
|
}
|
|
1854
2097
|
const parts = buildCollectionLambdaParts(operator, operands, mapper);
|
|
1855
2098
|
const { head, filterValue, nullableFields } = parts;
|
|
2099
|
+
// An absent to-one parent must stay denied under negation rather than satisfying it
|
|
2100
|
+
// vacuously (#309).
|
|
2101
|
+
const leadingHopsExist = buildLeadingHopsExistFilter(head, parts.restRelations);
|
|
2102
|
+
const requireLeadingHops = (filter) => leadingHopsExist === undefined
|
|
2103
|
+
? filter
|
|
2104
|
+
: { AND: [leadingHopsExist, filter] };
|
|
1856
2105
|
switch (operator) {
|
|
1857
2106
|
case "exists": {
|
|
1858
|
-
const base = {
|
|
2107
|
+
const base = requireLeadingHops({
|
|
2108
|
+
NOT: relationFilter(head, "some", filterValue),
|
|
2109
|
+
});
|
|
1859
2110
|
const unknownElement = buildUnknownElementFilter(parts);
|
|
1860
2111
|
if (unknownElement === undefined) {
|
|
1861
2112
|
return base;
|
|
@@ -1874,17 +2125,17 @@ function buildNegatedCollectionFilter(operator, operands, mapper) {
|
|
|
1874
2125
|
case "all":
|
|
1875
2126
|
// !all is TRUE only with a definitive false witness, which also absorbs error
|
|
1876
2127
|
// elements — exactly `some(NOT P)` in SQL (NULL columns keep NOT P UNKNOWN).
|
|
1877
|
-
return
|
|
2128
|
+
return relationFilter(head, "some", { NOT: filterValue });
|
|
1878
2129
|
case "except": {
|
|
1879
2130
|
// !except(c,P) = every element definitively matches P.
|
|
1880
|
-
const base =
|
|
2131
|
+
const base = requireLeadingHops(relationFilter(head, "none", { NOT: filterValue }));
|
|
1881
2132
|
if (nullableFields.size === 0) {
|
|
1882
2133
|
return base;
|
|
1883
2134
|
}
|
|
1884
2135
|
return {
|
|
1885
2136
|
AND: [
|
|
1886
2137
|
base,
|
|
1887
|
-
|
|
2138
|
+
relationFilter(head, "none", buildNullWitnessFilter(nullableFields)),
|
|
1888
2139
|
],
|
|
1889
2140
|
};
|
|
1890
2141
|
}
|
|
@@ -2223,14 +2474,17 @@ function handleOverlapsOperator(operands, mapper) {
|
|
|
2223
2474
|
*
|
|
2224
2475
|
* `startsWith` compiles to `LIKE` with no `ESCAPE` clause, so `%` and `_` in the prefix
|
|
2225
2476
|
* match as wildcards instead of literally and widen the filter past what the PDP allows.
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2477
|
+
* `\` is unsafe because PostgreSQL and MySQL treat it as the DEFAULT escape character with no
|
|
2478
|
+
* `ESCAPE` clause present, where SQLite has none at all — so the same prefix means different
|
|
2479
|
+
* things per provider. `[` is unsafe for a third reason: SQL Server opens a character class on
|
|
2480
|
+
* `[` even when an `ESCAPE` clause is declared, so it cannot be made literal at all. Fail loudly
|
|
2481
|
+
* rather than emit a filter that admits denied rows.
|
|
2229
2482
|
*/
|
|
2230
2483
|
function assertLikeSafePrefix(prefix) {
|
|
2231
|
-
if (/[%_\[]/.test(prefix)) {
|
|
2232
|
-
throw new Error("Cannot translate hierarchy prefix matching with LIKE metacharacters (%, _ or [): " +
|
|
2233
|
-
"Prisma emits LIKE without an ESCAPE clause,
|
|
2484
|
+
if (/[%_\[\\]/.test(prefix)) {
|
|
2485
|
+
throw new Error("Cannot translate hierarchy prefix matching with LIKE metacharacters (%, _, \\ or [): " +
|
|
2486
|
+
"Prisma emits LIKE without an ESCAPE clause, \\ is the default escape character on " +
|
|
2487
|
+
"PostgreSQL and MySQL, and [ opens a character class on SQL Server even with one");
|
|
2234
2488
|
}
|
|
2235
2489
|
}
|
|
2236
2490
|
function handleFieldOverlaps(left, right) {
|