condition-evaluator-internal.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. /*! firebase-admin v12.1.1 */
  2. /*!
  3. * Copyright 2024 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. 'use strict';
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.ConditionEvaluator = void 0;
  20. const remote_config_api_1 = require("./remote-config-api");
  21. const farmhash = require("farmhash");
  22. const long = require("long");
  23. /**
  24. * Encapsulates condition evaluation logic to simplify organization and
  25. * facilitate testing.
  26. *
  27. * @internal
  28. */
  29. class ConditionEvaluator {
  30. evaluateConditions(namedConditions, context) {
  31. // The order of the conditions is significant.
  32. // A JS Map preserves the order of insertion ("Iteration happens in insertion order"
  33. // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#description).
  34. const evaluatedConditions = new Map();
  35. for (const namedCondition of namedConditions) {
  36. evaluatedConditions.set(namedCondition.name, this.evaluateCondition(namedCondition.condition, context));
  37. }
  38. return evaluatedConditions;
  39. }
  40. evaluateCondition(condition, context, nestingLevel = 0) {
  41. if (nestingLevel >= ConditionEvaluator.MAX_CONDITION_RECURSION_DEPTH) {
  42. // TODO: add logging once we have a wrapped logger.
  43. return false;
  44. }
  45. if (condition.orCondition) {
  46. return this.evaluateOrCondition(condition.orCondition, context, nestingLevel + 1);
  47. }
  48. if (condition.andCondition) {
  49. return this.evaluateAndCondition(condition.andCondition, context, nestingLevel + 1);
  50. }
  51. if (condition.true) {
  52. return true;
  53. }
  54. if (condition.false) {
  55. return false;
  56. }
  57. if (condition.percent) {
  58. return this.evaluatePercentCondition(condition.percent, context);
  59. }
  60. // TODO: add logging once we have a wrapped logger.
  61. return false;
  62. }
  63. evaluateOrCondition(orCondition, context, nestingLevel) {
  64. const subConditions = orCondition.conditions || [];
  65. for (const subCondition of subConditions) {
  66. // Recursive call.
  67. const result = this.evaluateCondition(subCondition, context, nestingLevel + 1);
  68. // Short-circuit the evaluation result for true.
  69. if (result) {
  70. return result;
  71. }
  72. }
  73. return false;
  74. }
  75. evaluateAndCondition(andCondition, context, nestingLevel) {
  76. const subConditions = andCondition.conditions || [];
  77. for (const subCondition of subConditions) {
  78. // Recursive call.
  79. const result = this.evaluateCondition(subCondition, context, nestingLevel + 1);
  80. // Short-circuit the evaluation result for false.
  81. if (!result) {
  82. return result;
  83. }
  84. }
  85. return true;
  86. }
  87. evaluatePercentCondition(percentCondition, context) {
  88. if (!context.randomizationId) {
  89. // TODO: add logging once we have a wrapped logger.
  90. return false;
  91. }
  92. // This is the entry point for processing percent condition data from the response.
  93. // We're not using a proto library, so we can't assume undefined fields have
  94. // default values.
  95. const { seed, percentOperator, microPercent, microPercentRange } = percentCondition;
  96. if (!percentOperator) {
  97. // TODO: add logging once we have a wrapped logger.
  98. return false;
  99. }
  100. const normalizedMicroPercent = microPercent || 0;
  101. const normalizedMicroPercentUpperBound = microPercentRange?.microPercentUpperBound || 0;
  102. const normalizedMicroPercentLowerBound = microPercentRange?.microPercentLowerBound || 0;
  103. const seedPrefix = seed && seed.length > 0 ? `${seed}.` : '';
  104. const stringToHash = `${seedPrefix}${context.randomizationId}`;
  105. // Using a 64-bit long for consistency with the Remote Config fetch endpoint.
  106. let hash64 = long.fromString(farmhash.fingerprint64(stringToHash));
  107. // Negate the hash if its value is less than 0. We handle this manually because the
  108. // Long library doesn't provided an absolute value method.
  109. if (hash64.lt(0)) {
  110. hash64 = hash64.negate();
  111. }
  112. const instanceMicroPercentile = hash64.mod(100 * 1000000);
  113. switch (percentOperator) {
  114. case remote_config_api_1.PercentConditionOperator.LESS_OR_EQUAL:
  115. return instanceMicroPercentile.lte(normalizedMicroPercent);
  116. case remote_config_api_1.PercentConditionOperator.GREATER_THAN:
  117. return instanceMicroPercentile.gt(normalizedMicroPercent);
  118. case remote_config_api_1.PercentConditionOperator.BETWEEN:
  119. return instanceMicroPercentile.gt(normalizedMicroPercentLowerBound)
  120. && instanceMicroPercentile.lte(normalizedMicroPercentUpperBound);
  121. case remote_config_api_1.PercentConditionOperator.UNKNOWN:
  122. default:
  123. break;
  124. }
  125. // TODO: add logging once we have a wrapped logger.
  126. return false;
  127. }
  128. }
  129. exports.ConditionEvaluator = ConditionEvaluator;
  130. ConditionEvaluator.MAX_CONDITION_RECURSION_DEPTH = 10;