2
0

🚸 Improve parsing preprocessing on typebots

This commit is contained in:
Baptiste Arnaud
2023-08-23 10:57:38 +02:00
parent fe54888350
commit 0acede92ef
24 changed files with 132 additions and 584 deletions

View File

@@ -1,14 +0,0 @@
import { Webhook } from '@typebot.io/schemas'
import { sendRequest } from '@typebot.io/lib'
type Props = {
typebotId: string
data: Partial<Omit<Webhook, 'typebotId'>>
}
export const createWebhookQuery = ({ typebotId, data }: Props) =>
sendRequest<{ webhook: Webhook }>({
method: 'POST',
url: `/api/typebots/${typebotId}/webhooks`,
body: { data },
})

View File

@@ -1,27 +0,0 @@
import { Webhook } from '@typebot.io/schemas'
import { sendRequest } from '@typebot.io/lib'
import { createWebhookQuery } from './createWebhookQuery'
type Props = {
existingIds: { typebotId: string; webhookId: string }
newIds: { typebotId: string; webhookId: string }
}
export const duplicateWebhookQuery = async ({
existingIds,
newIds,
}: Props): Promise<Webhook | undefined> => {
const { data } = await sendRequest<{ webhook: Webhook }>(
`/api/typebots/${existingIds.typebotId}/webhooks/${existingIds.webhookId}`
)
if (!data) return
const newWebhook = {
...data.webhook,
id: newIds.webhookId,
typebotId: newIds.typebotId,
}
await createWebhookQuery({
typebotId: newIds.typebotId,
data: { ...data.webhook, id: newIds.webhookId },
})
return newWebhook
}

View File

@@ -1,15 +0,0 @@
import { Webhook } from '@typebot.io/schemas'
import { sendRequest } from '@typebot.io/lib'
type Props = {
typebotId: string
webhookId: string
data: Partial<Omit<Webhook, 'id' | 'typebotId'>>
}
export const updateWebhookQuery = ({ typebotId, webhookId, data }: Props) =>
sendRequest<{ webhook: Webhook }>({
method: 'PATCH',
url: `/api/typebots/${typebotId}/webhooks/${webhookId}`,
body: { data },
})

View File

@@ -24,7 +24,7 @@ export const getLinkedTypebots = authenticatedProcedure
.output( .output(
z.object({ z.object({
typebots: z.array( typebots: z.array(
typebotSchema.pick({ typebotSchema._def.schema.pick({
id: true, id: true,
groups: true, groups: true,
variables: true, variables: true,
@@ -58,7 +58,7 @@ export const getLinkedTypebots = authenticatedProcedure
throw new TRPCError({ code: 'NOT_FOUND', message: 'No typebot found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'No typebot found' })
const linkedTypebotIds = const linkedTypebotIds =
typebotSchema.shape.groups typebotSchema._def.schema.shape.groups
.parse(typebot.groups) .parse(typebot.groups)
.flatMap((group) => group.blocks) .flatMap((group) => group.blocks)
.reduce<string[]>( .reduce<string[]>(
@@ -102,8 +102,10 @@ export const getLinkedTypebots = authenticatedProcedure
}) })
.map((typebot) => ({ .map((typebot) => ({
...typebot, ...typebot,
groups: typebotSchema.shape.groups.parse(typebot.groups), groups: typebotSchema._def.schema.shape.groups.parse(typebot.groups),
variables: typebotSchema.shape.variables.parse(typebot.variables), variables: typebotSchema._def.schema.shape.variables.parse(
typebot.variables
),
})) }))
return { return {

View File

@@ -1,62 +0,0 @@
import { createId } from '@paralleldrive/cuid2'
import {
defaultSettings,
defaultTheme,
Group,
StartBlock,
Typebot,
} from '@typebot.io/schemas'
// TODO: remove
export type NewTypebotProps = Omit<
Typebot,
| 'createdAt'
| 'updatedAt'
| 'id'
| 'publicId'
| 'customDomain'
| 'icon'
| 'isArchived'
| 'isClosed'
| 'resultsTablePreferences'
>
export const parseNewTypebot = ({
folderId,
name,
workspaceId,
isBrandingEnabled = true,
}: {
folderId: string | null
workspaceId: string
name: string
ownerAvatarUrl?: string
isBrandingEnabled?: boolean
}): NewTypebotProps => {
const startGroupId = createId()
const startBlockId = createId()
const startBlock: StartBlock = {
groupId: startGroupId,
id: startBlockId,
label: 'Start',
type: 'start',
}
const startGroup: Group = {
id: startGroupId,
title: 'Start',
graphCoordinates: { x: 0, y: 0 },
blocks: [startBlock],
}
return {
folderId,
name,
version: '4',
workspaceId,
groups: [startGroup],
edges: [],
variables: [],
selectedThemeTemplateId: null,
theme: defaultTheme,
settings: defaultSettings({ isBrandingEnabled }),
}
}

View File

@@ -33,9 +33,7 @@ test('should not be able to submit taken url ID', async ({ page }) => {
await page.getByRole('textbox').press('Enter') await page.getByRole('textbox').press('Enter')
await expect( await expect(
page page
.getByText( .getByText('Can only contain lowercase letters, numbers and dashes.')
'Should contain only contain letters, numbers. Words can be separated by dashes.'
)
.nth(0) .nth(0)
).toBeVisible() ).toBeVisible()
await page.getByText(`${typebotId}-public`).click() await page.getByText(`${typebotId}-public`).click()

View File

@@ -1,4 +1,3 @@
import { parseInvalidTypebot } from '@/features/typebot/helpers/parseInvalidTypebot'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { Button, ButtonProps, chakra } from '@chakra-ui/react' import { Button, ButtonProps, chakra } from '@chakra-ui/react'
import { Typebot, typebotCreateSchema } from '@typebot.io/schemas' import { Typebot, typebotCreateSchema } from '@typebot.io/schemas'
@@ -19,9 +18,7 @@ export const ImportTypebotFromFileButton = ({
const file = e.target.files[0] const file = e.target.files[0]
const fileContent = await readFile(file) const fileContent = await readFile(file)
try { try {
const typebot = typebotCreateSchema.parse( const typebot = typebotCreateSchema.parse(JSON.parse(fileContent))
parseInvalidTypebot(JSON.parse(fileContent))
)
onNewTypebot(typebot as Typebot) onNewTypebot(typebot as Typebot)
} catch (err) { } catch (err) {
console.error(err) console.error(err)

View File

@@ -4,8 +4,6 @@ import { TRPCError } from '@trpc/server'
import { publicTypebotSchema } from '@typebot.io/schemas' import { publicTypebotSchema } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { isReadTypebotForbidden } from '../helpers/isReadTypebotForbidden' import { isReadTypebotForbidden } from '../helpers/isReadTypebotForbidden'
import { parseInvalidTypebot } from '../helpers/parseInvalidTypebot'
import { PublicTypebot } from '@typebot.io/schemas'
export const getPublishedTypebot = authenticatedProcedure export const getPublishedTypebot = authenticatedProcedure
.meta({ .meta({
@@ -50,7 +48,7 @@ export const getPublishedTypebot = authenticatedProcedure
try { try {
const parsedTypebot = publicTypebotSchema.parse( const parsedTypebot = publicTypebotSchema.parse(
parseInvalidTypebot(existingTypebot.publishedTypebot as PublicTypebot) existingTypebot.publishedTypebot
) )
return { return {

View File

@@ -4,10 +4,7 @@ import { TRPCError } from '@trpc/server'
import { Typebot, typebotSchema } from '@typebot.io/schemas' import { Typebot, typebotSchema } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { isReadTypebotForbidden } from '../helpers/isReadTypebotForbidden' import { isReadTypebotForbidden } from '../helpers/isReadTypebotForbidden'
import { omit } from '@typebot.io/lib'
import { Typebot as TypebotFromDb } from '@typebot.io/prisma'
import { migrateTypebotFromV3ToV4 } from '@typebot.io/lib/migrations/migrateTypebotFromV3ToV4' import { migrateTypebotFromV3ToV4 } from '@typebot.io/lib/migrations/migrateTypebotFromV3ToV4'
import { parseInvalidTypebot } from '../helpers/parseInvalidTypebot'
export const getTypebot = authenticatedProcedure export const getTypebot = authenticatedProcedure
.meta({ .meta({
@@ -46,8 +43,8 @@ export const getTypebot = authenticatedProcedure
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
try { try {
const parsedTypebot = await parseTypebot( const parsedTypebot = await migrateTypebot(
omit(existingTypebot, 'collaborators') typebotSchema.parse(existingTypebot)
) )
return { return {
@@ -66,10 +63,7 @@ export const getTypebot = authenticatedProcedure
} }
}) })
const parseTypebot = async (typebot: TypebotFromDb): Promise<Typebot> => { const migrateTypebot = async (typebot: Typebot): Promise<Typebot> => {
const parsedTypebot = typebotSchema.parse( if (['4', '5'].includes(typebot.version ?? '')) return typebot
typebot.version !== '5' ? parseInvalidTypebot(typebot as Typebot) : typebot return migrateTypebotFromV3ToV4(prisma)(typebot)
)
if (['4', '5'].includes(parsedTypebot.version ?? '')) return parsedTypebot
return migrateTypebotFromV3ToV4(prisma)(parsedTypebot)
} }

View File

@@ -21,7 +21,7 @@ export const listTypebots = authenticatedProcedure
.output( .output(
z.object({ z.object({
typebots: z.array( typebots: z.array(
typebotSchema typebotSchema._def.schema
.pick({ .pick({
name: true, name: true,
icon: true, icon: true,

View File

@@ -49,15 +49,21 @@ export const publishTypebot = authenticatedProcedure
}, },
data: { data: {
version: existingTypebot.version, version: existingTypebot.version,
edges: typebotSchema.shape.edges.parse(existingTypebot.edges), edges: typebotSchema._def.schema.shape.edges.parse(
groups: typebotSchema.shape.groups.parse(existingTypebot.groups), existingTypebot.edges
settings: typebotSchema.shape.settings.parse( ),
groups: typebotSchema._def.schema.shape.groups.parse(
existingTypebot.groups
),
settings: typebotSchema._def.schema.shape.settings.parse(
existingTypebot.settings existingTypebot.settings
), ),
variables: typebotSchema.shape.variables.parse( variables: typebotSchema._def.schema.shape.variables.parse(
existingTypebot.variables existingTypebot.variables
), ),
theme: typebotSchema.shape.theme.parse(existingTypebot.theme), theme: typebotSchema._def.schema.shape.theme.parse(
existingTypebot.theme
),
}, },
}) })
else else
@@ -65,15 +71,21 @@ export const publishTypebot = authenticatedProcedure
data: { data: {
version: existingTypebot.version, version: existingTypebot.version,
typebotId: existingTypebot.id, typebotId: existingTypebot.id,
edges: typebotSchema.shape.edges.parse(existingTypebot.edges), edges: typebotSchema._def.schema.shape.edges.parse(
groups: typebotSchema.shape.groups.parse(existingTypebot.groups), existingTypebot.edges
settings: typebotSchema.shape.settings.parse( ),
groups: typebotSchema._def.schema.shape.groups.parse(
existingTypebot.groups
),
settings: typebotSchema._def.schema.shape.settings.parse(
existingTypebot.settings existingTypebot.settings
), ),
variables: typebotSchema.shape.variables.parse( variables: typebotSchema._def.schema.shape.variables.parse(
existingTypebot.variables existingTypebot.variables
), ),
theme: typebotSchema.shape.theme.parse(existingTypebot.theme), theme: typebotSchema._def.schema.shape.theme.parse(
existingTypebot.theme
),
}, },
}) })

View File

@@ -27,7 +27,7 @@ export const updateTypebot = authenticatedProcedure
z.object({ z.object({
typebotId: z.string(), typebotId: z.string(),
typebot: typebotCreateSchema.merge( typebot: typebotCreateSchema.merge(
typebotSchema typebotSchema._def.schema
.pick({ .pick({
isClosed: true, isClosed: true,
}) })

View File

@@ -1,12 +0,0 @@
import { Edge, PublicTypebot, Typebot, edgeSchema } from '@typebot.io/schemas'
export const parseInvalidTypebot = (
typebot: Typebot | PublicTypebot
): Typebot | PublicTypebot => ({
...typebot,
version: typebot.version as null | '3' | '4' | '5',
edges: parseInvalidEdges(typebot.edges),
})
const parseInvalidEdges = (edges: Edge[]) =>
edges?.filter((edge) => edgeSchema.safeParse(edge).success)

View File

@@ -1,62 +0,0 @@
import { CustomDomain } from '@typebot.io/prisma'
import { got, HTTPError } from 'got'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from '@typebot.io/lib/api'
// TODO: delete
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res)
if (!user) return notAuthenticated(res)
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res)
if (req.method === 'GET') {
const customDomains = await prisma.customDomain.findMany({
where: {
workspace: { id: workspaceId, members: { some: { userId: user.id } } },
},
})
return res.send({ customDomains })
}
if (req.method === 'POST') {
const workspace = await prisma.workspace.findFirst({
where: { id: workspaceId, members: { some: { userId: user.id } } },
select: { id: true },
})
if (!workspace) return forbidden(res)
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as CustomDomain
try {
await createDomainOnVercel(data.name)
} catch (err) {
console.log(err)
if (err instanceof HTTPError && err.response.statusCode !== 409)
return res.status(err.response.statusCode).send(err.response.body)
}
const customDomains = await prisma.customDomain.create({
data: {
...data,
workspaceId,
},
})
return res.send({ customDomains })
}
return methodNotAllowed(res)
}
const createDomainOnVercel = (name: string) =>
got.post({
url: `https://api.vercel.com/v8/projects/${process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains?teamId=${process.env.VERCEL_TEAM_ID}`,
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
json: { name },
})
export default handler

View File

@@ -1,72 +0,0 @@
import { Plan } from '@typebot.io/prisma'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import {
methodNotAllowed,
notAuthenticated,
notFound,
} from '@typebot.io/lib/api'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import {
NewTypebotProps,
parseNewTypebot,
} from '@/features/dashboard/api/parseNewTypebot'
import { omit } from '@typebot.io/lib'
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
// TODO: Delete
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res)
if (!user) return notAuthenticated(res)
try {
if (req.method === 'POST') {
const workspace = await prisma.workspace.findFirst({
where: { id: req.body.workspaceId },
select: { plan: true },
})
if (!workspace) return notFound(res, "Couldn't find workspace")
const data =
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const formattedData = removeOldProperties(data) as
| NewTypebotProps
| Omit<NewTypebotProps, 'groups'>
const typebot = await prisma.typebot.create({
data:
'groups' in formattedData
? formattedData
: parseNewTypebot({
isBrandingEnabled: workspace.plan === Plan.FREE,
...data,
}),
})
await sendTelemetryEvents([
{
name: 'Typebot created',
userId: user.id,
workspaceId: typebot.workspaceId,
typebotId: typebot.id,
data: {
name: typebot.name,
},
},
])
return res.send(typebot)
}
return methodNotAllowed(res)
} catch (err) {
console.error(err)
if (err instanceof Error) {
return res.status(500).send({ title: err.name, message: err.message })
}
return res.status(500).send({ message: 'An error occured', error: err })
}
}
const removeOldProperties = (data: unknown) => {
if (data && typeof data === 'object' && 'publishedTypebotId' in data) {
return omit(data, 'publishedTypebotId')
}
return data
}
export default handler

View File

@@ -1,180 +0,0 @@
import { CollaborationType, Prisma } from '@typebot.io/prisma'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { Group, Typebot } from '@typebot.io/schemas'
import { omit } from '@typebot.io/lib'
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
import { archiveResults } from '@typebot.io/lib/api/helpers/archiveResults'
import { migrateTypebotFromV3ToV4 } from '@typebot.io/lib/migrations/migrateTypebotFromV3ToV4'
import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden'
// TODO: delete
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res)
if (!user) return notAuthenticated(res)
const typebotId = req.query.typebotId as string
if (req.method === 'GET') {
const fullTypebot = await prisma.typebot.findFirst({
where: {
id: typebotId,
isArchived: { not: true },
},
include: {
publishedTypebot: true,
collaborators: { select: { userId: true, type: true } },
webhooks: true,
},
})
if (!fullTypebot || (await isReadTypebotForbidden(fullTypebot, user)))
return res.status(404).send({ typebot: null })
const { publishedTypebot, collaborators, webhooks, ...typebot } =
fullTypebot
const isReadOnly =
collaborators.find((c) => c.userId === user.id)?.type ===
CollaborationType.READ
return res.send({
typebot: await migrateTypebot(typebot as Typebot),
publishedTypebot,
isReadOnly,
webhooks,
})
}
if (req.method === 'DELETE') {
const typebot = await prisma.typebot.findUnique({
where: {
id: typebotId,
},
select: {
id: true,
workspaceId: true,
groups: true,
collaborators: {
select: {
userId: true,
type: true,
},
},
},
})
if (!typebot || (await isWriteTypebotForbidden(typebot, user)))
return res.status(404).send({ typebot: null })
const { success } = await archiveResults(prisma)({
typebot: {
groups: typebot.groups as Group[],
},
resultsFilter: { typebotId },
})
if (!success) return res.status(500).send({ success: false, error: '' })
await prisma.publicTypebot.deleteMany({
where: { typebotId },
})
const typebots = await prisma.typebot.updateMany({
where: { id: typebotId },
data: { isArchived: true, publicId: null, customDomain: null },
})
return res.send({ typebots })
}
if (req.method === 'PUT') {
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as Typebot
const typebot = await prisma.typebot.findUnique({
where: {
id: typebotId,
},
select: {
id: true,
workspaceId: true,
groups: true,
updatedAt: true,
collaborators: {
select: {
userId: true,
type: true,
},
},
},
})
if (!typebot || (await isWriteTypebotForbidden(typebot, user)))
return res.status(404).send({ message: 'Typebot not found' })
if (
(typebot.updatedAt as Date).getTime() > new Date(data.updatedAt).getTime()
)
return res.send({
message: 'Found newer version of the typebot in database',
})
const updates = {
...omit(data, 'id', 'createdAt', 'updatedAt'),
theme: data.theme ?? undefined,
settings: data.settings ?? undefined,
resultsTablePreferences: data.resultsTablePreferences ?? undefined,
groups: data.groups ?? [],
variables: data.variables ?? [],
edges: data.edges ?? [],
} satisfies Prisma.TypebotUpdateInput
try {
const updatedTypebot = await prisma.typebot.update({
where: { id: typebotId },
data: updates,
})
return res.send({ typebot: updatedTypebot })
} catch (err) {
if (err instanceof Prisma.PrismaClientKnownRequestError) {
if (err.code === 'P2002') {
return res.status(409).send({
message:
err.meta && 'target' in err.meta && Array.isArray(err.meta.target)
? `${err.meta.target[0]} already exists`
: 'Duplicate conflict',
})
}
return res.status(500).send({ message: err.message })
}
}
}
if (req.method === 'PATCH') {
const typebot = await prisma.typebot.findUnique({
where: {
id: typebotId,
},
select: {
id: true,
workspaceId: true,
groups: true,
collaborators: {
select: {
userId: true,
type: true,
},
},
},
})
if (!typebot || (await isWriteTypebotForbidden(typebot, user)))
return res.status(404).send({ message: 'Typebot not found' })
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const updatedTypebot = await prisma.typebot.update({
where: { id: typebotId },
data,
})
return res.send({ typebot: updatedTypebot })
}
return methodNotAllowed(res)
}
const migrateTypebot = async (typebot: Typebot): Promise<Typebot> => {
if (typebot.version === '4') return typebot
return migrateTypebotFromV3ToV4(prisma)(typebot)
}
export default handler

View File

@@ -1,38 +0,0 @@
import { PublicTypebot } from '@typebot.io/schemas'
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { canReadTypebots } from '@/helpers/databaseRules'
// TODO: Delete (deprecated)
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true },
})
const publishedTypebot =
typebot?.publishedTypebot as unknown as PublicTypebot
if (!publishedTypebot) return res.status(404).send({ answersCounts: [] })
const answersCounts = await prisma.answer.groupBy({
by: ['groupId'],
where: {
groupId: { in: publishedTypebot.groups.map((g) => g.id) },
},
_count: { _all: true },
})
return res.status(200).send({
answersCounts: answersCounts.map((answer) => ({
groupId: answer.groupId,
totalAnswers: answer._count._all,
})),
})
}
return methodNotAllowed(res)
}
export default handler

View File

@@ -3044,8 +3044,7 @@
} }
}, },
"required": [ "required": [
"id", "id"
"valueToExtract"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -7327,8 +7326,7 @@
} }
}, },
"required": [ "required": [
"id", "id"
"valueToExtract"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -11184,8 +11182,7 @@
} }
}, },
"required": [ "required": [
"id", "id"
"valueToExtract"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -15176,8 +15173,7 @@
} }
}, },
"required": [ "required": [
"id", "id"
"valueToExtract"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -19049,8 +19045,7 @@
} }
}, },
"required": [ "required": [
"id", "id"
"valueToExtract"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -22976,8 +22971,7 @@
} }
}, },
"required": [ "required": [
"id", "id"
"valueToExtract"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -26966,8 +26960,7 @@
} }
}, },
"required": [ "required": [
"id", "id"
"valueToExtract"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -31143,7 +31136,6 @@
"operationId": "customDomains-createCustomDomain", "operationId": "customDomains-createCustomDomain",
"summary": "Create custom domain", "summary": "Create custom domain",
"tags": [ "tags": [
"Workspace",
"Custom domains" "Custom domains"
], ],
"security": [ "security": [
@@ -31218,7 +31210,6 @@
"operationId": "customDomains-deleteCustomDomain", "operationId": "customDomains-deleteCustomDomain",
"summary": "Delete custom domain", "summary": "Delete custom domain",
"tags": [ "tags": [
"Workspace",
"Custom domains" "Custom domains"
], ],
"security": [ "security": [
@@ -31276,7 +31267,6 @@
"operationId": "customDomains-listCustomDomains", "operationId": "customDomains-listCustomDomains",
"summary": "List custom domains", "summary": "List custom domains",
"tags": [ "tags": [
"Workspace",
"Custom domains" "Custom domains"
], ],
"security": [ "security": [

View File

@@ -2621,8 +2621,7 @@
} }
}, },
"required": [ "required": [
"id", "id"
"valueToExtract"
], ],
"additionalProperties": false "additionalProperties": false
} }

View File

@@ -80,7 +80,10 @@ const chatCompletionOptionsSchema = z
responseMapping: z.array( responseMapping: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
valueToExtract: z.enum(chatCompletionResponseValues), valueToExtract: z.preprocess(
(val) => (!val ? 'Message content' : val),
z.enum(chatCompletionResponseValues)
),
variableId: z.string().optional(), variableId: z.string().optional(),
}) })
), ),

View File

@@ -21,7 +21,7 @@ import { BubbleBlockType } from './blocks/bubbles/enums'
import { inputBlockSchemas } from './blocks/schemas' import { inputBlockSchemas } from './blocks/schemas'
import { chatCompletionMessageSchema } from './blocks/integrations/openai' import { chatCompletionMessageSchema } from './blocks/integrations/openai'
const typebotInSessionStateSchema = publicTypebotSchema.pick({ const typebotInSessionStateSchema = publicTypebotSchema._def.schema.pick({
id: true, id: true,
groups: true, groups: true,
edges: true, edges: true,
@@ -131,7 +131,7 @@ const scriptToExecuteSchema = z.object({
), ),
}) })
const startTypebotSchema = typebotSchema.pick({ const startTypebotSchema = typebotSchema._def.schema.pick({
id: true, id: true,
groups: true, groups: true,
edges: true, edges: true,
@@ -286,7 +286,7 @@ export const chatReplySchema = z.object({
.optional(), .optional(),
clientSideActions: z.array(clientSideActionSchema).optional(), clientSideActions: z.array(clientSideActionSchema).optional(),
sessionId: z.string().optional(), sessionId: z.string().optional(),
typebot: typebotSchema typebot: typebotSchema._def.schema
.pick({ id: true, theme: true, settings: true }) .pick({ id: true, theme: true, settings: true })
.optional(), .optional(),
resultId: z.string().optional(), resultId: z.string().optional(),

View File

@@ -8,22 +8,30 @@ import {
typebotSchema, typebotSchema,
} from './typebot' } from './typebot'
import { z } from 'zod' import { z } from 'zod'
import { preprocessTypebot } from './typebot/helpers/preprocessTypebot'
export const publicTypebotSchema = z.object({ export const publicTypebotSchema = z.preprocess(
id: z.string(), preprocessTypebot,
version: z.enum(['3', '4', '5']).nullable(), z.object({
createdAt: z.date(), id: z.string(),
updatedAt: z.date(), version: z.enum(['3', '4', '5']).nullable(),
typebotId: z.string(), createdAt: z.date(),
groups: z.array(groupSchema), updatedAt: z.date(),
edges: z.array(edgeSchema), typebotId: z.string(),
variables: z.array(variableSchema), groups: z.array(groupSchema),
theme: themeSchema, edges: z.array(edgeSchema),
settings: settingsSchema, variables: z.array(variableSchema),
}) satisfies z.ZodType<PrismaPublicTypebot> theme: themeSchema,
settings: settingsSchema,
})
) satisfies z.ZodType<PrismaPublicTypebot, z.ZodTypeDef, unknown>
const publicTypebotWithName = publicTypebotSchema.merge( const publicTypebotWithName = publicTypebotSchema._def.schema.merge(
typebotSchema.pick({ name: true, isArchived: true, isClosed: true }) typebotSchema._def.schema.pick({
name: true,
isArchived: true,
isClosed: true,
})
) )
export type PublicTypebot = z.infer<typeof publicTypebotSchema> export type PublicTypebot = z.infer<typeof publicTypebotSchema>

View File

@@ -0,0 +1,25 @@
import { Block } from '../../blocks'
import { Group, edgeSchema } from '../typebot'
export const preprocessTypebot = (typebot: any) => {
if (!typebot || typebot.version === '5') return typebot
return {
...typebot,
groups: typebot.groups.map(preprocessGroup),
edges: typebot.edges?.filter(
(edge: any) => edgeSchema.safeParse(edge).success
),
}
}
const preprocessGroup = (group: Group) => ({
...group,
blocks: group.blocks.map((block) =>
preprocessBlock(block, { groupId: group.id })
),
})
const preprocessBlock = (block: Block, { groupId }: { groupId: string }) => ({
...block,
groupId: block.groupId ?? groupId,
})

View File

@@ -4,6 +4,7 @@ import { themeSchema } from './theme'
import { variableSchema } from './variable' import { variableSchema } from './variable'
import { Typebot as TypebotPrisma } from '@typebot.io/prisma' import { Typebot as TypebotPrisma } from '@typebot.io/prisma'
import { blockSchema } from '../blocks/schemas' import { blockSchema } from '../blocks/schemas'
import { preprocessTypebot } from './helpers/preprocessTypebot'
export const groupSchema = z.object({ export const groupSchema = z.object({
id: z.string(), id: z.string(),
@@ -43,35 +44,38 @@ const isDomainNameWithPathNameCompatible = (str: string) =>
str str
) )
export const typebotSchema = z.object({ export const typebotSchema = z.preprocess(
version: z.enum(['3', '4', '5']).nullable(), preprocessTypebot,
id: z.string(), z.object({
name: z.string(), version: z.enum(['3', '4', '5']).nullable(),
groups: z.array(groupSchema), id: z.string(),
edges: z.array(edgeSchema), name: z.string(),
variables: z.array(variableSchema), groups: z.array(groupSchema),
theme: themeSchema, edges: z.array(edgeSchema),
selectedThemeTemplateId: z.string().nullable(), variables: z.array(variableSchema),
settings: settingsSchema, theme: themeSchema,
createdAt: z.date(), selectedThemeTemplateId: z.string().nullable(),
updatedAt: z.date(), settings: settingsSchema,
icon: z.string().nullable(), createdAt: z.date(),
folderId: z.string().nullable(), updatedAt: z.date(),
publicId: z icon: z.string().nullable(),
.string() folderId: z.string().nullable(),
.refine((str) => /^[a-zA-Z0-9-.]+$/.test(str)) publicId: z
.nullable(), .string()
customDomain: z .refine((str) => /^[a-zA-Z0-9-.]+$/.test(str))
.string() .nullable(),
.refine(isDomainNameWithPathNameCompatible) customDomain: z
.nullable(), .string()
workspaceId: z.string(), .refine(isDomainNameWithPathNameCompatible)
resultsTablePreferences: resultsTablePreferencesSchema.nullable(), .nullable(),
isArchived: z.boolean(), workspaceId: z.string(),
isClosed: z.boolean(), resultsTablePreferences: resultsTablePreferencesSchema.nullable(),
}) satisfies z.ZodType<TypebotPrisma> isArchived: z.boolean(),
isClosed: z.boolean(),
}) satisfies z.ZodType<TypebotPrisma, z.ZodTypeDef, unknown>
)
export const typebotCreateSchema = typebotSchema export const typebotCreateSchema = typebotSchema._def.schema
.pick({ .pick({
name: true, name: true,
icon: true, icon: true,