@cerbos/orm-prisma 4.0.0 → 5.0.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.
Files changed (203) hide show
  1. package/README.md +611 -460
  2. package/lib/collections.d.ts +30 -0
  3. package/lib/collections.d.ts.map +1 -0
  4. package/lib/collections.js +421 -0
  5. package/lib/collections.js.map +1 -0
  6. package/lib/comparison.d.ts +24 -0
  7. package/lib/comparison.d.ts.map +1 -0
  8. package/lib/comparison.js +731 -0
  9. package/lib/comparison.js.map +1 -0
  10. package/lib/errors.d.ts +13 -0
  11. package/lib/errors.d.ts.map +1 -0
  12. package/lib/errors.js +17 -0
  13. package/lib/errors.js.map +1 -0
  14. package/lib/evaluate.d.ts +5 -0
  15. package/lib/evaluate.d.ts.map +1 -0
  16. package/lib/evaluate.js +118 -0
  17. package/lib/evaluate.js.map +1 -0
  18. package/lib/fields.d.ts +34 -0
  19. package/lib/fields.d.ts.map +1 -0
  20. package/lib/fields.js +253 -0
  21. package/lib/fields.js.map +1 -0
  22. package/lib/functions.d.ts +5 -0
  23. package/lib/functions.d.ts.map +1 -0
  24. package/lib/functions.js +408 -0
  25. package/lib/functions.js.map +1 -0
  26. package/lib/hierarchy.d.ts +6 -0
  27. package/lib/hierarchy.d.ts.map +1 -0
  28. package/lib/hierarchy.js +266 -0
  29. package/lib/hierarchy.js.map +1 -0
  30. package/lib/index.d.ts +91 -8
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +34 -2301
  33. package/lib/index.js.map +1 -1
  34. package/lib/literals.d.ts +20 -0
  35. package/lib/literals.d.ts.map +1 -0
  36. package/lib/literals.js +290 -0
  37. package/lib/literals.js.map +1 -0
  38. package/lib/mapping.d.ts +95 -0
  39. package/lib/mapping.d.ts.map +1 -0
  40. package/lib/mapping.js +316 -0
  41. package/lib/mapping.js.map +1 -0
  42. package/lib/narrowing.d.ts +12 -0
  43. package/lib/narrowing.d.ts.map +1 -0
  44. package/lib/narrowing.js +32 -0
  45. package/lib/narrowing.js.map +1 -0
  46. package/lib/plan.d.ts +49 -0
  47. package/lib/plan.d.ts.map +1 -0
  48. package/lib/plan.js +86 -0
  49. package/lib/plan.js.map +1 -0
  50. package/lib/presence.d.ts +25 -0
  51. package/lib/presence.d.ts.map +1 -0
  52. package/lib/presence.js +82 -0
  53. package/lib/presence.js.map +1 -0
  54. package/lib/regex.d.ts +33 -0
  55. package/lib/regex.d.ts.map +1 -0
  56. package/lib/regex.js +480 -0
  57. package/lib/regex.js.map +1 -0
  58. package/lib/relations.d.ts +58 -0
  59. package/lib/relations.d.ts.map +1 -0
  60. package/lib/relations.js +175 -0
  61. package/lib/relations.js.map +1 -0
  62. package/lib/rewrite.d.ts +36 -0
  63. package/lib/rewrite.d.ts.map +1 -0
  64. package/lib/rewrite.js +366 -0
  65. package/lib/rewrite.js.map +1 -0
  66. package/lib/solve.d.ts +20 -0
  67. package/lib/solve.d.ts.map +1 -0
  68. package/lib/solve.js +77 -0
  69. package/lib/solve.js.map +1 -0
  70. package/lib/strings.d.ts +12 -0
  71. package/lib/strings.d.ts.map +1 -0
  72. package/lib/strings.js +110 -0
  73. package/lib/strings.js.map +1 -0
  74. package/lib/ternary.d.ts +12 -0
  75. package/lib/ternary.d.ts.map +1 -0
  76. package/lib/ternary.js +192 -0
  77. package/lib/ternary.js.map +1 -0
  78. package/lib/timestamp.d.ts +45 -0
  79. package/lib/timestamp.d.ts.map +1 -0
  80. package/lib/timestamp.js +225 -0
  81. package/lib/timestamp.js.map +1 -0
  82. package/lib/translate.d.ts +20 -0
  83. package/lib/translate.d.ts.map +1 -0
  84. package/lib/translate.js +287 -0
  85. package/lib/translate.js.map +1 -0
  86. package/lib/types.d.ts +29 -0
  87. package/lib/types.d.ts.map +1 -0
  88. package/lib/types.js +980 -0
  89. package/lib/types.js.map +1 -0
  90. package/lib/value.d.ts +8 -0
  91. package/lib/value.d.ts.map +1 -0
  92. package/lib/value.js +7 -0
  93. package/lib/value.js.map +1 -0
  94. package/package.json +31 -18
  95. package/src/adversarial.test.ts +488 -488
  96. package/src/collections.ts +738 -0
  97. package/src/comparison.ts +1157 -0
  98. package/src/corpus.ts +597 -0
  99. package/src/errors.ts +12 -0
  100. package/src/evaluate.ts +170 -0
  101. package/src/fields.ts +373 -0
  102. package/src/functions.ts +548 -0
  103. package/src/generated/prisma-adversarial/browser.ts +20 -0
  104. package/src/generated/prisma-adversarial/client.ts +20 -0
  105. package/src/generated/prisma-adversarial/commonInputTypes.ts +116 -127
  106. package/src/generated/prisma-adversarial/internal/class.ts +44 -4
  107. package/src/generated/prisma-adversarial/internal/prismaNamespace.ts +350 -3
  108. package/src/generated/prisma-adversarial/internal/prismaNamespaceBrowser.ts +49 -2
  109. package/src/generated/prisma-adversarial/models/AdversarialBoolListElement.ts +1272 -0
  110. package/src/generated/prisma-adversarial/models/AdversarialInner.ts +1369 -0
  111. package/src/generated/prisma-adversarial/models/AdversarialNumberListElement.ts +1316 -0
  112. package/src/generated/prisma-adversarial/models/AdversarialParent.ts +1474 -0
  113. package/src/generated/prisma-adversarial/models/AdversarialResource.ts +547 -75
  114. package/src/generated/prisma-adversarial/models.ts +4 -0
  115. package/src/generated/prisma-adversarial-mysql/browser.ts +64 -0
  116. package/src/generated/{prisma → prisma-adversarial-mysql}/client.ts +23 -18
  117. package/src/generated/{prisma → prisma-adversarial-mysql}/commonInputTypes.ts +167 -92
  118. package/src/generated/prisma-adversarial-mysql/internal/class.ts +284 -0
  119. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespace.ts +1518 -0
  120. package/src/generated/prisma-adversarial-mysql/internal/prismaNamespaceBrowser.ts +274 -0
  121. package/src/generated/prisma-adversarial-mysql/models/AdversarialBoolListElement.ts +1157 -0
  122. package/src/generated/prisma-adversarial-mysql/models/AdversarialCategory.ts +1286 -0
  123. package/src/generated/prisma-adversarial-mysql/models/AdversarialInner.ts +1247 -0
  124. package/src/generated/prisma-adversarial-mysql/models/AdversarialLabel.ts +1157 -0
  125. package/src/generated/prisma-adversarial-mysql/models/AdversarialNumberListElement.ts +1201 -0
  126. package/src/generated/prisma-adversarial-mysql/models/AdversarialParent.ts +1352 -0
  127. package/src/generated/prisma-adversarial-mysql/models/AdversarialResource.ts +2021 -0
  128. package/src/generated/prisma-adversarial-mysql/models/AdversarialSubCategory.ts +1286 -0
  129. package/src/generated/prisma-adversarial-mysql/models/AdversarialTag.ts +1190 -0
  130. package/src/generated/prisma-adversarial-mysql/models.ts +20 -0
  131. package/src/generated/prisma-adversarial-mysql-v6/index.d.ts +13822 -0
  132. package/src/generated/prisma-adversarial-pg/browser.ts +64 -0
  133. package/src/generated/prisma-adversarial-pg/client.ts +86 -0
  134. package/src/generated/prisma-adversarial-pg/commonInputTypes.ts +345 -0
  135. package/src/generated/prisma-adversarial-pg/enums.ts +15 -0
  136. package/src/generated/prisma-adversarial-pg/internal/class.ts +284 -0
  137. package/src/generated/prisma-adversarial-pg/internal/prismaNamespace.ts +1542 -0
  138. package/src/generated/{prisma → prisma-adversarial-pg}/internal/prismaNamespaceBrowser.ts +72 -43
  139. package/src/generated/prisma-adversarial-pg/models/AdversarialBoolListElement.ts +1275 -0
  140. package/src/generated/prisma-adversarial-pg/models/AdversarialCategory.ts +1404 -0
  141. package/src/generated/prisma-adversarial-pg/models/AdversarialInner.ts +1371 -0
  142. package/src/generated/prisma-adversarial-pg/models/AdversarialLabel.ts +1275 -0
  143. package/src/generated/prisma-adversarial-pg/models/AdversarialNumberListElement.ts +1319 -0
  144. package/src/generated/prisma-adversarial-pg/models/AdversarialParent.ts +1476 -0
  145. package/src/generated/prisma-adversarial-pg/models/AdversarialResource.ts +2139 -0
  146. package/src/generated/prisma-adversarial-pg/models/AdversarialSubCategory.ts +1404 -0
  147. package/src/generated/prisma-adversarial-pg/models/AdversarialTag.ts +1310 -0
  148. package/src/generated/prisma-adversarial-pg/models.ts +20 -0
  149. package/src/generated/prisma-adversarial-pg-v6/client.d.ts +1 -0
  150. package/src/generated/prisma-adversarial-pg-v6/default.d.ts +1 -0
  151. package/src/generated/prisma-adversarial-pg-v6/edge.d.ts +1 -0
  152. package/src/generated/prisma-adversarial-pg-v6/index.d.ts +14918 -0
  153. package/src/generated/prisma-adversarial-pg-v6/runtime/index-browser.d.ts +370 -0
  154. package/src/generated/prisma-adversarial-pg-v6/runtime/library.d.ts +3982 -0
  155. package/src/generated/prisma-adversarial-pg-v6/wasm.d.ts +1 -0
  156. package/src/generated/prisma-adversarial-v6/index.d.ts +6515 -304
  157. package/src/hierarchy.ts +405 -0
  158. package/src/index.ts +134 -3325
  159. package/src/literals.ts +363 -0
  160. package/src/mapping.ts +474 -0
  161. package/src/narrowing.ts +33 -0
  162. package/src/plan.ts +125 -0
  163. package/src/presence.ts +114 -0
  164. package/src/regex.ts +596 -0
  165. package/src/relations.ts +226 -0
  166. package/src/rewrite.ts +524 -0
  167. package/src/solve.ts +96 -0
  168. package/src/strings.ts +152 -0
  169. package/src/ternary.ts +359 -0
  170. package/src/test-setup.adversarial-url.ts +21 -0
  171. package/src/test-setup.adversarial.mysql.v6.ts +13 -0
  172. package/src/test-setup.adversarial.mysql.v7.ts +12 -0
  173. package/src/test-setup.adversarial.pg.v6.ts +13 -0
  174. package/src/test-setup.adversarial.pg.v7.ts +9 -0
  175. package/src/test-setup.adversarial.v7.ts +1 -0
  176. package/src/timestamp.ts +308 -0
  177. package/src/translate.ts +419 -0
  178. package/src/translator.test.ts +766 -0
  179. package/src/types.ts +1310 -0
  180. package/src/value.ts +16 -0
  181. package/src/generated/prisma/browser.ts +0 -59
  182. package/src/generated/prisma/internal/class.ts +0 -274
  183. package/src/generated/prisma/internal/prismaNamespace.ts +0 -1403
  184. package/src/generated/prisma/models/Category.ts +0 -1412
  185. package/src/generated/prisma/models/Label.ts +0 -1276
  186. package/src/generated/prisma/models/NestedResource.ts +0 -1562
  187. package/src/generated/prisma/models/NextLevelNestedResource.ts +0 -1337
  188. package/src/generated/prisma/models/Resource.ts +0 -2196
  189. package/src/generated/prisma/models/SubCategory.ts +0 -1412
  190. package/src/generated/prisma/models/Tag.ts +0 -1276
  191. package/src/generated/prisma/models/User.ts +0 -1503
  192. package/src/generated/prisma/models.ts +0 -19
  193. package/src/generated/prisma-v6/index.d.ts +0 -13726
  194. package/src/index.test.ts +0 -8885
  195. package/src/test-setup.v6.ts +0 -5
  196. package/src/test-setup.v7.ts +0 -7
  197. /package/src/generated/{prisma → prisma-adversarial-mysql}/enums.ts +0 -0
  198. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/client.d.ts +0 -0
  199. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/default.d.ts +0 -0
  200. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/edge.d.ts +0 -0
  201. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/index-browser.d.ts +0 -0
  202. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/runtime/library.d.ts +0 -0
  203. /package/src/generated/{prisma-v6 → prisma-adversarial-mysql-v6}/wasm.d.ts +0 -0
@@ -0,0 +1,114 @@
1
+ // `has()`, the presence test the planner emits from Cerbos 0.57.0 for an attribute it does not
2
+ // fold into a guard: whether the caller's attributes carry the key at all.
3
+
4
+ import type { PlanExpressionOperand } from "@cerbos/core";
5
+
6
+ import type { PrismaFilter } from "./index";
7
+ import { getLeafField, namesToOneRelation, resolveFieldReference } from "./mapping";
8
+ import type { TranslationContext } from "./mapping";
9
+ import { isNamedOperand } from "./plan";
10
+ import type { OperatorOperand } from "./plan";
11
+ import { negateFilter, wrapInRelations } from "./relations";
12
+ import { UnsupportedQueryPlanError } from "./errors";
13
+
14
+ /**
15
+ * `has(ref)`, or `!has(ref)` when `negated`.
16
+ *
17
+ * Presence is a property of the caller's null convention, not of the column: an attribute sent as
18
+ * an explicit null is present, so `has()` is true for every row, and one omitted when its column
19
+ * is NULL is present exactly when the column is not NULL. Element columns inside a lambda follow
20
+ * the omitted convention unless they are declared `nullable: false`, as the collection macros
21
+ * already assume. A to-one relation is present when the related row exists.
22
+ *
23
+ * `has()` itself never errors on a missing value, so `!has()` is a plain NOT. The one error it can
24
+ * raise is selecting through a missing to-one hop, `has(parent.x)` on a parentless row, which
25
+ * denies under both polarities, so every hop BEFORE the tested key is required outside the NOT.
26
+ */
27
+ export function handleHasOperator(
28
+ operands: PlanExpressionOperand[],
29
+ context: TranslationContext,
30
+ negated: boolean
31
+ ): PrismaFilter {
32
+ const [operand] = operands;
33
+
34
+ if (operands.length !== 1 || operand === undefined || !isNamedOperand(operand)) {
35
+ throw new UnsupportedQueryPlanError("has requires exactly one attribute reference");
36
+ }
37
+
38
+ const fieldRef = resolveFieldReference(operand.name, context);
39
+ const relations = fieldRef.relations ?? [];
40
+
41
+ if (relations.some((relation) => relation.type === "many")) {
42
+ throw new UnsupportedQueryPlanError(
43
+ `Cannot translate has(${operand.name}): a presence test over a to-many relation has no ` +
44
+ "Prisma filter that matches what the caller sends for it"
45
+ );
46
+ }
47
+
48
+ let presence: PrismaFilter;
49
+ let hops = relations;
50
+
51
+ if (namesToOneRelation(context.mapper, operand.name)) {
52
+ presence = wrapInRelations(relations, {});
53
+ hops = relations.slice(0, -1);
54
+ } else if (presenceFollowsColumn(operand.name, fieldRef, context)) {
55
+ presence = wrapInRelations(relations, {
56
+ [getLeafField(fieldRef.path)]: { not: null },
57
+ });
58
+ } else {
59
+ presence = wrapInRelations(relations, {});
60
+ }
61
+
62
+ if (!negated) return presence;
63
+ const absence = negateFilter(presence);
64
+
65
+ return hops.length === 0 ? absence : { AND: [wrapInRelations(hops, {}), absence] };
66
+ }
67
+
68
+ /**
69
+ * `has(R.attr.x)` on an attribute sent as an explicit null is true on every row, so it becomes the
70
+ * constant `true` before translation. Left to the translator it lowers to `{}`, which Prisma drops
71
+ * from an OR or a NOT, losing the rows it admits and, negated, admitting rows it denies.
72
+ */
73
+ export function constantPresence(
74
+ node: OperatorOperand,
75
+ context: TranslationContext
76
+ ): PlanExpressionOperand | undefined {
77
+ const [operand] = node.operands;
78
+
79
+ if (
80
+ node.operator !== "has" ||
81
+ node.operands.length !== 1 ||
82
+ operand === undefined ||
83
+ !isNamedOperand(operand) ||
84
+ !operand.name.startsWith("request.") ||
85
+ namesToOneRelation(context.mapper, operand.name)
86
+ ) {
87
+ return undefined;
88
+ }
89
+
90
+ const fieldRef = resolveFieldReference(operand.name, context);
91
+
92
+ if ((fieldRef.relations ?? []).length > 0) return undefined;
93
+
94
+ return presenceFollowsColumn(operand.name, fieldRef, context) ? undefined : { value: true };
95
+ }
96
+
97
+ /** Whether a NULL column means the key is absent, rather than sent as an explicit null. */
98
+ function presenceFollowsColumn(
99
+ reference: string,
100
+ fieldRef: ReturnType<typeof resolveFieldReference>,
101
+ context: TranslationContext
102
+ ): boolean {
103
+ const element = context.scopes.some((scope) =>
104
+ reference.startsWith(scope.variableName + ".")
105
+ );
106
+
107
+ if (element && fieldRef.nullAttributeRepresentation === undefined) {
108
+ return fieldRef.nullable !== false;
109
+ }
110
+
111
+ return (
112
+ (fieldRef.nullAttributeRepresentation ?? context.nullRepresentation) === "omitted"
113
+ );
114
+ }
package/src/regex.ts ADDED
@@ -0,0 +1,596 @@
1
+ // `matches(column, "pattern")` for the RE2 patterns whose language a boolean combination of LIKE
2
+ // filters decides exactly. Everything else is refused.
3
+
4
+ import type { PlanExpressionOperand } from "@cerbos/core";
5
+ import { RE2JS } from "re2js";
6
+
7
+ import { assertStringField, buildFieldFilter, buildPrefixFilter } from "./fields";
8
+ import type { PrismaFilter } from "./index";
9
+ import { isResolvedFieldReference, resolveFieldReference } from "./mapping";
10
+ import type { ResolvedFieldReference, TranslationContext } from "./mapping";
11
+ import { isNamedOperand, isValueOperand } from "./plan";
12
+ import { UnsupportedQueryPlanError } from "./errors";
13
+ import { isString, pair } from "./narrowing";
14
+
15
+ /** Past this many alternatives, a pattern is refused rather than spelled out. */
16
+ const MAX_ALTERNATIVES = 256;
17
+
18
+ // -- the pattern ---------------------------------------------------------------------------------
19
+
20
+ /** One position of an expanded pattern: a literal character, `.` (not a newline), or `.*`. */
21
+ type Token = { char: string } | { any: true } | { star: true };
22
+
23
+ type Node =
24
+ | { kind: "alt"; branches: Node[][] }
25
+ | { kind: "char"; chars: string[] }
26
+ | { kind: "any" }
27
+ | { kind: "begin" }
28
+ | { kind: "end" }
29
+ | { kind: "repeat"; node: Node; min: number; max: number };
30
+
31
+ /**
32
+ * A pattern RE2 itself rejects, as the parser below sees it. Whether RE2 rejects a pattern is
33
+ * decided by re2js (isInvalidPattern), not by this parser, whose refusals are always safe.
34
+ */
35
+ export class InvalidRegexError extends Error {}
36
+
37
+ function refuse(pattern: string, why: string): never {
38
+ throw new UnsupportedQueryPlanError(
39
+ `Cannot translate matches() with pattern ${JSON.stringify(pattern)}: ${why}. Prisma has no ` +
40
+ "regex filter; only patterns a combination of LIKE filters decides exactly are translated"
41
+ );
42
+ }
43
+
44
+ const DIGITS = [..."0123456789"];
45
+
46
+ /**
47
+ * A repetition count, `{n}`, `{n,}` or `{n,m}`; any other brace is a literal to RE2. A count is
48
+ * read as Go's `regexp/syntax` `parseInt` reads it, which takes no leading zero: `a{01}` is `a`
49
+ * then the text "{01}".
50
+ */
51
+ const REPEAT = /^\{(0|[1-9]\d*)(,(0|[1-9]\d*)?)?\}/;
52
+
53
+ /** RE2's bound on the copies a counted repetition, nested ones included, may make. */
54
+ const MAX_REPEAT = 1000;
55
+
56
+ /**
57
+ * Go's `repeatIsValid`: each repetition divides the budget by its count (its maximum, or its
58
+ * minimum when unbounded), and a count beyond what is left is invalid: `(a{2}){600}`.
59
+ */
60
+ function repeatIsValid(node: Node, budget: number): boolean {
61
+ switch (node.kind) {
62
+ case "repeat": {
63
+ if (node.max === 0) return true;
64
+ const count = node.max === Infinity ? node.min : node.max;
65
+
66
+ if (count > budget) return false;
67
+
68
+ return repeatIsValid(node.node, count === 0 ? budget : Math.floor(budget / count));
69
+ }
70
+
71
+ case "alt":
72
+ return node.branches.every((branch) => branch.every((inner) => repeatIsValid(inner, budget)));
73
+ default:
74
+ return true;
75
+ }
76
+ }
77
+
78
+ /** RE2's simple case folding over ASCII letters: `k` and `s` fold with a non-ASCII sign too. */
79
+ function foldOrbit(char: string): string[] {
80
+ const lower = char.toLowerCase();
81
+
82
+ if (!/^[a-z]$/.test(lower)) return [char];
83
+ const orbit = [lower, lower.toUpperCase()];
84
+
85
+ if (lower === "k") orbit.push("K");
86
+
87
+ if (lower === "s") orbit.push("ſ");
88
+
89
+ return orbit;
90
+ }
91
+
92
+ /** A recursive-descent parser over the pattern's code points (RE2 matches code points). */
93
+ class Parser {
94
+ private index = 0;
95
+ private caseInsensitive = false;
96
+ private readonly chars: string[];
97
+
98
+ constructor(private readonly pattern: string) {
99
+ this.chars = Array.from(pattern);
100
+ }
101
+
102
+ parse(): Node[][] {
103
+ if (this.pattern.startsWith("(?i)")) {
104
+ this.caseInsensitive = true;
105
+ this.index = 4;
106
+ }
107
+
108
+ const branches = this.alternation();
109
+
110
+ if (this.index !== this.chars.length) refuse(this.pattern, "unbalanced parenthesis");
111
+
112
+ return branches;
113
+ }
114
+
115
+ private peek(offset = 0): string | undefined {
116
+ return this.chars[this.index + offset];
117
+ }
118
+
119
+ private rest(): string {
120
+ return this.chars.slice(this.index).join("");
121
+ }
122
+
123
+ private next(): string | undefined {
124
+ return this.chars[this.index++];
125
+ }
126
+
127
+ private alternation(): Node[][] {
128
+ const branches: Node[][] = [this.sequence()];
129
+
130
+ while (this.peek() === "|") {
131
+ this.index++;
132
+ branches.push(this.sequence());
133
+ }
134
+
135
+ return branches;
136
+ }
137
+
138
+ private sequence(): Node[] {
139
+ const nodes: Node[] = [];
140
+
141
+ for (let char = this.peek(); char !== undefined && char !== "|" && char !== ")"; char = this.peek()) {
142
+ nodes.push(this.quantified(this.atom()));
143
+ }
144
+
145
+ return nodes;
146
+ }
147
+
148
+ private quantified(node: Node): Node {
149
+ const char = this.peek();
150
+ let min: number;
151
+ let max: number;
152
+
153
+ if (char === "*" || char === "+" || char === "?") {
154
+ this.index++;
155
+ [min, max] = char === "*" ? [0, Infinity] : char === "+" ? [1, Infinity] : [0, 1];
156
+ } else if (char === "{") {
157
+ const match = REPEAT.exec(this.rest());
158
+
159
+ // RE2 reads a brace that does not open a repetition as a literal.
160
+ if (match === null) return node;
161
+ this.index += Array.from(match[0]).length;
162
+ min = Number(match[1]);
163
+ max = match[2] === undefined ? min : match[3] === undefined ? Infinity : Number(match[3]);
164
+
165
+ if (min > MAX_REPEAT || (max !== Infinity && (max > MAX_REPEAT || max < min))) {
166
+ throw new InvalidRegexError("invalid repeat count");
167
+ }
168
+ } else {
169
+ return node;
170
+ }
171
+
172
+ const next = this.peek();
173
+
174
+ if (next === "?") refuse(this.pattern, "non-greedy repetition");
175
+
176
+ if (next === "*" || next === "+" || (next === "{" && REPEAT.test(this.rest()))) {
177
+ throw new InvalidRegexError("invalid nested repetition");
178
+ }
179
+
180
+ if (node.kind === "begin" || node.kind === "end") {
181
+ refuse(this.pattern, "a repeated anchor");
182
+ }
183
+
184
+ const repeat: Node = { kind: "repeat", node, min, max };
185
+
186
+ if (!repeatIsValid(repeat, MAX_REPEAT)) throw new InvalidRegexError("invalid repeat count");
187
+
188
+ return repeat;
189
+ }
190
+
191
+ private atom(): Node {
192
+ const char = this.next()!;
193
+
194
+ switch (char) {
195
+ case "^":
196
+ return { kind: "begin" };
197
+ case "$":
198
+ return { kind: "end" };
199
+ case ".":
200
+ return { kind: "any" };
201
+ case "(": {
202
+ if (this.peek() === "?") {
203
+ const rest = this.rest();
204
+
205
+ if (/^\?(=|!|<=|<!)/.test(rest)) throw new InvalidRegexError("lookaround");
206
+
207
+ if (rest.startsWith("?:")) {
208
+ this.index += 2;
209
+ } else {
210
+ refuse(this.pattern, "a group flag");
211
+ }
212
+ }
213
+
214
+ const branches = this.alternation();
215
+
216
+ if (this.peek() !== ")") refuse(this.pattern, "unbalanced parenthesis");
217
+ this.index++;
218
+
219
+ return { kind: "alt", branches };
220
+ }
221
+
222
+ case "[":
223
+ return this.characterClass();
224
+ case "\\":
225
+ return this.escape();
226
+ case "*":
227
+ case "+":
228
+ case "?":
229
+ throw new InvalidRegexError("missing argument to repetition operator");
230
+ case "{":
231
+ // A count with nothing before it to repeat; a brace that is not a count stays a literal.
232
+ if (REPEAT.test(`{${this.rest()}`)) {
233
+ throw new InvalidRegexError("missing argument to repetition operator");
234
+ }
235
+
236
+ return this.literal(char);
237
+ default:
238
+ return this.literal(char);
239
+ }
240
+ }
241
+
242
+ private literal(char: string): Node {
243
+ if (this.caseInsensitive && /[^\x00-\x7F]/.test(char)) {
244
+ refuse(this.pattern, "case-insensitive matching of a non-ASCII character");
245
+ }
246
+
247
+ return { kind: "char", chars: this.caseInsensitive ? foldOrbit(char) : [char] };
248
+ }
249
+
250
+ private escape(): Node {
251
+ const char = this.next();
252
+
253
+ if (char === undefined) throw new InvalidRegexError("trailing backslash");
254
+
255
+ if (char === "d") return { kind: "char", chars: DIGITS };
256
+ const octal = this.octalEscape(char);
257
+
258
+ if (octal !== undefined) return this.literal(octal);
259
+
260
+ if (/[A-Za-z0-9]/.test(char)) refuse(this.pattern, `the escape \\${char}`);
261
+
262
+ return this.literal(char);
263
+ }
264
+
265
+ /**
266
+ * The character of an octal escape whose first digit was just read, as Go's regexp/syntax reads
267
+ * it: `\0` on its own, and `\1` to `\7` only when another octal digit follows (alone, each is
268
+ * a backreference, which RE2 rejects); up to three digits in all. `\11` is a tab.
269
+ */
270
+ private octalEscape(first: string): string | undefined {
271
+ const isOctal = (c: string | undefined): c is string => c !== undefined && c >= "0" && c <= "7";
272
+
273
+ if (!isOctal(first)) {
274
+ if (/[89]/.test(first)) throw new InvalidRegexError(`invalid escape sequence: \\${first}`);
275
+
276
+ return undefined;
277
+ }
278
+
279
+ if (first !== "0" && !isOctal(this.peek())) {
280
+ throw new InvalidRegexError(`invalid escape sequence: \\${first}`);
281
+ }
282
+
283
+ let code = Number(first);
284
+
285
+ for (let digits = 1; digits < 3 && isOctal(this.peek()); digits++) {
286
+ code = code * 8 + Number(this.next());
287
+ }
288
+
289
+ return String.fromCodePoint(code);
290
+ }
291
+
292
+ private characterClass(): Node {
293
+ if (this.peek() === "^") refuse(this.pattern, "a negated character class");
294
+ const chars = new Set<string>();
295
+ let first = true;
296
+
297
+ for (;;) {
298
+ const char = this.next();
299
+
300
+ if (char === undefined) throw new InvalidRegexError("missing closing ]");
301
+
302
+ if (char === "]" && !first) break;
303
+ first = false;
304
+
305
+ if (char === "[" && this.rest().startsWith(":digit:]")) {
306
+ this.index += ":digit:]".length;
307
+ DIGITS.forEach((digit) => chars.add(digit));
308
+ continue;
309
+ }
310
+
311
+ if (char === "[" && this.peek() === ":") refuse(this.pattern, "a POSIX class other than digit");
312
+ let from = char;
313
+
314
+ if (char === "\\") {
315
+ const escaped = this.next();
316
+
317
+ if (escaped === undefined) throw new InvalidRegexError("trailing backslash");
318
+
319
+ if (escaped === "d") {
320
+ DIGITS.forEach((digit) => chars.add(digit));
321
+ continue;
322
+ }
323
+
324
+ const octal = this.octalEscape(escaped);
325
+
326
+ if (octal !== undefined) {
327
+ from = octal;
328
+ } else {
329
+ if (/[A-Za-z0-9]/.test(escaped)) refuse(this.pattern, `the escape \\${escaped}`);
330
+ from = escaped;
331
+ }
332
+ }
333
+
334
+ if (this.peek() === "-" && this.peek(1) !== "]") {
335
+ this.index++;
336
+ const to = this.next();
337
+
338
+ if (to === undefined || to === "\\") refuse(this.pattern, "an escaped class range");
339
+ const start = from.codePointAt(0)!;
340
+ const end = to.codePointAt(0)!;
341
+
342
+ if (end < start) throw new InvalidRegexError("invalid character class range");
343
+
344
+ if (end - start > 128) refuse(this.pattern, "a wide character range");
345
+
346
+ for (let code = start; code <= end; code++) chars.add(String.fromCodePoint(code));
347
+ continue;
348
+ }
349
+
350
+ chars.add(from);
351
+ }
352
+
353
+ const members = [...chars].flatMap((char) => {
354
+ if (this.caseInsensitive && /[^\x00-\x7F]/.test(char)) {
355
+ refuse(this.pattern, "case-insensitive matching of a non-ASCII character");
356
+ }
357
+
358
+ return this.caseInsensitive ? foldOrbit(char) : [char];
359
+ });
360
+
361
+ return { kind: "char", chars: [...new Set(members)] };
362
+ }
363
+ }
364
+
365
+ // -- expansion -----------------------------------------------------------------------------------
366
+
367
+ type Expansion = { begin: boolean; end: boolean; tokens: Token[] };
368
+
369
+ function product(pattern: string, left: Token[][], right: Token[][]): Token[][] {
370
+ if (left.length * right.length > MAX_ALTERNATIVES) {
371
+ refuse(pattern, `more than ${MAX_ALTERNATIVES} alternatives`);
372
+ }
373
+
374
+ return left.flatMap((l) => right.map((r) => [...l, ...r]));
375
+ }
376
+
377
+ /** Every token sequence `nodes` (holding no anchor) can match. */
378
+ function expand(pattern: string, nodes: Node[]): Token[][] {
379
+ let sequences: Token[][] = [[]];
380
+
381
+ for (const node of nodes) {
382
+ sequences = product(pattern, sequences, expandNode(pattern, node));
383
+ }
384
+
385
+ return sequences;
386
+ }
387
+
388
+ function expandNode(pattern: string, node: Node): Token[][] {
389
+ switch (node.kind) {
390
+ case "char":
391
+ return node.chars.map((char) => [{ char }]);
392
+ case "any":
393
+ return [[{ any: true }]];
394
+ case "alt": {
395
+ const all = node.branches.flatMap((branch) => expand(pattern, branch));
396
+
397
+ if (all.length > MAX_ALTERNATIVES) refuse(pattern, `more than ${MAX_ALTERNATIVES} alternatives`);
398
+
399
+ return all;
400
+ }
401
+
402
+ case "repeat": {
403
+ if (node.max === Infinity) {
404
+ if (node.node.kind !== "any") refuse(pattern, "unbounded repetition of anything but .");
405
+
406
+ return [[...Array.from({ length: node.min }, (): Token => ({ any: true })), { star: true }]];
407
+ }
408
+
409
+ const unit = expandNode(pattern, node.node);
410
+ const results: Token[][] = [];
411
+ let power: Token[][] = [[]];
412
+
413
+ for (let count = 0; count <= node.max; count++) {
414
+ if (count >= node.min) results.push(...power);
415
+
416
+ if (results.length > MAX_ALTERNATIVES) refuse(pattern, `more than ${MAX_ALTERNATIVES} alternatives`);
417
+
418
+ if (count < node.max) power = product(pattern, power, unit);
419
+ }
420
+
421
+ return results;
422
+ }
423
+
424
+ default:
425
+ refuse(pattern, "an anchor inside a group or away from the pattern's ends");
426
+ }
427
+ }
428
+
429
+ /** The pattern as alternatives, each anchored at its ends or not. */
430
+ export function expandPattern(pattern: string): Expansion[] {
431
+ const branches = new Parser(pattern).parse();
432
+ const expansions: Expansion[] = [];
433
+
434
+ for (const branch of branches) {
435
+ let nodes = branch;
436
+ const begin = nodes[0]?.kind === "begin";
437
+
438
+ if (begin) nodes = nodes.slice(1);
439
+ const end = nodes[nodes.length - 1]?.kind === "end";
440
+
441
+ if (end) nodes = nodes.slice(0, -1);
442
+
443
+ for (const tokens of expand(pattern, nodes)) {
444
+ expansions.push({ begin, end, tokens });
445
+
446
+ if (expansions.length > MAX_ALTERNATIVES) {
447
+ refuse(pattern, `more than ${MAX_ALTERNATIVES} alternatives`);
448
+ }
449
+ }
450
+ }
451
+
452
+ return expansions;
453
+ }
454
+
455
+ // -- the filter ----------------------------------------------------------------------------------
456
+
457
+ const LIKE_METACHARACTERS = /[%_\\]/;
458
+
459
+ /**
460
+ * One alternative as a filter on the column. Without a wildcard it is `equals`, `startsWith`,
461
+ * `endsWith` or `contains` as its anchors say. With one, only a pattern anchored at both ends is
462
+ * translated: a LIKE of `_` for `.` and `%` for `.*`, and no newline anywhere in the value — the
463
+ * literal characters are not newlines, so every newline in a match falls in a wildcard, which RE2's
464
+ * `.` does not match.
465
+ */
466
+ function alternativeFilter(
467
+ pattern: string,
468
+ fieldRef: ResolvedFieldReference,
469
+ { begin, end, tokens }: Expansion
470
+ ): PrismaFilter {
471
+ const characters = tokens.flatMap((token) => ("char" in token ? [token.char] : []));
472
+ const literal = characters.length === tokens.length ? characters.join("") : undefined;
473
+
474
+ if (literal !== undefined && begin && end) {
475
+ return buildFieldFilter(fieldRef, "equals", literal);
476
+ }
477
+
478
+ const likeText = tokens
479
+ .map((token) => ("char" in token ? token.char : "any" in token ? "_" : "%"))
480
+ .join("");
481
+
482
+ // A prefix is matched as a range, so it needs no escaping (see buildPrefixFilter).
483
+ if (literal !== undefined && begin) return buildPrefixFilter(fieldRef, literal);
484
+ const literals = tokens.flatMap((token) => ("char" in token ? [token.char] : []));
485
+
486
+ if (literals.some((char) => LIKE_METACHARACTERS.test(char))) {
487
+ refuse(pattern, "a literal LIKE metacharacter (%, _ or \\), which Prisma does not escape");
488
+ }
489
+
490
+ if (literal !== undefined) {
491
+ return buildFieldFilter(fieldRef, end ? "endsWith" : "contains", literal);
492
+ }
493
+
494
+ if (!begin || !end) {
495
+ refuse(pattern, "a wildcard in a pattern not anchored at both ends");
496
+ }
497
+
498
+ if (literals.includes("\n")) refuse(pattern, "a literal newline beside a wildcard");
499
+ const noNewline: PrismaFilter = { NOT: buildFieldFilter(fieldRef, "contains", "\n") };
500
+ const segments = likeText.split("%");
501
+
502
+ if (segments.length === 1) {
503
+ // Exactly the pattern's length: it matches as a prefix, and one more character never fits.
504
+ return {
505
+ AND: [
506
+ buildFieldFilter(fieldRef, "startsWith", likeText),
507
+ { NOT: buildFieldFilter(fieldRef, "startsWith", `${likeText}_`) },
508
+ noNewline,
509
+ ],
510
+ };
511
+ }
512
+
513
+ if (segments.length > 2) refuse(pattern, "more than one .* in one alternative");
514
+ // `A.*B`: A at the start, B at the end, and room for both without overlapping.
515
+ const [head, tail] = pair(segments);
516
+ const filters: PrismaFilter[] = [];
517
+
518
+ if (head !== "") filters.push(buildFieldFilter(fieldRef, "startsWith", head));
519
+
520
+ if (tail !== "") filters.push(buildFieldFilter(fieldRef, "endsWith", tail));
521
+
522
+ if (head !== "" && tail !== "") {
523
+ filters.push(buildFieldFilter(fieldRef, "startsWith", "_".repeat(Array.from(head).length + Array.from(tail).length)));
524
+ }
525
+
526
+ if (filters.length === 0) filters.push(buildFieldFilter(fieldRef, "startsWith", ""));
527
+
528
+ return { AND: [...filters, noNewline] };
529
+ }
530
+
531
+ /**
532
+ * Whether the pattern is one RE2 rejects, so that `matches()` raises an error on every row: what
533
+ * re2js, a port of RE2, refuses to compile. A pattern this adapter cannot translate is not invalid;
534
+ * it is refused when translated.
535
+ */
536
+ export function isInvalidPattern(pattern: string): boolean {
537
+ return re2Rejection(pattern) !== undefined;
538
+ }
539
+
540
+ /** RE2's reason for rejecting the pattern, or undefined when it compiles. */
541
+ function re2Rejection(pattern: string): string | undefined {
542
+ try {
543
+ RE2JS.compile(pattern);
544
+
545
+ return undefined;
546
+ } catch (error) {
547
+ return error instanceof Error ? error.message : String(error);
548
+ }
549
+ }
550
+
551
+ export function handleMatchesOperator(
552
+ operands: PlanExpressionOperand[],
553
+ context: TranslationContext
554
+ ): PrismaFilter {
555
+ const [receiver, patternOperand] = operands;
556
+
557
+ if (
558
+ operands.length !== 2 ||
559
+ receiver === undefined ||
560
+ !isNamedOperand(receiver) ||
561
+ patternOperand === undefined ||
562
+ !isValueOperand(patternOperand) ||
563
+ !isString(patternOperand.value)
564
+ ) {
565
+ throw new UnsupportedQueryPlanError(
566
+ "matches() is translated only for a column receiver and a literal pattern"
567
+ );
568
+ }
569
+
570
+ const pattern = patternOperand.value;
571
+ const fieldRef = resolveFieldReference(receiver.name, context);
572
+
573
+ if (!isResolvedFieldReference(fieldRef)) {
574
+ throw new UnsupportedQueryPlanError("matches() requires a column receiver");
575
+ }
576
+
577
+ assertStringField(fieldRef, "matches");
578
+ const rejection = re2Rejection(pattern);
579
+
580
+ if (rejection !== undefined) refuse(pattern, `RE2 rejects it (${rejection})`);
581
+ let expansions: Expansion[];
582
+
583
+ try {
584
+ expansions = expandPattern(pattern);
585
+ } catch (error) {
586
+ if (error instanceof InvalidRegexError) {
587
+ refuse(pattern, `RE2 rejects it (${error.message})`);
588
+ }
589
+
590
+ throw error;
591
+ }
592
+
593
+ const filters = expansions.map((expansion) => alternativeFilter(pattern, fieldRef, expansion));
594
+
595
+ return filters.length === 1 ? filters[0]! : { OR: filters };
596
+ }