From bfe5b3de433debd9e771ebad5c19095e1ea77807 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 31 May 2023 18:05:30 +0530 Subject: [PATCH] feat: adds paymentID and allow attendeeEmail filtering in Booking API (#9143) * adds payment id to return in GET bookings * Delete .gitkeep. Unintended * Delete Licenses. Unintended commit * initial working code for attendeeEmails filter * improve code readability * adds multiAttendee validation akin to multi userId * code improvement * adds swagger format email --- apps/api/lib/validations/booking.ts | 12 +- .../validations/shared/queryAttendeeEmail.ts | 20 ++ apps/api/pages/api/bookings/[id]/_get.ts | 63 +++++-- apps/api/pages/api/bookings/_get.ts | 176 +++++++++++++----- apps/api/pages/api/docs.ts | 19 ++ 5 files changed, 222 insertions(+), 68 deletions(-) create mode 100644 apps/api/lib/validations/shared/queryAttendeeEmail.ts diff --git a/apps/api/lib/validations/booking.ts b/apps/api/lib/validations/booking.ts index 53da5396aa..8298f3cce2 100644 --- a/apps/api/lib/validations/booking.ts +++ b/apps/api/lib/validations/booking.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { _BookingModel as Booking, _AttendeeModel, _UserModel } from "@calcom/prisma/zod"; +import { _BookingModel as Booking, _AttendeeModel, _UserModel, _PaymentModel } from "@calcom/prisma/zod"; import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils"; import { schemaQueryUserId } from "./shared/queryUserId"; @@ -49,6 +49,15 @@ export const schemaBookingReadPublic = Booking.extend({ locale: true, }) .optional(), + payment: z + .array( + _PaymentModel.pick({ + id: true, + success: true, + paymentOption: true, + }) + ) + .optional(), }).pick({ id: true, userId: true, @@ -61,6 +70,7 @@ export const schemaBookingReadPublic = Booking.extend({ timeZone: true, attendees: true, user: true, + payment: true, metadata: true, status: true, responses: true, diff --git a/apps/api/lib/validations/shared/queryAttendeeEmail.ts b/apps/api/lib/validations/shared/queryAttendeeEmail.ts new file mode 100644 index 0000000000..d7919bf53f --- /dev/null +++ b/apps/api/lib/validations/shared/queryAttendeeEmail.ts @@ -0,0 +1,20 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +import { baseApiParams } from "./baseApiParams"; + +// Extracted out as utility function so can be reused +// at different endpoints that require this validation. +export const schemaQueryAttendeeEmail = baseApiParams.extend({ + attendeeEmail: z.string().email(), +}); + +export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({ + attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(), +}); + +export const withValidQueryAttendeeEmail = withValidation({ + schema: schemaQueryAttendeeEmail, + type: "Zod", + mode: "query", +}); diff --git a/apps/api/pages/api/bookings/[id]/_get.ts b/apps/api/pages/api/bookings/[id]/_get.ts index 5064ba2d78..c549af8b86 100644 --- a/apps/api/pages/api/bookings/[id]/_get.ts +++ b/apps/api/pages/api/bookings/[id]/_get.ts @@ -32,40 +32,65 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform * content: * application/json: * schema: - * $ref: "#/components/schemas/ArrayOfBookings" + * $ref: "#/components/schemas/Booking" * examples: - * bookings: - * value: [ + * booking: + * value: * { - * "id": 1, - * "description": "Meeting with John", - * "eventTypeId": 2, - * "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8", - * "title": "Business Meeting", - * "startTime": "2023-04-20T10:00:00.000Z", - * "endTime": "2023-04-20T11:00:00.000Z", - * "timeZone": "Europe/London", - * "attendees": [ - * { - * "email": "example@cal.com", - * "name": "John Doe", - * "timeZone": "Europe/London", + * "booking": { + * "id": 91, + * "userId": 5, + * "description": "", + * "eventTypeId": 7, + * "uid": "bFJeNb2uX8ANpT3JL5EfXw", + * "title": "60min between Pro Example and John Doe", + * "startTime": "2023-05-25T09:30:00.000Z", + * "endTime": "2023-05-25T10:30:00.000Z", + * "attendees": [ + * { + * "email": "john.doe@example.com", + * "name": "John Doe", + * "timeZone": "Asia/Kolkata", + * "locale": "en" + * } + * ], + * "user": { + * "email": "pro@example.com", + * "name": "Pro Example", + * "timeZone": "Asia/Kolkata", * "locale": "en" + * }, + * "payment": [ + * { + * "id": 1, + * "success": true, + * "paymentOption": "ON_BOOKING" + * } + * ], + * "metadata": {}, + * "status": "ACCEPTED", + * "responses": { + * "email": "john.doe@example.com", + * "name": "John Doe", + * "location": { + * "optionValue": "", + * "value": "inPerson" + * } * } - * ] + * } * } - * ] * 401: * description: Authorization information is missing or invalid. * 404: * description: Booking was not found */ + export async function getHandler(req: NextApiRequest) { const { prisma, query } = req; const { id } = schemaQueryIdParseInt.parse(query); const booking = await prisma.booking.findUnique({ where: { id }, - include: { attendees: true, user: true }, + include: { attendees: true, user: true, payment: true }, }); return { booking: schemaBookingReadPublic.parse(booking) }; } diff --git a/apps/api/pages/api/bookings/_get.ts b/apps/api/pages/api/bookings/_get.ts index 006b39472e..9166b80ec9 100644 --- a/apps/api/pages/api/bookings/_get.ts +++ b/apps/api/pages/api/bookings/_get.ts @@ -5,6 +5,7 @@ import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import { schemaBookingReadPublic } from "~/lib/validations/booking"; +import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail"; import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; /** @@ -31,6 +32,19 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que * items: * type: integer * example: [2, 3, 4] + * - in: query + * name: attendeeEmails + * required: false + * schema: + * oneOf: + * - type: string + * format: email + * example: john.doe@example.com + * - type: array + * items: + * type: string + * format: email + * example: [john.doe@example.com, jane.doe@example.com] * operationId: listBookings * tags: * - bookings @@ -45,22 +59,47 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que * bookings: * value: [ * { - * "id": 1, - * "description": "Meeting with John", - * "eventTypeId": 2, - * "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8", - * "title": "Business Meeting", - * "startTime": "2023-04-20T10:00:00.000Z", - * "endTime": "2023-04-20T11:00:00.000Z", - * "timeZone": "Europe/London", - * "attendees": [ - * { - * "email": "example@cal.com", - * "name": "John Doe", - * "timeZone": "Europe/London", + * "booking": { + * "id": 91, + * "userId": 5, + * "description": "", + * "eventTypeId": 7, + * "uid": "bFJeNb2uX8ANpT3JL5EfXw", + * "title": "60min between Pro Example and John Doe", + * "startTime": "2023-05-25T09:30:00.000Z", + * "endTime": "2023-05-25T10:30:00.000Z", + * "attendees": [ + * { + * "email": "john.doe@example.com", + * "name": "John Doe", + * "timeZone": "Asia/Kolkata", + * "locale": "en" + * } + * ], + * "user": { + * "email": "pro@example.com", + * "name": "Pro Example", + * "timeZone": "Asia/Kolkata", * "locale": "en" + * }, + * "payment": [ + * { + * "id": 1, + * "success": true, + * "paymentOption": "ON_BOOKING" + * } + * ], + * "metadata": {}, + * "status": "ACCEPTED", + * "responses": { + * "email": "john.doe@example.com", + * "name": "John Doe", + * "location": { + * "optionValue": "", + * "value": "inPerson" + * } * } - * ] + * } * } * ] * 401: @@ -69,26 +108,42 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que * description: No bookings were found */ -async function handler(req: NextApiRequest) { - const { userId, isAdmin, prisma } = req; - const args: Prisma.BookingFindManyArgs = {}; - args.include = { - attendees: true, - user: true, - }; - - /** Only admins can query other users */ - if (isAdmin && req.query.userId) { - const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); - const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; - const users = await prisma.user.findMany({ - where: { id: { in: userIds } }, - select: { email: true }, - }); - const userEmails = users.map((u) => u.email); - args.where = { +/** + * Constructs the WHERE clause for Prisma booking findMany operation. + * + * @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee. + * @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls. + * @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it. + * @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array. + * + * @returns An object that represents the WHERE clause for the findMany/findUnique operation. + */ +function buildWhereClause( + userId: number, + attendeeEmails: string[], + userIds: number[] = [], + userEmails: string[] = [] +) { + const filterByAttendeeEmails = attendeeEmails.length > 0; + const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : { userId }; + let whereClause = {}; + if (filterByAttendeeEmails) { + whereClause = { + AND: [ + userFilter, + { + attendees: { + some: { + email: { in: attendeeEmails }, + }, + }, + }, + ], + }; + } else { + whereClause = { OR: [ - { userId: { in: userIds } }, + userFilter, { attendees: { some: { @@ -98,7 +153,45 @@ async function handler(req: NextApiRequest) { }, ], }; - } else if (!isAdmin) { + } + + return { + ...whereClause, + }; +} + +async function handler(req: NextApiRequest) { + const { userId, isAdmin, prisma } = req; + const args: Prisma.BookingFindManyArgs = {}; + args.include = { + attendees: true, + user: true, + payment: true, + }; + + const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query); + const attendeeEmails = Array.isArray(queryFilterForAttendeeEmails.attendeeEmail) + ? queryFilterForAttendeeEmails.attendeeEmail + : typeof queryFilterForAttendeeEmails.attendeeEmail === "string" + ? [queryFilterForAttendeeEmails.attendeeEmail] + : []; + const filterByAttendeeEmails = attendeeEmails.length > 0; + + /** Only admins can query other users */ + if (isAdmin) { + if (req.query.userId) { + const query = schemaQuerySingleOrMultipleUserIds.parse(req.query); + const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId]; + const users = await prisma.user.findMany({ + where: { id: { in: userIds } }, + select: { email: true }, + }); + const userEmails = users.map((u) => u.email); + args.where = buildWhereClause(userId, attendeeEmails, userIds, userEmails); + } else if (filterByAttendeeEmails) { + args.where = buildWhereClause(userId, attendeeEmails, [], []); + } + } else { const user = await prisma.user.findUnique({ where: { id: userId }, select: { @@ -108,20 +201,7 @@ async function handler(req: NextApiRequest) { if (!user) { throw new HttpError({ message: "User not found", statusCode: 500 }); } - args.where = { - OR: [ - { - userId, - }, - { - attendees: { - some: { - email: user.email, - }, - }, - }, - ], - }; + args.where = buildWhereClause(userId, attendeeEmails, [], []); } const data = await prisma.booking.findMany(args); return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) }; diff --git a/apps/api/pages/api/docs.ts b/apps/api/pages/api/docs.ts index 1773d87457..fc320d011b 100644 --- a/apps/api/pages/api/docs.ts +++ b/apps/api/pages/api/docs.ts @@ -98,6 +98,25 @@ const swaggerHandler = withSwagger({ }, }, }, + payment: { + type: Array, + items: { + properties: { + id: { + type: "number", + example: 1, + }, + success: { + type: "boolean", + example: true, + }, + paymentOption: { + type: "string", + example: "ON_BOOKING", + }, + }, + }, + }, }, }, },