2
0

Regroup database queries of /sendMessage in one place

Potentially reduces the total queries to database and it will help to migrate to Edge runtime
This commit is contained in:
Baptiste Arnaud
2023-07-18 14:31:20 +02:00
parent 1095cf7f09
commit aa4c16dad7
32 changed files with 520 additions and 482 deletions

View File

@ -12,7 +12,7 @@ import { filterChoiceItems } from './filterChoiceItems'
export const injectVariableValuesInButtonsInputBlock =
(state: SessionState) =>
async (block: ChoiceInputBlock): Promise<ChoiceInputBlock> => {
(block: ChoiceInputBlock): ChoiceInputBlock => {
if (block.options.dynamicVariableId) {
const variable = state.typebot.variables.find(
(variable) =>
@ -20,7 +20,7 @@ export const injectVariableValuesInButtonsInputBlock =
isDefined(variable.value)
) as VariableWithValue | undefined
if (!variable) return block
const value = await getVariableValue(state)(variable)
const value = getVariableValue(state)(variable)
return {
...block,
items: value.filter(isDefined).map((item, idx) => ({
@ -38,12 +38,12 @@ export const injectVariableValuesInButtonsInputBlock =
const getVariableValue =
(state: SessionState) =>
async (variable: VariableWithValue): Promise<(string | null)[]> => {
(variable: VariableWithValue): (string | null)[] => {
if (!Array.isArray(variable.value)) {
const [transformedVariable] = transformStringVariablesToList(
state.typebot.variables
)([variable.id])
await updateVariables(state)([transformedVariable])
updateVariables(state)([transformedVariable])
return transformedVariable.value as string[]
}
return variable.value

View File

@ -7,11 +7,9 @@ import {
import { isNotEmpty, byId } from '@typebot.io/lib'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { updateVariables } from '@/features/variables/updateVariables'
import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { matchFilter } from './helpers/matchFilter'
import { saveInfoLog } from '@/features/logs/saveInfoLog'
export const getRow = async (
state: SessionState,
@ -20,15 +18,13 @@ export const getRow = async (
options,
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
): Promise<ExecuteIntegrationResponse> => {
const logs: ReplyLog[] = []
const { sheetId, cellsToExtract, referenceCell, filter } = deepParseVariables(
state.typebot.variables
)(options)
if (!sheetId) return { outgoingEdgeId }
let log: ReplyLog | undefined
const variables = state.typebot.variables
const resultId = state.result?.id
const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId,
@ -48,16 +44,12 @@ export const getRow = async (
)
)
if (filteredRows.length === 0) {
log = {
logs.push({
status: 'info',
description: `Couldn't find any rows matching the filter`,
details: JSON.stringify(filter, null, 2),
}
await saveInfoLog({
resultId,
message: log.description,
})
return { outgoingEdgeId, logs: log ? [log] : undefined }
return { outgoingEdgeId, logs }
}
const extractingColumns = cellsToExtract
.map((cell) => cell.column)
@ -85,24 +77,19 @@ export const getRow = async (
},
[]
)
const newSessionState = await updateVariables(state)(newVariables)
const newSessionState = updateVariables(state)(newVariables)
return {
outgoingEdgeId,
newSessionState,
}
} catch (err) {
log = {
logs.push({
status: 'error',
description: `An error occurred while fetching the spreadsheet data`,
details: err,
}
await saveErrorLog({
resultId,
message: log.description,
details: err,
})
}
return { outgoingEdgeId, logs: log ? [log] : undefined }
return { outgoingEdgeId, logs }
}
const getTotalRows = <T>(

View File

@ -6,11 +6,9 @@ import {
import { parseCellValues } from './helpers/parseCellValues'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
export const insertRow = async (
{ result, typebot: { variables } }: SessionState,
{ typebot: { variables } }: SessionState,
{
outgoingEdgeId,
options,
@ -18,7 +16,7 @@ export const insertRow = async (
): Promise<ExecuteIntegrationResponse> => {
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
let log: ReplyLog | undefined
const logs: ReplyLog[] = []
const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId,
@ -31,27 +29,17 @@ export const insertRow = async (
await doc.loadInfo()
const sheet = doc.sheetsById[Number(options.sheetId)]
await sheet.addRow(parsedValues)
log = {
logs.push({
status: 'success',
description: `Succesfully inserted row in ${doc.title} > ${sheet.title}`,
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: log?.description,
}))
})
} catch (err) {
log = {
logs.push({
status: 'error',
description: `An error occured while inserting the row`,
details: err,
}
result &&
(await saveErrorLog({
resultId: result.id,
message: log.description,
details: err,
}))
})
}
return { outgoingEdgeId, logs: log ? [log] : undefined }
return { outgoingEdgeId, logs }
}

View File

@ -7,13 +7,10 @@ import { parseCellValues } from './helpers/parseCellValues'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { matchFilter } from './helpers/matchFilter'
import { saveInfoLog } from '@/features/logs/saveInfoLog'
export const updateRow = async (
{ result, typebot: { variables } }: SessionState,
{ typebot: { variables } }: SessionState,
{
outgoingEdgeId,
options,
@ -24,7 +21,7 @@ export const updateRow = async (
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))
return { outgoingEdgeId }
let log: ReplyLog | undefined
const logs: ReplyLog[] = []
const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId,
@ -42,18 +39,12 @@ export const updateRow = async (
: matchFilter(row, filter as NonNullable<typeof filter>)
)
if (filteredRows.length === 0) {
log = {
logs.push({
status: 'info',
description: `Could not find any row that matches the filter`,
details: JSON.stringify(filter, null, 2),
}
result &&
(await saveInfoLog({
resultId: result.id,
message: log.description,
details: log.details,
}))
return { outgoingEdgeId, logs: log ? [log] : undefined }
details: filter,
})
return { outgoingEdgeId, logs }
}
try {
@ -65,28 +56,17 @@ export const updateRow = async (
await rows[rowIndex].save()
}
log = log = {
logs.push({
status: 'success',
description: `Succesfully updated matching rows`,
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: log.description,
}))
})
} catch (err) {
console.log(err)
log = {
logs.push({
status: 'error',
description: `An error occured while updating the row`,
details: err,
}
result &&
(await saveErrorLog({
resultId: result.id,
message: log.description,
details: err,
}))
})
}
return { outgoingEdgeId, logs: log ? [log] : undefined }
return { outgoingEdgeId, logs }
}

View File

@ -50,7 +50,7 @@ export const createChatCompletionOpenAI = async (
newSessionState.typebot.variables
)(options.messages)
if (variablesTransformedToList.length > 0)
newSessionState = await updateVariables(state)(variablesTransformedToList)
newSessionState = updateVariables(state)(variablesTransformedToList)
const temperature = parseVariableNumber(newSessionState.typebot.variables)(
options.advancedSettings?.temperature

View File

@ -79,6 +79,7 @@ export const executeChatCompletionOpenAIRequest = async ({
logs.push({
status: 'error',
description: `Internal error`,
details: error,
})
return { logs }
}

View File

@ -1,4 +1,3 @@
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { updateVariables } from '@/features/variables/updateVariables'
import { byId, isDefined } from '@typebot.io/lib'
import { ChatReply, SessionState } from '@typebot.io/schemas'
@ -11,7 +10,7 @@ export const resumeChatCompletion =
{
outgoingEdgeId,
options,
logs,
logs = [],
}: {
outgoingEdgeId?: string
options: ChatCompletionOpenAIOptions
@ -44,12 +43,11 @@ export const resumeChatCompletion =
return newVariables
}, [])
if (newVariables.length > 0)
newSessionState = await updateVariables(newSessionState)(newVariables)
state.result &&
(await saveSuccessLog({
resultId: state.result.id,
message: 'OpenAI block successfully executed',
}))
newSessionState = updateVariables(newSessionState)(newVariables)
logs.push({
description: 'OpenAI block successfully executed',
status: 'success',
})
return {
outgoingEdgeId,
newSessionState,

View File

@ -4,6 +4,7 @@ import { render } from '@faire/mjml-react/utils/render'
import { DefaultBotNotificationEmail } from '@typebot.io/emails'
import {
PublicTypebot,
ReplyLog,
ResultInSession,
SendEmailBlock,
SendEmailOptions,
@ -18,14 +19,13 @@ import { parseAnswers } from '@typebot.io/lib/results'
import { decrypt } from '@typebot.io/lib/api'
import { defaultFrom, defaultTransportOptions } from './constants'
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
export const executeSendEmailBlock = async (
{ result, typebot }: SessionState,
block: SendEmailBlock
): Promise<ExecuteIntegrationResponse> => {
const logs: ReplyLog[] = []
const { options } = block
const { variables } = typebot
const isPreview = !result.id
@ -45,7 +45,7 @@ export const executeSendEmailBlock = async (
parseVariables(variables, { escapeHtml: true })(options.body ?? '')
try {
await sendEmail({
const sendEmailLogs = await sendEmail({
typebotId: typebot.id,
result,
credentialsId: options.credentialsId,
@ -61,17 +61,16 @@ export const executeSendEmailBlock = async (
isCustomBody: options.isCustomBody,
isBodyCode: options.isBodyCode,
})
if (sendEmailLogs) logs.push(...sendEmailLogs)
} catch (err) {
await saveErrorLog({
resultId: result.id,
message: 'Email not sent',
details: {
error: err,
},
logs.push({
status: 'error',
details: err,
description: `Email not sent`,
})
}
return { outgoingEdgeId: block.outgoingEdgeId }
return { outgoingEdgeId: block.outgoingEdgeId, logs }
}
const sendEmail = async ({
@ -91,7 +90,8 @@ const sendEmail = async ({
typebotId: string
result: ResultInSession
fileUrls?: string | string[]
}) => {
}): Promise<ReplyLog[] | undefined> => {
const logs: ReplyLog[] = []
const { name: replyToName } = parseEmailRecipient(replyTo)
const { host, port, isTlsEnabled, username, password, from } =
@ -117,9 +117,9 @@ const sendEmail = async ({
})
if (!emailBody) {
await saveErrorLog({
resultId: result.id,
message: 'Email not sent',
logs.push({
status: 'error',
description: 'Email not sent',
details: {
error: 'No email body found',
transportConfig,
@ -131,6 +131,7 @@ const sendEmail = async ({
emailBody,
},
})
return logs
}
const transporter = createTransport(transportConfig)
const fromName = isEmpty(replyToName) ? from.name : replyToName
@ -150,9 +151,9 @@ const sendEmail = async ({
}
try {
await transporter.sendMail(email)
await saveSuccessLog({
resultId: result.id,
message: 'Email successfully sent',
logs.push({
status: 'success',
description: 'Email successfully sent',
details: {
transportConfig: {
...transportConfig,
@ -162,11 +163,11 @@ const sendEmail = async ({
},
})
} catch (err) {
await saveErrorLog({
resultId: result.id,
message: 'Email not sent',
logs.push({
status: 'error',
description: 'Email not sent',
details: {
error: err,
error: err instanceof Error ? err.toString() : err,
transportConfig: {
...transportConfig,
auth: { user: transportConfig.auth.user, pass: '******' },
@ -175,6 +176,8 @@ const sendEmail = async ({
},
})
}
return logs
}
const getEmailInfo = async (

View File

@ -24,8 +24,6 @@ import { parseAnswers } from '@typebot.io/lib/results'
import got, { Method, HTTPError, OptionsInit } from 'got'
import { parseSampleResult } from './parseSampleResult'
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { parseVariables } from '@/features/variables/parseVariables'
import { resumeWebhookExecution } from './resumeWebhookExecution'
@ -39,21 +37,16 @@ export const executeWebhookBlock = async (
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
): Promise<ExecuteIntegrationResponse> => {
const { typebot, result } = state
let log: ReplyLog | undefined
const logs: ReplyLog[] = []
const webhook = (await prisma.webhook.findUnique({
where: { id: block.webhookId },
})) as Webhook | null
if (!webhook) {
log = {
logs.push({
status: 'error',
description: `Couldn't find webhook with id ${block.webhookId}`,
}
result &&
(await saveErrorLog({
resultId: result.id,
message: log.description,
}))
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
})
return { outgoingEdgeId: block.outgoingEdgeId, logs }
}
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const parsedWebhook = await parseWebhookAttributes(
@ -62,16 +55,11 @@ export const executeWebhookBlock = async (
result
)(preparedWebhook)
if (!parsedWebhook) {
log = {
logs.push({
status: 'error',
description: `Couldn't parse webhook attributes`,
}
result &&
(await saveErrorLog({
resultId: result.id,
message: log.description,
}))
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
})
return { outgoingEdgeId: block.outgoingEdgeId, logs }
}
if (block.options.isExecutedOnClient)
return {
@ -82,8 +70,14 @@ export const executeWebhookBlock = async (
},
],
}
const webhookResponse = await executeWebhook(parsedWebhook, result)
return resumeWebhookExecution(state, block)(webhookResponse)
const { response: webhookResponse, logs: executeWebhookLogs } =
await executeWebhook(parsedWebhook)
return resumeWebhookExecution({
state,
block,
logs: executeWebhookLogs,
response: webhookResponse,
})
}
const prepareWebhookAttributes = (
@ -162,9 +156,9 @@ const parseWebhookAttributes =
}
export const executeWebhook = async (
webhook: ParsedWebhook,
result: ResultInSession
): Promise<WebhookResponse> => {
webhook: ParsedWebhook
): Promise<{ response: WebhookResponse; logs?: ReplyLog[] }> => {
const logs: ReplyLog[] = []
const { headers, url, method, basicAuth, body, isJson } = webhook
const contentType = headers ? headers['Content-Type'] : undefined
@ -183,9 +177,9 @@ export const executeWebhook = async (
} satisfies OptionsInit
try {
const response = await got(request.url, omit(request, 'url'))
await saveSuccessLog({
resultId: result.id,
message: 'Webhook successfuly executed.',
logs.push({
status: 'success',
description: `Webhook successfuly executed.`,
details: {
statusCode: response.statusCode,
request,
@ -193,8 +187,11 @@ export const executeWebhook = async (
},
})
return {
statusCode: response.statusCode,
data: safeJsonParse(response.body).data,
response: {
statusCode: response.statusCode,
data: safeJsonParse(response.body).data,
},
logs,
}
} catch (error) {
if (error instanceof HTTPError) {
@ -202,30 +199,31 @@ export const executeWebhook = async (
statusCode: error.response.statusCode,
data: safeJsonParse(error.response.body as string).data,
}
await saveErrorLog({
resultId: result.id,
message: 'Webhook returned an error',
logs.push({
status: 'error',
description: `Webhook returned an error.`,
details: {
statusCode: error.response.statusCode,
request,
response,
},
})
return response
return { response, logs }
}
const response = {
statusCode: 500,
data: { message: `Error from Typebot server: ${error}` },
}
console.error(error)
await saveErrorLog({
resultId: result.id,
message: 'Webhook failed to execute',
logs.push({
status: 'error',
description: `Webhook failed to execute.`,
details: {
request,
response,
},
})
return response
return { response, logs }
}
}

View File

@ -1,6 +1,4 @@
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { parseVariables } from '@/features/variables/parseVariables'
import { updateVariables } from '@/features/variables/updateVariables'
import { byId } from '@typebot.io/lib'
@ -13,75 +11,71 @@ import {
} from '@typebot.io/schemas'
import { ReplyLog, SessionState } from '@typebot.io/schemas/features/chat'
export const resumeWebhookExecution =
(
state: SessionState,
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
) =>
async (response: {
type Props = {
state: SessionState
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
logs?: ReplyLog[]
response: {
statusCode: number
data?: unknown
}): Promise<ExecuteIntegrationResponse> => {
const { typebot, result } = state
let log: ReplyLog | undefined
const status = response.statusCode.toString()
const isError = status.startsWith('4') || status.startsWith('5')
}
}
if (isError) {
log = {
status: 'error',
description: `Webhook returned error: ${response.data}`,
details: JSON.stringify(response.data, null, 2).substring(0, 1000),
}
result &&
(await saveErrorLog({
resultId: result.id,
message: log.description,
details: log.details,
}))
} else {
log = {
status: 'success',
description: `Webhook executed successfully!`,
details: JSON.stringify(response.data, null, 2).substring(0, 1000),
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: log.description,
details: JSON.stringify(response.data, null, 2).substring(0, 1000),
}))
export const resumeWebhookExecution = ({
state,
block,
logs = [],
response,
}: Props): ExecuteIntegrationResponse => {
const { typebot } = state
const status = response.statusCode.toString()
const isError = status.startsWith('4') || status.startsWith('5')
const responseFromClient = logs.length === 0
if (responseFromClient)
logs.push(
isError
? {
status: 'error',
description: `Webhook returned error`,
details: response.data,
}
: {
status: 'success',
description: `Webhook executed successfully!`,
details: response.data,
}
)
const newVariables = block.options.responseVariableMapping.reduce<
VariableWithUnknowValue[]
>((newVariables, varMapping) => {
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
const existingVariable = typebot.variables.find(byId(varMapping.variableId))
if (!existingVariable) return newVariables
const func = Function(
'data',
`return data.${parseVariables(typebot.variables)(varMapping?.bodyPath)}`
)
try {
const value: unknown = func(response)
return [...newVariables, { ...existingVariable, value }]
} catch (err) {
return newVariables
}
const newVariables = block.options.responseVariableMapping.reduce<
VariableWithUnknowValue[]
>((newVariables, varMapping) => {
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
const existingVariable = typebot.variables.find(
byId(varMapping.variableId)
)
if (!existingVariable) return newVariables
const func = Function(
'data',
`return data.${parseVariables(typebot.variables)(varMapping?.bodyPath)}`
)
try {
const value: unknown = func(response)
return [...newVariables, { ...existingVariable, value }]
} catch (err) {
return newVariables
}
}, [])
if (newVariables.length > 0) {
const newSessionState = await updateVariables(state)(newVariables)
return {
outgoingEdgeId: block.outgoingEdgeId,
newSessionState,
}
}
}, [])
if (newVariables.length > 0) {
const newSessionState = updateVariables(state)(newVariables)
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: log ? [log] : undefined,
newSessionState,
logs,
}
}
return {
outgoingEdgeId: block.outgoingEdgeId,
logs,
}
}

View File

@ -63,5 +63,5 @@ test('should execute webhooks properly', async ({ page }) => {
await expect(
page.locator('text="Webhook successfuly executed." >> nth=1')
).toBeVisible()
await expect(page.locator('text="Webhook returned an error"')).toBeVisible()
await expect(page.locator('text="Webhook returned an error."')).toBeVisible()
})

View File

@ -6,10 +6,10 @@ import { parseVariables } from '@/features/variables/parseVariables'
import { parseGuessedValueType } from '@/features/variables/parseGuessedValueType'
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
export const executeSetVariable = async (
export const executeSetVariable = (
state: SessionState,
block: SetVariableBlock
): Promise<ExecuteLogicResponse> => {
): ExecuteLogicResponse => {
const { variables } = state.typebot
if (!block.options?.variableId)
return {
@ -48,7 +48,7 @@ export const executeSetVariable = async (
...existingVariable,
value: evaluatedExpression,
}
const newSessionState = await updateVariables(state)([newVariable])
const newSessionState = updateVariables(state)([newVariable])
return {
outgoingEdgeId: block.outgoingEdgeId,
newSessionState,

View File

@ -8,33 +8,32 @@ import {
SessionState,
TypebotInSession,
Variable,
ReplyLog,
} from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
export const executeTypebotLink = async (
state: SessionState,
block: TypebotLinkBlock
): Promise<ExecuteLogicResponse> => {
const logs: ReplyLog[] = []
if (!block.options.typebotId) {
state.result &&
saveErrorLog({
resultId: state.result.id,
message: 'Failed to link typebot',
details: 'Typebot ID is not specified',
})
return { outgoingEdgeId: block.outgoingEdgeId }
logs.push({
status: 'error',
description: `Failed to link typebot`,
details: `Typebot ID is not specified`,
})
return { outgoingEdgeId: block.outgoingEdgeId, logs }
}
const linkedTypebot = await getLinkedTypebot(state, block.options.typebotId)
if (!linkedTypebot) {
state.result &&
saveErrorLog({
resultId: state.result.id,
message: 'Failed to link typebot',
details: `Typebot with ID ${block.options.typebotId} not found`,
})
return { outgoingEdgeId: block.outgoingEdgeId }
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 = addLinkedTypebotToState(state, block, linkedTypebot)
@ -43,13 +42,12 @@ export const executeTypebotLink = async (
linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start'))
?.id
if (!nextGroupId) {
state.result &&
saveErrorLog({
resultId: state.result.id,
message: 'Failed to link typebot',
details: `Group with ID "${block.options.groupId}" not found`,
})
return { outgoingEdgeId: block.outgoingEdgeId }
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 } })

View File

@ -2,10 +2,10 @@ import { ExecuteLogicResponse } from '@/features/chat/types'
import { parseVariables } from '@/features/variables/parseVariables'
import { SessionState, WaitBlock } from '@typebot.io/schemas'
export const executeWait = async (
export const executeWait = (
{ typebot: { variables } }: SessionState,
block: WaitBlock
): Promise<ExecuteLogicResponse> => {
): ExecuteLogicResponse => {
if (!block.options.secondsToWaitFor)
return { outgoingEdgeId: block.outgoingEdgeId }
const parsedSecondsToWaitFor = safeParseInt(

View File

@ -1,14 +1,12 @@
import prisma from '@/lib/prisma'
import { publicProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Prisma } from '@typebot.io/prisma'
import {
ChatReply,
chatReplySchema,
ChatSession,
GoogleAnalyticsBlock,
IntegrationBlockType,
PixelBlock,
ReplyLog,
ResultInSession,
sendMessageInputSchema,
SessionState,
@ -19,19 +17,20 @@ import {
Variable,
VariableWithValue,
} from '@typebot.io/schemas'
import {
continueBotFlow,
getSession,
setResultAsCompleted,
startBotFlow,
} from '../helpers'
import { env, isDefined, isNotEmpty, omit } from '@typebot.io/lib'
import { prefillVariables } from '@/features/variables/prefillVariables'
import { injectVariablesFromExistingResult } from '@/features/variables/injectVariablesFromExistingResult'
import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { parseVariables } from '@/features/variables/parseVariables'
import { saveLog } from '@/features/logs/saveLog'
import { NodeType, parse } from 'node-html-parser'
import { saveStateToDatabase } from '../helpers/saveStateToDatabase'
import { getSession } from '../queries/getSession'
import { continueBotFlow } from '../helpers/continueBotFlow'
import { startBotFlow } from '../helpers/startBotFlow'
import { findTypebot } from '../queries/findTypebot'
import { findPublicTypebot } from '../queries/findPublicTypebot'
import { findResult } from '../queries/findResult'
import { createId } from '@paralleldrive/cuid2'
export const sendMessage = publicProcedure
.meta({
@ -53,17 +52,6 @@ export const sendMessage = publicProcedure
}) => {
const session = sessionId ? await getSession(sessionId) : null
if (clientLogs) {
for (const log of clientLogs) {
await saveLog({
message: log.description,
status: log.status as 'error' | 'success' | 'info',
resultId: session?.state.result.id,
details: log.details,
})
}
}
if (!session) {
const {
sessionId,
@ -74,7 +62,7 @@ export const sendMessage = publicProcedure
dynamicTheme,
logs,
clientSideActions,
} = await startSession(startParams, user?.id)
} = await startSession(startParams, user?.id, clientLogs)
return {
sessionId,
typebot: typebot
@ -95,24 +83,18 @@ export const sendMessage = publicProcedure
const { messages, input, clientSideActions, newSessionState, logs } =
await continueBotFlow(session.state)(message)
const containsSetVariableClientSideAction = clientSideActions?.some(
(action) => 'setVariable' in action
)
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
if (
!input &&
!containsSetVariableClientSideAction &&
session.state.result.answers.length > 0 &&
session.state.result.id
)
await setResultAsCompleted(session.state.result.id)
await prisma.chatSession.updateMany({
where: { id: session.id },
data: {
state: newSessionState,
},
})
if (newSessionState)
await saveStateToDatabase({
session: {
id: session.id,
state: newSessionState,
},
input,
logs: allLogs,
clientSideActions,
})
return {
messages,
@ -125,7 +107,11 @@ export const sendMessage = publicProcedure
}
)
const startSession = async (startParams?: StartParams, userId?: string) => {
const startSession = async (
startParams?: StartParams,
userId?: string,
clientLogs?: ReplyLog[]
) => {
if (!startParams)
throw new TRPCError({
code: 'BAD_REQUEST',
@ -231,11 +217,16 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
logs: startLogs.length > 0 ? startLogs : undefined,
}
const session = (await prisma.chatSession.create({
data: {
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
const session = await saveStateToDatabase({
session: {
state: newSessionState,
},
})) as ChatSession
input,
logs: allLogs,
clientSideActions,
})
return {
resultId: result?.id,
@ -270,45 +261,8 @@ const getTypebot = async (
'You need to authenticate the request to start a bot in preview mode.',
})
const typebotQuery = isPreview
? await prisma.typebot.findFirst({
where: { id: typebot, workspace: { members: { some: { userId } } } },
select: {
id: true,
groups: true,
edges: true,
settings: true,
theme: true,
variables: true,
isArchived: true,
},
})
: await prisma.publicTypebot.findFirst({
where: { typebot: { publicId: typebot } },
select: {
groups: true,
edges: true,
settings: true,
theme: true,
variables: true,
typebotId: true,
typebot: {
select: {
isArchived: true,
isClosed: true,
workspace: {
select: {
id: true,
plan: true,
additionalChatsIndex: true,
customChatsLimit: true,
isQuarantined: true,
isSuspended: true,
},
},
},
},
},
})
? await findTypebot({ id: typebot, userId })
: await findPublicTypebot({ publicId: typebot })
const parsedTypebot =
typebotQuery && 'typebot' in typebotQuery
@ -347,7 +301,6 @@ const getTypebot = async (
}
const getResult = async ({
typebotId,
isPreview,
resultId,
prefilledVariables,
@ -358,56 +311,31 @@ const getResult = async ({
isRememberUserEnabled: boolean
}) => {
if (isPreview) return
const select = {
id: true,
variables: true,
answers: { select: { blockId: true, variableId: true, content: true } },
} satisfies Prisma.ResultSelect
const existingResult =
resultId && isRememberUserEnabled
? ((await prisma.result.findFirst({
where: { id: resultId },
select,
})) as ResultInSession)
? ((await findResult({ id: resultId })) as ResultInSession)
: undefined
if (existingResult) {
const prefilledVariableWithValue = prefilledVariables.filter(
(prefilledVariable) => isDefined(prefilledVariable.value)
)
const updatedResult = {
variables: prefilledVariableWithValue.concat(
existingResult.variables.filter(
(resultVariable) =>
isDefined(resultVariable.value) &&
!prefilledVariableWithValue.some(
(prefilledVariable) =>
prefilledVariable.name === resultVariable.name
)
)
) as VariableWithValue[],
}
await prisma.result.updateMany({
where: { id: existingResult.id },
data: updatedResult,
})
return {
id: existingResult.id,
variables: updatedResult.variables,
answers: existingResult.answers,
}
} else {
return (await prisma.result.create({
data: {
isCompleted: false,
typebotId,
variables: prefilledVariables.filter((variable) =>
isDefined(variable.value)
),
},
select,
})) as ResultInSession
const prefilledVariableWithValue = prefilledVariables.filter(
(prefilledVariable) => isDefined(prefilledVariable.value)
)
const updatedResult = {
variables: prefilledVariableWithValue.concat(
existingResult?.variables.filter(
(resultVariable) =>
isDefined(resultVariable.value) &&
!prefilledVariableWithValue.some(
(prefilledVariable) =>
prefilledVariable.name === resultVariable.name
)
) ?? []
) as VariableWithValue[],
}
return {
id: existingResult?.id ?? createId(),
variables: updatedResult.variables,
answers: existingResult?.answers,
}
}

View File

@ -1,7 +1,4 @@
import prisma from '@/lib/prisma'
import { TRPCError } from '@trpc/server'
import { Prisma } from '@typebot.io/prisma'
import got from 'got'
import {
Block,
BlockType,
@ -16,7 +13,7 @@ import {
SetVariableBlock,
WebhookBlock,
} from '@typebot.io/schemas'
import { isInputBlock, isNotDefined, byId } from '@typebot.io/lib'
import { isInputBlock, byId } from '@typebot.io/lib'
import { executeGroup } from './executeGroup'
import { getNextGroup } from './getNextGroup'
import { validateEmail } from '@/features/blocks/inputs/email/validateEmail'
@ -28,6 +25,7 @@ import { parseVariables } from '@/features/variables/parseVariables'
import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { resumeChatCompletion } from '@/features/blocks/integrations/openai/resumeChatCompletion'
import { resumeWebhookExecution } from '@/features/blocks/integrations/webhook/resumeWebhookExecution'
import { upsertAnswer } from '../queries/upsertAnswer'
export const continueBotFlow =
(state: SessionState) =>
@ -60,13 +58,14 @@ export const continueBotFlow =
...existingVariable,
value: reply,
}
newSessionState = await updateVariables(state)([newVariable])
newSessionState = updateVariables(state)([newVariable])
}
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
const result = await resumeWebhookExecution(
const result = resumeWebhookExecution({
state,
block
)(JSON.parse(reply))
block,
response: JSON.parse(reply),
})
if (result.newSessionState) newSessionState = result.newSessionState
} else if (
block.type === IntegrationBlockType.OPEN_AI &&
@ -136,20 +135,20 @@ const processAndSaveAnswer =
async (reply: string | null): Promise<SessionState> => {
if (!reply) return state
let newState = await saveAnswer(state, block, itemId)(reply)
newState = await saveVariableValueIfAny(newState, block)(reply)
newState = saveVariableValueIfAny(newState, block)(reply)
return newState
}
const saveVariableValueIfAny =
(state: SessionState, block: InputBlock) =>
async (reply: string): Promise<SessionState> => {
(reply: string): 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)([
const newSessionState = updateVariables(state)([
{
...foundVariable,
value: Array.isArray(foundVariable.value)
@ -161,13 +160,6 @@ const saveVariableValueIfAny =
return newSessionState
}
export const setResultAsCompleted = async (resultId: string) => {
await prisma.result.updateMany({
where: { id: resultId },
data: { isCompleted: true },
})
}
const parseRetryMessage = (
block: InputBlock
): Pick<ChatReply, 'messages' | 'input'> => {
@ -192,55 +184,28 @@ const parseRetryMessage = (
const saveAnswer =
(state: SessionState, block: InputBlock, itemId?: string) =>
async (reply: string): Promise<SessionState> => {
const resultId = state.result?.id
const answer: Omit<Prisma.AnswerUncheckedCreateInput, 'resultId'> = {
blockId: block.id,
await upsertAnswer({
block,
answer: {
blockId: block.id,
itemId,
groupId: block.groupId,
content: reply,
variableId: block.options.variableId,
storageUsed: 0,
},
reply,
state,
itemId,
groupId: block.groupId,
content: reply,
variableId: block.options.variableId,
storageUsed: 0,
}
if (state.result.answers.length === 0 && resultId)
await setResultAsStarted(resultId)
})
const newSessionState = setNewAnswerInState(state)({
return 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,
blockId: block.id,
groupId: block.groupId,
},
},
create: { ...answer, resultId },
update: {
content: answer.content,
storageUsed: answer.storageUsed,
itemId: answer.itemId,
},
})
}
return newSessionState
}
const setResultAsStarted = async (resultId: string) => {
await prisma.result.updateMany({
where: { id: resultId },
data: { hasStarted: true },
})
}
const setNewAnswerInState =
(state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => {
const newAnswers = state.result.answers
@ -256,21 +221,6 @@ const setNewAnswerInState =
} 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'>) =>
(

View File

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

View File

@ -0,0 +1,48 @@
import { ChatReply, ChatSession } from '@typebot.io/schemas'
import { upsertResult } from '../queries/upsertResult'
import { saveLogs } from '../queries/saveLogs'
import { updateSession } from '../queries/updateSession'
import { formatLogDetails } from '@/features/logs/helpers/formatLogDetails'
import { createSession } from '../queries/createSession'
type Props = {
session: Pick<ChatSession, 'state'> & { id?: string }
input: ChatReply['input']
logs: ChatReply['logs']
clientSideActions: ChatReply['clientSideActions']
}
export const saveStateToDatabase = async ({
session: { state, id },
input,
logs,
clientSideActions,
}: Props) => {
if (id) await updateSession({ id, state })
const session = id ? { state, id } : await createSession({ state })
if (!state?.result?.id) return session
const containsSetVariableClientSideAction = clientSideActions?.some(
(action) => 'setVariable' in action
)
await upsertResult({
state,
isCompleted: Boolean(
!input &&
!containsSetVariableClientSideAction &&
state.result.answers.length > 0
),
})
if (logs && logs.length > 0)
await saveLogs(
logs.map((log) => ({
...log,
resultId: state.result.id as string,
details: formatLogDetails(log.details),
}))
)
return session
}

View File

@ -0,0 +1,13 @@
import prisma from '@/lib/prisma'
import { SessionState } from '@typebot.io/schemas'
type Props = {
state: SessionState
}
export const createSession = async ({ state }: Props) =>
prisma.chatSession.create({
data: {
state,
},
})

View File

@ -0,0 +1,34 @@
import prisma from '@/lib/prisma'
type Props = {
publicId: string
}
export const findPublicTypebot = ({ publicId }: Props) =>
prisma.publicTypebot.findFirst({
where: { typebot: { publicId } },
select: {
groups: true,
edges: true,
settings: true,
theme: true,
variables: true,
typebotId: true,
typebot: {
select: {
isArchived: true,
isClosed: true,
workspace: {
select: {
id: true,
plan: true,
additionalChatsIndex: true,
customChatsLimit: true,
isQuarantined: true,
isSuspended: true,
},
},
},
},
},
})

View File

@ -0,0 +1,14 @@
import prisma from '@/lib/prisma'
type Props = {
id: string
}
export const findResult = ({ id }: Props) =>
prisma.result.findFirst({
where: { id },
select: {
id: true,
variables: true,
answers: { select: { blockId: true, variableId: true, content: true } },
},
})

View File

@ -0,0 +1,20 @@
import prisma from '@/lib/prisma'
type Props = {
id: string
userId?: string
}
export const findTypebot = ({ id, userId }: Props) =>
prisma.typebot.findFirst({
where: { id, workspace: { members: { some: { userId } } } },
select: {
id: true,
groups: true,
edges: true,
settings: true,
theme: true,
variables: true,
isArchived: true,
},
})

View File

@ -0,0 +1,5 @@
import prisma from '@/lib/prisma'
import { Log } from '@typebot.io/schemas'
export const saveLogs = (logs: Omit<Log, 'id' | 'createdAt'>[]) =>
prisma.log.createMany({ data: logs })

View File

@ -0,0 +1,15 @@
import prisma from '@/lib/prisma'
import { SessionState } from '@typebot.io/schemas'
type Props = {
id: string
state: SessionState
}
export const updateSession = async ({ id, state }: Props) =>
prisma.chatSession.updateMany({
where: { id },
data: {
state,
},
})

View File

@ -0,0 +1,57 @@
import prisma from '@/lib/prisma'
import { isNotDefined } from '@typebot.io/lib'
import { Prisma } from '@typebot.io/prisma'
import { InputBlock, InputBlockType, SessionState } from '@typebot.io/schemas'
import got from 'got'
type Props = {
answer: Omit<Prisma.AnswerUncheckedCreateInput, 'resultId'>
block: InputBlock
reply: string
itemId?: string
state: SessionState
}
export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
if (!state.result?.id) return
if (reply.includes('http') && block.type === InputBlockType.FILE) {
answer.storageUsed = await computeStorageUsed(reply)
}
const where = {
resultId: state.result.id,
blockId: block.id,
groupId: block.groupId,
}
const existingAnswer = await prisma.answer.findUnique({
where: {
resultId_blockId_groupId: where,
},
select: { resultId: true },
})
if (existingAnswer)
return prisma.answer.updateMany({
where,
data: {
content: answer.content,
storageUsed: answer.storageUsed,
itemId: answer.itemId,
},
})
return prisma.answer.createMany({
data: [{ ...answer, resultId: state.result.id }],
})
}
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
}

View File

@ -0,0 +1,34 @@
import prisma from '@/lib/prisma'
import { SessionState } from '@typebot.io/schemas'
type Props = {
state: SessionState
isCompleted: boolean
}
export const upsertResult = async ({ state, isCompleted }: Props) => {
const existingResult = await prisma.result.findUnique({
where: { id: state.result.id },
select: { id: true },
})
if (existingResult) {
return prisma.result.updateMany({
where: { id: state.result.id },
data: {
isCompleted: isCompleted ? true : undefined,
hasStarted: state.result.answers.length > 0 ? true : undefined,
variables: state.result.variables,
},
})
}
return prisma.result.createMany({
data: [
{
id: state.result.id,
typebotId: state.typebot.id,
isCompleted: isCompleted ? true : false,
hasStarted: state.result.answers.length > 0 ? true : undefined,
variables: state.result.variables,
},
],
})
}

View File

@ -0,0 +1,11 @@
import { isNotDefined } from '@typebot.io/lib/utils'
export const formatLogDetails = (details: unknown): string | null => {
if (isNotDefined(details)) return null
if (details instanceof Error) return details.toString()
try {
return JSON.stringify(details, null, 2).substring(0, 1000)
} catch {
return null
}
}

View File

@ -1,11 +0,0 @@
import { saveLog } from './saveLog'
export const saveInfoLog = ({
resultId,
message,
details,
}: {
resultId: string | undefined
message: string
details?: unknown
}) => saveLog({ status: 'info', resultId, message, details })

View File

@ -1,5 +1,5 @@
import prisma from '@/lib/prisma'
import { isNotDefined } from '@typebot.io/lib'
import { formatLogDetails } from './helpers/formatLogDetails'
type Props = {
status: 'error' | 'success' | 'info'
@ -15,16 +15,7 @@ export const saveLog = ({ status, resultId, message, details }: Props) => {
resultId,
status,
description: message,
details: formatDetails(details) as string | null,
details: formatLogDetails(details) as string | null,
},
})
}
const formatDetails = (details: unknown) => {
if (isNotDefined(details)) return null
try {
return JSON.stringify(details, null, 2).substring(0, 1000)
} catch {
return details
}
}

View File

@ -1,4 +1,3 @@
import prisma from '@/lib/prisma'
import { isDefined } from '@typebot.io/lib'
import {
SessionState,
@ -10,7 +9,7 @@ import { safeStringify } from './safeStringify'
export const updateVariables =
(state: SessionState) =>
async (newVariables: VariableWithUnknowValue[]): Promise<SessionState> => ({
(newVariables: VariableWithUnknowValue[]): SessionState => ({
...state,
typebot: {
...state.typebot,
@ -18,15 +17,13 @@ export const updateVariables =
},
result: {
...state.result,
variables: await updateResultVariables(state)(newVariables),
variables: updateResultVariables(state)(newVariables),
},
})
const updateResultVariables =
({ result }: Pick<SessionState, 'result' | 'typebot'>) =>
async (
newVariables: VariableWithUnknowValue[]
): Promise<VariableWithValue[]> => {
(newVariables: VariableWithUnknowValue[]): VariableWithValue[] => {
const serializedNewVariables = newVariables.map((variable) => ({
...variable,
value: Array.isArray(variable.value)
@ -43,16 +40,6 @@ const updateResultVariables =
...serializedNewVariables,
].filter((variable) => isDefined(variable.value)) as VariableWithValue[]
if (result.id)
await prisma.result.updateMany({
where: {
id: result.id,
},
data: {
variables: updatedVariables,
},
})
return updatedVariables
}