@magda/typescript-common 1.2.1 → 2.0.0-alpha.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 (84) hide show
  1. package/dist/OpaCompileResponseParser.d.ts +260 -36
  2. package/dist/OpaCompileResponseParser.js +697 -297
  3. package/dist/OpaCompileResponseParser.js.map +1 -1
  4. package/dist/SQLUtils.d.ts +70 -0
  5. package/dist/SQLUtils.js +263 -0
  6. package/dist/SQLUtils.js.map +1 -0
  7. package/dist/ServerError.d.ts +9 -0
  8. package/dist/ServerError.js +17 -0
  9. package/dist/ServerError.js.map +1 -0
  10. package/dist/authorization-api/authMiddleware.d.ts +60 -1
  11. package/dist/authorization-api/authMiddleware.js +155 -3
  12. package/dist/authorization-api/authMiddleware.js.map +1 -1
  13. package/dist/authorization-api/constants.d.ts +5 -0
  14. package/dist/authorization-api/constants.js +13 -0
  15. package/dist/authorization-api/constants.js.map +1 -0
  16. package/dist/authorization-api/model.d.ts +2 -7
  17. package/dist/express/getNoCacheHeaders.d.ts +6 -0
  18. package/dist/express/getNoCacheHeaders.js +9 -0
  19. package/dist/express/getNoCacheHeaders.js.map +1 -0
  20. package/dist/express/setResponseNoCache.d.ts +3 -0
  21. package/dist/express/setResponseNoCache.js +9 -0
  22. package/dist/express/setResponseNoCache.js.map +1 -0
  23. package/dist/generated/registry/api.d.ts +47 -2
  24. package/dist/generated/registry/api.js +197 -2
  25. package/dist/generated/registry/api.js.map +1 -1
  26. package/dist/getAbsoluteUrl.d.ts +3 -2
  27. package/dist/getAbsoluteUrl.js +2 -1
  28. package/dist/getAbsoluteUrl.js.map +1 -1
  29. package/dist/getStorageUrl.d.ts +34 -0
  30. package/dist/getStorageUrl.js +138 -0
  31. package/dist/getStorageUrl.js.map +1 -0
  32. package/dist/handleServerError.d.ts +2 -0
  33. package/dist/handleServerError.js +17 -0
  34. package/dist/handleServerError.js.map +1 -0
  35. package/dist/opa/AspectQuery.d.ts +71 -0
  36. package/dist/opa/AspectQuery.js +219 -0
  37. package/dist/opa/AspectQuery.js.map +1 -0
  38. package/dist/opa/AuthDecision.d.ts +51 -0
  39. package/dist/opa/AuthDecision.js +241 -0
  40. package/dist/opa/AuthDecision.js.map +1 -0
  41. package/dist/opa/AuthDecisionQueryClient.d.ts +23 -0
  42. package/dist/opa/AuthDecisionQueryClient.js +113 -0
  43. package/dist/opa/AuthDecisionQueryClient.js.map +1 -0
  44. package/dist/pgTypes.d.ts +1 -0
  45. package/dist/pgTypes.js +18 -0
  46. package/dist/pgTypes.js.map +1 -0
  47. package/dist/registry/AuthorizedRegistryClient.d.ts +1 -0
  48. package/dist/registry/AuthorizedRegistryClient.js +17 -0
  49. package/dist/registry/AuthorizedRegistryClient.js.map +1 -1
  50. package/dist/registry/RegistryClient.d.ts +11 -0
  51. package/dist/registry/RegistryClient.js +46 -0
  52. package/dist/registry/RegistryClient.js.map +1 -1
  53. package/dist/test/getAuthDecision.spec.js +2 -2
  54. package/dist/test/getAuthDecision.spec.js.map +1 -1
  55. package/dist/test/getStorageUrl.spec.d.ts +1 -0
  56. package/dist/test/getStorageUrl.spec.js +95 -0
  57. package/dist/test/getStorageUrl.spec.js.map +1 -0
  58. package/dist/test/sampleAuthDecisions/content.json +29 -0
  59. package/dist/test/sampleAuthDecisions/datasetPermissionWithOrgUnitConstraint.json +79 -0
  60. package/dist/test/sampleAuthDecisions/extraLargeResponse.json +2519 -0
  61. package/dist/test/sampleAuthDecisions/simple.json +29 -0
  62. package/dist/test/sampleAuthDecisions/singleTermAspectRef.json +39 -0
  63. package/dist/test/sampleAuthDecisions/unconditionalFalseSimple.json +6 -0
  64. package/dist/test/sampleAuthDecisions/unconditionalNotMacthed.json +6 -0
  65. package/dist/test/sampleAuthDecisions/unconditionalNotMacthedWithExtraRefs.json +6 -0
  66. package/dist/test/sampleAuthDecisions/unconditionalTrue.json +6 -0
  67. package/dist/test/sampleAuthDecisions/unconditionalTrueSimple.json +6 -0
  68. package/dist/test/sampleAuthDecisions/unconditionalTrueWithDefaultRule.json +6 -0
  69. package/dist/test/sampleAuthDecisions/withDefaultRule.json +6 -0
  70. package/dist/test/{sampleOpaResponse.json → sampleOpaResponses/content.json} +0 -0
  71. package/dist/test/sampleOpaResponses/datasetPermissionWithOrgUnitConstraint.json +341 -0
  72. package/dist/test/sampleOpaResponses/extraLargeResponse.json +104869 -0
  73. package/dist/test/{sampleOpaResponseSimple.json → sampleOpaResponses/simple.json} +0 -0
  74. package/dist/test/sampleOpaResponses/singleTermAspectRef.json +233 -0
  75. package/dist/test/sampleOpaResponses/unconditionalFalseSimple.json +3 -0
  76. package/dist/test/sampleOpaResponses/unconditionalNotMacthed.json +73 -0
  77. package/dist/test/sampleOpaResponses/unconditionalNotMacthedWithExtraRefs.json +155 -0
  78. package/dist/test/{sampleOpaResponseUnconditionalTrue.json → sampleOpaResponses/unconditionalTrue.json} +0 -0
  79. package/dist/test/sampleOpaResponses/unconditionalTrueSimple.json +48 -0
  80. package/dist/test/{sampleOpaResponseUnconditionalTrueWithDefaultRule.json → sampleOpaResponses/unconditionalTrueWithDefaultRule.json} +0 -0
  81. package/dist/test/{sampleOpaResponseWithDefaultRule.json → sampleOpaResponses/withDefaultRule.json} +0 -0
  82. package/dist/test/testOpaCompileResponseParser.spec.js +212 -20
  83. package/dist/test/testOpaCompileResponseParser.spec.js.map +1 -1
  84. package/package.json +9 -3
@@ -1,9 +1,20 @@
1
1
  "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
2
13
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
14
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
15
  };
5
16
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.unknown2Ref = exports.value2String = exports.RegoRef = exports.RegoExp = exports.RegoTerm = exports.RegoOperators = exports.RegoRule = void 0;
17
+ exports.unknown2Ref = exports.RegoRuleSet = exports.value2String = exports.RegoRef = exports.RegoExp = exports.RegoTerm = exports.RegoOperators = exports.RegoRule = void 0;
7
18
  const lodash_1 = __importDefault(require("lodash"));
8
19
  /**
9
20
  * @class RegoRule
@@ -19,6 +30,18 @@ const lodash_1 = __importDefault(require("lodash"));
19
30
  */
20
31
  class RegoRule {
21
32
  constructor(options) {
33
+ var _a;
34
+ /**
35
+ * Whether the rule contains any expressions that has any resolvable references.
36
+ * reference start with `input.` should be considered as non-resolvable in context of partial evaluation.
37
+ * When this field is set to `true`, we should not attempt to evaluate this expression.
38
+ * i.e. evaluate() method should return immediately.
39
+ * This will speed up evaluation process.
40
+ *
41
+ * @type {boolean}
42
+ * @memberof RegoRule
43
+ */
44
+ this.hasNoResolvableRef = false;
22
45
  this.isCompleteEvaluated = false;
23
46
  this.name = lodash_1.default.isString(options.name) ? options.name : "";
24
47
  this.fullName = lodash_1.default.isString(options.fullName) ? options.fullName : "";
@@ -42,6 +65,68 @@ class RegoRule {
42
65
  if (!(this.parser instanceof OpaCompileResponseParser)) {
43
66
  throw new Error("Require parser parameter to create a RegoRule");
44
67
  }
68
+ this.removeDuplicateExpressions();
69
+ if (((_a = this.expressions) === null || _a === void 0 ? void 0 : _a.length) &&
70
+ this.expressions.findIndex((exp) => !exp.hasNoResolvableRef) === -1) {
71
+ this.hasNoResolvableRef = true;
72
+ }
73
+ this.evaluate();
74
+ }
75
+ /**
76
+ * OPA PE result might contain duplicate expressions.
77
+ * https://github.com/open-policy-agent/opa/issues/4516
78
+ * This method will remove those duplication by simply string comparison.
79
+ *
80
+ * @return {*}
81
+ * @memberof RegoRule
82
+ */
83
+ removeDuplicateExpressions() {
84
+ var _a;
85
+ if (!((_a = this === null || this === void 0 ? void 0 : this.expressions) === null || _a === void 0 ? void 0 : _a.length)) {
86
+ return;
87
+ }
88
+ const expSet = new Set();
89
+ this.expressions = this.expressions.filter((exp) => {
90
+ const orgSetLength = expSet.size;
91
+ // exp.toJSON() is faster than exp.toConciseJSON()
92
+ expSet.add(exp.toJSON(0, true));
93
+ return expSet.size > orgSetLength;
94
+ });
95
+ }
96
+ /**
97
+ * Test whether the rule is an "impossible" rule.
98
+ * If so, the rule should be simply discarded.
99
+ * See: https://github.com/open-policy-agent/opa/issues/4516
100
+ *
101
+ * @return {*} {boolean}
102
+ * @memberof RegoRule
103
+ */
104
+ isImpossible() {
105
+ var _a;
106
+ if (!((_a = this === null || this === void 0 ? void 0 : this.expressions) === null || _a === void 0 ? void 0 : _a.length)) {
107
+ return false;
108
+ }
109
+ const nonNegatedExpSet = new Set();
110
+ const negatedExps = this.expressions.filter((exp) => {
111
+ if (exp.isNegated) {
112
+ return true;
113
+ }
114
+ else {
115
+ // add non negated exp content to set
116
+ // exp.toJSON() is faster than exp.toConciseJSON()
117
+ nonNegatedExpSet.add(exp.toJSON(0, true));
118
+ return false;
119
+ }
120
+ });
121
+ if (!negatedExps.length) {
122
+ return false;
123
+ }
124
+ for (const exp of negatedExps) {
125
+ if (nonNegatedExpSet.has(exp.toJSON(0, true, true))) {
126
+ return true;
127
+ }
128
+ }
129
+ return false;
45
130
  }
46
131
  clone(options = {}) {
47
132
  const regoRule = new RegoRule(Object.assign({ name: this.name, fullName: this.fullName, isDefault: this.isDefault, value: this.value, isCompleteEvaluated: this.isCompleteEvaluated, expressions: this.expressions.map((e) => e.clone()), parser: this.parser }, options));
@@ -56,30 +141,56 @@ class RegoRule {
56
141
  * @memberof RegoRule
57
142
  */
58
143
  evaluate() {
59
- this.expressions = this.expressions.map((exp) => exp.evaluate());
60
- const falseExpression = this.expressions.find((exp) => exp.isMatch() === false);
61
- if (!lodash_1.default.isUndefined(falseExpression)) {
62
- // --- rule expressions are always evaluated in the context of AND
63
- // --- any false expression will make the rule not match
144
+ var _a;
145
+ if (this.hasNoResolvableRef) {
146
+ return this;
147
+ }
148
+ if (this.isCompleteEvaluated) {
149
+ return this;
150
+ }
151
+ if (!((_a = this === null || this === void 0 ? void 0 : this.expressions) === null || _a === void 0 ? void 0 : _a.length)) {
152
+ // a rule with empty body / no expression is matched
64
153
  this.isCompleteEvaluated = true;
65
- this.isMatched = false;
154
+ this.isMatched = true;
155
+ return this;
66
156
  }
67
- else {
68
- // --- filter out all expressions are evaluated
69
- // --- note any non-false value will considered as a match (true) i.e. 0 is equivalent to true
70
- // --- empty expression array indicates unconditional match (true)
71
- const idx = this.expressions.findIndex((exp) => !exp.isCompleteEvaluated);
72
- if (idx === -1) {
73
- this.isCompleteEvaluated = true;
74
- this.isMatched = true;
157
+ let unresolvable = false;
158
+ for (let i = 0; i < this.expressions.length; i++) {
159
+ const exp = this.expressions[i];
160
+ exp.evaluate();
161
+ if (!exp.isResolvable()) {
162
+ unresolvable = true;
163
+ continue;
75
164
  }
76
- else {
77
- // --- further dry the rule if the rule has unsolved exps
78
- // --- if a exp is matched (i.e. true) it can be strip out as true AND xxxx = xxxx
79
- this.expressions = this.expressions.filter((exp) => exp.isMatch() !== true);
165
+ if (!exp.isMatched()) {
166
+ // --- rule expressions are always evaluated in the context of AND
167
+ // --- any false expression will make the rule not match
168
+ this.isCompleteEvaluated = true;
169
+ this.isMatched = false;
170
+ return this;
80
171
  }
81
172
  }
82
- return this;
173
+ if (unresolvable) {
174
+ // there is at least one exp is unresolvable now
175
+ return this;
176
+ }
177
+ else {
178
+ this.isCompleteEvaluated = true;
179
+ this.isMatched = true;
180
+ return this;
181
+ }
182
+ }
183
+ /**
184
+ * Whether or not the rule is resolvable (i.e. we can tell whether it's matched or not) now.
185
+ *
186
+ * @return {*} {boolean}
187
+ * @memberof RegoRule
188
+ */
189
+ isResolvable() {
190
+ if (!this.isCompleteEvaluated) {
191
+ this.evaluate();
192
+ }
193
+ return this.isCompleteEvaluated;
83
194
  }
84
195
  /**
85
196
  * Generate Human Readable string of this rule
@@ -103,6 +214,30 @@ class RegoRule {
103
214
  return parts.join(" AND \n");
104
215
  }
105
216
  }
217
+ toData() {
218
+ return {
219
+ default: this.isDefault,
220
+ value: this.value,
221
+ fullName: this.fullName,
222
+ name: this.name,
223
+ expressions: this.expressions.map((exp, idx) => exp.toData(idx))
224
+ };
225
+ }
226
+ toJson() {
227
+ return JSON.stringify(this.toData());
228
+ }
229
+ toConciseData() {
230
+ return {
231
+ default: this.isDefault,
232
+ value: this.value,
233
+ fullName: this.fullName,
234
+ name: this.name,
235
+ expressions: this.expressions.map((exp) => exp.toConciseData())
236
+ };
237
+ }
238
+ toConciseJSON() {
239
+ return JSON.stringify(this.toConciseData());
240
+ }
106
241
  /**
107
242
  * Create RegoRule from Opa response data
108
243
  *
@@ -129,7 +264,6 @@ class RegoRule {
129
264
  parser
130
265
  };
131
266
  const regoRule = new RegoRule(ruleOptions);
132
- regoRule.evaluate();
133
267
  return regoRule;
134
268
  }
135
269
  static createExpressionsFromRuleBodyData(data, parser) {
@@ -176,9 +310,23 @@ exports.RegoOperators = {
176
310
  */
177
311
  class RegoTerm {
178
312
  constructor(type, value, parser) {
313
+ /**
314
+ * Whether the expression contains any resolvable references.
315
+ * reference start with `input.` should be considered as non-resolvable in context of partial evaluation.
316
+ * When this field is set to `true`, we should not attempt to evaluate this expression.
317
+ * i.e. evaluate() method should return immediately.
318
+ * This will speed up evaluation process.
319
+ *
320
+ * @type {boolean}
321
+ * @memberof RegoTerm
322
+ */
323
+ this.hasNoResolvableRef = false;
179
324
  this.type = type;
180
325
  this.value = value;
181
326
  this.parser = parser;
327
+ if (this.value instanceof RegoRef && this.value.hasNoResolvableRef) {
328
+ this.hasNoResolvableRef = true;
329
+ }
182
330
  }
183
331
  clone() {
184
332
  return new RegoTerm(this.type, this.value, this.parser);
@@ -193,7 +341,7 @@ class RegoTerm {
193
341
  if (this.value instanceof RegoRef)
194
342
  return this.value.fullRefString();
195
343
  else
196
- return this.value;
344
+ return JSON.stringify(this.value);
197
345
  }
198
346
  /**
199
347
  * If it's a reference term. A operator is an Reference term as well
@@ -329,6 +477,9 @@ class RegoTerm {
329
477
  * @memberof RegoTerm
330
478
  */
331
479
  getValue() {
480
+ if (this.hasNoResolvableRef) {
481
+ return undefined;
482
+ }
332
483
  if (!this.isRef()) {
333
484
  return this.value;
334
485
  }
@@ -338,13 +489,67 @@ class RegoTerm {
338
489
  }
339
490
  else {
340
491
  const fullName = this.fullRefString();
341
- const result = this.parser.completeRuleResults[fullName];
342
- if (lodash_1.default.isUndefined(result))
492
+ if (!this.parser.isRefResolvable(fullName)) {
343
493
  return undefined;
344
- return result.value;
494
+ }
495
+ return this.parser.getRefValue(fullName);
345
496
  }
346
497
  }
347
498
  }
499
+ /**
500
+ * Whether or not the RegoTerm is resolvable
501
+ *
502
+ * @return {*} {boolean}
503
+ * @memberof RegoTerm
504
+ */
505
+ isValueResolvable() {
506
+ if (this.hasNoResolvableRef) {
507
+ return false;
508
+ }
509
+ if (!this.isRef()) {
510
+ return true;
511
+ }
512
+ else {
513
+ if (this.isOperator()) {
514
+ return false;
515
+ }
516
+ else {
517
+ const fullName = this.fullRefString();
518
+ return this.parser.isRefResolvable(fullName);
519
+ }
520
+ }
521
+ }
522
+ toData() {
523
+ if (this.isRef()) {
524
+ return this.value.toData();
525
+ }
526
+ else {
527
+ return {
528
+ type: this.type,
529
+ value: this.value
530
+ };
531
+ }
532
+ }
533
+ toJson() {
534
+ return JSON.stringify(this.toData());
535
+ }
536
+ toConciseData() {
537
+ if (this.isRef()) {
538
+ return {
539
+ isRef: true,
540
+ value: this.fullRefString()
541
+ };
542
+ }
543
+ else {
544
+ return {
545
+ isRef: false,
546
+ value: this.value
547
+ };
548
+ }
549
+ }
550
+ toConciseJSON() {
551
+ return JSON.stringify(this.toConciseData());
552
+ }
348
553
  static parseFromData(data, parser) {
349
554
  if (data.type === "ref") {
350
555
  return new RegoTerm(data.type, RegoRef.parseFromData(data), parser);
@@ -363,6 +568,18 @@ exports.RegoTerm = RegoTerm;
363
568
  */
364
569
  class RegoExp {
365
570
  constructor(terms, isNegated = false, isCompleteEvaluated = false, value = null, parser) {
571
+ var _a;
572
+ /**
573
+ * Whether the expression contains any resolvable references.
574
+ * reference start with `input.` should be considered as non-resolvable in context of partial evaluation.
575
+ * When this field is set to `true`, we should not attempt to evaluate this expression.
576
+ * i.e. evaluate() method should return immediately.
577
+ * This will speed up evaluation process.
578
+ *
579
+ * @type {boolean}
580
+ * @memberof RegoExp
581
+ */
582
+ this.hasNoResolvableRef = false;
366
583
  /**
367
584
  * If it's complete evaluated
368
585
  *
@@ -382,6 +599,10 @@ class RegoExp {
382
599
  this.isCompleteEvaluated = isCompleteEvaluated;
383
600
  this.value = value;
384
601
  this.parser = parser;
602
+ if (((_a = this === null || this === void 0 ? void 0 : this.terms) === null || _a === void 0 ? void 0 : _a.length) &&
603
+ this.terms.findIndex((item) => !item.hasNoResolvableRef) === -1) {
604
+ this.hasNoResolvableRef = true;
605
+ }
385
606
  }
386
607
  clone() {
387
608
  const regoExp = new RegoExp(this.terms.map((t) => t.clone()), this.isNegated, this.isCompleteEvaluated, this.value, this.parser);
@@ -394,7 +615,22 @@ class RegoExp {
394
615
  * @memberof RegoExp
395
616
  */
396
617
  termsAsString() {
397
- return this.terms.map((t) => t.asString());
618
+ return this.terms.map((t) => t.asString()).join(" ");
619
+ }
620
+ /**
621
+ * Print concise format expression string presentation.
622
+ * Can be used for debugging
623
+ *
624
+ * @return {*}
625
+ * @memberof RegoExp
626
+ */
627
+ asString() {
628
+ if (this.isNegated) {
629
+ return "NOT " + this.termsAsString();
630
+ }
631
+ else {
632
+ return this.termsAsString();
633
+ }
398
634
  }
399
635
  /**
400
636
  * Output human readable string
@@ -462,18 +698,41 @@ class RegoExp {
462
698
  return this.value;
463
699
  }
464
700
  }
465
- isMatch() {
466
- const value = this.getValue();
467
- if (lodash_1.default.isUndefined(value)) {
701
+ /**
702
+ * Whether or not a expression should be considered as "matched".
703
+ * If all expressions of a rule are "matched", the rule will be considered as "matched".
704
+ * Thus, the rule has a value.
705
+ *
706
+ * Please note: if an expression's value is `0`, empty string "", null etc, the expression is considered as "matched".
707
+ * We only consider an expression as "Not Matched" when the expression has value `false` or is undefined.
708
+ *
709
+ * @return {boolean}
710
+ * @memberof RegoExp
711
+ */
712
+ isMatched() {
713
+ if (!this.isResolvable()) {
468
714
  return undefined;
469
715
  }
716
+ const isMatched = this.value === false || lodash_1.default.isUndefined(this.value) ? false : true;
717
+ if (this.isNegated) {
718
+ return !isMatched;
719
+ }
470
720
  else {
471
- if (value === false || lodash_1.default.isUndefined(value))
472
- return false;
473
- // --- 0 is a match
474
- return true;
721
+ return isMatched;
475
722
  }
476
723
  }
724
+ /**
725
+ * Whether or not the expression is resolvable now.
726
+ *
727
+ * @return {boolean}
728
+ * @memberof RegoExp
729
+ */
730
+ isResolvable() {
731
+ if (!this.isCompleteEvaluated) {
732
+ this.evaluate();
733
+ }
734
+ return this.isCompleteEvaluated;
735
+ }
477
736
  /**
478
737
  * Convert operator term to string and put rest operands into an array.
479
738
  * And then return a [Operator, Operands] structure
@@ -492,12 +751,7 @@ class RegoExp {
492
751
  operator = t.asOperator();
493
752
  }
494
753
  else {
495
- const value = t.getValue();
496
- if (!lodash_1.default.isUndefined(value)) {
497
- operands.push(new RegoTerm(typeof value, value, this.parser));
498
- }
499
- else
500
- operands.push(t);
754
+ operands.push(t);
501
755
  }
502
756
  });
503
757
  if (!operator) {
@@ -515,19 +769,27 @@ class RegoExp {
515
769
  * @memberof RegoExp
516
770
  */
517
771
  evaluate() {
772
+ if (this.hasNoResolvableRef) {
773
+ return this;
774
+ }
775
+ if (this.isCompleteEvaluated) {
776
+ return this;
777
+ }
778
+ // --- so far there is no 2 terms expression e.g. ! x
779
+ // --- builtin function should never be included in residual rule
780
+ // --- as we won't apply them on unknowns
518
781
  if (this.terms.length === 0) {
519
782
  // --- exp should be considered as matched (true)
520
- // --- unless isNegated is true
521
- // --- will try to normalise isNegated here
522
783
  this.isCompleteEvaluated = true;
523
- this.value = this.isNegated ? false : true;
524
- this.isNegated = false;
784
+ this.value = true;
785
+ return this;
525
786
  }
526
- if (this.terms.length === 1) {
787
+ else if (this.terms.length === 1) {
527
788
  const term = this.terms[0];
528
- const value = term.getValue();
529
- if (lodash_1.default.isUndefined(value))
789
+ if (!term.isValueResolvable()) {
530
790
  return this;
791
+ }
792
+ const value = term.getValue();
531
793
  this.value = value;
532
794
  this.isCompleteEvaluated = true;
533
795
  return this;
@@ -536,47 +798,101 @@ class RegoExp {
536
798
  // --- 3 terms expression e.g. true == true or x >= 3
537
799
  // --- we only evalute some redundant expression e.g. true == true or false != true
538
800
  const [operator, operands] = this.toOperatorOperandsArray();
539
- if (operands.findIndex((op) => op.isRef()) !== -1) {
540
- // --- this expression involve unknown no need to evalute
801
+ if (!operands[0].isValueResolvable() ||
802
+ !operands[1].isValueResolvable()) {
803
+ // if one of the term value is resolvable now, we can't evaluate further.
541
804
  return this;
542
805
  }
543
- else {
544
- const operandsValues = operands.map((op) => op.getValue());
545
- let value = null;
546
- switch (operator) {
547
- case "=":
548
- value = operandsValues[0] === operandsValues[1];
549
- break;
550
- case ">":
551
- value = operandsValues[0] > operandsValues[1];
552
- break;
553
- case "<":
554
- value = operandsValues[0] < operandsValues[1];
555
- break;
556
- case ">=":
557
- value = operandsValues[0] >= operandsValues[1];
558
- break;
559
- case "<=":
560
- value = operandsValues[0] <= operandsValues[1];
561
- break;
562
- case "!=":
563
- value = operandsValues[0] != operandsValues[1];
564
- break;
565
- default:
566
- throw new Error(`Invalid 3 terms rego expression, Unknown operator "${operator}": ${this.termsAsString()}`);
567
- }
568
- this.isCompleteEvaluated = true;
569
- this.value = value;
570
- return this;
806
+ const operandsValues = operands.map((op) => op.getValue());
807
+ if (operandsValues.findIndex((v) => typeof v === "undefined")) {
571
808
  }
809
+ let value = null;
810
+ switch (operator) {
811
+ case "=":
812
+ value = operandsValues[0] === operandsValues[1];
813
+ break;
814
+ case ">":
815
+ value = operandsValues[0] > operandsValues[1];
816
+ break;
817
+ case "<":
818
+ value = operandsValues[0] < operandsValues[1];
819
+ break;
820
+ case ">=":
821
+ value = operandsValues[0] >= operandsValues[1];
822
+ break;
823
+ case "<=":
824
+ value = operandsValues[0] <= operandsValues[1];
825
+ break;
826
+ case "!=":
827
+ value = operandsValues[0] != operandsValues[1];
828
+ break;
829
+ default:
830
+ throw new Error(`Invalid 3 terms rego expression, Unknown operator "${operator}": ${this.termsAsString()}`);
831
+ }
832
+ this.isCompleteEvaluated = true;
833
+ this.value = value;
834
+ return this;
572
835
  }
573
836
  else {
574
837
  throw new Error(`Invalid ${this.terms.length} terms rego expression: ${this.termsAsString()}`);
575
838
  }
576
- // --- so far there is no 2 terms expression e.g. ! x
577
- // --- builtin function should never be included in residual rule
578
- // --- as we won't apply them on unknowns
579
- return this;
839
+ }
840
+ toData(index = 0, ignoreIndex = false, ignoreNegated = false) {
841
+ const terms = this.terms.map((term) => term.toData());
842
+ if (this.isNegated && !ignoreNegated) {
843
+ if (ignoreIndex) {
844
+ return {
845
+ negated: true,
846
+ terms
847
+ };
848
+ }
849
+ else {
850
+ return {
851
+ negated: true,
852
+ index,
853
+ terms
854
+ };
855
+ }
856
+ }
857
+ else {
858
+ if (ignoreIndex) {
859
+ return {
860
+ terms
861
+ };
862
+ }
863
+ else {
864
+ return {
865
+ index,
866
+ terms
867
+ };
868
+ }
869
+ }
870
+ }
871
+ toJSON(index = 0, ignoreIndex = false, ignoreNegated = false) {
872
+ return JSON.stringify(this.toData(index, ignoreIndex, ignoreNegated));
873
+ }
874
+ toConciseData() {
875
+ let data;
876
+ if (this.terms.length === 1) {
877
+ const term = this.terms[0];
878
+ data = {
879
+ negated: this.isNegated,
880
+ operator: null,
881
+ operands: [term.toConciseData()]
882
+ };
883
+ }
884
+ else {
885
+ const [operator, operands] = this.toOperatorOperandsArray();
886
+ data = {
887
+ negated: this.isNegated,
888
+ operator,
889
+ operands: operands.map((item) => item.toConciseData())
890
+ };
891
+ }
892
+ return data;
893
+ }
894
+ toConciseJSON() {
895
+ return JSON.stringify(this.toConciseData());
580
896
  }
581
897
  static parseFromData(expData, parser) {
582
898
  const isNegated = expData.negated === true;
@@ -608,11 +924,38 @@ exports.RegoExp = RegoExp;
608
924
  */
609
925
  class RegoRef {
610
926
  constructor(parts) {
927
+ var _a;
928
+ /**
929
+ * Whether the expression contains any resolvable references.
930
+ * reference start with `input.` should be considered as non-resolvable in context of partial evaluation.
931
+ * When this field is set to `true`, we should not attempt to evaluate this expression.
932
+ * i.e. evaluate() method should return immediately.
933
+ * This will speed up evaluation process.
934
+ *
935
+ * @type {boolean}
936
+ * @memberof RegoRef
937
+ */
938
+ this.hasNoResolvableRef = false;
611
939
  this.parts = parts;
940
+ if (
941
+ // `input.` ref should be considered not resolvable in partial evaluate context.
942
+ (this.parts.length && ((_a = this.parts[0]) === null || _a === void 0 ? void 0 : _a.value) === "input") ||
943
+ this.isOperator()) {
944
+ this.hasNoResolvableRef = true;
945
+ }
612
946
  }
613
947
  clone() {
614
948
  return new RegoRef(this.parts.map((p) => (Object.assign({}, p))));
615
949
  }
950
+ toData() {
951
+ return {
952
+ type: "ref",
953
+ value: this.parts
954
+ };
955
+ }
956
+ toJson() {
957
+ return JSON.stringify(this.toData());
958
+ }
616
959
  static parseFromData(data) {
617
960
  if (data.type === "ref") {
618
961
  return new RegoRef(data.value);
@@ -662,10 +1005,8 @@ class RegoRef {
662
1005
  if (isFirstPart)
663
1006
  isFirstPart = false;
664
1007
  return partStr;
665
- //--- a.[_].[_] should be a[_][_]
666
1008
  })
667
- .join(".")
668
- .replace(/\.\[/g, "[");
1009
+ .join(".");
669
1010
  return this.removeAllPrefixs(str, removalPrefixs);
670
1011
  }
671
1012
  refString(removalPrefixs = []) {
@@ -718,6 +1059,189 @@ function value2String(value) {
718
1059
  return JSON.stringify(value);
719
1060
  }
720
1061
  exports.value2String = value2String;
1062
+ class RegoRuleSet {
1063
+ constructor(parser, rules, fullName = "", name = "") {
1064
+ var _a, _b;
1065
+ this.fullName = "";
1066
+ this.name = "";
1067
+ this.rules = [];
1068
+ this.defaultRule = null;
1069
+ this.isCompleteEvaluated = false;
1070
+ /**
1071
+ * Whether the ruleSet contains any rules that has any resolvable references.
1072
+ * reference start with `input.` should be considered as non-resolvable in context of partial evaluation.
1073
+ * When this field is set to `true`, we should not attempt to evaluate this expression.
1074
+ * i.e. evaluate() method should return immediately.
1075
+ * This will speed up evaluation process.
1076
+ *
1077
+ * @type {boolean}
1078
+ * @memberof RegoRuleSet
1079
+ */
1080
+ this.hasNoResolvableRef = false;
1081
+ this.parser = parser;
1082
+ if (rules === null || rules === void 0 ? void 0 : rules.length) {
1083
+ const defaultRuleIdx = rules.findIndex((r) => r.isDefault);
1084
+ if (defaultRuleIdx !== -1) {
1085
+ this.defaultRule = rules[defaultRuleIdx];
1086
+ }
1087
+ this.rules = rules.filter((r) => !r.isDefault);
1088
+ }
1089
+ if (fullName) {
1090
+ this.fullName = fullName;
1091
+ }
1092
+ else if ((_a = rules === null || rules === void 0 ? void 0 : rules[0]) === null || _a === void 0 ? void 0 : _a.fullName) {
1093
+ this.fullName = rules[0].fullName;
1094
+ }
1095
+ if (name) {
1096
+ this.name = name;
1097
+ }
1098
+ else if ((_b = rules === null || rules === void 0 ? void 0 : rules[0]) === null || _b === void 0 ? void 0 : _b.name) {
1099
+ this.name = rules[0].name;
1100
+ }
1101
+ if (this.rules.length &&
1102
+ this.rules.findIndex((r) => !r.hasNoResolvableRef) === -1) {
1103
+ this.hasNoResolvableRef = true;
1104
+ }
1105
+ this.evaluate();
1106
+ }
1107
+ evaluate() {
1108
+ var _a;
1109
+ if (this.hasNoResolvableRef) {
1110
+ return this;
1111
+ }
1112
+ if (this.isCompleteEvaluated) {
1113
+ return this;
1114
+ }
1115
+ if (!((_a = this.rules) === null || _a === void 0 ? void 0 : _a.length)) {
1116
+ if (!this.defaultRule) {
1117
+ this.isCompleteEvaluated = true;
1118
+ this.value = undefined;
1119
+ return this;
1120
+ }
1121
+ else {
1122
+ if (this.defaultRule.isResolvable()) {
1123
+ this.isCompleteEvaluated = true;
1124
+ this.value = this.defaultRule.value;
1125
+ return this;
1126
+ }
1127
+ else {
1128
+ return this;
1129
+ }
1130
+ }
1131
+ }
1132
+ this.rules.forEach((r) => r.evaluate());
1133
+ const matchedRule = this.rules.find((r) => r.isResolvable() && r.isMatched);
1134
+ if (matchedRule) {
1135
+ this.isCompleteEvaluated = true;
1136
+ this.value = matchedRule.value;
1137
+ return this;
1138
+ }
1139
+ if (this.rules.findIndex((r) => !r.isResolvable()) !== -1) {
1140
+ // still has rule unresolvable
1141
+ return this;
1142
+ }
1143
+ // rest (if any) are all unmatched rules
1144
+ if (!this.defaultRule) {
1145
+ this.isCompleteEvaluated = true;
1146
+ this.value = undefined;
1147
+ return this;
1148
+ }
1149
+ else {
1150
+ if (this.defaultRule.isResolvable()) {
1151
+ this.isCompleteEvaluated = true;
1152
+ this.value = this.defaultRule.value;
1153
+ return this;
1154
+ }
1155
+ else {
1156
+ return this;
1157
+ }
1158
+ }
1159
+ }
1160
+ isResolvable() {
1161
+ if (!this.isCompleteEvaluated) {
1162
+ this.evaluate();
1163
+ }
1164
+ return this.isCompleteEvaluated;
1165
+ }
1166
+ getResidualRules() {
1167
+ if (this.isResolvable()) {
1168
+ return [];
1169
+ }
1170
+ let rules = this.defaultRule
1171
+ ? [this.defaultRule, ...this.rules]
1172
+ : [...this.rules];
1173
+ rules = this.rules.filter((r) => !r.isResolvable());
1174
+ if (!rules.length) {
1175
+ return [];
1176
+ }
1177
+ rules = lodash_1.default.flatMap(rules, (rule) => {
1178
+ // all resolvable expressions can all be ignored as:
1179
+ // - if the expression is resolved to "matched", it won't impact the result of the rule
1180
+ // - if the expression is resolved to "unmatched", the rule should be resolved to "unmatched" earlier.
1181
+ const unresolvedExpressions = rule.expressions.filter((exp) => !exp.isResolvable());
1182
+ if (unresolvedExpressions.length !== 1) {
1183
+ return [rule];
1184
+ }
1185
+ // For rules with single expression, reduce the layer by replacing it with target reference rules
1186
+ const exp = unresolvedExpressions[0];
1187
+ if (exp.terms.length === 1) {
1188
+ const fullName = exp.terms[0].fullRefString();
1189
+ const ruleSet = this.parser.ruleSets[fullName];
1190
+ if (!ruleSet) {
1191
+ const compressedRule = rule.clone();
1192
+ compressedRule.expressions = [exp];
1193
+ return [compressedRule];
1194
+ }
1195
+ return ruleSet.getResidualRules();
1196
+ }
1197
+ else if (exp.terms.length === 3) {
1198
+ const [operator, [op1, op2]] = exp.toOperatorOperandsArray();
1199
+ if (operator != "=" && operator != "!=") {
1200
+ // For now, we will only further process the ref when operator is = or !=
1201
+ return [rule];
1202
+ }
1203
+ if (!op1.isValueResolvable() && !op2.isValueResolvable()) {
1204
+ // when both op1 & op1 are not resolvable ref, we will not attempt to process further
1205
+ return [rule];
1206
+ }
1207
+ const value = op1.isValueResolvable()
1208
+ ? op1.getValue()
1209
+ : op2.getValue();
1210
+ const refTerm = op1.isValueResolvable() ? op2 : op1;
1211
+ const fullName = refTerm.fullRefString();
1212
+ const ruleSet = this.parser.ruleSets[fullName];
1213
+ if (!ruleSet) {
1214
+ const compressedRule = rule.clone();
1215
+ compressedRule.expressions = [exp];
1216
+ return [compressedRule];
1217
+ }
1218
+ let refRules = ruleSet.getResidualRules();
1219
+ // when negated expression, reverse the operator
1220
+ const convertedOperator = exp.isNegated
1221
+ ? operator == "="
1222
+ ? "!="
1223
+ : "="
1224
+ : operator;
1225
+ if (convertedOperator == "=") {
1226
+ refRules = refRules.filter((r) => r.value == value);
1227
+ }
1228
+ else {
1229
+ refRules = refRules.filter((r) => r.value != value);
1230
+ }
1231
+ if (!refRules.length) {
1232
+ // this means this rule can never matched
1233
+ return [];
1234
+ }
1235
+ return refRules;
1236
+ }
1237
+ else {
1238
+ throw new Error(`Failed to produce residualRules for rule: ${rule.toJson()}`);
1239
+ }
1240
+ });
1241
+ return rules;
1242
+ }
1243
+ }
1244
+ exports.RegoRuleSet = RegoRuleSet;
721
1245
  /**
722
1246
  * OPA result Parser
723
1247
  *
@@ -756,6 +1280,13 @@ class OpaCompileResponseParser {
756
1280
  * @memberof OpaCompileResponseParser
757
1281
  */
758
1282
  this.rules = [];
1283
+ /**
1284
+ * Parsed, compressed & evaluated rule sets
1285
+ *
1286
+ * @type {RegoRuleSet[]}
1287
+ * @memberof OpaCompileResponseParser
1288
+ */
1289
+ this.ruleSets = {};
759
1290
  this.queries = [];
760
1291
  /**
761
1292
  * A cache of all resolved rule result
@@ -774,6 +1305,7 @@ class OpaCompileResponseParser {
774
1305
  * @memberof OpaCompileResponseParser
775
1306
  */
776
1307
  this.pseudoQueryRuleName = RegoRule.randomRuleName("default_rule_");
1308
+ this.ruleDuplicationCheckCache = {};
777
1309
  }
778
1310
  setQueryRuleResult(val) {
779
1311
  this.completeRuleResults[this.pseudoQueryRuleName] = {
@@ -797,20 +1329,26 @@ class OpaCompileResponseParser {
797
1329
  else {
798
1330
  this.data = json;
799
1331
  }
800
- if (!this.data.result) {
1332
+ /**
1333
+ * OPA might output {"result": {}} as unconditional `false` or never matched
1334
+ */
1335
+ if (!this.data.result || !Object.keys(this.data.result).length) {
801
1336
  // --- mean no rule matched
802
1337
  this.setQueryRuleResult(false);
803
1338
  return [];
804
1339
  }
805
1340
  this.data = this.data.result;
806
- if ((!this.data.queries ||
1341
+ if (!this.data.queries ||
807
1342
  !lodash_1.default.isArray(this.data.queries) ||
808
- !this.data.queries.length) &&
809
- (!lodash_1.default.isArray(this.data.support) || !this.data.support.length)) {
810
- // --- mean no rule matched
1343
+ !this.data.queries.length) {
811
1344
  this.setQueryRuleResult(false);
812
1345
  return [];
813
1346
  }
1347
+ if (this.data.queries.findIndex((q) => !(q === null || q === void 0 ? void 0 : q.length)) !== -1) {
1348
+ // when query is always true, the "queries" value in the result will contain an empty array
1349
+ this.setQueryRuleResult(true);
1350
+ return [];
1351
+ }
814
1352
  const queries = this.data.queries;
815
1353
  if (queries) {
816
1354
  if (queries.findIndex((ruleBody) => !ruleBody || !ruleBody.length) !== -1) {
@@ -828,9 +1366,9 @@ class OpaCompileResponseParser {
828
1366
  value: true,
829
1367
  parser: this
830
1368
  });
831
- rule.evaluate();
832
- this.originalRules.push(rule);
833
- this.rules.push(rule);
1369
+ // this.originalRules.push(rule);
1370
+ // this.rules.push(rule);
1371
+ this.addRule(rule);
834
1372
  });
835
1373
  }
836
1374
  const packages = this.data.support;
@@ -844,85 +1382,69 @@ class OpaCompileResponseParser {
844
1382
  const rules = p.rules;
845
1383
  rules.forEach((r) => {
846
1384
  const regoRule = RegoRule.parseFromData(r, packageName, this);
847
- this.originalRules.push(regoRule);
848
- // --- only save matched rules
849
- if (!regoRule.isCompleteEvaluated) {
850
- this.rules.push(regoRule);
851
- }
852
- else {
853
- if (regoRule.isMatched) {
854
- this.rules.push(regoRule);
855
- }
856
- }
1385
+ //this.originalRules.push(regoRule);
1386
+ //this.rules.push(regoRule);
1387
+ this.addRule(regoRule);
857
1388
  });
858
1389
  });
859
1390
  }
860
- this.calculateCompleteRuleResult();
861
- this.reduceDependencies();
1391
+ this.ruleDuplicationCheckCache = {};
1392
+ lodash_1.default.uniq(this.rules.map((r) => r.fullName)).forEach((fullName) => (this.ruleSets[fullName] = new RegoRuleSet(this, this.rules.filter((r) => r.fullName === fullName), fullName)));
1393
+ this.resolveAllRuleSets();
862
1394
  return this.rules;
863
1395
  }
864
- /**
865
- * Tried to merge rules outcome so that the ref value can be established easier
866
- * After this step, any rules doesn't involve unknown should be merged to one value
867
- * This will help to generate more concise query later.
868
- * `CompleteRule` rule involves no `unknowns`
869
- *
870
- * Only for internal usage
871
- *
872
- * @private
873
- * @memberof OpaCompileResponseParser
874
- */
875
- calculateCompleteRuleResult() {
876
- const fullNames = this.rules.map((r) => r.fullName);
877
- fullNames.forEach((fullName) => {
878
- const rules = this.rules.filter((r) => r.fullName === fullName);
879
- const nonCompletedRules = rules.filter((r) => !r.isCompleteEvaluated);
880
- const completedRules = rules.filter((r) => r.isCompleteEvaluated);
881
- const defaultRules = completedRules.filter((r) => r.isDefault);
882
- const nonDefaultRules = completedRules.filter((r) => !r.isDefault);
883
- if (nonDefaultRules.length) {
884
- // --- if a non default complete eveluated rules exist
885
- // --- it will be the final outcome
886
- this.completeRuleResults[fullName] = this.createCompleteRuleResult(nonDefaultRules[0]);
887
- return;
888
- }
889
- if (!nonCompletedRules.length) {
890
- // --- if no unevaluated rule left, default rule value should be used
891
- if (defaultRules.length) {
892
- this.completeRuleResults[fullName] = this.createCompleteRuleResult(defaultRules[0]);
893
- return;
894
- }
895
- else {
896
- // --- no matched complete non default rule left; Not possible
897
- throw new Error(`Unexpected empty rule result for ${fullName}`);
898
- }
1396
+ addRule(rule) {
1397
+ this.originalRules.push(rule);
1398
+ if (rule.isImpossible()) {
1399
+ return;
1400
+ }
1401
+ if (!this.ruleDuplicationCheckCache[rule.fullName]) {
1402
+ this.ruleDuplicationCheckCache[rule.fullName] = new Set();
1403
+ }
1404
+ const setData = this.ruleDuplicationCheckCache[rule.fullName];
1405
+ const _a = rule.toData(), { name, fullName } = _a, ruleData = __rest(_a, ["name", "fullName"]);
1406
+ const jsonData = JSON.stringify(ruleData);
1407
+ const size = setData.size;
1408
+ setData.add(jsonData);
1409
+ if (size === setData.size) {
1410
+ return;
1411
+ }
1412
+ this.rules.push(rule);
1413
+ }
1414
+ isRefResolvable(fullName) {
1415
+ if (this.completeRuleResults[fullName]) {
1416
+ return true;
1417
+ }
1418
+ const ruleSet = this.ruleSets[fullName];
1419
+ if (!ruleSet) {
1420
+ return false;
1421
+ }
1422
+ return ruleSet.isResolvable();
1423
+ }
1424
+ getRefValue(fullName) {
1425
+ const completeResult = this.completeRuleResults[fullName];
1426
+ if (completeResult) {
1427
+ return completeResult.value;
1428
+ }
1429
+ const ruleSet = this.ruleSets[fullName];
1430
+ if (!ruleSet || !ruleSet.isResolvable()) {
1431
+ return undefined;
1432
+ }
1433
+ return ruleSet.value;
1434
+ }
1435
+ resolveAllRuleSets() {
1436
+ while (true) {
1437
+ const unresolvedSetsNum = Object.values(this.ruleSets).filter((rs) => !rs.isResolvable()).length;
1438
+ if (!unresolvedSetsNum) {
1439
+ break;
899
1440
  }
900
- else {
901
- // --- do nothing
902
- // --- Some defaultRules might be able to strip out once
903
- // --- nonCompleteRules are determined later
904
- return;
1441
+ Object.values(this.ruleSets).forEach((rs) => rs.evaluate());
1442
+ const newUnresolvedSetsNum = Object.values(this.ruleSets).filter((rs) => !rs.isResolvable()).length;
1443
+ if (!newUnresolvedSetsNum ||
1444
+ newUnresolvedSetsNum >= unresolvedSetsNum) {
1445
+ break;
905
1446
  }
906
- });
907
- }
908
- /**
909
- * Only for internal usage
910
- *
911
- * @returns
912
- * @private
913
- * @memberof OpaCompileResponseParser
914
- */
915
- reduceDependencies() {
916
- const rules = this.rules.filter((r) => !r.isCompleteEvaluated);
917
- if (!rules.length)
918
- return;
919
- for (let i = 0; i < rules.length; i++) {
920
- const rule = rules[i];
921
- rule.expressions = rule.expressions.map((e) => e.evaluate());
922
- rule.evaluate();
923
1447
  }
924
- // --- unmatched non-default rule can be stripped out
925
- this.rules = this.rules.filter((r) => !(r.isCompleteEvaluated && !r.isMatched && !r.isDefault));
926
1448
  }
927
1449
  /**
928
1450
  * Call to evaluate a rule
@@ -932,136 +1454,28 @@ class OpaCompileResponseParser {
932
1454
  * @memberof OpaCompileResponseParser
933
1455
  */
934
1456
  evaluateRule(fullName) {
935
- var _a, _b, _c;
936
- if ((_b = (_a = this.completeRuleResults) === null || _a === void 0 ? void 0 : _a[fullName]) === null || _b === void 0 ? void 0 : _b.isCompleteEvaluated) {
937
- // --- already evaluated during paring or dependencies removal
938
- return (_c = this.completeRuleResults) === null || _c === void 0 ? void 0 : _c[fullName];
1457
+ if (this.completeRuleResults[fullName]) {
1458
+ return this.completeRuleResults[fullName];
939
1459
  }
940
- let rules = this.rules.filter((r) => r.fullName === fullName);
941
- const originalRuleName = rules[0].name;
942
- if (!rules.length) {
943
- // --- no any rule matched; often (depends on your policy) it means a overall non-matched (false)
1460
+ const ruleSet = this.ruleSets[fullName];
1461
+ if (!ruleSet) {
944
1462
  return null;
945
1463
  }
946
- const defaultRule = rules.find((r) => r.isDefault);
947
- const defaultValue = lodash_1.default.isUndefined(defaultRule)
948
- ? undefined
949
- : defaultRule.value;
950
- if (rules.find((r) => r.isCompleteEvaluated))
951
- // --- filter out default rules & unmatched
952
- // --- isMatch is only set when r.isCompleteEvaluated = true
953
- rules = rules.filter((r) => !(r.isDefault || (r.isCompleteEvaluated && !r.isMatched)));
954
- if (!rules.length) {
1464
+ if (ruleSet.isResolvable()) {
955
1465
  return {
956
1466
  fullName,
957
- name: defaultRule ? defaultRule.name : "",
958
- value: defaultValue,
959
- isCompleteEvaluated: true,
960
- residualRules: []
1467
+ name: ruleSet.name,
1468
+ value: ruleSet.value,
1469
+ isCompleteEvaluated: true
961
1470
  };
962
1471
  }
963
1472
  else {
964
- const matchedRule = rules.find((r) => r.isMatched);
965
- if (matchedRule) {
966
- return {
967
- fullName,
968
- name: originalRuleName,
969
- value: matchedRule.value,
970
- isCompleteEvaluated: true,
971
- residualRules: []
972
- };
973
- }
974
- const ruleWithEmptyExps = rules.find((r) => !r.expressions.length);
975
- if (ruleWithEmptyExps) {
976
- // empty exp / body means unconditional match
977
- return {
978
- fullName,
979
- name: originalRuleName,
980
- value: ruleWithEmptyExps.value,
981
- isCompleteEvaluated: true,
982
- residualRules: []
983
- };
984
- }
985
- if (rules.length === 1 && rules[0].expressions.length === 1) {
986
- rules[0].expressions[0].terms.length === 1;
987
- }
988
- // if a rules contains one expression only, we will try to resolve any possible rule ref
989
- rules = lodash_1.default.flatMap(rules, (rule) => {
990
- if (rules.length === 1 && rules[0].expressions.length === 1) {
991
- const exp = rules[0].expressions[0];
992
- if (exp.terms.length === 1 && exp.terms[0].isRef()) {
993
- const ruleRef = exp.terms[0].fullRefString();
994
- const result = this.evaluateRule(ruleRef);
995
- if (result) {
996
- if (result.isCompleteEvaluated) {
997
- return [
998
- RegoRule.createFromValue(result.value, this)
999
- ];
1000
- }
1001
- else {
1002
- return result.residualRules;
1003
- }
1004
- }
1005
- else {
1006
- return [rule];
1007
- }
1008
- }
1009
- else if (exp.terms.length === 3) {
1010
- const [opStr, [op1, op2]] = exp.toOperatorOperandsArray();
1011
- if (opStr === "=" &&
1012
- ((op1.isRef() && typeof op2.value === "boolean") ||
1013
- (op2.isRef() && typeof op1.value === "boolean"))) {
1014
- const ruleRef = op1.isRef()
1015
- ? op1.fullRefString()
1016
- : op2.fullRefString();
1017
- const bVal = typeof op1.value === "boolean"
1018
- ? op1.value
1019
- : op2.value;
1020
- const result = this.evaluateRule(ruleRef);
1021
- if (result) {
1022
- if (result.isCompleteEvaluated) {
1023
- if (bVal === false) {
1024
- return [
1025
- RegoRule.createFromValue(!result.value, this)
1026
- ];
1027
- }
1028
- else {
1029
- return [
1030
- RegoRule.createFromValue(result.value, this)
1031
- ];
1032
- }
1033
- }
1034
- else {
1035
- if (bVal === false) {
1036
- return result.residualRules.map((r) => r.clone({ value: !r.value }));
1037
- }
1038
- else {
1039
- return result.residualRules;
1040
- }
1041
- }
1042
- }
1043
- else {
1044
- return [rule];
1045
- }
1046
- }
1047
- else {
1048
- return [rule];
1049
- }
1050
- }
1051
- else {
1052
- return [rule];
1053
- }
1054
- }
1055
- else {
1056
- return [rule];
1057
- }
1058
- });
1059
1473
  return {
1060
1474
  fullName,
1061
- name: rules[0].name,
1475
+ name: ruleSet.name,
1062
1476
  value: undefined,
1063
1477
  isCompleteEvaluated: false,
1064
- residualRules: rules
1478
+ residualRules: ruleSet.getResidualRules()
1065
1479
  };
1066
1480
  }
1067
1481
  }
@@ -1086,6 +1500,9 @@ class OpaCompileResponseParser {
1086
1500
  if (result === null)
1087
1501
  return "null";
1088
1502
  if (result.isCompleteEvaluated) {
1503
+ if (typeof result.value === "undefined") {
1504
+ return "undefined";
1505
+ }
1089
1506
  return value2String(result.value);
1090
1507
  }
1091
1508
  let parts = result.residualRules.map((r) => r.toHumanReadableString());
@@ -1103,23 +1520,6 @@ class OpaCompileResponseParser {
1103
1520
  evaluateAsHumanReadableString() {
1104
1521
  return this.evaluateRuleAsHumanReadableString(this.pseudoQueryRuleName);
1105
1522
  }
1106
- /**
1107
- * Only for internal usage
1108
- *
1109
- * @param {RegoRule} rule
1110
- * @returns {CompleteRuleResult}
1111
- * @private
1112
- * @memberof OpaCompileResponseParser
1113
- */
1114
- createCompleteRuleResult(rule) {
1115
- return {
1116
- fullName: rule.fullName,
1117
- name: rule.name,
1118
- value: rule.value,
1119
- isCompleteEvaluated: true,
1120
- residualRules: []
1121
- };
1122
- }
1123
1523
  reportWarns(msg) {
1124
1524
  this.warns.push(msg);
1125
1525
  this.hasWarns = true;