@nmnmcc/ability 0.1.0 → 0.3.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 +353 -170
- package/dist/Authorizer.d.ts +216 -0
- package/dist/Authorizer.d.ts.map +1 -0
- package/dist/Authorizer.js +55 -0
- package/dist/Authorizer.js.map +1 -0
- package/dist/Condition.d.ts +536 -0
- package/dist/Condition.d.ts.map +1 -0
- package/dist/Condition.js +141 -0
- package/dist/Condition.js.map +1 -0
- package/dist/Decision.d.ts +131 -0
- package/dist/Decision.d.ts.map +1 -0
- package/dist/Decision.js +83 -0
- package/dist/Decision.js.map +1 -0
- package/dist/Domain.d.ts +148 -0
- package/dist/Domain.d.ts.map +1 -0
- package/dist/Domain.js +34 -0
- package/dist/Domain.js.map +1 -0
- package/dist/HttpApi.d.ts +129 -0
- package/dist/HttpApi.d.ts.map +1 -0
- package/dist/HttpApi.js +95 -0
- package/dist/HttpApi.js.map +1 -0
- package/dist/Policy.d.ts +429 -0
- package/dist/Policy.d.ts.map +1 -0
- package/dist/Policy.js +162 -0
- package/dist/Policy.js.map +1 -0
- package/dist/Principal.d.ts +35 -0
- package/dist/Principal.d.ts.map +1 -0
- package/dist/Principal.js +37 -0
- package/dist/Principal.js.map +1 -0
- package/dist/Resource.d.ts +213 -0
- package/dist/Resource.d.ts.map +1 -0
- package/dist/Resource.js +71 -0
- package/dist/Resource.js.map +1 -0
- package/dist/Rule.d.ts +189 -0
- package/dist/Rule.d.ts.map +1 -0
- package/dist/Rule.js +50 -0
- package/dist/Rule.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +1 -2
- package/dist/index.d.ts +9 -4
- package/dist/index.js +9 -4
- package/dist/internal/authorizer.d.ts +28 -0
- package/dist/internal/authorizer.d.ts.map +1 -0
- package/dist/internal/authorizer.js +153 -0
- package/dist/internal/authorizer.js.map +1 -0
- package/dist/internal/condition.d.ts +11 -0
- package/dist/internal/condition.d.ts.map +1 -0
- package/dist/internal/condition.js +393 -0
- package/dist/internal/condition.js.map +1 -0
- package/dist/internal/domain.js +34 -0
- package/dist/internal/domain.js.map +1 -0
- package/dist/internal/httpApi.js +41 -0
- package/dist/internal/httpApi.js.map +1 -0
- package/dist/internal/policy.js +524 -0
- package/dist/internal/policy.js.map +1 -0
- package/dist/internal/resource.js +66 -0
- package/dist/internal/resource.js.map +1 -0
- package/dist/internal/rule.js +131 -0
- package/dist/internal/rule.js.map +1 -0
- package/package.json +61 -74
- package/src/Authorizer.ts +330 -0
- package/src/Condition.ts +706 -0
- package/src/Decision.ts +117 -0
- package/src/Domain.ts +189 -0
- package/src/HttpApi.ts +148 -0
- package/src/Policy.ts +575 -0
- package/src/Principal.ts +41 -0
- package/src/Resource.ts +290 -0
- package/src/Rule.ts +265 -0
- package/src/index.ts +57 -0
- package/src/internal/authorizer.ts +271 -0
- package/src/internal/condition.ts +607 -0
- package/src/internal/domain.ts +56 -0
- package/src/internal/httpApi.ts +84 -0
- package/src/internal/policy.ts +815 -0
- package/src/internal/resource.ts +98 -0
- package/src/internal/rule.ts +203 -0
- package/dist/Ability.d.ts +0 -516
- package/dist/Ability.js +0 -211
- package/dist/AbilityExtra.d.ts +0 -79
- package/dist/AbilityExtra.js +0 -195
- package/dist/AbilityRef.d.ts +0 -110
- package/dist/AbilityRef.js +0 -160
- package/dist/internal/ability.d.ts +0 -114
- package/dist/internal/ability.js +0 -624
- package/dist/internal/serializable.js +0 -22
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
// A predicate keeps its error type and service type only in the public types.
|
|
2
|
+
// Inside this file they are erased to `unknown`, so the language service check
|
|
3
|
+
// for `unknown` in an Effect is off here.
|
|
4
|
+
// @effect-diagnostics anyUnknownInErrorContext:off
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
6
|
+
import * as Equal from "effect/Equal"
|
|
7
|
+
import { dual } from "effect/Function"
|
|
8
|
+
import * as Predicate from "effect/Predicate"
|
|
9
|
+
import * as Schema from "effect/Schema"
|
|
10
|
+
import type * as Condition from "../Condition.ts"
|
|
11
|
+
import type * as Resource from "../Resource.ts"
|
|
12
|
+
|
|
13
|
+
type Ast = Condition.Ast
|
|
14
|
+
type Scalar = Condition.Scalar
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
17
|
+
export const always: Condition.True = Object.freeze({ _tag: "True" })
|
|
18
|
+
|
|
19
|
+
/** @internal */
|
|
20
|
+
export const never: Condition.False = Object.freeze({ _tag: "False" })
|
|
21
|
+
|
|
22
|
+
const literal = (value: Scalar): Condition.Literal =>
|
|
23
|
+
Object.freeze({ _tag: "Literal", value })
|
|
24
|
+
|
|
25
|
+
const isOperand = (u: unknown): u is Condition.Operand =>
|
|
26
|
+
Predicate.hasProperty(u, "_tag") &&
|
|
27
|
+
(u._tag === "Literal" || u._tag === "Principal")
|
|
28
|
+
|
|
29
|
+
const toOperand = (value: unknown): Condition.Operand =>
|
|
30
|
+
isOperand(value) ? value : literal(value as Scalar)
|
|
31
|
+
|
|
32
|
+
const compare =
|
|
33
|
+
(op: Condition.CompareOperator) =>
|
|
34
|
+
(field: string, value: unknown): Condition.Compare =>
|
|
35
|
+
Object.freeze({ _tag: "Compare", op, field, value: toOperand(value) })
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
export const and = (...conditions: ReadonlyArray<Ast>): Condition.And =>
|
|
39
|
+
Object.freeze({ _tag: "And", conditions: Object.freeze([...conditions]) })
|
|
40
|
+
|
|
41
|
+
/** @internal */
|
|
42
|
+
export const or = (...conditions: ReadonlyArray<Ast>): Condition.Or =>
|
|
43
|
+
Object.freeze({ _tag: "Or", conditions: Object.freeze([...conditions]) })
|
|
44
|
+
|
|
45
|
+
/** @internal */
|
|
46
|
+
export const not = (condition: Ast): Condition.Not =>
|
|
47
|
+
Object.freeze({ _tag: "Not", condition })
|
|
48
|
+
|
|
49
|
+
/** @internal */
|
|
50
|
+
export const builder = (): any =>
|
|
51
|
+
Object.freeze({
|
|
52
|
+
eq: compare("eq"),
|
|
53
|
+
ne: compare("ne"),
|
|
54
|
+
lt: compare("lt"),
|
|
55
|
+
lte: compare("lte"),
|
|
56
|
+
gt: compare("gt"),
|
|
57
|
+
gte: compare("gte"),
|
|
58
|
+
in: (field: string, values: ReadonlyArray<unknown>): Condition.In =>
|
|
59
|
+
Object.freeze({
|
|
60
|
+
_tag: "In",
|
|
61
|
+
field,
|
|
62
|
+
values: Object.freeze(values.map(toOperand)),
|
|
63
|
+
}),
|
|
64
|
+
exists: (field: string): Condition.Exists =>
|
|
65
|
+
Object.freeze({ _tag: "Exists", field }),
|
|
66
|
+
under: (...prefix: ReadonlyArray<unknown>): Condition.Under =>
|
|
67
|
+
Object.freeze({
|
|
68
|
+
_tag: "Under",
|
|
69
|
+
prefix: Object.freeze(prefix.map(toOperand)),
|
|
70
|
+
}),
|
|
71
|
+
principal: (path: string): Condition.PrincipalRef =>
|
|
72
|
+
Object.freeze({ _tag: "Principal", path }),
|
|
73
|
+
and,
|
|
74
|
+
or,
|
|
75
|
+
not,
|
|
76
|
+
predicate: (
|
|
77
|
+
name: string,
|
|
78
|
+
test: (
|
|
79
|
+
value: unknown,
|
|
80
|
+
principal: unknown,
|
|
81
|
+
) => boolean | Effect.Effect<boolean, unknown, unknown>,
|
|
82
|
+
): Condition.Predicate =>
|
|
83
|
+
Object.freeze({
|
|
84
|
+
_tag: "Predicate",
|
|
85
|
+
name,
|
|
86
|
+
run: (value: unknown, principal: unknown) =>
|
|
87
|
+
Effect.suspend(() => {
|
|
88
|
+
const result = test(value, principal)
|
|
89
|
+
return Effect.isEffect(result) ? result : Effect.succeed(result)
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gets a field of an object by a path with dots. A missing field, or a field
|
|
96
|
+
* under a value that is not an object, gives `null`.
|
|
97
|
+
*
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
export const getField = (value: unknown, path: string): unknown => {
|
|
101
|
+
let cursor: unknown = value
|
|
102
|
+
for (const key of path.split(".")) {
|
|
103
|
+
if (!Predicate.isObject(cursor) || !Object.hasOwn(cursor, key)) {
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
cursor = (cursor as Record<string, unknown>)[key]
|
|
107
|
+
}
|
|
108
|
+
return cursor === undefined ? null : cursor
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gives the value of an operand. A principal field that is `null` or missing
|
|
113
|
+
* gives `undefined`. No field of a resource is `undefined`, so a test with it
|
|
114
|
+
* is never true. This keeps the check closed: a principal with no id never
|
|
115
|
+
* matches a resource with no owner.
|
|
116
|
+
*/
|
|
117
|
+
const resolve = (operand: Condition.Operand, principal: unknown): unknown =>
|
|
118
|
+
operand._tag === "Literal"
|
|
119
|
+
? operand.value
|
|
120
|
+
: (getField(principal, operand.path) ?? undefined)
|
|
121
|
+
|
|
122
|
+
const testCompare = (
|
|
123
|
+
op: Condition.CompareOperator,
|
|
124
|
+
left: unknown,
|
|
125
|
+
right: unknown,
|
|
126
|
+
): boolean => {
|
|
127
|
+
if (right === undefined) {
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
switch (op) {
|
|
131
|
+
case "eq":
|
|
132
|
+
return left === right
|
|
133
|
+
case "ne":
|
|
134
|
+
return left !== right
|
|
135
|
+
default: {
|
|
136
|
+
const ordered =
|
|
137
|
+
(typeof left === "number" && typeof right === "number") ||
|
|
138
|
+
(typeof left === "string" && typeof right === "string")
|
|
139
|
+
if (!ordered) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
const a = left as number | string
|
|
143
|
+
const b = right as number | string
|
|
144
|
+
switch (op) {
|
|
145
|
+
case "lt":
|
|
146
|
+
return a < b
|
|
147
|
+
case "lte":
|
|
148
|
+
return a <= b
|
|
149
|
+
case "gt":
|
|
150
|
+
return a > b
|
|
151
|
+
case "gte":
|
|
152
|
+
return a >= b
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gives the values of a resource path. It gives `undefined` when the resource
|
|
160
|
+
* has no path.
|
|
161
|
+
*/
|
|
162
|
+
const pathValues = (
|
|
163
|
+
value: unknown,
|
|
164
|
+
path: ReadonlyArray<Resource.PathSegment> | undefined,
|
|
165
|
+
): ReadonlyArray<unknown> | undefined =>
|
|
166
|
+
path?.map((segment) =>
|
|
167
|
+
typeof segment === "string" ? segment : getField(value, segment.field),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const testUnder = (
|
|
171
|
+
prefix: ReadonlyArray<Condition.Operand>,
|
|
172
|
+
input: Condition.Input,
|
|
173
|
+
): boolean => {
|
|
174
|
+
const values = pathValues(input.value, input.path)
|
|
175
|
+
if (values === undefined || prefix.length > values.length) {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
return prefix.every(
|
|
179
|
+
(operand, index) => resolve(operand, input.principal) === values[index],
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** @internal */
|
|
184
|
+
export const hasPredicate = (ast: Ast): boolean => {
|
|
185
|
+
switch (ast._tag) {
|
|
186
|
+
case "Predicate":
|
|
187
|
+
return true
|
|
188
|
+
case "And":
|
|
189
|
+
case "Or":
|
|
190
|
+
return ast.conditions.some(hasPredicate)
|
|
191
|
+
case "Not":
|
|
192
|
+
return hasPredicate(ast.condition)
|
|
193
|
+
default:
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** @internal */
|
|
199
|
+
export const evaluateSyncOf = (ast: Ast, input: Condition.Input): boolean => {
|
|
200
|
+
switch (ast._tag) {
|
|
201
|
+
case "True":
|
|
202
|
+
return true
|
|
203
|
+
case "False":
|
|
204
|
+
return false
|
|
205
|
+
case "Compare":
|
|
206
|
+
return testCompare(
|
|
207
|
+
ast.op,
|
|
208
|
+
getField(input.value, ast.field),
|
|
209
|
+
resolve(ast.value, input.principal),
|
|
210
|
+
)
|
|
211
|
+
case "In": {
|
|
212
|
+
const field = getField(input.value, ast.field)
|
|
213
|
+
return ast.values.some(
|
|
214
|
+
(operand) => resolve(operand, input.principal) === field,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
case "Exists":
|
|
218
|
+
return getField(input.value, ast.field) !== null
|
|
219
|
+
case "Under":
|
|
220
|
+
return testUnder(ast.prefix, input)
|
|
221
|
+
case "And":
|
|
222
|
+
return ast.conditions.every((child) => evaluateSyncOf(child, input))
|
|
223
|
+
case "Or":
|
|
224
|
+
return ast.conditions.some((child) => evaluateSyncOf(child, input))
|
|
225
|
+
case "Not":
|
|
226
|
+
return !evaluateSyncOf(ast.condition, input)
|
|
227
|
+
case "Predicate":
|
|
228
|
+
// The types stop this. A predicate needs `evaluate`.
|
|
229
|
+
throw new Error(
|
|
230
|
+
`The predicate "${ast.name}" needs an Effect. Use Condition.evaluate.`,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @internal */
|
|
236
|
+
export const evaluateOf = (
|
|
237
|
+
ast: Ast,
|
|
238
|
+
input: Condition.Input,
|
|
239
|
+
): Effect.Effect<boolean, unknown, unknown> => {
|
|
240
|
+
if (!hasPredicate(ast)) {
|
|
241
|
+
return Effect.sync(() => evaluateSyncOf(ast, input))
|
|
242
|
+
}
|
|
243
|
+
switch (ast._tag) {
|
|
244
|
+
case "Predicate":
|
|
245
|
+
return ast.run(input.value, input.principal)
|
|
246
|
+
case "Not":
|
|
247
|
+
return Effect.map(evaluateOf(ast.condition, input), (value) => !value)
|
|
248
|
+
case "And":
|
|
249
|
+
return Effect.gen(function* () {
|
|
250
|
+
for (const child of ast.conditions) {
|
|
251
|
+
if (!(yield* evaluateOf(child, input))) {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return true
|
|
256
|
+
})
|
|
257
|
+
case "Or":
|
|
258
|
+
return Effect.gen(function* () {
|
|
259
|
+
for (const child of ast.conditions) {
|
|
260
|
+
if (yield* evaluateOf(child, input)) {
|
|
261
|
+
return true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return false
|
|
265
|
+
})
|
|
266
|
+
default:
|
|
267
|
+
return Effect.sync(() => evaluateSyncOf(ast, input))
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const dedupe = (conditions: ReadonlyArray<Ast>): ReadonlyArray<Ast> => {
|
|
272
|
+
const output: Array<Ast> = []
|
|
273
|
+
for (const condition of conditions) {
|
|
274
|
+
if (!output.some((other) => Equal.equals(other, condition))) {
|
|
275
|
+
output.push(condition)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return output
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Removes the children that the other children make useless. In an `Or`,
|
|
283
|
+
* `a OR (a AND b)` is `a`. In an `And`, `a AND (a OR b)` is `a`.
|
|
284
|
+
*/
|
|
285
|
+
const absorb = (
|
|
286
|
+
inner: "And" | "Or",
|
|
287
|
+
conditions: ReadonlyArray<Ast>,
|
|
288
|
+
): ReadonlyArray<Ast> =>
|
|
289
|
+
conditions.filter(
|
|
290
|
+
(condition) =>
|
|
291
|
+
condition._tag !== inner ||
|
|
292
|
+
!conditions.some(
|
|
293
|
+
(other) =>
|
|
294
|
+
other !== condition &&
|
|
295
|
+
condition.conditions.some((part) => Equal.equals(part, other)),
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
const flatten = (
|
|
300
|
+
tag: "And" | "Or",
|
|
301
|
+
conditions: ReadonlyArray<Ast>,
|
|
302
|
+
): ReadonlyArray<Ast> =>
|
|
303
|
+
conditions.flatMap((child) => {
|
|
304
|
+
const simple = simplify(child)
|
|
305
|
+
return simple._tag === tag ? simple.conditions : [simple]
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
/** @internal */
|
|
309
|
+
export const simplify = (ast: Ast): Ast => {
|
|
310
|
+
switch (ast._tag) {
|
|
311
|
+
case "And": {
|
|
312
|
+
const children = flatten("And", ast.conditions).filter(
|
|
313
|
+
(child) => child._tag !== "True",
|
|
314
|
+
)
|
|
315
|
+
if (children.some((child) => child._tag === "False")) {
|
|
316
|
+
return never
|
|
317
|
+
}
|
|
318
|
+
const unique = absorb("Or", dedupe(children))
|
|
319
|
+
if (unique.length === 0) {
|
|
320
|
+
return always
|
|
321
|
+
}
|
|
322
|
+
return unique.length === 1 ? unique[0]! : and(...unique)
|
|
323
|
+
}
|
|
324
|
+
case "Or": {
|
|
325
|
+
const children = flatten("Or", ast.conditions).filter(
|
|
326
|
+
(child) => child._tag !== "False",
|
|
327
|
+
)
|
|
328
|
+
if (children.some((child) => child._tag === "True")) {
|
|
329
|
+
return always
|
|
330
|
+
}
|
|
331
|
+
const unique = absorb("And", dedupe(children))
|
|
332
|
+
if (unique.length === 0) {
|
|
333
|
+
return never
|
|
334
|
+
}
|
|
335
|
+
return unique.length === 1 ? unique[0]! : or(...unique)
|
|
336
|
+
}
|
|
337
|
+
case "Not": {
|
|
338
|
+
const child = simplify(ast.condition)
|
|
339
|
+
switch (child._tag) {
|
|
340
|
+
case "True":
|
|
341
|
+
return never
|
|
342
|
+
case "False":
|
|
343
|
+
return always
|
|
344
|
+
case "Not":
|
|
345
|
+
return child.condition
|
|
346
|
+
case "Compare":
|
|
347
|
+
// A principal field with no value makes `eq` and `ne` both false,
|
|
348
|
+
// so only a fixed value lets `not eq` become `ne`.
|
|
349
|
+
if (
|
|
350
|
+
child.value._tag === "Literal" &&
|
|
351
|
+
(child.op === "eq" || child.op === "ne")
|
|
352
|
+
) {
|
|
353
|
+
return Object.freeze({
|
|
354
|
+
...child,
|
|
355
|
+
op: child.op === "eq" ? "ne" : "eq",
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
return not(child)
|
|
359
|
+
default:
|
|
360
|
+
return not(child)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
case "In":
|
|
364
|
+
return ast.values.length === 0 ? never : ast
|
|
365
|
+
default:
|
|
366
|
+
return ast
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Puts the value of the principal into an operand. It gives `undefined` for a
|
|
372
|
+
* principal field with no value. See {@link resolve}.
|
|
373
|
+
*/
|
|
374
|
+
const bindOperand = (
|
|
375
|
+
operand: Condition.Operand,
|
|
376
|
+
principal: unknown,
|
|
377
|
+
): Condition.Literal | undefined => {
|
|
378
|
+
if (operand._tag === "Literal") {
|
|
379
|
+
return operand
|
|
380
|
+
}
|
|
381
|
+
const value = resolve(operand, principal)
|
|
382
|
+
return value === undefined ? undefined : literal(value as Scalar)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const expandUnder = (
|
|
386
|
+
prefix: ReadonlyArray<Condition.Operand>,
|
|
387
|
+
principal: unknown,
|
|
388
|
+
path: ReadonlyArray<Resource.PathSegment> | undefined,
|
|
389
|
+
): Ast => {
|
|
390
|
+
if (path === undefined || prefix.length > path.length) {
|
|
391
|
+
return never
|
|
392
|
+
}
|
|
393
|
+
const parts: Array<Ast> = []
|
|
394
|
+
prefix.forEach((operand, index) => {
|
|
395
|
+
const value = resolve(operand, principal)
|
|
396
|
+
const segment = path[index]!
|
|
397
|
+
if (value === undefined) {
|
|
398
|
+
parts.push(never)
|
|
399
|
+
} else if (typeof segment === "string") {
|
|
400
|
+
parts.push(segment === value ? always : never)
|
|
401
|
+
} else {
|
|
402
|
+
parts.push(
|
|
403
|
+
Object.freeze({
|
|
404
|
+
_tag: "Compare",
|
|
405
|
+
op: "eq",
|
|
406
|
+
field: segment.field,
|
|
407
|
+
value: literal(value as Scalar),
|
|
408
|
+
}),
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
return and(...parts)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const bind = (
|
|
416
|
+
ast: Ast,
|
|
417
|
+
principal: unknown,
|
|
418
|
+
path: ReadonlyArray<Resource.PathSegment> | undefined,
|
|
419
|
+
): Ast => {
|
|
420
|
+
switch (ast._tag) {
|
|
421
|
+
case "Compare": {
|
|
422
|
+
const value = bindOperand(ast.value, principal)
|
|
423
|
+
return value === undefined ? never : Object.freeze({ ...ast, value })
|
|
424
|
+
}
|
|
425
|
+
case "In":
|
|
426
|
+
return Object.freeze({
|
|
427
|
+
...ast,
|
|
428
|
+
values: Object.freeze(
|
|
429
|
+
ast.values.flatMap((operand) => {
|
|
430
|
+
const value = bindOperand(operand, principal)
|
|
431
|
+
return value === undefined ? [] : [value]
|
|
432
|
+
}),
|
|
433
|
+
),
|
|
434
|
+
})
|
|
435
|
+
case "Under":
|
|
436
|
+
return expandUnder(ast.prefix, principal, path)
|
|
437
|
+
case "And":
|
|
438
|
+
return and(...ast.conditions.map((child) => bind(child, principal, path)))
|
|
439
|
+
case "Or":
|
|
440
|
+
return or(...ast.conditions.map((child) => bind(child, principal, path)))
|
|
441
|
+
case "Not":
|
|
442
|
+
return not(bind(ast.condition, principal, path))
|
|
443
|
+
case "Predicate":
|
|
444
|
+
return Object.freeze({
|
|
445
|
+
_tag: "Predicate",
|
|
446
|
+
name: ast.name,
|
|
447
|
+
run: (value: unknown) => ast.run(value, principal),
|
|
448
|
+
})
|
|
449
|
+
default:
|
|
450
|
+
return ast
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** @internal */
|
|
455
|
+
export const partialOf = (
|
|
456
|
+
ast: Ast,
|
|
457
|
+
input: {
|
|
458
|
+
readonly principal?: object | undefined
|
|
459
|
+
readonly path?: ReadonlyArray<Resource.PathSegment> | undefined
|
|
460
|
+
},
|
|
461
|
+
): Ast => simplify(bind(ast, input.principal, input.path))
|
|
462
|
+
|
|
463
|
+
/** @internal */
|
|
464
|
+
export const evaluate = dual(2, evaluateOf)
|
|
465
|
+
|
|
466
|
+
/** @internal */
|
|
467
|
+
export const evaluateSync = dual(2, evaluateSyncOf)
|
|
468
|
+
|
|
469
|
+
/** @internal */
|
|
470
|
+
export const partial = dual(2, partialOf)
|
|
471
|
+
|
|
472
|
+
/** @internal */
|
|
473
|
+
export class Untranslatable extends Schema.TaggedError<Untranslatable>(
|
|
474
|
+
"@nmnmcc/ability/Condition/Untranslatable",
|
|
475
|
+
)("Untranslatable", { reason: Schema.String }) {}
|
|
476
|
+
|
|
477
|
+
const foldNode = <A>(ast: Ast, interpreter: Condition.Interpreter<A>): A => {
|
|
478
|
+
switch (ast._tag) {
|
|
479
|
+
case "True":
|
|
480
|
+
return interpreter.true()
|
|
481
|
+
case "False":
|
|
482
|
+
return interpreter.false()
|
|
483
|
+
case "Compare":
|
|
484
|
+
if (ast.value._tag !== "Literal") {
|
|
485
|
+
throw Untranslatable.make({
|
|
486
|
+
reason: `The field "${ast.field}" is compared with a principal field. Call Condition.partial first.`,
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
return interpreter.compare(ast.op, ast.field, ast.value.value)
|
|
490
|
+
case "In":
|
|
491
|
+
return interpreter.in(
|
|
492
|
+
ast.field,
|
|
493
|
+
ast.values.map((operand) => {
|
|
494
|
+
if (operand._tag !== "Literal") {
|
|
495
|
+
throw Untranslatable.make({
|
|
496
|
+
reason: `The field "${ast.field}" uses a principal field. Call Condition.partial first.`,
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
return operand.value
|
|
500
|
+
}),
|
|
501
|
+
)
|
|
502
|
+
case "Exists":
|
|
503
|
+
return interpreter.exists(ast.field)
|
|
504
|
+
case "Under":
|
|
505
|
+
throw Untranslatable.make({
|
|
506
|
+
reason:
|
|
507
|
+
"An Under node needs the resource path. Call Condition.partial first.",
|
|
508
|
+
})
|
|
509
|
+
case "And":
|
|
510
|
+
return interpreter.and(
|
|
511
|
+
ast.conditions.map((child) => foldNode(child, interpreter)),
|
|
512
|
+
)
|
|
513
|
+
case "Or":
|
|
514
|
+
return interpreter.or(
|
|
515
|
+
ast.conditions.map((child) => foldNode(child, interpreter)),
|
|
516
|
+
)
|
|
517
|
+
case "Not":
|
|
518
|
+
return interpreter.not(foldNode(ast.condition, interpreter))
|
|
519
|
+
case "Predicate":
|
|
520
|
+
if (interpreter.predicate === undefined) {
|
|
521
|
+
throw Untranslatable.make({
|
|
522
|
+
reason: `The predicate "${ast.name}" can not become a query`,
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
return interpreter.predicate(ast.name)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** @internal */
|
|
530
|
+
export const fold = dual(
|
|
531
|
+
2,
|
|
532
|
+
<A>(
|
|
533
|
+
ast: Ast,
|
|
534
|
+
interpreter: Condition.Interpreter<A>,
|
|
535
|
+
): Effect.Effect<A, Untranslatable> =>
|
|
536
|
+
Effect.try({
|
|
537
|
+
try: () => foldNode(ast, interpreter),
|
|
538
|
+
catch: (cause) => {
|
|
539
|
+
if (cause instanceof Untranslatable) {
|
|
540
|
+
return cause
|
|
541
|
+
}
|
|
542
|
+
throw cause
|
|
543
|
+
},
|
|
544
|
+
}),
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
/** @internal */
|
|
548
|
+
export const fields = (ast: Ast): ReadonlyArray<string> => {
|
|
549
|
+
const output = new Set<string>()
|
|
550
|
+
const visit = (node: Ast): void => {
|
|
551
|
+
switch (node._tag) {
|
|
552
|
+
case "Compare":
|
|
553
|
+
case "In":
|
|
554
|
+
case "Exists":
|
|
555
|
+
output.add(node.field)
|
|
556
|
+
return
|
|
557
|
+
case "And":
|
|
558
|
+
case "Or":
|
|
559
|
+
node.conditions.forEach(visit)
|
|
560
|
+
return
|
|
561
|
+
case "Not":
|
|
562
|
+
visit(node.condition)
|
|
563
|
+
return
|
|
564
|
+
default:
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
visit(ast)
|
|
569
|
+
return Object.freeze(Array.from(output))
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const ScalarSchema = Schema.Union([
|
|
573
|
+
Schema.String,
|
|
574
|
+
Schema.Finite,
|
|
575
|
+
Schema.Boolean,
|
|
576
|
+
Schema.Null,
|
|
577
|
+
])
|
|
578
|
+
|
|
579
|
+
const OperandSchema = Schema.Union([
|
|
580
|
+
Schema.TaggedStruct("Literal", { value: ScalarSchema }),
|
|
581
|
+
Schema.TaggedStruct("Principal", { path: Schema.String }),
|
|
582
|
+
])
|
|
583
|
+
|
|
584
|
+
const Recursive = Schema.suspend(
|
|
585
|
+
(): Schema.Codec<Condition.Condition, Condition.Encoded> => schema,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
/** @internal */
|
|
589
|
+
export const schema: Schema.Codec<Condition.Condition, Condition.Encoded> =
|
|
590
|
+
Schema.Union([
|
|
591
|
+
Schema.TaggedStruct("True", {}),
|
|
592
|
+
Schema.TaggedStruct("False", {}),
|
|
593
|
+
Schema.TaggedStruct("Compare", {
|
|
594
|
+
op: Schema.Literals(["eq", "ne", "lt", "lte", "gt", "gte"]),
|
|
595
|
+
field: Schema.String,
|
|
596
|
+
value: OperandSchema,
|
|
597
|
+
}),
|
|
598
|
+
Schema.TaggedStruct("In", {
|
|
599
|
+
field: Schema.String,
|
|
600
|
+
values: Schema.Array(OperandSchema),
|
|
601
|
+
}),
|
|
602
|
+
Schema.TaggedStruct("Exists", { field: Schema.String }),
|
|
603
|
+
Schema.TaggedStruct("Under", { prefix: Schema.Array(OperandSchema) }),
|
|
604
|
+
Schema.TaggedStruct("And", { conditions: Schema.Array(Recursive) }),
|
|
605
|
+
Schema.TaggedStruct("Or", { conditions: Schema.Array(Recursive) }),
|
|
606
|
+
Schema.TaggedStruct("Not", { condition: Recursive }),
|
|
607
|
+
]) as never
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as Predicate from "effect/Predicate"
|
|
2
|
+
import type * as Schema from "effect/Schema"
|
|
3
|
+
import type * as Domain from "../Domain.ts"
|
|
4
|
+
import type * as Principal from "../Principal.ts"
|
|
5
|
+
import type * as Resource from "../Resource.ts"
|
|
6
|
+
import { descriptor } from "./resource.ts"
|
|
7
|
+
|
|
8
|
+
/** @internal */
|
|
9
|
+
export const TypeId = "~@nmnmcc/ability/Domain" as const
|
|
10
|
+
|
|
11
|
+
/** @internal */
|
|
12
|
+
export const make = (options: {
|
|
13
|
+
readonly principal: Principal.Any
|
|
14
|
+
readonly resources: ReadonlyArray<Resource.Any>
|
|
15
|
+
readonly meta?: Schema.Top
|
|
16
|
+
}): any => {
|
|
17
|
+
const resources = new Map<string, Resource.Descriptor>()
|
|
18
|
+
for (const resource of options.resources) {
|
|
19
|
+
const value = descriptor(resource)
|
|
20
|
+
if (resources.has(value.kind)) {
|
|
21
|
+
// Two resources with one kind name is a mistake in the code, not in the
|
|
22
|
+
// data, so it is a defect.
|
|
23
|
+
throw new Error(`Two resources have the kind name "${value.kind}"`)
|
|
24
|
+
}
|
|
25
|
+
if (value.kind === "*") {
|
|
26
|
+
throw new Error(`A resource may not have the kind name "*"`)
|
|
27
|
+
}
|
|
28
|
+
resources.set(value.kind, value)
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
[TypeId]: TypeId,
|
|
32
|
+
principal: options.principal,
|
|
33
|
+
resources,
|
|
34
|
+
meta: options.meta,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @internal */
|
|
39
|
+
export const isDomain = (u: unknown): u is Domain.Any =>
|
|
40
|
+
Predicate.hasProperty(u, TypeId)
|
|
41
|
+
|
|
42
|
+
/** @internal */
|
|
43
|
+
export const getDescriptor = (
|
|
44
|
+
domain: Domain.Any,
|
|
45
|
+
kind: string,
|
|
46
|
+
): Resource.Descriptor | undefined => domain.resources.get(kind)
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Gives the kind name of a resource value, from its `_tag` field.
|
|
50
|
+
*
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export const kindOf = (value: unknown): string | undefined =>
|
|
54
|
+
Predicate.hasProperty(value, "_tag") && typeof value._tag === "string"
|
|
55
|
+
? value._tag
|
|
56
|
+
: undefined
|