2
0

♻️ (viewer) Remove barrel exports and flatten folder arch

This commit is contained in:
Baptiste Arnaud
2023-03-15 12:21:52 +01:00
parent 44d7a0bcb8
commit f3af07b7ff
113 changed files with 398 additions and 426 deletions

View File

@@ -0,0 +1,19 @@
import { createId } from '@paralleldrive/cuid2'
import { SessionState, Edge } from '@typebot.io/schemas'
export const addEdgeToTypebot = (
state: SessionState,
edge: Edge
): SessionState => ({
...state,
typebot: {
...state.typebot,
edges: [...state.typebot.edges, edge],
},
})
export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({
id: createId(),
from: { blockId: '', groupId: '' },
to,
})

View File

@@ -0,0 +1,264 @@
import prisma from '@/lib/prisma'
import { TRPCError } from '@trpc/server'
import { Prisma } from '@typebot.io/prisma'
import got from 'got'
import {
Block,
BlockType,
BubbleBlockType,
ChatReply,
InputBlock,
InputBlockType,
ResultInSession,
SessionState,
} from '@typebot.io/schemas'
import { isInputBlock, isNotDefined } from '@typebot.io/lib'
import { executeGroup } from './executeGroup'
import { getNextGroup } from './getNextGroup'
import { validateEmail } from '@/features/blocks/inputs/email/validateEmail'
import { formatPhoneNumber } from '@/features/blocks/inputs/phone/formatPhoneNumber'
import { validatePhoneNumber } from '@/features/blocks/inputs/phone/validatePhoneNumber'
import { validateUrl } from '@/features/blocks/inputs/url/validateUrl'
import { updateVariables } from '@/features/variables/updateVariables'
import { parseVariables } from '@/features/variables/parseVariables'
export const continueBotFlow =
(state: SessionState) =>
async (
reply?: string
): Promise<ChatReply & { newSessionState?: SessionState }> => {
const group = state.typebot.groups.find(
(group) => group.id === state.currentBlock?.groupId
)
const blockIndex =
group?.blocks.findIndex(
(block) => block.id === state.currentBlock?.blockId
) ?? -1
const block = blockIndex >= 0 ? group?.blocks[blockIndex ?? 0] : null
if (!block || !group)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Current block not found',
})
if (!isInputBlock(block))
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Current block is not an input block',
})
if (reply && !isReplyValid(reply, block)) return parseRetryMessage(block)
const formattedReply = formatReply(reply, block.type)
if (!formattedReply && !canSkip(block.type)) {
return parseRetryMessage(block)
}
const newSessionState = await processAndSaveAnswer(
state,
block
)(formattedReply)
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply)
if (groupHasMoreBlocks && !nextEdgeId) {
return executeGroup(newSessionState)({
...group,
blocks: group.blocks.slice(blockIndex + 1),
})
}
if (!nextEdgeId && state.linkedTypebots.queue.length === 0)
return { messages: [] }
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
if (!nextGroup) return { messages: [] }
return executeGroup(newSessionState)(nextGroup.group)
}
const processAndSaveAnswer =
(state: SessionState, block: InputBlock) =>
async (reply: string | null): Promise<SessionState> => {
if (!reply) return state
let newState = await saveAnswer(state, block)(reply)
newState = await saveVariableValueIfAny(newState, block)(reply)
return newState
}
const saveVariableValueIfAny =
(state: SessionState, block: InputBlock) =>
async (reply: string): Promise<SessionState> => {
if (!block.options.variableId) return state
const foundVariable = state.typebot.variables.find(
(variable) => variable.id === block.options.variableId
)
if (!foundVariable) return state
const newSessionState = await updateVariables(state)([
{
...foundVariable,
value: Array.isArray(foundVariable.value)
? foundVariable.value.concat(reply)
: reply,
},
])
return newSessionState
}
export const setResultAsCompleted = async (resultId: string) => {
await prisma.result.update({
where: { id: resultId },
data: { isCompleted: true },
})
}
const parseRetryMessage = (
block: InputBlock
): Pick<ChatReply, 'messages' | 'input'> => {
const retryMessage =
'retryMessageContent' in block.options && block.options.retryMessageContent
? block.options.retryMessageContent
: 'Invalid message. Please, try again.'
return {
messages: [
{
id: block.id,
type: BubbleBlockType.TEXT,
content: {
plainText: retryMessage,
html: `<div>${retryMessage}</div>`,
},
},
],
input: block,
}
}
const saveAnswer =
(state: SessionState, block: InputBlock) =>
async (reply: string): Promise<SessionState> => {
const resultId = state.result?.id
const answer = {
resultId,
blockId: block.id,
groupId: block.groupId,
content: reply,
variableId: block.options.variableId,
storageUsed: 0,
}
if (state.result.answers.length === 0 && state.result.id)
await setResultAsStarted(state.result.id)
const newSessionState = setNewAnswerInState(state)({
blockId: block.id,
variableId: block.options.variableId ?? null,
content: reply,
})
if (resultId) {
if (reply.includes('http') && block.type === InputBlockType.FILE) {
answer.storageUsed = await computeStorageUsed(reply)
}
await prisma.answer.upsert({
where: {
resultId_blockId_groupId: {
resultId,
groupId: block.groupId,
blockId: block.id,
},
},
create: answer as Prisma.AnswerUncheckedCreateInput,
update: answer,
})
}
return newSessionState
}
const setResultAsStarted = async (resultId: string) => {
await prisma.result.update({
where: { id: resultId },
data: { hasStarted: true },
})
}
const setNewAnswerInState =
(state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => {
const newAnswers = state.result.answers
.filter((answer) => answer.blockId !== newAnswer.blockId)
.concat(newAnswer)
return {
...state,
result: {
...state.result,
answers: newAnswers,
},
} satisfies SessionState
}
const computeStorageUsed = async (reply: string) => {
let storageUsed = 0
const fileUrls = reply.split(', ')
const hasReachedStorageLimit = fileUrls[0] === null
if (!hasReachedStorageLimit) {
for (const url of fileUrls) {
const { headers } = await got(url)
const size = headers['content-length']
if (isNotDefined(size)) continue
storageUsed += parseInt(size, 10)
}
}
return storageUsed
}
const getOutgoingEdgeId =
({ typebot: { variables } }: Pick<SessionState, 'typebot'>) =>
(block: InputBlock, reply: string | null) => {
if (
block.type === InputBlockType.CHOICE &&
!block.options.isMultipleChoice &&
reply
) {
const matchedItem = block.items.find(
(item) => parseVariables(variables)(item.content) === reply
)
if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId
}
return block.outgoingEdgeId
}
export const formatReply = (
inputValue: string | undefined,
blockType: BlockType
): string | null => {
if (!inputValue) return null
switch (blockType) {
case InputBlockType.PHONE:
return formatPhoneNumber(inputValue)
}
return inputValue
}
export const isReplyValid = (inputValue: string, block: Block): boolean => {
switch (block.type) {
case InputBlockType.EMAIL:
return validateEmail(inputValue)
case InputBlockType.PHONE:
return validatePhoneNumber(inputValue)
case InputBlockType.URL:
return validateUrl(inputValue)
}
return true
}
export const canSkip = (inputType: InputBlockType) =>
inputType === InputBlockType.FILE

View File

@@ -0,0 +1,176 @@
import {
BubbleBlock,
BubbleBlockType,
ChatReply,
Group,
InputBlock,
InputBlockType,
RuntimeOptions,
SessionState,
} from '@typebot.io/schemas'
import {
isBubbleBlock,
isDefined,
isInputBlock,
isIntegrationBlock,
isLogicBlock,
} from '@typebot.io/lib'
import { executeLogic } from './executeLogic'
import { getNextGroup } from './getNextGroup'
import { executeIntegration } from './executeIntegration'
import { injectVariableValuesInButtonsInputBlock } from '@/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock'
import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { computePaymentInputRuntimeOptions } from '@/features/blocks/inputs/payment/computePaymentInputRuntimeOptions'
export const executeGroup =
(
state: SessionState,
currentReply?: ChatReply,
currentLastBubbleId?: string
) =>
async (
group: Group
): Promise<ChatReply & { newSessionState: SessionState }> => {
const messages: ChatReply['messages'] = currentReply?.messages ?? []
let clientSideActions: ChatReply['clientSideActions'] =
currentReply?.clientSideActions
let logs: ChatReply['logs'] = currentReply?.logs
let nextEdgeId = null
let lastBubbleBlockId: string | undefined = currentLastBubbleId
let newSessionState = state
for (const block of group.blocks) {
nextEdgeId = block.outgoingEdgeId
if (isBubbleBlock(block)) {
messages.push(
parseBubbleBlock(newSessionState.typebot.variables)(block)
)
lastBubbleBlockId = block.id
continue
}
if (isInputBlock(block))
return {
messages,
input: await injectVariablesValueInBlock(newSessionState)(block),
newSessionState: {
...newSessionState,
currentBlock: {
groupId: group.id,
blockId: block.id,
},
},
clientSideActions,
logs,
}
const executionResponse = isLogicBlock(block)
? await executeLogic(newSessionState, lastBubbleBlockId)(block)
: isIntegrationBlock(block)
? await executeIntegration(newSessionState, lastBubbleBlockId)(block)
: null
if (!executionResponse) continue
if (
'clientSideActions' in executionResponse &&
executionResponse.clientSideActions
)
clientSideActions = [
...(clientSideActions ?? []),
...executionResponse.clientSideActions,
]
if (executionResponse.logs)
logs = [...(logs ?? []), ...executionResponse.logs]
if (executionResponse.newSessionState)
newSessionState = executionResponse.newSessionState
if (executionResponse.outgoingEdgeId) {
nextEdgeId = executionResponse.outgoingEdgeId
break
}
}
if (!nextEdgeId)
return { messages, newSessionState, clientSideActions, logs }
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
if (nextGroup?.updatedContext) newSessionState = nextGroup.updatedContext
if (!nextGroup) {
return { messages, newSessionState, clientSideActions, logs }
}
return executeGroup(
newSessionState,
{
messages,
clientSideActions,
logs,
},
lastBubbleBlockId
)(nextGroup.group)
}
const computeRuntimeOptions =
(state: Pick<SessionState, 'result' | 'typebot'>) =>
(block: InputBlock): Promise<RuntimeOptions> | undefined => {
switch (block.type) {
case InputBlockType.PAYMENT: {
return computePaymentInputRuntimeOptions(state)(block.options)
}
}
}
const getPrefilledInputValue =
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
const variableValue = variables.find(
(variable) =>
variable.id === block.options.variableId && isDefined(variable.value)
)?.value
if (!variableValue || Array.isArray(variableValue)) return
return variableValue
}
const parseBubbleBlock =
(variables: SessionState['typebot']['variables']) =>
(block: BubbleBlock): ChatReply['messages'][0] => {
switch (block.type) {
case BubbleBlockType.EMBED: {
const message = deepParseVariables(variables)(block)
return {
...message,
content: {
...message.content,
height:
typeof message.content.height === 'string'
? parseFloat(message.content.height)
: message.content.height,
},
}
}
default:
return deepParseVariables(variables)(block)
}
}
const injectVariablesValueInBlock =
(state: Pick<SessionState, 'result' | 'typebot'>) =>
async (block: InputBlock): Promise<ChatReply['input']> => {
switch (block.type) {
case InputBlockType.CHOICE: {
return injectVariableValuesInButtonsInputBlock(state.typebot.variables)(
block
)
}
default: {
return deepParseVariables(state.typebot.variables)({
...block,
runtimeOptions: await computeRuntimeOptions(state)(block),
prefilledValue: getPrefilledInputValue(state.typebot.variables)(
block
),
})
}
}
}

View File

@@ -0,0 +1,34 @@
import { executeOpenAIBlock } from '@/features/blocks/integrations/openai/executeOpenAIBlock'
import { executeSendEmailBlock } from '@/features/blocks/integrations/sendEmail/executeSendEmailBlock'
import { executeWebhookBlock } from '@/features/blocks/integrations/webhook/executeWebhookBlock'
import { executeChatwootBlock } from '@/features/blocks/integrations/chatwoot/executeChatwootBlock'
import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock'
import { executeGoogleSheetBlock } from '@/features/blocks/integrations/googleSheets/executeGoogleSheetBlock'
import {
IntegrationBlock,
IntegrationBlockType,
SessionState,
} from '@typebot.io/schemas'
import { ExecuteIntegrationResponse } from '../types'
export const executeIntegration =
(state: SessionState, lastBubbleBlockId?: string) =>
async (block: IntegrationBlock): Promise<ExecuteIntegrationResponse> => {
switch (block.type) {
case IntegrationBlockType.GOOGLE_SHEETS:
return executeGoogleSheetBlock(state, block)
case IntegrationBlockType.CHATWOOT:
return executeChatwootBlock(state, block, lastBubbleBlockId)
case IntegrationBlockType.GOOGLE_ANALYTICS:
return executeGoogleAnalyticsBlock(state, block, lastBubbleBlockId)
case IntegrationBlockType.EMAIL:
return executeSendEmailBlock(state, block)
case IntegrationBlockType.WEBHOOK:
case IntegrationBlockType.ZAPIER:
case IntegrationBlockType.MAKE_COM:
case IntegrationBlockType.PABBLY_CONNECT:
return executeWebhookBlock(state, block)
case IntegrationBlockType.OPEN_AI:
return executeOpenAIBlock(state, block)
}
}

View File

@@ -0,0 +1,30 @@
import { executeWait } from '@/features/blocks/logic/wait/executeWait'
import { LogicBlock, LogicBlockType, SessionState } from '@typebot.io/schemas'
import { ExecuteLogicResponse } from '../types'
import { executeScript } from '@/features/blocks/logic/script/executeScript'
import { executeJumpBlock } from '@/features/blocks/logic/jump/executeJumpBlock'
import { executeRedirect } from '@/features/blocks/logic/redirect/executeRedirect'
import { executeCondition } from '@/features/blocks/logic/condition/executeCondition'
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/executeTypebotLink'
export const executeLogic =
(state: SessionState, lastBubbleBlockId?: string) =>
async (block: LogicBlock): Promise<ExecuteLogicResponse> => {
switch (block.type) {
case LogicBlockType.SET_VARIABLE:
return executeSetVariable(state, block)
case LogicBlockType.CONDITION:
return executeCondition(state, block)
case LogicBlockType.REDIRECT:
return executeRedirect(state, block, lastBubbleBlockId)
case LogicBlockType.SCRIPT:
return executeScript(state, block, lastBubbleBlockId)
case LogicBlockType.TYPEBOT_LINK:
return executeTypebotLink(state, block)
case LogicBlockType.WAIT:
return executeWait(state, block, lastBubbleBlockId)
case LogicBlockType.JUMP:
return executeJumpBlock(state, block.options)
}
}

View File

@@ -0,0 +1,38 @@
import { byId } from '@typebot.io/lib'
import { Group, SessionState } from '@typebot.io/schemas'
export type NextGroup = {
group: Group
updatedContext?: SessionState
}
export const getNextGroup =
(state: SessionState) =>
(edgeId?: string): NextGroup | null => {
const { typebot } = state
const nextEdge = typebot.edges.find(byId(edgeId))
if (!nextEdge) {
if (state.linkedTypebots.queue.length > 0) {
const nextEdgeId = state.linkedTypebots.queue[0].edgeId
const updatedContext = {
...state,
linkedBotQueue: state.linkedTypebots.queue.slice(1),
}
const nextGroup = getNextGroup(updatedContext)(nextEdgeId)
if (!nextGroup) return null
return {
...nextGroup,
updatedContext,
}
}
return null
}
const nextGroup = typebot.groups.find(byId(nextEdge.to.groupId))
if (!nextGroup) return null
const startBlockIndex = nextEdge.to.blockId
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
: 0
return {
group: { ...nextGroup, blocks: nextGroup.blocks.slice(startBlockIndex) },
}
}

View File

@@ -0,0 +1,12 @@
import prisma from '@/lib/prisma'
import { ChatSession } from '@typebot.io/schemas'
export const getSession = async (
sessionId: string
): Promise<Pick<ChatSession, 'state' | 'id'> | null> => {
const session = (await prisma.chatSession.findUnique({
where: { id: sessionId },
select: { id: true, state: true },
})) as Pick<ChatSession, 'state' | 'id'> | null
return session
}

View File

@@ -0,0 +1,5 @@
export * from './continueBotFlow'
export * from './executeGroup'
export * from './getNextGroup'
export * from './getSessionState'
export * from './startBotFlow'

View File

@@ -0,0 +1,26 @@
import { TRPCError } from '@trpc/server'
import { ChatReply, SessionState } from '@typebot.io/schemas'
import { executeGroup } from './executeGroup'
import { getNextGroup } from './getNextGroup'
export const startBotFlow = async (
state: SessionState,
startGroupId?: string
): Promise<ChatReply & { newSessionState: SessionState }> => {
if (startGroupId) {
const group = state.typebot.groups.find(
(group) => group.id === startGroupId
)
if (!group)
throw new TRPCError({
code: 'BAD_REQUEST',
message: "startGroupId doesn't exist",
})
return executeGroup(state)(group)
}
const firstEdgeId = state.typebot.groups[0].blocks[0].outgoingEdgeId
if (!firstEdgeId) return { messages: [], newSessionState: state }
const nextGroup = getNextGroup(state)(firstEdgeId)
if (!nextGroup) return { messages: [], newSessionState: state }
return executeGroup(state)(nextGroup.group)
}