feat(webhook): ⚡️ Show linked typebots results in webhook sample
This commit is contained in:
@ -1,13 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import Cors from 'cors'
|
||||
import { initMiddleware, methodNotAllowed } from 'utils'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await cors(req, res)
|
||||
if (req.method === 'GET') return res.status(200).send({ message: 'success' })
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
@ -27,7 +27,11 @@ import { stringify } from 'qs'
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import Cors from 'cors'
|
||||
import { parseSampleResult } from 'services/api/webhooks'
|
||||
import { saveErrorLog, saveSuccessLog } from 'services/api/utils'
|
||||
import {
|
||||
getLinkedTypebots,
|
||||
saveErrorLog,
|
||||
saveSuccessLog,
|
||||
} from 'services/api/utils'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
@ -117,9 +121,13 @@ export const executeWebhook =
|
||||
convertKeyValueTableToObject(webhook.queryParams, variables)
|
||||
)
|
||||
const contentType = headers ? headers['Content-Type'] : undefined
|
||||
const linkedTypebots = await getLinkedTypebots(typebot)
|
||||
const body =
|
||||
webhook.method !== HttpMethod.GET
|
||||
? getBodyContent(typebot)({
|
||||
? await getBodyContent(
|
||||
typebot,
|
||||
linkedTypebots
|
||||
)({
|
||||
body: webhook.body,
|
||||
resultValues,
|
||||
blockId,
|
||||
@ -178,8 +186,11 @@ export const executeWebhook =
|
||||
}
|
||||
|
||||
const getBodyContent =
|
||||
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
||||
({
|
||||
(
|
||||
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>,
|
||||
linkedTypebots: (Typebot | PublicTypebot)[]
|
||||
) =>
|
||||
async ({
|
||||
body,
|
||||
resultValues,
|
||||
blockId,
|
||||
@ -187,13 +198,22 @@ const getBodyContent =
|
||||
body?: string | null
|
||||
resultValues?: ResultValues
|
||||
blockId: string
|
||||
}): string | undefined => {
|
||||
}): Promise<string | undefined> => {
|
||||
if (!body) return
|
||||
return body === '{{state}}'
|
||||
? JSON.stringify(
|
||||
resultValues
|
||||
? parseAnswers(typebot)(resultValues)
|
||||
: parseSampleResult(typebot)(blockId)
|
||||
? parseAnswers({
|
||||
blocks: [
|
||||
...typebot.blocks,
|
||||
...linkedTypebots.flatMap((t) => t.blocks),
|
||||
],
|
||||
variables: [
|
||||
...typebot.variables,
|
||||
...linkedTypebots.flatMap((t) => t.variables),
|
||||
],
|
||||
})(resultValues)
|
||||
: await parseSampleResult(typebot, linkedTypebots)(blockId)
|
||||
)
|
||||
: body
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import prisma from 'libs/prisma'
|
||||
import { Typebot } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authenticateUser } from 'services/api/utils'
|
||||
import { authenticateUser, getLinkedTypebots } from 'services/api/utils'
|
||||
import { parseSampleResult } from 'services/api/webhooks'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
|
||||
@ -19,7 +19,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
.flatMap((b) => b.steps)
|
||||
.find((s) => s.id === stepId)
|
||||
if (!step) return res.status(404).send({ message: 'Block not found' })
|
||||
return res.send(parseSampleResult(typebot)(step.blockId))
|
||||
const linkedTypebots = await getLinkedTypebots(typebot, user)
|
||||
return res.send(
|
||||
await parseSampleResult(typebot, linkedTypebots)(step.blockId)
|
||||
)
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import prisma from 'libs/prisma'
|
||||
import { Typebot } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authenticateUser } from 'services/api/utils'
|
||||
import { authenticateUser, getLinkedTypebots } from 'services/api/utils'
|
||||
import { parseSampleResult } from 'services/api/webhooks'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
|
||||
@ -15,7 +15,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
||||
})) as unknown as Typebot | undefined
|
||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||
return res.send(parseSampleResult(typebot)(blockId))
|
||||
const linkedTypebots = await getLinkedTypebots(typebot, user)
|
||||
return res.send(await parseSampleResult(typebot, linkedTypebots)(blockId))
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
39
apps/viewer/services/api/dbRules.ts
Normal file
39
apps/viewer/services/api/dbRules.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { CollaborationType, Prisma, User } from 'db'
|
||||
|
||||
const parseWhereFilter = (
|
||||
typebotIds: string[] | string,
|
||||
user: User,
|
||||
type: 'read' | 'write'
|
||||
): Prisma.TypebotWhereInput => ({
|
||||
OR: [
|
||||
{
|
||||
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
||||
ownerId:
|
||||
(type === 'read' && user.email === process.env.ADMIN_EMAIL) ||
|
||||
process.env.NEXT_PUBLIC_E2E_TEST
|
||||
? undefined
|
||||
: user.id,
|
||||
},
|
||||
{
|
||||
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
||||
collaborators: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
type: type === 'write' ? CollaborationType.WRITE : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export const canReadTypebot = (typebotId: string, user: User) =>
|
||||
parseWhereFilter(typebotId, user, 'read')
|
||||
|
||||
export const canWriteTypebot = (typebotId: string, user: User) =>
|
||||
parseWhereFilter(typebotId, user, 'write')
|
||||
|
||||
export const canReadTypebots = (typebotIds: string[], user: User) =>
|
||||
parseWhereFilter(typebotIds, user, 'read')
|
||||
|
||||
export const canWriteTypebots = (typebotIds: string[], user: User) =>
|
||||
parseWhereFilter(typebotIds, user, 'write')
|
@ -1,6 +1,9 @@
|
||||
import { User } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { LogicStepType, Typebot, TypebotLinkStep, PublicTypebot } from 'models'
|
||||
import { NextApiRequest } from 'next'
|
||||
import { isDefined } from 'utils'
|
||||
import { canReadTypebots } from './dbRules'
|
||||
|
||||
export const authenticateUser = async (
|
||||
req: NextApiRequest
|
||||
@ -52,3 +55,34 @@ const formatDetails = (details: any) => {
|
||||
return details
|
||||
}
|
||||
}
|
||||
|
||||
export const getLinkedTypebots = async (
|
||||
typebot: Typebot | PublicTypebot,
|
||||
user?: User
|
||||
): Promise<(Typebot | PublicTypebot)[]> => {
|
||||
const linkedTypebotIds = (
|
||||
typebot.blocks
|
||||
.flatMap((b) => b.steps)
|
||||
.filter(
|
||||
(s) =>
|
||||
s.type === LogicStepType.TYPEBOT_LINK &&
|
||||
isDefined(s.options.typebotId)
|
||||
) as TypebotLinkStep[]
|
||||
).map((s) => s.options.typebotId as string)
|
||||
if (linkedTypebotIds.length === 0) return []
|
||||
const typebots = (await ('typebotId' in typebot
|
||||
? prisma.publicTypebot.findMany({
|
||||
where: { id: { in: linkedTypebotIds } },
|
||||
})
|
||||
: prisma.typebot.findMany({
|
||||
where: user
|
||||
? {
|
||||
AND: [
|
||||
{ id: { in: linkedTypebotIds } },
|
||||
canReadTypebots(linkedTypebotIds, user as User),
|
||||
],
|
||||
}
|
||||
: { id: { in: linkedTypebotIds } },
|
||||
}))) as unknown as (Typebot | PublicTypebot)[]
|
||||
return typebots
|
||||
}
|
||||
|
@ -1,26 +1,84 @@
|
||||
import {
|
||||
InputStep,
|
||||
InputStepType,
|
||||
LogicStepType,
|
||||
PublicTypebot,
|
||||
ResultHeaderCell,
|
||||
Step,
|
||||
Typebot,
|
||||
TypebotLinkStep,
|
||||
} from 'models'
|
||||
import { isInputStep, byId, parseResultHeader, isNotDefined } from 'utils'
|
||||
|
||||
export const parseSampleResult =
|
||||
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
||||
(currentBlockId: string): Record<string, string> => {
|
||||
const header = parseResultHeader(typebot)
|
||||
const previousInputSteps = getPreviousInputSteps(typebot)({
|
||||
blockId: currentBlockId,
|
||||
(
|
||||
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>,
|
||||
linkedTypebots: (Typebot | PublicTypebot)[]
|
||||
) =>
|
||||
async (currentBlockId: string): Promise<Record<string, string>> => {
|
||||
const header = parseResultHeader({
|
||||
blocks: [...typebot.blocks, ...linkedTypebots.flatMap((t) => t.blocks)],
|
||||
variables: [
|
||||
...typebot.variables,
|
||||
...linkedTypebots.flatMap((t) => t.variables),
|
||||
],
|
||||
})
|
||||
const linkedInputSteps = await extractLinkedInputSteps(
|
||||
typebot,
|
||||
linkedTypebots
|
||||
)(currentBlockId)
|
||||
|
||||
return {
|
||||
message: 'This is a sample result, it has been generated ⬇️',
|
||||
'Submitted at': new Date().toISOString(),
|
||||
...parseBlocksResultSample(previousInputSteps, header),
|
||||
...parseBlocksResultSample(linkedInputSteps, header),
|
||||
}
|
||||
}
|
||||
|
||||
const extractLinkedInputSteps =
|
||||
(
|
||||
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>,
|
||||
linkedTypebots: (Typebot | PublicTypebot)[]
|
||||
) =>
|
||||
async (
|
||||
currentBlockId?: string,
|
||||
direction: 'backward' | 'forward' = 'backward'
|
||||
): Promise<InputStep[]> => {
|
||||
const previousLinkedTypebotSteps = walkEdgesAndExtract(
|
||||
'linkedBot',
|
||||
direction,
|
||||
typebot
|
||||
)({
|
||||
blockId: currentBlockId,
|
||||
}) as TypebotLinkStep[]
|
||||
|
||||
const linkedBotInputs =
|
||||
previousLinkedTypebotSteps.length > 0
|
||||
? await Promise.all(
|
||||
previousLinkedTypebotSteps.map((linkedBot) =>
|
||||
extractLinkedInputSteps(
|
||||
linkedTypebots.find((t) =>
|
||||
'typebotId' in t
|
||||
? t.typebotId === linkedBot.options.typebotId
|
||||
: t.id === linkedBot.options.typebotId
|
||||
) as Typebot | PublicTypebot,
|
||||
linkedTypebots
|
||||
)(linkedBot.options.blockId, 'forward')
|
||||
)
|
||||
)
|
||||
: []
|
||||
|
||||
return (
|
||||
walkEdgesAndExtract(
|
||||
'input',
|
||||
direction,
|
||||
typebot
|
||||
)({
|
||||
blockId: currentBlockId,
|
||||
}) as InputStep[]
|
||||
).concat(linkedBotInputs.flatMap((l) => l))
|
||||
}
|
||||
|
||||
const parseBlocksResultSample = (
|
||||
inputSteps: InputStep[],
|
||||
header: ResultHeaderCell[]
|
||||
@ -63,50 +121,71 @@ const getSampleValue = (step: InputStep) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getPreviousInputSteps =
|
||||
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
||||
({ blockId }: { blockId: string }): InputStep[] => {
|
||||
const previousInputSteps = getPreviousInputStepsInBlock(typebot)({
|
||||
blockId,
|
||||
const walkEdgesAndExtract =
|
||||
(
|
||||
type: 'input' | 'linkedBot',
|
||||
direction: 'backward' | 'forward',
|
||||
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>
|
||||
) =>
|
||||
({ blockId }: { blockId?: string }): Step[] => {
|
||||
const currentBlockId =
|
||||
blockId ??
|
||||
(typebot.blocks.find((b) => b.steps[0].type === 'start')?.id as string)
|
||||
const stepsInBlock = extractStepsInBlock(
|
||||
type,
|
||||
typebot
|
||||
)({
|
||||
blockId: currentBlockId,
|
||||
})
|
||||
const previousBlockIds = getPreviousBlockIds(typebot)(blockId)
|
||||
const otherBlockIds = getBlockIds(typebot, direction)(currentBlockId)
|
||||
return [
|
||||
...previousInputSteps,
|
||||
...previousBlockIds.flatMap((blockId) =>
|
||||
getPreviousInputStepsInBlock(typebot)({ blockId })
|
||||
...stepsInBlock,
|
||||
...otherBlockIds.flatMap((blockId) =>
|
||||
extractStepsInBlock(type, typebot)({ blockId })
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
const getPreviousBlockIds =
|
||||
const getBlockIds =
|
||||
(
|
||||
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>,
|
||||
direction: 'backward' | 'forward',
|
||||
existingBlockIds?: string[]
|
||||
) =>
|
||||
(blockId: string): string[] => {
|
||||
const previousBlocks = typebot.edges.reduce<string[]>(
|
||||
(blockIds, edge) =>
|
||||
(!existingBlockIds || !existingBlockIds.includes(edge.from.blockId)) &&
|
||||
const blocks = typebot.edges.reduce<string[]>((blockIds, edge) => {
|
||||
if (direction === 'forward')
|
||||
return (!existingBlockIds ||
|
||||
!existingBlockIds?.includes(edge.to.blockId)) &&
|
||||
edge.from.blockId === blockId
|
||||
? [...blockIds, edge.to.blockId]
|
||||
: blockIds
|
||||
return (!existingBlockIds ||
|
||||
!existingBlockIds.includes(edge.from.blockId)) &&
|
||||
edge.to.blockId === blockId
|
||||
? [...blockIds, edge.from.blockId]
|
||||
: blockIds,
|
||||
[]
|
||||
)
|
||||
const newBlocks = [...(existingBlockIds ?? []), ...previousBlocks]
|
||||
return previousBlocks.concat(
|
||||
previousBlocks.flatMap(getPreviousBlockIds(typebot, newBlocks))
|
||||
? [...blockIds, edge.from.blockId]
|
||||
: blockIds
|
||||
}, [])
|
||||
const newBlocks = [...(existingBlockIds ?? []), ...blocks]
|
||||
return blocks.concat(
|
||||
blocks.flatMap(getBlockIds(typebot, direction, newBlocks))
|
||||
)
|
||||
}
|
||||
|
||||
const getPreviousInputStepsInBlock =
|
||||
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
||||
const extractStepsInBlock =
|
||||
(
|
||||
type: 'input' | 'linkedBot',
|
||||
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>
|
||||
) =>
|
||||
({ blockId, stepId }: { blockId: string; stepId?: string }) => {
|
||||
const currentBlock = typebot.blocks.find(byId(blockId))
|
||||
if (!currentBlock) return []
|
||||
const inputSteps: InputStep[] = []
|
||||
const steps: Step[] = []
|
||||
for (const step of currentBlock.steps) {
|
||||
if (step.id === stepId) break
|
||||
if (isInputStep(step)) inputSteps.push(step)
|
||||
if (type === 'input' && isInputStep(step)) steps.push(step)
|
||||
if (type === 'linkedBot' && step.type === LogicStepType.TYPEBOT_LINK)
|
||||
steps.push(step)
|
||||
}
|
||||
return inputSteps
|
||||
return steps
|
||||
}
|
||||
|
Reference in New Issue
Block a user