@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,226 @@
1
+ // Nesting a filter through the relations a reference crosses, and the hop-existence guards that
2
+ // keep an absent to-one parent denied under negation.
3
+
4
+ import type { PlanExpressionOperand } from "@cerbos/core";
5
+
6
+ import type { PrismaFilter } from "./index";
7
+ import { resolveFieldReference } from "./mapping";
8
+ import type { RelationConfig, TranslationContext } from "./mapping";
9
+ import { isNamedOperand, isOperatorOperand } from "./plan";
10
+ import { UnsupportedQueryPlanError } from "./errors";
11
+
12
+ /** The Prisma operator that reaches into a relation for an existence test. */
13
+ export function relationOperator(relation: RelationConfig): string {
14
+ return relation.type === "one" ? "is" : "some";
15
+ }
16
+
17
+ /**
18
+ * One relation-scoped filter (`{ tags: { some: … } }`), narrowed by the store-side predicate the
19
+ * caller declared on the mapping.
20
+ *
21
+ * Every nested relation filter the adapter emits goes through here, so a declared
22
+ * `subqueryFilter` reaches the collection macros, the membership shapes, the emptiness checks and
23
+ * the hop-existence guard alike. Adding a new relation shape means routing it through here, or
24
+ * the declaration silently stops applying to it.
25
+ *
26
+ * `every` is the one operator that cannot simply absorb the predicate. `{ every: AND(W, P) }`
27
+ * would REQUIRE every record to satisfy W, which is the opposite of "ignore the records W hides",
28
+ * so it is rewritten to `{ none: AND(W, NOT P) }` — no visible record violates P. Both agree with
29
+ * `every` when nothing is declared, and both stay vacuously true over an empty relation.
30
+ */
31
+ export function relationFilter(
32
+ relation: RelationConfig,
33
+ operator: string,
34
+ inner: PrismaFilter
35
+ ): PrismaFilter {
36
+ const declared = relation.subqueryFilter;
37
+
38
+ if (declared === undefined) {
39
+ return { [relation.name]: { [operator]: inner } };
40
+ }
41
+
42
+ const isEmpty = Object.keys(inner).length === 0;
43
+
44
+ if (operator === "every") {
45
+ return isEmpty
46
+ ? { [relation.name]: { every: inner } }
47
+ : { [relation.name]: { none: { AND: [declared, { NOT: inner }] } } };
48
+ }
49
+
50
+ return {
51
+ [relation.name]: {
52
+ [operator]: isEmpty ? declared : { AND: [declared, inner] },
53
+ },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * `filter` nested through every relation in `relations`, outermost first, each reached with its
59
+ * existence operator (`is` for to-one, `some` for to-many). No relations leaves it unchanged.
60
+ */
61
+ export function wrapInRelations(
62
+ relations: RelationConfig[] | undefined,
63
+ filter: PrismaFilter
64
+ ): PrismaFilter {
65
+ let current = filter;
66
+
67
+ for (const relation of [...(relations ?? [])].reverse()) {
68
+ current = relationFilter(relation, relationOperator(relation), current);
69
+ }
70
+
71
+ return current;
72
+ }
73
+
74
+ /**
75
+ * "Every one of these relations exists", as a nested Prisma filter with an empty innermost
76
+ * predicate — `{ parent: { is: { inner: { is: {} } } } }`.
77
+ */
78
+ function buildHopsExistFilter(
79
+ hops: RelationConfig[]
80
+ ): PrismaFilter | undefined {
81
+ return hops.length === 0 ? undefined : wrapInRelations(hops, {});
82
+ }
83
+
84
+ /**
85
+ * "Every intermediate hop of a dotted path exists", as a Prisma filter.
86
+ *
87
+ * CEL cannot dot through a list, so each intermediate segment of `a.b.c` is a to-ONE
88
+ * parent: absent, the caller sends no attribute and CEL raises a missing-path error, which
89
+ * denies. `NOT { categories: { some: { subCategories: { some: P } } } }` is TRUE for a row
90
+ * with no category at all, so the negation returns rows the PDP denies — the parent must be
91
+ * required separately (cerbos/query-plan-adapters#309).
92
+ *
93
+ * Returns undefined when the reference has no intermediate hop, so a direct relation keeps
94
+ * its empty-collection semantics (`!tags.exists(...)` over zero tags is still TRUE).
95
+ */
96
+ export function buildLeadingHopsExistFilter(
97
+ head: RelationConfig,
98
+ restRelations: RelationConfig[]
99
+ ): PrismaFilter | undefined {
100
+ if (restRelations.length === 0) {
101
+ return undefined;
102
+ }
103
+
104
+ // The last entry is the collection being iterated; everything before it is a hop.
105
+ return buildHopsExistFilter([head, ...restRelations.slice(0, -1)]);
106
+ }
107
+
108
+ /**
109
+ * "Every to-one hop this expression dots through exists", as a Prisma filter, or undefined
110
+ * when it dots through none.
111
+ *
112
+ * Prisma has no UNKNOWN: a chained relation filter is exact under the POSITIVE polarity —
113
+ * `categories: { some: { subCategories: { some: P } } }` cannot hold without the category —
114
+ * but `NOT` inverts the hop requirement along with the predicate, so every operator built on
115
+ * a chain readmits the parentless rows once negated. #309 fixed that inside the collection
116
+ * macros, which left the sibling operators reached through the same chain unguarded:
117
+ * membership and `hasIntersection` (cerbos/query-plan-adapters#315), and the negated count
118
+ * spelling `!(size(chain) > 0)` (#316), all of which the macros' guard never saw.
119
+ *
120
+ * Requiring the hops OUTSIDE the negation is what fixes all of them at once, and it is
121
+ * unconditionally faithful to CEL rather than a per-operator patch: a missing hop is a
122
+ * missing-path error, which denies under BOTH polarities, so the requirement never depends
123
+ * on which operator sits above the chain.
124
+ *
125
+ * Lambda bodies are deliberately not walked — their references resolve against a scoped
126
+ * mapper, and a negated macro carries its own guard through the negated collection operators.
127
+ */
128
+ function buildExpressionHopsExistFilter(
129
+ operand: PlanExpressionOperand,
130
+ context: TranslationContext
131
+ ): PrismaFilter | undefined {
132
+ const hops: PrismaFilter[] = [];
133
+ const seen = new Set<string>();
134
+
135
+ const visit = (expr: PlanExpressionOperand): void => {
136
+ if (isNamedOperand(expr)) {
137
+ if (seen.has(expr.name)) {
138
+ return;
139
+ }
140
+
141
+ seen.add(expr.name);
142
+ const { relations } = resolveFieldReference(expr.name, context);
143
+
144
+ if (!relations || relations.length === 0) {
145
+ return;
146
+ }
147
+
148
+ const last = relations[relations.length - 1]!;
149
+
150
+ // Every to-ONE relation on the path is a hop that must EXIST — including a TRAILING one,
151
+ // which is the case a chain of to-many relations never produces. `parent.aBool` resolves
152
+ // to a single to-one relation, and `NOT { parent: { is: { aBool: true } } }` is TRUE for
153
+ // a row with no parent at all, so the negation returns rows the PDP denies. A trailing
154
+ // to-MANY relation is the collection being iterated rather than a hop, and keeps its
155
+ // empty-collection semantics (`!tags.exists(...)` over zero tags is still TRUE).
156
+ const hopFilter = buildHopsExistFilter(
157
+ last.type === "one" ? relations : relations.slice(0, -1)
158
+ );
159
+
160
+ if (hopFilter !== undefined) {
161
+ hops.push(hopFilter);
162
+ }
163
+
164
+ return;
165
+ }
166
+
167
+ if (!isOperatorOperand(expr) || expr.operator === "lambda") {
168
+ return;
169
+ }
170
+
171
+ expr.operands.forEach(visit);
172
+ };
173
+
174
+ visit(operand);
175
+
176
+ if (hops.length === 0) {
177
+ return undefined;
178
+ }
179
+
180
+ return hops.length === 1 ? hops[0]! : { AND: hops };
181
+ }
182
+
183
+ /**
184
+ * `{ NOT: filter }` for a filter translated from a plan sub-expression.
185
+ *
186
+ * An unconditional filter (`{}`, which the hierarchy operators return for a relationship that
187
+ * holds between two constants) cannot be negated this way: Prisma drops an empty condition, so
188
+ * `{ NOT: {} }` is true and matches every row, where the negation of an unconditional condition
189
+ * matches none. Prisma has no field-free false condition to emit instead, so the shape is refused.
190
+ */
191
+ export function negateFilter(filter: PrismaFilter): PrismaFilter {
192
+ if (Object.keys(filter).length === 0) {
193
+ throw new UnsupportedQueryPlanError(
194
+ "Cannot negate an unconditional filter: Prisma evaluates { NOT: {} } as true and matches " +
195
+ "every row, where the negated condition matches none"
196
+ );
197
+ }
198
+
199
+ return { NOT: filter };
200
+ }
201
+
202
+ /**
203
+ * `{ NOT: filter }`, with every to-one hop `operand` dots through required OUTSIDE the
204
+ * negation so an absent parent stays denied (see buildExpressionHopsExistFilter).
205
+ */
206
+ export function negateRequiringHops(
207
+ operand: PlanExpressionOperand,
208
+ filter: PrismaFilter,
209
+ context: TranslationContext
210
+ ): PrismaFilter {
211
+ const hops = buildExpressionHopsExistFilter(operand, context);
212
+ const negated = negateFilter(filter);
213
+
214
+ return hops === undefined ? negated : { AND: [hops, negated] };
215
+ }
216
+
217
+ /**
218
+ * Whether negating `expr` needs a hop requirement (see buildExpressionHopsExistFilter): it dots
219
+ * through at least one to-one hop, which is absent on a parentless row.
220
+ */
221
+ export function referencesRequiredHop(
222
+ expr: PlanExpressionOperand,
223
+ context: TranslationContext
224
+ ): boolean {
225
+ return buildExpressionHopsExistFilter(expr, context) !== undefined;
226
+ }