@nmnmcc/ability 2.0.7 → 2.1.1

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 (86) hide show
  1. package/README.md +50 -27
  2. package/dist/Authorizer.d.ts +21 -12
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js.map +1 -1
  5. package/dist/Condition.d.ts +116 -16
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js +27 -18
  8. package/dist/Condition.js.map +1 -1
  9. package/dist/Domain.d.ts +9 -0
  10. package/dist/Domain.d.ts.map +1 -1
  11. package/dist/Domain.js +5 -0
  12. package/dist/Domain.js.map +1 -1
  13. package/dist/Limits.d.ts +44 -13
  14. package/dist/Limits.d.ts.map +1 -1
  15. package/dist/Limits.js +52 -22
  16. package/dist/Limits.js.map +1 -1
  17. package/dist/Policy.d.ts +95 -12
  18. package/dist/Policy.d.ts.map +1 -1
  19. package/dist/Policy.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/internal/algebra.js +2 -2
  22. package/dist/internal/analysis.js +81 -75
  23. package/dist/internal/analysis.js.map +1 -1
  24. package/dist/internal/authorizer.d.ts.map +1 -1
  25. package/dist/internal/authorizer.js +11 -7
  26. package/dist/internal/authorizer.js.map +1 -1
  27. package/dist/internal/bind.js +86 -64
  28. package/dist/internal/bind.js.map +1 -1
  29. package/dist/internal/catalog.d.ts +33 -10
  30. package/dist/internal/catalog.d.ts.map +1 -1
  31. package/dist/internal/catalog.js +147 -72
  32. package/dist/internal/catalog.js.map +1 -1
  33. package/dist/internal/decide.js +25 -29
  34. package/dist/internal/decide.js.map +1 -1
  35. package/dist/internal/domain.js +19 -5
  36. package/dist/internal/domain.js.map +1 -1
  37. package/dist/internal/json.js +120 -76
  38. package/dist/internal/json.js.map +1 -1
  39. package/dist/internal/limits.d.ts +28 -0
  40. package/dist/internal/limits.d.ts.map +1 -1
  41. package/dist/internal/limits.js +75 -16
  42. package/dist/internal/limits.js.map +1 -1
  43. package/dist/internal/nodes.d.ts.map +1 -1
  44. package/dist/internal/nodes.js +53 -44
  45. package/dist/internal/nodes.js.map +1 -1
  46. package/dist/internal/patterns.js +50 -15
  47. package/dist/internal/patterns.js.map +1 -1
  48. package/dist/internal/policy-json.js +169 -127
  49. package/dist/internal/policy-json.js.map +1 -1
  50. package/dist/internal/policy-nodes.d.ts.map +1 -1
  51. package/dist/internal/policy-nodes.js +16 -13
  52. package/dist/internal/policy-nodes.js.map +1 -1
  53. package/dist/internal/policy.d.ts.map +1 -1
  54. package/dist/internal/policy.js +38 -22
  55. package/dist/internal/policy.js.map +1 -1
  56. package/dist/internal/resource.js +1 -1
  57. package/dist/internal/rule.d.ts.map +1 -1
  58. package/dist/internal/rule.js +6 -5
  59. package/dist/internal/rule.js.map +1 -1
  60. package/dist/internal/search.d.ts.map +1 -1
  61. package/dist/internal/search.js +106 -92
  62. package/dist/internal/search.js.map +1 -1
  63. package/dist/internal/trampoline.js +19 -5
  64. package/dist/internal/trampoline.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/Authorizer.ts +30 -20
  67. package/src/Condition.ts +308 -83
  68. package/src/Domain.ts +9 -0
  69. package/src/Limits.ts +69 -29
  70. package/src/Policy.ts +131 -15
  71. package/src/internal/analysis.ts +154 -124
  72. package/src/internal/authorizer.ts +29 -14
  73. package/src/internal/bind.ts +193 -136
  74. package/src/internal/catalog.ts +295 -112
  75. package/src/internal/decide.ts +58 -49
  76. package/src/internal/domain.ts +39 -3
  77. package/src/internal/json.ts +212 -117
  78. package/src/internal/limits.ts +101 -31
  79. package/src/internal/nodes.ts +65 -47
  80. package/src/internal/patterns.ts +72 -26
  81. package/src/internal/policy-json.ts +266 -197
  82. package/src/internal/policy-nodes.ts +31 -23
  83. package/src/internal/policy.ts +63 -23
  84. package/src/internal/rule.ts +16 -3
  85. package/src/internal/search.ts +186 -136
  86. package/src/internal/trampoline.ts +51 -4
@@ -136,34 +136,57 @@ export interface Field {
136
136
  * above the field may be an object that does not have it as a data field:
137
137
  * a class without the field, which may have a getter of that name, a
138
138
  * value such as a date or a list, or any object, as of `Schema.Unknown`.
139
- * A decode or a `make` copies a struct or a record into a plain object,
140
- * so a struct without the field gives `null` for it.
139
+ * A struct of a union without the field makes it not plain too, since a
140
+ * value made by hand may have it with any value.
141
141
  */
142
142
  readonly plain: boolean
143
143
  }
144
144
 
145
- /** A place in the tree of a catalog, with the places below it. */
145
+ /**
146
+ * A place in the graph of a catalog. A path of a field leads to a place, and
147
+ * many paths may lead to one place, as in a recursive Schema: `next` and
148
+ * `next.next` of a list of links are one place. A place keeps the ids of the
149
+ * places below it, so the graph has no loop of objects.
150
+ */
146
151
  export interface Branch {
147
- /** The field at this place. The root has none. */
148
- readonly field: Field | undefined
149
- /** The places below, by name, in the order of the Schema. */
150
- readonly children: ReadonlyArray<readonly [string, Branch]>
152
+ readonly id: number
153
+ /** The kinds of the values of the field at this place. */
154
+ readonly kinds: Kinds
155
+ /** The field holds a scalar, a record or a list: see `Field.listed`. */
156
+ readonly values: boolean
157
+ /** The field may hold a struct. */
158
+ readonly structs: boolean
159
+ readonly open: boolean
160
+ readonly plain: boolean
161
+ /** The ids of the places below, by name, in the order of the Schema. */
162
+ readonly children: ReadonlyArray<readonly [string, number]>
151
163
  /**
152
164
  * The places below, by name. A lookup gets each name from `split`, as a
153
165
  * new string, and a `HashMap` finds a new string about 20% faster than a
154
166
  * native `Map`. The names come from the Schema, so a name from a policy
155
167
  * can not make a long list of names with one hash here.
156
168
  */
157
- readonly names: HashMap.HashMap<string, Branch>
169
+ readonly names: HashMap.HashMap<string, number>
158
170
  /** The place for the keys of a record, when the value may be a record. */
159
171
  readonly key: Key | undefined
160
- readonly keyBranch: Branch | undefined
172
+ readonly keyBranch: number | undefined
161
173
  }
162
174
 
163
175
  /** The catalog of the fields of a Schema. */
164
176
  export interface Catalog {
165
- /** Every field, a field before the fields below it. */
177
+ /**
178
+ * The fields that a list shows, a field before the fields below it. For a
179
+ * recursive Schema, the list goes into each recursive place one time on
180
+ * each path, so it is finite.
181
+ */
166
182
  readonly fields: ReadonlyArray<Field>
183
+ /** The places, by id. The root is the place with the id 0. */
184
+ readonly branches: ReadonlyArray<Branch>
185
+ /**
186
+ * The number of steps from a place to a place below it. A search of the
187
+ * catalog, as for a field pattern, reads each one, so this is its work.
188
+ */
189
+ readonly edges: number
167
190
  readonly root: Branch
168
191
  }
169
192
 
@@ -341,9 +364,8 @@ const declarationId = (ast: SchemaAST.Declaration): string | undefined => {
341
364
  : undefined
342
365
  }
343
366
 
344
- /** The things that a walk of the catalog knows at one place. */
367
+ /** The things that the walk of a catalog knows at one place. */
345
368
  interface Place {
346
- readonly path: ReadonlyArray<Segment>
347
369
  readonly schemas: ReadonlyArray<SchemaAST.AST>
348
370
  /** The value may be missing. */
349
371
  readonly missing: boolean
@@ -411,12 +433,15 @@ const namesOf = (
411
433
  ),
412
434
  )
413
435
 
414
- /**
415
- * Makes the branch of a place, with the branches below it. `count` is the
416
- * number of fields so far. It belongs to one walk, and it stops a Schema with
417
- * very many fields before the walk is done.
418
- */
419
- const branchOf = (place: Place, count: { size: number }): Branch => {
436
+ /** A place of the walk, and the places below it, before they have ids. */
437
+ interface Made {
438
+ readonly branch: Omit<Branch, "id" | "children" | "names" | "keyBranch">
439
+ readonly children: ReadonlyArray<readonly [string, Place]>
440
+ readonly keyPlace: Place | undefined
441
+ }
442
+
443
+ /** Works out the facts of a place, and the places below it. */
444
+ const placeOf = (place: Place): Made => {
420
445
  const structs = Arr.filter(Arr.map(place.schemas, structOf), isStruct)
421
446
  const values = Arr.filter(
422
447
  place.schemas,
@@ -435,49 +460,6 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
435
460
  structs,
436
461
  (struct) => struct.indexSignatures.length > 0,
437
462
  )
438
- // The last part that a path may have: the walk does not go below it.
439
- const last = place.path.length === Limits.fieldPathParts
440
- const field: Field | undefined =
441
- place.path.length === 0
442
- ? undefined
443
- : {
444
- path: place.path,
445
- text: textOf(place.path),
446
- kinds,
447
- listed:
448
- values.length > 0 ||
449
- records.length > 0 ||
450
- (last && structs.length > 0),
451
- // A field that is a scalar in one branch of a union and a struct
452
- // in another holds parts too.
453
- open:
454
- records.length > 0 ||
455
- structs.length > 0 ||
456
- valueKinds.object ||
457
- place.top,
458
- plain: place.plain,
459
- }
460
- if (field !== undefined) {
461
- count.size += 1
462
- if (count.size > Limits.catalogFields) {
463
- throw Limits.exceeded(
464
- "A Schema",
465
- "catalogFields",
466
- "fields",
467
- count.size,
468
- Limits.catalogFields,
469
- )
470
- }
471
- }
472
- if (last) {
473
- return {
474
- field,
475
- children: [],
476
- names: HashMap.empty(),
477
- key: undefined,
478
- keyBranch: undefined,
479
- }
480
- }
481
463
  const structProperties = Arr.map(structs, propertiesOf)
482
464
  const children = Arr.map(namesOf(structs), (name) => {
483
465
  const properties = Arr.flatMap(structProperties, (named) => {
@@ -492,7 +474,6 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
492
474
  const top =
493
475
  place.top || valueKinds.object || properties.length < structs.length
494
476
  const below: Place = {
495
- path: [...place.path, name],
496
477
  schemas: Arr.flatMap(properties, (property) =>
497
478
  alternatives(property.type, new Set()),
498
479
  ),
@@ -507,80 +488,282 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
507
488
  // a read below it may not find plain data.
508
489
  plain: place.plain && !top,
509
490
  }
510
- return [name, branchOf(below, count)] as const
491
+ return [name, below] as const
511
492
  })
512
- const names = HashMap.fromIterable(children)
513
493
  const signatures = Arr.flatMap(records, (struct) => struct.indexSignatures)
514
- if (signatures.length === 0) {
515
- return { field, children, names, key: undefined, keyBranch: undefined }
516
- }
517
494
  const keyTop =
518
495
  place.top || valueKinds.object || records.length < structs.length
519
- const key: Key = {
520
- _tag: "Key",
521
- parameters: Arr.map(signatures, (signature) => signature.parameter),
522
- }
523
- const keyBranch = branchOf(
524
- {
525
- path: [...place.path, key],
526
- schemas: Arr.flatMap(signatures, (signature) =>
527
- alternatives(signature.type, new Set()),
528
- ),
529
- // A record may not have a given key.
530
- missing: true,
531
- top: keyTop,
532
- plain: place.plain && !keyTop,
496
+ return {
497
+ branch: {
498
+ kinds,
499
+ values: values.length > 0 || records.length > 0,
500
+ structs: structs.length > 0,
501
+ // A field that is a scalar in one branch of a union and a struct in
502
+ // another holds parts too.
503
+ open:
504
+ records.length > 0 ||
505
+ structs.length > 0 ||
506
+ valueKinds.object ||
507
+ place.top,
508
+ plain: place.plain,
509
+ key:
510
+ signatures.length === 0
511
+ ? undefined
512
+ : {
513
+ _tag: "Key",
514
+ parameters: Arr.map(
515
+ signatures,
516
+ (signature) => signature.parameter,
517
+ ),
518
+ },
533
519
  },
534
- count,
520
+ children,
521
+ keyPlace:
522
+ signatures.length === 0
523
+ ? undefined
524
+ : {
525
+ schemas: Arr.flatMap(signatures, (signature) =>
526
+ alternatives(signature.type, new Set()),
527
+ ),
528
+ // A record may not have a given key.
529
+ missing: true,
530
+ top: keyTop,
531
+ plain: place.plain && !keyTop,
532
+ },
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Makes the places of a Schema. Each place comes one time: two paths with the
538
+ * same Schemas and the same marks lead to one place. The walk keeps a queue
539
+ * of the places that it has found, and it makes them in order, so a deep
540
+ * Schema does not fill the stack.
541
+ */
542
+ const branchesOf = (root: Place): ReadonlyArray<Branch> => {
543
+ // The id of each Schema node, and of each place, for this walk. The ids
544
+ // are in native maps, because the walk must find a Schema node by the node
545
+ // itself: `Equal` compares Schema nodes by structure, and a recursive
546
+ // Schema has no end. The maps and the queue belong to this walk.
547
+ const astIds = new Map<SchemaAST.AST, number>()
548
+ const placeIds = new Map<string, number>()
549
+ const queue: Array<Place> = []
550
+ const idOf = (place: Place): number => {
551
+ const ids = Arr.map(place.schemas, (schema) => {
552
+ const known = astIds.get(schema)
553
+ if (known !== undefined) {
554
+ return known
555
+ }
556
+ astIds.set(schema, astIds.size)
557
+ return astIds.size - 1
558
+ })
559
+ const key = `${Array.from(new Set(ids))
560
+ .sort((a, b) => a - b)
561
+ .join(
562
+ ",",
563
+ )}|${String(place.missing)}|${String(place.top)}|${String(place.plain)}`
564
+ const known = placeIds.get(key)
565
+ if (known !== undefined) {
566
+ return known
567
+ }
568
+ if (queue.length >= Limits.ceilings.catalogFields) {
569
+ throw Limits.exceeded(
570
+ "A Schema",
571
+ "catalogFields",
572
+ "places",
573
+ queue.length + 1,
574
+ Limits.ceilings.catalogFields,
575
+ )
576
+ }
577
+ placeIds.set(key, queue.length)
578
+ queue.push(place)
579
+ return queue.length - 1
580
+ }
581
+ idOf(root)
582
+ return Arr.unfold(0, (id) => {
583
+ const place = queue[id]
584
+ if (place === undefined) {
585
+ return Option.none()
586
+ }
587
+ const made = placeOf(place)
588
+ const children = Arr.map(
589
+ made.children,
590
+ ([name, below]) => [name, idOf(below)] as const,
591
+ )
592
+ const branch: Branch = {
593
+ ...made.branch,
594
+ id,
595
+ children,
596
+ names: HashMap.fromIterable(children),
597
+ keyBranch: made.keyPlace === undefined ? undefined : idOf(made.keyPlace),
598
+ }
599
+ return Option.some([branch, id + 1] as const)
600
+ })
601
+ }
602
+
603
+ /** The field at a place, for a path. */
604
+ const fieldAt = (
605
+ branch: Branch,
606
+ path: ReadonlyArray<Segment>,
607
+ parts: number,
608
+ ): Field => ({
609
+ path,
610
+ text: textOf(path),
611
+ kinds: branch.kinds,
612
+ // A struct at the last part that a path may have is one field, since a
613
+ // path does not go below it.
614
+ listed: branch.values || (path.length === parts && branch.structs),
615
+ open: branch.open,
616
+ plain: branch.plain,
617
+ })
618
+
619
+ /**
620
+ * The fields that a list shows, a field before the fields below it, down to
621
+ * `parts` parts. A path goes into a place that is on it already one time
622
+ * only, so the list of a recursive Schema is finite. The walk keeps a stack
623
+ * of the places to visit, so a deep Schema does not fill the stack. The
624
+ * stack belongs to this walk.
625
+ */
626
+ const listOf = (
627
+ branches: ReadonlyArray<Branch>,
628
+ parts: number,
629
+ ): ReadonlyArray<Field> => {
630
+ interface Visit {
631
+ readonly branch: Branch
632
+ readonly path: ReadonlyArray<Segment>
633
+ /** The ids of the places on the path, as a set of Effect that shares its parts. */
634
+ readonly above: HashSet.HashSet<number>
635
+ }
636
+ const below = (visit: Visit): ReadonlyArray<Visit> => {
637
+ const { branch, path } = visit
638
+ if (
639
+ path.length === parts ||
640
+ (path.length > 0 && HashSet.has(visit.above, branch.id))
641
+ ) {
642
+ return []
643
+ }
644
+ const above = HashSet.add(visit.above, branch.id)
645
+ const named = Arr.map(branch.children, ([name, id]) => ({
646
+ branch: branches[id]!,
647
+ path: [...path, name],
648
+ above,
649
+ }))
650
+ return branch.key === undefined || branch.keyBranch === undefined
651
+ ? named
652
+ : [
653
+ ...named,
654
+ {
655
+ branch: branches[branch.keyBranch]!,
656
+ path: [...path, branch.key],
657
+ above,
658
+ },
659
+ ]
660
+ }
661
+ const stack: Array<Visit> = [
662
+ { branch: branches[0]!, path: [], above: HashSet.empty() },
663
+ ]
664
+ return Arr.flatten(
665
+ Arr.unfold(stack, (rest) => {
666
+ const visit = rest.pop()
667
+ if (visit === undefined) {
668
+ return Option.none()
669
+ }
670
+ rest.push(...Arr.reverse(below(visit)))
671
+ const field =
672
+ visit.path.length === 0
673
+ ? []
674
+ : [fieldAt(visit.branch, visit.path, parts)]
675
+ if (field.length > 0 && rest.length + 1 > Limits.ceilings.catalogFields) {
676
+ throw Limits.exceeded(
677
+ "A Schema",
678
+ "catalogFields",
679
+ "fields",
680
+ rest.length + 1,
681
+ Limits.ceilings.catalogFields,
682
+ )
683
+ }
684
+ return Option.some([field, rest] as const)
685
+ }),
535
686
  )
536
- return { field, children, names, key, keyBranch }
537
687
  }
538
688
 
539
- const fieldsOf = (branch: Branch): ReadonlyArray<Field> => [
540
- ...(branch.field === undefined ? [] : [branch.field]),
541
- ...Arr.flatMap(branch.children, ([, child]) => fieldsOf(child)),
542
- ...(branch.keyBranch === undefined ? [] : fieldsOf(branch.keyBranch)),
543
- ]
689
+ /**
690
+ * The fields that a list shows for paths of at most `parts` parts, as for a
691
+ * domain with that limit. See `Catalog.fields`.
692
+ */
693
+ export const fieldsOf = (
694
+ catalog: Catalog,
695
+ parts: number,
696
+ ): ReadonlyArray<Field> =>
697
+ parts === Limits.fieldPathParts
698
+ ? catalog.fields
699
+ : listOf(catalog.branches, parts)
544
700
 
545
701
  /**
546
- * Makes the catalog of a Schema. It throws when a field name has a dot or is
547
- * empty, and when the Schema has more fields than `catalogFields`.
702
+ * Makes the catalog of a Schema, with the list of its fields for the default
703
+ * limit of the parts of a path. It throws when a field name has a dot or is
704
+ * empty, and when the Schema has more places or fields than the ceiling of
705
+ * `catalogFields`. A domain checks the size of the list with its own limit.
548
706
  */
549
707
  export const catalogOf = (ast: SchemaAST.AST): Catalog => {
550
- const root = branchOf(
551
- {
552
- path: [],
553
- schemas: alternatives(ast, new Set()),
554
- missing: false,
555
- top: false,
556
- plain: true,
557
- },
558
- { size: 0 },
559
- )
560
- return { fields: fieldsOf(root), root }
708
+ const branches = branchesOf({
709
+ schemas: alternatives(ast, new Set()),
710
+ missing: false,
711
+ top: false,
712
+ plain: true,
713
+ })
714
+ return {
715
+ fields: listOf(branches, Limits.fieldPathParts),
716
+ branches,
717
+ edges: Arr.reduce(
718
+ branches,
719
+ 0,
720
+ (total, branch) =>
721
+ total +
722
+ branch.children.length +
723
+ (branch.keyBranch === undefined ? 0 : 1),
724
+ ),
725
+ root: branches[0]!,
726
+ }
561
727
  }
562
728
 
563
- /** Gives the field of a catalog at a path with dots, if the catalog has it. */
564
- export const lookup = (catalog: Catalog, path: string): Field | undefined => {
565
- const names = path.split(".", Limits.fieldPathParts + 1)
566
- if (names.length > Limits.fieldPathParts) {
729
+ /**
730
+ * Gives the field of a catalog at a path with dots, if the catalog has it.
731
+ * A path has at most `parts` parts. The lookup follows one place for each
732
+ * part, in a loop.
733
+ */
734
+ export const lookup = (
735
+ catalog: Catalog,
736
+ path: string,
737
+ parts: number = Limits.fieldPathParts,
738
+ ): Field | undefined => {
739
+ const names = path.split(".", parts + 1)
740
+ if (names.length > parts) {
567
741
  return undefined
568
742
  }
569
- const branch = Arr.reduce(
743
+ const found = Arr.reduce(
570
744
  names,
571
- catalog.root as Branch | undefined,
745
+ { branch: catalog.root, path: [] } as
746
+ | { readonly branch: Branch; readonly path: ReadonlyArray<Segment> }
747
+ | undefined,
572
748
  (current, name) => {
573
749
  if (current === undefined) {
574
750
  return undefined
575
751
  }
576
- const named = HashMap.get(current.names, name)
752
+ const named = HashMap.get(current.branch.names, name)
577
753
  if (Option.isSome(named)) {
578
- return named.value
754
+ return {
755
+ branch: catalog.branches[named.value]!,
756
+ path: [...current.path, name],
757
+ }
579
758
  }
580
- return current.key !== undefined && keyFits(current.key, name)
581
- ? current.keyBranch
759
+ const key = current.branch.key
760
+ const keyBranch = current.branch.keyBranch
761
+ return key !== undefined && keyBranch !== undefined && keyFits(key, name)
762
+ ? { branch: catalog.branches[keyBranch]!, path: [...current.path, key] }
582
763
  : undefined
583
764
  },
584
765
  )
585
- return branch?.field
766
+ return found === undefined
767
+ ? undefined
768
+ : fieldAt(found.branch, found.path, parts)
586
769
  }