@magda/typescript-common 1.2.1-alpha.0 → 2.0.0-alpha.0

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 (73) hide show
  1. package/dist/OpaCompileResponseParser.d.ts +147 -34
  2. package/dist/OpaCompileResponseParser.js +479 -293
  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 +59 -1
  11. package/dist/authorization-api/authMiddleware.js +146 -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 +1 -6
  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 +36 -2
  24. package/dist/generated/registry/api.js +140 -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/opa/AspectQuery.d.ts +71 -0
  30. package/dist/opa/AspectQuery.js +216 -0
  31. package/dist/opa/AspectQuery.js.map +1 -0
  32. package/dist/opa/AuthDecision.d.ts +51 -0
  33. package/dist/opa/AuthDecision.js +241 -0
  34. package/dist/opa/AuthDecision.js.map +1 -0
  35. package/dist/opa/AuthDecisionQueryClient.d.ts +23 -0
  36. package/dist/opa/AuthDecisionQueryClient.js +110 -0
  37. package/dist/opa/AuthDecisionQueryClient.js.map +1 -0
  38. package/dist/pgTypes.d.ts +1 -0
  39. package/dist/pgTypes.js +18 -0
  40. package/dist/pgTypes.js.map +1 -0
  41. package/dist/registry/AuthorizedRegistryClient.d.ts +1 -0
  42. package/dist/registry/AuthorizedRegistryClient.js +17 -0
  43. package/dist/registry/AuthorizedRegistryClient.js.map +1 -1
  44. package/dist/registry/RegistryClient.d.ts +10 -0
  45. package/dist/registry/RegistryClient.js +32 -0
  46. package/dist/registry/RegistryClient.js.map +1 -1
  47. package/dist/test/getAuthDecision.spec.js +2 -2
  48. package/dist/test/getAuthDecision.spec.js.map +1 -1
  49. package/dist/test/sampleAuthDecisions/content.json +29 -0
  50. package/dist/test/sampleAuthDecisions/datasetPermissionWithOrgUnitConstraint.json +79 -0
  51. package/dist/test/sampleAuthDecisions/simple.json +29 -0
  52. package/dist/test/sampleAuthDecisions/singleTermAspectRef.json +39 -0
  53. package/dist/test/sampleAuthDecisions/unconditionalFalseSimple.json +6 -0
  54. package/dist/test/sampleAuthDecisions/unconditionalNotMacthed.json +6 -0
  55. package/dist/test/sampleAuthDecisions/unconditionalNotMacthedWithExtraRefs.json +6 -0
  56. package/dist/test/sampleAuthDecisions/unconditionalTrue.json +6 -0
  57. package/dist/test/sampleAuthDecisions/unconditionalTrueSimple.json +6 -0
  58. package/dist/test/sampleAuthDecisions/unconditionalTrueWithDefaultRule.json +6 -0
  59. package/dist/test/sampleAuthDecisions/withDefaultRule.json +6 -0
  60. package/dist/test/{sampleOpaResponse.json → sampleOpaResponses/content.json} +0 -0
  61. package/dist/test/sampleOpaResponses/datasetPermissionWithOrgUnitConstraint.json +341 -0
  62. package/dist/test/{sampleOpaResponseSimple.json → sampleOpaResponses/simple.json} +0 -0
  63. package/dist/test/sampleOpaResponses/singleTermAspectRef.json +233 -0
  64. package/dist/test/sampleOpaResponses/unconditionalFalseSimple.json +3 -0
  65. package/dist/test/sampleOpaResponses/unconditionalNotMacthed.json +73 -0
  66. package/dist/test/sampleOpaResponses/unconditionalNotMacthedWithExtraRefs.json +155 -0
  67. package/dist/test/{sampleOpaResponseUnconditionalTrue.json → sampleOpaResponses/unconditionalTrue.json} +0 -0
  68. package/dist/test/sampleOpaResponses/unconditionalTrueSimple.json +48 -0
  69. package/dist/test/{sampleOpaResponseUnconditionalTrueWithDefaultRule.json → sampleOpaResponses/unconditionalTrueWithDefaultRule.json} +0 -0
  70. package/dist/test/{sampleOpaResponseWithDefaultRule.json → sampleOpaResponses/withDefaultRule.json} +0 -0
  71. package/dist/test/testOpaCompileResponseParser.spec.js +195 -20
  72. package/dist/test/testOpaCompileResponseParser.spec.js.map +1 -1
  73. package/package.json +7 -3
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.unknown2Ref = exports.value2String = exports.RegoRef = exports.RegoExp = exports.RegoTerm = exports.RegoOperators = exports.RegoRule = void 0;
6
+ exports.unknown2Ref = exports.RegoRuleSet = exports.value2String = exports.RegoRef = exports.RegoExp = exports.RegoTerm = exports.RegoOperators = exports.RegoRule = void 0;
7
7
  const lodash_1 = __importDefault(require("lodash"));
8
8
  /**
9
9
  * @class RegoRule
@@ -42,6 +42,7 @@ class RegoRule {
42
42
  if (!(this.parser instanceof OpaCompileResponseParser)) {
43
43
  throw new Error("Require parser parameter to create a RegoRule");
44
44
  }
45
+ this.evaluate();
45
46
  }
46
47
  clone(options = {}) {
47
48
  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 +57,53 @@ class RegoRule {
56
57
  * @memberof RegoRule
57
58
  */
58
59
  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
60
+ var _a;
61
+ if (this.isCompleteEvaluated) {
62
+ return this;
63
+ }
64
+ if (!((_a = this === null || this === void 0 ? void 0 : this.expressions) === null || _a === void 0 ? void 0 : _a.length)) {
65
+ // a rule with empty body / no expression is matched
64
66
  this.isCompleteEvaluated = true;
65
- this.isMatched = false;
67
+ this.isMatched = true;
68
+ return this;
66
69
  }
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;
70
+ let unresolvable = false;
71
+ for (let i = 0; i < this.expressions.length; i++) {
72
+ const exp = this.expressions[i];
73
+ exp.evaluate();
74
+ if (!exp.isResolvable()) {
75
+ unresolvable = true;
76
+ continue;
75
77
  }
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);
78
+ if (!exp.isMatched()) {
79
+ // --- rule expressions are always evaluated in the context of AND
80
+ // --- any false expression will make the rule not match
81
+ this.isCompleteEvaluated = true;
82
+ this.isMatched = false;
83
+ return this;
80
84
  }
81
85
  }
82
- return this;
86
+ if (unresolvable) {
87
+ // there is at least one exp is unresolvable now
88
+ return this;
89
+ }
90
+ else {
91
+ this.isCompleteEvaluated = true;
92
+ this.isMatched = true;
93
+ return this;
94
+ }
95
+ }
96
+ /**
97
+ * Whether or not the rule is resolvable (i.e. we can tell whether it's matched or not) now.
98
+ *
99
+ * @return {*} {boolean}
100
+ * @memberof RegoRule
101
+ */
102
+ isResolvable() {
103
+ if (!this.isCompleteEvaluated) {
104
+ this.evaluate();
105
+ }
106
+ return this.isCompleteEvaluated;
83
107
  }
84
108
  /**
85
109
  * Generate Human Readable string of this rule
@@ -103,6 +127,30 @@ class RegoRule {
103
127
  return parts.join(" AND \n");
104
128
  }
105
129
  }
130
+ toData() {
131
+ return {
132
+ default: this.isDefault,
133
+ value: this.value,
134
+ fullName: this.fullName,
135
+ name: this.name,
136
+ expressions: this.expressions.map((exp, idx) => exp.toData(idx))
137
+ };
138
+ }
139
+ toJson() {
140
+ return JSON.stringify(this.toData());
141
+ }
142
+ toConciseData() {
143
+ return {
144
+ default: this.isDefault,
145
+ value: this.value,
146
+ fullName: this.fullName,
147
+ name: this.name,
148
+ expressions: this.expressions.map((exp) => exp.toConciseData())
149
+ };
150
+ }
151
+ toConciseJSON() {
152
+ return JSON.stringify(this.toConciseData());
153
+ }
106
154
  /**
107
155
  * Create RegoRule from Opa response data
108
156
  *
@@ -129,7 +177,6 @@ class RegoRule {
129
177
  parser
130
178
  };
131
179
  const regoRule = new RegoRule(ruleOptions);
132
- regoRule.evaluate();
133
180
  return regoRule;
134
181
  }
135
182
  static createExpressionsFromRuleBodyData(data, parser) {
@@ -338,13 +385,64 @@ class RegoTerm {
338
385
  }
339
386
  else {
340
387
  const fullName = this.fullRefString();
341
- const result = this.parser.completeRuleResults[fullName];
342
- if (lodash_1.default.isUndefined(result))
388
+ if (!this.parser.isRefResolvable(fullName)) {
343
389
  return undefined;
344
- return result.value;
390
+ }
391
+ return this.parser.getRefValue(fullName);
345
392
  }
346
393
  }
347
394
  }
395
+ /**
396
+ * Whether or not the RegoTerm is resolvable
397
+ *
398
+ * @return {*} {boolean}
399
+ * @memberof RegoTerm
400
+ */
401
+ isValueResolvable() {
402
+ if (!this.isRef()) {
403
+ return true;
404
+ }
405
+ else {
406
+ if (this.isOperator()) {
407
+ return false;
408
+ }
409
+ else {
410
+ const fullName = this.fullRefString();
411
+ return this.parser.isRefResolvable(fullName);
412
+ }
413
+ }
414
+ }
415
+ toData() {
416
+ if (this.isRef()) {
417
+ return this.value.toData();
418
+ }
419
+ else {
420
+ return {
421
+ type: this.type,
422
+ value: this.value
423
+ };
424
+ }
425
+ }
426
+ toJson() {
427
+ return JSON.stringify(this.toData());
428
+ }
429
+ toConciseData() {
430
+ if (this.isRef()) {
431
+ return {
432
+ isRef: true,
433
+ value: this.fullRefString()
434
+ };
435
+ }
436
+ else {
437
+ return {
438
+ isRef: false,
439
+ value: this.value
440
+ };
441
+ }
442
+ }
443
+ toConciseJSON() {
444
+ return JSON.stringify(this.toConciseData());
445
+ }
348
446
  static parseFromData(data, parser) {
349
447
  if (data.type === "ref") {
350
448
  return new RegoTerm(data.type, RegoRef.parseFromData(data), parser);
@@ -462,17 +560,40 @@ class RegoExp {
462
560
  return this.value;
463
561
  }
464
562
  }
465
- isMatch() {
466
- const value = this.getValue();
467
- if (lodash_1.default.isUndefined(value)) {
563
+ /**
564
+ * Whether or not a expression should be considered as "matched".
565
+ * If all expressions of a rule are "matched", the rule will be considered as "matched".
566
+ * Thus, the rule has a value.
567
+ *
568
+ * Please note: if an expression's value is `0`, empty string "", null etc, the expression is considered as "matched".
569
+ * We only consider an expression as "Not Matched" when the expression has value `false` or is undefined.
570
+ *
571
+ * @return {boolean}
572
+ * @memberof RegoExp
573
+ */
574
+ isMatched() {
575
+ if (!this.isResolvable()) {
468
576
  return undefined;
469
577
  }
578
+ const isMatched = this.value === false || lodash_1.default.isUndefined(this.value) ? false : true;
579
+ if (this.isNegated) {
580
+ return !isMatched;
581
+ }
470
582
  else {
471
- if (value === false || lodash_1.default.isUndefined(value))
472
- return false;
473
- // --- 0 is a match
474
- return true;
583
+ return isMatched;
584
+ }
585
+ }
586
+ /**
587
+ * Whether or not the expression is resolvable now.
588
+ *
589
+ * @return {boolean}
590
+ * @memberof RegoExp
591
+ */
592
+ isResolvable() {
593
+ if (!this.isCompleteEvaluated) {
594
+ this.evaluate();
475
595
  }
596
+ return this.isCompleteEvaluated;
476
597
  }
477
598
  /**
478
599
  * Convert operator term to string and put rest operands into an array.
@@ -492,12 +613,7 @@ class RegoExp {
492
613
  operator = t.asOperator();
493
614
  }
494
615
  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);
616
+ operands.push(t);
501
617
  }
502
618
  });
503
619
  if (!operator) {
@@ -515,19 +631,24 @@ class RegoExp {
515
631
  * @memberof RegoExp
516
632
  */
517
633
  evaluate() {
634
+ if (this.isCompleteEvaluated) {
635
+ return this;
636
+ }
637
+ // --- so far there is no 2 terms expression e.g. ! x
638
+ // --- builtin function should never be included in residual rule
639
+ // --- as we won't apply them on unknowns
518
640
  if (this.terms.length === 0) {
519
641
  // --- exp should be considered as matched (true)
520
- // --- unless isNegated is true
521
- // --- will try to normalise isNegated here
522
642
  this.isCompleteEvaluated = true;
523
- this.value = this.isNegated ? false : true;
524
- this.isNegated = false;
643
+ this.value = true;
644
+ return this;
525
645
  }
526
- if (this.terms.length === 1) {
646
+ else if (this.terms.length === 1) {
527
647
  const term = this.terms[0];
528
- const value = term.getValue();
529
- if (lodash_1.default.isUndefined(value))
648
+ if (!term.isValueResolvable()) {
530
649
  return this;
650
+ }
651
+ const value = term.getValue();
531
652
  this.value = value;
532
653
  this.isCompleteEvaluated = true;
533
654
  return this;
@@ -536,47 +657,86 @@ class RegoExp {
536
657
  // --- 3 terms expression e.g. true == true or x >= 3
537
658
  // --- we only evalute some redundant expression e.g. true == true or false != true
538
659
  const [operator, operands] = this.toOperatorOperandsArray();
539
- if (operands.findIndex((op) => op.isRef()) !== -1) {
540
- // --- this expression involve unknown no need to evalute
660
+ if (!operands[0].isValueResolvable() ||
661
+ !operands[1].isValueResolvable()) {
662
+ // if one of the term value is resolvable now, we can't evaluate further.
541
663
  return this;
542
664
  }
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;
665
+ const operandsValues = operands.map((op) => op.getValue());
666
+ if (operandsValues.findIndex((v) => typeof v === "undefined")) {
667
+ }
668
+ let value = null;
669
+ switch (operator) {
670
+ case "=":
671
+ value = operandsValues[0] === operandsValues[1];
672
+ break;
673
+ case ">":
674
+ value = operandsValues[0] > operandsValues[1];
675
+ break;
676
+ case "<":
677
+ value = operandsValues[0] < operandsValues[1];
678
+ break;
679
+ case ">=":
680
+ value = operandsValues[0] >= operandsValues[1];
681
+ break;
682
+ case "<=":
683
+ value = operandsValues[0] <= operandsValues[1];
684
+ break;
685
+ case "!=":
686
+ value = operandsValues[0] != operandsValues[1];
687
+ break;
688
+ default:
689
+ throw new Error(`Invalid 3 terms rego expression, Unknown operator "${operator}": ${this.termsAsString()}`);
571
690
  }
691
+ this.isCompleteEvaluated = true;
692
+ this.value = value;
693
+ return this;
572
694
  }
573
695
  else {
574
696
  throw new Error(`Invalid ${this.terms.length} terms rego expression: ${this.termsAsString()}`);
575
697
  }
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;
698
+ }
699
+ toData(index = 0) {
700
+ const terms = this.terms.map((term) => term.toData());
701
+ if (this.isNegated) {
702
+ return {
703
+ negated: true,
704
+ index,
705
+ terms
706
+ };
707
+ }
708
+ else {
709
+ return {
710
+ index,
711
+ terms
712
+ };
713
+ }
714
+ }
715
+ toJSON(index = 0) {
716
+ return JSON.stringify(this.toData(index));
717
+ }
718
+ toConciseData() {
719
+ let data;
720
+ if (this.terms.length === 1) {
721
+ const term = this.terms[0];
722
+ data = {
723
+ negated: this.isNegated,
724
+ operator: null,
725
+ operands: [term.toConciseData()]
726
+ };
727
+ }
728
+ else {
729
+ const [operator, operands] = this.toOperatorOperandsArray();
730
+ data = {
731
+ negated: this.isNegated,
732
+ operator,
733
+ operands: operands.map((item) => item.toConciseData())
734
+ };
735
+ }
736
+ return data;
737
+ }
738
+ toConciseJSON() {
739
+ return JSON.stringify(this.toConciseData());
580
740
  }
581
741
  static parseFromData(expData, parser) {
582
742
  const isNegated = expData.negated === true;
@@ -613,6 +773,15 @@ class RegoRef {
613
773
  clone() {
614
774
  return new RegoRef(this.parts.map((p) => (Object.assign({}, p))));
615
775
  }
776
+ toData() {
777
+ return {
778
+ type: "ref",
779
+ value: this.parts
780
+ };
781
+ }
782
+ toJson() {
783
+ return JSON.stringify(this.toData());
784
+ }
616
785
  static parseFromData(data) {
617
786
  if (data.type === "ref") {
618
787
  return new RegoRef(data.value);
@@ -662,10 +831,8 @@ class RegoRef {
662
831
  if (isFirstPart)
663
832
  isFirstPart = false;
664
833
  return partStr;
665
- //--- a.[_].[_] should be a[_][_]
666
834
  })
667
- .join(".")
668
- .replace(/\.\[/g, "[");
835
+ .join(".");
669
836
  return this.removeAllPrefixs(str, removalPrefixs);
670
837
  }
671
838
  refString(removalPrefixs = []) {
@@ -718,6 +885,171 @@ function value2String(value) {
718
885
  return JSON.stringify(value);
719
886
  }
720
887
  exports.value2String = value2String;
888
+ class RegoRuleSet {
889
+ constructor(parser, rules, fullName = "", name = "") {
890
+ var _a, _b;
891
+ this.fullName = "";
892
+ this.name = "";
893
+ this.rules = [];
894
+ this.defaultRule = null;
895
+ this.isCompleteEvaluated = false;
896
+ this.parser = parser;
897
+ if (rules === null || rules === void 0 ? void 0 : rules.length) {
898
+ const defaultRuleIdx = rules.findIndex((r) => r.isDefault);
899
+ if (defaultRuleIdx !== -1) {
900
+ this.defaultRule = rules[defaultRuleIdx];
901
+ }
902
+ this.rules = rules.filter((r) => !r.isDefault);
903
+ }
904
+ if (fullName) {
905
+ this.fullName = fullName;
906
+ }
907
+ else if ((_a = rules === null || rules === void 0 ? void 0 : rules[0]) === null || _a === void 0 ? void 0 : _a.fullName) {
908
+ this.fullName = rules[0].fullName;
909
+ }
910
+ if (name) {
911
+ this.name = name;
912
+ }
913
+ else if ((_b = rules === null || rules === void 0 ? void 0 : rules[0]) === null || _b === void 0 ? void 0 : _b.name) {
914
+ this.name = rules[0].name;
915
+ }
916
+ this.evaluate();
917
+ }
918
+ evaluate() {
919
+ var _a;
920
+ if (this.isCompleteEvaluated) {
921
+ return this;
922
+ }
923
+ if (!((_a = this.rules) === null || _a === void 0 ? void 0 : _a.length)) {
924
+ if (!this.defaultRule) {
925
+ this.isCompleteEvaluated = true;
926
+ this.value = undefined;
927
+ return this;
928
+ }
929
+ else {
930
+ if (this.defaultRule.isResolvable()) {
931
+ this.isCompleteEvaluated = true;
932
+ this.value = this.defaultRule.value;
933
+ return this;
934
+ }
935
+ else {
936
+ return this;
937
+ }
938
+ }
939
+ }
940
+ this.rules.forEach((r) => r.evaluate());
941
+ const matchedRule = this.rules.find((r) => r.isResolvable() && r.isMatched);
942
+ if (matchedRule) {
943
+ this.isCompleteEvaluated = true;
944
+ this.value = matchedRule.value;
945
+ return this;
946
+ }
947
+ if (this.rules.findIndex((r) => !r.isResolvable()) !== -1) {
948
+ // still has rule unresolvable
949
+ return this;
950
+ }
951
+ // rest (if any) are all unmatched rules
952
+ if (!this.defaultRule) {
953
+ this.isCompleteEvaluated = true;
954
+ this.value = undefined;
955
+ return this;
956
+ }
957
+ else {
958
+ if (this.defaultRule.isResolvable()) {
959
+ this.isCompleteEvaluated = true;
960
+ this.value = this.defaultRule.value;
961
+ return this;
962
+ }
963
+ else {
964
+ return this;
965
+ }
966
+ }
967
+ }
968
+ isResolvable() {
969
+ if (!this.isCompleteEvaluated) {
970
+ this.evaluate();
971
+ }
972
+ return this.isCompleteEvaluated;
973
+ }
974
+ getResidualRules() {
975
+ if (this.isResolvable()) {
976
+ return [];
977
+ }
978
+ let rules = this.defaultRule
979
+ ? [this.defaultRule, ...this.rules]
980
+ : [...this.rules];
981
+ rules = this.rules.filter((r) => !r.isResolvable());
982
+ if (!rules.length) {
983
+ return [];
984
+ }
985
+ rules = lodash_1.default.flatMap(rules, (rule) => {
986
+ // all resolvable expressions can all be ignored as:
987
+ // - if the expression is resolved to "matched", it won't impact the result of the rule
988
+ // - if the expression is resolved to "unmatched", the rule should be resolved to "unmatched" earlier.
989
+ const unresolvedExpressions = rule.expressions.filter((exp) => !exp.isResolvable());
990
+ if (unresolvedExpressions.length !== 1) {
991
+ return [rule];
992
+ }
993
+ // For rules with single expression, reduce the layer by replacing it with target reference rules
994
+ const exp = unresolvedExpressions[0];
995
+ if (exp.terms.length === 1) {
996
+ const fullName = exp.terms[0].fullRefString();
997
+ const ruleSet = this.parser.ruleSets[fullName];
998
+ if (!ruleSet) {
999
+ const compressedRule = rule.clone();
1000
+ compressedRule.expressions = [exp];
1001
+ return [compressedRule];
1002
+ }
1003
+ return ruleSet.getResidualRules();
1004
+ }
1005
+ else if (exp.terms.length === 3) {
1006
+ const [operator, [op1, op2]] = exp.toOperatorOperandsArray();
1007
+ if (operator != "=" && operator != "!=") {
1008
+ // For now, we will only further process the ref when operator is = or !=
1009
+ return [rule];
1010
+ }
1011
+ if (!op1.isValueResolvable() && !op2.isValueResolvable()) {
1012
+ // when both op1 & op1 are not resolvable ref, we will not attempt to process further
1013
+ return [rule];
1014
+ }
1015
+ const value = op1.isValueResolvable()
1016
+ ? op1.getValue()
1017
+ : op2.getValue();
1018
+ const refTerm = op1.isValueResolvable() ? op2 : op1;
1019
+ const fullName = refTerm.fullRefString();
1020
+ const ruleSet = this.parser.ruleSets[fullName];
1021
+ if (!ruleSet) {
1022
+ const compressedRule = rule.clone();
1023
+ compressedRule.expressions = [exp];
1024
+ return [compressedRule];
1025
+ }
1026
+ let refRules = ruleSet.getResidualRules();
1027
+ // when negated expression, reverse the operator
1028
+ const convertedOperator = exp.isNegated
1029
+ ? operator == "="
1030
+ ? "!="
1031
+ : "="
1032
+ : operator;
1033
+ if (convertedOperator == "=") {
1034
+ refRules = refRules.filter((r) => r.value == value);
1035
+ }
1036
+ else {
1037
+ refRules = refRules.filter((r) => r.value != value);
1038
+ }
1039
+ if (!refRules.length) {
1040
+ // this means this rule can never matched
1041
+ return [];
1042
+ }
1043
+ return refRules;
1044
+ }
1045
+ else {
1046
+ throw new Error(`Failed to produce residualRules for rule: ${rule.toJson()}`);
1047
+ }
1048
+ });
1049
+ return rules;
1050
+ }
1051
+ }
1052
+ exports.RegoRuleSet = RegoRuleSet;
721
1053
  /**
722
1054
  * OPA result Parser
723
1055
  *
@@ -756,6 +1088,13 @@ class OpaCompileResponseParser {
756
1088
  * @memberof OpaCompileResponseParser
757
1089
  */
758
1090
  this.rules = [];
1091
+ /**
1092
+ * Parsed, compressed & evaluated rule sets
1093
+ *
1094
+ * @type {RegoRuleSet[]}
1095
+ * @memberof OpaCompileResponseParser
1096
+ */
1097
+ this.ruleSets = {};
759
1098
  this.queries = [];
760
1099
  /**
761
1100
  * A cache of all resolved rule result
@@ -797,20 +1136,26 @@ class OpaCompileResponseParser {
797
1136
  else {
798
1137
  this.data = json;
799
1138
  }
800
- if (!this.data.result) {
1139
+ /**
1140
+ * OPA might output {"result": {}} as unconditional `false` or never matched
1141
+ */
1142
+ if (!this.data.result || !Object.keys(this.data.result).length) {
801
1143
  // --- mean no rule matched
802
1144
  this.setQueryRuleResult(false);
803
1145
  return [];
804
1146
  }
805
1147
  this.data = this.data.result;
806
- if ((!this.data.queries ||
1148
+ if (!this.data.queries ||
807
1149
  !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
1150
+ !this.data.queries.length) {
811
1151
  this.setQueryRuleResult(false);
812
1152
  return [];
813
1153
  }
1154
+ if (this.data.queries.findIndex((q) => !(q === null || q === void 0 ? void 0 : q.length)) !== -1) {
1155
+ // when query is always true, the "queries" value in the result will contain an empty array
1156
+ this.setQueryRuleResult(true);
1157
+ return [];
1158
+ }
814
1159
  const queries = this.data.queries;
815
1160
  if (queries) {
816
1161
  if (queries.findIndex((ruleBody) => !ruleBody || !ruleBody.length) !== -1) {
@@ -828,7 +1173,6 @@ class OpaCompileResponseParser {
828
1173
  value: true,
829
1174
  parser: this
830
1175
  });
831
- rule.evaluate();
832
1176
  this.originalRules.push(rule);
833
1177
  this.rules.push(rule);
834
1178
  });
@@ -845,84 +1189,48 @@ class OpaCompileResponseParser {
845
1189
  rules.forEach((r) => {
846
1190
  const regoRule = RegoRule.parseFromData(r, packageName, this);
847
1191
  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
- }
1192
+ this.rules.push(regoRule);
857
1193
  });
858
1194
  });
859
1195
  }
860
- this.calculateCompleteRuleResult();
861
- this.reduceDependencies();
1196
+ 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)));
1197
+ this.resolveAllRuleSets();
862
1198
  return this.rules;
863
1199
  }
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
- }
1200
+ isRefResolvable(fullName) {
1201
+ if (this.completeRuleResults[fullName]) {
1202
+ return true;
1203
+ }
1204
+ const ruleSet = this.ruleSets[fullName];
1205
+ if (!ruleSet) {
1206
+ return false;
1207
+ }
1208
+ return ruleSet.isResolvable();
1209
+ }
1210
+ getRefValue(fullName) {
1211
+ const completeResult = this.completeRuleResults[fullName];
1212
+ if (completeResult) {
1213
+ return completeResult.value;
1214
+ }
1215
+ const ruleSet = this.ruleSets[fullName];
1216
+ if (!ruleSet || !ruleSet.isResolvable()) {
1217
+ return undefined;
1218
+ }
1219
+ return ruleSet.value;
1220
+ }
1221
+ resolveAllRuleSets() {
1222
+ while (true) {
1223
+ const unresolvedSetsNum = Object.values(this.ruleSets).filter((rs) => !rs.isResolvable()).length;
1224
+ if (!unresolvedSetsNum) {
1225
+ break;
899
1226
  }
900
- else {
901
- // --- do nothing
902
- // --- Some defaultRules might be able to strip out once
903
- // --- nonCompleteRules are determined later
904
- return;
1227
+ Object.values(this.ruleSets).forEach((rs) => rs.evaluate());
1228
+ const newUnresolvedSetsNum = Object.values(this.ruleSets).filter((rs) => !rs.isResolvable()).length;
1229
+ if (!newUnresolvedSetsNum ||
1230
+ newUnresolvedSetsNum >= unresolvedSetsNum) {
1231
+ break;
905
1232
  }
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
- }
924
- // --- unmatched non-default rule can be stripped out
925
- this.rules = this.rules.filter((r) => !(r.isCompleteEvaluated && !r.isMatched && !r.isDefault));
1233
+ }
926
1234
  }
927
1235
  /**
928
1236
  * Call to evaluate a rule
@@ -932,136 +1240,28 @@ class OpaCompileResponseParser {
932
1240
  * @memberof OpaCompileResponseParser
933
1241
  */
934
1242
  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];
1243
+ if (this.completeRuleResults[fullName]) {
1244
+ return this.completeRuleResults[fullName];
939
1245
  }
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)
1246
+ const ruleSet = this.ruleSets[fullName];
1247
+ if (!ruleSet) {
944
1248
  return null;
945
1249
  }
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) {
1250
+ if (ruleSet.isResolvable()) {
955
1251
  return {
956
1252
  fullName,
957
- name: defaultRule ? defaultRule.name : "",
958
- value: defaultValue,
959
- isCompleteEvaluated: true,
960
- residualRules: []
1253
+ name: ruleSet.name,
1254
+ value: ruleSet.value,
1255
+ isCompleteEvaluated: true
961
1256
  };
962
1257
  }
963
1258
  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
1259
  return {
1060
1260
  fullName,
1061
- name: rules[0].name,
1261
+ name: ruleSet.name,
1062
1262
  value: undefined,
1063
1263
  isCompleteEvaluated: false,
1064
- residualRules: rules
1264
+ residualRules: ruleSet.getResidualRules()
1065
1265
  };
1066
1266
  }
1067
1267
  }
@@ -1086,6 +1286,9 @@ class OpaCompileResponseParser {
1086
1286
  if (result === null)
1087
1287
  return "null";
1088
1288
  if (result.isCompleteEvaluated) {
1289
+ if (typeof result.value === "undefined") {
1290
+ return "undefined";
1291
+ }
1089
1292
  return value2String(result.value);
1090
1293
  }
1091
1294
  let parts = result.residualRules.map((r) => r.toHumanReadableString());
@@ -1103,23 +1306,6 @@ class OpaCompileResponseParser {
1103
1306
  evaluateAsHumanReadableString() {
1104
1307
  return this.evaluateRuleAsHumanReadableString(this.pseudoQueryRuleName);
1105
1308
  }
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
1309
  reportWarns(msg) {
1124
1310
  this.warns.push(msg);
1125
1311
  this.hasWarns = true;