All files / src/builder mutateVariables.ts

100% Statements 45/45
94.54% Branches 52/55
100% Functions 5/5
100% Lines 43/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113  4x   4x           461x 461x 461x                 71x       119x 16x   103x 2x     101x 101x 224x 224x 218x       101x   101x 105x   105x       105x     237x           177x   105x 218x 218x 55x         163x         163x       105x     101x               62x           61x 56x 54x 5x     49x 49x   49x 23x     49x 119x   49x 49x    
import type { VariableSchema, VariableValue } from "@featurevisor/types";
import { mutate } from "./mutator";
 
const MUTATION_OP_SUFFIX = /:((?:append|prepend|after|before|remove))$/;
 
/**
 * Get the root variable name from an override key (e.g. "tags:append" -> "tags", "payload.rows:append" -> "payload").
 */
function rootVariableFromOverrideKey(overrideKey: string): string {
  const withoutSuffix = overrideKey.replace(MUTATION_OP_SUFFIX, "").trim();
  const firstSegment = withoutSuffix.includes(".") ? withoutSuffix.split(".")[0] : withoutSuffix;
  return firstSegment.replace(/\s*\[.*\]\s*$/, "").trim();
}
 
/**
 * Resolve variable values from schema defaults and overrides.
 * Override keys may be variable keys or dot-notation paths (e.g. "foo", "foo.a.b", "tags:append", "items[id=2]:after").
 * Uses the mutator so nested paths and mutation notations are supported.
 * Returns only variables that were desired to be overridden (i.e. appear in overrides).
 */
export function resolveMutationsForMultipleVariables(
  variablesSchema: Record<string, VariableSchema> | undefined,
  overrides: Record<string, VariableValue> | undefined,
): Record<string, VariableValue> | undefined {
  if (!overrides || Object.keys(overrides).length === 0) {
    return undefined;
  }
  if (!variablesSchema || Object.keys(variablesSchema).length === 0) {
    return undefined;
  }
 
  const variableKeysToOutput = new Set<string>();
  for (const overrideKey of Object.keys(overrides)) {
    const variableKey = rootVariableFromOverrideKey(overrideKey);
    if (variableKey && variablesSchema[variableKey]) {
      variableKeysToOutput.add(variableKey);
    }
  }
 
  const result: Record<string, VariableValue> = {};
 
  for (const variableKey of variableKeysToOutput) {
    const schema = variablesSchema[variableKey];
    let value: VariableValue =
      schema.defaultValue !== undefined && schema.defaultValue !== null
        ? (JSON.parse(JSON.stringify(schema.defaultValue)) as VariableValue)
        : undefined;
 
    const keysForThisVariable = Object.keys(overrides)
      .filter(
        (k) =>
          rootVariableFromOverrideKey(k) === variableKey &&
          (k === variableKey ||
            k.startsWith(variableKey + ".") ||
            k.startsWith(variableKey + "[") ||
            k.startsWith(variableKey + ":")),
      )
      .sort((a, b) => a.length - b.length);
 
    for (const overrideKey of keysForThisVariable) {
      const overrideValue = overrides[overrideKey];
      if (overrideKey === variableKey) {
        value =
          overrideValue !== undefined && overrideValue !== null
            ? (JSON.parse(JSON.stringify(overrideValue)) as VariableValue)
            : overrideValue;
      } else {
        const notation = overrideKey.startsWith(variableKey + "[")
          ? overrideKey.slice(variableKey.length)
          : overrideKey.startsWith(variableKey + ":")
            ? overrideKey.slice(variableKey.length)
            : overrideKey.slice(variableKey.length + 1);
        value = mutate(schema, value, notation, overrideValue);
      }
    }
 
    result[variableKey] = value;
  }
 
  return Object.keys(result).length > 0 ? result : undefined;
}
 
/**
 * Resolve a single variable's override value (e.g. from variableOverrides).
 * If the value is a plain object with path-like keys, it is merged with the variable's default;
 * otherwise the value is returned as-is (full replacement).
 */
export function resolveMutationsForSingleVariable(
  variablesSchema: Record<string, VariableSchema> | undefined,
  variableKey: string,
  overrideValue: VariableValue,
  baseValue?: VariableValue,
): VariableValue {
  if (!variablesSchema || !variablesSchema[variableKey]) return overrideValue;
  if (overrideValue === null || overrideValue === undefined) return overrideValue;
  if (typeof overrideValue !== "object" || Array.isArray(overrideValue)) {
    return overrideValue;
  }
 
  const pathMap = overrideValue as Record<string, VariableValue>;
  const flat: Record<string, VariableValue> = {};
 
  if (typeof baseValue !== "undefined") {
    flat[variableKey] = JSON.parse(JSON.stringify(baseValue)) as VariableValue;
  }
 
  for (const [k, v] of Object.entries(pathMap)) {
    flat[k === variableKey ? variableKey : variableKey + "." + k] = v;
  }
  const resolved = resolveMutationsForMultipleVariables(variablesSchema, flat);
  return resolved && variableKey in resolved ? resolved[variableKey] : overrideValue;
}