Refactor schedule endpoints (#185)

pull/9078/head
Omar López 2022-10-13 14:54:38 -06:00 committed by GitHub
parent 31610dd178
commit e3fa0e546b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 201 additions and 244 deletions

View File

@ -3,21 +3,16 @@ import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { _ScheduleModel as Schedule, _AvailabilityModel as Availability } from "@calcom/prisma/zod";
const schemaScheduleBaseBodyParams = Schedule.omit({ id: true }).partial();
import { timeZone } from "./shared/timeZone";
const schemaScheduleRequiredParams = z.object({
name: z.string().optional(),
userId: z.union([z.number(), z.array(z.number())]).optional(),
});
export const schemaScheduleBodyParams = schemaScheduleBaseBodyParams.merge(schemaScheduleRequiredParams);
const schemaScheduleBaseBodyParams = Schedule.omit({ id: true, timeZone: true }).partial();
export const schemaSingleScheduleBodyParams = schemaScheduleBaseBodyParams.merge(
z.object({ userId: z.number().optional() })
z.object({ userId: z.number().optional(), timeZone: timeZone.optional() })
);
export const schemaCreateScheduleBodyParams = schemaScheduleBaseBodyParams.merge(
z.object({ userId: z.number().optional(), name: z.string() })
z.object({ userId: z.number().optional(), name: z.string(), timeZone })
);
export const schemaSchedulePublic = z

View File

@ -2,4 +2,6 @@ import tzdata from "tzdata";
import * as z from "zod";
// @note: This is a custom validation that checks if the timezone is valid and exists in the tzdb library
export const timeZone = z.string().refine((tz: string) => Object.keys(tzdata.zones).includes(tz));
export const timeZone = z.string().refine((tz: string) => Object.keys(tzdata.zones).includes(tz), {
message: `Expected one of the following: ${Object.keys(tzdata.zones).join(", ")}`,
});

View File

@ -0,0 +1,19 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt";
async function authMiddleware(req: NextApiRequest) {
const { userId, isAdmin, prisma } = req;
const { id } = schemaQueryIdParseInt.parse(req.query);
// Admins can just skip this check
if (isAdmin) return;
// Check if the current user can access the schedule
const schedule = await prisma.schedule.findFirst({
where: { id, userId },
});
if (!schedule) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
}
export default authMiddleware;

View File

@ -0,0 +1,41 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt";
/**
* @swagger
* /schedules/{id}:
* delete:
* operationId: removeScheduleById
* summary: Remove an existing schedule
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID of the schedule to delete
* tags:
* - schedules
* responses:
* 201:
* description: OK, schedule removed successfully
* 400:
* description: Bad request. Schedule id is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
export async function deleteHandler(req: NextApiRequest) {
const { prisma, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
/* If we're deleting any default user schedule, we unset it */
await prisma.user.updateMany({ where: { defaultScheduleId: id }, data: { defaultScheduleId: undefined } });
await prisma.schedule.delete({ where: { id } });
return { message: `Schedule with id: ${id} deleted successfully` };
}
export default defaultResponder(deleteHandler);

View File

@ -0,0 +1,38 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { schemaSchedulePublic } from "@lib/validations/schedule";
import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt";
/**
* @swagger
* /schedules/{id}:
* get:
* operationId: getScheduleById
* summary: Find a schedule
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID of the schedule to get
* tags:
* - schedules
* responses:
* 200:
* description: OK
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: Schedule was not found
*/
export async function getHandler(req: NextApiRequest) {
const { prisma, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const data = await prisma.schedule.findUniqueOrThrow({ where: { id }, include: { availability: true } });
return { schedule: schemaSchedulePublic.parse(data) };
}
export default defaultResponder(getHandler);

View File

@ -0,0 +1,39 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { schemaSchedulePublic, schemaSingleScheduleBodyParams } from "@lib/validations/schedule";
import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformParseInt";
/**
* @swagger
* /schedules/{id}:
* patch:
* operationId: editScheduleById
* summary: Edit an existing schedule
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID of the schedule to edit
* tags:
* - schedules
* responses:
* 201:
* description: OK, schedule edited successfully
* 400:
* description: Bad request. Schedule body is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
export async function patchHandler(req: NextApiRequest) {
const { prisma, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const data = schemaSingleScheduleBodyParams.parse(req.body);
const result = await prisma.schedule.update({ where: { id }, data, include: { availability: true } });
return { schedule: schemaSchedulePublic.parse(result) };
}
export default defaultResponder(patchHandler);

View File

@ -1,188 +1,18 @@
import type { NextApiRequest, NextApiResponse } from "next";
import safeParseJSON from "@lib/helpers/safeParseJSON";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import { withMiddleware } from "@lib/helpers/withMiddleware";
import type { ScheduleResponse } from "@lib/types";
import { schemaSingleScheduleBodyParams, schemaSchedulePublic } from "@lib/validations/schedule";
import {
schemaQueryIdParseInt,
withValidQueryIdTransformParseInt,
} from "@lib/validations/shared/queryIdTransformParseInt";
export async function scheduleById(
{ method, query, body, userId, isAdmin, prisma }: NextApiRequest,
res: NextApiResponse<ScheduleResponse>
) {
body = safeParseJSON(body);
if (!body.success) {
res.status(400).json({ message: body.message });
return;
}
import authMiddleware from "./_auth-middleware";
const safeBody = schemaSingleScheduleBodyParams.safeParse(body);
if (!safeBody.success) {
res.status(400).json({ message: "Bad request" });
return;
}
const safeQuery = schemaQueryIdParseInt.safeParse(query);
if (safeBody.data.userId && !isAdmin) {
res.status(401).json({ message: "Unauthorized" });
return;
}
if (!safeQuery.success) {
res.status(400).json({ message: "Your query was invalid" });
return;
}
const userSchedules = await prisma.schedule.findMany({ where: { userId: safeBody.data.userId || userId } });
const userScheduleIds = userSchedules.map((schedule) => schedule.id);
if (!userScheduleIds.includes(safeQuery.data.id)) {
res.status(401).json({ message: "Unauthorized" });
return;
} else {
switch (method) {
/**
* @swagger
* /schedules/{id}:
* get:
* operationId: getScheduleById
* summary: Find a schedule
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID of the schedule to get
* tags:
* - schedules
* responses:
* 200:
* description: OK
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: Schedule was not found
*/
case "GET":
await prisma.schedule
.findUnique({
where: { id: safeQuery.data.id },
include: { availability: true },
})
.then((data) => schemaSchedulePublic.parse(data))
.then((schedule) => res.status(200).json({ schedule }))
.catch((error: Error) =>
res.status(404).json({
message: `Schedule with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
/**
* @swagger
* /schedules/{id}:
* patch:
* operationId: editScheduleById
* summary: Edit an existing schedule
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID of the schedule to edit
* tags:
* - schedules
* responses:
* 201:
* description: OK, schedule edited successfuly
* 400:
* description: Bad request. Schedule body is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
case "PATCH":
if (!safeBody.success) {
{
res.status(400).json({ message: "Invalid request body" });
return;
}
}
delete safeBody.data.userId;
await prisma.schedule
.update({ where: { id: safeQuery.data.id }, data: safeBody.data })
.then((data) => schemaSchedulePublic.parse(data))
.then((schedule) => res.status(200).json({ schedule }))
.catch((error: Error) =>
res.status(404).json({
message: `Schedule with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
/**
* @swagger
* /schedules/{id}:
* delete:
* operationId: removeScheduleById
* summary: Remove an existing schedule
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID of the schedule to delete
* tags:
* - schedules
* responses:
* 201:
* description: OK, schedule removed successfuly
* 400:
* description: Bad request. Schedule id is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
case "DELETE":
// Look for user to check if schedule is user's default
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) throw new Error("User not found");
if (user.defaultScheduleId === safeQuery.data.id) {
// unset default
await prisma.user.update({
where: {
id: userId,
},
data: {
defaultScheduleId: undefined,
},
});
}
await prisma.schedule
.delete({ where: { id: safeQuery.data.id } })
.then(() =>
res.status(200).json({
message: `Schedule with id: ${safeQuery.data.id} deleted successfully`,
})
)
.catch((error: Error) =>
res.status(404).json({
message: `Schedule with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
default:
res.status(405).json({ message: "Method not allowed" });
break;
}
}
}
export default withMiddleware("HTTP_GET_DELETE_PATCH")(withValidQueryIdTransformParseInt(scheduleById));
export default withMiddleware("HTTP_GET_DELETE_PATCH")(
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
await authMiddleware(req);
return defaultHandler({
GET: import("./_get"),
PATCH: import("./_patch"),
DELETE: import("./_delete"),
})(req, res);
})
);

View File

@ -1,3 +1,4 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { z } from "zod";
@ -5,6 +6,7 @@ import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { schemaSchedulePublic } from "@lib/validations/schedule";
import { schemaQuerySingleOrMultipleUserIds } from "@lib/validations/shared/queryUserId";
export const schemaUserIds = z
.union([z.string(), z.array(z.string())])
@ -26,34 +28,25 @@ export const schemaUserIds = z
* 404:
* description: No schedules were found
*/
async function handler({ body, prisma, userId, isAdmin, query }: NextApiRequest) {
let userIds: number[] = [userId];
async function handler(req: NextApiRequest) {
const { prisma, userId, isAdmin } = req;
const args: Prisma.ScheduleFindManyArgs = isAdmin ? {} : { where: { userId } };
args.include = { availability: true };
if (!isAdmin && query.userId) {
// throw 403 Forbidden when the userId is given but user is not an admin
throw new HttpError({ statusCode: 403 });
}
// When isAdmin && userId is given parse it and use it instead of the current (admin) user.
else if (query.userId) {
const result = schemaUserIds.safeParse(query.userId);
if (result.success && result.data) {
userIds = result.data;
}
}
if (!isAdmin && req.query.userId)
throw new HttpError({
statusCode: 401,
message: "Unauthorized: Only admins can query other users",
});
const data = await prisma.schedule.findMany({
where: {
userId: { in: userIds },
},
include: { availability: true },
...(Array.isArray(body.userId) && { orderBy: { userId: "asc" } }),
});
const schedules = data.map((schedule) => schemaSchedulePublic.parse(schedule));
if (schedules) {
return { schedules };
if (isAdmin && req.query.userId) {
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
args.where = { userId: { in: userIds } };
if (Array.isArray(query.userId)) args.orderBy = { userId: "asc" };
}
throw new HttpError({ statusCode: 404, message: "No schedules were found" });
const data = await prisma.schedule.findMany(args);
return { schedules: data.map((s) => schemaSchedulePublic.parse(s)) };
}
export default defaultResponder(handler);

View File

@ -1,7 +1,8 @@
import { HttpError } from "@/../../packages/lib/http-error";
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { schemaCreateScheduleBodyParams, schemaSchedulePublic } from "@lib/validations/schedule";
@ -22,36 +23,35 @@ import { schemaCreateScheduleBodyParams, schemaSchedulePublic } from "@lib/valid
* 401:
* description: Authorization information is missing or invalid.
*/
async function postHandler({ body, userId, isAdmin, prisma }: NextApiRequest) {
const parsedBody = schemaCreateScheduleBodyParams.parse(body);
if (parsedBody.userId && !isAdmin) {
throw new HttpError({ statusCode: 403 });
}
async function postHandler(req: NextApiRequest) {
const { userId, isAdmin, prisma } = req;
const body = schemaCreateScheduleBodyParams.parse(req.body);
let args: Prisma.ScheduleCreateArgs = { data: { ...body, userId } };
const data = await prisma.schedule.create({
data: {
...parsedBody,
userId: parsedBody.userId || userId,
availability: {
createMany: {
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE).map((schedule) => ({
days: schedule.days,
startTime: schedule.startTime,
endTime: schedule.endTime,
})),
},
},
/* If ADMIN we create the schedule for selected user */
if (isAdmin && body.userId) args = { data: { ...body, userId: body.userId } };
if (!isAdmin && body.userId)
throw new HttpError({ statusCode: 403, message: "ADMIN required for `userId`" });
// We create default availabilities for the schedule
args.data.availability = {
createMany: {
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE).map((schedule) => ({
days: schedule.days,
startTime: schedule.startTime,
endTime: schedule.endTime,
})),
},
});
};
// We include the recently created availability
args.include = { availability: true };
const createSchedule = schemaSchedulePublic.safeParse(data);
if (!createSchedule.success) {
throw new HttpError({ statusCode: 400, message: "Could not create new schedule" });
}
const data = await prisma.schedule.create(args);
return {
schedule: createSchedule.data,
message: "Schedule created succesfully",
schedule: schemaSchedulePublic.parse(data),
message: "Schedule created successfully",
};
}