Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/common/src/utils/variables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe('Utils | variables', () => {
const replacedVariable = replaceVariables(
`{${variable}.name}`,
{ [variable]: { name: 'world' } },
JSON.stringify
{ modifier: JSON.stringify }
);
expect(replacedVariable).toBe('"world"');
});
Expand Down
45 changes: 27 additions & 18 deletions packages/common/src/utils/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ export const variableReplacer = (
match: string,
inner: string,
variables: Record<string, unknown>,
modifier?: (variable: unknown) => unknown
modifier: (variable: unknown) => unknown = (val) => val
): unknown => {
const { id, path } = splitVariableName(inner);
if (!(id in variables)) {
return match;
}

if (!path) {
return typeof modifier === 'function' ? modifier(variables[id]) : variables[id];
return modifier(variables[id]);
}

try {
const variable = typeof variables[id] === 'string' ? JSON.parse(variables[id] as string) : variables[id];
return typeof modifier === 'function' ? modifier(_get(variable, path, 0)) : _get(variable, path, 0);

return modifier(_get(variable, path, 0));
} catch (err: any) {
if (err?.message.includes('is not valid JSON')) {
return 0;
Expand Down Expand Up @@ -67,32 +68,36 @@ export const splitVariableName = (
export function replaceVariables(
phrase: string | undefined | null,
variables: Record<string, unknown>,
modifier?: (variable: unknown) => unknown,
options?: { trim?: boolean; keepTypeIfOnlyVariable?: false }
options?: { modifier?: (variable: unknown) => unknown; trim?: boolean; keepTypeIfOnlyVariable?: false }
Comment on lines 68 to +71
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change modifies the public API signature by moving the modifier parameter from being a separate third parameter to being part of the options object. This is a breaking change that will affect any external code calling replaceVariables with the old signature (e.g., replaceVariables(phrase, vars, modifierFn)). Consider providing backward compatibility or ensuring all call sites across the codebase and dependent packages have been updated. A deprecation path or migration guide may be helpful.

Copilot uses AI. Check for mistakes.
): string;
export function replaceVariables(
phrase: string | undefined | null,
variables: Record<string, unknown>,
modifier?: (variable: unknown) => unknown,
options?: { trim?: boolean; keepTypeIfOnlyVariable: true }
options: { modifier?: (variable: unknown) => unknown; trim?: boolean; keepTypeIfOnlyVariable: true }
): unknown;
export function replaceVariables(
phrase: string | undefined | null,
variables: Record<string, unknown>,
modifier: ((variable: unknown) => unknown) | undefined = undefined,
{ trim = true, keepTypeIfOnlyVariable = false }: { trim?: boolean; keepTypeIfOnlyVariable?: boolean } = {}
{
trim = true,
modifier,
keepTypeIfOnlyVariable = false,
}: { modifier?: ((variable: unknown) => unknown) | undefined; trim?: boolean; keepTypeIfOnlyVariable?: boolean } = {}
): string | unknown {
if (!phrase || (trim && !phrase.trim())) {
const formattedPhrase = trim ? phrase?.trim() : phrase;

if (!formattedPhrase) {
return '';
}

if (keepTypeIfOnlyVariable && phrase.match(VARIABLE_ONLY_REGEXP)) {
if (keepTypeIfOnlyVariable && formattedPhrase.match(VARIABLE_ONLY_REGEXP)) {
// remove the curly braces {} from phrase to get the inner
const inner = phrase.slice(1, -1);
return variableReplacer(phrase, inner, variables, modifier);
const inner = formattedPhrase.slice(1, -1);

return variableReplacer(formattedPhrase, inner, variables, modifier);
}

return phrase.replace(READABLE_VARIABLE_REGEXP, (match, inner) =>
return formattedPhrase.replace(READABLE_VARIABLE_REGEXP, (match, inner) =>
String(variableReplacer(match, inner, variables, modifier))
);
}
Expand Down Expand Up @@ -123,18 +128,22 @@ export const transformStringVariableToNumber = (str: string | number | null): nu
return Number.isNaN(number) ? str : number;
};

export const deepVariableSubstitution = <T>(bodyData: T, variableMap: Record<string, unknown>): T => {
const _recurse = (subCollection: any, modifier?: (variable: unknown) => unknown): any => {
export const deepVariableSubstitution = <T>(
bodyData: T,
variableMap: Record<string, unknown>,
options?: { trim?: boolean; modifier?: (variable: unknown) => unknown; keepTypeIfOnlyVariable?: boolean }
): T => {
const _recurse = (subCollection: any): any => {
if (!subCollection) {
return subCollection;
}

if (typeof subCollection === 'string') {
return replaceVariables(subCollection, variableMap, modifier);
return replaceVariables(subCollection, variableMap, options as any);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of as any bypasses TypeScript's type checking. This could hide type incompatibilities between the options passed to deepVariableSubstitution and those expected by replaceVariables. Consider using a more specific type assertion or refactoring the type definitions to ensure type safety. The main concern is that keepTypeIfOnlyVariable: true in options would change the return type of replaceVariables to unknown, but this function is being used in a context where a string is expected, which could cause runtime issues.

Copilot uses AI. Check for mistakes.
}

if (Array.isArray(subCollection)) {
return subCollection.map((v) => _recurse(v, modifier));
return subCollection.map((v) => _recurse(v));
}

if (typeof subCollection === 'object') {
Expand Down
Loading