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

@ -31,7 +31,12 @@
"@googleapis/drive": "4.0.0",
"@sentry/nextjs": "7.19.0",
"@stripe/stripe-js": "1.44.1",
"@tanstack/react-query": "^4.16.1",
"@tanstack/react-table": "8.5.28",
"@trpc/client": "10.0.0-rc.8",
"@trpc/next": "10.0.0-rc.8",
"@trpc/react-query": "10.0.0-rc.8",
"@trpc/server": "10.0.0-rc.8",
"@udecode/plate-basic-marks": "18.9.0",
"@udecode/plate-common": "^7.0.2",
"@udecode/plate-core": "18.9.0",
@ -80,6 +85,7 @@
"svg-round-corners": "0.4.1",
"swr": "1.3.0",
"tinycolor2": "1.4.2",
"trpc-openapi": "1.0.0-alpha.4",
"typebot-js": "workspace:*",
"use-debounce": "8.0.4"
},
@ -110,7 +116,10 @@
"eslint-plugin-react": "7.31.10",
"models": "workspace:*",
"next-transpile-modules": "10.0.0",
"superjson": "^1.11.0",
"tsconfig": "workspace:*",
"typescript": "4.8.4",
"utils": "workspace:*"
"utils": "workspace:*",
"zod": "3.19.1"
}
}

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,12 +3,8 @@ 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 ({
export const archiveResults = async ({
typebotId,
user,
resultsFilter,
@ -21,7 +17,7 @@ export const archiveResults =
where: canWriteTypebot(typebotId, user),
select: { groups: true },
})
if (!typebot) return forbidden(res)
if (!typebot) return { success: false }
const fileUploadBlockIds = (typebot as Typebot).groups
.flatMap((g) => g.blocks)
.filter((b) => b.type === InputBlockType.FILE)
@ -52,4 +48,6 @@ export const archiveResults =
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[]
}
) => getKey(workspaceId, typebotId, pageIndex, previousPageData),
fetcher,
const { data, error, fetchNextPage, hasNextPage, refetch } =
trpc.results.getResults.useInfiniteQuery(
{
revalidateAll: true,
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
typebotId,
limit: '50',
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
)
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 (
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)

View File

@ -1,26 +1,11 @@
{
"extends": "tsconfig/nextjs.json",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"composite": true,
"downlevelIteration": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.json"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -34,7 +34,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { resultValues, variables } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as {
resultValues: ResultValues | undefined
resultValues:
| (Omit<ResultValues, 'createdAt'> & {
createdAt: string
})
| undefined
variables: Variable[]
}
const typebot = (await prisma.typebot.findUnique({
@ -83,7 +87,9 @@ export const executeWebhook =
webhook: Webhook,
variables: Variable[],
groupId: string,
resultValues?: ResultValues,
resultValues?: Omit<ResultValues, 'createdAt'> & {
createdAt: string
},
resultId?: string
): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method)
@ -193,7 +199,9 @@ const getBodyContent =
groupId,
}: {
body?: string | null
resultValues?: ResultValues
resultValues?: Omit<ResultValues, 'createdAt'> & {
createdAt: string
}
groupId: string
}): Promise<string | undefined> => {
if (!body) return

View File

@ -26,7 +26,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { resultValues, variables } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as {
resultValues: ResultValues | undefined
resultValues:
| (Omit<ResultValues, 'createdAt'> & {
createdAt: string
})
| undefined
variables: Variable[]
}
const typebot = (await prisma.typebot.findUnique({

View File

@ -54,7 +54,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
} = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as SendEmailOptions & {
resultValues: ResultValues
resultValues: Omit<ResultValues, 'createdAt'> & {
createdAt: string
}
fileUrls?: string
}
const { name: replyToName } = parseEmailRecipient(replyTo)
@ -162,10 +164,14 @@ const getEmailBody = async ({
isBodyCode,
typebotId,
resultValues,
}: { typebotId: string; resultValues: ResultValues } & Pick<
SendEmailOptions,
'isCustomBody' | 'isBodyCode' | 'body'
>): Promise<{ html?: string; text?: string } | undefined> => {
}: {
typebotId: string
resultValues: Omit<ResultValues, 'createdAt'> & {
createdAt: string
}
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
{ html?: string; text?: string } | undefined
> => {
if (isCustomBody || (isNotDefined(isCustomBody) && !isEmpty(body)))
return {
html: isBodyCode ? body : undefined,

View File

@ -1,8 +1,6 @@
{
"name": "configs",
"version": "1.0.0",
"main": "./index.ts",
"types": "./index.ts",
"license": "AGPL-3.0-or-later",
"private": true,
"devDependencies": {

View File

@ -21,6 +21,7 @@
"devDependencies": {
"prisma": "4.6.1",
"typescript": "4.8.4",
"dotenv-cli": "6.0.0"
"dotenv-cli": "6.0.0",
"tsconfig": "workspace:*"
}
}

View File

@ -1,15 +1,5 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"composite": true,
"outDir": "build",
"isolatedModules": false
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "build"]
"extends": "tsconfig/base.json",
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}

View File

@ -11,7 +11,8 @@
"devDependencies": {
"typescript": "4.8.4",
"next": "13.0.3",
"db": "workspace:*"
"db": "workspace:*",
"tsconfig": "workspace:*"
},
"peerDependencies": {
"next": "12.0.0",

View File

@ -1,12 +1,19 @@
import { Answer as AnswerFromPrisma } from 'db'
import { z } from 'zod'
export type Answer = Omit<
AnswerFromPrisma,
'resultId' | 'createdAt' | 'storageUsed'
> & { storageUsed?: number }
export const answerSchema = z.object({
createdAt: z.date(),
resultId: z.string(),
blockId: z.string(),
groupId: z.string(),
variableId: z.string().nullable(),
content: z.string(),
storageUsed: z.number().nullable(),
})
export type Stats = {
totalViews: number
totalStarts: number
totalCompleted: number
}
export type Answer = z.infer<typeof answerSchema>

View File

@ -1,14 +1,39 @@
import { Result as ResultFromPrisma } from 'db'
import { Answer } from './answer'
import { z } from 'zod'
import { answerSchema } from './answer'
import { InputBlockType } from './blocks'
import { VariableWithValue } from './typebot/variable'
import { variableWithValueSchema } from './typebot/variable'
export type Result = Omit<ResultFromPrisma, 'createdAt' | 'variables'> & {
createdAt: string
variables: VariableWithValue[]
}
export const resultSchema = z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
typebotId: z.string(),
variables: z.array(variableWithValueSchema),
isCompleted: z.boolean(),
hasStarted: z.boolean().nullable(),
isArchived: z.boolean().nullable(),
})
export type ResultWithAnswers = Result & { answers: Answer[] }
export const resultWithAnswersSchema = resultSchema.and(
z.object({
answers: z.array(answerSchema),
})
)
export const logSchema = z.object({
id: z.string(),
createdAt: z.date(),
resultId: z.string(),
status: z.string(),
description: z.string(),
details: z.string().nullable(),
})
export type Result = z.infer<typeof resultSchema>
export type ResultWithAnswers = z.infer<typeof resultWithAnswersSchema>
export type Log = z.infer<typeof logSchema>
export type ResultValues = Pick<
ResultWithAnswers,

View File

@ -3,21 +3,29 @@ import { z } from 'zod'
export const variableSchema = z.object({
id: z.string(),
name: z.string(),
value: z.string().optional().nullable(),
value: z.string().nullish(),
})
/**
* Variable when retrieved from the database
*/
export type VariableWithValue = Omit<Variable, 'value'> & {
value: string
}
export const variableWithValueSchema = z.object({
id: z.string(),
name: z.string(),
value: z.string(),
})
/**
* Variable when computed or retrieved from a block
*/
export type VariableWithUnknowValue = Omit<VariableWithValue, 'value'> & {
value: unknown
}
const VariableWithUnknowValueSchema = z.object({
id: z.string(),
name: z.string(),
value: z.unknown(),
})
export type Variable = z.infer<typeof variableSchema>
export type VariableWithValue = z.infer<typeof variableWithValueSchema>
export type VariableWithUnknowValue = z.infer<
typeof VariableWithUnknowValueSchema
>

View File

@ -1,17 +1,5 @@
{
"compilerOptions": {
"target": "es5",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDeclarationOnly": true
}
"extends": "tsconfig/base.json",
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,21 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true,
"downlevelIteration": true
},
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"allowJs": true,
"declaration": false,
"declarationMap": false,
"incremental": true,
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"noEmit": true,
"resolveJsonModule": true,
"target": "es5"
},
"include": ["src", "next-env.d.ts"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,3 @@
{
"name": "tsconfig"
}

View File

@ -14,7 +14,8 @@
"models": "workspace:*",
"next": "13.0.3",
"nodemailer": "6.8.0",
"typescript": "4.8.4"
"typescript": "4.8.4",
"tsconfig": "workspace:*"
},
"peerDependencies": {
"aws-sdk": "2.1152.0",

View File

@ -177,7 +177,9 @@ export const parseAnswers =
createdAt,
answers,
variables: resultVariables,
}: Pick<ResultWithAnswers, 'createdAt' | 'answers' | 'variables'>): {
}: Pick<ResultWithAnswers, 'answers' | 'variables'> & {
createdAt: string
}): {
[key: string]: string
} => {
const header = parseResultHeader(typebot, linkedTypebots)

View File

@ -1,17 +1,5 @@
{
"compilerOptions": {
"target": "es5",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDeclarationOnly": true
}
"extends": "tsconfig/base.json",
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}

183
pnpm-lock.yaml generated
View File

@ -35,7 +35,12 @@ importers:
'@playwright/test': 1.27.1
'@sentry/nextjs': 7.19.0
'@stripe/stripe-js': 1.44.1
'@tanstack/react-query': ^4.16.1
'@tanstack/react-table': 8.5.28
'@trpc/client': 10.0.0-rc.8
'@trpc/next': 10.0.0-rc.8
'@trpc/react-query': 10.0.0-rc.8
'@trpc/server': 10.0.0-rc.8
'@types/canvas-confetti': 1.6.0
'@types/google-spreadsheet': 3.3.0
'@types/jsonwebtoken': 8.5.9
@ -104,13 +109,17 @@ importers:
slate-react: 0.83.2
stripe: 10.17.0
styled-components: 5.3.6
superjson: ^1.11.0
svg-round-corners: 0.4.1
swr: 1.3.0
tinycolor2: 1.4.2
trpc-openapi: 1.0.0-alpha.4
tsconfig: workspace:*
typebot-js: workspace:*
typescript: 4.8.4
use-debounce: 8.0.4
utils: workspace:*
zod: 3.19.1
dependencies:
'@chakra-ui/css-reset': 2.0.10_hp5f5nkljdiwilp4rgxyefcplu
'@chakra-ui/react': 2.4.1_ydu7fid2w3vesyc65jyv4dbawa
@ -132,7 +141,12 @@ importers:
'@googleapis/drive': 4.0.0
'@sentry/nextjs': 7.19.0_next@13.0.3+react@18.2.0
'@stripe/stripe-js': 1.44.1
'@tanstack/react-query': 4.16.1_biqbaboplfbrettd7655fr4n2y
'@tanstack/react-table': 8.5.28_biqbaboplfbrettd7655fr4n2y
'@trpc/client': 10.0.0-rc.8_@trpc+server@10.0.0-rc.8
'@trpc/next': 10.0.0-rc.8_js37qhiwaz7seiawrwo5qa4qoq
'@trpc/react-query': 10.0.0-rc.8_bd5nqhlee2rdtlo5aa5wyumt5u
'@trpc/server': 10.0.0-rc.8
'@udecode/plate-basic-marks': 18.9.0_gmohhyxeb2dq4peb7jfnhckv34
'@udecode/plate-common': 7.0.2_l45ryt4vvbsc5mzu26yyqixq5e
'@udecode/plate-core': 18.9.0_gmohhyxeb2dq4peb7jfnhckv34
@ -181,6 +195,7 @@ importers:
svg-round-corners: 0.4.1
swr: 1.3.0_react@18.2.0
tinycolor2: 1.4.2
trpc-openapi: 1.0.0-alpha.4_m7lve6cjvd26xbjqz4zsiln46q
typebot-js: link:../../packages/typebot-js
use-debounce: 8.0.4_react@18.2.0
devDependencies:
@ -210,8 +225,11 @@ importers:
eslint-plugin-react: 7.31.10_eslint@8.27.0
models: link:../../packages/models
next-transpile-modules: 10.0.0
superjson: 1.11.0
tsconfig: link:../../packages/tsconfig
typescript: 4.8.4
utils: link:../../packages/utils
zod: 3.19.1
apps/docs:
specifiers:
@ -496,12 +514,14 @@ importers:
'@prisma/client': 4.6.1
dotenv-cli: 6.0.0
prisma: 4.6.1
tsconfig: workspace:*
typescript: 4.8.4
dependencies:
'@prisma/client': 4.6.1_prisma@4.6.1
devDependencies:
dotenv-cli: 6.0.0
prisma: 4.6.1
tsconfig: link:../tsconfig
typescript: 4.8.4
packages/emails:
@ -532,6 +552,7 @@ importers:
specifiers:
db: workspace:*
next: 13.0.3
tsconfig: workspace:*
typescript: 4.8.4
zod: 3.19.1
dependencies:
@ -539,6 +560,7 @@ importers:
devDependencies:
db: link:../db
next: 13.0.3_biqbaboplfbrettd7655fr4n2y
tsconfig: link:../tsconfig
typescript: 4.8.4
packages/scripts:
@ -565,6 +587,9 @@ importers:
typescript: 4.8.4
utils: link:../utils
packages/tsconfig:
specifiers: {}
packages/typebot-js:
specifiers:
'@types/jest': 29.2.3
@ -605,6 +630,7 @@ importers:
models: workspace:*
next: 13.0.3
nodemailer: 6.8.0
tsconfig: workspace:*
typescript: 4.8.4
devDependencies:
'@playwright/test': 1.27.1
@ -615,6 +641,7 @@ importers:
models: link:../models
next: 13.0.3_biqbaboplfbrettd7655fr4n2y
nodemailer: 6.8.0
tsconfig: link:../tsconfig
typescript: 4.8.4
packages/wordpress:
@ -5597,6 +5624,28 @@ packages:
dependencies:
defer-to-connect: 2.0.1
/@tanstack/query-core/4.15.1:
resolution: {integrity: sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ==}
dev: false
/@tanstack/react-query/4.16.1_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@tanstack/query-core': 4.15.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
use-sync-external-store: 1.2.0_react@18.2.0
dev: false
/@tanstack/react-table/8.5.28_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-Rj6oSi2Ef3pYnsv1aCQrN+fhkLJO/gIoVAvfBui/harFc+1+6OPfN0C6O4jOKt0bq0UyQI+yghYkfpAkhqJ31A==}
engines: {node: '>=12'}
@ -5630,6 +5679,55 @@ packages:
engines: {node: '>= 10'}
dev: true
/@trpc/client/10.0.0-rc.8_@trpc+server@10.0.0-rc.8:
resolution: {integrity: sha512-NuQl7g1tkfFYn6P6dZJJkVsxnxutYpaxmpG/w5ZKW3QVu6cY1joUOrWakunKrghh3HS+svgLaPz0Xl0PlmdSlw==}
peerDependencies:
'@trpc/server': 10.0.0-rc.8
dependencies:
'@trpc/server': 10.0.0-rc.8
dev: false
/@trpc/next/10.0.0-rc.8_js37qhiwaz7seiawrwo5qa4qoq:
resolution: {integrity: sha512-l2TZF22virmC3RROD7qu0dnc3sdYfVvkOhAu1qP5+8fRgs4cgHKBFdalQ+SKBnU0RNuVLYVw0CkyvU+gHo5u2w==}
peerDependencies:
'@tanstack/react-query': ^4.3.8
'@trpc/client': 10.0.0-rc.8
'@trpc/react-query': ^10.0.0-proxy-beta.21
'@trpc/server': 10.0.0-rc.8
next: '*'
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@tanstack/react-query': 4.16.1_biqbaboplfbrettd7655fr4n2y
'@trpc/client': 10.0.0-rc.8_@trpc+server@10.0.0-rc.8
'@trpc/react-query': 10.0.0-rc.8_bd5nqhlee2rdtlo5aa5wyumt5u
'@trpc/server': 10.0.0-rc.8
next: 13.0.3_mqvh5p7ejg4taogoj6tpk3gd5a
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-ssr-prepass: 1.5.0_react@18.2.0
dev: false
/@trpc/react-query/10.0.0-rc.8_bd5nqhlee2rdtlo5aa5wyumt5u:
resolution: {integrity: sha512-LKL29llvGtwYmfKGuCYSgNNChOL8/6PInY90i+ZgI8TLO6ZCJaVrPBSvqtxKj3MSezbK3kOxWIviKVFwV35L0A==}
peerDependencies:
'@tanstack/react-query': ^4.3.8
'@trpc/client': 10.0.0-rc.8
'@trpc/server': 10.0.0-rc.8
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@tanstack/react-query': 4.16.1_biqbaboplfbrettd7655fr4n2y
'@trpc/client': 10.0.0-rc.8_@trpc+server@10.0.0-rc.8
'@trpc/server': 10.0.0-rc.8
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/@trpc/server/10.0.0-rc.8:
resolution: {integrity: sha512-+NsQqaJGHTiiBcOesH1bIictnnSO+SXaCOy5pM/324QBnJRt9VOO8oYDLcHSwAbJpAbOA+vl6YHs8OoykW6D2g==}
dev: false
/@trysound/sax/0.2.0:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@ -8105,6 +8203,15 @@ packages:
engines: {node: '>=6'}
dev: false
/co-body/6.1.0:
resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==}
dependencies:
inflation: 2.0.0
qs: 6.11.0
raw-body: 2.5.1
type-is: 1.6.18
dev: false
/co/4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@ -8340,6 +8447,13 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/copy-anything/3.0.2:
resolution: {integrity: sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA==}
engines: {node: '>=12.13'}
dependencies:
is-what: 4.1.7
dev: true
/copy-text-to-clipboard/3.0.1:
resolution: {integrity: sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==}
engines: {node: '>=12'}
@ -11457,6 +11571,11 @@ packages:
engines: {node: '>=12'}
dev: false
/inflation/2.0.0:
resolution: {integrity: sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==}
engines: {node: '>= 0.8.0'}
dev: false
/inflight/1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies:
@ -11844,6 +11963,11 @@ packages:
dependencies:
call-bind: 1.0.2
/is-what/4.1.7:
resolution: {integrity: sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==}
engines: {node: '>=12.13'}
dev: true
/is-whitespace-character/1.0.4:
resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==}
dev: false
@ -13836,6 +13960,22 @@ packages:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
dev: true
/node-mocks-http/1.12.1:
resolution: {integrity: sha512-jrA7Sn3qI6GsHgWtUW3gMj0vO6Yz0nJjzg3jRZYjcfj4tzi8oWPauDK1qHVJoAxTbwuDHF1JiM9GISZ/ocI/ig==}
engines: {node: '>=0.6'}
dependencies:
accepts: 1.3.8
content-disposition: 0.5.4
depd: 1.1.2
fresh: 0.5.2
merge-descriptors: 1.0.1
methods: 1.1.2
mime: 1.6.0
parseurl: 1.3.3
range-parser: 1.2.1
type-is: 1.6.18
dev: false
/node-releases/2.0.6:
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
@ -14030,6 +14170,10 @@ packages:
is-wsl: 2.2.0
dev: false
/openapi-types/12.0.2:
resolution: {integrity: sha512-GuTo7FyZjOIWVhIhQSWJVaws6A82sWIGyQogxxYBYKZ0NBdyP2CYSIgOwFfSB+UVoPExk/YzFpyYitHS8KVZtA==}
dev: false
/opener/1.5.2:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
hasBin: true
@ -15723,6 +15867,14 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
/react-ssr-prepass/1.5.0_react@18.2.0:
resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/react-style-singleton/2.2.1_fan5qbzahqtxlm5dzefqlqx5ia:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@ -17055,6 +17207,13 @@ packages:
pirates: 4.0.5
ts-interface-checker: 0.1.13
/superjson/1.11.0:
resolution: {integrity: sha512-6PfAg1FKhqkwWvPb2uXhH4MkMttdc17eJ91+Aoz4s1XUEDZFmLfFx/xVA3wgkPxAGy5dpozgGdK6V/n20Wj9yg==}
engines: {node: '>=10'}
dependencies:
copy-anything: 3.0.2
dev: true
/supports-color/5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@ -17346,6 +17505,21 @@ packages:
resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==}
dev: false
/trpc-openapi/1.0.0-alpha.4_m7lve6cjvd26xbjqz4zsiln46q:
resolution: {integrity: sha512-Zt+IUlYY/pw3Kk78ByTCOZb47r8zJFBUqr1imiOzyVjpaUMVMSPA2RpATCiOJQGSvR70YqoZAMPAu+KPoDmh2w==}
peerDependencies:
'@trpc/server': ^10.0.0-rc.7
zod: ^3.14.4
dependencies:
'@trpc/server': 10.0.0-rc.8
co-body: 6.1.0
lodash.clonedeep: 4.5.0
node-mocks-http: 1.12.1
openapi-types: 12.0.2
zod: 3.19.1
zod-to-json-schema: 3.19.0_zod@3.19.1
dev: false
/ts-easing/0.2.0:
resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==}
dev: false
@ -18523,9 +18697,16 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
/zod-to-json-schema/3.19.0_zod@3.19.1:
resolution: {integrity: sha512-ChPYAl3MWnZwKgNMMYP+/MgYWekL4Ef6n0ot/PBURTUfBpzjnSFB8unZaL3MDwH0YcAwQW63n2pnPJi/dqHfwg==}
peerDependencies:
zod: ^3.19.0
dependencies:
zod: 3.19.1
dev: false
/zod/3.19.1:
resolution: {integrity: sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==}
dev: false
/zustand/3.7.2_react@18.2.0:
resolution: {integrity: sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==}