2
0

🐛(setVariable) Improve how set variable is evaluated

Avoid evaluating in sandbox when not necessary
This commit is contained in:
Baptiste Arnaud
2024-08-29 15:05:35 +02:00
parent 0c7d2afd51
commit 97a3212356

View File

@@ -5,8 +5,9 @@ import {
SetVariableHistoryItem, SetVariableHistoryItem,
Variable, Variable,
VariableWithUnknowValue, VariableWithUnknowValue,
VariableWithValue,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { byId, isEmpty } from '@typebot.io/lib' import { byId, isEmpty, isNotDefined } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '../../../types' import { ExecuteLogicResponse } from '../../../types'
import { parseScriptToExecuteClientSideAction } from '../script/executeScript' import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
import { parseVariables } from '@typebot.io/variables/parseVariables' import { parseVariables } from '@typebot.io/variables/parseVariables'
@@ -41,19 +42,19 @@ export const executeSetVariable = async (
block.id, block.id,
setVariableHistory setVariableHistory
) )
const isCustomValue = !block.options.type || block.options.type === 'Custom'
const isCode = const isCode =
(!block.options.type || block.options.type === 'Custom') && (!block.options.type || block.options.type === 'Custom') &&
(block.options.isCode ?? defaultSetVariableOptions.isCode) (block.options.isCode ?? defaultSetVariableOptions.isCode)
if ( if (
expressionToEvaluate && expressionToEvaluate &&
expressionToEvaluate.type === 'code' &&
!state.whatsApp && !state.whatsApp &&
((isCustomValue && block.options.isExecutedOnClient) || (block.options.isExecutedOnClient ||
block.options.type === 'Moment of the day') block.options.type === 'Moment of the day')
) { ) {
const scriptToExecute = parseScriptToExecuteClientSideAction( const scriptToExecute = parseScriptToExecuteClientSideAction(
variables, variables,
expressionToEvaluate expressionToEvaluate.code
) )
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
@@ -116,15 +117,31 @@ export const executeSetVariable = async (
const evaluateSetVariableExpression = const evaluateSetVariableExpression =
(variables: Variable[]) => (variables: Variable[]) =>
(str: string): { value: unknown; error?: string } => { (
expression:
| {
type: 'code'
code: string
}
| { type: 'value'; value: VariableWithValue['value'] }
): { value: unknown; error?: string } => {
if (expression.type === 'value') return { value: expression.value }
const isSingleVariable = const isSingleVariable =
str.startsWith('{{') && str.endsWith('}}') && str.split('{{').length === 2 expression.code.startsWith('{{') &&
if (isSingleVariable) return { value: parseVariables(variables)(str) } expression.code.endsWith('}}') &&
expression.code.split('{{').length === 2
if (isSingleVariable)
return { value: parseVariables(variables)(expression.code) }
// To avoid octal number evaluation // To avoid octal number evaluation
if (!isNaN(str as unknown as number) && /0[^.].+/.test(str)) if (
return { value: str } !isNaN(expression.code as unknown as number) &&
/0[^.].+/.test(expression.code)
)
return { value: expression.code }
try { try {
const body = parseVariables(variables, { fieldToParse: 'id' })(str) const body = parseVariables(variables, { fieldToParse: 'id' })(
expression.code
)
return { return {
value: createCodeRunner({ variables })( value: createCodeRunner({ variables })(
body.includes('return ') ? body : `return ${body}` body.includes('return ') ? body : `return ${body}`
@@ -132,7 +149,7 @@ const evaluateSetVariableExpression =
} }
} catch (err) { } catch (err) {
return { return {
value: parseVariables(variables)(str), value: parseVariables(variables)(expression.code),
error: stringifyError(err), error: stringifyError(err),
} }
} }
@@ -144,82 +161,176 @@ const getExpressionToEvaluate =
options: SetVariableBlock['options'], options: SetVariableBlock['options'],
blockId: string, blockId: string,
setVariableHistory: SetVariableHistoryItem[] setVariableHistory: SetVariableHistoryItem[]
): Promise<string | null> => { ): Promise<
| { type: 'code'; code: string }
| { type: 'value'; value: VariableWithValue['value'] }
| null
> => {
switch (options?.type) { switch (options?.type) {
case 'Contact name': case 'Contact name':
return state.whatsApp?.contact.name ?? null return state.whatsApp?.contact.name
? { type: 'value', value: state.whatsApp.contact.name }
: null
case 'Phone number': { case 'Phone number': {
const phoneNumber = state.whatsApp?.contact.phoneNumber return state.whatsApp?.contact.phoneNumber
return phoneNumber ? `"${state.whatsApp?.contact.phoneNumber}"` : null ? { type: 'value', value: state.whatsApp.contact.phoneNumber }
: null
} }
case 'Now': { case 'Now': {
const timeZone = parseVariables( const timeZone = parseVariables(
state.typebotsQueue[0].typebot.variables state.typebotsQueue[0].typebot.variables
)(options.timeZone) )(options.timeZone)
if (isEmpty(timeZone)) return 'new Date().toISOString()' if (isEmpty(timeZone))
return toISOWithTz(new Date(), timeZone) return { type: 'value', value: new Date().toISOString() }
return { type: 'value', value: toISOWithTz(new Date(), timeZone) }
} }
case 'Today': case 'Today':
return 'new Date().toISOString()' return { type: 'value', value: new Date().toISOString() }
case 'Tomorrow': { case 'Tomorrow': {
const timeZone = parseVariables( const timeZone = parseVariables(
state.typebotsQueue[0].typebot.variables state.typebotsQueue[0].typebot.variables
)(options.timeZone) )(options.timeZone)
if (isEmpty(timeZone)) if (isEmpty(timeZone))
return 'new Date(Date.now() + 86400000).toISOString()' return {
return toISOWithTz(new Date(Date.now() + 86400000), timeZone) type: 'value',
value: new Date(Date.now() + 86400000).toISOString(),
}
return {
type: 'value',
value: toISOWithTz(new Date(Date.now() + 86400000), timeZone),
}
} }
case 'Yesterday': { case 'Yesterday': {
const timeZone = parseVariables( const timeZone = parseVariables(
state.typebotsQueue[0].typebot.variables state.typebotsQueue[0].typebot.variables
)(options.timeZone) )(options.timeZone)
if (isEmpty(timeZone)) if (isEmpty(timeZone))
return 'new Date(Date.now() - 86400000).toISOString()' return {
return toISOWithTz(new Date(Date.now() - 86400000), timeZone) type: 'value',
value: new Date(Date.now() - 86400000).toISOString(),
}
return {
type: 'value',
value: toISOWithTz(new Date(Date.now() - 86400000), timeZone),
}
} }
case 'Random ID': { case 'Random ID': {
return `"${createId()}"` return { type: 'value', value: createId() }
} }
case 'Result ID': case 'Result ID':
case 'User ID': { case 'User ID': {
return state.typebotsQueue[0].resultId ?? `"${createId()}"` return {
type: 'value',
value: state.typebotsQueue[0].resultId ?? createId(),
}
} }
case 'Map item with same index': { case 'Map item with same index': {
return `const itemIndex = ${options.mapListItemParams?.baseListVariableId}.indexOf(${options.mapListItemParams?.baseItemVariableId}) const baseListVariableValue =
return ${options.mapListItemParams?.targetListVariableId}.at(itemIndex)` state.typebotsQueue[0].typebot.variables.find(
byId(options.mapListItemParams?.baseListVariableId)
)?.value
const baseItemVariableValue =
state.typebotsQueue[0].typebot.variables.find(
byId(options.mapListItemParams?.baseItemVariableId)
)?.value
const targetListVariableValue =
state.typebotsQueue[0].typebot.variables.find(
byId(options.mapListItemParams?.targetListVariableId)
)?.value
if (
!Array.isArray(baseListVariableValue) ||
!baseItemVariableValue ||
typeof baseItemVariableValue !== 'string'
)
return null
const itemIndex = baseListVariableValue.indexOf(baseItemVariableValue)
if (itemIndex === -1 || !Array.isArray(targetListVariableValue))
return null
const value = targetListVariableValue.at(itemIndex)
if (isEmpty(value)) return null
return {
type: 'value',
value,
}
} }
case 'Pop': { case 'Pop': {
return `${options.variableId} && Array.isArray(${options.variableId}) ? ${options.variableId}.slice(0, -1) : []` const variableValue = state.typebotsQueue[0].typebot.variables.find(
byId(options.variableId)
)?.value
if (isNotDefined(variableValue)) return null
if (!Array.isArray(variableValue))
return {
type: 'value',
value: variableValue,
}
return {
type: 'value',
value: variableValue.slice(0, -1),
}
} }
case 'Shift': { case 'Shift': {
return `${options.variableId} && Array.isArray(${options.variableId}) ? ${options.variableId}.slice(1) : []` const variableValue = state.typebotsQueue[0].typebot.variables.find(
byId(options.variableId)
)?.value
if (isNotDefined(variableValue)) return null
if (!Array.isArray(variableValue))
return {
type: 'value',
value: variableValue,
}
return {
type: 'value',
value: variableValue.slice(1),
}
} }
case 'Append value(s)': { case 'Append value(s)': {
if (!options.variableId) return null
const item = parseVariables(state.typebotsQueue[0].typebot.variables)( const item = parseVariables(state.typebotsQueue[0].typebot.variables)(
options.item options.item
).replaceAll('`', '\\`') )
if (isEmpty(item)) return `return ${options.variableId}` const variableValue = state.typebotsQueue[0].typebot.variables.find(
return `if(!${options.variableId}) return [\`${item}\`]; byId(options.variableId)
if(!Array.isArray(${options.variableId})) return [${options.variableId}, \`${item}\`]; )?.value
return (${options.variableId}).concat(\`${item}\`);` if (isNotDefined(variableValue))
return {
type: 'value',
value: [item],
}
if (isEmpty(item))
return {
type: 'value',
value: Array.isArray(variableValue)
? variableValue
: [variableValue],
}
if (!Array.isArray(variableValue))
return { type: 'value', value: [variableValue, item] }
return { type: 'value', value: variableValue.concat(item) }
} }
case 'Empty': { case 'Empty': {
return null return null
} }
case 'Moment of the day': { case 'Moment of the day': {
return `const now = new Date() return {
type: 'code',
code: `const now = new Date()
if(now.getHours() < 12) return 'morning' if(now.getHours() < 12) return 'morning'
if(now.getHours() >= 12 && now.getHours() < 18) return 'afternoon' if(now.getHours() >= 12 && now.getHours() < 18) return 'afternoon'
if(now.getHours() >= 18) return 'evening' if(now.getHours() >= 18) return 'evening'
if(now.getHours() >= 22 || now.getHours() < 6) return 'night'` if(now.getHours() >= 22 || now.getHours() < 6) return 'night'`,
}
} }
case 'Environment name': { case 'Environment name': {
return state.whatsApp ? 'whatsapp' : 'web' return {
type: 'value',
value: state.whatsApp ? 'whatsapp' : 'web',
}
} }
case 'Transcript': { case 'Transcript': {
const props = await parseTranscriptProps(state) const props = await parseTranscriptProps(state)
if (!props) return '' if (!props) return null
const typebotWithEmptyVariables = { const typebotWithEmptyVariables = {
...state.typebotsQueue[0].typebot, ...state.typebotsQueue[0].typebot,
variables: state.typebotsQueue[0].typebot.variables.map((v) => ({ variables: state.typebotsQueue[0].typebot.variables.map((v) => ({
@@ -234,22 +345,23 @@ const getExpressionToEvaluate =
setVariableHistory: setVariableHistory:
props.setVariableHistory.concat(setVariableHistory), props.setVariableHistory.concat(setVariableHistory),
}) })
return ( return {
'return `' + type: 'value',
transcript value: transcript
.map( .map(
(message) => (message) =>
`${ `${
message.role === 'bot' ? 'Assistant:' : 'User:' message.role === 'bot' ? 'Assistant:' : 'User:'
} "${parseTranscriptMessageText(message)}"` } "${parseTranscriptMessageText(message)}"`
) )
.join('\n\n') + .join('\n\n'),
'`' }
)
} }
case 'Custom': case 'Custom':
case undefined: { case undefined: {
return options?.expressionToEvaluate ?? null return options?.expressionToEvaluate
? { type: 'code', code: options.expressionToEvaluate }
: null
} }
} }
} }