diff --git a/lib/validations/event-type.ts b/lib/validations/event-type.ts index 579793a446..37db4637eb 100644 --- a/lib/validations/event-type.ts +++ b/lib/validations/event-type.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { _EventTypeModel as EventType } from "@calcom/prisma/zod"; +import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod"; import { customInputSchema } from "@calcom/prisma/zod-utils"; import { Frequency } from "~/lib/types"; @@ -18,6 +18,11 @@ const recurringEventInputSchema = z.object({ tzid: timeZone.optional(), }); +const hostSchema = _HostModel.pick({ + isFixed: true, + userId: true, +}); + export const schemaEventTypeBaseBodyParams = EventType.pick({ title: true, description: true, @@ -25,11 +30,11 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({ length: true, hidden: true, position: true, - teamId: true, eventName: true, timeZone: true, periodType: true, periodStartDate: true, + schedulingType: true, periodEndDate: true, periodDays: true, periodCountCalendarDays: true, @@ -39,13 +44,14 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({ minimumBookingNotice: true, beforeEventBuffer: true, afterEventBuffer: true, - schedulingType: true, + teamId: true, price: true, currency: true, slotInterval: true, successRedirectUrl: true, locations: true, }) + .merge(z.object({ hosts: z.array(hostSchema).optional().default([]) })) .partial() .strict(); diff --git a/pages/api/event-types/[id]/_patch.ts b/pages/api/event-types/[id]/_patch.ts index 57662a623e..d44908b2b1 100644 --- a/pages/api/event-types/[id]/_patch.ts +++ b/pages/api/event-types/[id]/_patch.ts @@ -1,3 +1,5 @@ +import type { Prisma } from "@prisma/client"; +import { SchedulingType } from "@prisma/client"; import type { NextApiRequest } from "next"; import type { z } from "zod"; @@ -7,6 +9,7 @@ import { defaultResponder } from "@calcom/lib/server"; import type { schemaEventTypeBaseBodyParams } from "~/lib/validations/event-type"; import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; +import ensureOnlyMembersAsHosts from "~/pages/api/event-types/_utils/ensureOnlyMembersAsHosts"; import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission"; @@ -49,6 +52,16 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission * slug: * type: string * description: Unique slug for the event type + * hosts: + * type: array + * items: + * type: object + * properties: + * userId: + * type: number + * isFixed: + * type: boolean + * description: Host MUST be available for any slot to be bookable. * hidden: * type: boolean * description: If the event type should be hidden from your public booking page @@ -156,10 +169,25 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission export async function patchHandler(req: NextApiRequest) { const { prisma, query, body } = req; const { id } = schemaQueryIdParseInt.parse(query); - const data = schemaEventTypeEditBodyParams.parse(body); - await checkPermissions(req, body); - const event_type = await prisma.eventType.update({ where: { id }, data }); - return { event_type: schemaEventTypeReadPublic.parse(event_type) }; + const { hosts = [], ...parsedBody } = schemaEventTypeEditBodyParams.parse(body); + + const data: Prisma.EventTypeUpdateArgs["data"] = { + ...parsedBody, + }; + + if (hosts) { + await ensureOnlyMembersAsHosts(req, parsedBody); + data.hosts = { + deleteMany: {}, + create: hosts.map((host) => ({ + ...host, + isFixed: data.schedulingType === SchedulingType.COLLECTIVE || host.isFixed, + })), + }; + } + await checkPermissions(req, parsedBody); + const eventType = await prisma.eventType.update({ where: { id }, data }); + return { event_type: schemaEventTypeReadPublic.parse(eventType) }; } async function checkPermissions(req: NextApiRequest, body: z.infer) { diff --git a/pages/api/event-types/_post.ts b/pages/api/event-types/_post.ts index 28be45e2f5..687f4f220c 100644 --- a/pages/api/event-types/_post.ts +++ b/pages/api/event-types/_post.ts @@ -7,6 +7,7 @@ import { defaultResponder } from "@calcom/lib/server"; import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type"; import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission"; +import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts"; /** * @swagger @@ -46,6 +47,17 @@ import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission" * slug: * type: string * description: Unique slug for the event type + * example: my-event + * hosts: + * type: array + * items: + * type: object + * properties: + * userId: + * type: number + * isFixed: + * type: boolean + * description: Host MUST be available for any slot to be bookable. * hidden: * type: boolean * description: If the event type should be hidden from your public booking page @@ -176,22 +188,33 @@ import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission" * description: Authorization information is missing or invalid. */ async function postHandler(req: NextApiRequest) { - const { userId, isAdmin, prisma } = req; - const body = schemaEventTypeCreateBodyParams.parse(req.body); - let args: Prisma.EventTypeCreateArgs = { - data: { ...body, userId, users: { connect: { id: userId } } }, + const { userId, isAdmin, prisma, body } = req; + + const { hosts = [], ...parsedBody } = schemaEventTypeCreateBodyParams.parse(body || {}); + + let data: Prisma.EventTypeCreateArgs["data"] = { + ...parsedBody, + userId, + users: { connect: { id: userId } }, }; await checkPermissions(req); - if (isAdmin && body.userId) args = { data: { ...body, users: { connect: { id: body.userId } } } }; + if (isAdmin && parsedBody.userId) { + data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } }; + } - await checkTeamEventEditPermission(req, body); + await checkTeamEventEditPermission(req, parsedBody); + await ensureOnlyMembersAsHosts(req, parsedBody); - const data = await prisma.eventType.create(args); + if (hosts) { + data.hosts = { createMany: { data: hosts } }; + } + + const eventType = await prisma.eventType.create({ data }); return { - event_type: schemaEventTypeReadPublic.parse(data), + event_type: schemaEventTypeReadPublic.parse(eventType), message: "Event type created successfully", }; } diff --git a/pages/api/event-types/_utils/ensureOnlyMembersAsHosts.ts b/pages/api/event-types/_utils/ensureOnlyMembersAsHosts.ts new file mode 100644 index 0000000000..301c5307a1 --- /dev/null +++ b/pages/api/event-types/_utils/ensureOnlyMembersAsHosts.ts @@ -0,0 +1,21 @@ +import type { NextApiRequest } from "next"; +import type { z } from "zod"; + +import type { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type"; + +export default async function ensureOnlyMembersAsHosts( + req: NextApiRequest, + body: Pick, "hosts" | "teamId"> +) { + if (body.teamId && body.hosts && body.hosts.length > 0) { + const teamMemberCount = await req.prisma.membership.count({ + where: { + teamId: body.teamId, + userId: { in: body.hosts.map((host) => host.userId) }, + }, + }); + if (teamMemberCount !== body.hosts.length) { + throw new Error("You can only add members of the team to a team event type."); + } + } +}