@nmnmcc/ability 2.1.1 → 2.1.3

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 (41) hide show
  1. package/README.md +2 -2
  2. package/dist/Authorizer.d.ts +11 -11
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js.map +1 -1
  5. package/dist/Condition.d.ts +6 -4
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js +5 -3
  8. package/dist/Condition.js.map +1 -1
  9. package/dist/Policy.d.ts +26 -15
  10. package/dist/Policy.d.ts.map +1 -1
  11. package/dist/Policy.js +7 -6
  12. package/dist/Policy.js.map +1 -1
  13. package/dist/internal/algebra.js +8 -5
  14. package/dist/internal/algebra.js.map +1 -1
  15. package/dist/internal/authorizer.js +23 -9
  16. package/dist/internal/authorizer.js.map +1 -1
  17. package/dist/internal/bind.js +16 -10
  18. package/dist/internal/bind.js.map +1 -1
  19. package/dist/internal/catalog.d.ts.map +1 -1
  20. package/dist/internal/catalog.js +37 -7
  21. package/dist/internal/catalog.js.map +1 -1
  22. package/dist/internal/decide.js +2 -2
  23. package/dist/internal/decide.js.map +1 -1
  24. package/dist/internal/patterns.js +26 -26
  25. package/dist/internal/patterns.js.map +1 -1
  26. package/dist/internal/policy-nodes.js +21 -12
  27. package/dist/internal/policy-nodes.js.map +1 -1
  28. package/dist/internal/search.js +38 -26
  29. package/dist/internal/search.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/Authorizer.ts +10 -10
  32. package/src/Condition.ts +14 -8
  33. package/src/Policy.ts +34 -17
  34. package/src/internal/algebra.ts +12 -8
  35. package/src/internal/authorizer.ts +49 -34
  36. package/src/internal/bind.ts +28 -30
  37. package/src/internal/catalog.ts +50 -14
  38. package/src/internal/decide.ts +3 -1
  39. package/src/internal/patterns.ts +37 -29
  40. package/src/internal/policy-nodes.ts +19 -13
  41. package/src/internal/search.ts +31 -21
@@ -815,48 +815,46 @@ export const treeIssues = (
815
815
  // shares the path of its parent. The tree may be the JSON form, with no
816
816
  // depth that a node knows, so the walk is a walk of the trampoline for an
817
817
  // input of an unknown depth.
818
- type Found = Issue | readonly [RuleData, Place.Place]
818
+ // The rules go into one list, in order: a walk that joined the lists of
819
+ // the children at each node would copy the list of a deep tree at each
820
+ // level. The list belongs to this walk.
821
+ const found: Array<readonly [RuleData, Place.Place]> = []
819
822
  const rulesOf = Trampoline.adaptive<
820
823
  { readonly node: TreeNode; readonly at: At },
821
- ReadonlyArray<Found>
824
+ undefined
822
825
  >((R, child) => ({ node, at }, level) => {
826
+ const below = (
827
+ parts: ReadonlyArray<{ readonly node: TreeNode; readonly at: At }>,
828
+ ) =>
829
+ R.map(
830
+ R.forEach(parts, (part) => child(part, level + 1)),
831
+ () => undefined,
832
+ )
823
833
  switch (node._tag) {
824
834
  case "Rules":
825
- return R.succeed(
826
- Arr.map(
827
- node.rules,
828
- (rule, index) => [rule, Place.at(at, "rules", index)] as const,
829
- ),
830
- )
835
+ Arr.forEach(node.rules, (rule, index) => {
836
+ found.push([rule, Place.at(at, "rules", index)])
837
+ })
838
+ return R.succeed(undefined)
831
839
  case "All":
832
840
  case "Any":
833
841
  case "FirstOf":
834
- return R.map(
835
- R.forEach(node.policies, (inner, index) =>
836
- child(
837
- { node: inner, at: Place.at(at, "policies", index) },
838
- level + 1,
839
- ),
840
- ),
841
- Arr.flatten,
842
+ return below(
843
+ Arr.map(node.policies, (inner, index) => ({
844
+ node: inner,
845
+ at: Place.at(at, "policies", index),
846
+ })),
842
847
  )
843
848
  case "Ceiling":
844
- return R.map(
845
- R.forEach(
846
- [
847
- { node: node.cap, at: Place.at(at, "cap") },
848
- { node: node.policy, at: Place.at(at, "policy") },
849
- ],
850
- (part) => child(part, level + 1),
851
- ),
852
- Arr.flatten,
853
- )
849
+ return below([
850
+ { node: node.cap, at: Place.at(at, "cap") },
851
+ { node: node.policy, at: Place.at(at, "policy") },
852
+ ])
854
853
  }
855
854
  })
855
+ rulesOf({ node: root, at: undefined })
856
856
  const memo = makeMemo(domain.limits)
857
- return Arr.flatMap(rulesOf({ node: root, at: undefined }), (found) =>
858
- Array.isArray(found)
859
- ? ruleIssues(domain, found[0], found[1], memo)
860
- : [found as Issue],
857
+ return Arr.flatMap(found, ([rule, place]) =>
858
+ ruleIssues(domain, rule, place, memo),
861
859
  )
862
860
  }
@@ -18,6 +18,7 @@ import { hasProperty } from "effect/Predicate"
18
18
  import * as Schema from "effect/Schema"
19
19
  import * as SchemaAST from "effect/SchemaAST"
20
20
  import * as Limits from "./limits.ts"
21
+ import * as Trampoline from "./trampoline.ts"
21
22
 
22
23
  /** The kinds of values that a field may hold. */
23
24
  export interface Kinds {
@@ -234,18 +235,31 @@ const classStruct = (
234
235
  const alternatives = (
235
236
  ast: SchemaAST.AST,
236
237
  open: ReadonlySet<SchemaAST.AST>,
237
- ): ReadonlyArray<SchemaAST.AST> => {
238
+ ): ReadonlyArray<SchemaAST.AST> => alternativesOf({ ast, open })
239
+
240
+ /**
241
+ * The walk of {@link alternatives}. A union may hold a union, very many
242
+ * levels down, so it is a walk of the trampoline for an input of an unknown
243
+ * depth.
244
+ */
245
+ const alternativesOf = Trampoline.adaptive<
246
+ { readonly ast: SchemaAST.AST; readonly open: ReadonlySet<SchemaAST.AST> },
247
+ ReadonlyArray<SchemaAST.AST>
248
+ >((R, child) => ({ ast, open }, depth) => {
238
249
  switch (ast._tag) {
239
250
  case "Union":
240
- return Arr.flatMap(ast.types, (type) => alternatives(type, open))
251
+ return R.map(
252
+ R.forEach(ast.types, (type) => child({ ast: type, open }, depth + 1)),
253
+ Arr.flatten,
254
+ )
241
255
  case "Suspend":
242
256
  return open.has(ast)
243
- ? []
244
- : alternatives(ast.thunk(), new Set([...open, ast]))
257
+ ? R.succeed([])
258
+ : child({ ast: ast.thunk(), open: new Set([...open, ast]) }, depth + 1)
245
259
  default:
246
- return [ast]
260
+ return R.succeed([ast])
247
261
  }
248
- }
262
+ })
249
263
 
250
264
  /** The struct of a Schema: a struct, or the struct of a class. */
251
265
  const structOf = (ast: SchemaAST.AST): SchemaAST.Objects | undefined =>
@@ -258,13 +272,30 @@ const structOf = (ast: SchemaAST.AST): SchemaAST.Objects | undefined =>
258
272
  const hasCheck = (
259
273
  checks: ReadonlyArray<SchemaAST.Check<never>>,
260
274
  id: string,
261
- ): boolean =>
262
- Arr.some(
263
- checks,
264
- (check) =>
265
- check.annotations?.representation?.id === id ||
266
- (check._tag === "FilterGroup" && hasCheck(check.checks, id)),
267
- )
275
+ ): boolean => hasCheckIn({ checks, id })
276
+
277
+ /**
278
+ * The walk of {@link hasCheck}. A group of checks may hold a group, very many
279
+ * levels down, as `.and(...)` many times makes, so it is a walk of the
280
+ * trampoline for an input of an unknown depth.
281
+ */
282
+ const hasCheckIn = Trampoline.adaptive<
283
+ {
284
+ readonly checks: ReadonlyArray<SchemaAST.Check<never>>
285
+ readonly id: string
286
+ },
287
+ boolean
288
+ >(
289
+ (R, child) =>
290
+ ({ checks, id }, depth) =>
291
+ R.some(checks, (check) =>
292
+ check.annotations?.representation?.id === id
293
+ ? R.succeed(true)
294
+ : check._tag === "FilterGroup"
295
+ ? child({ checks: check.checks, id }, depth + 1)
296
+ : R.succeed(false),
297
+ ),
298
+ )
268
299
 
269
300
  /** A struct with named fields, or with the keys of a record. */
270
301
  const isStruct = (
@@ -667,7 +698,12 @@ const listOf = (
667
698
  if (visit === undefined) {
668
699
  return Option.none()
669
700
  }
670
- rest.push(...Arr.reverse(below(visit)))
701
+ // The visits go onto the stack one at a time: a spread into a call
702
+ // would pass one argument for each field, past the most arguments
703
+ // of a call.
704
+ Arr.forEach(Arr.reverse(below(visit)), (next) => {
705
+ rest.push(next)
706
+ })
671
707
  const field =
672
708
  visit.path.length === 0
673
709
  ? []
@@ -388,7 +388,9 @@ export const permittedFields = (
388
388
  const rules = policy.pipe(nodeOf, PolicyNodes.rulesOf)
389
389
  // The fields with names only: a list can not name every key of a record.
390
390
  const listed = Arr.filter(
391
- descriptor.catalog.fields,
391
+ // The list for the limit of the parts of a path of the domain, as the
392
+ // decision of each field reads it.
393
+ Catalog.fieldsOf(descriptor.catalog, policy.domain.limits.fieldPathParts),
392
394
  (field) =>
393
395
  field.listed &&
394
396
  Arr.every(field.path, (segment) => typeof segment === "string"),
@@ -201,11 +201,13 @@ export const matchesBelow = (
201
201
  /**
202
202
  * Tests if a pattern matches some field of a catalog, with at most `parts`
203
203
  * parts. A recursive Schema has no end of fields, so the search goes over the
204
- * pairs of a place of the catalog and the places of the match: each pair
205
- * comes one time, a level after a level, so the search is short, and it
206
- * keeps a queue, so a deep Schema does not fill the stack. The set of the
207
- * pairs is native, with keys that are numbers, and the set and the queue
208
- * belong to this call.
204
+ * pairs of a place of the catalog and one place of the match. There are at
205
+ * most the places of the catalog times the parts of the pattern of them, so
206
+ * the work is the steps of the catalog times the parts of the pattern, as the
207
+ * binding counts it. Each pair comes one time, a level after a level, so the
208
+ * search finds the shortest field first, and it keeps a queue, so a deep
209
+ * Schema does not fill the stack. The set of the pairs is native, and the set
210
+ * and the queue belong to this call.
209
211
  */
210
212
  export const fitsCatalog = (
211
213
  pattern: Pattern,
@@ -214,46 +216,52 @@ export const fitsCatalog = (
214
216
  ): boolean => {
215
217
  interface Visit {
216
218
  readonly branch: Catalog.Branch
217
- readonly states: States
219
+ readonly state: number
218
220
  readonly depth: number
219
221
  }
220
222
  const seen = new Set<string>()
221
- const next = (visit: Visit): ReadonlyArray<Visit> => {
222
- if (visit.states.length === 0 || visit.depth === parts) {
223
- return []
224
- }
225
- const steps: ReadonlyArray<readonly [Catalog.Segment, number]> = [
226
- ...visit.branch.children,
227
- ...(visit.branch.key === undefined || visit.branch.keyBranch === undefined
228
- ? []
229
- : [[visit.branch.key, visit.branch.keyBranch] as const]),
230
- ]
231
- return Arr.flatMap(steps, ([segment, id]) => {
232
- const states = step(pattern, visit.states, segment)
233
- const key = `${String(id)}|${states.join(",")}`
234
- if (states.length === 0 || seen.has(key)) {
223
+ const visits = (
224
+ id: number,
225
+ states: States,
226
+ depth: number,
227
+ ): ReadonlyArray<Visit> =>
228
+ Arr.flatMap(states, (state) => {
229
+ const key = `${String(id)}|${String(state)}`
230
+ if (seen.has(key)) {
235
231
  return []
236
232
  }
237
233
  seen.add(key)
238
- return [{ branch: catalog.branches[id]!, states, depth: visit.depth + 1 }]
234
+ return [{ branch: catalog.branches[id]!, state, depth }]
239
235
  })
236
+ const next = (visit: Visit): ReadonlyArray<Visit> => {
237
+ if (visit.depth === parts) {
238
+ return []
239
+ }
240
+ const branch = visit.branch
241
+ const steps: ReadonlyArray<readonly [Catalog.Segment, number]> =
242
+ branch.key === undefined || branch.keyBranch === undefined
243
+ ? branch.children
244
+ : [...branch.children, [branch.key, branch.keyBranch] as const]
245
+ return Arr.flatMap(steps, ([segment, id]) =>
246
+ visits(id, step(pattern, [visit.state], segment), visit.depth + 1),
247
+ )
240
248
  }
241
- const queue: Array<Visit> = [
242
- { branch: catalog.root, states: start(pattern), depth: 0 },
243
- ]
244
- // The walk stops at the first field that the pattern matches.
245
- const found = (visit: Visit) =>
246
- visit.depth > 0 && isDone(pattern, visit.states)
249
+ const queue: Array<Visit> = [...visits(catalog.root.id, start(pattern), 0)]
250
+ // The walk stops at the first field that the pattern matches. The visits
251
+ // go into the queue one at a time: a spread into a call would pass one
252
+ // argument for each, past the most arguments of a call.
247
253
  return Arr.some(
248
254
  Arr.unfold(0, (index) => {
249
255
  const visit = queue[index]
250
256
  if (visit === undefined) {
251
257
  return Option.none()
252
258
  }
253
- if (found(visit)) {
259
+ if (visit.depth > 0 && visit.state === pattern.length) {
254
260
  return Option.some([true, queue.length] as const)
255
261
  }
256
- queue.push(...next(visit))
262
+ Arr.forEach(next(visit), (found) => {
263
+ queue.push(found)
264
+ })
257
265
  return Option.some([false, index + 1] as const)
258
266
  }),
259
267
  (match) => match,
@@ -196,29 +196,35 @@ export const isNode = (u: unknown): u is Node => u instanceof Base
196
196
  * into the condition of each rule takes the rules from this list first, so it
197
197
  * does not go down the levels of the tree and of a condition at once.
198
198
  */
199
- export const rulesOf = (root: Node): ReadonlyArray<Rule.Rule> =>
199
+ export const rulesOf = (root: Node): ReadonlyArray<Rule.Rule> => {
200
+ // The rules go into one list, in order. A walk that joined the lists of
201
+ // the children at each node would copy the list of a deep tree at each
202
+ // level. The list belongs to this walk.
203
+ const rules: Array<Rule.Rule> = []
200
204
  // The walk takes the steps of a trampoline, so a deep tree does not fill
201
205
  // the stack.
202
206
  Trampoline.run(root.depth, (R) => {
203
- const go = (node: Node): Trampoline.Step<ReadonlyArray<Rule.Rule>> => {
207
+ const children = (nodes: ReadonlyArray<Node>) =>
208
+ R.map(
209
+ R.forEach(nodes, (child) => R.suspend(() => go(child))),
210
+ () => undefined,
211
+ )
212
+ const go = (node: Node): Trampoline.Step<undefined> => {
204
213
  switch (node._tag) {
205
214
  case "Rules":
206
- return R.succeed(node.rules)
215
+ Arr.forEach(node.rules, (rule) => {
216
+ rules.push(rule)
217
+ })
218
+ return R.succeed(undefined)
207
219
  case "All":
208
220
  case "Any":
209
221
  case "FirstOf":
210
- return R.map(
211
- R.forEach(node.policies, (child) => R.suspend(() => go(child))),
212
- Arr.flatten,
213
- )
222
+ return children(node.policies)
214
223
  case "Ceiling":
215
- return R.map(
216
- R.forEach([node.cap, node.policy], (child) =>
217
- R.suspend(() => go(child)),
218
- ),
219
- Arr.flatten,
220
- )
224
+ return children([node.cap, node.policy])
221
225
  }
222
226
  }
223
227
  return go(root)
224
228
  })
229
+ return rules
230
+ }
@@ -317,28 +317,35 @@ const fix = (root: Node, field: string, value: unknown, search: Search): Node =>
317
317
  })
318
318
 
319
319
  /** The fields of the tests of a condition, once for each test. */
320
- const testedFields = (root: Node): ReadonlyArray<string> =>
320
+ const testedFields = (root: Node): ReadonlyArray<string> => {
321
+ // The fields go into one list, in order. A walk that joined the lists of
322
+ // the children would copy the list of a deep condition at each level. The
323
+ // list belongs to this walk.
324
+ const fields: Array<string> = []
321
325
  Trampoline.run(root.depth, (R) => {
322
- const go = (node: Node): Trampoline.Step<ReadonlyArray<string>> => {
326
+ const go = (node: Node): Trampoline.Step<undefined> => {
323
327
  switch (node._tag) {
324
328
  case "Compare":
325
329
  case "In":
326
330
  case "Exists":
327
- return R.succeed([node.field])
331
+ fields.push(node.field)
332
+ return R.succeed(undefined)
328
333
  case "And":
329
334
  case "Or":
330
335
  return R.map(
331
336
  R.forEach(node.conditions, (child) => R.suspend(() => go(child))),
332
- Arr.flatten,
337
+ () => undefined,
333
338
  )
334
339
  case "Not":
335
340
  return R.suspend(() => go(node.condition))
336
341
  default:
337
- return R.succeed([])
342
+ return R.succeed(undefined)
338
343
  }
339
344
  }
340
345
  return go(root)
341
346
  })
347
+ return fields
348
+ }
342
349
 
343
350
  /**
344
351
  * Picks the next field to split on. A field comes before the fields below it,
@@ -486,20 +493,29 @@ const exampleOf = (
486
493
  * belongs to one search.
487
494
  */
488
495
  const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
489
- type Pair = readonly [string, ReadonlyArray<Nodes.Scalar>]
496
+ // The constants go into one map as the walk finds them. A walk that
497
+ // joined the lists of the children would copy the list of a deep
498
+ // condition at each level. The map belongs to one search.
499
+ const all = new Map<string, Set<Nodes.Scalar>>()
500
+ const add = (field: string, values: ReadonlyArray<Nodes.Scalar>): void => {
501
+ const known = all.get(field) ?? new Set<Nodes.Scalar>()
502
+ Arr.forEach(values, (value) => known.add(value))
503
+ all.set(field, known)
504
+ }
490
505
  // The walk takes the steps of a trampoline, so a deep condition does not
491
506
  // fill the stack.
492
- const pairs = Trampoline.run(node.depth, (R) => {
493
- const go = (current: Node): Trampoline.Step<ReadonlyArray<Pair>> => {
507
+ Trampoline.run(node.depth, (R) => {
508
+ const go = (current: Node): Trampoline.Step<undefined> => {
494
509
  switch (current._tag) {
495
510
  case "Compare":
496
- return R.succeed([[current.field, [literalOf(current.value)]]])
511
+ add(current.field, [literalOf(current.value)])
512
+ return R.succeed(undefined)
497
513
  case "In":
498
- return R.succeed([
499
- [current.field, Arr.map(current.values, literalOf)],
500
- ])
514
+ add(current.field, Arr.map(current.values, literalOf))
515
+ return R.succeed(undefined)
501
516
  case "Exists":
502
- return R.succeed([[current.field, []]])
517
+ add(current.field, [])
518
+ return R.succeed(undefined)
503
519
  case "Under":
504
520
  throw new Error(
505
521
  "Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.",
@@ -510,22 +526,16 @@ const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
510
526
  R.forEach(current.conditions, (child) =>
511
527
  R.suspend(() => go(child)),
512
528
  ),
513
- Arr.flatten,
529
+ () => undefined,
514
530
  )
515
531
  case "Not":
516
532
  return R.suspend(() => go(current.condition))
517
533
  default:
518
- return R.succeed([])
534
+ return R.succeed(undefined)
519
535
  }
520
536
  }
521
537
  return go(node)
522
538
  })
523
- const all = new Map<string, Set<Nodes.Scalar>>()
524
- Arr.forEach(pairs, ([field, values]) => {
525
- const known = all.get(field) ?? new Set<Nodes.Scalar>()
526
- Arr.forEach(values, (value) => known.add(value))
527
- all.set(field, known)
528
- })
529
539
  return all
530
540
  }
531
541