⚡ 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:
@ -12,7 +12,7 @@ import { filterChoiceItems } from './filterChoiceItems'
|
||||
|
||||
export const injectVariableValuesInButtonsInputBlock =
|
||||
(state: SessionState) =>
|
||||
async (block: ChoiceInputBlock): Promise<ChoiceInputBlock> => {
|
||||
(block: ChoiceInputBlock): ChoiceInputBlock => {
|
||||
if (block.options.dynamicVariableId) {
|
||||
const variable = state.typebot.variables.find(
|
||||
(variable) =>
|
||||
@ -20,7 +20,7 @@ export const injectVariableValuesInButtonsInputBlock =
|
||||
isDefined(variable.value)
|
||||
) as VariableWithValue | undefined
|
||||
if (!variable) return block
|
||||
const value = await getVariableValue(state)(variable)
|
||||
const value = getVariableValue(state)(variable)
|
||||
return {
|
||||
...block,
|
||||
items: value.filter(isDefined).map((item, idx) => ({
|
||||
@ -38,12 +38,12 @@ export const injectVariableValuesInButtonsInputBlock =
|
||||
|
||||
const getVariableValue =
|
||||
(state: SessionState) =>
|
||||
async (variable: VariableWithValue): Promise<(string | null)[]> => {
|
||||
(variable: VariableWithValue): (string | null)[] => {
|
||||
if (!Array.isArray(variable.value)) {
|
||||
const [transformedVariable] = transformStringVariablesToList(
|
||||
state.typebot.variables
|
||||
)([variable.id])
|
||||
await updateVariables(state)([transformedVariable])
|
||||
updateVariables(state)([transformedVariable])
|
||||
return transformedVariable.value as string[]
|
||||
}
|
||||
return variable.value
|
||||
|
@ -7,11 +7,9 @@ import {
|
||||
import { isNotEmpty, byId } from '@typebot.io/lib'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
||||
import { updateVariables } from '@/features/variables/updateVariables'
|
||||
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||
import { matchFilter } from './helpers/matchFilter'
|
||||
import { saveInfoLog } from '@/features/logs/saveInfoLog'
|
||||
|
||||
export const getRow = async (
|
||||
state: SessionState,
|
||||
@ -20,15 +18,13 @@ export const getRow = async (
|
||||
options,
|
||||
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const logs: ReplyLog[] = []
|
||||
const { sheetId, cellsToExtract, referenceCell, filter } = deepParseVariables(
|
||||
state.typebot.variables
|
||||
)(options)
|
||||
if (!sheetId) return { outgoingEdgeId }
|
||||
|
||||
let log: ReplyLog | undefined
|
||||
|
||||
const variables = state.typebot.variables
|
||||
const resultId = state.result?.id
|
||||
|
||||
const doc = await getAuthenticatedGoogleDoc({
|
||||
credentialsId: options.credentialsId,
|
||||
@ -48,16 +44,12 @@ export const getRow = async (
|
||||
)
|
||||
)
|
||||
if (filteredRows.length === 0) {
|
||||
log = {
|
||||
logs.push({
|
||||
status: 'info',
|
||||
description: `Couldn't find any rows matching the filter`,
|
||||
details: JSON.stringify(filter, null, 2),
|
||||
}
|
||||
await saveInfoLog({
|
||||
resultId,
|
||||
message: log.description,
|
||||
})
|
||||
return { outgoingEdgeId, logs: log ? [log] : undefined }
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
const extractingColumns = cellsToExtract
|
||||
.map((cell) => cell.column)
|
||||
@ -85,24 +77,19 @@ export const getRow = async (
|
||||
},
|
||||
[]
|
||||
)
|
||||
const newSessionState = await updateVariables(state)(newVariables)
|
||||
const newSessionState = updateVariables(state)(newVariables)
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
newSessionState,
|
||||
}
|
||||
} catch (err) {
|
||||
log = {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `An error occurred while fetching the spreadsheet data`,
|
||||
details: err,
|
||||
}
|
||||
await saveErrorLog({
|
||||
resultId,
|
||||
message: log.description,
|
||||
details: err,
|
||||
})
|
||||
}
|
||||
return { outgoingEdgeId, logs: log ? [log] : undefined }
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
|
||||
const getTotalRows = <T>(
|
||||
|
@ -6,11 +6,9 @@ import {
|
||||
import { parseCellValues } from './helpers/parseCellValues'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
||||
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
||||
|
||||
export const insertRow = async (
|
||||
{ result, typebot: { variables } }: SessionState,
|
||||
{ typebot: { variables } }: SessionState,
|
||||
{
|
||||
outgoingEdgeId,
|
||||
options,
|
||||
@ -18,7 +16,7 @@ export const insertRow = async (
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
|
||||
|
||||
let log: ReplyLog | undefined
|
||||
const logs: ReplyLog[] = []
|
||||
|
||||
const doc = await getAuthenticatedGoogleDoc({
|
||||
credentialsId: options.credentialsId,
|
||||
@ -31,27 +29,17 @@ export const insertRow = async (
|
||||
await doc.loadInfo()
|
||||
const sheet = doc.sheetsById[Number(options.sheetId)]
|
||||
await sheet.addRow(parsedValues)
|
||||
log = {
|
||||
logs.push({
|
||||
status: 'success',
|
||||
description: `Succesfully inserted row in ${doc.title} > ${sheet.title}`,
|
||||
}
|
||||
result &&
|
||||
(await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: log?.description,
|
||||
}))
|
||||
})
|
||||
} catch (err) {
|
||||
log = {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `An error occured while inserting the row`,
|
||||
details: err,
|
||||
}
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
details: err,
|
||||
}))
|
||||
})
|
||||
}
|
||||
return { outgoingEdgeId, logs: log ? [log] : undefined }
|
||||
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
|
@ -7,13 +7,10 @@ import { parseCellValues } from './helpers/parseCellValues'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
||||
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
||||
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
||||
import { matchFilter } from './helpers/matchFilter'
|
||||
import { saveInfoLog } from '@/features/logs/saveInfoLog'
|
||||
|
||||
export const updateRow = async (
|
||||
{ result, typebot: { variables } }: SessionState,
|
||||
{ typebot: { variables } }: SessionState,
|
||||
{
|
||||
outgoingEdgeId,
|
||||
options,
|
||||
@ -24,7 +21,7 @@ export const updateRow = async (
|
||||
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))
|
||||
return { outgoingEdgeId }
|
||||
|
||||
let log: ReplyLog | undefined
|
||||
const logs: ReplyLog[] = []
|
||||
|
||||
const doc = await getAuthenticatedGoogleDoc({
|
||||
credentialsId: options.credentialsId,
|
||||
@ -42,18 +39,12 @@ export const updateRow = async (
|
||||
: matchFilter(row, filter as NonNullable<typeof filter>)
|
||||
)
|
||||
if (filteredRows.length === 0) {
|
||||
log = {
|
||||
logs.push({
|
||||
status: 'info',
|
||||
description: `Could not find any row that matches the filter`,
|
||||
details: JSON.stringify(filter, null, 2),
|
||||
}
|
||||
result &&
|
||||
(await saveInfoLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
details: log.details,
|
||||
}))
|
||||
return { outgoingEdgeId, logs: log ? [log] : undefined }
|
||||
details: filter,
|
||||
})
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
|
||||
try {
|
||||
@ -65,28 +56,17 @@ export const updateRow = async (
|
||||
await rows[rowIndex].save()
|
||||
}
|
||||
|
||||
log = log = {
|
||||
logs.push({
|
||||
status: 'success',
|
||||
description: `Succesfully updated matching rows`,
|
||||
}
|
||||
result &&
|
||||
(await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
}))
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
log = {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `An error occured while updating the row`,
|
||||
details: err,
|
||||
}
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
details: err,
|
||||
}))
|
||||
})
|
||||
}
|
||||
return { outgoingEdgeId, logs: log ? [log] : undefined }
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -79,6 +79,7 @@ export const executeChatCompletionOpenAIRequest = async ({
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Internal error`,
|
||||
details: error,
|
||||
})
|
||||
return { logs }
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -4,6 +4,7 @@ import { render } from '@faire/mjml-react/utils/render'
|
||||
import { DefaultBotNotificationEmail } from '@typebot.io/emails'
|
||||
import {
|
||||
PublicTypebot,
|
||||
ReplyLog,
|
||||
ResultInSession,
|
||||
SendEmailBlock,
|
||||
SendEmailOptions,
|
||||
@ -18,14 +19,13 @@ import { parseAnswers } from '@typebot.io/lib/results'
|
||||
import { decrypt } from '@typebot.io/lib/api'
|
||||
import { defaultFrom, defaultTransportOptions } from './constants'
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
||||
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
||||
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
|
||||
|
||||
export const executeSendEmailBlock = async (
|
||||
{ result, typebot }: SessionState,
|
||||
block: SendEmailBlock
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const logs: ReplyLog[] = []
|
||||
const { options } = block
|
||||
const { variables } = typebot
|
||||
const isPreview = !result.id
|
||||
@ -45,7 +45,7 @@ export const executeSendEmailBlock = async (
|
||||
parseVariables(variables, { escapeHtml: true })(options.body ?? '')
|
||||
|
||||
try {
|
||||
await sendEmail({
|
||||
const sendEmailLogs = await sendEmail({
|
||||
typebotId: typebot.id,
|
||||
result,
|
||||
credentialsId: options.credentialsId,
|
||||
@ -61,17 +61,16 @@ export const executeSendEmailBlock = async (
|
||||
isCustomBody: options.isCustomBody,
|
||||
isBodyCode: options.isBodyCode,
|
||||
})
|
||||
if (sendEmailLogs) logs.push(...sendEmailLogs)
|
||||
} catch (err) {
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: 'Email not sent',
|
||||
details: {
|
||||
error: err,
|
||||
},
|
||||
logs.push({
|
||||
status: 'error',
|
||||
details: err,
|
||||
description: `Email not sent`,
|
||||
})
|
||||
}
|
||||
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
|
||||
const sendEmail = async ({
|
||||
@ -91,7 +90,8 @@ const sendEmail = async ({
|
||||
typebotId: string
|
||||
result: ResultInSession
|
||||
fileUrls?: string | string[]
|
||||
}) => {
|
||||
}): Promise<ReplyLog[] | undefined> => {
|
||||
const logs: ReplyLog[] = []
|
||||
const { name: replyToName } = parseEmailRecipient(replyTo)
|
||||
|
||||
const { host, port, isTlsEnabled, username, password, from } =
|
||||
@ -117,9 +117,9 @@ const sendEmail = async ({
|
||||
})
|
||||
|
||||
if (!emailBody) {
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: 'Email not sent',
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: 'Email not sent',
|
||||
details: {
|
||||
error: 'No email body found',
|
||||
transportConfig,
|
||||
@ -131,6 +131,7 @@ const sendEmail = async ({
|
||||
emailBody,
|
||||
},
|
||||
})
|
||||
return logs
|
||||
}
|
||||
const transporter = createTransport(transportConfig)
|
||||
const fromName = isEmpty(replyToName) ? from.name : replyToName
|
||||
@ -150,9 +151,9 @@ const sendEmail = async ({
|
||||
}
|
||||
try {
|
||||
await transporter.sendMail(email)
|
||||
await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: 'Email successfully sent',
|
||||
logs.push({
|
||||
status: 'success',
|
||||
description: 'Email successfully sent',
|
||||
details: {
|
||||
transportConfig: {
|
||||
...transportConfig,
|
||||
@ -162,11 +163,11 @@ const sendEmail = async ({
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: 'Email not sent',
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: 'Email not sent',
|
||||
details: {
|
||||
error: err,
|
||||
error: err instanceof Error ? err.toString() : err,
|
||||
transportConfig: {
|
||||
...transportConfig,
|
||||
auth: { user: transportConfig.auth.user, pass: '******' },
|
||||
@ -175,6 +176,8 @@ const sendEmail = async ({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return logs
|
||||
}
|
||||
|
||||
const getEmailInfo = async (
|
||||
|
@ -24,8 +24,6 @@ import { parseAnswers } from '@typebot.io/lib/results'
|
||||
import got, { Method, HTTPError, OptionsInit } from 'got'
|
||||
import { parseSampleResult } from './parseSampleResult'
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
||||
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
||||
import { parseVariables } from '@/features/variables/parseVariables'
|
||||
import { resumeWebhookExecution } from './resumeWebhookExecution'
|
||||
|
||||
@ -39,21 +37,16 @@ export const executeWebhookBlock = async (
|
||||
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const { typebot, result } = state
|
||||
let log: ReplyLog | undefined
|
||||
const logs: ReplyLog[] = []
|
||||
const webhook = (await prisma.webhook.findUnique({
|
||||
where: { id: block.webhookId },
|
||||
})) as Webhook | null
|
||||
if (!webhook) {
|
||||
log = {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Couldn't find webhook with id ${block.webhookId}`,
|
||||
}
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
}))
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
||||
const parsedWebhook = await parseWebhookAttributes(
|
||||
@ -62,16 +55,11 @@ export const executeWebhookBlock = async (
|
||||
result
|
||||
)(preparedWebhook)
|
||||
if (!parsedWebhook) {
|
||||
log = {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Couldn't parse webhook attributes`,
|
||||
}
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
}))
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
if (block.options.isExecutedOnClient)
|
||||
return {
|
||||
@ -82,8 +70,14 @@ export const executeWebhookBlock = async (
|
||||
},
|
||||
],
|
||||
}
|
||||
const webhookResponse = await executeWebhook(parsedWebhook, result)
|
||||
return resumeWebhookExecution(state, block)(webhookResponse)
|
||||
const { response: webhookResponse, logs: executeWebhookLogs } =
|
||||
await executeWebhook(parsedWebhook)
|
||||
return resumeWebhookExecution({
|
||||
state,
|
||||
block,
|
||||
logs: executeWebhookLogs,
|
||||
response: webhookResponse,
|
||||
})
|
||||
}
|
||||
|
||||
const prepareWebhookAttributes = (
|
||||
@ -162,9 +156,9 @@ const parseWebhookAttributes =
|
||||
}
|
||||
|
||||
export const executeWebhook = async (
|
||||
webhook: ParsedWebhook,
|
||||
result: ResultInSession
|
||||
): Promise<WebhookResponse> => {
|
||||
webhook: ParsedWebhook
|
||||
): Promise<{ response: WebhookResponse; logs?: ReplyLog[] }> => {
|
||||
const logs: ReplyLog[] = []
|
||||
const { headers, url, method, basicAuth, body, isJson } = webhook
|
||||
const contentType = headers ? headers['Content-Type'] : undefined
|
||||
|
||||
@ -183,9 +177,9 @@ export const executeWebhook = async (
|
||||
} satisfies OptionsInit
|
||||
try {
|
||||
const response = await got(request.url, omit(request, 'url'))
|
||||
await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: 'Webhook successfuly executed.',
|
||||
logs.push({
|
||||
status: 'success',
|
||||
description: `Webhook successfuly executed.`,
|
||||
details: {
|
||||
statusCode: response.statusCode,
|
||||
request,
|
||||
@ -193,8 +187,11 @@ export const executeWebhook = async (
|
||||
},
|
||||
})
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
data: safeJsonParse(response.body).data,
|
||||
response: {
|
||||
statusCode: response.statusCode,
|
||||
data: safeJsonParse(response.body).data,
|
||||
},
|
||||
logs,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
@ -202,30 +199,31 @@ export const executeWebhook = async (
|
||||
statusCode: error.response.statusCode,
|
||||
data: safeJsonParse(error.response.body as string).data,
|
||||
}
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: 'Webhook returned an error',
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Webhook returned an error.`,
|
||||
details: {
|
||||
statusCode: error.response.statusCode,
|
||||
request,
|
||||
response,
|
||||
},
|
||||
})
|
||||
return response
|
||||
return { response, logs }
|
||||
}
|
||||
const response = {
|
||||
statusCode: 500,
|
||||
data: { message: `Error from Typebot server: ${error}` },
|
||||
}
|
||||
console.error(error)
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: 'Webhook failed to execute',
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Webhook failed to execute.`,
|
||||
details: {
|
||||
request,
|
||||
response,
|
||||
},
|
||||
})
|
||||
return response
|
||||
return { response, logs }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
||||
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
||||
import { parseVariables } from '@/features/variables/parseVariables'
|
||||
import { updateVariables } from '@/features/variables/updateVariables'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
@ -13,75 +11,71 @@ import {
|
||||
} from '@typebot.io/schemas'
|
||||
import { ReplyLog, SessionState } from '@typebot.io/schemas/features/chat'
|
||||
|
||||
export const resumeWebhookExecution =
|
||||
(
|
||||
state: SessionState,
|
||||
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
|
||||
) =>
|
||||
async (response: {
|
||||
type Props = {
|
||||
state: SessionState
|
||||
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
|
||||
logs?: ReplyLog[]
|
||||
response: {
|
||||
statusCode: number
|
||||
data?: unknown
|
||||
}): Promise<ExecuteIntegrationResponse> => {
|
||||
const { typebot, result } = state
|
||||
let log: ReplyLog | undefined
|
||||
const status = response.statusCode.toString()
|
||||
const isError = status.startsWith('4') || status.startsWith('5')
|
||||
}
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
log = {
|
||||
status: 'error',
|
||||
description: `Webhook returned error: ${response.data}`,
|
||||
details: JSON.stringify(response.data, null, 2).substring(0, 1000),
|
||||
}
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
details: log.details,
|
||||
}))
|
||||
} else {
|
||||
log = {
|
||||
status: 'success',
|
||||
description: `Webhook executed successfully!`,
|
||||
details: JSON.stringify(response.data, null, 2).substring(0, 1000),
|
||||
}
|
||||
result &&
|
||||
(await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
details: JSON.stringify(response.data, null, 2).substring(0, 1000),
|
||||
}))
|
||||
export const resumeWebhookExecution = ({
|
||||
state,
|
||||
block,
|
||||
logs = [],
|
||||
response,
|
||||
}: Props): ExecuteIntegrationResponse => {
|
||||
const { typebot } = state
|
||||
const status = response.statusCode.toString()
|
||||
const isError = status.startsWith('4') || status.startsWith('5')
|
||||
|
||||
const responseFromClient = logs.length === 0
|
||||
|
||||
if (responseFromClient)
|
||||
logs.push(
|
||||
isError
|
||||
? {
|
||||
status: 'error',
|
||||
description: `Webhook returned error`,
|
||||
details: response.data,
|
||||
}
|
||||
: {
|
||||
status: 'success',
|
||||
description: `Webhook executed successfully!`,
|
||||
details: response.data,
|
||||
}
|
||||
)
|
||||
|
||||
const newVariables = block.options.responseVariableMapping.reduce<
|
||||
VariableWithUnknowValue[]
|
||||
>((newVariables, varMapping) => {
|
||||
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
||||
const existingVariable = typebot.variables.find(byId(varMapping.variableId))
|
||||
if (!existingVariable) return newVariables
|
||||
const func = Function(
|
||||
'data',
|
||||
`return data.${parseVariables(typebot.variables)(varMapping?.bodyPath)}`
|
||||
)
|
||||
try {
|
||||
const value: unknown = func(response)
|
||||
return [...newVariables, { ...existingVariable, value }]
|
||||
} catch (err) {
|
||||
return newVariables
|
||||
}
|
||||
|
||||
const newVariables = block.options.responseVariableMapping.reduce<
|
||||
VariableWithUnknowValue[]
|
||||
>((newVariables, varMapping) => {
|
||||
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
||||
const existingVariable = typebot.variables.find(
|
||||
byId(varMapping.variableId)
|
||||
)
|
||||
if (!existingVariable) return newVariables
|
||||
const func = Function(
|
||||
'data',
|
||||
`return data.${parseVariables(typebot.variables)(varMapping?.bodyPath)}`
|
||||
)
|
||||
try {
|
||||
const value: unknown = func(response)
|
||||
return [...newVariables, { ...existingVariable, value }]
|
||||
} catch (err) {
|
||||
return newVariables
|
||||
}
|
||||
}, [])
|
||||
if (newVariables.length > 0) {
|
||||
const newSessionState = await updateVariables(state)(newVariables)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
newSessionState,
|
||||
}
|
||||
}
|
||||
|
||||
}, [])
|
||||
if (newVariables.length > 0) {
|
||||
const newSessionState = updateVariables(state)(newVariables)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
logs: log ? [log] : undefined,
|
||||
newSessionState,
|
||||
logs,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
logs,
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -6,10 +6,10 @@ import { parseVariables } from '@/features/variables/parseVariables'
|
||||
import { parseGuessedValueType } from '@/features/variables/parseGuessedValueType'
|
||||
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
|
||||
|
||||
export const executeSetVariable = async (
|
||||
export const executeSetVariable = (
|
||||
state: SessionState,
|
||||
block: SetVariableBlock
|
||||
): Promise<ExecuteLogicResponse> => {
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebot
|
||||
if (!block.options?.variableId)
|
||||
return {
|
||||
@ -48,7 +48,7 @@ export const executeSetVariable = async (
|
||||
...existingVariable,
|
||||
value: evaluatedExpression,
|
||||
}
|
||||
const newSessionState = await updateVariables(state)([newVariable])
|
||||
const newSessionState = updateVariables(state)([newVariable])
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
newSessionState,
|
||||
|
@ -8,33 +8,32 @@ import {
|
||||
SessionState,
|
||||
TypebotInSession,
|
||||
Variable,
|
||||
ReplyLog,
|
||||
} from '@typebot.io/schemas'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { ExecuteLogicResponse } from '@/features/chat/types'
|
||||
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
||||
|
||||
export const executeTypebotLink = async (
|
||||
state: SessionState,
|
||||
block: TypebotLinkBlock
|
||||
): Promise<ExecuteLogicResponse> => {
|
||||
const logs: ReplyLog[] = []
|
||||
if (!block.options.typebotId) {
|
||||
state.result &&
|
||||
saveErrorLog({
|
||||
resultId: state.result.id,
|
||||
message: 'Failed to link typebot',
|
||||
details: 'Typebot ID is not specified',
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Failed to link typebot`,
|
||||
details: `Typebot ID is not specified`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
const linkedTypebot = await getLinkedTypebot(state, block.options.typebotId)
|
||||
if (!linkedTypebot) {
|
||||
state.result &&
|
||||
saveErrorLog({
|
||||
resultId: state.result.id,
|
||||
message: 'Failed to link typebot',
|
||||
details: `Typebot with ID ${block.options.typebotId} not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Failed to link typebot`,
|
||||
details: `Typebot with ID ${block.options.typebotId} not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
let newSessionState = addLinkedTypebotToState(state, block, linkedTypebot)
|
||||
|
||||
@ -43,13 +42,12 @@ export const executeTypebotLink = async (
|
||||
linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start'))
|
||||
?.id
|
||||
if (!nextGroupId) {
|
||||
state.result &&
|
||||
saveErrorLog({
|
||||
resultId: state.result.id,
|
||||
message: 'Failed to link typebot',
|
||||
details: `Group with ID "${block.options.groupId}" not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Failed to link typebot`,
|
||||
details: `Group with ID "${block.options.groupId}" not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
|
||||
const portalEdge = createPortalEdge({ to: { groupId: nextGroupId } })
|
||||
|
@ -2,10 +2,10 @@ import { ExecuteLogicResponse } from '@/features/chat/types'
|
||||
import { parseVariables } from '@/features/variables/parseVariables'
|
||||
import { SessionState, WaitBlock } from '@typebot.io/schemas'
|
||||
|
||||
export const executeWait = async (
|
||||
export const executeWait = (
|
||||
{ typebot: { variables } }: SessionState,
|
||||
block: WaitBlock
|
||||
): Promise<ExecuteLogicResponse> => {
|
||||
): ExecuteLogicResponse => {
|
||||
if (!block.options.secondsToWaitFor)
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const parsedSecondsToWaitFor = safeParseInt(
|
||||
|
@ -1,14 +1,12 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Prisma } from '@typebot.io/prisma'
|
||||
import {
|
||||
ChatReply,
|
||||
chatReplySchema,
|
||||
ChatSession,
|
||||
GoogleAnalyticsBlock,
|
||||
IntegrationBlockType,
|
||||
PixelBlock,
|
||||
ReplyLog,
|
||||
ResultInSession,
|
||||
sendMessageInputSchema,
|
||||
SessionState,
|
||||
@ -19,19 +17,20 @@ import {
|
||||
Variable,
|
||||
VariableWithValue,
|
||||
} from '@typebot.io/schemas'
|
||||
import {
|
||||
continueBotFlow,
|
||||
getSession,
|
||||
setResultAsCompleted,
|
||||
startBotFlow,
|
||||
} from '../helpers'
|
||||
import { env, isDefined, isNotEmpty, omit } from '@typebot.io/lib'
|
||||
import { prefillVariables } from '@/features/variables/prefillVariables'
|
||||
import { injectVariablesFromExistingResult } from '@/features/variables/injectVariablesFromExistingResult'
|
||||
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||
import { parseVariables } from '@/features/variables/parseVariables'
|
||||
import { saveLog } from '@/features/logs/saveLog'
|
||||
import { NodeType, parse } from 'node-html-parser'
|
||||
import { saveStateToDatabase } from '../helpers/saveStateToDatabase'
|
||||
import { getSession } from '../queries/getSession'
|
||||
import { continueBotFlow } from '../helpers/continueBotFlow'
|
||||
import { startBotFlow } from '../helpers/startBotFlow'
|
||||
import { findTypebot } from '../queries/findTypebot'
|
||||
import { findPublicTypebot } from '../queries/findPublicTypebot'
|
||||
import { findResult } from '../queries/findResult'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
export const sendMessage = publicProcedure
|
||||
.meta({
|
||||
@ -53,17 +52,6 @@ export const sendMessage = publicProcedure
|
||||
}) => {
|
||||
const session = sessionId ? await getSession(sessionId) : null
|
||||
|
||||
if (clientLogs) {
|
||||
for (const log of clientLogs) {
|
||||
await saveLog({
|
||||
message: log.description,
|
||||
status: log.status as 'error' | 'success' | 'info',
|
||||
resultId: session?.state.result.id,
|
||||
details: log.details,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
const {
|
||||
sessionId,
|
||||
@ -74,7 +62,7 @@ export const sendMessage = publicProcedure
|
||||
dynamicTheme,
|
||||
logs,
|
||||
clientSideActions,
|
||||
} = await startSession(startParams, user?.id)
|
||||
} = await startSession(startParams, user?.id, clientLogs)
|
||||
return {
|
||||
sessionId,
|
||||
typebot: typebot
|
||||
@ -95,24 +83,18 @@ export const sendMessage = publicProcedure
|
||||
const { messages, input, clientSideActions, newSessionState, logs } =
|
||||
await continueBotFlow(session.state)(message)
|
||||
|
||||
const containsSetVariableClientSideAction = clientSideActions?.some(
|
||||
(action) => 'setVariable' in action
|
||||
)
|
||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
||||
|
||||
if (
|
||||
!input &&
|
||||
!containsSetVariableClientSideAction &&
|
||||
session.state.result.answers.length > 0 &&
|
||||
session.state.result.id
|
||||
)
|
||||
await setResultAsCompleted(session.state.result.id)
|
||||
|
||||
await prisma.chatSession.updateMany({
|
||||
where: { id: session.id },
|
||||
data: {
|
||||
state: newSessionState,
|
||||
},
|
||||
})
|
||||
if (newSessionState)
|
||||
await saveStateToDatabase({
|
||||
session: {
|
||||
id: session.id,
|
||||
state: newSessionState,
|
||||
},
|
||||
input,
|
||||
logs: allLogs,
|
||||
clientSideActions,
|
||||
})
|
||||
|
||||
return {
|
||||
messages,
|
||||
@ -125,7 +107,11 @@ export const sendMessage = publicProcedure
|
||||
}
|
||||
)
|
||||
|
||||
const startSession = async (startParams?: StartParams, userId?: string) => {
|
||||
const startSession = async (
|
||||
startParams?: StartParams,
|
||||
userId?: string,
|
||||
clientLogs?: ReplyLog[]
|
||||
) => {
|
||||
if (!startParams)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
@ -231,11 +217,16 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
|
||||
logs: startLogs.length > 0 ? startLogs : undefined,
|
||||
}
|
||||
|
||||
const session = (await prisma.chatSession.create({
|
||||
data: {
|
||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
||||
|
||||
const session = await saveStateToDatabase({
|
||||
session: {
|
||||
state: newSessionState,
|
||||
},
|
||||
})) as ChatSession
|
||||
input,
|
||||
logs: allLogs,
|
||||
clientSideActions,
|
||||
})
|
||||
|
||||
return {
|
||||
resultId: result?.id,
|
||||
@ -270,45 +261,8 @@ const getTypebot = async (
|
||||
'You need to authenticate the request to start a bot in preview mode.',
|
||||
})
|
||||
const typebotQuery = isPreview
|
||||
? await prisma.typebot.findFirst({
|
||||
where: { id: typebot, workspace: { members: { some: { userId } } } },
|
||||
select: {
|
||||
id: true,
|
||||
groups: true,
|
||||
edges: true,
|
||||
settings: true,
|
||||
theme: true,
|
||||
variables: true,
|
||||
isArchived: true,
|
||||
},
|
||||
})
|
||||
: await prisma.publicTypebot.findFirst({
|
||||
where: { typebot: { publicId: typebot } },
|
||||
select: {
|
||||
groups: true,
|
||||
edges: true,
|
||||
settings: true,
|
||||
theme: true,
|
||||
variables: true,
|
||||
typebotId: true,
|
||||
typebot: {
|
||||
select: {
|
||||
isArchived: true,
|
||||
isClosed: true,
|
||||
workspace: {
|
||||
select: {
|
||||
id: true,
|
||||
plan: true,
|
||||
additionalChatsIndex: true,
|
||||
customChatsLimit: true,
|
||||
isQuarantined: true,
|
||||
isSuspended: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
? await findTypebot({ id: typebot, userId })
|
||||
: await findPublicTypebot({ publicId: typebot })
|
||||
|
||||
const parsedTypebot =
|
||||
typebotQuery && 'typebot' in typebotQuery
|
||||
@ -347,7 +301,6 @@ const getTypebot = async (
|
||||
}
|
||||
|
||||
const getResult = async ({
|
||||
typebotId,
|
||||
isPreview,
|
||||
resultId,
|
||||
prefilledVariables,
|
||||
@ -358,56 +311,31 @@ const getResult = async ({
|
||||
isRememberUserEnabled: boolean
|
||||
}) => {
|
||||
if (isPreview) return
|
||||
const select = {
|
||||
id: true,
|
||||
variables: true,
|
||||
answers: { select: { blockId: true, variableId: true, content: true } },
|
||||
} satisfies Prisma.ResultSelect
|
||||
|
||||
const existingResult =
|
||||
resultId && isRememberUserEnabled
|
||||
? ((await prisma.result.findFirst({
|
||||
where: { id: resultId },
|
||||
select,
|
||||
})) as ResultInSession)
|
||||
? ((await findResult({ id: resultId })) as ResultInSession)
|
||||
: undefined
|
||||
|
||||
if (existingResult) {
|
||||
const prefilledVariableWithValue = prefilledVariables.filter(
|
||||
(prefilledVariable) => isDefined(prefilledVariable.value)
|
||||
)
|
||||
const updatedResult = {
|
||||
variables: prefilledVariableWithValue.concat(
|
||||
existingResult.variables.filter(
|
||||
(resultVariable) =>
|
||||
isDefined(resultVariable.value) &&
|
||||
!prefilledVariableWithValue.some(
|
||||
(prefilledVariable) =>
|
||||
prefilledVariable.name === resultVariable.name
|
||||
)
|
||||
)
|
||||
) as VariableWithValue[],
|
||||
}
|
||||
await prisma.result.updateMany({
|
||||
where: { id: existingResult.id },
|
||||
data: updatedResult,
|
||||
})
|
||||
return {
|
||||
id: existingResult.id,
|
||||
variables: updatedResult.variables,
|
||||
answers: existingResult.answers,
|
||||
}
|
||||
} else {
|
||||
return (await prisma.result.create({
|
||||
data: {
|
||||
isCompleted: false,
|
||||
typebotId,
|
||||
variables: prefilledVariables.filter((variable) =>
|
||||
isDefined(variable.value)
|
||||
),
|
||||
},
|
||||
select,
|
||||
})) as ResultInSession
|
||||
const prefilledVariableWithValue = prefilledVariables.filter(
|
||||
(prefilledVariable) => isDefined(prefilledVariable.value)
|
||||
)
|
||||
|
||||
const updatedResult = {
|
||||
variables: prefilledVariableWithValue.concat(
|
||||
existingResult?.variables.filter(
|
||||
(resultVariable) =>
|
||||
isDefined(resultVariable.value) &&
|
||||
!prefilledVariableWithValue.some(
|
||||
(prefilledVariable) =>
|
||||
prefilledVariable.name === resultVariable.name
|
||||
)
|
||||
) ?? []
|
||||
) as VariableWithValue[],
|
||||
}
|
||||
return {
|
||||
id: existingResult?.id ?? createId(),
|
||||
variables: updatedResult.variables,
|
||||
answers: existingResult?.answers,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Prisma } from '@typebot.io/prisma'
|
||||
import got from 'got'
|
||||
import {
|
||||
Block,
|
||||
BlockType,
|
||||
@ -16,7 +13,7 @@ import {
|
||||
SetVariableBlock,
|
||||
WebhookBlock,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isInputBlock, isNotDefined, byId } from '@typebot.io/lib'
|
||||
import { isInputBlock, byId } from '@typebot.io/lib'
|
||||
import { executeGroup } from './executeGroup'
|
||||
import { getNextGroup } from './getNextGroup'
|
||||
import { validateEmail } from '@/features/blocks/inputs/email/validateEmail'
|
||||
@ -28,6 +25,7 @@ import { parseVariables } from '@/features/variables/parseVariables'
|
||||
import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||
import { resumeChatCompletion } from '@/features/blocks/integrations/openai/resumeChatCompletion'
|
||||
import { resumeWebhookExecution } from '@/features/blocks/integrations/webhook/resumeWebhookExecution'
|
||||
import { upsertAnswer } from '../queries/upsertAnswer'
|
||||
|
||||
export const continueBotFlow =
|
||||
(state: SessionState) =>
|
||||
@ -60,13 +58,14 @@ export const continueBotFlow =
|
||||
...existingVariable,
|
||||
value: reply,
|
||||
}
|
||||
newSessionState = await updateVariables(state)([newVariable])
|
||||
newSessionState = updateVariables(state)([newVariable])
|
||||
}
|
||||
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
|
||||
const result = await resumeWebhookExecution(
|
||||
const result = resumeWebhookExecution({
|
||||
state,
|
||||
block
|
||||
)(JSON.parse(reply))
|
||||
block,
|
||||
response: JSON.parse(reply),
|
||||
})
|
||||
if (result.newSessionState) newSessionState = result.newSessionState
|
||||
} else if (
|
||||
block.type === IntegrationBlockType.OPEN_AI &&
|
||||
@ -136,20 +135,20 @@ const processAndSaveAnswer =
|
||||
async (reply: string | null): Promise<SessionState> => {
|
||||
if (!reply) return state
|
||||
let newState = await saveAnswer(state, block, itemId)(reply)
|
||||
newState = await saveVariableValueIfAny(newState, block)(reply)
|
||||
newState = saveVariableValueIfAny(newState, block)(reply)
|
||||
return newState
|
||||
}
|
||||
|
||||
const saveVariableValueIfAny =
|
||||
(state: SessionState, block: InputBlock) =>
|
||||
async (reply: string): Promise<SessionState> => {
|
||||
(reply: string): SessionState => {
|
||||
if (!block.options.variableId) return state
|
||||
const foundVariable = state.typebot.variables.find(
|
||||
(variable) => variable.id === block.options.variableId
|
||||
)
|
||||
if (!foundVariable) return state
|
||||
|
||||
const newSessionState = await updateVariables(state)([
|
||||
const newSessionState = updateVariables(state)([
|
||||
{
|
||||
...foundVariable,
|
||||
value: Array.isArray(foundVariable.value)
|
||||
@ -161,13 +160,6 @@ const saveVariableValueIfAny =
|
||||
return newSessionState
|
||||
}
|
||||
|
||||
export const setResultAsCompleted = async (resultId: string) => {
|
||||
await prisma.result.updateMany({
|
||||
where: { id: resultId },
|
||||
data: { isCompleted: true },
|
||||
})
|
||||
}
|
||||
|
||||
const parseRetryMessage = (
|
||||
block: InputBlock
|
||||
): Pick<ChatReply, 'messages' | 'input'> => {
|
||||
@ -192,55 +184,28 @@ const parseRetryMessage = (
|
||||
const saveAnswer =
|
||||
(state: SessionState, block: InputBlock, itemId?: string) =>
|
||||
async (reply: string): Promise<SessionState> => {
|
||||
const resultId = state.result?.id
|
||||
const answer: Omit<Prisma.AnswerUncheckedCreateInput, 'resultId'> = {
|
||||
blockId: block.id,
|
||||
await upsertAnswer({
|
||||
block,
|
||||
answer: {
|
||||
blockId: block.id,
|
||||
itemId,
|
||||
groupId: block.groupId,
|
||||
content: reply,
|
||||
variableId: block.options.variableId,
|
||||
storageUsed: 0,
|
||||
},
|
||||
reply,
|
||||
state,
|
||||
itemId,
|
||||
groupId: block.groupId,
|
||||
content: reply,
|
||||
variableId: block.options.variableId,
|
||||
storageUsed: 0,
|
||||
}
|
||||
if (state.result.answers.length === 0 && resultId)
|
||||
await setResultAsStarted(resultId)
|
||||
})
|
||||
|
||||
const newSessionState = setNewAnswerInState(state)({
|
||||
return setNewAnswerInState(state)({
|
||||
blockId: block.id,
|
||||
variableId: block.options.variableId ?? null,
|
||||
content: reply,
|
||||
})
|
||||
|
||||
if (resultId) {
|
||||
if (reply.includes('http') && block.type === InputBlockType.FILE) {
|
||||
answer.storageUsed = await computeStorageUsed(reply)
|
||||
}
|
||||
await prisma.answer.upsert({
|
||||
where: {
|
||||
resultId_blockId_groupId: {
|
||||
resultId,
|
||||
blockId: block.id,
|
||||
groupId: block.groupId,
|
||||
},
|
||||
},
|
||||
create: { ...answer, resultId },
|
||||
update: {
|
||||
content: answer.content,
|
||||
storageUsed: answer.storageUsed,
|
||||
itemId: answer.itemId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return newSessionState
|
||||
}
|
||||
|
||||
const setResultAsStarted = async (resultId: string) => {
|
||||
await prisma.result.updateMany({
|
||||
where: { id: resultId },
|
||||
data: { hasStarted: true },
|
||||
})
|
||||
}
|
||||
|
||||
const setNewAnswerInState =
|
||||
(state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => {
|
||||
const newAnswers = state.result.answers
|
||||
@ -256,21 +221,6 @@ const setNewAnswerInState =
|
||||
} satisfies SessionState
|
||||
}
|
||||
|
||||
const computeStorageUsed = async (reply: string) => {
|
||||
let storageUsed = 0
|
||||
const fileUrls = reply.split(', ')
|
||||
const hasReachedStorageLimit = fileUrls[0] === null
|
||||
if (!hasReachedStorageLimit) {
|
||||
for (const url of fileUrls) {
|
||||
const { headers } = await got(url)
|
||||
const size = headers['content-length']
|
||||
if (isNotDefined(size)) continue
|
||||
storageUsed += parseInt(size, 10)
|
||||
}
|
||||
}
|
||||
return storageUsed
|
||||
}
|
||||
|
||||
const getOutgoingEdgeId =
|
||||
({ typebot: { variables } }: Pick<SessionState, 'typebot'>) =>
|
||||
(
|
||||
|
@ -1,5 +0,0 @@
|
||||
export * from './continueBotFlow'
|
||||
export * from './executeGroup'
|
||||
export * from './getNextGroup'
|
||||
export * from './getSessionState'
|
||||
export * from './startBotFlow'
|
48
apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts
Normal file
48
apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts
Normal 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
|
||||
}
|
13
apps/viewer/src/features/chat/queries/createSession.ts
Normal file
13
apps/viewer/src/features/chat/queries/createSession.ts
Normal 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,
|
||||
},
|
||||
})
|
34
apps/viewer/src/features/chat/queries/findPublicTypebot.ts
Normal file
34
apps/viewer/src/features/chat/queries/findPublicTypebot.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
14
apps/viewer/src/features/chat/queries/findResult.ts
Normal file
14
apps/viewer/src/features/chat/queries/findResult.ts
Normal 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 } },
|
||||
},
|
||||
})
|
20
apps/viewer/src/features/chat/queries/findTypebot.ts
Normal file
20
apps/viewer/src/features/chat/queries/findTypebot.ts
Normal 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,
|
||||
},
|
||||
})
|
5
apps/viewer/src/features/chat/queries/saveLogs.ts
Normal file
5
apps/viewer/src/features/chat/queries/saveLogs.ts
Normal 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 })
|
15
apps/viewer/src/features/chat/queries/updateSession.ts
Normal file
15
apps/viewer/src/features/chat/queries/updateSession.ts
Normal 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,
|
||||
},
|
||||
})
|
57
apps/viewer/src/features/chat/queries/upsertAnswer.ts
Normal file
57
apps/viewer/src/features/chat/queries/upsertAnswer.ts
Normal 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
|
||||
}
|
34
apps/viewer/src/features/chat/queries/upsertResult.ts
Normal file
34
apps/viewer/src/features/chat/queries/upsertResult.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
11
apps/viewer/src/features/logs/helpers/formatLogDetails.ts
Normal file
11
apps/viewer/src/features/logs/helpers/formatLogDetails.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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 })
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import {
|
||||
SessionState,
|
||||
@ -10,7 +9,7 @@ import { safeStringify } from './safeStringify'
|
||||
|
||||
export const updateVariables =
|
||||
(state: SessionState) =>
|
||||
async (newVariables: VariableWithUnknowValue[]): Promise<SessionState> => ({
|
||||
(newVariables: VariableWithUnknowValue[]): SessionState => ({
|
||||
...state,
|
||||
typebot: {
|
||||
...state.typebot,
|
||||
@ -18,15 +17,13 @@ export const updateVariables =
|
||||
},
|
||||
result: {
|
||||
...state.result,
|
||||
variables: await updateResultVariables(state)(newVariables),
|
||||
variables: updateResultVariables(state)(newVariables),
|
||||
},
|
||||
})
|
||||
|
||||
const updateResultVariables =
|
||||
({ result }: Pick<SessionState, 'result' | 'typebot'>) =>
|
||||
async (
|
||||
newVariables: VariableWithUnknowValue[]
|
||||
): Promise<VariableWithValue[]> => {
|
||||
(newVariables: VariableWithUnknowValue[]): VariableWithValue[] => {
|
||||
const serializedNewVariables = newVariables.map((variable) => ({
|
||||
...variable,
|
||||
value: Array.isArray(variable.value)
|
||||
@ -43,16 +40,6 @@ const updateResultVariables =
|
||||
...serializedNewVariables,
|
||||
].filter((variable) => isDefined(variable.value)) as VariableWithValue[]
|
||||
|
||||
if (result.id)
|
||||
await prisma.result.updateMany({
|
||||
where: {
|
||||
id: result.id,
|
||||
},
|
||||
data: {
|
||||
variables: updatedVariables,
|
||||
},
|
||||
})
|
||||
|
||||
return updatedVariables
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user