♻️ Export bot-engine code into its own package
This commit is contained in:
16
packages/bot-engine/blocks/logic/abTest/executeAbTest.ts
Normal file
16
packages/bot-engine/blocks/logic/abTest/executeAbTest.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AbTestBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
|
||||
export const executeAbTest = (
|
||||
_: SessionState,
|
||||
block: AbTestBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const aEdgeId = block.items[0].outgoingEdgeId
|
||||
const random = Math.random() * 100
|
||||
if (random < block.options.aPercent && aEdgeId) {
|
||||
return { outgoingEdgeId: aEdgeId }
|
||||
}
|
||||
const bEdgeId = block.items[1].outgoingEdgeId
|
||||
if (bEdgeId) return { outgoingEdgeId: bEdgeId }
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
}
|
||||
170
packages/bot-engine/blocks/logic/condition/executeCondition.ts
Normal file
170
packages/bot-engine/blocks/logic/condition/executeCondition.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { isNotDefined, isDefined } from '@typebot.io/lib'
|
||||
import {
|
||||
Comparison,
|
||||
ComparisonOperators,
|
||||
Condition,
|
||||
LogicalOperator,
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
|
||||
export const executeCondition =
|
||||
(variables: Variable[]) =>
|
||||
(condition: Condition): boolean =>
|
||||
condition.logicalOperator === LogicalOperator.AND
|
||||
? condition.comparisons.every(executeComparison(variables))
|
||||
: condition.comparisons.some(executeComparison(variables))
|
||||
|
||||
const executeComparison =
|
||||
(variables: Variable[]) =>
|
||||
(comparison: Comparison): boolean => {
|
||||
if (!comparison?.variableId) return false
|
||||
const inputValue =
|
||||
variables.find((v) => v.id === comparison.variableId)?.value ?? null
|
||||
const value =
|
||||
comparison.value === 'undefined' || comparison.value === 'null'
|
||||
? null
|
||||
: findUniqueVariableValue(variables)(comparison.value) ??
|
||||
parseVariables(variables)(comparison.value)
|
||||
if (isNotDefined(comparison.comparisonOperator)) return false
|
||||
switch (comparison.comparisonOperator) {
|
||||
case ComparisonOperators.CONTAINS: {
|
||||
const contains = (a: string | null, b: string | null) => {
|
||||
if (b === '' || !b || !a) return false
|
||||
return a
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.normalize()
|
||||
.includes(b.toLowerCase().trim().normalize())
|
||||
}
|
||||
return compare(contains, inputValue, value, 'some')
|
||||
}
|
||||
case ComparisonOperators.NOT_CONTAINS: {
|
||||
const notContains = (a: string | null, b: string | null) => {
|
||||
if (b === '' || !b || !a) return true
|
||||
return !a
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.normalize()
|
||||
.includes(b.toLowerCase().trim().normalize())
|
||||
}
|
||||
return compare(notContains, inputValue, value)
|
||||
}
|
||||
case ComparisonOperators.EQUAL: {
|
||||
return compare(
|
||||
(a, b) => {
|
||||
if (typeof a === 'string' && typeof b === 'string')
|
||||
return a.normalize() === b.normalize()
|
||||
return a === b
|
||||
},
|
||||
inputValue,
|
||||
value
|
||||
)
|
||||
}
|
||||
case ComparisonOperators.NOT_EQUAL: {
|
||||
return compare(
|
||||
(a, b) => {
|
||||
if (typeof a === 'string' && typeof b === 'string')
|
||||
return a.normalize() !== b.normalize()
|
||||
return a !== b
|
||||
},
|
||||
inputValue,
|
||||
value
|
||||
)
|
||||
}
|
||||
case ComparisonOperators.GREATER: {
|
||||
if (isNotDefined(inputValue) || isNotDefined(value)) return false
|
||||
if (typeof inputValue === 'string') {
|
||||
if (typeof value === 'string')
|
||||
return parseDateOrNumber(inputValue) > parseDateOrNumber(value)
|
||||
return Number(inputValue) > value.length
|
||||
}
|
||||
if (typeof value === 'string') return inputValue.length > Number(value)
|
||||
return inputValue.length > value.length
|
||||
}
|
||||
case ComparisonOperators.LESS: {
|
||||
if (isNotDefined(inputValue) || isNotDefined(value)) return false
|
||||
if (typeof inputValue === 'string') {
|
||||
if (typeof value === 'string')
|
||||
return parseDateOrNumber(inputValue) < parseDateOrNumber(value)
|
||||
return Number(inputValue) < value.length
|
||||
}
|
||||
if (typeof value === 'string') return inputValue.length < Number(value)
|
||||
return inputValue.length < value.length
|
||||
}
|
||||
case ComparisonOperators.IS_SET: {
|
||||
return isDefined(inputValue) && inputValue.length > 0
|
||||
}
|
||||
case ComparisonOperators.IS_EMPTY: {
|
||||
return isNotDefined(inputValue) || inputValue.length === 0
|
||||
}
|
||||
case ComparisonOperators.STARTS_WITH: {
|
||||
const startsWith = (a: string | null, b: string | null) => {
|
||||
if (b === '' || !b || !a) return false
|
||||
return a
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.normalize()
|
||||
.startsWith(b.toLowerCase().trim().normalize())
|
||||
}
|
||||
return compare(startsWith, inputValue, value)
|
||||
}
|
||||
case ComparisonOperators.ENDS_WITH: {
|
||||
const endsWith = (a: string | null, b: string | null) => {
|
||||
if (b === '' || !b || !a) return false
|
||||
return a
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.normalize()
|
||||
.endsWith(b.toLowerCase().trim().normalize())
|
||||
}
|
||||
return compare(endsWith, inputValue, value)
|
||||
}
|
||||
case ComparisonOperators.MATCHES_REGEX: {
|
||||
const matchesRegex = (a: string | null, b: string | null) => {
|
||||
if (b === '' || !b || !a) return false
|
||||
return new RegExp(b).test(a)
|
||||
}
|
||||
return compare(matchesRegex, inputValue, value, 'some')
|
||||
}
|
||||
case ComparisonOperators.NOT_MATCH_REGEX: {
|
||||
const matchesRegex = (a: string | null, b: string | null) => {
|
||||
if (b === '' || !b || !a) return false
|
||||
return !new RegExp(b).test(a)
|
||||
}
|
||||
return compare(matchesRegex, inputValue, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const compare = (
|
||||
compareStrings: (a: string | null, b: string | null) => boolean,
|
||||
a: Exclude<Variable['value'], undefined>,
|
||||
b: Exclude<Variable['value'], undefined>,
|
||||
type: 'every' | 'some' = 'every'
|
||||
): boolean => {
|
||||
if (!a || typeof a === 'string') {
|
||||
if (!b || typeof b === 'string') return compareStrings(a, b)
|
||||
return type === 'every'
|
||||
? b.every((b) => compareStrings(a, b))
|
||||
: b.some((b) => compareStrings(a, b))
|
||||
}
|
||||
if (!b || typeof b === 'string') {
|
||||
return type === 'every'
|
||||
? a.every((a) => compareStrings(a, b))
|
||||
: a.some((a) => compareStrings(a, b))
|
||||
}
|
||||
if (type === 'every')
|
||||
return a.every((a) => b.every((b) => compareStrings(a, b)))
|
||||
return a.some((a) => b.some((b) => compareStrings(a, b)))
|
||||
}
|
||||
|
||||
const parseDateOrNumber = (value: string): number => {
|
||||
const parsed = Number(value)
|
||||
if (isNaN(parsed)) {
|
||||
const time = Date.parse(value)
|
||||
return time
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ConditionBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { executeCondition } from './executeCondition'
|
||||
|
||||
export const executeConditionBlock = (
|
||||
state: SessionState,
|
||||
block: ConditionBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
const passedCondition = block.items.find((item) =>
|
||||
executeCondition(variables)(item.content)
|
||||
)
|
||||
return {
|
||||
outgoingEdgeId: passedCondition
|
||||
? passedCondition.outgoingEdgeId
|
||||
: block.outgoingEdgeId,
|
||||
}
|
||||
}
|
||||
29
packages/bot-engine/blocks/logic/jump/executeJumpBlock.ts
Normal file
29
packages/bot-engine/blocks/logic/jump/executeJumpBlock.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { addEdgeToTypebot, createPortalEdge } from '../../../addEdgeToTypebot'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { SessionState } from '@typebot.io/schemas'
|
||||
import { JumpBlock } from '@typebot.io/schemas/features/blocks/logic/jump'
|
||||
|
||||
export const executeJumpBlock = (
|
||||
state: SessionState,
|
||||
{ groupId, blockId }: JumpBlock['options']
|
||||
): ExecuteLogicResponse => {
|
||||
const { typebot } = state.typebotsQueue[0]
|
||||
const groupToJumpTo = typebot.groups.find((group) => group.id === groupId)
|
||||
const blockToJumpTo =
|
||||
groupToJumpTo?.blocks.find((block) => block.id === blockId) ??
|
||||
groupToJumpTo?.blocks[0]
|
||||
|
||||
if (!blockToJumpTo?.groupId)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Block to jump to is not found',
|
||||
})
|
||||
|
||||
const portalEdge = createPortalEdge({
|
||||
to: { groupId: blockToJumpTo?.groupId, blockId: blockToJumpTo?.id },
|
||||
})
|
||||
const newSessionState = addEdgeToTypebot(state, portalEdge)
|
||||
|
||||
return { outgoingEdgeId: portalEdge.id, newSessionState }
|
||||
}
|
||||
21
packages/bot-engine/blocks/logic/redirect/executeRedirect.ts
Normal file
21
packages/bot-engine/blocks/logic/redirect/executeRedirect.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { RedirectBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { sanitizeUrl } from '@typebot.io/lib'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
|
||||
export const executeRedirect = (
|
||||
state: SessionState,
|
||||
block: RedirectBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
|
||||
return {
|
||||
clientSideActions: [
|
||||
{
|
||||
redirect: { url: formattedUrl, isNewTab: block.options.isNewTab },
|
||||
},
|
||||
],
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
}
|
||||
}
|
||||
47
packages/bot-engine/blocks/logic/script/executeScript.ts
Normal file
47
packages/bot-engine/blocks/logic/script/executeScript.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'
|
||||
import { extractVariablesFromText } from '../../../variables/extractVariablesFromText'
|
||||
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
|
||||
export const executeScript = (
|
||||
state: SessionState,
|
||||
block: ScriptBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
if (!block.options.content || state.whatsApp)
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
|
||||
const scriptToExecute = parseScriptToExecuteClientSideAction(
|
||||
variables,
|
||||
block.options.content
|
||||
)
|
||||
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
clientSideActions: [
|
||||
{
|
||||
scriptToExecute: scriptToExecute,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export const parseScriptToExecuteClientSideAction = (
|
||||
variables: Variable[],
|
||||
contentToEvaluate: string
|
||||
) => {
|
||||
const content = parseVariables(variables, { fieldToParse: 'id' })(
|
||||
contentToEvaluate
|
||||
)
|
||||
const args = extractVariablesFromText(variables)(contentToEvaluate).map(
|
||||
(variable) => ({
|
||||
id: variable.id,
|
||||
value: parseGuessedValueType(variable.value),
|
||||
})
|
||||
)
|
||||
return {
|
||||
content,
|
||||
args,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { SessionState, SetVariableBlock, Variable } from '@typebot.io/schemas'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
|
||||
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
|
||||
export const executeSetVariable = (
|
||||
state: SessionState,
|
||||
block: SetVariableBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
if (!block.options?.variableId)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
}
|
||||
const expressionToEvaluate = getExpressionToEvaluate(state)(block.options)
|
||||
const isCustomValue = !block.options.type || block.options.type === 'Custom'
|
||||
if (
|
||||
expressionToEvaluate &&
|
||||
!state.whatsApp &&
|
||||
((isCustomValue && block.options.isExecutedOnClient) ||
|
||||
block.options.type === 'Moment of the day')
|
||||
) {
|
||||
const scriptToExecute = parseScriptToExecuteClientSideAction(
|
||||
variables,
|
||||
expressionToEvaluate
|
||||
)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
clientSideActions: [
|
||||
{
|
||||
setVariable: {
|
||||
scriptToExecute,
|
||||
},
|
||||
expectsDedicatedReply: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
const evaluatedExpression = expressionToEvaluate
|
||||
? evaluateSetVariableExpression(variables)(expressionToEvaluate)
|
||||
: undefined
|
||||
const existingVariable = variables.find(byId(block.options.variableId))
|
||||
if (!existingVariable) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const newVariable = {
|
||||
...existingVariable,
|
||||
value: evaluatedExpression,
|
||||
}
|
||||
const newSessionState = updateVariablesInSession(state)([newVariable])
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
newSessionState,
|
||||
}
|
||||
}
|
||||
|
||||
const evaluateSetVariableExpression =
|
||||
(variables: Variable[]) =>
|
||||
(str: string): unknown => {
|
||||
const isSingleVariable =
|
||||
str.startsWith('{{') && str.endsWith('}}') && str.split('{{').length === 2
|
||||
if (isSingleVariable) return parseVariables(variables)(str)
|
||||
const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
|
||||
str.includes('return ') ? str : `return ${str}`
|
||||
)
|
||||
try {
|
||||
const func = Function(...variables.map((v) => v.id), evaluating)
|
||||
return func(...variables.map((v) => parseGuessedValueType(v.value)))
|
||||
} catch (err) {
|
||||
return parseVariables(variables)(str)
|
||||
}
|
||||
}
|
||||
|
||||
const getExpressionToEvaluate =
|
||||
(state: SessionState) =>
|
||||
(options: SetVariableBlock['options']): string | null => {
|
||||
switch (options.type) {
|
||||
case 'Contact name':
|
||||
return state.whatsApp?.contact.name ?? ''
|
||||
case 'Phone number':
|
||||
return `"${state.whatsApp?.contact.phoneNumber}"` ?? ''
|
||||
case 'Now':
|
||||
case 'Today':
|
||||
return 'new Date().toISOString()'
|
||||
case 'Tomorrow': {
|
||||
return 'new Date(Date.now() + 86400000).toISOString()'
|
||||
}
|
||||
case 'Yesterday': {
|
||||
return 'new Date(Date.now() - 86400000).toISOString()'
|
||||
}
|
||||
case 'Random ID': {
|
||||
return 'Math.random().toString(36).substring(2, 15)'
|
||||
}
|
||||
case 'User ID': {
|
||||
return (
|
||||
state.typebotsQueue[0].resultId ??
|
||||
'Math.random().toString(36).substring(2, 15)'
|
||||
)
|
||||
}
|
||||
case 'Map item with same index': {
|
||||
return `const itemIndex = ${options.mapListItemParams?.baseListVariableId}.indexOf(${options.mapListItemParams?.baseItemVariableId})
|
||||
return ${options.mapListItemParams?.targetListVariableId}.at(itemIndex)`
|
||||
}
|
||||
case 'Empty': {
|
||||
return null
|
||||
}
|
||||
case 'Moment of the day': {
|
||||
return `const now = new Date()
|
||||
if(now.getHours() < 12) return 'morning'
|
||||
if(now.getHours() >= 12 && now.getHours() < 18) return 'afternoon'
|
||||
if(now.getHours() >= 18) return 'evening'
|
||||
if(now.getHours() >= 22 || now.getHours() < 6) return 'night'`
|
||||
}
|
||||
case 'Custom':
|
||||
case undefined: {
|
||||
return options.expressionToEvaluate ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { addEdgeToTypebot, createPortalEdge } from '../../../addEdgeToTypebot'
|
||||
import {
|
||||
TypebotLinkBlock,
|
||||
SessionState,
|
||||
Variable,
|
||||
ReplyLog,
|
||||
Edge,
|
||||
typebotInSessionStateSchema,
|
||||
TypebotInSession,
|
||||
} from '@typebot.io/schemas'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { isNotDefined } from '@typebot.io/lib/utils'
|
||||
import { createResultIfNotExist } from '../../../queries/createResultIfNotExist'
|
||||
import { executeJumpBlock } from '../jump/executeJumpBlock'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
|
||||
export const executeTypebotLink = async (
|
||||
state: SessionState,
|
||||
block: TypebotLinkBlock
|
||||
): Promise<ExecuteLogicResponse> => {
|
||||
const logs: ReplyLog[] = []
|
||||
const typebotId = block.options.typebotId
|
||||
if (
|
||||
typebotId === 'current' ||
|
||||
typebotId === state.typebotsQueue[0].typebot.id
|
||||
) {
|
||||
return executeJumpBlock(state, {
|
||||
groupId: block.options.groupId,
|
||||
})
|
||||
}
|
||||
if (!typebotId) {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Failed to link typebot`,
|
||||
details: `Typebot ID is not specified`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
const linkedTypebot = await fetchTypebot(state, typebotId)
|
||||
if (!linkedTypebot) {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Failed to link typebot`,
|
||||
details: `Typebot with ID ${block.options.typebotId} not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
let newSessionState = await addLinkedTypebotToState(
|
||||
state,
|
||||
block,
|
||||
linkedTypebot
|
||||
)
|
||||
|
||||
const nextGroupId =
|
||||
block.options.groupId ??
|
||||
linkedTypebot.groups.find((group) =>
|
||||
group.blocks.some((block) => block.type === 'start')
|
||||
)?.id
|
||||
if (!nextGroupId) {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Failed to link typebot`,
|
||||
details: `Group with ID "${block.options.groupId}" not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
|
||||
const portalEdge = createPortalEdge({ to: { groupId: nextGroupId } })
|
||||
|
||||
newSessionState = addEdgeToTypebot(newSessionState, portalEdge)
|
||||
|
||||
return {
|
||||
outgoingEdgeId: portalEdge.id,
|
||||
newSessionState,
|
||||
}
|
||||
}
|
||||
|
||||
const addLinkedTypebotToState = async (
|
||||
state: SessionState,
|
||||
block: TypebotLinkBlock,
|
||||
linkedTypebot: TypebotInSession
|
||||
): Promise<SessionState> => {
|
||||
const currentTypebotInQueue = state.typebotsQueue[0]
|
||||
const isPreview = isNotDefined(currentTypebotInQueue.resultId)
|
||||
|
||||
const resumeEdge = createResumeEdgeIfNecessary(state, block)
|
||||
|
||||
const currentTypebotWithResumeEdge = resumeEdge
|
||||
? {
|
||||
...currentTypebotInQueue,
|
||||
typebot: {
|
||||
...currentTypebotInQueue.typebot,
|
||||
edges: [...currentTypebotInQueue.typebot.edges, resumeEdge],
|
||||
},
|
||||
}
|
||||
: currentTypebotInQueue
|
||||
|
||||
const shouldMergeResults = block.options.mergeResults !== false
|
||||
|
||||
if (
|
||||
currentTypebotInQueue.resultId &&
|
||||
currentTypebotInQueue.answers.length === 0 &&
|
||||
shouldMergeResults
|
||||
) {
|
||||
await createResultIfNotExist({
|
||||
resultId: currentTypebotInQueue.resultId,
|
||||
typebot: currentTypebotInQueue.typebot,
|
||||
hasStarted: false,
|
||||
isCompleted: false,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
typebotsQueue: [
|
||||
{
|
||||
typebot: {
|
||||
...linkedTypebot,
|
||||
variables: fillVariablesWithExistingValues(
|
||||
linkedTypebot.variables,
|
||||
currentTypebotInQueue.typebot.variables
|
||||
),
|
||||
},
|
||||
resultId: isPreview
|
||||
? undefined
|
||||
: shouldMergeResults
|
||||
? currentTypebotInQueue.resultId
|
||||
: createId(),
|
||||
edgeIdToTriggerWhenDone: block.outgoingEdgeId ?? resumeEdge?.id,
|
||||
answers: shouldMergeResults ? currentTypebotInQueue.answers : [],
|
||||
isMergingWithParent: shouldMergeResults,
|
||||
},
|
||||
currentTypebotWithResumeEdge,
|
||||
...state.typebotsQueue.slice(1),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const createResumeEdgeIfNecessary = (
|
||||
state: SessionState,
|
||||
block: TypebotLinkBlock
|
||||
): Edge | undefined => {
|
||||
const currentTypebotInQueue = state.typebotsQueue[0]
|
||||
const blockId = block.id
|
||||
if (block.outgoingEdgeId) return
|
||||
const currentGroup = currentTypebotInQueue.typebot.groups.find((group) =>
|
||||
group.blocks.some((block) => block.id === blockId)
|
||||
)
|
||||
if (!currentGroup) return
|
||||
const currentBlockIndex = currentGroup.blocks.findIndex(
|
||||
(block) => block.id === blockId
|
||||
)
|
||||
const nextBlockInGroup =
|
||||
currentBlockIndex === -1
|
||||
? undefined
|
||||
: currentGroup.blocks[currentBlockIndex + 1]
|
||||
if (!nextBlockInGroup) return
|
||||
return {
|
||||
id: createId(),
|
||||
from: {
|
||||
groupId: '',
|
||||
blockId: '',
|
||||
},
|
||||
to: {
|
||||
groupId: nextBlockInGroup.groupId,
|
||||
blockId: nextBlockInGroup.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const fillVariablesWithExistingValues = (
|
||||
emptyVariables: Variable[],
|
||||
existingVariables: Variable[]
|
||||
): Variable[] =>
|
||||
emptyVariables.map((emptyVariable) => {
|
||||
const matchedVariable = existingVariables.find(
|
||||
(existingVariable) => existingVariable.name === emptyVariable.name
|
||||
)
|
||||
|
||||
return {
|
||||
...emptyVariable,
|
||||
value: matchedVariable?.value,
|
||||
}
|
||||
})
|
||||
|
||||
const fetchTypebot = async (state: SessionState, typebotId: string) => {
|
||||
const { resultId } = state.typebotsQueue[0]
|
||||
const isPreview = !resultId
|
||||
if (isPreview) {
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: { id: typebotId },
|
||||
select: {
|
||||
version: true,
|
||||
id: true,
|
||||
edges: true,
|
||||
groups: true,
|
||||
variables: true,
|
||||
},
|
||||
})
|
||||
return typebotInSessionStateSchema.parse(typebot)
|
||||
}
|
||||
const typebot = await prisma.publicTypebot.findUnique({
|
||||
where: { typebotId },
|
||||
select: {
|
||||
version: true,
|
||||
id: true,
|
||||
edges: true,
|
||||
groups: true,
|
||||
variables: true,
|
||||
},
|
||||
})
|
||||
if (!typebot) return null
|
||||
return typebotInSessionStateSchema.parse({
|
||||
...typebot,
|
||||
id: typebotId,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { User } from '@typebot.io/prisma'
|
||||
|
||||
type Props = {
|
||||
isPreview?: boolean
|
||||
typebotIds: string[]
|
||||
user?: User
|
||||
}
|
||||
|
||||
export const fetchLinkedTypebots = async ({
|
||||
user,
|
||||
isPreview,
|
||||
typebotIds,
|
||||
}: Props) => {
|
||||
if (!user || !isPreview)
|
||||
return prisma.publicTypebot.findMany({
|
||||
where: { id: { in: typebotIds } },
|
||||
})
|
||||
const linkedTypebots = await prisma.typebot.findMany({
|
||||
where: { id: { in: typebotIds } },
|
||||
include: {
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
select: {
|
||||
members: {
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return linkedTypebots.filter(
|
||||
(typebot) =>
|
||||
typebot.collaborators.some(
|
||||
(collaborator) => collaborator.userId === user.id
|
||||
) || typebot.workspace.members.some((member) => member.userId === user.id)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { User } from '@typebot.io/prisma'
|
||||
import {
|
||||
LogicBlockType,
|
||||
PublicTypebot,
|
||||
Typebot,
|
||||
TypebotLinkBlock,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { fetchLinkedTypebots } from './fetchLinkedTypebots'
|
||||
|
||||
type Props = {
|
||||
typebots: Pick<PublicTypebot, 'groups'>[]
|
||||
user?: User
|
||||
isPreview?: boolean
|
||||
}
|
||||
|
||||
export const getPreviouslyLinkedTypebots =
|
||||
({ typebots, user, isPreview }: Props) =>
|
||||
async (
|
||||
capturedLinkedBots: (Typebot | PublicTypebot)[]
|
||||
): Promise<(Typebot | PublicTypebot)[]> => {
|
||||
const linkedTypebotIds = typebots
|
||||
.flatMap((typebot) =>
|
||||
(
|
||||
typebot.groups
|
||||
.flatMap((group) => group.blocks)
|
||||
.filter(
|
||||
(block) =>
|
||||
block.type === LogicBlockType.TYPEBOT_LINK &&
|
||||
isDefined(block.options.typebotId) &&
|
||||
!capturedLinkedBots.some(
|
||||
(bot) =>
|
||||
('typebotId' in bot ? bot.typebotId : bot.id) ===
|
||||
block.options.typebotId
|
||||
)
|
||||
) as TypebotLinkBlock[]
|
||||
).map((s) => s.options.typebotId)
|
||||
)
|
||||
.filter(isDefined)
|
||||
if (linkedTypebotIds.length === 0) return capturedLinkedBots
|
||||
const linkedTypebots = (await fetchLinkedTypebots({
|
||||
user,
|
||||
typebotIds: linkedTypebotIds,
|
||||
isPreview,
|
||||
})) as (Typebot | PublicTypebot)[]
|
||||
return getPreviouslyLinkedTypebots({
|
||||
typebots: linkedTypebots,
|
||||
user,
|
||||
isPreview,
|
||||
})([...capturedLinkedBots, ...linkedTypebots])
|
||||
}
|
||||
32
packages/bot-engine/blocks/logic/wait/executeWait.ts
Normal file
32
packages/bot-engine/blocks/logic/wait/executeWait.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { SessionState, WaitBlock } from '@typebot.io/schemas'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
|
||||
export const executeWait = (
|
||||
state: SessionState,
|
||||
block: WaitBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
if (!block.options.secondsToWaitFor)
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const parsedSecondsToWaitFor = safeParseInt(
|
||||
parseVariables(variables)(block.options.secondsToWaitFor)
|
||||
)
|
||||
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
clientSideActions: parsedSecondsToWaitFor
|
||||
? [
|
||||
{
|
||||
wait: { secondsToWaitFor: parsedSecondsToWaitFor },
|
||||
expectsDedicatedReply: block.options.shouldPause,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const safeParseInt = (value: string) => {
|
||||
const parsedValue = parseInt(value)
|
||||
return isNaN(parsedValue) ? undefined : parsedValue
|
||||
}
|
||||
Reference in New Issue
Block a user