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
This commit is contained in:
parent
db5dbdea40
commit
bfe5b3de43
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
});
|
|
@ -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) };
|
||||
}
|
||||
|
|
|
@ -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)) };
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue
Block a user