2
0

🔥 Remove disable response saving option

Doesn't work properly when it comes to keep tracking storage usage
This commit is contained in:
Baptiste Arnaud
2023-03-07 14:41:57 +01:00
parent 0c19ea20f8
commit b77e2c8d2c
26 changed files with 194 additions and 182 deletions

View File

@ -11,17 +11,18 @@ import Stripe from 'stripe'
import { decrypt } from 'utils/api/encryption'
export const computePaymentInputRuntimeOptions =
(state: Pick<SessionState, 'isPreview' | 'typebot'>) =>
(state: Pick<SessionState, 'result' | 'typebot'>) =>
(options: PaymentInputOptions) =>
createStripePaymentIntent(state)(options)
const createStripePaymentIntent =
(state: Pick<SessionState, 'isPreview' | 'typebot'>) =>
(state: Pick<SessionState, 'result' | 'typebot'>) =>
async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => {
const {
isPreview,
result,
typebot: { variables },
} = state
const isPreview = !result.id
if (!options.credentialsId)
throw new TRPCError({
code: 'BAD_REQUEST',

View File

@ -50,10 +50,11 @@ if (window.$chatwoot) {
}`
export const executeChatwootBlock = (
{ typebot: { variables }, isPreview }: SessionState,
{ typebot: { variables }, result }: SessionState,
block: ChatwootBlock,
lastBubbleBlockId?: string
): ExecuteIntegrationResponse => {
const isPreview = !result.id
const chatwootCode = parseChatwootOpenCode(block.options)
return {
outgoingEdgeId: block.outgoingEdgeId,

View File

@ -6,7 +6,7 @@ import { render } from '@faire/mjml-react/utils/render'
import { DefaultBotNotificationEmail } from 'emails'
import {
PublicTypebot,
ResultValues,
ResultInSession,
SendEmailBlock,
SendEmailOptions,
SessionState,
@ -20,11 +20,12 @@ import { decrypt } from 'utils/api'
import { defaultFrom, defaultTransportOptions } from '../constants'
export const executeSendEmailBlock = async (
{ result, typebot, isPreview }: SessionState,
{ result, typebot }: SessionState,
block: SendEmailBlock
): Promise<ExecuteIntegrationResponse> => {
const { options } = block
const { variables } = typebot
const isPreview = !result.id
if (isPreview)
return {
outgoingEdgeId: block.outgoingEdgeId,
@ -37,7 +38,7 @@ export const executeSendEmailBlock = async (
}
await sendEmail({
typebotId: typebot.id,
resultId: result?.id,
result,
credentialsId: options.credentialsId,
recipients: options.recipients.map(parseVariables(variables)),
subject: parseVariables(variables)(options.subject ?? ''),
@ -57,7 +58,7 @@ export const executeSendEmailBlock = async (
const sendEmail = async ({
typebotId,
resultId,
result,
credentialsId,
recipients,
body,
@ -70,7 +71,7 @@ const sendEmail = async ({
fileUrls,
}: SendEmailOptions & {
typebotId: string
resultId?: string
result: ResultInSession
fileUrls?: string | string[]
}) => {
const { name: replyToName } = parseEmailRecipient(replyTo)
@ -94,12 +95,12 @@ const sendEmail = async ({
isCustomBody,
isBodyCode,
typebotId,
resultId,
result,
})
if (!emailBody) {
await saveErrorLog({
resultId,
resultId: result.id,
message: 'Email not sent',
details: {
error: 'No email body found',
@ -132,7 +133,7 @@ const sendEmail = async ({
try {
await transporter.sendMail(email)
await saveSuccessLog({
resultId,
resultId: result.id,
message: 'Email successfully sent',
details: {
transportConfig: {
@ -144,7 +145,7 @@ const sendEmail = async ({
})
} catch (err) {
await saveErrorLog({
resultId,
resultId: result.id,
message: 'Email not sent',
details: {
error: err,
@ -182,10 +183,10 @@ const getEmailBody = async ({
isCustomBody,
isBodyCode,
typebotId,
resultId,
result,
}: {
typebotId: string
resultId?: string
result: ResultInSession
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
{ html?: string; text?: string } | undefined
> => {
@ -198,12 +199,7 @@ const getEmailBody = async ({
where: { typebotId },
})) as unknown as PublicTypebot
if (!typebot) return
const resultValues = (await prisma.result.findUnique({
where: { id: resultId },
include: { answers: true },
})) as ResultValues | null
if (!resultValues) return
const answers = parseAnswers(typebot, [])(resultValues)
const answers = parseAnswers(typebot, [])(result)
return {
html: render(
<DefaultBotNotificationEmail

View File

@ -16,16 +16,15 @@ import {
WebhookOptions,
defaultWebhookAttributes,
HttpMethod,
ResultValues,
PublicTypebot,
KeyValue,
ReplyLog,
ResultInSession,
} from 'models'
import { stringify } from 'qs'
import { byId, omit } from 'utils'
import { parseAnswers } from 'utils/results'
import got, { Method, Headers, HTTPError } from 'got'
import { getResultValues } from '@/features/results/api'
import { parseSampleResult } from './parseSampleResult'
export const executeWebhookBlock = async (
@ -50,14 +49,11 @@ export const executeWebhookBlock = async (
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
}
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const resultValues =
(result && (await getResultValues(result.id))) ?? undefined
const webhookResponse = await executeWebhook({ typebot })(
preparedWebhook,
typebot.variables,
block.groupId,
resultValues,
result?.id
result
)
const status = webhookResponse.statusCode.toString()
const isError = status.startsWith('4') || status.startsWith('5')
@ -139,8 +135,7 @@ export const executeWebhook =
webhook: Webhook,
variables: Variable[],
groupId: string,
resultValues?: ResultValues,
resultId?: string
result: ResultInSession
): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method)
return {
@ -176,7 +171,7 @@ export const executeWebhook =
[]
)({
body: webhook.body,
resultValues,
result,
groupId,
variables,
})
@ -206,7 +201,7 @@ export const executeWebhook =
try {
const response = await got(request.url, omit(request, 'url'))
await saveSuccessLog({
resultId,
resultId: result.id,
message: 'Webhook successfuly executed.',
details: {
statusCode: response.statusCode,
@ -225,7 +220,7 @@ export const executeWebhook =
data: safeJsonParse(error.response.body as string).data,
}
await saveErrorLog({
resultId,
resultId: result.id,
message: 'Webhook returned an error',
details: {
request,
@ -240,7 +235,7 @@ export const executeWebhook =
}
console.error(error)
await saveErrorLog({
resultId,
resultId: result.id,
message: 'Webhook failed to execute',
details: {
request,
@ -258,20 +253,20 @@ const getBodyContent =
) =>
async ({
body,
resultValues,
result,
groupId,
variables,
}: {
body?: string | null
resultValues?: ResultValues
result?: ResultInSession
groupId: string
variables: Variable[]
}): Promise<string | undefined> => {
if (!body) return
return body === '{{state}}'
? JSON.stringify(
resultValues
? parseAnswers(typebot, linkedTypebots)(resultValues)
result
? parseAnswers(typebot, linkedTypebots)(result)
: await parseSampleResult(typebot, linkedTypebots)(
groupId,
variables

View File

@ -29,7 +29,7 @@ export const parseSampleResult =
return {
message: 'This is a sample result, it has been generated ⬇️',
'Submitted at': new Date().toISOString(),
submittedAt: new Date().toISOString(),
...parseResultSample(linkedInputBlocks, header, variables),
}
}

View File

@ -115,7 +115,8 @@ const getLinkedTypebot = async (
state: SessionState,
typebotId: string
): Promise<TypebotInSession | null> => {
const { typebot, isPreview } = state
const { typebot, result } = state
const isPreview = !result.id
if (typebotId === 'current') return typebot
const availableTypebots =
'linkedTypebots' in state
@ -123,12 +124,12 @@ const getLinkedTypebot = async (
: [typebot]
const linkedTypebot =
availableTypebots.find(byId(typebotId)) ??
(await fetchTypebot({ isPreview }, typebotId))
(await fetchTypebot(isPreview, typebotId))
return linkedTypebot
}
const fetchTypebot = async (
{ isPreview }: Pick<SessionState, 'isPreview'>,
isPreview: boolean,
typebotId: string
): Promise<TypebotInSession | null> => {
if (isPreview) {

View File

@ -13,7 +13,7 @@ import {
ChatReply,
chatReplySchema,
ChatSession,
Result,
ResultInSession,
sendMessageInputSchema,
SessionState,
StartParams,
@ -85,7 +85,11 @@ export const sendMessageProcedure = publicProcedure
},
})
if (!input && session.state.result?.hasStarted)
if (
!input &&
session.state.result.answers.length > 0 &&
session.state.result.id
)
await setResultAsCompleted(session.state.result.id)
return {
@ -106,9 +110,6 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
message: 'No typebot provided in startParams',
})
const isPreview =
startParams?.isPreview || typeof startParams?.typebot !== 'string'
const typebot = await getTypebot(startParams, userId)
const prefilledVariables = startParams.prefilledVariables
@ -117,8 +118,8 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
const result = await getResult({
...startParams,
isPreview,
typebot: typebot.id,
isPreview: startParams.isPreview || typeof startParams.typebot !== 'string',
typebotId: typebot.id,
prefilledVariables,
isNewResultOnRefreshEnabled:
typebot.settings.general.isNewResultOnRefreshEnabled ?? false,
@ -140,10 +141,11 @@ const startSession = async (startParams?: StartParams, userId?: string) => {
typebots: [],
queue: [],
},
result: result
? { id: result.id, variables: result.variables, hasStarted: false }
: undefined,
isPreview,
result: {
id: result?.id,
variables: result?.variables ?? [],
answers: result?.answers ?? [],
},
currentTypebotId: typebot.id,
dynamicTheme: parseDynamicThemeInState(typebot.theme),
}
@ -297,20 +299,21 @@ const getTypebot = async (
}
const getResult = async ({
typebot,
typebotId,
isPreview,
resultId,
prefilledVariables,
isNewResultOnRefreshEnabled,
}: Pick<StartParams, 'isPreview' | 'resultId' | 'typebot'> & {
}: Pick<StartParams, 'isPreview' | 'resultId'> & {
typebotId: string
prefilledVariables: Variable[]
isNewResultOnRefreshEnabled: boolean
}) => {
if (isPreview || typeof typebot !== 'string') return
if (isPreview) return
const select = {
id: true,
variables: true,
hasStarted: true,
answers: { select: { blockId: true, variableId: true, content: true } },
} satisfies Prisma.ResultSelect
const existingResult =
@ -318,7 +321,7 @@ const getResult = async ({
? ((await prisma.result.findFirst({
where: { id: resultId },
select,
})) as Pick<Result, 'id' | 'variables' | 'hasStarted'>)
})) as ResultInSession)
: undefined
if (existingResult) {
@ -344,19 +347,19 @@ const getResult = async ({
return {
id: existingResult.id,
variables: updatedResult.variables,
hasStarted: existingResult.hasStarted,
answers: existingResult.answers,
}
} else {
return (await prisma.result.create({
data: {
isCompleted: false,
typebotId: typebot,
typebotId,
variables: prefilledVariables.filter((variable) =>
isDefined(variable.value)
),
},
select,
})) as Pick<Result, 'id' | 'variables' | 'hasStarted'>
})) as ResultInSession
}
}

View File

@ -7,6 +7,7 @@ import { validateUrl } from '@/features/blocks/inputs/url/api'
import { parseVariables, updateVariables } from '@/features/variables'
import prisma from '@/lib/prisma'
import { TRPCError } from '@trpc/server'
import { Prisma } from 'db'
import got from 'got'
import {
Block,
@ -15,6 +16,7 @@ import {
ChatReply,
InputBlock,
InputBlockType,
ResultInSession,
SessionState,
} from 'models'
import { isInputBlock, isNotDefined } from 'utils'
@ -86,17 +88,9 @@ const processAndSaveAnswer =
(state: SessionState, block: InputBlock) =>
async (reply: string | null): Promise<SessionState> => {
if (!reply) return state
if (!state.isPreview && state.result) {
await saveAnswer(state.result.id, block)(reply)
if (!state.result.hasStarted) await setResultAsStarted(state.result.id)
}
const newState = await saveVariableValueIfAny(state, block)(reply)
return {
...newState,
result: newState.result
? { ...newState.result, hasStarted: true }
: undefined,
}
let newState = await saveAnswer(state, block)(reply)
newState = await saveVariableValueIfAny(newState, block)(reply)
return newState
}
const saveVariableValueIfAny =
@ -115,13 +109,6 @@ const saveVariableValueIfAny =
return newSessionState
}
const setResultAsStarted = async (resultId: string) => {
await prisma.result.update({
where: { id: resultId },
data: { hasStarted: true },
})
}
export const setResultAsCompleted = async (resultId: string) => {
await prisma.result.update({
where: { id: resultId },
@ -152,31 +139,65 @@ const parseRetryMessage = (
}
const saveAnswer =
(resultId: string, block: InputBlock) => async (reply: string) => {
(state: SessionState, block: InputBlock) =>
async (reply: string): Promise<SessionState> => {
const resultId = state.result?.id
const answer = {
resultId: resultId,
resultId,
blockId: block.id,
groupId: block.groupId,
content: reply,
variableId: block.options.variableId,
storageUsed: 0,
}
if (state.result.answers.length === 0 && state.result.id)
await setResultAsStarted(state.result.id)
const newSessionState = setNewAnswerInState(state)({
blockId: block.id,
variableId: block.options.variableId ?? null,
content: reply,
})
if (reply.includes('http') && block.type === InputBlockType.FILE) {
answer.storageUsed = await computeStorageUsed(reply)
}
await prisma.answer.upsert({
where: {
resultId_blockId_groupId: {
resultId,
groupId: block.groupId,
blockId: block.id,
if (resultId)
await prisma.answer.upsert({
where: {
resultId_blockId_groupId: {
resultId,
groupId: block.groupId,
blockId: block.id,
},
},
create: answer as Prisma.AnswerUncheckedCreateInput,
update: answer,
})
return newSessionState
}
const setResultAsStarted = async (resultId: string) => {
await prisma.result.update({
where: { id: resultId },
data: { hasStarted: true },
})
}
const setNewAnswerInState =
(state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => {
const newAnswers = state.result.answers
.filter((answer) => answer.blockId !== newAnswer.blockId)
.concat(newAnswer)
return {
...state,
result: {
...state.result,
answers: newAnswers,
},
create: answer,
update: answer,
})
} satisfies SessionState
}
const computeStorageUsed = async (reply: string) => {

View File

@ -113,7 +113,7 @@ export const executeGroup =
}
const computeRuntimeOptions =
(state: Pick<SessionState, 'isPreview' | 'typebot'>) =>
(state: Pick<SessionState, 'result' | 'typebot'>) =>
(block: InputBlock): Promise<RuntimeOptions> | undefined => {
switch (block.type) {
case InputBlockType.PAYMENT: {
@ -158,7 +158,7 @@ const parseBubbleBlock =
}
const injectVariablesValueInBlock =
(state: Pick<SessionState, 'isPreview' | 'typebot'>) =>
(state: Pick<SessionState, 'result' | 'typebot'>) =>
async (block: InputBlock): Promise<ChatReply['input']> => {
switch (block.type) {
case InputBlockType.CHOICE: {

View File

@ -1 +0,0 @@
export * from './utils'

View File

@ -1,8 +0,0 @@
import prisma from '@/lib/prisma'
import { ResultValues } from 'models'
export const getResultValues = async (resultId: string) =>
(await prisma.result.findUnique({
where: { id: resultId },
include: { answers: true },
})) as ResultValues | null

View File

@ -1 +0,0 @@
export * from './getResultValues'

View File

@ -157,12 +157,10 @@ export const updateVariables =
...state.typebot,
variables: updateTypebotVariables(state)(newVariables),
},
result: state.result
? {
...state.result,
variables: await updateResultVariables(state)(newVariables),
}
: undefined,
result: {
...state.result,
variables: await updateResultVariables(state)(newVariables),
},
})
const updateResultVariables =
@ -170,7 +168,6 @@ const updateResultVariables =
async (
newVariables: VariableWithUnknowValue[]
): Promise<VariableWithValue[]> => {
if (!result) return []
const serializedNewVariables = newVariables.map((variable) => ({
...variable,
value: Array.isArray(variable.value)
@ -187,14 +184,15 @@ const updateResultVariables =
...serializedNewVariables,
].filter((variable) => isDefined(variable.value)) as VariableWithValue[]
await prisma.result.update({
where: {
id: result.id,
},
data: {
variables: updatedVariables,
},
})
if (result.id)
await prisma.result.update({
where: {
id: result.id,
},
data: {
variables: updatedVariables,
},
})
return updatedVariables
}