2
0

feat(webhook): ️ Show linked typebots results in webhook sample

This commit is contained in:
Baptiste Arnaud
2022-04-21 09:18:35 -07:00
parent 937621ee07
commit 12f43cdb88
13 changed files with 249 additions and 88 deletions

View File

@ -1,13 +1,13 @@
import { CollaborationType, Prisma, User } from 'db'
const parseWhereFilter = (
typebotId: string,
typebotIds: string[] | string,
user: User,
type: 'read' | 'write'
): Prisma.TypebotWhereInput => ({
OR: [
{
id: typebotId,
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
ownerId:
(type === 'read' && user.email === process.env.ADMIN_EMAIL) ||
process.env.NEXT_PUBLIC_E2E_TEST
@ -15,7 +15,7 @@ const parseWhereFilter = (
: user.id,
},
{
id: typebotId,
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
collaborators: {
some: {
userId: user.id,
@ -31,3 +31,9 @@ export const canReadTypebot = (typebotId: string, user: User) =>
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')

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View 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')

View File

@ -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
}

View File

@ -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
}