1eadef8ea0
## Problem
When a workflow passes a variable into a text input (e.g. an **AI
Agent** prompt) and that variable resolves to an object or array, the
resolved string contained `[object Object]` instead of the actual
content. The AI then received useless input.
## Cause
`resolveString` in the shared variable resolver builds the final string
with `String.prototype.replace`. When an embedded `{{variable}}`
resolved to an object, the replace callback returned the object
directly, which JS coerces to `"[object Object]"`. The rich-text
resolver had the same issue via `String(resolvedValue)`.
## Fix
When an embedded variable resolves to a non-null object (or array),
serialize it with `JSON.stringify` before inserting it into the
surrounding string. Primitive values keep their existing coercion
behavior, and the single-variable case (`{{message}}` with nothing
around it) still returns the raw object so non-string consumers are
unaffected.
Applied the same guard to both the plain and rich-text variable
resolvers for consistency.
## Tests
Added cases covering embedded object/array variables in both resolvers,
plus a guard test confirming a standalone `{{variable}}` still returns
the raw object.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21612?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
88 lines
2.1 KiB
TypeScript
88 lines
2.1 KiB
TypeScript
import { evalFromContext } from '@/utils/evalFromContext';
|
|
import { isDefined } from '@/utils/validation';
|
|
|
|
const isString = (value: unknown): value is string => {
|
|
return typeof value === 'string';
|
|
};
|
|
|
|
const VARIABLE_PATTERN = RegExp('\\{\\{([^{}]+)\\}\\}', 'g');
|
|
|
|
export const resolveInput = (
|
|
unresolvedInput: unknown,
|
|
context: Record<string, unknown>,
|
|
): unknown => {
|
|
if (!isDefined(unresolvedInput)) {
|
|
return unresolvedInput;
|
|
}
|
|
|
|
if (isString(unresolvedInput)) {
|
|
return resolveString(unresolvedInput, context);
|
|
}
|
|
|
|
if (Array.isArray(unresolvedInput)) {
|
|
return resolveArray(unresolvedInput, context);
|
|
}
|
|
|
|
if (typeof unresolvedInput === 'object' && unresolvedInput !== null) {
|
|
return resolveObject(unresolvedInput, context);
|
|
}
|
|
|
|
return unresolvedInput;
|
|
};
|
|
|
|
const resolveArray = (
|
|
input: unknown[],
|
|
context: Record<string, unknown>,
|
|
): unknown[] => {
|
|
const resolvedArray = input;
|
|
|
|
for (let i = 0; i < input.length; ++i) {
|
|
resolvedArray[i] = resolveInput(input[i], context);
|
|
}
|
|
|
|
return resolvedArray;
|
|
};
|
|
|
|
const resolveObject = (
|
|
input: object,
|
|
context: Record<string, unknown>,
|
|
): object => {
|
|
return Object.entries(input).reduce<Record<string, unknown>>(
|
|
(resolvedObject, [key, value]) => {
|
|
const resolvedKey = resolveInput(key, context);
|
|
|
|
resolvedObject[
|
|
typeof resolvedKey === 'string' ? resolvedKey : String(resolvedKey)
|
|
] = resolveInput(value, context);
|
|
|
|
return resolvedObject;
|
|
},
|
|
{},
|
|
);
|
|
};
|
|
|
|
const resolveString = (
|
|
input: string,
|
|
context: Record<string, unknown>,
|
|
): string => {
|
|
const matchedTokens = input.match(VARIABLE_PATTERN);
|
|
|
|
if (!matchedTokens || matchedTokens.length === 0) {
|
|
return input;
|
|
}
|
|
|
|
if (matchedTokens.length === 1 && matchedTokens[0] === input) {
|
|
return evalFromContext(input, context);
|
|
}
|
|
|
|
return input.replace(VARIABLE_PATTERN, (matchedToken, _) => {
|
|
const processedToken = evalFromContext(matchedToken, context);
|
|
|
|
if (typeof processedToken === 'object' && processedToken !== null) {
|
|
return JSON.stringify(processedToken);
|
|
}
|
|
|
|
return processedToken;
|
|
});
|
|
};
|