@nmnmcc/ability 2.0.6 → 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 (89) hide show
  1. package/README.md +56 -32
  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 +118 -18
  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 +101 -74
  22. package/dist/internal/algebra.js.map +1 -1
  23. package/dist/internal/analysis.js +81 -75
  24. package/dist/internal/analysis.js.map +1 -1
  25. package/dist/internal/authorizer.d.ts.map +1 -1
  26. package/dist/internal/authorizer.js +16 -11
  27. package/dist/internal/authorizer.js.map +1 -1
  28. package/dist/internal/bind.js +119 -80
  29. package/dist/internal/bind.js.map +1 -1
  30. package/dist/internal/catalog.d.ts +33 -10
  31. package/dist/internal/catalog.d.ts.map +1 -1
  32. package/dist/internal/catalog.js +153 -78
  33. package/dist/internal/catalog.js.map +1 -1
  34. package/dist/internal/decide.js +25 -29
  35. package/dist/internal/decide.js.map +1 -1
  36. package/dist/internal/domain.js +19 -5
  37. package/dist/internal/domain.js.map +1 -1
  38. package/dist/internal/json.js +120 -76
  39. package/dist/internal/json.js.map +1 -1
  40. package/dist/internal/limits.d.ts +28 -0
  41. package/dist/internal/limits.d.ts.map +1 -1
  42. package/dist/internal/limits.js +75 -16
  43. package/dist/internal/limits.js.map +1 -1
  44. package/dist/internal/nodes.d.ts +7 -0
  45. package/dist/internal/nodes.d.ts.map +1 -1
  46. package/dist/internal/nodes.js +86 -47
  47. package/dist/internal/nodes.js.map +1 -1
  48. package/dist/internal/patterns.js +50 -15
  49. package/dist/internal/patterns.js.map +1 -1
  50. package/dist/internal/policy-json.js +169 -127
  51. package/dist/internal/policy-json.js.map +1 -1
  52. package/dist/internal/policy-nodes.d.ts.map +1 -1
  53. package/dist/internal/policy-nodes.js +17 -14
  54. package/dist/internal/policy-nodes.js.map +1 -1
  55. package/dist/internal/policy.d.ts.map +1 -1
  56. package/dist/internal/policy.js +38 -22
  57. package/dist/internal/policy.js.map +1 -1
  58. package/dist/internal/resource.js +1 -1
  59. package/dist/internal/rule.d.ts.map +1 -1
  60. package/dist/internal/rule.js +6 -5
  61. package/dist/internal/rule.js.map +1 -1
  62. package/dist/internal/search.d.ts.map +1 -1
  63. package/dist/internal/search.js +106 -92
  64. package/dist/internal/search.js.map +1 -1
  65. package/dist/internal/trampoline.js +89 -0
  66. package/dist/internal/trampoline.js.map +1 -0
  67. package/package.json +1 -1
  68. package/src/Authorizer.ts +30 -20
  69. package/src/Condition.ts +308 -83
  70. package/src/Domain.ts +9 -0
  71. package/src/Limits.ts +69 -29
  72. package/src/Policy.ts +131 -15
  73. package/src/internal/algebra.ts +218 -135
  74. package/src/internal/analysis.ts +154 -122
  75. package/src/internal/authorizer.ts +34 -16
  76. package/src/internal/bind.ts +279 -148
  77. package/src/internal/catalog.ts +307 -133
  78. package/src/internal/decide.ts +58 -49
  79. package/src/internal/domain.ts +39 -3
  80. package/src/internal/json.ts +212 -117
  81. package/src/internal/limits.ts +101 -31
  82. package/src/internal/nodes.ts +101 -59
  83. package/src/internal/patterns.ts +72 -26
  84. package/src/internal/policy-json.ts +266 -197
  85. package/src/internal/policy-nodes.ts +33 -24
  86. package/src/internal/policy.ts +63 -23
  87. package/src/internal/rule.ts +16 -3
  88. package/src/internal/search.ts +186 -136
  89. package/src/internal/trampoline.ts +194 -0
@@ -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,22 +433,16 @@ 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
- // The structs of classes. A value of a class is not copied into a plain
422
- // object, so it keeps its getters and methods.
423
- const classes = Arr.filter(
424
- Arr.map(
425
- Arr.filter(place.schemas, (schema) => schema._tag === "Declaration"),
426
- structOf,
427
- ),
428
- isStruct,
429
- )
430
446
  const values = Arr.filter(
431
447
  place.schemas,
432
448
  (schema) => !isStruct(structOf(schema)),
@@ -444,58 +460,20 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
444
460
  structs,
445
461
  (struct) => struct.indexSignatures.length > 0,
446
462
  )
447
- // The last part that a path may have: the walk does not go below it.
448
- const last = place.path.length === Limits.fieldPathParts
449
- const field: Field | undefined =
450
- place.path.length === 0
451
- ? undefined
452
- : {
453
- path: place.path,
454
- text: textOf(place.path),
455
- kinds,
456
- listed:
457
- values.length > 0 ||
458
- records.length > 0 ||
459
- (last && structs.length > 0),
460
- // A field that is a scalar in one branch of a union and a struct
461
- // in another holds parts too.
462
- open:
463
- records.length > 0 ||
464
- structs.length > 0 ||
465
- valueKinds.object ||
466
- place.top,
467
- plain: place.plain,
468
- }
469
- if (field !== undefined) {
470
- count.size += 1
471
- if (count.size > Limits.catalogFields) {
472
- throw Limits.exceeded(
473
- "A Schema",
474
- "catalogFields",
475
- "fields",
476
- count.size,
477
- Limits.catalogFields,
478
- )
479
- }
480
- }
481
- if (last) {
482
- return {
483
- field,
484
- children: [],
485
- names: HashMap.empty(),
486
- key: undefined,
487
- keyBranch: undefined,
488
- }
489
- }
490
463
  const structProperties = Arr.map(structs, propertiesOf)
491
- const classProperties = Arr.map(classes, propertiesOf)
492
464
  const children = Arr.map(namesOf(structs), (name) => {
493
465
  const properties = Arr.flatMap(structProperties, (named) => {
494
466
  const property = named.get(name)
495
467
  return property === undefined ? [] : [property]
496
468
  })
469
+ // A struct without the name may still have it as an extra field, with
470
+ // any value: only a decode or a `make` takes extra fields away, and a
471
+ // decision may get a value made by hand. A value that may be any
472
+ // object, as one of `Schema.Unknown` may, may have the name with any
473
+ // value too.
474
+ const top =
475
+ place.top || valueKinds.object || properties.length < structs.length
497
476
  const below: Place = {
498
- path: [...place.path, name],
499
477
  schemas: Arr.flatMap(properties, (property) =>
500
478
  alternatives(property.type, new Set()),
501
479
  ),
@@ -505,91 +483,287 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
505
483
  place.missing ||
506
484
  values.length > 0 ||
507
485
  Arr.some(properties, (property) => SchemaAST.isOptional(property.type)),
508
- // A struct without the name may still have it as an extra field,
509
- // with any value: only a decode takes extra fields away. A value
510
- // that may be any object, as one of `Schema.Unknown` may, may have
511
- // the name with any value too.
512
- top: place.top || valueKinds.object || properties.length < structs.length,
513
- plain:
514
- place.plain &&
515
- !valueKinds.object &&
516
- Arr.every(classProperties, (named) => named.has(name)),
486
+ top,
487
+ // A value that may be anything may be a list, or have a getter, so
488
+ // a read below it may not find plain data.
489
+ plain: place.plain && !top,
517
490
  }
518
- return [name, branchOf(below, count)] as const
491
+ return [name, below] as const
519
492
  })
520
- const names = HashMap.fromIterable(children)
521
493
  const signatures = Arr.flatMap(records, (struct) => struct.indexSignatures)
522
- if (signatures.length === 0) {
523
- return { field, children, names, key: undefined, keyBranch: undefined }
494
+ const keyTop =
495
+ place.top || valueKinds.object || records.length < structs.length
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
+ },
519
+ },
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
524
580
  }
525
- const key: Key = {
526
- _tag: "Key",
527
- parameters: Arr.map(signatures, (signature) => signature.parameter),
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>
528
635
  }
529
- const keyBranch = branchOf(
530
- {
531
- path: [...place.path, key],
532
- schemas: Arr.flatMap(signatures, (signature) =>
533
- alternatives(signature.type, new Set()),
534
- ),
535
- // A record may not have a given key.
536
- missing: true,
537
- top: place.top || valueKinds.object || records.length < structs.length,
538
- plain:
539
- place.plain &&
540
- !valueKinds.object &&
541
- Arr.every(classes, (struct) => struct.indexSignatures.length > 0),
542
- },
543
- count,
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
+ }),
544
686
  )
545
- return { field, children, names, key, keyBranch }
546
687
  }
547
688
 
548
- const fieldsOf = (branch: Branch): ReadonlyArray<Field> => [
549
- ...(branch.field === undefined ? [] : [branch.field]),
550
- ...Arr.flatMap(branch.children, ([, child]) => fieldsOf(child)),
551
- ...(branch.keyBranch === undefined ? [] : fieldsOf(branch.keyBranch)),
552
- ]
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)
553
700
 
554
701
  /**
555
- * Makes the catalog of a Schema. It throws when a field name has a dot or is
556
- * 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.
557
706
  */
558
707
  export const catalogOf = (ast: SchemaAST.AST): Catalog => {
559
- const root = branchOf(
560
- {
561
- path: [],
562
- schemas: alternatives(ast, new Set()),
563
- missing: false,
564
- top: false,
565
- plain: true,
566
- },
567
- { size: 0 },
568
- )
569
- 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
+ }
570
727
  }
571
728
 
572
- /** Gives the field of a catalog at a path with dots, if the catalog has it. */
573
- export const lookup = (catalog: Catalog, path: string): Field | undefined => {
574
- const names = path.split(".", Limits.fieldPathParts + 1)
575
- 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) {
576
741
  return undefined
577
742
  }
578
- const branch = Arr.reduce(
743
+ const found = Arr.reduce(
579
744
  names,
580
- catalog.root as Branch | undefined,
745
+ { branch: catalog.root, path: [] } as
746
+ | { readonly branch: Branch; readonly path: ReadonlyArray<Segment> }
747
+ | undefined,
581
748
  (current, name) => {
582
749
  if (current === undefined) {
583
750
  return undefined
584
751
  }
585
- const named = HashMap.get(current.names, name)
752
+ const named = HashMap.get(current.branch.names, name)
586
753
  if (Option.isSome(named)) {
587
- return named.value
754
+ return {
755
+ branch: catalog.branches[named.value]!,
756
+ path: [...current.path, name],
757
+ }
588
758
  }
589
- return current.key !== undefined && keyFits(current.key, name)
590
- ? 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] }
591
763
  : undefined
592
764
  },
593
765
  )
594
- return branch?.field
766
+ return found === undefined
767
+ ? undefined
768
+ : fieldAt(found.branch, found.path, parts)
595
769
  }