feat(api): ✨ Add list results endpoint
This commit is contained in:
@@ -53,6 +53,7 @@ type HeaderCell = {
|
|||||||
Header: JSX.Element
|
Header: JSX.Element
|
||||||
accessor: string
|
accessor: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseSubmissionsColumns = (
|
export const parseSubmissionsColumns = (
|
||||||
typebot: PublicTypebot
|
typebot: PublicTypebot
|
||||||
): HeaderCell[] => {
|
): HeaderCell[] => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Result, VariableWithValue } from 'models'
|
import { ResultWithAnswers, Typebot, VariableWithValue } from 'models'
|
||||||
import useSWRInfinite from 'swr/infinite'
|
import useSWRInfinite from 'swr/infinite'
|
||||||
import { fetcher } from './utils'
|
import { fetcher } from './utils'
|
||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import { Answer } from 'db'
|
import { Answer } from 'db'
|
||||||
import { isDefined, sendRequest } from 'utils'
|
import { byId, isDefined, sendRequest } from 'utils'
|
||||||
|
|
||||||
const paginationLimit = 50
|
const paginationLimit = 50
|
||||||
|
|
||||||
@@ -21,7 +21,6 @@ const getKey = (
|
|||||||
}&limit=${paginationLimit}`
|
}&limit=${paginationLimit}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResultWithAnswers = Result & { answers: Answer[] }
|
|
||||||
export const useResults = ({
|
export const useResults = ({
|
||||||
typebotId,
|
typebotId,
|
||||||
onError,
|
onError,
|
||||||
@@ -113,3 +112,28 @@ export const convertResultsToTableData = (results?: ResultWithAnswers[]) =>
|
|||||||
return { ...o, [variable.id]: variable.value }
|
return { ...o, [variable.id]: variable.value }
|
||||||
}, {}),
|
}, {}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
export const parseAnswers = (
|
||||||
|
result: ResultWithAnswers,
|
||||||
|
{ blocks, variables }: Pick<Typebot, 'blocks' | 'variables'>
|
||||||
|
) => ({
|
||||||
|
submittedAt: result.createdAt,
|
||||||
|
...[...result.answers, ...result.prefilledVariables].reduce<{
|
||||||
|
[key: string]: string
|
||||||
|
}>((o, answerOrVariable) => {
|
||||||
|
if ('blockId' in answerOrVariable) {
|
||||||
|
const answer = answerOrVariable as Answer
|
||||||
|
const key = answer.variableId
|
||||||
|
? variables.find(byId(answer.variableId))?.name
|
||||||
|
: blocks.find(byId(answer.blockId))?.title
|
||||||
|
if (!key) return o
|
||||||
|
return {
|
||||||
|
...o,
|
||||||
|
[key]: answer.content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const variable = answerOrVariable as VariableWithValue
|
||||||
|
if (isDefined(o[variable.id])) return o
|
||||||
|
return { ...o, [variable.id]: variable.value }
|
||||||
|
}, {}),
|
||||||
|
})
|
||||||
|
|||||||
28
apps/viewer/pages/api/typebots/[typebotId]/results.ts
Normal file
28
apps/viewer/pages/api/typebots/[typebotId]/results.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import prisma from 'libs/prisma'
|
||||||
|
import { ResultWithAnswers, Typebot } from 'models'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { authenticateUser } from 'services/api/utils'
|
||||||
|
import { methodNotAllowed, parseAnswers } from 'utils'
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const user = await authenticateUser(req)
|
||||||
|
if (!user) return res.status(401).json({ message: 'Not authenticated' })
|
||||||
|
const typebotId = req.query.typebotId.toString()
|
||||||
|
const typebot = await prisma.typebot.findUnique({
|
||||||
|
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
||||||
|
})
|
||||||
|
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||||
|
const limit = Number(req.query.limit)
|
||||||
|
const results = (await prisma.result.findMany({
|
||||||
|
where: { typebotId: typebot.id },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: limit,
|
||||||
|
include: { answers: true },
|
||||||
|
})) as unknown as ResultWithAnswers[]
|
||||||
|
res.send({ results: results.map(parseAnswers(typebot as Typebot)) })
|
||||||
|
}
|
||||||
|
methodNotAllowed(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler
|
||||||
@@ -145,3 +145,37 @@ export const importTypebotInDatabase = async (
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createResults = async ({ typebotId }: { typebotId: string }) => {
|
||||||
|
await prisma.result.deleteMany()
|
||||||
|
await prisma.result.createMany({
|
||||||
|
data: [
|
||||||
|
...Array.from(Array(200)).map((_, idx) => {
|
||||||
|
const today = new Date()
|
||||||
|
const rand = Math.random()
|
||||||
|
return {
|
||||||
|
id: `result${idx}`,
|
||||||
|
typebotId,
|
||||||
|
createdAt: new Date(
|
||||||
|
today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx)
|
||||||
|
),
|
||||||
|
isCompleted: rand > 0.5,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
return createAnswers()
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAnswers = () => {
|
||||||
|
return prisma.answer.createMany({
|
||||||
|
data: [
|
||||||
|
...Array.from(Array(200)).map((_, idx) => ({
|
||||||
|
resultId: `result${idx}`,
|
||||||
|
content: `content${idx}`,
|
||||||
|
stepId: 'step1',
|
||||||
|
blockId: 'block1',
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import test, { expect } from '@playwright/test'
|
import test, { expect } from '@playwright/test'
|
||||||
import { createTypebots, parseDefaultBlockWithStep } from '../services/database'
|
import {
|
||||||
|
createResults,
|
||||||
|
createTypebots,
|
||||||
|
parseDefaultBlockWithStep,
|
||||||
|
} from '../services/database'
|
||||||
import {
|
import {
|
||||||
IntegrationStepType,
|
IntegrationStepType,
|
||||||
defaultWebhookOptions,
|
defaultWebhookOptions,
|
||||||
@@ -19,6 +23,7 @@ test.beforeAll(async () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
await createResults({ typebotId })
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -98,3 +103,18 @@ test('can unsubscribe webhook', async ({ request }) => {
|
|||||||
message: 'success',
|
message: 'success',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('can list results', async ({ request }) => {
|
||||||
|
expect(
|
||||||
|
(await request.get(`/api/typebots/${typebotId}/results`)).status()
|
||||||
|
).toBe(401)
|
||||||
|
const response = await request.get(
|
||||||
|
`/api/typebots/${typebotId}/results?limit=10`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: 'Bearer userToken' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const { results } = await response.json()
|
||||||
|
expect(results).toHaveLength(10)
|
||||||
|
expect(results[0]).toMatchObject({ 'Block #1': 'content199' })
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Result as ResultFromPrisma } from 'db'
|
import { Result as ResultFromPrisma } from 'db'
|
||||||
import { VariableWithValue } from '.'
|
import { Answer, VariableWithValue } from '.'
|
||||||
|
|
||||||
export type Result = Omit<
|
export type Result = Omit<
|
||||||
ResultFromPrisma,
|
ResultFromPrisma,
|
||||||
'createdAt' | 'prefilledVariables'
|
'createdAt' | 'prefilledVariables'
|
||||||
> & { createdAt: string; prefilledVariables: VariableWithValue[] }
|
> & { createdAt: string; prefilledVariables: VariableWithValue[] }
|
||||||
|
|
||||||
|
export type ResultWithAnswers = Result & { answers: Answer[] }
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { Typebot, Answer, VariableWithValue, ResultWithAnswers } from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { byId, isDefined } from '.'
|
||||||
|
|
||||||
export const methodNotAllowed = (res: NextApiResponse) =>
|
export const methodNotAllowed = (res: NextApiResponse) =>
|
||||||
res.status(405).json({ message: 'Method Not Allowed' })
|
res.status(405).json({ message: 'Method Not Allowed' })
|
||||||
@@ -20,3 +22,27 @@ export const initMiddleware =
|
|||||||
return resolve(result)
|
return resolve(result)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const parseAnswers =
|
||||||
|
({ blocks, variables }: Pick<Typebot, 'blocks' | 'variables'>) =>
|
||||||
|
(result: ResultWithAnswers) => ({
|
||||||
|
submittedAt: result.createdAt,
|
||||||
|
...[...result.answers, ...result.prefilledVariables].reduce<{
|
||||||
|
[key: string]: string
|
||||||
|
}>((o, answerOrVariable) => {
|
||||||
|
if ('blockId' in answerOrVariable) {
|
||||||
|
const answer = answerOrVariable as Answer
|
||||||
|
const key = answer.variableId
|
||||||
|
? variables.find(byId(answer.variableId))?.name
|
||||||
|
: blocks.find(byId(answer.blockId))?.title
|
||||||
|
if (!key) return o
|
||||||
|
return {
|
||||||
|
...o,
|
||||||
|
[key]: answer.content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const variable = answerOrVariable as VariableWithValue
|
||||||
|
if (isDefined(o[variable.id])) return o
|
||||||
|
return { ...o, [variable.id]: variable.value }
|
||||||
|
}, {}),
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user