diff --git a/apps/api/v1/lib/helpers/captureErrors.ts b/apps/api/v1/lib/helpers/captureErrors.ts index 654750c074..216276f3fd 100644 --- a/apps/api/v1/lib/helpers/captureErrors.ts +++ b/apps/api/v1/lib/helpers/captureErrors.ts @@ -1,20 +1,14 @@ import { captureException as SentryCaptureException } from "@sentry/nextjs"; import type { NextMiddleware } from "next-api-middleware"; -import { redactError } from "@calcom/lib/redactError"; - export const captureErrors: NextMiddleware = async (_req, res, next) => { try { // Catch any errors that are thrown in remaining // middleware and the API route handler await next(); } catch (error) { + console.error(error); SentryCaptureException(error); - const redactedError = redactError(error); - if (redactedError instanceof Error) { - res.status(400).json({ message: redactedError.message, error: redactedError }); - return; - } - res.status(400).json({ message: "Something went wrong", error }); + res.status(500).json({ message: "Something went wrong" }); } }; diff --git a/apps/api/v1/lib/selects/event-type.ts b/apps/api/v1/lib/selects/event-type.ts new file mode 100644 index 0000000000..7339833144 --- /dev/null +++ b/apps/api/v1/lib/selects/event-type.ts @@ -0,0 +1,68 @@ +import type { Prisma } from "@calcom/prisma/client"; + +export const eventTypeSelect = { + id: true, + title: true, + slug: true, + length: true, + hidden: true, + position: true, + userId: true, + teamId: true, + scheduleId: true, + eventName: true, + timeZone: true, + periodType: true, + periodStartDate: true, + periodEndDate: true, + periodDays: true, + periodCountCalendarDays: true, + requiresConfirmation: true, + recurringEvent: true, + disableGuests: true, + hideCalendarNotes: true, + minimumBookingNotice: true, + beforeEventBuffer: true, + afterEventBuffer: true, + schedulingType: true, + price: true, + currency: true, + slotInterval: true, + parentId: true, + successRedirectUrl: true, + description: true, + locations: true, + metadata: true, + seatsPerTimeSlot: true, + seatsShowAttendees: true, + seatsShowAvailabilityCount: true, + bookingFields: true, + bookingLimits: true, + onlyShowFirstAvailableSlot: true, + durationLimits: true, + customInputs: { + select: { id: true, label: true, required: true, options: true, type: true, placeholder: true }, + }, + hashedLink: { + select: { link: true }, + }, + team: { + select: { slug: true }, + }, + hosts: { + select: { + userId: true, + isFixed: true, + scheduleId: true, + }, + }, + owner: { + select: { username: true, id: true }, + }, + children: { + select: { + id: true, + userId: true, + }, + }, +} satisfies Prisma.EventTypeSelect; diff --git a/apps/api/v1/lib/validations/event-type.ts b/apps/api/v1/lib/validations/event-type.ts index ec40237fb4..798ad2c442 100644 --- a/apps/api/v1/lib/validations/event-type.ts +++ b/apps/api/v1/lib/validations/event-type.ts @@ -6,13 +6,12 @@ import { MIN_EVENT_DURATION_MINUTES, } from "@calcom/lib/constants"; import slugify from "@calcom/lib/slugify"; -import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils"; +import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; import { EventTypeSchema } from "@calcom/prisma/zod/modelSchema/EventTypeSchema"; import { HostSchema } from "@calcom/prisma/zod/modelSchema/HostSchema"; import { Frequency } from "~/lib/types"; -import { jsonSchema } from "./shared/jsonSchema"; import { schemaQueryUserId } from "./shared/queryUserId"; import { timeZone } from "./shared/timeZone"; @@ -118,67 +117,3 @@ const schemaEventTypeEditParams = z .strict(); export const schemaEventTypeEditBodyParams = schemaEventTypeBaseBodyParams.merge(schemaEventTypeEditParams); -export const schemaEventTypeReadPublic = EventTypeSchema.pick({ - id: true, - title: true, - slug: true, - length: true, - hidden: true, - position: true, - userId: true, - teamId: true, - scheduleId: true, - eventName: true, - timeZone: true, - periodType: true, - periodStartDate: true, - periodEndDate: true, - periodDays: true, - periodCountCalendarDays: true, - requiresConfirmation: true, - recurringEvent: true, - disableGuests: true, - hideCalendarNotes: true, - minimumBookingNotice: true, - beforeEventBuffer: true, - afterEventBuffer: true, - schedulingType: true, - price: true, - currency: true, - slotInterval: true, - parentId: true, - successRedirectUrl: true, - description: true, - locations: true, - metadata: true, - seatsPerTimeSlot: true, - seatsShowAttendees: true, - seatsShowAvailabilityCount: true, - bookingFields: true, - bookingLimits: true, - onlyShowFirstAvailableSlot: true, - durationLimits: true, -}).merge( - z.object({ - children: z.array(childrenSchema).optional().default([]), - hosts: z.array(hostSchema).optional().default([]), - locations: z - .array( - z.object({ - link: z.string().optional(), - address: z.string().optional(), - hostPhoneNumber: z.string().optional(), - type: z.any().optional(), - }) - ) - .nullable(), - metadata: jsonSchema.nullable(), - customInputs: customInputSchema.array().optional(), - link: z.string().optional(), - hashedLink: z - .array(z.object({ link: z.string() })) - .optional() - .default([]), - bookingFields: eventTypeBookingFields.optional().nullable(), - }) -); diff --git a/apps/api/v1/pages/api/event-types/[id]/_delete.ts b/apps/api/v1/pages/api/event-types/[id]/_delete.ts index 0242d666ba..f6af2d58fa 100644 --- a/apps/api/v1/pages/api/event-types/[id]/_delete.ts +++ b/apps/api/v1/pages/api/event-types/[id]/_delete.ts @@ -1,8 +1,7 @@ import type { NextApiRequest } from "next"; import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; +import { prisma } from "@calcom/prisma"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; @@ -66,4 +65,4 @@ async function checkPermissions(req: NextApiRequest) { throw new HttpError({ statusCode: 403, message: "Forbidden" }); } -export default defaultResponder(deleteHandler); +export default deleteHandler; diff --git a/apps/api/v1/pages/api/event-types/[id]/_get.ts b/apps/api/v1/pages/api/event-types/[id]/_get.ts index 123ccbd742..e80ac95e49 100644 --- a/apps/api/v1/pages/api/event-types/[id]/_get.ts +++ b/apps/api/v1/pages/api/event-types/[id]/_get.ts @@ -1,11 +1,10 @@ import type { NextApiRequest } from "next"; import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; +import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; -import { schemaEventTypeReadPublic } from "~/lib/validations/event-type"; +import { eventTypeSelect } from "~/lib/selects/event-type"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware"; @@ -49,15 +48,13 @@ export async function getHandler(req: NextApiRequest) { const eventType = await prisma.eventType.findUnique({ where: { id }, - include: { - customInputs: true, - hashedLink: { select: { link: true } }, - team: { select: { slug: true } }, - hosts: { select: { userId: true, isFixed: true } }, - owner: { select: { username: true, id: true } }, - children: { select: { id: true, userId: true } }, - }, + select: eventTypeSelect, }); + + if (!eventType) { + throw new HttpError({ statusCode: 404, message: "Event type not found" }); + } + await checkPermissions(req, eventType); const link = eventType ? getCalLink(eventType) : null; @@ -74,9 +71,12 @@ export async function getHandler(req: NextApiRequest) { eventType.scheduleId = user.defaultScheduleId; } - // TODO: eventType when not found should be a 404 - // but API consumers may depend on the {} behaviour. - return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) }; + return { + event_type: { + ...eventType, + link, + }, + }; } type BaseEventTypeCheckPermissions = { @@ -100,4 +100,4 @@ async function checkPermissions( throw new HttpError({ statusCode: 403, message: "Forbidden" }); } -export default defaultResponder(getHandler); +export default getHandler; diff --git a/apps/api/v1/pages/api/event-types/[id]/_patch.ts b/apps/api/v1/pages/api/event-types/[id]/_patch.ts index 3dcff2f4e8..5755855df9 100644 --- a/apps/api/v1/pages/api/event-types/[id]/_patch.ts +++ b/apps/api/v1/pages/api/event-types/[id]/_patch.ts @@ -2,13 +2,13 @@ import type { NextApiRequest } from "next"; import type { z } from "zod"; import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; +import { prisma } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; +import { eventTypeSelect } from "~/lib/selects/event-type"; import type { schemaEventTypeBaseBodyParams } from "~/lib/validations/event-type"; -import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type"; +import { schemaEventTypeEditBodyParams } from "~/lib/validations/event-type"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; import ensureOnlyMembersAsHosts from "~/pages/api/event-types/_utils/ensureOnlyMembersAsHosts"; @@ -235,8 +235,8 @@ export async function patchHandler(req: NextApiRequest) { }; } await checkPermissions(req, parsedBody); - const eventType = await prisma.eventType.update({ where: { id }, data }); - return { event_type: schemaEventTypeReadPublic.parse(eventType) }; + const eventType = await prisma.eventType.update({ where: { id }, data, select: eventTypeSelect }); + return { event_type: eventType }; } async function checkPermissions(req: NextApiRequest, body: z.infer) { @@ -249,4 +249,4 @@ async function checkPermissions(req: NextApiRequest, body: z.infer({ eventTypes: data, prisma, userIds })).map( (eventType) => { const link = getCalLink(eventType); - return schemaEventTypeReadPublic.parse({ ...eventType, link }); + return { ...eventType, link }; } ), }; diff --git a/apps/api/v1/pages/api/event-types/_post.ts b/apps/api/v1/pages/api/event-types/_post.ts index 15e1aecc72..02a3c4097d 100644 --- a/apps/api/v1/pages/api/event-types/_post.ts +++ b/apps/api/v1/pages/api/event-types/_post.ts @@ -2,11 +2,12 @@ import type { NextApiRequest } from "next"; import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; +import { prisma } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; -import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type"; +import { eventTypeSelect } from "~/lib/selects/event-type"; +import { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type"; import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware"; import checkParentEventOwnership from "./_utils/checkParentEventOwnership"; @@ -306,10 +307,10 @@ async function postHandler(req: NextApiRequest) { data.hosts = { createMany: { data: hosts } }; } - const eventType = await prisma.eventType.create({ data, include: { hosts: true } }); + const eventType = await prisma.eventType.create({ data, select: eventTypeSelect }); return { - event_type: schemaEventTypeReadPublic.parse(eventType), + event_type: eventType, message: "Event type created successfully", }; } diff --git a/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts b/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts index 24312019bd..637af631d6 100644 --- a/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts +++ b/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts @@ -2,10 +2,10 @@ import type { NextApiRequest } from "next"; import { z } from "zod"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; +import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; -import { schemaEventTypeReadPublic } from "~/lib/validations/event-type"; +import { eventTypeSelect } from "~/lib/selects/event-type"; const querySchema = z.object({ teamId: z.coerce.number(), @@ -57,17 +57,11 @@ async function getHandler(req: NextApiRequest) { members: { some: { userId } }, }, }, - include: { - customInputs: true, - team: { select: { slug: true } }, - hosts: { select: { userId: true, isFixed: true } }, - owner: { select: { username: true, id: true } }, - children: { select: { id: true, userId: true } }, - }, + select: eventTypeSelect, + }; + return { + event_types: await prisma.eventType.findMany(args), }; - - const data = await prisma.eventType.findMany(args); - return { event_types: data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)) }; } export default defaultResponder(getHandler); diff --git a/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts b/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts index 863317a2d5..2e891b0973 100644 --- a/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts +++ b/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts @@ -60,7 +60,7 @@ describe("DELETE /api/event-types/[id]", () => { describe("Error", async () => { test("Fails to remove event type if user is not OWNER/ADMIN of team associated with event type", async () => { - const { req, res } = createMocks({ + const { req } = createMocks({ method: "DELETE", body: {}, query: { @@ -71,15 +71,18 @@ describe("DELETE /api/event-types/[id]", () => { // Assign userId to the request objects req.userId = memberUser; - await handler(req, res); - expect(res.statusCode).toBe(403); // Check if the deletion was successful + await expect(handler(req)).rejects.toThrowError( + expect.objectContaining({ + statusCode: 403, + }) + ); }); }); describe("Success", async () => { test("Removes event type if user is owner of team associated with event type", async () => { // Mocks for DELETE request - const { req, res } = createMocks({ + const { req } = createMocks({ method: "DELETE", body: {}, query: { @@ -90,8 +93,8 @@ describe("DELETE /api/event-types/[id]", () => { // Assign userId to the request objects req.userId = adminUser; - await handler(req, res); - expect(res.statusCode).toBe(200); // Check if the deletion was successful + const deletedEventType = await handler(req); + expect(deletedEventType).not.toBeNull(); }); }); }); diff --git a/apps/api/v1/test/lib/event-types/[id]/_get.test.ts b/apps/api/v1/test/lib/event-types/[id]/_get.test.ts index 8f1db30a3a..e1307b9722 100644 --- a/apps/api/v1/test/lib/event-types/[id]/_get.test.ts +++ b/apps/api/v1/test/lib/event-types/[id]/_get.test.ts @@ -16,7 +16,7 @@ type CustomNextApiResponse = NextApiResponse & Response; describe("GET /api/event-types/[id]", () => { describe("Errors", () => { test("Returns 403 if user not admin/team member/event owner", async () => { - const { req, res } = createMocks({ + const { req } = createMocks({ method: "GET", body: {}, query: { @@ -32,16 +32,40 @@ describe("GET /api/event-types/[id]", () => { ); req.userId = 333333; - await handler(req, res); - expect(res.statusCode).toBe(403); + await expect(handler(req)).rejects.toThrowError( + expect.objectContaining({ + statusCode: 403, + }) + ); + }); + test("Returns 404 if event type not found", async () => { + const { req } = createMocks({ + method: "GET", + body: {}, + query: { + id: 123456, + }, + }); + + req.isSystemWideAdmin = true; + + prismaMock.eventType.findUnique.mockResolvedValue(null); + + req.userId = 333333; + + await expect(handler(req)).rejects.toThrowError( + expect.objectContaining({ + statusCode: 404, + }) + ); }); }); describe("Success", async () => { test("Returns event type if user is admin", async () => { const eventTypeId = 123456; - const { req, res } = createMocks({ + const { req } = createMocks({ method: "GET", body: {}, query: { @@ -57,17 +81,17 @@ describe("GET /api/event-types/[id]", () => { req.isSystemWideAdmin = true; req.userId = 333333; - await handler(req, res); - expect(res.statusCode).toBe(200); - expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId); + const responseData = await handler(req); + + expect(responseData.event_type.id).toEqual(eventTypeId); }); test("Returns event type if user is in team associated with event type", async () => { const eventTypeId = 123456; const teamId = 9999; const userId = 333333; - const { req, res } = createMocks({ + const { req } = createMocks({ method: "GET", body: {}, query: { @@ -94,10 +118,10 @@ describe("GET /api/event-types/[id]", () => { req.isSystemWideAdmin = false; req.userId = userId; - await handler(req, res); - expect(res.statusCode).toBe(200); - expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId); + const responseData = await handler(req); + + expect(responseData.event_type.id).toEqual(eventTypeId); expect(prismaMock.team.findFirst).toHaveBeenCalledWith({ where: { id: teamId, @@ -116,7 +140,7 @@ describe("GET /api/event-types/[id]", () => { test("Returns event type if user is the event type owner", async () => { const eventTypeId = 123456; const userId = 333333; - const { req, res } = createMocks({ + const { req } = createMocks({ method: "GET", body: {}, query: { @@ -134,10 +158,10 @@ describe("GET /api/event-types/[id]", () => { req.isSystemWideAdmin = false; req.userId = userId; - await handler(req, res); - expect(res.statusCode).toBe(200); - expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId); + const responseData = await handler(req); + + expect(responseData.event_type.id).toEqual(eventTypeId); expect(prismaMock.team.findFirst).not.toHaveBeenCalled(); }); }); diff --git a/packages/lib/server/defaultHandler.test.ts b/packages/lib/server/defaultHandler.test.ts index b15d62bd42..8c79d48a25 100644 --- a/packages/lib/server/defaultHandler.test.ts +++ b/packages/lib/server/defaultHandler.test.ts @@ -39,23 +39,4 @@ describe("defaultHandler Test Suite", () => { expect(getHandler).toHaveBeenCalledWith(req, res); }); - - it("should return 500 for errors thrown in handler", async () => { - const getHandler = vi.fn().mockRejectedValue(new Error("Test Error")); - const handlers = { - GET: { default: getHandler }, - }; - const handler = defaultHandler(handlers); - - const { req, res } = createMocks({ - method: "GET", - }); - - await handler(req, res); - - expect(res._getStatusCode()).toBe(500); - expect(res._getJSONData()).toEqual({ - message: "Something went wrong", - }); - }); }); diff --git a/packages/lib/server/defaultHandler.ts b/packages/lib/server/defaultHandler.ts index 4f64be2d1f..251dd646d4 100644 --- a/packages/lib/server/defaultHandler.ts +++ b/packages/lib/server/defaultHandler.ts @@ -13,12 +13,5 @@ export const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest .status(405) .json({ message: `Method Not Allowed (Allow: ${Object.keys(handlers).join(",")})` }); } - - try { - await handler(req, res); - return; - } catch (error) { - console.error(error); - return res.status(500).json({ message: "Something went wrong" }); - } + return await handler(req, res); };