@nmnmcc/ability 1.0.1 → 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.
- package/README.md +112 -69
- package/dist/Authorizer.d.ts +43 -47
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js +5 -290
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +164 -332
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +75 -890
- package/dist/Condition.js.map +1 -1
- package/dist/Decision.d.ts +13 -13
- package/dist/Decision.d.ts.map +1 -1
- package/dist/Decision.js.map +1 -1
- package/dist/Domain.d.ts +10 -21
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +4 -62
- package/dist/Domain.js.map +1 -1
- package/dist/HttpApi.d.ts +31 -7
- package/dist/HttpApi.d.ts.map +1 -1
- package/dist/HttpApi.js +25 -8
- package/dist/HttpApi.js.map +1 -1
- package/dist/Limits.d.ts +114 -0
- package/dist/Limits.d.ts.map +1 -0
- package/dist/Limits.js +132 -0
- package/dist/Limits.js.map +1 -0
- package/dist/Policy.d.ts +271 -269
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +68 -1177
- package/dist/Policy.js.map +1 -1
- package/dist/Resource.d.ts +4 -177
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +4 -803
- package/dist/Resource.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -5
- package/dist/internal/algebra.js +267 -0
- package/dist/internal/algebra.js.map +1 -0
- package/dist/internal/analysis.js +245 -0
- package/dist/internal/analysis.js.map +1 -0
- package/dist/internal/authorizer.d.ts +30 -0
- package/dist/internal/authorizer.d.ts.map +1 -0
- package/dist/internal/authorizer.js +221 -0
- package/dist/internal/authorizer.js.map +1 -0
- package/dist/internal/bind.js +286 -0
- package/dist/internal/bind.js.map +1 -0
- package/dist/internal/catalog.d.ts +56 -0
- package/dist/internal/catalog.d.ts.map +1 -0
- package/dist/internal/catalog.js +292 -0
- package/dist/internal/catalog.js.map +1 -0
- package/dist/internal/decide.js +166 -0
- package/dist/internal/decide.js.map +1 -0
- package/dist/internal/domain.js +32 -0
- package/dist/internal/domain.js.map +1 -0
- package/dist/internal/json.js +134 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/limits.d.ts +14 -0
- package/dist/internal/limits.d.ts.map +1 -0
- package/dist/internal/limits.js +43 -0
- package/dist/internal/limits.js.map +1 -0
- package/dist/internal/nodes.d.ts +121 -0
- package/dist/internal/nodes.d.ts.map +1 -0
- package/dist/internal/nodes.js +277 -0
- package/dist/internal/nodes.js.map +1 -0
- package/dist/internal/patterns.js +110 -0
- package/dist/internal/patterns.js.map +1 -0
- package/dist/internal/policy-json.js +255 -0
- package/dist/internal/policy-json.js.map +1 -0
- package/dist/internal/policy-nodes.d.ts +61 -0
- package/dist/internal/policy-nodes.d.ts.map +1 -0
- package/dist/internal/policy-nodes.js +146 -0
- package/dist/internal/policy-nodes.js.map +1 -0
- package/dist/internal/policy.d.ts +42 -0
- package/dist/internal/policy.d.ts.map +1 -0
- package/dist/internal/policy.js +124 -0
- package/dist/internal/policy.js.map +1 -0
- package/dist/internal/read.js +39 -0
- package/dist/internal/read.js.map +1 -0
- package/dist/internal/resource.js +115 -0
- package/dist/internal/resource.js.map +1 -0
- package/dist/internal/rule.d.ts +26 -0
- package/dist/internal/rule.d.ts.map +1 -0
- package/dist/internal/rule.js +93 -0
- package/dist/internal/rule.js.map +1 -0
- package/dist/internal/search.d.ts +22 -0
- package/dist/internal/search.d.ts.map +1 -0
- package/dist/internal/search.js +274 -0
- package/dist/internal/search.js.map +1 -0
- package/dist/internal/suggest.js +46 -0
- package/dist/internal/suggest.js.map +1 -0
- package/package.json +3 -4
- package/src/Authorizer.ts +67 -583
- package/src/Condition.ts +339 -2095
- package/src/Decision.ts +6 -5
- package/src/Domain.ts +12 -69
- package/src/HttpApi.ts +72 -10
- package/src/Limits.ts +132 -0
- package/src/Policy.ts +386 -2772
- package/src/Resource.ts +7 -1548
- package/src/index.ts +3 -9
- package/src/internal/algebra.ts +482 -0
- package/src/internal/analysis.ts +478 -0
- package/src/internal/authorizer.ts +418 -0
- package/src/internal/bind.ts +596 -0
- package/src/internal/catalog.ts +534 -0
- package/src/internal/decide.ts +402 -0
- package/src/internal/domain.ts +66 -0
- package/src/internal/json.ts +260 -0
- package/src/internal/limits.ts +78 -0
- package/src/internal/nodes.ts +404 -0
- package/src/internal/patterns.ts +211 -0
- package/src/internal/policy-json.ts +484 -0
- package/src/internal/policy-nodes.ts +207 -0
- package/src/internal/policy.ts +264 -0
- package/src/internal/read.ts +55 -0
- package/src/internal/resource.ts +180 -0
- package/src/internal/rule.ts +181 -0
- package/src/internal/search.ts +541 -0
- package/src/internal/suggest.ts +104 -0
- package/dist/Principal.d.ts +0 -35
- package/dist/Principal.d.ts.map +0 -1
- package/dist/Principal.js +0 -37
- package/dist/Principal.js.map +0 -1
- package/dist/Rule.d.ts +0 -275
- package/dist/Rule.d.ts.map +0 -1
- package/dist/Rule.js +0 -257
- package/dist/Rule.js.map +0 -1
- package/src/Principal.ts +0 -41
- 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.
|
|
37
|
+
* @since 2.0.0
|
|
38
38
|
*/
|
|
39
|
-
export * as
|
|
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
|
|
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
|
+
}
|