Compare commits

...

1 Commits

Author SHA1 Message Date
Syed Ali Shahbaz 6cdb3f4a82
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
2023-05-31 12:35:30 +00:00
5 changed files with 222 additions and 68 deletions

View File

@ -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,

View File

@ -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",
});

View File

@ -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) };
}

View File

@ -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)) };

View File

@ -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",
},
},
},
},
},
},
},