🧑‍💻 (folders) Add folder trpc endpoints (#1218)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced folder management capabilities including creation,
deletion, update, listing, and retrieval within workspaces.
- Added telemetry tracking for client events, Typebot publish events,
and analytics page views.
	- Enhanced settings to track client events under specific conditions.
- Implemented server-side logic for analytics tracking with PostHog
integration.
- Added API documentation for folder operations (create, delete, get,
list, update).
- **Refactor**
	- Updated `onConfirm` function's return type in `ConfirmModal`.
	- Simplified folder creation process in tests.
- Refactored logic for handling file upload blocks and parsing publish
events in Typebot publishing.
	- Migrated handler functions to TRPC endpoints for folder operations.
- **Documentation**
- Introduced documentation for new folder and telemetry functionalities.
- **Chores**
- Added new schemas for folders and telemetry events, including event
tracking and folder structure.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2024-02-05 12:14:03 +01:00
committed by GitHub
parent 9014c4ab09
commit 84b9aca40b
37 changed files with 1399 additions and 168 deletions

View File

@@ -0,0 +1,6 @@
import { router } from '@/helpers/server/trpc'
import { trackClientEvents } from './trackClientEvents'
export const telemetryRouter = router({
trackClientEvents,
})

View File

@@ -0,0 +1,95 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import prisma from '@typebot.io/lib/prisma'
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
import { WorkspaceRole } from '@typebot.io/prisma'
import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden'
import { trackEvents } from '@typebot.io/lib/telemetry/trackEvents'
import { clientSideCreateEventSchema } from '@typebot.io/schemas'
export const trackClientEvents = authenticatedProcedure
.input(
z.object({
events: z.array(clientSideCreateEventSchema),
})
)
.output(
z.object({
message: z.literal('success'),
})
)
.mutation(async ({ input: { events }, ctx: { user } }) => {
const workspaces = await prisma.workspace.findMany({
where: {
id: {
in: events
.filter((event) => 'workspaceId' in event)
.map((event) => (event as { workspaceId: string }).workspaceId),
},
},
select: {
id: true,
members: true,
},
})
const typebots = await prisma.typebot.findMany({
where: {
id: {
in: events
.filter((event) => 'typebotId' in event)
.map((event) => (event as { typebotId: string }).typebotId),
},
},
select: {
id: true,
workspaceId: true,
workspace: {
select: {
isSuspended: true,
isPastDue: true,
members: {
select: {
role: true,
userId: true,
},
},
},
},
collaborators: {
select: {
userId: true,
type: true,
},
},
},
})
for (const event of events) {
if ('workspaceId' in event) {
const workspace = workspaces.find((w) => w.id === event.workspaceId)
const userRole = getUserRoleInWorkspace(user.id, workspace?.members)
if (
userRole === undefined ||
userRole === WorkspaceRole.GUEST ||
!workspace
)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
}
if ('typebotId' in event) {
const typebot = typebots.find((t) => t.id === event.typebotId)
if (!typebot || (await isWriteTypebotForbidden(typebot, user)))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Typebot not found',
})
}
}
await trackEvents(events.map((e) => ({ ...e, userId: user.id })))
return { message: 'success' }
})

View File

@@ -0,0 +1,48 @@
import { env } from '@typebot.io/env'
import prisma from '@typebot.io/lib/prisma'
import { parseGroups, Typebot } from '@typebot.io/schemas'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
type Props = {
existingTypebot: Pick<Typebot, 'id' | 'workspaceId'>
userId: string
hasFileUploadBlocks: boolean
}
export const parseTypebotPublishEvents = async ({
existingTypebot,
userId,
hasFileUploadBlocks,
}: Props) => {
if (!env.NEXT_PUBLIC_POSTHOG_KEY) return []
const events = []
const existingPublishedTypebot = await prisma.publicTypebot.findFirst({
where: {
typebotId: existingTypebot.id,
},
select: {
version: true,
groups: true,
settings: true,
},
})
const isPublishingFileUploadBlockForTheFirstTime =
hasFileUploadBlocks &&
(!existingPublishedTypebot ||
!parseGroups(existingPublishedTypebot.groups, {
typebotVersion: existingPublishedTypebot.version,
}).some((group) =>
group.blocks.some((block) => block.type === InputBlockType.FILE)
))
if (isPublishingFileUploadBlockForTheFirstTime)
events.push({
name: 'File upload block published',
workspaceId: existingTypebot.workspaceId,
typebotId: existingTypebot.id,
userId,
} as const)
return events
}

View File

@@ -0,0 +1,31 @@
import { getAuthOptions } from '@/pages/api/auth/[...nextauth]'
import prisma from '@typebot.io/lib/prisma'
import { trackEvents } from '@typebot.io/lib/telemetry/trackEvents'
import { User } from '@typebot.io/schemas'
import { GetServerSidePropsContext } from 'next'
import { getServerSession } from 'next-auth'
export const trackAnalyticsPageView = async (
context: GetServerSidePropsContext
) => {
const typebotId = context.params?.typebotId as string | undefined
if (!typebotId) return
const typebot = await prisma.typebot.findUnique({
where: { id: typebotId },
select: { workspaceId: true },
})
if (!typebot) return
const session = await getServerSession(
context.req,
context.res,
getAuthOptions({})
)
await trackEvents([
{
name: 'Analytics visited',
typebotId,
userId: (session?.user as User).id,
workspaceId: typebot.workspaceId,
},
])
}