2
0

♻️ (results) Introduce tRPC and use it for the results

This commit is contained in:
Baptiste Arnaud
2022-11-18 18:21:40 +01:00
parent c9cc82cc08
commit d58f9bd3a1
58 changed files with 750 additions and 421 deletions

View File

@ -13,7 +13,7 @@ export const useAnswersCount = ({
{ answersCounts: AnswersCount[] },
Error
>(
typebotId ? `/api/typebots/${typebotId}/results/answers/count` : null,
typebotId ? `/api/typebots/${typebotId}/analytics/answersCount` : null,
fetcher
)
if (error) onError(error)

View File

@ -1,7 +1,7 @@
import { BoxProps, Flex } from '@chakra-ui/react'
import { BoxProps, Flex, useEventListener } from '@chakra-ui/react'
import { useGraph, useGroupsCoordinates } from '../../providers'
import { Source } from 'models'
import React, { MouseEvent, useEffect, useRef, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
export const SourceEndpoint = ({
source,
@ -15,11 +15,6 @@ export const SourceEndpoint = ({
const { groupsCoordinates } = useGroupsCoordinates()
const ref = useRef<HTMLDivElement | null>(null)
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
setConnectingIds({ source })
}
useEffect(() => {
if (ranOnce || !ref.current || Object.keys(groupsCoordinates).length === 0)
return
@ -32,6 +27,23 @@ export const SourceEndpoint = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref.current, groupsCoordinates])
useEventListener(
'pointerdown',
(e) => {
e.stopPropagation()
setConnectingIds({ source })
},
ref.current
)
useEventListener(
'mousedown',
(e) => {
e.stopPropagation()
},
ref.current
)
if (!groupsCoordinates) return <></>
return (
<Flex
@ -39,13 +51,10 @@ export const SourceEndpoint = ({
data-testid="endpoint"
boxSize="32px"
rounded="full"
onPointerDownCapture={handleMouseDown}
onMouseDownCapture={(e) => e.stopPropagation()}
cursor="copy"
justify="center"
align="center"
pointerEvents="all"
className="prevent-group-drag"
{...props}
>
<Flex
@ -54,7 +63,6 @@ export const SourceEndpoint = ({
align="center"
bgColor="gray.100"
rounded="full"
pointerEvents="none"
>
<Flex
boxSize="13px"

View File

@ -4,6 +4,7 @@ import {
Popover,
PopoverTrigger,
useDisclosure,
useEventListener,
} from '@chakra-ui/react'
import React, { useEffect, useRef, useState } from 'react'
import {
@ -152,6 +153,8 @@ export const BlockNode = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [openedBlockId])
useEventListener('pointerdown', (e) => e.stopPropagation(), blockRef.current)
return isEditing && isTextBubbleBlock(block) ? (
<TextBubbleEditor
id={block.id}

View File

@ -1,6 +1,6 @@
import { useToast } from '@/hooks/useToast'
import { ResultHeaderCell, ResultWithAnswers } from 'models'
import { createContext, ReactNode, useContext, useMemo } from 'react'
import { KeyedMutator } from 'swr'
import { parseResultHeader } from 'utils'
import { useTypebot } from '../editor/providers/TypebotProvider'
import { useResultsQuery } from './hooks/useResultsQuery'
@ -10,42 +10,37 @@ import { convertResultsToTableData } from './utils'
const resultsContext = createContext<{
resultsList: { results: ResultWithAnswers[] }[] | undefined
flatResults: ResultWithAnswers[]
hasMore: boolean
hasNextPage: boolean
resultHeader: ResultHeaderCell[]
totalResults: number
tableData: TableData[]
onDeleteResults: (totalResultsDeleted: number) => void
fetchMore: () => void
mutate: KeyedMutator<
{
results: ResultWithAnswers[]
}[]
>
fetchNextPage: () => void
refetchResults: () => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
export const ResultsProvider = ({
children,
workspaceId,
typebotId,
totalResults,
onDeleteResults,
}: {
children: ReactNode
workspaceId: string
typebotId: string
totalResults: number
onDeleteResults: (totalResultsDeleted: number) => void
}) => {
const { publishedTypebot, linkedTypebots } = useTypebot()
const { data, mutate, setSize, hasMore } = useResultsQuery({
workspaceId,
const { showToast } = useToast()
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
typebotId,
onError: (error) => {
showToast({ description: error })
},
})
const fetchMore = () => setSize((state) => state + 1)
const resultHeader = useMemo(
() =>
publishedTypebot
@ -70,13 +65,13 @@ export const ResultsProvider = ({
value={{
resultsList: data,
flatResults: data?.flatMap((d) => d.results) ?? [],
hasMore: hasMore ?? true,
hasNextPage: hasNextPage ?? true,
tableData,
resultHeader,
totalResults,
onDeleteResults,
fetchMore,
mutate,
fetchNextPage,
refetchResults: refetch,
}}
>
{children}

View File

@ -3,53 +3,51 @@ import { canWriteTypebot } from '@/utils/api/dbRules'
import { deleteFiles } from '@/utils/api/storage'
import { User, Prisma } from 'db'
import { InputBlockType, Typebot } from 'models'
import { NextApiResponse } from 'next'
import { forbidden } from 'utils/api'
export const archiveResults =
(res: NextApiResponse) =>
async ({
typebotId,
user,
resultsFilter,
}: {
typebotId: string
user: User
resultsFilter?: Prisma.ResultWhereInput
}) => {
const typebot = await prisma.typebot.findFirst({
where: canWriteTypebot(typebotId, user),
select: { groups: true },
export const archiveResults = async ({
typebotId,
user,
resultsFilter,
}: {
typebotId: string
user: User
resultsFilter?: Prisma.ResultWhereInput
}) => {
const typebot = await prisma.typebot.findFirst({
where: canWriteTypebot(typebotId, user),
select: { groups: true },
})
if (!typebot) return { success: false }
const fileUploadBlockIds = (typebot as Typebot).groups
.flatMap((g) => g.blocks)
.filter((b) => b.type === InputBlockType.FILE)
.map((b) => b.id)
if (fileUploadBlockIds.length > 0) {
const filesToDelete = await prisma.answer.findMany({
where: { result: resultsFilter, blockId: { in: fileUploadBlockIds } },
})
if (!typebot) return forbidden(res)
const fileUploadBlockIds = (typebot as Typebot).groups
.flatMap((g) => g.blocks)
.filter((b) => b.type === InputBlockType.FILE)
.map((b) => b.id)
if (fileUploadBlockIds.length > 0) {
const filesToDelete = await prisma.answer.findMany({
where: { result: resultsFilter, blockId: { in: fileUploadBlockIds } },
if (filesToDelete.length > 0)
await deleteFiles({
urls: filesToDelete.flatMap((a) => a.content.split(', ')),
})
if (filesToDelete.length > 0)
await deleteFiles({
urls: filesToDelete.flatMap((a) => a.content.split(', ')),
})
}
await prisma.log.deleteMany({
where: {
result: resultsFilter,
},
})
await prisma.answer.deleteMany({
where: {
result: resultsFilter,
},
})
await prisma.result.updateMany({
where: resultsFilter,
data: {
isArchived: true,
variables: [],
},
})
}
await prisma.log.deleteMany({
where: {
result: resultsFilter,
},
})
await prisma.answer.deleteMany({
where: {
result: resultsFilter,
},
})
await prisma.result.updateMany({
where: resultsFilter,
data: {
isArchived: true,
variables: [],
},
})
return { success: true }
}

View File

@ -1 +1,2 @@
export * from './archiveResults'
export * from './router'

View File

@ -0,0 +1,39 @@
import { canWriteTypebot } from '@/utils/api/dbRules'
import { authenticatedProcedure } from '@/utils/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { archiveResults } from '../archiveResults'
export const deleteResultsProcedure = authenticatedProcedure
.meta({
openapi: {
method: 'DELETE',
path: '/typebots/{typebotId}/results',
protect: true,
},
})
.input(
z.object({
typebotId: z.string(),
ids: z.string().optional(),
})
)
.output(z.void())
.mutation(async ({ input, ctx: { user } }) => {
const idsArray = input.ids?.split(',')
const { typebotId } = input
const { success } = await archiveResults({
typebotId,
user,
resultsFilter: {
id: (idsArray?.length ?? 0) > 0 ? { in: idsArray } : undefined,
typebot: canWriteTypebot(typebotId, user),
},
})
if (!success)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Typebot not found',
})
})

View File

@ -0,0 +1,30 @@
import prisma from '@/lib/prisma'
import { canReadTypebot } from '@/utils/api/dbRules'
import { authenticatedProcedure } from '@/utils/server/trpc'
import { logSchema } from 'models'
import { z } from 'zod'
export const getResultLogsProcedure = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/typebots/{typebotId}/results/{resultId}/logs',
protect: true,
},
})
.input(
z.object({
typebotId: z.string(),
resultId: z.string(),
})
)
.output(z.object({ logs: z.array(logSchema) }))
.query(async ({ input: { typebotId, resultId }, ctx: { user } }) => {
const logs = await prisma.log.findMany({
where: {
result: { id: resultId, typebot: canReadTypebot(typebotId, user) },
},
})
return { logs }
})

View File

@ -0,0 +1,59 @@
import prisma from '@/lib/prisma'
import { canReadTypebot } from '@/utils/api/dbRules'
import { authenticatedProcedure } from '@/utils/server/trpc'
import { TRPCError } from '@trpc/server'
import { ResultWithAnswers, resultWithAnswersSchema } from 'models'
import { z } from 'zod'
const maxLimit = 200
export const getResultsProcedure = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/typebots/{typebotId}/results',
protect: true,
},
})
.input(
z.object({
typebotId: z.string(),
limit: z.string().regex(/^[0-9]{1,3}$/),
cursor: z.string().optional(),
})
)
.output(
z.object({
results: z.array(resultWithAnswersSchema),
nextCursor: z.string().nullish(),
})
)
.query(async ({ input, ctx: { user } }) => {
const limit = Number(input.limit)
if (limit < 1 || limit > maxLimit)
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'limit must be between 1 and 200',
})
const { cursor } = input
const results = (await prisma.result.findMany({
take: limit + 1,
cursor: cursor ? { id: cursor } : undefined,
where: {
typebot: canReadTypebot(input.typebotId, user),
answers: { some: {} },
},
orderBy: {
createdAt: 'desc',
},
include: { answers: true },
})) as ResultWithAnswers[]
let nextCursor: typeof cursor | undefined
if (results.length > limit) {
const nextResult = results.pop()
nextCursor = nextResult?.id
}
return { results, nextCursor }
})

View File

@ -0,0 +1,3 @@
export * from './deleteResultsProcedure'
export * from './getResultLogsProcedure'
export * from './getResultsProcedure'

View File

@ -0,0 +1,12 @@
import { router } from '@/utils/server/trpc'
import {
deleteResultsProcedure,
getResultLogsProcedure,
getResultsProcedure,
} from './procedures'
export const resultsRouter = router({
getResults: getResultsProcedure,
deleteResults: deleteResultsProcedure,
getResultLogs: getResultLogsProcedure,
})

View File

@ -27,7 +27,7 @@ type Props = {
onClose: () => void
}
export const LogsModal = ({ typebotId, resultId, onClose }: Props) => {
const { isLoading, logs } = useLogs(typebotId, resultId ?? undefined)
const { isLoading, logs } = useLogs(typebotId, resultId)
return (
<Modal isOpen={isDefined(resultId)} onClose={onClose} size="xl">

View File

@ -160,7 +160,6 @@ export const ResultsPage = () => {
<AnalyticsGraphContainer stats={stats} />
) : (
<ResultsProvider
workspaceId={workspace.id}
typebotId={publishedTypebot.typebotId}
totalResults={stats?.totalStarts ?? 0}
onDeleteResults={handleDeletedResults}

View File

@ -9,6 +9,7 @@ type Props = {
size: number
isExpandButtonVisible: boolean
cellIndex: number
isSelected: boolean
onExpandButtonClick: () => void
}
@ -63,5 +64,6 @@ export default memo(
Cell,
(prev, next) =>
prev.size === next.size &&
prev.isExpandButtonVisible === next.isExpandButtonVisible
prev.isExpandButtonVisible === next.isExpandButtonVisible &&
prev.isSelected === next.isSelected
)

View File

@ -13,10 +13,10 @@ import { useTypebot } from '@/features/editor'
import { unparse } from 'papaparse'
import React, { useState } from 'react'
import { useToast } from '@/hooks/useToast'
import { getAllResultsQuery } from '../../queries/getAllResultsQuery'
import { convertResultsToTableData } from '../../utils'
import { deleteResultsQuery } from '../../queries/deleteResultsQuery'
import { useResults } from '../../ResultsProvider'
import { trpc } from '@/lib/trpc'
import { TRPCError } from '@trpc/server'
type ResultsActionButtonsProps = {
selectedResultsId: string[]
@ -31,10 +31,8 @@ export const ResultsActionButtons = ({
const { typebot } = useTypebot()
const { showToast } = useToast()
const {
resultsList: data,
flatResults: results,
resultHeader,
mutate,
totalResults,
tableData,
onDeleteResults,
@ -42,14 +40,44 @@ export const ResultsActionButtons = ({
const { isOpen, onOpen, onClose } = useDisclosure()
const [isDeleteLoading, setIsDeleteLoading] = useState(false)
const [isExportLoading, setIsExportLoading] = useState(false)
const trpcContext = trpc.useContext()
const deleteResultsMutation = trpc.results.deleteResults.useMutation({
onMutate: () => {
setIsDeleteLoading(true)
},
onError: (error) => showToast({ description: error.message }),
onSuccess: async () => {
await trpcContext.results.getResults.invalidate()
},
onSettled: () => {
onDeleteResults(selectedResultsId.length)
onClearSelection()
setIsDeleteLoading(false)
},
})
const workspaceId = typebot?.workspaceId
const typebotId = typebot?.id
const getAllTableData = async () => {
if (!workspaceId || !typebotId) return []
const results = await getAllResultsQuery(workspaceId, typebotId)
return convertResultsToTableData(results, resultHeader)
const allResults = []
let cursor: string | undefined | null
do {
try {
const { results, nextCursor } =
await trpcContext.results.getResults.fetch({
typebotId,
limit: '200',
})
allResults.push(...results)
cursor = nextCursor
} catch (error) {
showToast({ description: (error as TRPCError).message })
}
} while (cursor)
return convertResultsToTableData(allResults, resultHeader)
}
const totalSelected =
@ -59,27 +87,13 @@ export const ResultsActionButtons = ({
const deleteResults = async () => {
if (!workspaceId || !typebotId) return
setIsDeleteLoading(true)
const { error } = await deleteResultsQuery(
workspaceId,
deleteResultsMutation.mutate({
typebotId,
totalSelected === totalResults ? [] : selectedResultsId
)
if (error) showToast({ description: error.message, title: error.name })
else {
mutate(
ids:
totalSelected === totalResults
? []
: data?.map((d) => ({
results: d.results.filter(
(r) => !selectedResultsId.includes(r.id)
),
}))
)
}
onDeleteResults(selectedResultsId.length)
onClearSelection()
setIsDeleteLoading(false)
? undefined
: selectedResultsId.join(','),
})
}
const exportResultsToCSV = async () => {

View File

@ -247,9 +247,10 @@ const IndeterminateCheckbox = React.forwardRef(
const resolvedRef: any = ref || defaultRef
return (
<Flex justify="center" data-testid="checkbox" {...rest}>
<Flex justify="center" data-testid="checkbox">
<Checkbox
ref={resolvedRef}
{...rest}
isIndeterminate={indeterminate}
isChecked={checked}
/>

View File

@ -10,7 +10,12 @@ type Props = {
onExpandButtonClick: () => void
}
export const Row = ({ row, bottomElement, onExpandButtonClick }: Props) => {
export const Row = ({
row,
bottomElement,
onExpandButtonClick,
isSelected,
}: Props) => {
const [isExpandButtonVisible, setIsExpandButtonVisible] = useState(false)
const showExpandButton = () => setIsExpandButtonVisible(true)
@ -35,6 +40,7 @@ export const Row = ({ row, bottomElement, onExpandButtonClick }: Props) => {
isExpandButtonVisible={isExpandButtonVisible}
cellIndex={cellIndex}
onExpandButtonClick={onExpandButtonClick}
isSelected={isSelected}
/>
))}
</tr>

View File

@ -9,8 +9,8 @@ import { SubmissionsTable } from './ResultsTable'
export const ResultsTableContainer = () => {
const {
flatResults: results,
fetchMore,
hasMore,
fetchNextPage,
hasNextPage,
resultHeader,
tableData,
} = useResults()
@ -60,8 +60,8 @@ export const ResultsTableContainer = () => {
preferences={typebot.resultsTablePreferences}
resultHeader={resultHeader}
data={tableData}
onScrollToBottom={fetchMore}
hasMore={hasMore}
onScrollToBottom={fetchNextPage}
hasMore={hasNextPage}
onLogOpenIndex={handleLogOpenIndex}
onResultExpandIndex={handleResultExpandIndex}
/>

View File

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

View File

@ -1,17 +1,19 @@
import { fetcher } from '@/utils/helpers'
import { Log } from 'db'
import useSWR from 'swr'
import { trpc } from '@/lib/trpc'
import { isDefined } from '@udecode/plate-common'
export const useLogs = (
typebotId: string,
resultId?: string,
onError?: (e: Error) => void
resultId: string | null,
onError?: (error: string) => void
) => {
const { data, error } = useSWR<{ logs: Log[] }>(
resultId ? `/api/typebots/${typebotId}/results/${resultId}/logs` : null,
fetcher
const { data, error } = trpc.results.getResultLogs.useQuery(
{
resultId: resultId ?? '',
typebotId,
},
{ enabled: isDefined(resultId) }
)
if (error && onError) onError(error)
if (error && onError) onError(error.message)
return {
logs: data?.logs,
isLoading: !error && !data,

View File

@ -1,64 +1,29 @@
import { fetcher } from '@/utils/helpers'
import { ResultWithAnswers } from 'models'
import { env } from 'utils'
import useSWRInfinite from 'swr/infinite'
const paginationLimit = 50
import { trpc } from '@/lib/trpc'
export const useResultsQuery = ({
workspaceId,
typebotId,
onError,
}: {
workspaceId: string
typebotId: string
onError?: (error: Error) => void
onError?: (error: string) => void
}) => {
const { data, error, mutate, setSize, size, isValidating } = useSWRInfinite<
{ results: ResultWithAnswers[] },
Error
>(
(
pageIndex: number,
previousPageData: {
results: ResultWithAnswers[]
const { data, error, fetchNextPage, hasNextPage, refetch } =
trpc.results.getResults.useInfiniteQuery(
{
typebotId,
limit: '50',
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
) => getKey(workspaceId, typebotId, pageIndex, previousPageData),
fetcher,
{
revalidateAll: true,
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
}
)
)
if (error && onError) onError(error)
if (error && onError) onError(error.message)
return {
data,
data: data?.pages,
isLoading: !error && !data,
mutate,
setSize,
size,
hasMore:
isValidating ||
(data &&
data.length > 0 &&
data[data.length - 1].results.length > 0 &&
data.length === paginationLimit),
fetchNextPage,
hasNextPage,
refetch,
}
}
const getKey = (
workspaceId: string,
typebotId: string,
pageIndex: number,
previousPageData: {
results: ResultWithAnswers[]
}
) => {
if (previousPageData && previousPageData.results.length === 0) return null
if (pageIndex === 0)
return `/api/typebots/${typebotId}/results?limit=50&workspaceId=${workspaceId}`
return `/api/typebots/${typebotId}/results?lastResultId=${
previousPageData.results[previousPageData.results.length - 1].id
}&limit=${paginationLimit}&workspaceId=${workspaceId}`
}

View File

@ -10,7 +10,7 @@ export const useStats = ({
onError: (error: Error) => void
}) => {
const { data, error, mutate } = useSWR<{ stats: Stats }, Error>(
typebotId ? `/api/typebots/${typebotId}/results/stats` : null,
typebotId ? `/api/typebots/${typebotId}/analytics/stats` : null,
fetcher
)
if (error) onError(error)

View File

@ -1 +1 @@
export { ResultsPage } from './components/ResultsPage'
export * from './components'

View File

@ -1,19 +0,0 @@
import { stringify } from 'qs'
import { sendRequest } from 'utils'
export const deleteResultsQuery = async (
workspaceId: string,
typebotId: string,
ids: string[]
) => {
const params = stringify({
workspaceId,
})
return sendRequest({
url: `/api/typebots/${typebotId}/results?${params}`,
method: 'DELETE',
body: {
ids,
},
})
}

View File

@ -1,29 +0,0 @@
import { ResultWithAnswers } from 'models'
import { stringify } from 'qs'
import { sendRequest } from 'utils'
export const getAllResultsQuery = async (
workspaceId: string,
typebotId: string
) => {
const results = []
let hasMore = true
let lastResultId: string | undefined = undefined
do {
const query = stringify({ limit: 200, lastResultId, workspaceId })
const { data, error } = await sendRequest<{ results: ResultWithAnswers[] }>(
{
url: `/api/typebots/${typebotId}/results?${query}`,
method: 'GET',
}
)
if (error) {
console.error(error)
break
}
results.push(...(data?.results ?? []))
lastResultId = results[results.length - 1]?.id as string | undefined
if (data?.results.length === 0) hasMore = false
} while (hasMore)
return results
}

View File

@ -12,17 +12,13 @@ import { HeaderCell, TableData } from './types'
import { CodeIcon, CalendarIcon, FileIcon } from '@/components/icons'
import { TextLink } from '@/components/TextLink'
export const parseDateToReadable = (dateStr: string): string => {
const date = new Date(dateStr)
return (
date.toDateString().split(' ').slice(1, 3).join(' ') +
', ' +
date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})
)
}
export const parseDateToReadable = (date: Date): string =>
date.toDateString().split(' ').slice(1, 3).join(' ') +
', ' +
date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})
export const parseSubmissionsColumns = (
resultHeader: ResultHeaderCell[]

View File

@ -0,0 +1,21 @@
import { httpBatchLink } from '@trpc/client'
import { createTRPCNext } from '@trpc/next'
import type { AppRouter } from '../utils/server/routers/_app'
import superjson from 'superjson'
const getBaseUrl = () =>
typeof window !== 'undefined' ? '' : process.env.NEXTAUTH_URL
export const trpc = createTRPCNext<AppRouter>({
config() {
return {
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
transformer: superjson,
}
},
ssr: true,
})

View File

@ -17,6 +17,7 @@ import { WorkspaceProvider } from '@/features/workspace'
import { toTitleCase } from 'utils'
import { Session } from 'next-auth'
import { Plan } from 'db'
import { trpc } from '@/lib/trpc'
const { ToastContainer, toast } = createStandaloneToast(customTheme)
@ -72,4 +73,4 @@ const App = ({
)
}
export default App
export default trpc.withTRPC(App)

View File

@ -0,0 +1,8 @@
import { createContext } from '@/utils/server/context'
import { appRouter } from '@/utils/server/routers/_app'
import { createOpenApiNextHandler } from 'trpc-openapi'
export default createOpenApiNextHandler({
router: appRouter,
createContext,
})

View File

@ -0,0 +1,8 @@
import { createContext } from '@/utils/server/context'
import { appRouter } from '@/utils/server/routers/_app'
import { createNextApiHandler } from '@trpc/server/adapters/next'
export default createNextApiHandler({
router: appRouter,
createContext,
})

View File

@ -39,11 +39,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}
if (req.method === 'DELETE') {
await archiveResults(res)({
const { success } = await archiveResults({
typebotId,
user,
resultsFilter: { typebotId },
})
if (!success) return res.status(500).send({ success: false })
await prisma.publicTypebot.deleteMany({
where: { typebot: canWriteTypebot(typebotId, user) },
})

View File

@ -1,67 +0,0 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { canReadTypebot, canWriteTypebot } from '@/utils/api/dbRules'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from 'utils/api'
import { getAuthenticatedUser } from '@/features/auth'
import { archiveResults } from '@/features/results/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res, 'workspaceId is required')
const workspace = await prisma.workspace.findFirst({
where:
user.email === process.env.ADMIN_EMAIL
? undefined
: { id: workspaceId, members: { some: { userId: user.id } } },
select: { plan: true },
})
if (!workspace) return forbidden(res)
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const lastResultId = req.query.lastResultId?.toString()
const take = Number(req.query.limit?.toString())
const results = await prisma.result.findMany({
take: isNaN(take) ? undefined : take,
skip: lastResultId ? 1 : 0,
cursor: lastResultId
? {
id: lastResultId,
}
: undefined,
where: {
typebot: canReadTypebot(typebotId, user),
answers: { some: {} },
},
orderBy: {
createdAt: 'desc',
},
include: { answers: true },
})
return res.status(200).send({ results })
}
if (req.method === 'DELETE') {
const typebotId = req.query.typebotId as string
const data = req.body as { ids: string[] }
const ids = data.ids
await archiveResults(res)({
typebotId,
user,
resultsFilter: {
id: ids.length > 0 ? { in: ids } : undefined,
typebot: canWriteTypebot(typebotId, user),
},
})
return res.status(200).send({ message: 'done' })
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -1,24 +0,0 @@
import { withSentry } from '@sentry/nextjs'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { canReadTypebot } from '@/utils/api/dbRules'
import { getAuthenticatedUser } from '@/features/auth'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const resultId = req.query.resultId as string
const logs = await prisma.log.findMany({
where: {
result: { id: resultId, typebot: canReadTypebot(typebotId, user) },
},
})
return res.send({ logs })
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,13 @@
import { getAuthenticatedUser } from '@/features/auth'
import { inferAsyncReturnType } from '@trpc/server'
import * as trpcNext from '@trpc/server/adapters/next'
export async function createContext(opts: trpcNext.CreateNextContextOptions) {
const user = await getAuthenticatedUser(opts.req)
return {
user,
}
}
export type Context = inferAsyncReturnType<typeof createContext>

View File

@ -0,0 +1,8 @@
import { resultsRouter } from '@/features/results/api'
import { router } from '../trpc'
export const appRouter = router({
results: resultsRouter,
})
export type AppRouter = typeof appRouter

View File

@ -0,0 +1,29 @@
import { TRPCError, initTRPC } from '@trpc/server'
import { Context } from './context'
import { OpenApiMeta } from 'trpc-openapi'
import superjson from 'superjson'
const t = initTRPC.context<Context>().meta<OpenApiMeta>().create({
transformer: superjson,
})
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
})
}
return next({
ctx: {
user: ctx.user,
},
})
})
export const middleware = t.middleware
export const router = t.router
export const publicProcedure = t.procedure
export const authenticatedProcedure = t.procedure.use(isAuthed)