@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.
- package/README.md +2 -2
- package/dist/Authorizer.d.ts +11 -11
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +6 -4
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +5 -3
- package/dist/Condition.js.map +1 -1
- package/dist/Policy.d.ts +26 -15
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +7 -6
- package/dist/Policy.js.map +1 -1
- package/dist/internal/algebra.js +8 -5
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/authorizer.js +23 -9
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +16 -10
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +37 -7
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +2 -2
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/patterns.js +26 -26
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-nodes.js +21 -12
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/search.js +38 -26
- package/dist/internal/search.js.map +1 -1
- package/package.json +1 -1
- package/src/Authorizer.ts +10 -10
- package/src/Condition.ts +14 -8
- package/src/Policy.ts +34 -17
- package/src/internal/algebra.ts +12 -8
- package/src/internal/authorizer.ts +49 -34
- package/src/internal/bind.ts +28 -30
- package/src/internal/catalog.ts +50 -14
- package/src/internal/decide.ts +3 -1
- package/src/internal/patterns.ts +37 -29
- package/src/internal/policy-nodes.ts +19 -13
- package/src/internal/search.ts +31 -21
package/src/internal/bind.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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(
|
|
858
|
-
|
|
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
|
}
|
package/src/internal/catalog.ts
CHANGED
|
@@ -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
|
|
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
|
-
:
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
? []
|
package/src/internal/decide.ts
CHANGED
|
@@ -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
|
-
|
|
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"),
|
package/src/internal/patterns.ts
CHANGED
|
@@ -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
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
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
|
|
219
|
+
readonly state: number
|
|
218
220
|
readonly depth: number
|
|
219
221
|
}
|
|
220
222
|
const seen = new Set<string>()
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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]!,
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
//
|
|
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 (
|
|
259
|
+
if (visit.depth > 0 && visit.state === pattern.length) {
|
|
254
260
|
return Option.some([true, queue.length] as const)
|
|
255
261
|
}
|
|
256
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/internal/search.ts
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
const go = (current: Node): Trampoline.Step<
|
|
507
|
+
Trampoline.run(node.depth, (R) => {
|
|
508
|
+
const go = (current: Node): Trampoline.Step<undefined> => {
|
|
494
509
|
switch (current._tag) {
|
|
495
510
|
case "Compare":
|
|
496
|
-
|
|
511
|
+
add(current.field, [literalOf(current.value)])
|
|
512
|
+
return R.succeed(undefined)
|
|
497
513
|
case "In":
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
])
|
|
514
|
+
add(current.field, Arr.map(current.values, literalOf))
|
|
515
|
+
return R.succeed(undefined)
|
|
501
516
|
case "Exists":
|
|
502
|
-
|
|
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
|
-
|
|
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
|
|