@nmnmcc/ability 1.0.0 → 2.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 (127) hide show
  1. package/README.md +143 -87
  2. package/dist/Authorizer.d.ts +43 -47
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +5 -290
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +164 -332
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +75 -890
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Decision.d.ts +13 -13
  11. package/dist/Decision.d.ts.map +1 -1
  12. package/dist/Decision.js.map +1 -1
  13. package/dist/Domain.d.ts +10 -21
  14. package/dist/Domain.d.ts.map +1 -1
  15. package/dist/Domain.js +4 -62
  16. package/dist/Domain.js.map +1 -1
  17. package/dist/HttpApi.d.ts +31 -7
  18. package/dist/HttpApi.d.ts.map +1 -1
  19. package/dist/HttpApi.js +28 -11
  20. package/dist/HttpApi.js.map +1 -1
  21. package/dist/Limits.d.ts +114 -0
  22. package/dist/Limits.d.ts.map +1 -0
  23. package/dist/Limits.js +132 -0
  24. package/dist/Limits.js.map +1 -0
  25. package/dist/Policy.d.ts +274 -272
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +71 -1180
  28. package/dist/Policy.js.map +1 -1
  29. package/dist/Resource.d.ts +8 -181
  30. package/dist/Resource.d.ts.map +1 -1
  31. package/dist/Resource.js +8 -807
  32. package/dist/Resource.js.map +1 -1
  33. package/dist/index.d.ts +3 -4
  34. package/dist/index.js +4 -5
  35. package/dist/internal/algebra.js +267 -0
  36. package/dist/internal/algebra.js.map +1 -0
  37. package/dist/internal/analysis.js +245 -0
  38. package/dist/internal/analysis.js.map +1 -0
  39. package/dist/internal/authorizer.d.ts +30 -0
  40. package/dist/internal/authorizer.d.ts.map +1 -0
  41. package/dist/internal/authorizer.js +221 -0
  42. package/dist/internal/authorizer.js.map +1 -0
  43. package/dist/internal/bind.js +286 -0
  44. package/dist/internal/bind.js.map +1 -0
  45. package/dist/internal/catalog.d.ts +56 -0
  46. package/dist/internal/catalog.d.ts.map +1 -0
  47. package/dist/internal/catalog.js +292 -0
  48. package/dist/internal/catalog.js.map +1 -0
  49. package/dist/internal/decide.js +166 -0
  50. package/dist/internal/decide.js.map +1 -0
  51. package/dist/internal/domain.js +32 -0
  52. package/dist/internal/domain.js.map +1 -0
  53. package/dist/internal/json.js +134 -0
  54. package/dist/internal/json.js.map +1 -0
  55. package/dist/internal/limits.d.ts +14 -0
  56. package/dist/internal/limits.d.ts.map +1 -0
  57. package/dist/internal/limits.js +43 -0
  58. package/dist/internal/limits.js.map +1 -0
  59. package/dist/internal/nodes.d.ts +121 -0
  60. package/dist/internal/nodes.d.ts.map +1 -0
  61. package/dist/internal/nodes.js +277 -0
  62. package/dist/internal/nodes.js.map +1 -0
  63. package/dist/internal/patterns.js +110 -0
  64. package/dist/internal/patterns.js.map +1 -0
  65. package/dist/internal/policy-json.js +255 -0
  66. package/dist/internal/policy-json.js.map +1 -0
  67. package/dist/internal/policy-nodes.d.ts +61 -0
  68. package/dist/internal/policy-nodes.d.ts.map +1 -0
  69. package/dist/internal/policy-nodes.js +146 -0
  70. package/dist/internal/policy-nodes.js.map +1 -0
  71. package/dist/internal/policy.d.ts +42 -0
  72. package/dist/internal/policy.d.ts.map +1 -0
  73. package/dist/internal/policy.js +124 -0
  74. package/dist/internal/policy.js.map +1 -0
  75. package/dist/internal/read.js +39 -0
  76. package/dist/internal/read.js.map +1 -0
  77. package/dist/internal/resource.js +115 -0
  78. package/dist/internal/resource.js.map +1 -0
  79. package/dist/internal/rule.d.ts +26 -0
  80. package/dist/internal/rule.d.ts.map +1 -0
  81. package/dist/internal/rule.js +93 -0
  82. package/dist/internal/rule.js.map +1 -0
  83. package/dist/internal/search.d.ts +22 -0
  84. package/dist/internal/search.d.ts.map +1 -0
  85. package/dist/internal/search.js +274 -0
  86. package/dist/internal/search.js.map +1 -0
  87. package/dist/internal/suggest.js +46 -0
  88. package/dist/internal/suggest.js.map +1 -0
  89. package/package.json +4 -5
  90. package/src/Authorizer.ts +69 -585
  91. package/src/Condition.ts +339 -2095
  92. package/src/Decision.ts +6 -5
  93. package/src/Domain.ts +13 -70
  94. package/src/HttpApi.ts +75 -13
  95. package/src/Limits.ts +132 -0
  96. package/src/Policy.ts +386 -2772
  97. package/src/Resource.ts +14 -1555
  98. package/src/index.ts +3 -9
  99. package/src/internal/algebra.ts +482 -0
  100. package/src/internal/analysis.ts +478 -0
  101. package/src/internal/authorizer.ts +418 -0
  102. package/src/internal/bind.ts +596 -0
  103. package/src/internal/catalog.ts +534 -0
  104. package/src/internal/decide.ts +402 -0
  105. package/src/internal/domain.ts +66 -0
  106. package/src/internal/json.ts +260 -0
  107. package/src/internal/limits.ts +78 -0
  108. package/src/internal/nodes.ts +404 -0
  109. package/src/internal/patterns.ts +211 -0
  110. package/src/internal/policy-json.ts +484 -0
  111. package/src/internal/policy-nodes.ts +207 -0
  112. package/src/internal/policy.ts +264 -0
  113. package/src/internal/read.ts +55 -0
  114. package/src/internal/resource.ts +180 -0
  115. package/src/internal/rule.ts +181 -0
  116. package/src/internal/search.ts +541 -0
  117. package/src/internal/suggest.ts +104 -0
  118. package/dist/Principal.d.ts +0 -35
  119. package/dist/Principal.d.ts.map +0 -1
  120. package/dist/Principal.js +0 -37
  121. package/dist/Principal.js.map +0 -1
  122. package/dist/Rule.d.ts +0 -275
  123. package/dist/Rule.d.ts.map +0 -1
  124. package/dist/Rule.js +0 -257
  125. package/dist/Rule.js.map +0 -1
  126. package/src/Principal.ts +0 -41
  127. package/src/Rule.ts +0 -683
package/src/index.ts CHANGED
@@ -34,24 +34,18 @@ export * as Domain from "./Domain.ts"
34
34
 
35
35
  /**
36
36
  * @stability stable
37
- * @since 0.3.0
37
+ * @since 2.0.0
38
38
  */
39
- export * as Policy from "./Policy.ts"
39
+ export * as Limits from "./Limits.ts"
40
40
 
41
41
  /**
42
42
  * @stability stable
43
43
  * @since 0.3.0
44
44
  */
45
- export * as Principal from "./Principal.ts"
45
+ export * as Policy from "./Policy.ts"
46
46
 
47
47
  /**
48
48
  * @stability stable
49
49
  * @since 0.3.0
50
50
  */
51
51
  export * as Resource from "./Resource.ts"
52
-
53
- /**
54
- * @stability stable
55
- * @since 0.3.0
56
- */
57
- export * as Rule from "./Rule.ts"
@@ -0,0 +1,482 @@
1
+ /**
2
+ * The work on condition nodes: smart constructors, simplification, the step
3
+ * that puts the principal in, tests on a value, and folds.
4
+ *
5
+ * The constructors of the nodes keep each condition small and low, so each
6
+ * walk here is a plain walk with no memo. A shared child is walked again for
7
+ * each place, and the limits of the nodes keep that work small.
8
+ */
9
+ // A predicate gives an Effect with an error type and a service type that this
10
+ // code does not know, so they are `unknown` here.
11
+ // @effect-diagnostics anyUnknownInErrorContext:off
12
+ import * as Arr from "effect/Array"
13
+ import type * as Context from "effect/Context"
14
+ import * as Effect from "effect/Effect"
15
+ import * as HashSet from "effect/HashSet"
16
+ import * as Option from "effect/Option"
17
+ import type * as Resource from "../Resource.ts"
18
+ import * as Nodes from "./nodes.ts"
19
+ import { readPath } from "./read.ts"
20
+
21
+ type Node = Nodes.Node
22
+
23
+ const { always, never } = Nodes
24
+
25
+ // Smart constructors
26
+
27
+ /**
28
+ * Keeps the first node of each structure, in order. A node with a predicate is
29
+ * equal only to itself, as its test is a function.
30
+ */
31
+ const dedupe = (nodes: ReadonlyArray<Node>): ReadonlyArray<Node> =>
32
+ Arr.flatten(
33
+ Arr.mapAccum(nodes, HashSet.empty<Node>(), (seen, node) => [
34
+ HashSet.add(seen, node),
35
+ HashSet.has(seen, node) ? [] : [node],
36
+ ])[1],
37
+ )
38
+
39
+ /**
40
+ * Removes the children that the other children make useless. In an `Or`,
41
+ * `a OR (a AND b)` is `a`. In an `And`, `a AND (a OR b)` is `a`.
42
+ */
43
+ const absorb = (
44
+ inner: "And" | "Or",
45
+ nodes: ReadonlyArray<Node>,
46
+ ): ReadonlyArray<Node> => {
47
+ const all = HashSet.fromIterable(nodes)
48
+ return Arr.filter(
49
+ nodes,
50
+ (node) =>
51
+ node._tag !== inner ||
52
+ !Arr.some(node.conditions, (part) => HashSet.has(all, part)),
53
+ )
54
+ }
55
+
56
+ /**
57
+ * Gives the result of an `And` with a `False` child, or of an `Or` with a
58
+ * `True` child. The children are tested in order, so a predicate before the
59
+ * constant still runs, and it may fail. When there is such a predicate, the
60
+ * children before the constant stay, so the failure is not lost.
61
+ */
62
+ const shortCircuit = (
63
+ tag: "And" | "Or",
64
+ children: ReadonlyArray<Node>,
65
+ index: number,
66
+ ): Node => {
67
+ const constant = tag === "And" ? never : always
68
+ const neutral = tag === "And" ? "True" : "False"
69
+ const before = Arr.filter(
70
+ Arr.take(children, index),
71
+ (child) => child._tag !== neutral,
72
+ )
73
+ if (!Arr.some(before, (child) => child.opaque)) {
74
+ return constant
75
+ }
76
+ return tag === "And"
77
+ ? new Nodes.And([...before, constant])
78
+ : new Nodes.Or([...before, constant])
79
+ }
80
+
81
+ /**
82
+ * Makes an `And` or an `Or` of simple children, and makes it simple. It looks
83
+ * one level down only:
84
+ * - it puts the children of a child of the same kind into the node;
85
+ * - it stops at a `False` in an `And`, or at a `True` in an `Or`;
86
+ * - it removes `True` from an `And`, `False` from an `Or`, copies, and the
87
+ * children that other children make useless.
88
+ *
89
+ * A predicate may fail, and it may give another answer when it runs again. So
90
+ * when a child has a predicate, the children stay as they are, and each one
91
+ * runs in its place. A child with a predicate is not put into the node either.
92
+ */
93
+ const junction = (tag: "And" | "Or", children: ReadonlyArray<Node>): Node => {
94
+ const constant = tag === "And" ? "False" : "True"
95
+ const neutral = tag === "And" ? "True" : "False"
96
+ const flat = Arr.flatMap(children, (child) =>
97
+ child._tag === tag && !child.opaque ? child.conditions : [child],
98
+ )
99
+ return Option.match(
100
+ Arr.findFirstIndex(flat, (child) => child._tag === constant),
101
+ {
102
+ onSome: (stop) => shortCircuit(tag, flat, stop),
103
+ onNone: () => {
104
+ const kept = Arr.filter(flat, (child) => child._tag !== neutral)
105
+ const unique = Arr.some(kept, (child) => child.opaque)
106
+ ? kept
107
+ : absorb(tag === "And" ? "Or" : "And", dedupe(kept))
108
+ return unique.length === 0
109
+ ? tag === "And"
110
+ ? always
111
+ : never
112
+ : unique.length === 1
113
+ ? unique[0]!
114
+ : tag === "And"
115
+ ? new Nodes.And(unique)
116
+ : new Nodes.Or(unique)
117
+ },
118
+ },
119
+ )
120
+ }
121
+
122
+ /** An `And` of simple children, made simple. */
123
+ export const and = (children: ReadonlyArray<Node>): Node =>
124
+ junction("And", children)
125
+
126
+ /** An `Or` of simple children, made simple. */
127
+ export const or = (children: ReadonlyArray<Node>): Node =>
128
+ junction("Or", children)
129
+
130
+ /** A `Not` of a simple child, made simple. */
131
+ export const not = (child: Node): Node => {
132
+ switch (child._tag) {
133
+ case "True":
134
+ return never
135
+ case "False":
136
+ return always
137
+ case "Not":
138
+ return child.condition
139
+ case "Compare":
140
+ // A principal field with no value makes `eq` and `ne` both false, so
141
+ // only a fixed value lets `not eq` become `ne`.
142
+ return child.value._tag === "Literal" &&
143
+ (child.op === "eq" || child.op === "ne")
144
+ ? new Nodes.Compare({
145
+ op: child.op === "eq" ? "ne" : "eq",
146
+ field: child.field,
147
+ value: child.value,
148
+ })
149
+ : new Nodes.Not(child)
150
+ default:
151
+ return new Nodes.Not(child)
152
+ }
153
+ }
154
+
155
+ /** Makes a condition simple, from its leaves up. */
156
+ export const simplify = (node: Node): Node => {
157
+ switch (node._tag) {
158
+ case "And":
159
+ case "Or":
160
+ return junction(node._tag, Arr.map(node.conditions, simplify))
161
+ case "Not":
162
+ return node.condition.pipe(simplify, not)
163
+ case "In":
164
+ return node.values.length === 0 ? never : node
165
+ default:
166
+ return node
167
+ }
168
+ }
169
+
170
+ // Tests on a value
171
+
172
+ /** The data that a condition needs for a test. */
173
+ export interface Input {
174
+ readonly value: object
175
+ readonly principal?: object | undefined
176
+ readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
177
+ }
178
+
179
+ /**
180
+ * Gets a field of a value by a path with dots. It reads only plain data: see
181
+ * `readPart`. A missing field, or a field under a value that is not an object,
182
+ * gives `null`.
183
+ */
184
+ const getField = (value: unknown, path: string): unknown =>
185
+ readPath(value, path.split("."))
186
+
187
+ /**
188
+ * Gives the value of an operand. A principal field that is `null`, missing, or
189
+ * not a scalar gives `undefined`. No field of a resource is `undefined`, so a
190
+ * test with it is never true. This keeps the check closed: a principal with no
191
+ * id never matches a resource with no owner.
192
+ */
193
+ const resolve = (operand: Nodes.Operand, principal: unknown): unknown => {
194
+ if (operand._tag === "Literal") {
195
+ return operand.value
196
+ }
197
+ const value = getField(principal, operand.path)
198
+ return value !== null && Nodes.isScalar(value) ? value : undefined
199
+ }
200
+
201
+ export const testCompare = (
202
+ op: Nodes.CompareOperator,
203
+ left: unknown,
204
+ right: unknown,
205
+ ): boolean => {
206
+ if (right === undefined) {
207
+ return false
208
+ }
209
+ switch (op) {
210
+ case "eq":
211
+ return left === right
212
+ case "ne":
213
+ return left !== right
214
+ default: {
215
+ const ordered =
216
+ (typeof left === "number" && typeof right === "number") ||
217
+ (typeof left === "string" && typeof right === "string")
218
+ if (!ordered) {
219
+ return false
220
+ }
221
+ const a = left as number | string
222
+ const b = right as number | string
223
+ switch (op) {
224
+ case "lt":
225
+ return a < b
226
+ case "lte":
227
+ return a <= b
228
+ case "gt":
229
+ return a > b
230
+ case "gte":
231
+ return a >= b
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ /** Gives the values of the path of a resource, or `undefined` for no path. */
238
+ const pathValues = (
239
+ value: unknown,
240
+ path: ReadonlyArray<Resource.PathSegment> | undefined,
241
+ ): ReadonlyArray<unknown> | undefined =>
242
+ path?.map((segment) =>
243
+ typeof segment === "string" ? segment : getField(value, segment.field),
244
+ )
245
+
246
+ const testUnder = (
247
+ prefix: ReadonlyArray<Nodes.Operand>,
248
+ input: Input,
249
+ ): boolean => {
250
+ const values = pathValues(input.value, input.path)
251
+ if (values === undefined || prefix.length > values.length) {
252
+ return false
253
+ }
254
+ // An operand with no value never matches, also a hole in a path that gives
255
+ // `undefined` too.
256
+ return prefix.every((operand, index) => {
257
+ const value = resolve(operand, input.principal)
258
+ return value !== undefined && value === values[index]
259
+ })
260
+ }
261
+
262
+ /** Tests a condition with no predicate on a value. */
263
+ export const evaluateSync = (node: Node, input: Input): boolean => {
264
+ switch (node._tag) {
265
+ case "True":
266
+ return true
267
+ case "False":
268
+ return false
269
+ case "Compare":
270
+ return testCompare(
271
+ node.op,
272
+ getField(input.value, node.field),
273
+ resolve(node.value, input.principal),
274
+ )
275
+ case "In": {
276
+ const field = getField(input.value, node.field)
277
+ return node.values.some(
278
+ (operand) => resolve(operand, input.principal) === field,
279
+ )
280
+ }
281
+ case "Exists":
282
+ return getField(input.value, node.field) !== null
283
+ case "Under":
284
+ return testUnder(node.prefix, input)
285
+ case "And":
286
+ return Arr.every(node.conditions, (child) => evaluateSync(child, input))
287
+ case "Or":
288
+ return Arr.some(node.conditions, (child) => evaluateSync(child, input))
289
+ case "Not":
290
+ return !evaluateSync(node.condition, input)
291
+ case "Predicate":
292
+ // The types stop this. A predicate needs `evaluate`.
293
+ throw new Error(
294
+ `The predicate "${node.name}" needs an Effect. Use Condition.evaluate.`,
295
+ )
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Tests a condition on a value. The children of `And` and `Or` run in order,
301
+ * and they stop at the first child that gives the answer.
302
+ */
303
+ export const evaluate = (
304
+ node: Node,
305
+ input: Input,
306
+ ): Effect.Effect<boolean, unknown, unknown> => {
307
+ if (!node.opaque) {
308
+ return Effect.sync(() => evaluateSync(node, input))
309
+ }
310
+ switch (node._tag) {
311
+ case "Predicate":
312
+ return Nodes.runOf(node)(input.value, input.principal)
313
+ case "Not":
314
+ return Effect.map(evaluate(node.condition, input), (value) => !value)
315
+ case "And":
316
+ return Effect.map(
317
+ Effect.findFirst(node.conditions, (child) =>
318
+ Effect.map(evaluate(child, input), (value) => !value),
319
+ ),
320
+ Option.isNone,
321
+ )
322
+ case "Or":
323
+ return Effect.map(
324
+ Effect.findFirst(node.conditions, (child) => evaluate(child, input)),
325
+ Option.isSome,
326
+ )
327
+ default:
328
+ return Effect.sync(() => evaluateSync(node, input))
329
+ }
330
+ }
331
+
332
+ // The principal and the path
333
+
334
+ /**
335
+ * Puts the value of the principal into an operand. It gives `undefined` for a
336
+ * principal field with no value. See {@link resolve}.
337
+ */
338
+ const bindOperand = (
339
+ operand: Nodes.Operand,
340
+ principal: unknown,
341
+ ): Nodes.Literal | undefined => {
342
+ if (operand._tag === "Literal") {
343
+ return operand
344
+ }
345
+ const value = resolve(operand, principal)
346
+ return value === undefined ? undefined : Nodes.literal(value as Nodes.Scalar)
347
+ }
348
+
349
+ const expandUnder = (
350
+ prefix: ReadonlyArray<Nodes.Operand>,
351
+ principal: unknown,
352
+ path: ReadonlyArray<Resource.PathSegment> | undefined,
353
+ ): Node => {
354
+ if (path === undefined || prefix.length > path.length) {
355
+ return never
356
+ }
357
+ return and(
358
+ Arr.map(prefix, (operand, index): Node => {
359
+ const value = resolve(operand, principal)
360
+ const segment = path[index]!
361
+ return value === undefined
362
+ ? never
363
+ : typeof segment === "string"
364
+ ? segment === value
365
+ ? always
366
+ : never
367
+ : new Nodes.Compare({
368
+ op: "eq",
369
+ field: segment.field,
370
+ value: Nodes.literal(value as Nodes.Scalar),
371
+ })
372
+ }),
373
+ )
374
+ }
375
+
376
+ /** The principal and the path of the resource, for {@link partial}. */
377
+ export interface PartialInput {
378
+ readonly principal?: object | undefined
379
+ readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
380
+ }
381
+
382
+ /**
383
+ * Puts the principal into a condition and expands `Under` with the path of the
384
+ * resource. The result is simple. It has no principal fields and no `Under`
385
+ * nodes. A predicate keeps the principal.
386
+ */
387
+ export const partial = (node: Node, input: PartialInput): Node => {
388
+ const bind = (current: Node): Node => {
389
+ switch (current._tag) {
390
+ case "Compare": {
391
+ const value = bindOperand(current.value, input.principal)
392
+ return value === undefined
393
+ ? never
394
+ : value === current.value
395
+ ? current
396
+ : new Nodes.Compare({ op: current.op, field: current.field, value })
397
+ }
398
+ case "In": {
399
+ const values = Arr.flatMap(current.values, (operand) =>
400
+ Option.toArray(
401
+ Option.fromUndefinedOr(bindOperand(operand, input.principal)),
402
+ ),
403
+ )
404
+ return values.length === 0
405
+ ? never
406
+ : new Nodes.In({ field: current.field, values })
407
+ }
408
+ case "Under":
409
+ return expandUnder(current.prefix, input.principal, input.path)
410
+ case "And":
411
+ return and(Arr.map(current.conditions, bind))
412
+ case "Or":
413
+ return or(Arr.map(current.conditions, bind))
414
+ case "Not":
415
+ return current.condition.pipe(bind, not)
416
+ case "Predicate": {
417
+ const run = Nodes.runOf(current)
418
+ return new Nodes.Predicate({
419
+ name: current.name,
420
+ run: (value) => run(value, input.principal),
421
+ })
422
+ }
423
+ default:
424
+ return current
425
+ }
426
+ }
427
+ return bind(node)
428
+ }
429
+
430
+ /** Gives the services in `context` to every predicate of a condition. */
431
+ export const provide = (node: Node, context: Context.Context<never>): Node => {
432
+ if (!node.opaque) {
433
+ return node
434
+ }
435
+ switch (node._tag) {
436
+ case "Predicate": {
437
+ const run = Nodes.runOf(node)
438
+ return new Nodes.Predicate({
439
+ name: node.name,
440
+ run: (value, principal) =>
441
+ Effect.provideContext(run(value, principal), context),
442
+ })
443
+ }
444
+ case "And":
445
+ return new Nodes.And(
446
+ Arr.map(node.conditions, (child) => provide(child, context)),
447
+ )
448
+ case "Or":
449
+ return new Nodes.Or(
450
+ Arr.map(node.conditions, (child) => provide(child, context)),
451
+ )
452
+ case "Not":
453
+ return new Nodes.Not(provide(node.condition, context))
454
+ default:
455
+ return node
456
+ }
457
+ }
458
+
459
+ /** The fields that a condition tests, in the order that they first come. */
460
+ export const fields = (node: Node): ReadonlyArray<string> => {
461
+ const all = (current: Node): ReadonlyArray<string> => {
462
+ switch (current._tag) {
463
+ case "Compare":
464
+ case "In":
465
+ case "Exists":
466
+ return [current.field]
467
+ case "And":
468
+ case "Or":
469
+ return Arr.flatMap(current.conditions, all)
470
+ case "Not":
471
+ return all(current.condition)
472
+ default:
473
+ return []
474
+ }
475
+ }
476
+ return Arr.flatten(
477
+ Arr.mapAccum(all(node), HashSet.empty<string>(), (seen, field) => [
478
+ HashSet.add(seen, field),
479
+ HashSet.has(seen, field) ? [] : [field],
480
+ ])[1],
481
+ )
482
+ }