@nmnmcc/ability 2.0.4 → 2.0.6

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 (55) hide show
  1. package/README.md +7 -7
  2. package/dist/Condition.d.ts +4 -4
  3. package/dist/Condition.d.ts.map +1 -1
  4. package/dist/Condition.js.map +1 -1
  5. package/dist/Domain.d.ts +1 -1
  6. package/dist/Policy.d.ts +2 -2
  7. package/dist/Policy.d.ts.map +1 -1
  8. package/dist/Policy.js.map +1 -1
  9. package/dist/internal/algebra.js +85 -10
  10. package/dist/internal/algebra.js.map +1 -1
  11. package/dist/internal/authorizer.d.ts.map +1 -1
  12. package/dist/internal/authorizer.js +37 -14
  13. package/dist/internal/authorizer.js.map +1 -1
  14. package/dist/internal/bind.js +15 -12
  15. package/dist/internal/bind.js.map +1 -1
  16. package/dist/internal/catalog.d.ts +6 -0
  17. package/dist/internal/catalog.d.ts.map +1 -1
  18. package/dist/internal/catalog.js +12 -9
  19. package/dist/internal/catalog.js.map +1 -1
  20. package/dist/internal/decide.js +1 -1
  21. package/dist/internal/domain.js +1 -1
  22. package/dist/internal/json.js +2 -10
  23. package/dist/internal/json.js.map +1 -1
  24. package/dist/internal/nodes.d.ts +4 -1
  25. package/dist/internal/nodes.d.ts.map +1 -1
  26. package/dist/internal/nodes.js +17 -7
  27. package/dist/internal/nodes.js.map +1 -1
  28. package/dist/internal/patterns.js +1 -1
  29. package/dist/internal/patterns.js.map +1 -1
  30. package/dist/internal/policy-json.js +2 -8
  31. package/dist/internal/policy-json.js.map +1 -1
  32. package/dist/internal/policy-nodes.d.ts +4 -1
  33. package/dist/internal/policy-nodes.d.ts.map +1 -1
  34. package/dist/internal/policy-nodes.js +13 -4
  35. package/dist/internal/policy-nodes.js.map +1 -1
  36. package/dist/internal/rule.d.ts.map +1 -1
  37. package/dist/internal/rule.js +6 -6
  38. package/dist/internal/rule.js.map +1 -1
  39. package/dist/internal/search.d.ts.map +1 -1
  40. package/dist/internal/search.js +51 -34
  41. package/dist/internal/search.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/Condition.ts +5 -2
  44. package/src/Policy.ts +1 -2
  45. package/src/internal/algebra.ts +118 -21
  46. package/src/internal/authorizer.ts +56 -14
  47. package/src/internal/bind.ts +39 -18
  48. package/src/internal/catalog.ts +40 -27
  49. package/src/internal/json.ts +2 -11
  50. package/src/internal/nodes.ts +21 -11
  51. package/src/internal/patterns.ts +7 -3
  52. package/src/internal/policy-json.ts +2 -9
  53. package/src/internal/policy-nodes.ts +14 -6
  54. package/src/internal/rule.ts +13 -9
  55. package/src/internal/search.ts +81 -75
@@ -7,7 +7,6 @@
7
7
  */
8
8
  import * as Arr from "effect/Array"
9
9
  import * as HashMap from "effect/HashMap"
10
- import * as MutableHashMap from "effect/MutableHashMap"
11
10
  import * as Option from "effect/Option"
12
11
  import * as Result from "effect/Result"
13
12
  import type * as Domain from "../Domain.ts"
@@ -39,10 +38,18 @@ export interface RuleData {
39
38
  * time. It goes away when the binding ends.
40
39
  */
41
40
  interface Memo {
42
- /** The suggestion for each name, by the place where it was looked for. */
43
- readonly suggestions: MutableHashMap.MutableHashMap<string, string>
44
- /** If a pattern matches a field of a kind, by the kind and the pattern. */
45
- readonly fits: MutableHashMap.MutableHashMap<string, boolean>
41
+ /**
42
+ * The suggestion for each name, by the place where it was looked for. The
43
+ * names come from a policy, so the map is native, as `fits` is.
44
+ */
45
+ readonly suggestions: Map<string, string>
46
+ /**
47
+ * If a pattern matches a field of a kind, by the kind and the pattern. The
48
+ * patterns come from a policy, so the map is native: the hash of a string
49
+ * in Effect has no random seed, and a policy could hold very many
50
+ * patterns with one hash. The engine hashes a string with a random seed.
51
+ */
52
+ readonly fits: Map<string, boolean>
46
53
  /** The count of the issues of conditions so far. It belongs to one binding. */
47
54
  readonly issues: { count: number; noted: boolean }
48
55
  /** The steps of the checks of patterns so far. It belongs to one binding. */
@@ -50,8 +57,8 @@ interface Memo {
50
57
  }
51
58
 
52
59
  const makeMemo = (): Memo => ({
53
- suggestions: MutableHashMap.empty(),
54
- fits: MutableHashMap.empty(),
60
+ suggestions: new Map(),
61
+ fits: new Map(),
55
62
  issues: { count: 0, noted: false },
56
63
  work: { steps: 0, noted: false },
57
64
  })
@@ -72,15 +79,15 @@ const suggestIn = (
72
79
  names: () => ReadonlyArray<string>,
73
80
  ): string => {
74
81
  const key = `${scope}\u0000${name}`
75
- const known = MutableHashMap.get(memo.suggestions, key)
76
- if (Option.isSome(known)) {
77
- return known.value
82
+ const known = memo.suggestions.get(key)
83
+ if (known !== undefined) {
84
+ return known
78
85
  }
79
- if (MutableHashMap.size(memo.suggestions) >= maxSuggestions) {
86
+ if (memo.suggestions.size >= maxSuggestions) {
80
87
  return message
81
88
  }
82
89
  const made = withSuggestion(message, name, names())
83
- MutableHashMap.set(memo.suggestions, key, made)
90
+ memo.suggestions.set(key, made)
84
91
  return made
85
92
  }
86
93
 
@@ -230,7 +237,7 @@ const patternIssues = (
230
237
  Arr.reduce(known, false, (before, descriptor) => {
231
238
  const key = `${descriptor.kind}\u0000${pattern}`
232
239
  return (
233
- Option.getOrElse(MutableHashMap.get(memo.fits, key), () => {
240
+ Option.getOrElse(Option.fromUndefinedOr(memo.fits.get(key)), () => {
234
241
  // Each check of a pattern reads the fields of a catalog. The
235
242
  // work of one binding has a limit, so very many patterns stop
236
243
  // with one issue, and the policy does not load.
@@ -240,7 +247,7 @@ const patternIssues = (
240
247
  return true
241
248
  }
242
249
  const found = Patterns.fitsCatalog(parsed, descriptor.catalog)
243
- MutableHashMap.set(memo.fits, key, found)
250
+ memo.fits.set(key, found)
244
251
  return found
245
252
  }) || before
246
253
  )
@@ -282,6 +289,14 @@ const principalField = (
282
289
  at: ReadonlyArray<PropertyKey>,
283
290
  ): Catalog.Field | Issue => {
284
291
  const found = Catalog.lookup(context.domain.principalCatalog, ref)
292
+ // A read of the principal that is not plain data is a defect, as a read
293
+ // of a resource is: see `readIssues`.
294
+ if (found !== undefined && !found.plain) {
295
+ return {
296
+ path: at,
297
+ issue: `A condition may not read the field "${ref}" of the principal. A value above it may be an object that does not have it as a data field, as a class with a getter, a date or a list. Use a predicate for it.`,
298
+ }
299
+ }
285
300
  return found !== undefined
286
301
  ? found
287
302
  : {
@@ -577,8 +592,13 @@ export const ruleIssues = (
577
592
  memo: Memo = makeMemo(),
578
593
  ): ReadonlyArray<Issue> => {
579
594
  const all = rule.resources.includes("*")
580
- // Each kind is checked one time, also when the list names it again.
581
- const names = Arr.dedupe(Arr.filter(rule.resources, (kind) => kind !== "*"))
595
+ // Each kind is checked one time, also when the list names it again. The
596
+ // names come from a policy, so a native `Set` finds the copies:
597
+ // `Arr.dedupe` puts the names in groups by the hash of Effect, which has
598
+ // no random seed, and very many names with one hash make one long group.
599
+ const names = Array.from(
600
+ new Set(Arr.filter(rule.resources, (kind) => kind !== "*")),
601
+ )
582
602
  const kinds = Array.from(HashMap.keys(domain.resources))
583
603
  const named = Arr.flatMap(names, (kind) =>
584
604
  Option.toArray(HashMap.get(domain.resources, kind)),
@@ -599,8 +619,9 @@ export const ruleIssues = (
599
619
  ? Arr.filter(known, (descriptor) => hasAction(descriptor, rule.actions))
600
620
  : applicable,
601
621
  }
602
- const actions = Arr.dedupe(
603
- Arr.flatMap(known, (descriptor) => descriptor.actions),
622
+ // A native `Set` finds copies about ten times as fast as `Arr.dedupe`.
623
+ const actions = Array.from(
624
+ new Set(Arr.flatMap(known, (descriptor) => descriptor.actions)),
604
625
  )
605
626
  return [
606
627
  ...Arr.map(
@@ -69,7 +69,9 @@ export const joinKinds = (left: Kinds, right: Kinds): Kinds => ({
69
69
  ? left.number
70
70
  : right.number,
71
71
  object: left.object || right.object,
72
- literals: Arr.dedupe([...left.literals, ...right.literals]),
72
+ // A native `Set` finds copies about ten times as fast as `Arr.dedupe`. It
73
+ // takes `0` and `-0` as one, as `includes` does when a test reads them.
74
+ literals: Array.from(new Set([...left.literals, ...right.literals])),
73
75
  })
74
76
 
75
77
  /** Tests if a number fits the numbers of a field. */
@@ -146,6 +148,12 @@ export interface Branch {
146
148
  readonly field: Field | undefined
147
149
  /** The places below, by name, in the order of the Schema. */
148
150
  readonly children: ReadonlyArray<readonly [string, Branch]>
151
+ /**
152
+ * The places below, by name. A lookup gets each name from `split`, as a
153
+ * new string, and a `HashMap` finds a new string about 20% faster than a
154
+ * native `Map`. The names come from the Schema, so a name from a policy
155
+ * can not make a long list of names with one hash here.
156
+ */
149
157
  readonly names: HashMap.HashMap<string, Branch>
150
158
  /** The place for the keys of a record, when the value may be a record. */
151
159
  readonly key: Key | undefined
@@ -361,12 +369,12 @@ const nameOf = (signature: SchemaAST.PropertySignature): string | undefined =>
361
369
  /**
362
370
  * The properties of a struct by name. A walk makes it one time for each
363
371
  * struct of a place, so a struct with very many fields does not take a
364
- * search for each name.
372
+ * search for each name. It is a native `Map`, as `Branch.names` is.
365
373
  */
366
374
  const propertiesOf = (
367
375
  struct: SchemaAST.Objects,
368
- ): HashMap.HashMap<string, SchemaAST.PropertySignature> =>
369
- HashMap.fromIterable(
376
+ ): ReadonlyMap<string, SchemaAST.PropertySignature> =>
377
+ new Map(
370
378
  Arr.flatMap(struct.propertySignatures, (signature) => {
371
379
  const name = nameOf(signature)
372
380
  return name === undefined ? [] : [[name, signature] as const]
@@ -377,25 +385,29 @@ const propertiesOf = (
377
385
  const namesOf = (
378
386
  structs: ReadonlyArray<SchemaAST.Objects>,
379
387
  ): ReadonlyArray<string> =>
380
- Arr.dedupe(
381
- Arr.flatMap(structs, (struct) =>
382
- Arr.flatMap(struct.propertySignatures, (signature) => {
383
- const name = nameOf(signature)
384
- if (name === undefined) {
385
- return []
386
- }
387
- // A dot in a path is the step into a nested field. A field name with
388
- // a dot would make two fields have one path, so it is a defect.
389
- if (name.includes(".")) {
390
- throw new Error(`A field name may not have a dot: "${name}"`)
391
- }
392
- // An empty name would make an empty part in a path, such as
393
- // `.secret`.
394
- if (name === "") {
395
- throw new Error("A field name may not be empty")
396
- }
397
- return [name]
398
- }),
388
+ // A native `Set` keeps the first copy of each name, in order, about ten
389
+ // times as fast as `Arr.dedupe`.
390
+ Array.from(
391
+ new Set(
392
+ Arr.flatMap(structs, (struct) =>
393
+ Arr.flatMap(struct.propertySignatures, (signature) => {
394
+ const name = nameOf(signature)
395
+ if (name === undefined) {
396
+ return []
397
+ }
398
+ // A dot in a path is the step into a nested field. A field name with
399
+ // a dot would make two fields have one path, so it is a defect.
400
+ if (name.includes(".")) {
401
+ throw new Error(`A field name may not have a dot: "${name}"`)
402
+ }
403
+ // An empty name would make an empty part in a path, such as
404
+ // `.secret`.
405
+ if (name === "") {
406
+ throw new Error("A field name may not be empty")
407
+ }
408
+ return [name]
409
+ }),
410
+ ),
399
411
  ),
400
412
  )
401
413
 
@@ -478,9 +490,10 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
478
490
  const structProperties = Arr.map(structs, propertiesOf)
479
491
  const classProperties = Arr.map(classes, propertiesOf)
480
492
  const children = Arr.map(namesOf(structs), (name) => {
481
- const properties = Arr.flatMap(structProperties, (named) =>
482
- Option.toArray(HashMap.get(named, name)),
483
- )
493
+ const properties = Arr.flatMap(structProperties, (named) => {
494
+ const property = named.get(name)
495
+ return property === undefined ? [] : [property]
496
+ })
484
497
  const below: Place = {
485
498
  path: [...place.path, name],
486
499
  schemas: Arr.flatMap(properties, (property) =>
@@ -500,7 +513,7 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
500
513
  plain:
501
514
  place.plain &&
502
515
  !valueKinds.object &&
503
- Arr.every(classProperties, (named) => HashMap.has(named, name)),
516
+ Arr.every(classProperties, (named) => named.has(name)),
504
517
  }
505
518
  return [name, branchOf(below, count)] as const
506
519
  })
@@ -143,15 +143,6 @@ const decodeOperands = (
143
143
  decodeOperand(operand, [...path, name, index]),
144
144
  )
145
145
 
146
- const compareOperators: ReadonlyArray<string> = [
147
- "eq",
148
- "ne",
149
- "lt",
150
- "lte",
151
- "gt",
152
- "gte",
153
- ]
154
-
155
146
  const decodeNode = (
156
147
  input: unknown,
157
148
  path: ReadonlyArray<PropertyKey>,
@@ -199,11 +190,11 @@ const makeNode = (
199
190
  return Nodes.never
200
191
  case "Compare": {
201
192
  const op = readString(input, "op", path)
202
- if (!compareOperators.includes(op)) {
193
+ if (!Nodes.isCompareOperator(op)) {
203
194
  throw problem([...path, "op"], `"${op}" is not a compare operator`)
204
195
  }
205
196
  return new Nodes.Compare({
206
- op: op as Nodes.CompareOperator,
197
+ op,
207
198
  field: readString(input, "field", path),
208
199
  value: decodeOperand(read(input, "value", path), [...path, "value"]),
209
200
  })
@@ -21,12 +21,31 @@ import * as Arr from "effect/Array"
21
21
  import * as Data from "effect/Data"
22
22
  import type * as Effect from "effect/Effect"
23
23
  import { hasProperty } from "effect/Predicate"
24
+ import * as Schema from "effect/Schema"
24
25
  import * as Limits from "./limits.ts"
25
26
 
26
27
  /** A value that a condition can compare. */
27
28
  export type Scalar = string | number | boolean | null
28
29
 
29
- export type CompareOperator = "eq" | "ne" | "lt" | "lte" | "gt" | "gte"
30
+ /** The operators of a `Compare` test. The decoders read this list too. */
31
+ export const CompareOperator = Schema.Literals([
32
+ "eq",
33
+ "ne",
34
+ "lt",
35
+ "lte",
36
+ "gt",
37
+ "gte",
38
+ ])
39
+
40
+ export type CompareOperator = typeof CompareOperator.Type
41
+
42
+ /**
43
+ * Tests if a value is an operator. It reads the list of the Schema with
44
+ * `includes`: for a short list, this is about twice as fast as
45
+ * `Schema.is`, and a decoder tests each node of a large input.
46
+ */
47
+ export const isCompareOperator = (u: unknown): u is CompareOperator =>
48
+ (CompareOperator.literals as ReadonlyArray<unknown>).includes(u)
30
49
 
31
50
  /** A fixed value. */
32
51
  export interface Literal {
@@ -49,15 +68,6 @@ export const literal = (value: Scalar): Literal => operands.Literal({ value })
49
68
  export const principalRef = (path: string): PrincipalRef =>
50
69
  operands.Principal({ path })
51
70
 
52
- const compareOperators: ReadonlyArray<string> = [
53
- "eq",
54
- "ne",
55
- "lt",
56
- "lte",
57
- "gt",
58
- "gte",
59
- ]
60
-
61
71
  /** Tests if a value is a scalar. Any number is one, also `NaN`. */
62
72
  export const isScalar = (value: unknown): value is Scalar =>
63
73
  value === null ||
@@ -211,7 +221,7 @@ export class Compare extends Base {
211
221
  readonly field: string
212
222
  readonly value: Operand
213
223
  }) {
214
- if (!compareOperators.includes(props.op)) {
224
+ if (!isCompareOperator(props.op)) {
215
225
  throw new Error(`"${String(props.op)}" is not a compare operator`)
216
226
  }
217
227
  super(
@@ -117,9 +117,13 @@ type States = ReadonlyArray<number>
117
117
 
118
118
  /** Adds the place after each `**`, since a `**` may match no names. */
119
119
  const close = (pattern: Pattern, states: States): States =>
120
- Arr.dedupe(
121
- Arr.flatMap(states, (state) =>
122
- pattern[state]?._tag === "Globstar" ? [state, state + 1] : [state],
120
+ // A native `Set` keeps the first copy of each place, in order. It is
121
+ // faster than `Arr.dedupe`, and this is on the path of each match.
122
+ Array.from(
123
+ new Set(
124
+ Arr.flatMap(states, (state) =>
125
+ pattern[state]?._tag === "Globstar" ? [state, state + 1] : [state],
126
+ ),
123
127
  ),
124
128
  )
125
129
 
@@ -136,13 +136,6 @@ export const encode = (
136
136
  return toJson(node, (rule) => forms.get(rule))
137
137
  })
138
138
 
139
- const algorithms: ReadonlyArray<string> = [
140
- "denyOverrides",
141
- "permitOverrides",
142
- "firstApplicable",
143
- "lastApplicable",
144
- ]
145
-
146
139
  /** Reads a list of strings. */
147
140
  const readStrings = (
148
141
  input: object,
@@ -374,7 +367,7 @@ const readTree = (
374
367
  switch (tag) {
375
368
  case "Rules": {
376
369
  const algorithm = Json.readString(input, "algorithm", path)
377
- if (!algorithms.includes(algorithm)) {
370
+ if (!PolicyNodes.isAlgorithm(algorithm)) {
378
371
  throw Json.problem(
379
372
  [...path, "algorithm"],
380
373
  `"${algorithm}" is not a combining algorithm`,
@@ -392,7 +385,7 @@ const readTree = (
392
385
  _tag: "Rules",
393
386
  place,
394
387
  size: 1 + found.length,
395
- algorithm: algorithm as PolicyNodes.Algorithm,
388
+ algorithm,
396
389
  rules: found,
397
390
  })
398
391
  }
@@ -11,19 +11,27 @@
11
11
  */
12
12
  import * as Arr from "effect/Array"
13
13
  import * as Data from "effect/Data"
14
+ import * as Schema from "effect/Schema"
14
15
  import * as Rule from "./rule.ts"
15
16
  import * as Limits from "./limits.ts"
16
17
  import * as Nodes from "./nodes.ts"
17
18
 
18
- export type Algorithm =
19
- "denyOverrides" | "permitOverrides" | "firstApplicable" | "lastApplicable"
20
-
21
- const algorithms: ReadonlyArray<string> = [
19
+ /** The combining algorithms of a list of rules. The decoder reads this list too. */
20
+ export const Algorithm = Schema.Literals([
22
21
  "denyOverrides",
23
22
  "permitOverrides",
24
23
  "firstApplicable",
25
24
  "lastApplicable",
26
- ]
25
+ ])
26
+
27
+ export type Algorithm = typeof Algorithm.Type
28
+
29
+ /**
30
+ * Tests if a value is a combining algorithm. It reads the list of the Schema
31
+ * with `includes`, as `Nodes.isCompareOperator` does.
32
+ */
33
+ export const isAlgorithm = (u: unknown): u is Algorithm =>
34
+ (Algorithm.literals as ReadonlyArray<unknown>).includes(u)
27
35
 
28
36
  interface Facts {
29
37
  readonly size: number
@@ -103,7 +111,7 @@ export class Rules extends Base {
103
111
  readonly algorithm: Algorithm
104
112
  readonly rules: ReadonlyArray<Rule.Rule>
105
113
  }) {
106
- if (!algorithms.includes(props.algorithm)) {
114
+ if (!isAlgorithm(props.algorithm)) {
107
115
  throw new Error(
108
116
  `"${String(props.algorithm)}" is not a combining algorithm`,
109
117
  )
@@ -7,8 +7,6 @@
7
7
  */
8
8
  import * as Arr from "effect/Array"
9
9
  import * as Data from "effect/Data"
10
- import * as MutableHashMap from "effect/MutableHashMap"
11
- import * as Option from "effect/Option"
12
10
  import * as Condition from "../Condition.ts"
13
11
  import type * as Domain from "../Domain.ts"
14
12
  import type * as Policy from "../Policy.ts"
@@ -170,13 +168,19 @@ export const matchesInside = (pattern: string, field: string): boolean =>
170
168
  export const fitsOf = (
171
169
  catalog: Catalog.Catalog,
172
170
  ): ((pattern: string) => boolean) => {
173
- const memo = MutableHashMap.empty<string, boolean>()
174
- return (pattern) =>
175
- Option.getOrElse(MutableHashMap.get(memo, pattern), () => {
176
- const found = Patterns.fitsCatalog(Patterns.parseBound(pattern), catalog)
177
- MutableHashMap.set(memo, pattern, found)
178
- return found
179
- })
171
+ // A native `Map`: the hash of a string in Effect has no random seed, and a
172
+ // policy could hold very many patterns with one hash. The engine hashes a
173
+ // string with a random seed.
174
+ const memo = new Map<string, boolean>()
175
+ return (pattern) => {
176
+ const known = memo.get(pattern)
177
+ if (known !== undefined) {
178
+ return known
179
+ }
180
+ const found = Patterns.fitsCatalog(Patterns.parseBound(pattern), catalog)
181
+ memo.set(pattern, found)
182
+ return found
183
+ }
180
184
  }
181
185
 
182
186
  /**