All files / builder mutateVariables.ts

100% Statements 45/45
93.75% Branches 45/48
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  2x   2x           291x 291x 291x                 2x       84x 6x   78x 2x     76x 76x 139x 139x 133x       76x   76x 80x   80x       80x     152x           72x   80x 133x 133x 35x         98x         98x       80x     76x               2x           41x 36x 34x 5x     29x 29x   29x 8x     29x 54x   29x 29x    
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;
}