diff --git a/__tests__/event-types.test.ts b/__tests__/event-types/[id]/index.test.ts similarity index 83% rename from __tests__/event-types.test.ts rename to __tests__/event-types/[id]/index.test.ts index 2a76abd3cd..4c3cfc655b 100644 --- a/__tests__/event-types.test.ts +++ b/__tests__/event-types/[id]/index.test.ts @@ -2,14 +2,14 @@ import { createMocks } from "node-mocks-http"; import prisma from "@calcom/prisma"; -import handleEvent from "../pages/api/event-types/[id]"; +import handleEvent from "../../../pages/api/event-types/[id]"; afterAll((done) => { prisma.$disconnect().then(); done(); }); -describe("/api/event-types/[id] with valid id as string returns an event-type", () => { +describe("GET /api/event-types/[id] with valid id as string returns an event-type", () => { it("returns a message with the specified events", async () => { const { req, res } = createMocks({ method: "GET", @@ -27,7 +27,7 @@ describe("/api/event-types/[id] with valid id as string returns an event-type", // This can never happen under our normal nextjs setup where query is always a string | string[]. // But seemed a good example for testing an error validation -describe("/api/event-types/[id] errors if query id is number, requires a string", () => { +describe("GET /api/event-types/[id] errors if query id is number, requires a string", () => { it("returns a message with the specified events", async () => { const { req, res } = createMocks({ method: "GET", @@ -50,7 +50,7 @@ describe("/api/event-types/[id] errors if query id is number, requires a string" }); }); -describe("/api/event-types/[id] an id not present in db like 0, throws 404 not found", () => { +describe("GET /api/event-types/[id] an id not present in db like 0, throws 404 not found", () => { it("returns a message with the specified events", async () => { const { req, res } = createMocks({ method: "GET", @@ -65,7 +65,7 @@ describe("/api/event-types/[id] an id not present in db like 0, throws 404 not f }); }); -describe("/api/event-types/[id] only allow GET, fails with POST", () => { +describe("POST /api/event-types/[id] fails, only GET allowed", () => { it("returns a message with the specified events", async () => { const { req, res } = createMocks({ method: "POST", // This POST method is not allowed diff --git a/lib/validations/queryIdTransformParseInt.ts b/lib/validations/queryIdTransformParseInt.ts new file mode 100644 index 0000000000..e90f1735ce --- /dev/null +++ b/lib/validations/queryIdTransformParseInt.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +// Extracted out as utility function so can be reused +// at different endpoints that require this validation. +const schema = z + .object({ + // since nextjs parses query params as strings, + // we need to cast them to numbers using z.transform() and parseInt() + id: z + .string() + .regex(/^\d+$/) + .transform((id) => parseInt(id)), + }) + .strict(); +export default schema; diff --git a/pages/api/event-types/[id]/delete.ts b/pages/api/event-types/[id]/delete.ts new file mode 100644 index 0000000000..038a23d734 --- /dev/null +++ b/pages/api/event-types/[id]/delete.ts @@ -0,0 +1,42 @@ +import { PrismaClient, EventType } from "@prisma/client"; +import schema from "lib/validations/queryIdTransformParseInt"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { withValidation } from "next-validations"; + +const prisma = new PrismaClient(); + +type ResponseData = { + message?: string; + error?: any; +}; + +export async function eventType(req: NextApiRequest, res: NextApiResponse) { + const { query, method } = req; + const safe = await schema.safeParse(query); + if (safe.success) { + if (method === "DELETE") { + // DELETE WILL DELETE THE EVENT TYPE + prisma.eventType + .delete({ where: { id: safe.data.id } }) + .then(() => { + // We only remove the event type from the database if there's an existing resource. + res.status(200).json({ message: `event-type with id: ${safe.data.id} deleted successfully` }); + }) + .catch((error) => { + // This catches the error thrown by prisma.eventType.delete() if the resource is not found. + res.status(400).json({ error: error }); + }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ error: "Only DELETE Method allowed in /event-types/[id]/delete endpoint" }); + } + } +} + +const validate = withValidation({ + schema, + type: "Zod", + mode: "query", +}); + +export default validate(eventType); diff --git a/pages/api/event-types/[id]/edit.ts b/pages/api/event-types/[id]/edit.ts new file mode 100644 index 0000000000..34c55b296b --- /dev/null +++ b/pages/api/event-types/[id]/edit.ts @@ -0,0 +1,49 @@ +import { PrismaClient, EventType } from "@prisma/client"; +import schemaQuery from "lib/validations/queryIdTransformParseInt"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const prisma = new PrismaClient(); + +const schema = z + .object({ + title: z.string().min(3), + slug: z.string().min(3), + length: z.number().min(1).max(1440), // max is a full day. + description: z.string().min(3).optional(), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields + +type ResponseData = { + data?: EventType; + error?: any; +}; + +export async function editEventType(req: NextApiRequest, res: NextApiResponse) { + const { query, body, method } = req; + const safeQuery = await schemaQuery.safeParse(query); + const safeBody = await schema.safeParse(body); + + if (safeQuery.success && safeBody.success) { + if (method === "PATCH") { + const event = await prisma.eventType.update({ + where: { id: safeQuery.data.id }, + data: { ...safeBody.data }, + }); + if (event) res.status(200).json({ data: event }); + if (!event) res.status(404).json({ error: "Event type not found" }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ error: "Only GET Method allowed" }); + } + } +} + +const validate = withValidation({ + schema, + type: "Zod", + mode: "body", +}); + +export default validate(editEventType); diff --git a/pages/api/event-types/[id].ts b/pages/api/event-types/[id]/index.ts similarity index 55% rename from pages/api/event-types/[id].ts rename to pages/api/event-types/[id]/index.ts index 8694d32c38..5fb6a6a519 100644 --- a/pages/api/event-types/[id].ts +++ b/pages/api/event-types/[id]/index.ts @@ -1,27 +1,10 @@ import { PrismaClient, EventType } from "@prisma/client"; +import schema from "lib/validations/queryIdTransformParseInt"; import type { NextApiRequest, NextApiResponse } from "next"; import { withValidation } from "next-validations"; -import { z } from "zod"; const prisma = new PrismaClient(); -const schema = z - .object({ - // since nextjs parses query params as strings, - // we need to cast them to numbers using z.transform() and parseInt() - id: z - .string() - .regex(/^\d+$/) - .transform((id) => parseInt(id)), - }) - .strict(); - -const validate = withValidation({ - schema, - type: "Zod", - mode: "query", -}); - type ResponseData = { data?: EventType; error?: any; @@ -29,18 +12,31 @@ type ResponseData = { export async function eventType(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - if (method === "GET") { - const safe = await schema.safeParse(query); - if (safe.success) { + const safe = await schema.safeParse(query); + if (safe.success) { + if (method === "GET") { const event = await prisma.eventType.findUnique({ where: { id: safe.data.id } }); if (event) res.status(200).json({ data: event }); if (!event) res.status(404).json({ error: "Event type not found" }); + } else if (method === "PATCH") { + const event = await prisma.eventType.update({ + where: { id: safe.data.id }, + data: { title: "Updated title" }, + }); + if (event) res.status(200).json({ data: event }); + if (!event) res.status(404).json({ error: "Event type not found" }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ error: "Only GET Method allowed" }); } - } else { - // Reject any other HTTP method than POST - res.status(405).json({ error: "Only GET Method allowed" }); } } +const validate = withValidation({ + schema, + type: "Zod", + mode: "query", +}); + export default validate(eventType); diff --git a/pages/api/event-types/new.ts b/pages/api/event-types/new.ts index f294a259bb..baf590a4d8 100644 --- a/pages/api/event-types/new.ts +++ b/pages/api/event-types/new.ts @@ -14,7 +14,7 @@ const schema = z }) .strict(); // Adding strict so that we can disallow passing in extra fields -type schema = z.infer; +// type schema = z.infer; const validate = withValidation({ schema, type: "Zod", diff --git a/tsconfig.json b/tsconfig.json index 8581d9636a..67dc71427a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ ], "compilerOptions": { "strictNullChecks": true, - "baseUrl": "./", + "baseUrl": ".", "target": "es5", "lib": [ "dom", @@ -17,9 +17,9 @@ "incremental": true, "module": "esnext", "resolveJsonModule": true, - "jsx": "preserve" + "jsx": "preserve", }, "include": [ - "./" + "./**/*.ts" ] }