initial commit
This commit is contained in:
230
server/node_modules/firebase-admin/lib/remote-config/condition-evaluator-internal.js
generated
vendored
Normal file
230
server/node_modules/firebase-admin/lib/remote-config/condition-evaluator-internal.js
generated
vendored
Normal file
@@ -0,0 +1,230 @@
|
||||
/*! firebase-admin v13.5.0 */
|
||||
/*!
|
||||
* Copyright 2024 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ConditionEvaluator = void 0;
|
||||
const remote_config_api_1 = require("./remote-config-api");
|
||||
const crypto_1 = require("crypto");
|
||||
/**
|
||||
* Encapsulates condition evaluation logic to simplify organization and
|
||||
* facilitate testing.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ConditionEvaluator {
|
||||
evaluateConditions(namedConditions, context) {
|
||||
// The order of the conditions is significant.
|
||||
// A JS Map preserves the order of insertion ("Iteration happens in insertion order"
|
||||
// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#description).
|
||||
const evaluatedConditions = new Map();
|
||||
for (const namedCondition of namedConditions) {
|
||||
evaluatedConditions.set(namedCondition.name, this.evaluateCondition(namedCondition.condition, context));
|
||||
}
|
||||
return evaluatedConditions;
|
||||
}
|
||||
evaluateCondition(condition, context, nestingLevel = 0) {
|
||||
if (nestingLevel >= ConditionEvaluator.MAX_CONDITION_RECURSION_DEPTH) {
|
||||
// TODO: add logging once we have a wrapped logger.
|
||||
return false;
|
||||
}
|
||||
if (condition.orCondition) {
|
||||
return this.evaluateOrCondition(condition.orCondition, context, nestingLevel + 1);
|
||||
}
|
||||
if (condition.andCondition) {
|
||||
return this.evaluateAndCondition(condition.andCondition, context, nestingLevel + 1);
|
||||
}
|
||||
if (condition.true) {
|
||||
return true;
|
||||
}
|
||||
if (condition.false) {
|
||||
return false;
|
||||
}
|
||||
if (condition.percent) {
|
||||
return this.evaluatePercentCondition(condition.percent, context);
|
||||
}
|
||||
if (condition.customSignal) {
|
||||
return this.evaluateCustomSignalCondition(condition.customSignal, context);
|
||||
}
|
||||
// TODO: add logging once we have a wrapped logger.
|
||||
return false;
|
||||
}
|
||||
evaluateOrCondition(orCondition, context, nestingLevel) {
|
||||
const subConditions = orCondition.conditions || [];
|
||||
for (const subCondition of subConditions) {
|
||||
// Recursive call.
|
||||
const result = this.evaluateCondition(subCondition, context, nestingLevel + 1);
|
||||
// Short-circuit the evaluation result for true.
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
evaluateAndCondition(andCondition, context, nestingLevel) {
|
||||
const subConditions = andCondition.conditions || [];
|
||||
for (const subCondition of subConditions) {
|
||||
// Recursive call.
|
||||
const result = this.evaluateCondition(subCondition, context, nestingLevel + 1);
|
||||
// Short-circuit the evaluation result for false.
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
evaluatePercentCondition(percentCondition, context) {
|
||||
if (!context.randomizationId) {
|
||||
// TODO: add logging once we have a wrapped logger.
|
||||
return false;
|
||||
}
|
||||
// This is the entry point for processing percent condition data from the response.
|
||||
// We're not using a proto library, so we can't assume undefined fields have
|
||||
// default values.
|
||||
const { seed, percentOperator, microPercent, microPercentRange } = percentCondition;
|
||||
if (!percentOperator) {
|
||||
// TODO: add logging once we have a wrapped logger.
|
||||
return false;
|
||||
}
|
||||
const normalizedMicroPercent = microPercent || 0;
|
||||
const normalizedMicroPercentUpperBound = microPercentRange?.microPercentUpperBound || 0;
|
||||
const normalizedMicroPercentLowerBound = microPercentRange?.microPercentLowerBound || 0;
|
||||
const seedPrefix = seed && seed.length > 0 ? `${seed}.` : '';
|
||||
const stringToHash = `${seedPrefix}${context.randomizationId}`;
|
||||
const hash = ConditionEvaluator.hashSeededRandomizationId(stringToHash);
|
||||
const instanceMicroPercentile = hash % BigInt(100 * 1000000);
|
||||
switch (percentOperator) {
|
||||
case remote_config_api_1.PercentConditionOperator.LESS_OR_EQUAL:
|
||||
return instanceMicroPercentile <= normalizedMicroPercent;
|
||||
case remote_config_api_1.PercentConditionOperator.GREATER_THAN:
|
||||
return instanceMicroPercentile > normalizedMicroPercent;
|
||||
case remote_config_api_1.PercentConditionOperator.BETWEEN:
|
||||
return instanceMicroPercentile > normalizedMicroPercentLowerBound
|
||||
&& instanceMicroPercentile <= normalizedMicroPercentUpperBound;
|
||||
case remote_config_api_1.PercentConditionOperator.UNKNOWN:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// TODO: add logging once we have a wrapped logger.
|
||||
return false;
|
||||
}
|
||||
static hashSeededRandomizationId(seededRandomizationId) {
|
||||
const hex = (0, crypto_1.createHash)('sha256').update(seededRandomizationId).digest('hex');
|
||||
return BigInt(`0x${hex}`);
|
||||
}
|
||||
evaluateCustomSignalCondition(customSignalCondition, context) {
|
||||
const { customSignalOperator, customSignalKey, targetCustomSignalValues, } = customSignalCondition;
|
||||
if (!customSignalOperator || !customSignalKey || !targetCustomSignalValues) {
|
||||
// TODO: add logging once we have a wrapped logger.
|
||||
return false;
|
||||
}
|
||||
if (!targetCustomSignalValues.length) {
|
||||
return false;
|
||||
}
|
||||
// Extract the value of the signal from the evaluation context.
|
||||
const actualCustomSignalValue = context[customSignalKey];
|
||||
if (actualCustomSignalValue == undefined) {
|
||||
return false;
|
||||
}
|
||||
switch (customSignalOperator) {
|
||||
case remote_config_api_1.CustomSignalOperator.STRING_CONTAINS:
|
||||
return compareStrings(targetCustomSignalValues, actualCustomSignalValue, (target, actual) => actual.includes(target));
|
||||
case remote_config_api_1.CustomSignalOperator.STRING_DOES_NOT_CONTAIN:
|
||||
return !compareStrings(targetCustomSignalValues, actualCustomSignalValue, (target, actual) => actual.includes(target));
|
||||
case remote_config_api_1.CustomSignalOperator.STRING_EXACTLY_MATCHES:
|
||||
return compareStrings(targetCustomSignalValues, actualCustomSignalValue, (target, actual) => actual.trim() === target.trim());
|
||||
case remote_config_api_1.CustomSignalOperator.STRING_CONTAINS_REGEX:
|
||||
return compareStrings(targetCustomSignalValues, actualCustomSignalValue, (target, actual) => new RegExp(target).test(actual));
|
||||
// For numeric operators only one target value is allowed.
|
||||
case remote_config_api_1.CustomSignalOperator.NUMERIC_LESS_THAN:
|
||||
return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r < 0);
|
||||
case remote_config_api_1.CustomSignalOperator.NUMERIC_LESS_EQUAL:
|
||||
return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r <= 0);
|
||||
case remote_config_api_1.CustomSignalOperator.NUMERIC_EQUAL:
|
||||
return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r === 0);
|
||||
case remote_config_api_1.CustomSignalOperator.NUMERIC_NOT_EQUAL:
|
||||
return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r !== 0);
|
||||
case remote_config_api_1.CustomSignalOperator.NUMERIC_GREATER_THAN:
|
||||
return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r > 0);
|
||||
case remote_config_api_1.CustomSignalOperator.NUMERIC_GREATER_EQUAL:
|
||||
return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r >= 0);
|
||||
// For semantic operators only one target value is allowed.
|
||||
case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN:
|
||||
return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r < 0);
|
||||
case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL:
|
||||
return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r <= 0);
|
||||
case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_EQUAL:
|
||||
return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r === 0);
|
||||
case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL:
|
||||
return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r !== 0);
|
||||
case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN:
|
||||
return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r > 0);
|
||||
case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL:
|
||||
return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r >= 0);
|
||||
}
|
||||
// TODO: add logging once we have a wrapped logger.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
exports.ConditionEvaluator = ConditionEvaluator;
|
||||
ConditionEvaluator.MAX_CONDITION_RECURSION_DEPTH = 10;
|
||||
// Compares the actual string value of a signal against a list of target
|
||||
// values. If any of the target values are a match, returns true.
|
||||
function compareStrings(targetValues, actualValue, predicateFn) {
|
||||
const actual = String(actualValue);
|
||||
return targetValues.some((target) => predicateFn(target, actual));
|
||||
}
|
||||
// Compares two numbers against each other.
|
||||
// Calls the predicate function with -1, 0, 1 if actual is less than, equal to, or greater than target.
|
||||
function compareNumbers(actualValue, targetValue, predicateFn) {
|
||||
const target = Number(targetValue);
|
||||
const actual = Number(actualValue);
|
||||
if (isNaN(target) || isNaN(actual)) {
|
||||
return false;
|
||||
}
|
||||
return predicateFn(actual < target ? -1 : actual > target ? 1 : 0);
|
||||
}
|
||||
// Max number of segments a numeric version can have. This is enforced by the server as well.
|
||||
const MAX_LENGTH = 5;
|
||||
// Compares semantic version strings against each other.
|
||||
// Calls the predicate function with -1, 0, 1 if actual is less than, equal to, or greater than target.
|
||||
function compareSemanticVersions(actualValue, targetValue, predicateFn) {
|
||||
const version1 = String(actualValue).split('.').map(Number);
|
||||
const version2 = targetValue.split('.').map(Number);
|
||||
if (version1.length > MAX_LENGTH || version2.length > MAX_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < MAX_LENGTH; i++) {
|
||||
// Check to see if segments are present. Note that these may be present and be NaN.
|
||||
const version1HasSegment = version1[i] !== undefined;
|
||||
const version2HasSegment = version2[i] !== undefined;
|
||||
// Insert zeros if undefined for easier comparison.
|
||||
if (!version1HasSegment)
|
||||
version1[i] = 0;
|
||||
if (!version2HasSegment)
|
||||
version2[i] = 0;
|
||||
// At this point, if either segment is NaN, we return false directly.
|
||||
if (isNaN(version1[i]) || isNaN(version2[i]))
|
||||
return false;
|
||||
// Check if we have a difference in segments. Otherwise continue to next segment.
|
||||
if (version1[i] < version2[i])
|
||||
return predicateFn(-1);
|
||||
if (version1[i] > version2[i])
|
||||
return predicateFn(1);
|
||||
}
|
||||
// If this point is reached, the semantic versions are equal.
|
||||
return predicateFn(0);
|
||||
}
|
||||
Reference in New Issue
Block a user