2
0

(bot) Improve variables parsing and predictability

This commit is contained in:
Baptiste Arnaud
2022-10-15 09:35:27 +02:00
parent b87ba4023d
commit 3dc3ab201d
6 changed files with 73 additions and 50 deletions

View File

@@ -1,5 +1,11 @@
import { Answer, ResultValues, VariableWithValue } from 'models' import {
Answer,
ResultValues,
VariableWithUnknowValue,
VariableWithValue,
} from 'models'
import React, { createContext, ReactNode, useContext, useState } from 'react' import React, { createContext, ReactNode, useContext, useState } from 'react'
import { safeStringify } from 'services/variable'
const answersContext = createContext<{ const answersContext = createContext<{
resultId?: string resultId?: string
@@ -7,7 +13,7 @@ const answersContext = createContext<{
addAnswer: ( addAnswer: (
answer: Answer & { uploadedFiles: boolean } answer: Answer & { uploadedFiles: boolean }
) => Promise<void> | undefined ) => Promise<void> | undefined
updateVariables: (variables: VariableWithValue[]) => void updateVariables: (variables: VariableWithUnknowValue[]) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})
@@ -39,13 +45,18 @@ export const AnswersContext = ({
return onNewAnswer && onNewAnswer(answer) return onNewAnswer && onNewAnswer(answer)
} }
const updateVariables = (variables: VariableWithValue[]) => const updateVariables = (newVariables: VariableWithUnknowValue[]) => {
const serializedNewVariables = newVariables.map((variable) => ({
...variable,
value: safeStringify(variable.value),
})) as VariableWithValue[]
setResultValues((resultValues) => { setResultValues((resultValues) => {
const updatedVariables = [ const updatedVariables = [
...resultValues.variables.filter((v) => ...resultValues.variables.filter((v) =>
variables.every((variable) => variable.id !== v.id) serializedNewVariables.every((variable) => variable.id !== v.id)
), ),
...variables, ...serializedNewVariables,
] ]
if (onVariablesUpdated) onVariablesUpdated(updatedVariables) if (onVariablesUpdated) onVariablesUpdated(updatedVariables)
return { return {
@@ -53,6 +64,7 @@ export const AnswersContext = ({
variables: updatedVariables, variables: updatedVariables,
} }
}) })
}
return ( return (
<answersContext.Provider <answersContext.Provider

View File

@@ -8,6 +8,7 @@ import React, {
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react'
import { safeStringify } from 'services/variable'
export type LinkedTypebot = Pick< export type LinkedTypebot = Pick<
PublicTypebot | Typebot, PublicTypebot | Typebot,
@@ -28,7 +29,7 @@ const typebotContext = createContext<{
linkedBotQueue: LinkedTypebotQueue linkedBotQueue: LinkedTypebotQueue
isLoading: boolean isLoading: boolean
setCurrentTypebotId: (id: string) => void setCurrentTypebotId: (id: string) => void
updateVariableValue: (variableId: string, value: string | number) => void updateVariableValue: (variableId: string, value: unknown) => void
createEdge: (edge: Edge) => void createEdge: (edge: Edge) => void
injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot
popEdgeIdFromLinkedTypebotQueue: () => void popEdgeIdFromLinkedTypebotQueue: () => void
@@ -71,8 +72,9 @@ export const TypebotContext = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [typebot.theme, typebot.settings]) }, [typebot.theme, typebot.settings])
const updateVariableValue = (variableId: string, value: string | number) => { const updateVariableValue = (variableId: string, value: unknown) => {
const formattedValue = formatIncomingVariableValue(value) const formattedValue = safeStringify(value)
setLocalTypebot((typebot) => ({ setLocalTypebot((typebot) => ({
...typebot, ...typebot,
variables: typebot.variables.map((v) => variables: typebot.variables.map((v) =>
@@ -140,14 +142,4 @@ export const TypebotContext = ({
) )
} }
const formatIncomingVariableValue = (
value?: string | number
): string | number | undefined => {
// This first check avoid to parse 004 as the number 4.
if (typeof value === 'string' && value.startsWith('0') && value.length > 1)
return value
if (typeof value === 'number') return value
return isNaN(value?.toString() as unknown as number) ? value : Number(value)
}
export const useTypebot = () => useContext(typebotContext) export const useTypebot = () => useContext(typebotContext)

View File

@@ -18,6 +18,7 @@ import {
VariableWithValue, VariableWithValue,
MakeComBlock, MakeComBlock,
PabblyConnectBlock, PabblyConnectBlock,
VariableWithUnknowValue,
} from 'models' } from 'models'
import { stringify } from 'qs' import { stringify } from 'qs'
import { byId, sendRequest } from 'utils' import { byId, sendRequest } from 'utils'
@@ -34,8 +35,8 @@ type IntegrationContext = {
resultValues: ResultValues resultValues: ResultValues
groups: Group[] groups: Group[]
resultId?: string resultId?: string
updateVariables: (variables: VariableWithValue[]) => void updateVariables: (variables: VariableWithUnknowValue[]) => void
updateVariableValue: (variableId: string, value: string | number) => void updateVariableValue: (variableId: string, value: unknown) => void
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
} }
@@ -252,7 +253,7 @@ const executeWebhook = async (
details: JSON.stringify(error ?? data, null, 2).substring(0, 1000), details: JSON.stringify(error ?? data, null, 2).substring(0, 1000),
}) })
const newVariables = block.options.responseVariableMapping.reduce< const newVariables = block.options.responseVariableMapping.reduce<
VariableWithValue[] VariableWithUnknowValue[]
>((newVariables, varMapping) => { >((newVariables, varMapping) => {
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
const existingVariable = variables.find(byId(varMapping.variableId)) const existingVariable = variables.find(byId(varMapping.variableId))
@@ -262,13 +263,8 @@ const executeWebhook = async (
`return data.${parseVariables(variables)(varMapping?.bodyPath)}` `return data.${parseVariables(variables)(varMapping?.bodyPath)}`
) )
try { try {
const value = func(data) const value: unknown = func(data)
updateVariableValue( updateVariableValue(existingVariable?.id, value)
existingVariable?.id,
typeof value !== 'number' && typeof value !== 'string'
? JSON.stringify(value)
: value
)
return [...newVariables, { ...existingVariable, value }] return [...newVariables, { ...existingVariable, value }]
} catch (err) { } catch (err) {
return newVariables return newVariables

View File

@@ -16,7 +16,7 @@ import {
PublicTypebot, PublicTypebot,
Typebot, Typebot,
Edge, Edge,
VariableWithValue, VariableWithUnknowValue,
} from 'models' } from 'models'
import { byId, isDefined, isNotDefined, sendRequest } from 'utils' import { byId, isDefined, isNotDefined, sendRequest } from 'utils'
import { sanitizeUrl } from './utils' import { sanitizeUrl } from './utils'
@@ -35,8 +35,8 @@ type LogicContext = {
typebotId: string typebotId: string
}) => void }) => void
setCurrentTypebotId: (id: string) => void setCurrentTypebotId: (id: string) => void
updateVariableValue: (variableId: string, value: string) => void updateVariableValue: (variableId: string, value: unknown) => void
updateVariables: (variables: VariableWithValue[]) => void updateVariables: (variables: VariableWithUnknowValue[]) => void
injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
createEdge: (edge: Edge) => void createEdge: (edge: Edge) => void
@@ -97,10 +97,8 @@ const executeComparison =
if (!comparison?.variableId) return false if (!comparison?.variableId) return false
const inputValue = ( const inputValue = (
variables.find((v) => v.id === comparison.variableId)?.value ?? '' variables.find((v) => v.id === comparison.variableId)?.value ?? ''
) ).trim()
.toString() const value = parseVariables(variables)(comparison.value).trim()
.trim()
const value = parseVariables(variables)(comparison.value).toString().trim()
if (isNotDefined(value)) return false if (isNotDefined(value)) return false
switch (comparison.comparisonOperator) { switch (comparison.comparisonOperator) {
case ComparisonOperators.CONTAINS: { case ComparisonOperators.CONTAINS: {

View File

@@ -23,30 +23,44 @@ export const parseVariables =
if (options.fieldToParse === 'id') return variable.id if (options.fieldToParse === 'id') return variable.id
const { value } = variable const { value } = variable
if (isNotDefined(value)) return '' if (isNotDefined(value)) return ''
if (options.escapeForJson) return jsonParse(value.toString()) if (options.escapeForJson) return jsonParse(value)
return value.toString() const parsedValue = safeStringify(value)
if (!parsedValue) return ''
return parsedValue
}) })
} }
export const safeStringify = (val: unknown): string | null => {
if (isNotDefined(val)) return null
if (typeof val === 'string') return val
try {
return JSON.stringify(val)
} catch {
console.warn('Failed to safely stringify variable value', val)
return null
}
}
const jsonParse = (str: string) => const jsonParse = (str: string) =>
str str
.replace(/\n/g, `\\n`) .replace(/\n/g, `\\n`)
.replace(/"/g, `\\"`) .replace(/"/g, `\\"`)
.replace(/\\[^n"]/g, `\\\\ `) .replace(/\\[^n"]/g, `\\\\ `)
export const evaluateExpression = (variables: Variable[]) => (str: string) => { export const evaluateExpression =
const evaluating = parseVariables(variables, { fieldToParse: 'id' })( (variables: Variable[]) =>
str.includes('return ') ? str : `return ${str}` (str: string): unknown => {
) const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
try { str.includes('return ') ? str : `return ${str}`
const func = Function(...variables.map((v) => v.id), evaluating) )
const evaluatedResult = func(...variables.map((v) => v.value)) try {
return isNotDefined(evaluatedResult) ? '' : evaluatedResult const func = Function(...variables.map((v) => v.id), evaluating)
} catch (err) { return func(...variables.map((v) => v.value))
console.log(`Evaluating: ${evaluating}`, err) } catch (err) {
return str console.log(`Evaluating: ${evaluating}`, err)
return str
}
} }
}
export const parseVariablesInObject = ( export const parseVariablesInObject = (
object: { [key: string]: string | number }, object: { [key: string]: string | number },

View File

@@ -3,10 +3,21 @@ import { z } from 'zod'
export const variableSchema = z.object({ export const variableSchema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
value: z.string().or(z.number()).optional(), value: z.string().optional().nullable(),
}) })
/**
* Variable when retrieved from the database
*/
export type VariableWithValue = Omit<Variable, 'value'> & { export type VariableWithValue = Omit<Variable, 'value'> & {
value: string value: string
} }
/**
* Variable when computed or retrieved from a block
*/
export type VariableWithUnknowValue = Omit<VariableWithValue, 'value'> & {
value: unknown
}
export type Variable = z.infer<typeof variableSchema> export type Variable = z.infer<typeof variableSchema>