diff --git a/apps/builder/services/api/dbRules.ts b/apps/builder/services/api/dbRules.ts index b6669c9d7b..ab5312c542 100644 --- a/apps/builder/services/api/dbRules.ts +++ b/apps/builder/services/api/dbRules.ts @@ -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') diff --git a/apps/viewer/pages/api/ping.ts b/apps/viewer/pages/api/ping.ts deleted file mode 100644 index 570cf2da65..0000000000 --- a/apps/viewer/pages/api/ping.ts +++ /dev/null @@ -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 diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts index 37f0b80d02..dc32e2dced 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts @@ -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: Pick, + linkedTypebots: (Typebot | PublicTypebot)[] + ) => + async ({ body, resultValues, blockId, @@ -187,13 +198,22 @@ const getBodyContent = body?: string | null resultValues?: ResultValues blockId: string - }): string | undefined => { + }): Promise => { 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 } diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts index 85f400c858..2ea8d11cc3 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/sampleResult.ts @@ -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) } diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts index f13ef36daf..9b067ef3b4 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts @@ -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) } diff --git a/apps/viewer/services/api/dbRules.ts b/apps/viewer/services/api/dbRules.ts new file mode 100644 index 0000000000..ab5312c542 --- /dev/null +++ b/apps/viewer/services/api/dbRules.ts @@ -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') diff --git a/apps/viewer/services/api/utils.ts b/apps/viewer/services/api/utils.ts index bc87d02291..68e5f6dd17 100644 --- a/apps/viewer/services/api/utils.ts +++ b/apps/viewer/services/api/utils.ts @@ -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 +} diff --git a/apps/viewer/services/api/webhooks.ts b/apps/viewer/services/api/webhooks.ts index 21a54881e8..256c02952f 100644 --- a/apps/viewer/services/api/webhooks.ts +++ b/apps/viewer/services/api/webhooks.ts @@ -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) => - (currentBlockId: string): Record => { - const header = parseResultHeader(typebot) - const previousInputSteps = getPreviousInputSteps(typebot)({ - blockId: currentBlockId, + ( + typebot: Pick, + linkedTypebots: (Typebot | PublicTypebot)[] + ) => + async (currentBlockId: string): Promise> => { + 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, + linkedTypebots: (Typebot | PublicTypebot)[] + ) => + async ( + currentBlockId?: string, + direction: 'backward' | 'forward' = 'backward' + ): Promise => { + 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) => - ({ blockId }: { blockId: string }): InputStep[] => { - const previousInputSteps = getPreviousInputStepsInBlock(typebot)({ - blockId, +const walkEdgesAndExtract = + ( + type: 'input' | 'linkedBot', + direction: 'backward' | 'forward', + typebot: Pick + ) => + ({ 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, + direction: 'backward' | 'forward', existingBlockIds?: string[] ) => (blockId: string): string[] => { - const previousBlocks = typebot.edges.reduce( - (blockIds, edge) => - (!existingBlockIds || !existingBlockIds.includes(edge.from.blockId)) && + const blocks = typebot.edges.reduce((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) => +const extractStepsInBlock = + ( + type: 'input' | 'linkedBot', + typebot: Pick + ) => ({ 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 } diff --git a/packages/db/package.json b/packages/db/package.json index 138ac823ab..a7fe1f0830 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -5,13 +5,13 @@ "main": "./index.ts", "types": "./index.ts", "devDependencies": { - "prisma": "^3.11.1", + "prisma": "^3.12.0", "ts-node": "^10.7.0", "typescript": "^4.6.3", "dotenv-cli": "5.1.0" }, "dependencies": { - "@prisma/client": "^3.11.1" + "@prisma/client": "^3.12.0" }, "scripts": { "dx": "dotenv -e ../../apps/builder/.env.local prisma db push && yarn generate:schema && yarn start:sutdio ", diff --git a/packages/utils/src/api/index.ts b/packages/utils/src/api/index.ts new file mode 100644 index 0000000000..9c56149efa --- /dev/null +++ b/packages/utils/src/api/index.ts @@ -0,0 +1 @@ +export * from './utils' diff --git a/packages/utils/src/apiUtils.ts b/packages/utils/src/api/utils.ts similarity index 84% rename from packages/utils/src/apiUtils.ts rename to packages/utils/src/api/utils.ts index eaf1c2ed0d..90e2cddd53 100644 --- a/packages/utils/src/apiUtils.ts +++ b/packages/utils/src/api/utils.ts @@ -1,13 +1,4 @@ -import { - Typebot, - Answer, - VariableWithValue, - ResultWithAnswers, - PublicTypebot, -} from 'models' import { NextApiRequest, NextApiResponse } from 'next' -import { parseResultHeader } from './results' -import { isDefined } from './utils' export const methodNotAllowed = (res: NextApiResponse) => res.status(405).json({ message: 'Method Not Allowed' }) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 6364cfc41f..bced8c0dfe 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,4 @@ export * from './utils' -export * from './apiUtils' +export * from './api' export * from './encryption' export * from './results' diff --git a/yarn.lock b/yarn.lock index 2e7a50d232..e2f7f38025 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3635,22 +3635,22 @@ mem "^8.0.0" php-parser "3.1.0-beta.5" -"@prisma/client@^3.11.1": - version "3.11.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.11.1.tgz#bde6dec71ae133d04ce1c6658e3d76627a3c6dc7" - integrity sha512-B3C7zQG4HbjJzUr2Zg9UVkBJutbqq9/uqkl1S138+keZCubJrwizx3RuIvGwI+s+pm3qbsyNqXiZgL3Ir0fSng== +"@prisma/client@^3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.12.0.tgz#a0eb49ffea5c128dd11dffb896d7139a60073d12" + integrity sha512-4NEQjUcWja/NVBvfuDFscWSk1/rXg3+wj+TSkqXCb1tKlx/bsUE00rxsvOvGg7VZ6lw1JFpGkwjwmsOIc4zvQw== dependencies: - "@prisma/engines-version" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" + "@prisma/engines-version" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" -"@prisma/engines-version@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9": - version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#81a1835b495ad287ad7824dbd62f74e9eee90fb9" - integrity sha512-HkcsDniA4iNb/gi0iuyOJNAM7nD/LwQ0uJm15v360O5dee3TM4lWdSQiTYBMK6FF68ACUItmzSur7oYuUZ2zkQ== +"@prisma/engines-version@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980": + version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#829ca3d9d0d92555f44644606d4edfd45b2f5886" + integrity sha512-o+jo8d7ZEiVpcpNWUDh3fj2uPQpBxl79XE9ih9nkogJbhw6P33274SHnqheedZ7PyvPIK/mvU8MLNYgetgXPYw== -"@prisma/engines@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9": - version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#09ac23f8f615a8586d8d44538060ada199fe872c" - integrity sha512-MILbsGnvmnhCbFGa2/iSnsyGyazU3afzD7ldjCIeLIGKkNBMSZgA2IvpYsAXl+6qFHKGrS3B2otKfV31dwMSQw== +"@prisma/engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980": + version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#e52e364084c4d05278f62768047b788665e64a45" + integrity sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -12712,12 +12712,12 @@ prism-react-renderer@^1.2.1, prism-react-renderer@^1.3.1: resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d" integrity sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ== -prisma@^3.11.1: - version "3.11.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.11.1.tgz#fff9c0bcf83cb30c2e1d650882d5eb3c5565e028" - integrity sha512-aYn8bQwt1xwR2oSsVNHT4PXU7EhsThIwmpNB/MNUaaMx5OPLTro6VdNJe/sJssXFLxhamfWeMjwmpXjljo6xkg== +prisma@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.12.0.tgz#9675e0e72407122759d3eadcb6d27cdccd3497bd" + integrity sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg== dependencies: - "@prisma/engines" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" + "@prisma/engines" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" prismjs@^1.27.0: version "1.27.0"