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

@@ -27,6 +27,6 @@ test.describe.parallel('Templates page', () => {
await page.click('text=Customer Support') await page.click('text=Customer Support')
await expect(page.locator('text=How can I help you?')).toBeVisible() await expect(page.locator('text=How can I help you?')).toBeVisible()
await page.click('text=Use this template') await page.click('text=Use this template')
await expect(page).toHaveURL(new RegExp(`/edit`), { timeout: 10000 }) await expect(page).toHaveURL(new RegExp(`/edit`), { timeout: 20000 })
}) })
}) })

View File

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

View File

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

View File

@@ -6,11 +6,9 @@ import {
import { parseCellValues } from './helpers/parseCellValues' import { parseCellValues } from './helpers/parseCellValues'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc' import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { ExecuteIntegrationResponse } from '@/features/chat/types' import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
export const insertRow = async ( export const insertRow = async (
{ result, typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
{ {
outgoingEdgeId, outgoingEdgeId,
options, options,
@@ -18,7 +16,7 @@ export const insertRow = async (
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId } if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
let log: ReplyLog | undefined const logs: ReplyLog[] = []
const doc = await getAuthenticatedGoogleDoc({ const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId, credentialsId: options.credentialsId,
@@ -31,27 +29,17 @@ export const insertRow = async (
await doc.loadInfo() await doc.loadInfo()
const sheet = doc.sheetsById[Number(options.sheetId)] const sheet = doc.sheetsById[Number(options.sheetId)]
await sheet.addRow(parsedValues) await sheet.addRow(parsedValues)
log = { logs.push({
status: 'success', status: 'success',
description: `Succesfully inserted row in ${doc.title} > ${sheet.title}`, description: `Succesfully inserted row in ${doc.title} > ${sheet.title}`,
} })
result &&
(await saveSuccessLog({
resultId: result.id,
message: log?.description,
}))
} catch (err) { } catch (err) {
log = { logs.push({
status: 'error', status: 'error',
description: `An error occured while inserting the row`, description: `An error occured while inserting the row`,
details: err, 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 { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { deepParseVariables } from '@/features/variables/deepParseVariable' import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { ExecuteIntegrationResponse } from '@/features/chat/types' import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { matchFilter } from './helpers/matchFilter' import { matchFilter } from './helpers/matchFilter'
import { saveInfoLog } from '@/features/logs/saveInfoLog'
export const updateRow = async ( export const updateRow = async (
{ result, typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
{ {
outgoingEdgeId, outgoingEdgeId,
options, options,
@@ -24,7 +21,7 @@ export const updateRow = async (
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter)) if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))
return { outgoingEdgeId } return { outgoingEdgeId }
let log: ReplyLog | undefined const logs: ReplyLog[] = []
const doc = await getAuthenticatedGoogleDoc({ const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId, credentialsId: options.credentialsId,
@@ -42,18 +39,12 @@ export const updateRow = async (
: matchFilter(row, filter as NonNullable<typeof filter>) : matchFilter(row, filter as NonNullable<typeof filter>)
) )
if (filteredRows.length === 0) { if (filteredRows.length === 0) {
log = { logs.push({
status: 'info', status: 'info',
description: `Could not find any row that matches the filter`, description: `Could not find any row that matches the filter`,
details: JSON.stringify(filter, null, 2), details: filter,
} })
result && return { outgoingEdgeId, logs }
(await saveInfoLog({
resultId: result.id,
message: log.description,
details: log.details,
}))
return { outgoingEdgeId, logs: log ? [log] : undefined }
} }
try { try {
@@ -65,28 +56,17 @@ export const updateRow = async (
await rows[rowIndex].save() await rows[rowIndex].save()
} }
log = log = { logs.push({
status: 'success', status: 'success',
description: `Succesfully updated matching rows`, description: `Succesfully updated matching rows`,
} })
result &&
(await saveSuccessLog({
resultId: result.id,
message: log.description,
}))
} catch (err) { } catch (err) {
console.log(err) console.log(err)
log = { logs.push({
status: 'error', status: 'error',
description: `An error occured while updating the row`, description: `An error occured while updating the row`,
details: err, 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 newSessionState.typebot.variables
)(options.messages) )(options.messages)
if (variablesTransformedToList.length > 0) if (variablesTransformedToList.length > 0)
newSessionState = await updateVariables(state)(variablesTransformedToList) newSessionState = updateVariables(state)(variablesTransformedToList)
const temperature = parseVariableNumber(newSessionState.typebot.variables)( const temperature = parseVariableNumber(newSessionState.typebot.variables)(
options.advancedSettings?.temperature options.advancedSettings?.temperature

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
import { ExecuteIntegrationResponse } from '@/features/chat/types' 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 { parseVariables } from '@/features/variables/parseVariables'
import { updateVariables } from '@/features/variables/updateVariables' import { updateVariables } from '@/features/variables/updateVariables'
import { byId } from '@typebot.io/lib' import { byId } from '@typebot.io/lib'
@@ -13,75 +11,71 @@ import {
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { ReplyLog, SessionState } from '@typebot.io/schemas/features/chat' import { ReplyLog, SessionState } from '@typebot.io/schemas/features/chat'
export const resumeWebhookExecution = type Props = {
( state: SessionState
state: SessionState, block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock logs?: ReplyLog[]
) => response: {
async (response: {
statusCode: number statusCode: number
data?: unknown 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) { export const resumeWebhookExecution = ({
log = { state,
status: 'error', block,
description: `Webhook returned error: ${response.data}`, logs = [],
details: JSON.stringify(response.data, null, 2).substring(0, 1000), response,
} }: Props): ExecuteIntegrationResponse => {
result && const { typebot } = state
(await saveErrorLog({ const status = response.statusCode.toString()
resultId: result.id, const isError = status.startsWith('4') || status.startsWith('5')
message: log.description,
details: log.details, const responseFromClient = logs.length === 0
}))
} else { if (responseFromClient)
log = { logs.push(
status: 'success', isError
description: `Webhook executed successfully!`, ? {
details: JSON.stringify(response.data, null, 2).substring(0, 1000), status: 'error',
} description: `Webhook returned error`,
result && details: response.data,
(await saveSuccessLog({ }
resultId: result.id, : {
message: log.description, status: 'success',
details: JSON.stringify(response.data, null, 2).substring(0, 1000), 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< if (newVariables.length > 0) {
VariableWithUnknowValue[] const newSessionState = updateVariables(state)(newVariables)
>((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,
}
}
return { return {
outgoingEdgeId: block.outgoingEdgeId, 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( await expect(
page.locator('text="Webhook successfuly executed." >> nth=1') page.locator('text="Webhook successfuly executed." >> nth=1')
).toBeVisible() ).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 { parseGuessedValueType } from '@/features/variables/parseGuessedValueType'
import { parseScriptToExecuteClientSideAction } from '../script/executeScript' import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
export const executeSetVariable = async ( export const executeSetVariable = (
state: SessionState, state: SessionState,
block: SetVariableBlock block: SetVariableBlock
): Promise<ExecuteLogicResponse> => { ): ExecuteLogicResponse => {
const { variables } = state.typebot const { variables } = state.typebot
if (!block.options?.variableId) if (!block.options?.variableId)
return { return {
@@ -48,7 +48,7 @@ export const executeSetVariable = async (
...existingVariable, ...existingVariable,
value: evaluatedExpression, value: evaluatedExpression,
} }
const newSessionState = await updateVariables(state)([newVariable]) const newSessionState = updateVariables(state)([newVariable])
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
newSessionState, newSessionState,

View File

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

View File

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

View File

@@ -1,7 +1,4 @@
import prisma from '@/lib/prisma'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { Prisma } from '@typebot.io/prisma'
import got from 'got'
import { import {
Block, Block,
BlockType, BlockType,
@@ -16,7 +13,7 @@ import {
SetVariableBlock, SetVariableBlock,
WebhookBlock, WebhookBlock,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { isInputBlock, isNotDefined, byId } from '@typebot.io/lib' import { isInputBlock, byId } from '@typebot.io/lib'
import { executeGroup } from './executeGroup' import { executeGroup } from './executeGroup'
import { getNextGroup } from './getNextGroup' import { getNextGroup } from './getNextGroup'
import { validateEmail } from '@/features/blocks/inputs/email/validateEmail' 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 { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { resumeChatCompletion } from '@/features/blocks/integrations/openai/resumeChatCompletion' import { resumeChatCompletion } from '@/features/blocks/integrations/openai/resumeChatCompletion'
import { resumeWebhookExecution } from '@/features/blocks/integrations/webhook/resumeWebhookExecution' import { resumeWebhookExecution } from '@/features/blocks/integrations/webhook/resumeWebhookExecution'
import { upsertAnswer } from '../queries/upsertAnswer'
export const continueBotFlow = export const continueBotFlow =
(state: SessionState) => (state: SessionState) =>
@@ -60,13 +58,14 @@ export const continueBotFlow =
...existingVariable, ...existingVariable,
value: reply, value: reply,
} }
newSessionState = await updateVariables(state)([newVariable]) newSessionState = updateVariables(state)([newVariable])
} }
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) { } else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
const result = await resumeWebhookExecution( const result = resumeWebhookExecution({
state, state,
block block,
)(JSON.parse(reply)) response: JSON.parse(reply),
})
if (result.newSessionState) newSessionState = result.newSessionState if (result.newSessionState) newSessionState = result.newSessionState
} else if ( } else if (
block.type === IntegrationBlockType.OPEN_AI && block.type === IntegrationBlockType.OPEN_AI &&
@@ -136,20 +135,20 @@ const processAndSaveAnswer =
async (reply: string | null): Promise<SessionState> => { async (reply: string | null): Promise<SessionState> => {
if (!reply) return state if (!reply) return state
let newState = await saveAnswer(state, block, itemId)(reply) let newState = await saveAnswer(state, block, itemId)(reply)
newState = await saveVariableValueIfAny(newState, block)(reply) newState = saveVariableValueIfAny(newState, block)(reply)
return newState return newState
} }
const saveVariableValueIfAny = const saveVariableValueIfAny =
(state: SessionState, block: InputBlock) => (state: SessionState, block: InputBlock) =>
async (reply: string): Promise<SessionState> => { (reply: string): SessionState => {
if (!block.options.variableId) return state if (!block.options.variableId) return state
const foundVariable = state.typebot.variables.find( const foundVariable = state.typebot.variables.find(
(variable) => variable.id === block.options.variableId (variable) => variable.id === block.options.variableId
) )
if (!foundVariable) return state if (!foundVariable) return state
const newSessionState = await updateVariables(state)([ const newSessionState = updateVariables(state)([
{ {
...foundVariable, ...foundVariable,
value: Array.isArray(foundVariable.value) value: Array.isArray(foundVariable.value)
@@ -161,13 +160,6 @@ const saveVariableValueIfAny =
return newSessionState return newSessionState
} }
export const setResultAsCompleted = async (resultId: string) => {
await prisma.result.updateMany({
where: { id: resultId },
data: { isCompleted: true },
})
}
const parseRetryMessage = ( const parseRetryMessage = (
block: InputBlock block: InputBlock
): Pick<ChatReply, 'messages' | 'input'> => { ): Pick<ChatReply, 'messages' | 'input'> => {
@@ -192,55 +184,28 @@ const parseRetryMessage = (
const saveAnswer = const saveAnswer =
(state: SessionState, block: InputBlock, itemId?: string) => (state: SessionState, block: InputBlock, itemId?: string) =>
async (reply: string): Promise<SessionState> => { async (reply: string): Promise<SessionState> => {
const resultId = state.result?.id await upsertAnswer({
const answer: Omit<Prisma.AnswerUncheckedCreateInput, 'resultId'> = { block,
blockId: block.id, answer: {
blockId: block.id,
itemId,
groupId: block.groupId,
content: reply,
variableId: block.options.variableId,
storageUsed: 0,
},
reply,
state,
itemId, 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, blockId: block.id,
variableId: block.options.variableId ?? null, variableId: block.options.variableId ?? null,
content: reply, 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 = const setNewAnswerInState =
(state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => { (state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => {
const newAnswers = state.result.answers const newAnswers = state.result.answers
@@ -256,21 +221,6 @@ const setNewAnswerInState =
} satisfies SessionState } 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 = const getOutgoingEdgeId =
({ typebot: { variables } }: Pick<SessionState, 'typebot'>) => ({ 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 prisma from '@/lib/prisma'
import { isNotDefined } from '@typebot.io/lib' import { formatLogDetails } from './helpers/formatLogDetails'
type Props = { type Props = {
status: 'error' | 'success' | 'info' status: 'error' | 'success' | 'info'
@@ -15,16 +15,7 @@ export const saveLog = ({ status, resultId, message, details }: Props) => {
resultId, resultId,
status, status,
description: message, 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 { isDefined } from '@typebot.io/lib'
import { import {
SessionState, SessionState,
@@ -10,7 +9,7 @@ import { safeStringify } from './safeStringify'
export const updateVariables = export const updateVariables =
(state: SessionState) => (state: SessionState) =>
async (newVariables: VariableWithUnknowValue[]): Promise<SessionState> => ({ (newVariables: VariableWithUnknowValue[]): SessionState => ({
...state, ...state,
typebot: { typebot: {
...state.typebot, ...state.typebot,
@@ -18,15 +17,13 @@ export const updateVariables =
}, },
result: { result: {
...state.result, ...state.result,
variables: await updateResultVariables(state)(newVariables), variables: updateResultVariables(state)(newVariables),
}, },
}) })
const updateResultVariables = const updateResultVariables =
({ result }: Pick<SessionState, 'result' | 'typebot'>) => ({ result }: Pick<SessionState, 'result' | 'typebot'>) =>
async ( (newVariables: VariableWithUnknowValue[]): VariableWithValue[] => {
newVariables: VariableWithUnknowValue[]
): Promise<VariableWithValue[]> => {
const serializedNewVariables = newVariables.map((variable) => ({ const serializedNewVariables = newVariables.map((variable) => ({
...variable, ...variable,
value: Array.isArray(variable.value) value: Array.isArray(variable.value)
@@ -43,16 +40,6 @@ const updateResultVariables =
...serializedNewVariables, ...serializedNewVariables,
].filter((variable) => isDefined(variable.value)) as VariableWithValue[] ].filter((variable) => isDefined(variable.value)) as VariableWithValue[]
if (result.id)
await prisma.result.updateMany({
where: {
id: result.id,
},
data: {
variables: updatedVariables,
},
})
return updatedVariables return updatedVariables
} }