From aa4c16dad78e7e4dcd9b78f9b4916fd416da3c5f Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 18 Jul 2023 14:31:20 +0200 Subject: [PATCH] :zap: Regroup database queries of /sendMessage in one place Potentially reduces the total queries to database and it will help to migrate to Edge runtime --- .../src/features/templates/templates.spec.ts | 2 +- ...injectVariableValuesInButtonsInputBlock.ts | 8 +- .../integrations/googleSheets/getRow.ts | 25 +-- .../integrations/googleSheets/insertRow.ts | 28 +-- .../integrations/googleSheets/updateRow.ts | 42 +--- .../openai/createChatCompletionOpenAI.ts | 2 +- .../executeChatCompletionOpenAIRequest.ts | 1 + .../openai/resumeChatCompletion.ts | 14 +- .../sendEmail/executeSendEmailBlock.tsx | 45 +++-- .../webhook/executeWebhookBlock.ts | 72 ++++--- .../webhook/resumeWebhookExecution.ts | 124 ++++++------ .../integrations/webhook/webhook.spec.ts | 2 +- .../logic/setVariable/executeSetVariable.ts | 6 +- .../logic/typebotLink/executeTypebotLink.ts | 42 ++-- .../features/blocks/logic/wait/executeWait.ts | 4 +- .../src/features/chat/api/sendMessage.ts | 188 ++++++------------ .../features/chat/helpers/continueBotFlow.ts | 98 +++------ .../viewer/src/features/chat/helpers/index.ts | 5 - .../chat/helpers/saveStateToDatabase.ts | 48 +++++ .../features/chat/queries/createSession.ts | 13 ++ .../chat/queries/findPublicTypebot.ts | 34 ++++ .../src/features/chat/queries/findResult.ts | 14 ++ .../src/features/chat/queries/findTypebot.ts | 20 ++ .../getSession.ts} | 0 .../src/features/chat/queries/saveLogs.ts | 5 + .../features/chat/queries/updateSession.ts | 15 ++ .../src/features/chat/queries/upsertAnswer.ts | 57 ++++++ .../src/features/chat/queries/upsertResult.ts | 34 ++++ .../features/logs/helpers/formatLogDetails.ts | 11 + apps/viewer/src/features/logs/saveInfoLog.ts | 11 - apps/viewer/src/features/logs/saveLog.ts | 13 +- .../src/features/variables/updateVariables.ts | 19 +- 32 files changed, 520 insertions(+), 482 deletions(-) delete mode 100644 apps/viewer/src/features/chat/helpers/index.ts create mode 100644 apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts create mode 100644 apps/viewer/src/features/chat/queries/createSession.ts create mode 100644 apps/viewer/src/features/chat/queries/findPublicTypebot.ts create mode 100644 apps/viewer/src/features/chat/queries/findResult.ts create mode 100644 apps/viewer/src/features/chat/queries/findTypebot.ts rename apps/viewer/src/features/chat/{helpers/getSessionState.ts => queries/getSession.ts} (100%) create mode 100644 apps/viewer/src/features/chat/queries/saveLogs.ts create mode 100644 apps/viewer/src/features/chat/queries/updateSession.ts create mode 100644 apps/viewer/src/features/chat/queries/upsertAnswer.ts create mode 100644 apps/viewer/src/features/chat/queries/upsertResult.ts create mode 100644 apps/viewer/src/features/logs/helpers/formatLogDetails.ts delete mode 100644 apps/viewer/src/features/logs/saveInfoLog.ts diff --git a/apps/builder/src/features/templates/templates.spec.ts b/apps/builder/src/features/templates/templates.spec.ts index 99bcd9039b6..d9384065c99 100644 --- a/apps/builder/src/features/templates/templates.spec.ts +++ b/apps/builder/src/features/templates/templates.spec.ts @@ -27,6 +27,6 @@ test.describe.parallel('Templates page', () => { await page.click('text=Customer Support') await expect(page.locator('text=How can I help you?')).toBeVisible() 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 }) }) }) diff --git a/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts b/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts index 74aff85dba3..3e88445c124 100644 --- a/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts +++ b/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts @@ -12,7 +12,7 @@ import { filterChoiceItems } from './filterChoiceItems' export const injectVariableValuesInButtonsInputBlock = (state: SessionState) => - async (block: ChoiceInputBlock): Promise => { + (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 diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts index b501c934ca1..5e2b0a4e671 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts +++ b/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts @@ -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 => { + 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 = ( diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts index 7aec00e0544..0055f4b154b 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts +++ b/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts @@ -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 => { 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 } } diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts index abc24b69b6c..c75b809513c 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts +++ b/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts @@ -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) ) 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 } } diff --git a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts b/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts index a484bda6d23..b563f2605de 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts +++ b/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts @@ -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 diff --git a/apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts b/apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts index 64704cabf16..3f796e31b51 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts +++ b/apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts @@ -79,6 +79,7 @@ export const executeChatCompletionOpenAIRequest = async ({ logs.push({ status: 'error', description: `Internal error`, + details: error, }) return { logs } } diff --git a/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts b/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts index 1c140b4bd92..dd74d417484 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts +++ b/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts @@ -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, diff --git a/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx b/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx index 6cc3e7f52ac..a93e418e318 100644 --- a/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx +++ b/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx @@ -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 => { + 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 => { + 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 ( diff --git a/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts b/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts index 03646f631fc..99fa60519d9 100644 --- a/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts +++ b/apps/viewer/src/features/blocks/integrations/webhook/executeWebhookBlock.ts @@ -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 => { 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 => { + 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 } } } diff --git a/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts b/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts index d52cfdf6061..753a870a674 100644 --- a/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts +++ b/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts @@ -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 => { - 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 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, - } - } + 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 + } + }, []) + if (newVariables.length > 0) { + const newSessionState = updateVariables(state)(newVariables) return { outgoingEdgeId: block.outgoingEdgeId, - logs: log ? [log] : undefined, + newSessionState, + logs, } } + + return { + outgoingEdgeId: block.outgoingEdgeId, + logs, + } +} diff --git a/apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts b/apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts index c76121070f2..ebe6c39727f 100644 --- a/apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts +++ b/apps/viewer/src/features/blocks/integrations/webhook/webhook.spec.ts @@ -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() }) diff --git a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts b/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts index 3e2bd4caae3..04f84ed48a2 100644 --- a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts +++ b/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts @@ -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 => { 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, diff --git a/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts b/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts index f908491f24d..058d26fb322 100644 --- a/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts +++ b/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts @@ -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 => { + 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 } }) diff --git a/apps/viewer/src/features/blocks/logic/wait/executeWait.ts b/apps/viewer/src/features/blocks/logic/wait/executeWait.ts index ed8acf6d36b..0a02b7bfa6a 100644 --- a/apps/viewer/src/features/blocks/logic/wait/executeWait.ts +++ b/apps/viewer/src/features/blocks/logic/wait/executeWait.ts @@ -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 => { if (!block.options.secondsToWaitFor) return { outgoingEdgeId: block.outgoingEdgeId } const parsedSecondsToWaitFor = safeParseInt( diff --git a/apps/viewer/src/features/chat/api/sendMessage.ts b/apps/viewer/src/features/chat/api/sendMessage.ts index daa0dda277c..37b92a5280c 100644 --- a/apps/viewer/src/features/chat/api/sendMessage.ts +++ b/apps/viewer/src/features/chat/api/sendMessage.ts @@ -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 - ) - - 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, - }, - }) + const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs + + 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, } } diff --git a/apps/viewer/src/features/chat/helpers/continueBotFlow.ts b/apps/viewer/src/features/chat/helpers/continueBotFlow.ts index 09e35acd471..81dd927067b 100644 --- a/apps/viewer/src/features/chat/helpers/continueBotFlow.ts +++ b/apps/viewer/src/features/chat/helpers/continueBotFlow.ts @@ -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 => { 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 => { + (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 => { @@ -192,55 +184,28 @@ const parseRetryMessage = ( const saveAnswer = (state: SessionState, block: InputBlock, itemId?: string) => async (reply: string): Promise => { - const resultId = state.result?.id - const answer: Omit = { - 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) => ( diff --git a/apps/viewer/src/features/chat/helpers/index.ts b/apps/viewer/src/features/chat/helpers/index.ts deleted file mode 100644 index 6d9a8531e4e..00000000000 --- a/apps/viewer/src/features/chat/helpers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './continueBotFlow' -export * from './executeGroup' -export * from './getNextGroup' -export * from './getSessionState' -export * from './startBotFlow' diff --git a/apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts b/apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts new file mode 100644 index 00000000000..4bb9c5913b8 --- /dev/null +++ b/apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts @@ -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 & { 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 +} diff --git a/apps/viewer/src/features/chat/queries/createSession.ts b/apps/viewer/src/features/chat/queries/createSession.ts new file mode 100644 index 00000000000..81bc3632867 --- /dev/null +++ b/apps/viewer/src/features/chat/queries/createSession.ts @@ -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, + }, + }) diff --git a/apps/viewer/src/features/chat/queries/findPublicTypebot.ts b/apps/viewer/src/features/chat/queries/findPublicTypebot.ts new file mode 100644 index 00000000000..5459cba059d --- /dev/null +++ b/apps/viewer/src/features/chat/queries/findPublicTypebot.ts @@ -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, + }, + }, + }, + }, + }, + }) diff --git a/apps/viewer/src/features/chat/queries/findResult.ts b/apps/viewer/src/features/chat/queries/findResult.ts new file mode 100644 index 00000000000..d261d63588b --- /dev/null +++ b/apps/viewer/src/features/chat/queries/findResult.ts @@ -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 } }, + }, + }) diff --git a/apps/viewer/src/features/chat/queries/findTypebot.ts b/apps/viewer/src/features/chat/queries/findTypebot.ts new file mode 100644 index 00000000000..2f5cf2160c9 --- /dev/null +++ b/apps/viewer/src/features/chat/queries/findTypebot.ts @@ -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, + }, + }) diff --git a/apps/viewer/src/features/chat/helpers/getSessionState.ts b/apps/viewer/src/features/chat/queries/getSession.ts similarity index 100% rename from apps/viewer/src/features/chat/helpers/getSessionState.ts rename to apps/viewer/src/features/chat/queries/getSession.ts diff --git a/apps/viewer/src/features/chat/queries/saveLogs.ts b/apps/viewer/src/features/chat/queries/saveLogs.ts new file mode 100644 index 00000000000..aefdd740ff3 --- /dev/null +++ b/apps/viewer/src/features/chat/queries/saveLogs.ts @@ -0,0 +1,5 @@ +import prisma from '@/lib/prisma' +import { Log } from '@typebot.io/schemas' + +export const saveLogs = (logs: Omit[]) => + prisma.log.createMany({ data: logs }) diff --git a/apps/viewer/src/features/chat/queries/updateSession.ts b/apps/viewer/src/features/chat/queries/updateSession.ts new file mode 100644 index 00000000000..0c5da4a7e1b --- /dev/null +++ b/apps/viewer/src/features/chat/queries/updateSession.ts @@ -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, + }, + }) diff --git a/apps/viewer/src/features/chat/queries/upsertAnswer.ts b/apps/viewer/src/features/chat/queries/upsertAnswer.ts new file mode 100644 index 00000000000..9b1ca7e1847 --- /dev/null +++ b/apps/viewer/src/features/chat/queries/upsertAnswer.ts @@ -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 + 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 +} diff --git a/apps/viewer/src/features/chat/queries/upsertResult.ts b/apps/viewer/src/features/chat/queries/upsertResult.ts new file mode 100644 index 00000000000..cdbc15d9cee --- /dev/null +++ b/apps/viewer/src/features/chat/queries/upsertResult.ts @@ -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, + }, + ], + }) +} diff --git a/apps/viewer/src/features/logs/helpers/formatLogDetails.ts b/apps/viewer/src/features/logs/helpers/formatLogDetails.ts new file mode 100644 index 00000000000..ec4850225fb --- /dev/null +++ b/apps/viewer/src/features/logs/helpers/formatLogDetails.ts @@ -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 + } +} diff --git a/apps/viewer/src/features/logs/saveInfoLog.ts b/apps/viewer/src/features/logs/saveInfoLog.ts deleted file mode 100644 index eda0ac56d38..00000000000 --- a/apps/viewer/src/features/logs/saveInfoLog.ts +++ /dev/null @@ -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 }) diff --git a/apps/viewer/src/features/logs/saveLog.ts b/apps/viewer/src/features/logs/saveLog.ts index 8fb54830862..5e989c693e5 100644 --- a/apps/viewer/src/features/logs/saveLog.ts +++ b/apps/viewer/src/features/logs/saveLog.ts @@ -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 - } -} diff --git a/apps/viewer/src/features/variables/updateVariables.ts b/apps/viewer/src/features/variables/updateVariables.ts index a4aa74ddb47..509f9535393 100644 --- a/apps/viewer/src/features/variables/updateVariables.ts +++ b/apps/viewer/src/features/variables/updateVariables.ts @@ -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 => ({ + (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) => - async ( - newVariables: VariableWithUnknowValue[] - ): Promise => { + (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 }