feat: add parentid eventtype API along with children eventTypeIds (#11714)
* add parentId * Allow POST parentId * adds eventtypeid of children * add doc * adds userId in children * adds validations * adds docstring * fix status codes * check fix for ownerspull/11761/head
parent
ceb8906c27
commit
49b0f48be0
|
@ -24,6 +24,11 @@ const hostSchema = _HostModel.pick({
|
||||||
userId: true,
|
userId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const childrenSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
userId: z.number().int(),
|
||||||
|
});
|
||||||
|
|
||||||
export const schemaEventTypeBaseBodyParams = EventType.pick({
|
export const schemaEventTypeBaseBodyParams = EventType.pick({
|
||||||
title: true,
|
title: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
@ -45,6 +50,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
||||||
disableGuests: true,
|
disableGuests: true,
|
||||||
hideCalendarNotes: true,
|
hideCalendarNotes: true,
|
||||||
minimumBookingNotice: true,
|
minimumBookingNotice: true,
|
||||||
|
parentId: true,
|
||||||
beforeEventBuffer: true,
|
beforeEventBuffer: true,
|
||||||
afterEventBuffer: true,
|
afterEventBuffer: true,
|
||||||
teamId: true,
|
teamId: true,
|
||||||
|
@ -56,7 +62,12 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
||||||
bookingLimits: true,
|
bookingLimits: true,
|
||||||
durationLimits: true,
|
durationLimits: true,
|
||||||
})
|
})
|
||||||
.merge(z.object({ hosts: z.array(hostSchema).optional().default([]) }))
|
.merge(
|
||||||
|
z.object({
|
||||||
|
children: z.array(childrenSchema).optional().default([]),
|
||||||
|
hosts: z.array(hostSchema).optional().default([]),
|
||||||
|
})
|
||||||
|
)
|
||||||
.partial()
|
.partial()
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
@ -73,6 +84,7 @@ const schemaEventTypeCreateParams = z
|
||||||
seatsShowAvailabilityCount: z.boolean().optional(),
|
seatsShowAvailabilityCount: z.boolean().optional(),
|
||||||
bookingFields: eventTypeBookingFields.optional(),
|
bookingFields: eventTypeBookingFields.optional(),
|
||||||
scheduleId: z.number().optional(),
|
scheduleId: z.number().optional(),
|
||||||
|
parentId: z.number().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
@ -125,6 +137,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
||||||
price: true,
|
price: true,
|
||||||
currency: true,
|
currency: true,
|
||||||
slotInterval: true,
|
slotInterval: true,
|
||||||
|
parentId: true,
|
||||||
successRedirectUrl: true,
|
successRedirectUrl: true,
|
||||||
description: true,
|
description: true,
|
||||||
locations: true,
|
locations: true,
|
||||||
|
@ -137,6 +150,8 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
||||||
durationLimits: true,
|
durationLimits: true,
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
children: z.array(childrenSchema).optional().default([]),
|
||||||
|
hosts: z.array(hostSchema).optional().default([]),
|
||||||
locations: z
|
locations: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|
|
@ -52,6 +52,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||||
team: { select: { slug: true } },
|
team: { select: { slug: true } },
|
||||||
users: true,
|
users: true,
|
||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
|
children: { select: { id: true, userId: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await checkPermissions(req, eventType);
|
await checkPermissions(req, eventType);
|
||||||
|
|
|
@ -46,6 +46,7 @@ async function getHandler(req: NextApiRequest) {
|
||||||
team: { select: { slug: true } },
|
team: { select: { slug: true } },
|
||||||
users: true,
|
users: true,
|
||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
|
children: { select: { id: true, userId: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// this really should return [], but backwards compatibility..
|
// this really should return [], but backwards compatibility..
|
||||||
|
|
|
@ -6,7 +6,9 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||||
|
|
||||||
|
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
||||||
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
|
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
|
||||||
|
import checkUserMembership from "./_utils/checkUserMembership";
|
||||||
import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,10 +120,13 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
||||||
* schedulingType:
|
* schedulingType:
|
||||||
* type: string
|
* type: string
|
||||||
* description: The type of scheduling if a Team event. Required for team events only
|
* description: The type of scheduling if a Team event. Required for team events only
|
||||||
* enum: [ROUND_ROBIN, COLLECTIVE]
|
* enum: [ROUND_ROBIN, COLLECTIVE, MANAGED]
|
||||||
* price:
|
* price:
|
||||||
* type: integer
|
* type: integer
|
||||||
* description: Price of the event type booking
|
* description: Price of the event type booking
|
||||||
|
* parentId:
|
||||||
|
* type: integer
|
||||||
|
* description: EventTypeId of the parent managed event
|
||||||
* currency:
|
* currency:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Currency acronym. Eg- usd, eur, gbp, etc.
|
* description: Currency acronym. Eg- usd, eur, gbp, etc.
|
||||||
|
@ -276,6 +281,11 @@ async function postHandler(req: NextApiRequest) {
|
||||||
|
|
||||||
await checkPermissions(req);
|
await checkPermissions(req);
|
||||||
|
|
||||||
|
if (parsedBody.parentId) {
|
||||||
|
await checkParentEventOwnership(parsedBody.parentId, userId);
|
||||||
|
await checkUserMembership(parsedBody.parentId, parsedBody.userId);
|
||||||
|
}
|
||||||
|
|
||||||
if (isAdmin && parsedBody.userId) {
|
if (isAdmin && parsedBody.userId) {
|
||||||
data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } };
|
data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } };
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a user, identified by the provided userId, has ownership (or admin rights) over
|
||||||
|
* the team associated with the event type identified by the parentId.
|
||||||
|
*
|
||||||
|
* @param parentId - The ID of the parent event type.
|
||||||
|
* @param userId - The ID of the user.
|
||||||
|
*
|
||||||
|
* @throws {HttpError} If the parent event type is not found,
|
||||||
|
* if the parent event type doesn't belong to any team,
|
||||||
|
* or if the user doesn't have ownership or admin rights to the associated team.
|
||||||
|
*/
|
||||||
|
export default async function checkParentEventOwnership(parentId: number, userId: number) {
|
||||||
|
const parentEventType = await prisma.eventType.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parentId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
teamId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parentEventType) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: "Parent event type not found.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parentEventType.teamId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "This event type is not capable of having children",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamMember = await prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: parentEventType.teamId,
|
||||||
|
userId: userId,
|
||||||
|
OR: [{ role: "OWNER" }, { role: "ADMIN" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!teamMember) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: "User is not authorized to access the team to which the parent event type belongs.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a user, identified by the provided userId, is a member of the team associated
|
||||||
|
* with the event type identified by the parentId.
|
||||||
|
*
|
||||||
|
* @param parentId - The ID of the event type.
|
||||||
|
* @param userId - The ID of the user.
|
||||||
|
*
|
||||||
|
* @throws {HttpError} If the event type is not found,
|
||||||
|
* if the event type doesn't belong to any team,
|
||||||
|
* or if the user isn't a member of the associated team.
|
||||||
|
*/
|
||||||
|
export default async function checkUserMembership(parentId: number, userId: number) {
|
||||||
|
const parentEventType = await prisma.eventType.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parentId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
teamId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parentEventType) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: "Event type not found.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parentEventType.teamId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "This event type is not capable of having children.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamMember = await prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: parentEventType.teamId,
|
||||||
|
userId: userId,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!teamMember) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "User is not a team member.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue