UNPKG

hypertune

Version:

[Hypertune](https://www.hypertune.com/) is the most flexible platform for feature flags, A/B testing, analytics and app configuration. Built with full end-to-end type-safety, Git-style version control and local, synchronous, in-memory flag evaluation. Opt

843 lines 62.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = reduce; exports.areEqual = areEqual; /* eslint-disable capitalized-comments */ const constants_1 = require("../constants"); const emptyThrows_1 = __importDefault(require("./emptyThrows")); const evaluate_1 = __importStar(require("./evaluate")); const expression_1 = require("./expression"); const hash_1 = __importDefault(require("./hash")); const nullThrows_1 = __importDefault(require("./nullThrows")); const reductionLogs_1 = require("./reductionLogs"); const stableStringify_1 = __importDefault(require("./stableStringify")); const types_1 = require("../types"); const asError_1 = __importDefault(require("./asError")); const getNestedValue_1 = __importDefault(require("./getNestedValue")); const asPlainObjectOrThrow_1 = __importDefault(require("./asPlainObjectOrThrow")); const getInlineFragment_1 = __importDefault(require("./getInlineFragment")); /** * Summary * * Expression reduction takes a query and/or some arguments and applies * interpreter steps to reduce the expression as much as possible. It is * analogous to beta-reduction in the lambda calculus. If all function arguments * are supplied, we return a fully reduced expression in "normal form". Normal * form expressions can then be converted into JSON. Otherwise, we return a * partially reduced expression in "complex form" which requires further * reduction with additional arguments for the function expressions it contains. * * Each time we reduce an expression into a simpler form, we keep track of the * expressions we've needed to "evaluate" to compute the reduction. We track * this via "logs" which we attach to the result expression. This lets users * visualize how often different parts of the expression tree have been used. * * Partial Field Arguments * * When a query field has a partial field arguments object, we reduce the * expression tree as much as possible given the available fields within this * object and leave the function expression that references it intact so that on * a subsequent reduction, when the full field arguments object can be provided, * we can fully reduce the tree correctly. There are 3 changes to the main * reduction logic that allow for this: * * 1. We leave function expressions intact if any of the arguments passed to it * is a partial object. * * 2. We have a reduction flag that toggles partial object variable reduction. * If it's off (which it is by default), we do not reduce variable expressions * that reference partial objects. * * 3. The only time we enable partial object variable reduction is in the * (initial) object reduction in a get field expression when we check to see if * we can access the desired field. (And if we can't, we return the get field * expression with its object reduced but with partial object variable reduction * disabled.) * * Complexity and Logic * * For any expression, there are a finite number of reduction steps until it * cannot be reduced further. The reduction logic of each expression type * attempts to reduce the expression and its children as much as possible given * the current context. The logic for application and variable expressions is * especially prone to complexity blow-up. In particular, an application * argument should be reduced as much as possible before being passed to a * function expression reduction, where it becomes a variable, so that the * reduction isn't repeated for every variable expression in the function body * that references it. */ // TODO: Refactor using fold with pass down? // eslint-disable-next-line max-params function reduce(splits, commitConfig, query, variableValues, rootExpression, allowMissingVariables) { var _a, _b; const innerQuery = getInnerQuery(query, variableValues, allowMissingVariables); return innerReduce(splits, commitConfig, createAssignmentCache(), /* enablePartialObjectVariableReduction */ false, (_a = innerQuery === null || innerQuery === void 0 ? void 0 : innerQuery.fragmentDefinitions) !== null && _a !== void 0 ? _a : {}, (_b = innerQuery === null || innerQuery === void 0 ? void 0 : innerQuery.fieldQuery) !== null && _b !== void 0 ? _b : null, /* args */ null, /* variables */ {}, rootExpression); } function createAssignmentCache() { const cache = new Map(); return { get: (splitId, unitId) => { var _a; return (_a = cache.get(getAssignmentCacheKey(splitId, unitId))) !== null && _a !== void 0 ? _a : null; }, set: (splitId, unitId, value) => { cache.set(getAssignmentCacheKey(splitId, unitId), value); }, }; } function getAssignmentCacheKey(splitId, unitId) { return (0, stableStringify_1.default)({ splitId, unitId }); } function getInnerQuery(query, variableValues, allowMissingVariables) { return query ? Object.assign(Object.assign({}, query), { fragmentDefinitions: Object.fromEntries(Object.entries(query.fragmentDefinitions).map(([fragmentName, fragment]) => [ fragmentName, getInnerInlineFragment(query.variableDefinitions, fragment, variableValues, allowMissingVariables), ])), fieldQuery: getInnerFieldQuery(query.variableDefinitions, query.fieldQuery, variableValues, allowMissingVariables) }) : null; } function getInnerFieldQuery(variableDefinitions, fieldQuery, variableValues, allowMissingVariables) { return Object.fromEntries(Object.entries(fieldQuery).map(([objectTypeName, fragment]) => [ objectTypeName, fragment.type === "InlineFragment" ? getInnerInlineFragment(variableDefinitions, fragment, variableValues, allowMissingVariables) : fragment, ])); } function getInnerInlineFragment(variableDefinitions, fragment, variableValues, allowMissingVariables) { return { type: "InlineFragment", objectTypeName: fragment.objectTypeName, selection: Object.fromEntries(Object.entries(fragment.selection).map(([fieldName, { fieldArguments, fieldQuery }]) => [ fieldName, { fieldArguments: (0, expression_1.getAnonymousObjectExpression)(replaceVariables(variableDefinitions, fieldArguments, variableValues, allowMissingVariables), /* isTransient */ true), fieldQuery: fieldQuery ? getInnerFieldQuery(variableDefinitions, fieldQuery, variableValues, allowMissingVariables) : null, }, ])), }; } function replaceVariables(variableDefinitions, value, variableValues, allowMissingVariables) { var _a; switch (typeof value) { case "string": case "boolean": case "number": case "bigint": return value; case "object": { if ((0, types_1.isQueryVariable)(value)) { const variableName = value.name; if (!(variableName in variableDefinitions)) { throw new Error(`Unknown query variable ${variableName}`); } const { defaultValue } = variableDefinitions[variableName]; if (!(variableName in variableValues) && !defaultValue && !allowMissingVariables) { throw new Error(`Missing query variable ${variableName}`); } // TODO: this will actually return undefined when the variable is not present // This is expected behavior when running codegen, as we allowMissingVariables // The undefined is handled by filtering it out in the ObjectValueWithVariables case // However, it makes the types of this function a bit of a lie, which would be nice to fix. return (_a = variableValues[variableName]) !== null && _a !== void 0 ? _a : defaultValue; } if (Array.isArray(value)) { return value.map((v) => replaceVariables(variableDefinitions, v, variableValues, allowMissingVariables)); } return Object.fromEntries(Object.entries(value) .map(([fieldName, fieldValue]) => [ fieldName, replaceVariables(variableDefinitions, fieldValue, variableValues, allowMissingVariables), ]) .filter(([, fieldValue]) => fieldValue !== undefined)); } default: { throw new Error(`Unexpected value type: ${typeof value}`); } } } // eslint-disable-next-line max-params function innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, innerQuery, args, // Reduced expressions (given a null query) // Reduced expressions (given a null query) variables, expression) { if (!!args && expression.type !== "FunctionExpression") { throw new Error(`Reduction arguments (${JSON.stringify(args)}) given for an expression that isn't a function (${JSON.stringify(expression.type)}).`); } const thisLogs = (0, reductionLogs_1.mergeLogs)(expression.logs, (0, reductionLogs_1.getExpressionEvaluationCountLogs)(expression)); switch (expression.type) { case "NoOpExpression": case "BooleanExpression": case "IntExpression": case "FloatExpression": case "StringExpression": case "RegexExpression": case "EnumExpression": return expression; /** * If there's no query, return the whole object expression with all fields * reduced (with no query or arguments). * * Else return the object expression with only the query fields included. * Pre-reduce query fields without query field arguments first. Then if * the result is a function expression, reduce them again with query field * arguments. */ case "ObjectExpression": { if (!innerQuery) { return Object.assign(Object.assign({}, expression), { fields: Object.fromEntries(Object.keys(expression.fields).map((fieldName) => [ fieldName, innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.fields[fieldName], `Object expression has null field "${fieldName}".`)), ])) }); } const fragment = innerQuery[expression.objectTypeName]; const selection = fragment ? (0, getInlineFragment_1.default)(fragmentDefinitions, fragment).selection : {}; const fields = Object.fromEntries(Object.keys(selection).flatMap((fieldName) => { const unreducedField = expression.fields[fieldName]; if (!unreducedField) { return []; } const field = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, selection[fieldName].fieldQuery, null, // args variables, unreducedField); return [ [ fieldName, field.type === "FunctionExpression" ? innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, selection[fieldName].fieldQuery, [selection[fieldName].fieldArguments], variables, field) : field, ], ]; })); return Object.assign(Object.assign({}, expression), { fields }); } /** * Reduce the object without any query or arguments with partial object * variable reduction enabled (where we reduce partial object variable * expressions to the partial object expressions they reference in the * variables map). Then check if we can get the field we want. If we can, * return it. * * Else return the whole get field expression with the object reduced but * this time with partial object variable reduction disabled. So we reduce * what we can in the object while leaving partial object variable * references intact for subsequent reductions which may pass different * partial objects for these variables (with different fields present). */ case "GetFieldExpression": { const object1 = innerReduce(splits, commitConfig, assignmentCache, true, // enablePartialObjectVariableReduction fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.object, "Get field expression has null object.")); const field = getField(object1, (0, nullThrows_1.default)(expression.fieldPath, "Get field expression has null field path.")); if (field) { const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, object1.logs, field.logs); return Object.assign(Object.assign({}, field), { logs }); } const object2 = innerReduce(splits, commitConfig, assignmentCache, false, // enablePartialObjectVariableReduction fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.object, "Get field expression has null object.")); return Object.assign(Object.assign({}, expression), { object: object2 }); } /** * Reduce the object with the current query. If the result is not an * object expression, return the whole update object expression with the * reduced object. * * Else return the object expression but with its fields updated with the * field update expressions. Only apply a field update if there's no query * or its field is in the query. Pre-reduce field updates without query * field arguments first. Then if we have a query and the result is a * function expression, reduce them again with query field arguments. */ case "UpdateObjectExpression": { const object = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, innerQuery, null, // args variables, (0, nullThrows_1.default)(expression.object, "Update object expression has null object.")); if (object.type !== "ObjectExpression") { return Object.assign(Object.assign({}, expression), { object }); } const { objectTypeName } = object; const fragment = innerQuery ? innerQuery[objectTypeName] : null; const selection = !fragment ? null : (0, getInlineFragment_1.default)(fragmentDefinitions, fragment).selection; Object.keys(expression.updates).forEach((fieldName) => { if (!selection || selection[fieldName]) { if (!object.fields[fieldName]) { throw new Error("Update object expression has object with missing update " + `field "${fieldName}".`); } const update = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, selection ? selection[fieldName].fieldQuery : null, null, // args variables, (0, nullThrows_1.default)(expression.updates[fieldName], `Update object expression has null update "${fieldName}".`)); object.fields[fieldName] = selection && update.type === "FunctionExpression" ? innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, selection[fieldName].fieldQuery, [selection[fieldName].fieldArguments], variables, update) : update; } }); const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, object.logs); return Object.assign(Object.assign({}, object), { logs }); } /** * Return the whole list expression with all items reduced with the * current query (but no arguments). */ case "ListExpression": return Object.assign(Object.assign({}, expression), { items: expression.items.map((item, index) => innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, innerQuery, null, // args variables, (0, nullThrows_1.default)(item, `List expression has null item at index ${index}.`))) }); /** * Reduce the control, each case 'when' and 'then', and the default. The * case 'then' and default expressions get reduced with the current query * as they are the possible return values and so on the query path. The * control and case 'when' expressions are not reduced with the current * query as they are not possible return values and so off the query path. * * We try to evaluate the control and then each case 'when'. As soon as * a 'when' value matches the control value, we return the corresponding * 'then' expression. We attach the merged logs of the switch expression, * the control and each case 'when' that was evaluated, to the returned * 'then' expression. * * If none of the 'when' values matched the control value, we return the * default expression. * * If evaluation fails at any point due to expressions being in complex * form, i.e. not fully reduced, we return the whole switch expression but * with the reduced control, case 'when's and 'then's, and default. */ case "SwitchExpression": { const control = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.control, "Switch expression has null control.")); const cases = expression.cases.map((item, index) => ({ id: item.id, when: innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(item.when, `Switch expression case has null 'when' at index ${index}.`)), then: innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, innerQuery, null, // args variables, (0, nullThrows_1.default)(item.then, `Switch expression case has null 'then' at index ${index}.`)), })); const defaultExpression = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, innerQuery, null, // args variables, (0, nullThrows_1.default)(expression.default, "Switch expression has null default.")); try { const { value: controlValue, logs: controlLogs } = (0, evaluate_1.default)(control); let logs = (0, reductionLogs_1.mergeLogs)(thisLogs, controlLogs); for (const { when, then } of cases) { const { value: whenValue, logs: whenLogs } = (0, evaluate_1.default)(when); logs = (0, reductionLogs_1.mergeLogs)(logs, whenLogs); if (areEqual(controlValue, whenValue)) { logs = (0, reductionLogs_1.mergeLogs)(logs, then.logs); return Object.assign(Object.assign({}, then), { logs }); } } logs = (0, reductionLogs_1.mergeLogs)(logs, defaultExpression.logs); return Object.assign(Object.assign({}, defaultExpression), { logs }); } catch (error) { const { message } = (0, asError_1.default)(error); if (message !== evaluate_1.complexFormExpressionEvaluationError) { throw error; } } return Object.assign(Object.assign({}, expression), { control, cases, default: defaultExpression }); } /** * Similar to switch expression. */ case "EnumSwitchExpression": { const control = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.control, "Enum switch expression has null control.")); const cases = Object.fromEntries(Object.keys(expression.cases).map((enumValue) => [ enumValue, innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, innerQuery, null, // args variables, (0, nullThrows_1.default)(expression.cases[enumValue], `Enum switch expression has null case "${enumValue}".`)), ])); try { const { value: controlValue, logs: controlLogs } = (0, evaluate_1.default)(control); if (typeof controlValue !== "string") { throw new Error("Evaluated control of enum switch expression is not a string."); } const then = cases[controlValue]; if (!then) { throw new Error(`Evaluated control of enum switch expression "${controlValue}" is missing in cases.`); } const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, controlLogs, then.logs); return Object.assign(Object.assign({}, then), { logs }); } catch (error) { const { message } = (0, asError_1.default)(error); if (message !== evaluate_1.complexFormExpressionEvaluationError) { throw error; } } return Object.assign(Object.assign({}, expression), { control, cases }); } /** * Reduce a and b. Evaluate them both. Compare their values. Return a new * "transient" result expression that represents the comparison result. * Attach the merged logs of the comparison expression, a and b, to the * result expression logs. If evaluation fails, return the whole * comparison expression with its reduced subexpressions. */ case "ComparisonExpression": { const a = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.a, "Comparison expression has null a.")); const b = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.b, "Comparison expression has null b.")); try { const { value: aValue, logs: aLogs } = (0, evaluate_1.default)(a); const { value: bValue, logs: bLogs } = (0, evaluate_1.default)(b); const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, aLogs, bLogs); const operator = (0, nullThrows_1.default)(expression.operator, "Comparison expression has null operator."); switch (operator) { case "==": case "!=": { const result = areEqual(aValue, bValue); return getComparisonResultExpression(operator === "==" ? result : !result, logs); } case "<": { if (typeof aValue !== "number" || typeof bValue !== "number") { throw new Error("Evaluated operands of comparison expression with '<' " + "operator are not both numbers."); } return getComparisonResultExpression(aValue < bValue, logs); } case "<=": { if (typeof aValue !== "number" || typeof bValue !== "number") { throw new Error("Evaluated operands of comparison expression with '<=' " + "operator are not both numbers."); } return getComparisonResultExpression(aValue <= bValue, logs); } case ">": { if (typeof aValue !== "number" || typeof bValue !== "number") { throw new Error("Evaluated operands of comparison expression with '>' " + "operator are not both numbers."); } return getComparisonResultExpression(aValue > bValue, logs); } case ">=": { if (typeof aValue !== "number" || typeof bValue !== "number") { throw new Error("Evaluated operands of comparison expression with '>=' " + "operator are not both numbers."); } return getComparisonResultExpression(aValue >= bValue, logs); } case "AND": { if (typeof aValue !== "boolean" || typeof bValue !== "boolean") { throw new Error("Evaluated operands of comparison expression with 'AND' " + "operator are not both booleans."); } return getComparisonResultExpression(aValue && bValue, logs); } case "OR": { if (typeof aValue !== "boolean" || typeof bValue !== "boolean") { throw new Error("Evaluated operands of comparison expression with 'OR' " + "operator are not both booleans."); } return getComparisonResultExpression(aValue || bValue, logs); } case "in": case "notIn": { if (!Array.isArray(bValue)) { throw new Error(`Second evaluated operand of comparison expression with " + "'${operator}' operator is not an array.`); } const result = bValue.some((itemValue) => areEqual(aValue, itemValue)); return getComparisonResultExpression(operator === "in" ? result : !result, logs); } case "startsWith": case "notStartsWith": { const shouldInvert = operator === "notStartsWith"; if (typeof aValue === "string" && typeof bValue === "string") { const result = aValue.startsWith(bValue); return getComparisonResultExpression(shouldInvert ? !result : result, logs); } if (Array.isArray(aValue)) { const result = aValue.length === 0 ? false : areEqual(aValue[0], bValue); return getComparisonResultExpression(shouldInvert ? !result : result, logs); } throw new Error(`Evaluated operands of comparison expression with '${operator}'` + "operator are not both strings or an array and an element."); } case "endsWith": case "notEndsWith": { const shouldInvert = operator === "notEndsWith"; if (typeof aValue === "string" && typeof bValue === "string") { const result = aValue.endsWith(bValue); return getComparisonResultExpression(shouldInvert ? !result : result, logs); } if (Array.isArray(aValue)) { const result = aValue.length === 0 ? false : areEqual(aValue[aValue.length - 1], bValue); return getComparisonResultExpression(shouldInvert ? !result : result, logs); } throw new Error(`Evaluated operands of comparison expression with '${operator}'` + "operator are not both strings or an array and an element."); } case "contains": case "notContains": { const shouldInvert = operator === "notContains"; if (typeof aValue === "string" && typeof bValue === "string") { const result = aValue.includes(bValue); return getComparisonResultExpression(shouldInvert ? !result : result, logs); } if (Array.isArray(aValue)) { const result = aValue.some((itemValue) => areEqual(itemValue, bValue)); return getComparisonResultExpression(shouldInvert ? !result : result, logs); } throw new Error(`Evaluated operands of comparison expression with '${operator}'` + "operator are not both strings or an array and an element."); } case "matches": case "notMatches": { if (typeof aValue !== "string" || typeof bValue !== "string") { throw new Error(`Evaluated operands of comparison expression with " + "'${operator}' operator are not both strings.`); } const bRegex = new RegExp(bValue); const result = bRegex.test(aValue); return getComparisonResultExpression(operator === "matches" ? result : !result, logs); } default: { const neverOperator = operator; throw new Error(`unexpected operator: ${neverOperator}`); } } } catch (error) { const { message } = (0, asError_1.default)(error); if (message !== evaluate_1.complexFormExpressionEvaluationError) { throw error; } } return Object.assign(Object.assign({}, expression), { a, b }); } /** * Similar to comparison expression. */ case "ArithmeticExpression": { const a = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.a, "Arithmetic expression has null a")); const b = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.b, "Arithmetic expression has null b")); try { const { value: aValue, logs: aLogs } = (0, evaluate_1.default)(a); const { value: bValue, logs: bLogs } = (0, evaluate_1.default)(b); if (typeof aValue !== "number" || typeof bValue !== "number") { throw new Error("Evaluated operands of arithmetic expression are not both " + "numbers."); } const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, aLogs, bLogs); const operator = (0, nullThrows_1.default)(expression.operator, "Arithmetic expression has null operator."); switch (operator) { case "+": return getArithmeticResultExpression(aValue + bValue, logs); case "-": return getArithmeticResultExpression(aValue - bValue, logs); case "*": return getArithmeticResultExpression(aValue * bValue, logs); case "/": return getArithmeticResultExpression(aValue / bValue, logs); case "POW": return getArithmeticResultExpression(Math.pow(aValue, bValue), logs); case "MOD": return getArithmeticResultExpression(aValue % bValue, logs); default: { const neverOperator = operator; throw new Error(`unexpected operator: ${neverOperator}`); } } } catch (error) { const { message } = (0, asError_1.default)(error); if (message !== evaluate_1.complexFormExpressionEvaluationError) { throw error; } } return Object.assign(Object.assign({}, expression), { a, b }); } case "RoundNumberExpression": { const number = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.number, "Round number expression has null number.")); try { const { value: numberValue, logs: numberLogs } = (0, evaluate_1.default)(number); if (typeof numberValue !== "number") { throw new Error("Evaluated number of round number expression is not a number."); } const result = (0, expression_1.getIntExpression)(Math.round(numberValue), /* isTransient */ true); const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, numberLogs, result.logs); return Object.assign(Object.assign({}, result), { logs }); } catch (error) { const { message } = (0, asError_1.default)(error); if (message !== evaluate_1.complexFormExpressionEvaluationError) { throw error; } } return Object.assign(Object.assign({}, expression), { number }); } case "StringifyNumberExpression": { const number = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.number, "Stringify number expression has null number.")); try { const { value: numberValue, logs: numberLogs } = (0, evaluate_1.default)(number); if (typeof numberValue !== "number") { throw new Error("Evaluated number of stringify number expression is not a number."); } const result = (0, expression_1.getStringExpression)(numberValue.toString(), /* isTransient */ true); const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, numberLogs, result.logs); return Object.assign(Object.assign({}, result), { logs }); } catch (error) { const { message } = (0, asError_1.default)(error); if (message !== evaluate_1.complexFormExpressionEvaluationError) { throw error; } } return Object.assign(Object.assign({}, expression), { number }); } case "StringConcatExpression": { const strings = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.strings, "String concat expression has null strings.")); try { const { value: stringsValue, logs: stringsLogs } = (0, evaluate_1.default)(strings); if (!Array.isArray(stringsValue)) { throw new Error("Evaluated strings of string concat expression is not an array."); } if (!stringsValue.every((x) => typeof x === "string")) { throw new Error("Evaluated strings array of string concat expression contains " + "a value that isn't a string."); } const result = (0, expression_1.getStringExpression)(stringsValue.join(""), /* isTransient */ true); const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, stringsLogs, result.logs); return Object.assign(Object.assign({}, result), { logs }); } catch (error) { const { message } = (0, asError_1.default)(error); if (message !== evaluate_1.complexFormExpressionEvaluationError) { throw error; } } return Object.assign(Object.assign({}, expression), { strings }); } case "GetUrlQueryParameterExpression": { const url = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.url, "Get url query parameter expression has null url.")); const queryParameterName = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.queryParameterName, "Get url query parameter expression has null query parameter name.")); try { const { value: urlValue, logs: urlLogs } = (0, evaluate_1.default)(url); if (typeof urlValue !== "string") { throw new Error("Evaluated url of get url query parameter expression is not a " + "string."); } const { value: queryParameterNameValue, logs: queryParameterNameLogs } = (0, evaluate_1.default)(queryParameterName); if (typeof queryParameterNameValue !== "string") { throw new Error("Evaluated query parameter name of get url query parameter " + "expression is not a string."); } let queryParameterValue = ""; try { const urlObject = new URL(urlValue); queryParameterValue = urlObject.searchParams.get(queryParameterNameValue) || ""; } catch (error) { // } const result = (0, expression_1.getStringExpression)(queryParameterValue, /* isTransient */ true); const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, urlLogs, queryParameterNameLogs, result.logs); return Object.assign(Object.assign({}, result), { logs }); } catch (error) { const { message } = (0, asError_1.default)(error); if (message !== evaluate_1.complexFormExpressionEvaluationError) { throw error; } } return Object.assign(Object.assign({}, expression), { url, queryParameterName }); } case "SplitExpression": { const split = (0, nullThrows_1.default)(splits[(0, nullThrows_1.default)(expression.splitId, "Split expression has null split ID.")], "Split expression has invalid split ID."); const dimensionId = (0, nullThrows_1.default)(expression.dimensionId, "Split expression has null dimension ID."); const expose = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.expose, "Split expression has null expose.")); const unitId = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.unitId, "Split expression has null unit ID.")); const eventPayload = expression.eventPayload ? innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, expression.eventPayload) : null; const unreducedDimensionMapping = expression.dimensionMapping; if (unreducedDimensionMapping.type !== "discrete") { // TODO: Implement throw new Error("Split expression has dimension mapping which isn't discrete."); } const dimensionMapping = { type: "discrete", cases: Object.fromEntries(Object.keys(unreducedDimensionMapping.cases).map((armId) => [ armId, // TODO: Validate armId innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, innerQuery, null, // args variables, (0, nullThrows_1.default)(unreducedDimensionMapping.cases[armId], "Split expression has discrete dimension mapping with null " + `case for arm "${armId}".`)), ])), }; try { const { value: exposeValue, logs: exposeLogs } = (0, evaluate_1.default)(expose); if (typeof exposeValue !== "boolean") { throw new Error("Evaluated expose of split expression is not a boolean."); } const { value: unitIdValue, logs: unitIdLogs } = (0, evaluate_1.default)(unitId); if (typeof unitIdValue !== "string") { throw new Error("Evaluated unit ID of split expression is not a string."); } let assignment = null; let eventObjectTypeName = null; let eventPayloadValue = null; let eventPayloadLogs; // If we've previously assigned this unit for this split, reuse the // previous assignment and also reuse its feature values in the // exposure log. const cacheEntry = assignmentCache.get(split.id, unitIdValue); if (cacheEntry) { assignment = cacheEntry.assignment; eventObjectTypeName = cacheEntry.eventObjectTypeName; eventPayloadValue = cacheEntry.eventPayload; eventPayloadLogs = cacheEntry.eventPayloadLogs; } else { const payloadResult = eventPayload ? (0, evaluate_1.default)(eventPayload) : { value: null, logs: {} }; eventPayloadValue = payloadResult.value === null ? null : (0, asPlainObjectOrThrow_1.default)(payloadResult.value, new Error("Evaluated payload of split expression is not an object.")); eventPayloadLogs = payloadResult.logs; eventObjectTypeName = expression.eventObjectTypeName; if (!!eventPayloadValue !== !!eventObjectTypeName) { // If we have value and not name or vice versa, // then the expression is broken so we just throw an exception. throw new Error("Evaluated payload of split expression or event object type name is missing."); } assignment = getAssignment(commitConfig, split, unitIdValue, eventPayloadValue); assignmentCache.set(split.id, unitIdValue, { assignment, eventObjectTypeName, eventPayloadLogs, eventPayload: eventPayloadValue, }); } const shouldExpose = exposeValue && Object.values(assignment).some((dimensionAssignment) => dimensionAssignment.type !== "discrete" || dimensionAssignment.armId !== constants_1.defaultArmKey); const dimensionAssignment = (0, nullThrows_1.default)(assignment[dimensionId], "Split expression has invalid dimension ID."); if (dimensionAssignment.type === "continuous") { // TODO: Implement throw new Error("Split expression has continuous dimension."); } const result = (0, nullThrows_1.default)(dimensionMapping.cases[dimensionAssignment.armId], `Split expression has reduced dimension mapping with missing case for assigned arm "${dimensionAssignment.armId}".`); const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, exposeLogs, unitIdLogs, eventPayloadLogs, shouldExpose ? (0, reductionLogs_1.getExposureLogs)({ splitId: split.id, unitId: unitIdValue, event: eventPayloadValue && expression.eventObjectTypeName ? { objectTypeName: expression.eventObjectTypeName, payload: eventPayloadValue, } : null, assignment, }) : undefined, result.logs); return Object.assign(Object.assign({}, result), { logs }); } catch (error) { const { message } = (0, asError_1.default)(error); if (message !== evaluate_1.complexFormExpressionEvaluationError) { throw error; } } return Object.assign(Object.assign({}, expression), { expose, unitId, dimensionMapping, eventPayload }); } case "LogEventExpression": { const eventPayload = innerReduce(splits, commitConfig, assignmentCache, enablePartialObjectVariableReduction, fragmentDefinitions, null, // innerQuery null, // args variables, (0, nullThrows_1.default)(expression.eventPayload, new Error("Log event expression is missing event payload."))); try { const { value: rawPayloadValue, logs: payloadLogs } = (0, evaluate_1.default)(eventPayload); const payloadValue = (0, asPlainObjectOrThrow_1.default)(rawPayloadValue, new Error("Evaluated payload of log event expression is not an object.")); const result = (0, expression_1.getNoOpExpression)(/* isTransient */ true); const logs = (0, reductionLogs_1.mergeLogs)(thisLogs, payloadLogs, (0, reductionLogs_1.getEventLogs)({ objectTypeName: (0, nullThrows_1.default)(expression.eventObjectTypeName, new Error(`Log event expression with id "${expression.id}" is missing event type name.`)), payload: payloadValue, }), result.logs); r