⚡ (bot) Improve variables parsing and predictability
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user