From 936572e7e149f3fd1c1a36bdd01a01e78cdde656 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sun, 27 Mar 2022 00:58:22 +0100 Subject: [PATCH] feat: adds availabilitesi and attendees endpoints, some cleanup less try/catch more if/else --- .gitignore | 1 + lib/utils/stringifyISODate.ts | 3 +- lib/validations/apiKey.ts | 3 +- lib/validations/attendee.ts | 20 ++++ lib/validations/availability.ts | 65 +++++++++++++ lib/validations/booking-reference.ts | 65 +++++++++++++ lib/validations/credential.ts | 65 +++++++++++++ lib/validations/daily-event-reference.ts | 65 +++++++++++++ lib/validations/destination-calendar.ts | 65 +++++++++++++ lib/validations/membership.ts | 65 +++++++++++++ lib/validations/payment.ts | 65 +++++++++++++ lib/validations/schedule.ts | 65 +++++++++++++ lib/validations/selected-calendar.ts | 65 +++++++++++++ .../shared/queryIdTransformParseInt.ts | 6 +- lib/validations/webhook.ts | 65 +++++++++++++ pages/api/api-keys/[id]/delete.ts | 7 +- pages/api/api-keys/[id]/edit.ts | 6 +- pages/api/api-keys/[id]/index.ts | 6 +- pages/api/api-keys/index.ts | 12 +-- pages/api/api-keys/new.ts | 22 ++--- pages/api/attendees/[id]/delete.ts | 35 +++++++ pages/api/attendees/[id]/edit.ts | 33 +++++++ pages/api/attendees/[id]/index.ts | 27 ++++++ pages/api/attendees/index.ts | 19 ++++ pages/api/attendees/new.ts | 27 ++++++ pages/api/availabilities/[id]/delete.ts | 27 ++++++ pages/api/availabilities/[id]/edit.ts | 33 +++++++ pages/api/availabilities/[id]/index.ts | 28 ++++++ pages/api/availabilities/index.ts | 19 ++++ pages/api/availabilities/new.ts | 30 ++++++ pages/api/bookings/[id]/delete.ts | 37 ++++---- pages/api/bookings/[id]/edit.ts | 4 +- pages/api/bookings/[id]/index.ts | 21 ++--- pages/api/event-types/[id]/delete.ts | 36 ++++---- pages/api/event-types/[id]/edit.ts | 4 +- pages/api/event-types/[id]/index.ts | 21 ++--- pages/api/teams/[id]/delete.ts | 36 ++++---- pages/api/teams/[id]/edit.ts | 4 +- pages/api/teams/[id]/index.ts | 21 ++--- pages/api/users/[id]/delete.ts | 36 ++++---- pages/api/users/[id]/edit.ts | 4 +- pages/api/users/[id]/index.ts | 20 ++-- tests/bookings/[id]/booking.id.edit.test.ts | 92 +++++++++++++++++++ tests/bookings/[id]/booking.id.index.test.ts | 85 +++++++++++++++++ tests/bookings/booking.index.test.ts | 31 +++++++ tests/bookings/booking.new.test.ts | 71 ++++++++++++++ ...m.id.test.edit.ts => team.id.edit.test.ts} | 0 ...id.test.index.ts => team.id.index.test.ts} | 0 48 files changed, 1365 insertions(+), 172 deletions(-) create mode 100644 lib/validations/attendee.ts create mode 100644 lib/validations/availability.ts create mode 100644 lib/validations/booking-reference.ts create mode 100644 lib/validations/credential.ts create mode 100644 lib/validations/daily-event-reference.ts create mode 100644 lib/validations/destination-calendar.ts create mode 100644 lib/validations/membership.ts create mode 100644 lib/validations/payment.ts create mode 100644 lib/validations/schedule.ts create mode 100644 lib/validations/selected-calendar.ts create mode 100644 lib/validations/webhook.ts create mode 100644 pages/api/attendees/[id]/delete.ts create mode 100644 pages/api/attendees/[id]/edit.ts create mode 100644 pages/api/attendees/[id]/index.ts create mode 100644 pages/api/attendees/index.ts create mode 100644 pages/api/attendees/new.ts create mode 100644 pages/api/availabilities/[id]/delete.ts create mode 100644 pages/api/availabilities/[id]/edit.ts create mode 100644 pages/api/availabilities/[id]/index.ts create mode 100644 pages/api/availabilities/index.ts create mode 100644 pages/api/availabilities/new.ts create mode 100644 tests/bookings/[id]/booking.id.edit.test.ts create mode 100644 tests/bookings/[id]/booking.id.index.test.ts create mode 100644 tests/bookings/booking.index.test.ts create mode 100644 tests/bookings/booking.new.test.ts rename tests/teams/[id]/{team.id.test.edit.ts => team.id.edit.test.ts} (100%) rename tests/teams/[id]/{team.id.test.index.ts => team.id.index.test.ts} (100%) diff --git a/.gitignore b/.gitignore index a50ad511f9..b04bde734a 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ yarn-error.log* .idea ### VisualStudioCode template +.vscode/ .vscode/* !.vscode/settings.json !.vscode/tasks.json diff --git a/lib/utils/stringifyISODate.ts b/lib/utils/stringifyISODate.ts index b9317853ad..bb2ec71339 100644 --- a/lib/utils/stringifyISODate.ts +++ b/lib/utils/stringifyISODate.ts @@ -1,8 +1,7 @@ export const stringifyISODate = (date: Date|undefined): string => { return `${date?.toISOString()}` } - +// FIXME: debug this, supposed to take an array/object and auto strinfy date-like values export const autoStringifyDateValues = ([key, value]: [string, unknown]): [string, unknown] => { - console.log(key,value) return [key, typeof value === "object" && value instanceof Date ? stringifyISODate(value) : value] } \ No newline at end of file diff --git a/lib/validations/apiKey.ts b/lib/validations/apiKey.ts index a8d89d78f3..044e3768f3 100644 --- a/lib/validations/apiKey.ts +++ b/lib/validations/apiKey.ts @@ -9,7 +9,8 @@ const schemaApiKey = z expiresAt: z.date().optional(), // default is 30 days note: z.string().min(1).optional(), }) - .strict(); // Adding strict so that we can disallow passing in extra fields + .strict(); + const withValidApiKey = withValidation({ schema: schemaApiKey, type: "Zod", diff --git a/lib/validations/attendee.ts b/lib/validations/attendee.ts new file mode 100644 index 0000000000..290b337779 --- /dev/null +++ b/lib/validations/attendee.ts @@ -0,0 +1,20 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaAttendee = z + .object({ + id: z.number(), + email: z.string().min(3), + name: z.string().min(3).email(), + timeZone: z.string().default("Europe/London"), + locale: z.string().optional(), + bookingId: z.number(), + }) + .strict(); +const withValidAttendee = withValidation({ + schema: schemaAttendee, + type: "Zod", + mode: "body", +}); + +export { schemaAttendee, withValidAttendee }; diff --git a/lib/validations/availability.ts b/lib/validations/availability.ts new file mode 100644 index 0000000000..b9ecf2fe20 --- /dev/null +++ b/lib/validations/availability.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaAvailability = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaAvailability)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidAvailability = withValidation({ + schema: schemaAvailability, + type: "Zod", + mode: "body", +}); + +export { schemaAvailability, withValidAvailability }; diff --git a/lib/validations/booking-reference.ts b/lib/validations/booking-reference.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/booking-reference.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/lib/validations/credential.ts b/lib/validations/credential.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/credential.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/lib/validations/daily-event-reference.ts b/lib/validations/daily-event-reference.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/daily-event-reference.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/lib/validations/destination-calendar.ts b/lib/validations/destination-calendar.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/destination-calendar.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/lib/validations/membership.ts b/lib/validations/membership.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/membership.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/lib/validations/payment.ts b/lib/validations/payment.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/payment.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/lib/validations/schedule.ts b/lib/validations/schedule.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/schedule.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/lib/validations/selected-calendar.ts b/lib/validations/selected-calendar.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/selected-calendar.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/lib/validations/shared/queryIdTransformParseInt.ts b/lib/validations/shared/queryIdTransformParseInt.ts index f99ea64e18..d185d1a255 100644 --- a/lib/validations/shared/queryIdTransformParseInt.ts +++ b/lib/validations/shared/queryIdTransformParseInt.ts @@ -3,7 +3,7 @@ import { z } from "zod"; // Extracted out as utility function so can be reused // at different endpoints that require this validation. -const schemaQueryId = z +const schemaQueryIdParseInt = z .object({ // since nextjs parses query params as strings, // we need to cast them to numbers using z.transform() and parseInt() @@ -15,9 +15,9 @@ const schemaQueryId = z .strict(); const withValidQueryIdTransformParseInt = withValidation({ - schema: schemaQueryId, + schema: schemaQueryIdParseInt, type: "Zod", mode: "query", }); -export { schemaQueryId, withValidQueryIdTransformParseInt }; +export { schemaQueryIdParseInt, withValidQueryIdTransformParseInt }; diff --git a/lib/validations/webhook.ts b/lib/validations/webhook.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/webhook.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/pages/api/api-keys/[id]/delete.ts b/pages/api/api-keys/[id]/delete.ts index dbd64971f9..b5644e6ce8 100644 --- a/pages/api/api-keys/[id]/delete.ts +++ b/pages/api/api-keys/[id]/delete.ts @@ -11,6 +11,7 @@ type ResponseData = { export async function apiKey(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; const safe = await schemaQueryIdAsString.safeParse(query); + if (method === "DELETE" && safe.success) { // DELETE WILL DELETE THE EVENT TYPE await prisma.apiKey @@ -23,10 +24,8 @@ export async function apiKey(req: NextApiRequest, res: NextApiResponse { res.status(404).json({ message: `apiKey with ID ${safeQuery.data.id} not found and wasn't updated`, error }) }); - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only PATCH Method allowed for updating API keys" }); - } + // Reject any other HTTP method than POST + } else res.status(405).json({ message: "Only PATCH Method allowed for updating API keys" }); } export default withValidQueryIdString(withValidApiKey(editApiKey)); diff --git a/pages/api/api-keys/[id]/index.ts b/pages/api/api-keys/[id]/index.ts index 169e1c121d..49b3a7f6a2 100644 --- a/pages/api/api-keys/[id]/index.ts +++ b/pages/api/api-keys/[id]/index.ts @@ -18,10 +18,8 @@ export async function apiKey(req: NextApiRequest, res: NextApiResponse) { const { method } = req; if (method === "GET") { - // try { const apiKeys = await prisma.apiKey.findMany({}); res.status(200).json({ data: { ...apiKeys } }); - // Without any params this never fails. not sure how to force test unavailable prisma query - // } catch (error) { - // // FIXME: Add zod for validation/error handling - // res.status(400).json({ error: error }); - // } - - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only GET Method allowed" }); - } + } else res.status(405).json({ message: "Only GET Method allowed" }); } diff --git a/pages/api/api-keys/new.ts b/pages/api/api-keys/new.ts index 4a88584d67..8787824fbd 100644 --- a/pages/api/api-keys/new.ts +++ b/pages/api/api-keys/new.ts @@ -15,21 +15,13 @@ async function createApiKey(req: NextApiRequest, res: NextApiResponse) { + const { query, method } = req; + const safe = await schemaQueryIdParseInt.safeParse(query); + + if (method === "DELETE" && safe.success) { + // DELETE WILL DELETE THE EVENT TYPE + prisma.attendee + .delete({ where: { id: safe.data.id } }) + .then(() => { + // We only remove the attendee type from the database if there's an existing resource. + res.status(200).json({ message: `attendee-type with id: ${safe.data.id} deleted successfully` }); + }) + .catch((error) => { + // This catches the error thrown by prisma.attendee.delete() if the resource is not found. + res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); + }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only DELETE Method allowed in /attendee-types/[id]/delete endpoint" }); + } +} + +export default withValidQueryIdTransformParseInt(attendee); diff --git a/pages/api/attendees/[id]/edit.ts b/pages/api/attendees/[id]/edit.ts new file mode 100644 index 0000000000..42a742a46b --- /dev/null +++ b/pages/api/attendees/[id]/edit.ts @@ -0,0 +1,33 @@ +import prisma from "@calcom/prisma"; + +import { Attendee } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaAttendee, withValidAttendee } from "@lib/validations/attendee"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +type ResponseData = { + data?: Attendee; + message?: string; + error?: unknown; +}; + +export async function editAttendee(req: NextApiRequest, res: NextApiResponse) { + const { query, body, method } = req; + const safeQuery = await schemaQueryIdParseInt.safeParse(query); + const safeBody = await schemaAttendee.safeParse(body); + + if (method === "PATCH" && safeQuery.success && safeBody.success) { + await prisma.attendee.update({ + where: { id: safeQuery.data.id }, + data: safeBody.data, + }).then(attendee => { + res.status(200).json({ data: attendee }); + }).catch(error => { + res.status(404).json({ message: `Event type with ID ${safeQuery.data.id} not found and wasn't updated`, error }) + }); + // Reject any other HTTP method than POST + } else res.status(405).json({ message: "Only PATCH Method allowed for updating attendees" }); +} + +export default withValidQueryIdTransformParseInt(withValidAttendee(editAttendee)); diff --git a/pages/api/attendees/[id]/index.ts b/pages/api/attendees/[id]/index.ts new file mode 100644 index 0000000000..a5521dd057 --- /dev/null +++ b/pages/api/attendees/[id]/index.ts @@ -0,0 +1,27 @@ +import prisma from "@calcom/prisma"; + +import { Attendee } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +type ResponseData = { + data?: Attendee; + message?: string; + error?: unknown; +}; + +export async function attendee(req: NextApiRequest, res: NextApiResponse) { + const { query, method } = req; + const safe = await schemaQueryIdParseInt.safeParse(query); + + if (method === "GET" && safe.success) { + const attendee = await prisma.attendee.findUnique({ where: { id: safe.data.id } }); + + if (attendee) res.status(200).json({ data: attendee }); + if (!attendee) res.status(404).json({ message: "Event type not found" }); + // Reject any other HTTP method than POST + } else res.status(405).json({ message: "Only GET Method allowed" }); +} + +export default withValidQueryIdTransformParseInt(attendee); diff --git a/pages/api/attendees/index.ts b/pages/api/attendees/index.ts new file mode 100644 index 0000000000..121457759f --- /dev/null +++ b/pages/api/attendees/index.ts @@ -0,0 +1,19 @@ +import prisma from "@calcom/prisma"; + +import { Attendee } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +type ResponseData = { + data?: Attendee[]; + error?: unknown; +}; + +export default async function attendee(req: NextApiRequest, res: NextApiResponse) { + try { + const attendees = await prisma.attendee.findMany(); + res.status(200).json({ data: { ...attendees } }); + } catch (error) { + // FIXME: Add zod for validation/error handling + res.status(400).json({ error: error }); + } +} diff --git a/pages/api/attendees/new.ts b/pages/api/attendees/new.ts new file mode 100644 index 0000000000..12762f14f0 --- /dev/null +++ b/pages/api/attendees/new.ts @@ -0,0 +1,27 @@ +import prisma from "@calcom/prisma"; + +import { Attendee } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaAttendee, withValidAttendee } from "@lib/validations/attendee"; + +type ResponseData = { + data?: Attendee; + message?: string; + error?: string; +}; + +async function createAttendee(req: NextApiRequest, res: NextApiResponse) { + const { body, method } = req; + const safe = schemaAttendee.safeParse(body); + + if (method === "POST" && safe.success) { + await prisma.attendee + .create({ data: safe.data }) + .then((attendee) => res.status(201).json({ data: attendee })) + .catch((error) => res.status(400).json({ message: "Could not create attendee type", error: error })); + // Reject any other HTTP method than POST + } else res.status(405).json({ error: "Only POST Method allowed" }); +} + +export default withValidAttendee(createAttendee); diff --git a/pages/api/availabilities/[id]/delete.ts b/pages/api/availabilities/[id]/delete.ts new file mode 100644 index 0000000000..cd3c840801 --- /dev/null +++ b/pages/api/availabilities/[id]/delete.ts @@ -0,0 +1,27 @@ +import prisma from "@calcom/prisma"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + + +type ResponseData = { + message?: string; + error?: unknown; +}; + +export async function availability(req: NextApiRequest, res: NextApiResponse) { + const { query, method } = req; + const safe = await schemaQueryIdParseInt.safeParse(query); + if (method === "DELETE" && safe.success && safe.data) { + const availability = await prisma.availability + .delete({ where: { id: safe.data.id } }) + // We only remove the availability type from the database if there's an existing resource. + if (availability) res.status(200).json({ message: `availability with id: ${safe.data.id} deleted successfully` }); + // This catches the error thrown by prisma.availability.delete() if the resource is not found. + else res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`}); + // Reject any other HTTP method than POST + } else res.status(405).json({ message: "Only DELETE Method allowed in /availabilities/[id]/delete endpoint" }); +} + +export default withValidQueryIdTransformParseInt(availability); diff --git a/pages/api/availabilities/[id]/edit.ts b/pages/api/availabilities/[id]/edit.ts new file mode 100644 index 0000000000..3b9f3d7707 --- /dev/null +++ b/pages/api/availabilities/[id]/edit.ts @@ -0,0 +1,33 @@ +import prisma from "@calcom/prisma"; + +import { Availability } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaAvailability, withValidAvailability,} from "@lib/validations/availability"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +type ResponseData = { + data?: Availability; + message?: string; + error?: unknown; +}; + +export async function editAvailability(req: NextApiRequest, res: NextApiResponse) { + const { query, body, method } = req; + const safeQuery = await schemaQueryIdParseInt.safeParse(query); + const safeBody = await schemaAvailability.safeParse(body); + + if (method === "PATCH" && safeQuery.success && safeBody.success) { + await prisma.availability.update({ + where: { id: safeQuery.data.id }, + data: safeBody.data, + }).then(availability => { + res.status(200).json({ data: availability }); + }).catch(error => { + res.status(404).json({ message: `Event type with ID ${safeQuery.data.id} not found and wasn't updated`, error }) + }); + // Reject any other HTTP method than PATCH + } else res.status(405).json({ message: "Only PATCH Method allowed for updating availabilities" }); +} + +export default withValidQueryIdTransformParseInt(withValidAvailability(editAvailability)); diff --git a/pages/api/availabilities/[id]/index.ts b/pages/api/availabilities/[id]/index.ts new file mode 100644 index 0000000000..7d10930907 --- /dev/null +++ b/pages/api/availabilities/[id]/index.ts @@ -0,0 +1,28 @@ +import prisma from "@calcom/prisma"; + +import { Availability } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +type ResponseData = { + data?: Availability; + message?: string; + error?: unknown; +}; + +export async function availability(req: NextApiRequest, res: NextApiResponse) { + const { query, method } = req; + const safe = await schemaQueryIdParseInt.safeParse(query); + + if (method === "GET" && safe.success) { + const availability = await prisma.availability.findUnique({ where: { id: safe.data.id } }); + + if (availability) res.status(200).json({ data: availability }); + if (!availability) res.status(404).json({ message: "Event type not found" }); + // Reject any other HTTP method than POST + } else res.status(405).json({ message: "Only GET Method allowed" }); +} + + +export default withValidQueryIdTransformParseInt(availability); diff --git a/pages/api/availabilities/index.ts b/pages/api/availabilities/index.ts new file mode 100644 index 0000000000..f529fd6ed2 --- /dev/null +++ b/pages/api/availabilities/index.ts @@ -0,0 +1,19 @@ +import prisma from "@calcom/prisma"; + +import { Availability } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +type ResponseData = { + data?: Availability[]; + error?: unknown; +}; + +export default async function availability(req: NextApiRequest, res: NextApiResponse) { + try { + const availabilities = await prisma.availability.findMany(); + res.status(200).json({ data: { ...availabilities } }); + } catch (error) { + // FIXME: Add zod for validation/error handling + res.status(400).json({ error: error }); + } +} diff --git a/pages/api/availabilities/new.ts b/pages/api/availabilities/new.ts new file mode 100644 index 0000000000..fdb7abca24 --- /dev/null +++ b/pages/api/availabilities/new.ts @@ -0,0 +1,30 @@ +import prisma from "@calcom/prisma"; + +import { Availability } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaAvailability, withValidAvailability } from "@lib/validations/availability"; + +type ResponseData = { + data?: Availability; + message?: string; + error?: string; +}; + +async function createAvailability(req: NextApiRequest, res: NextApiResponse) { + const { body, method } = req; + if (method === "POST") { + const safe = schemaAvailability.safeParse(body); + if (safe.success && safe.data) { + await prisma.availability + .create({ data: safe.data }) + .then((availability) => res.status(201).json({ data: availability })) + .catch((error) => res.status(400).json({ message: "Could not create availability type", error: error })); + } + } else { + // Reject any other HTTP method than POST + res.status(405).json({ error: "Only POST Method allowed" }); + } +} + +export default withValidAvailability(createAvailability); diff --git a/pages/api/bookings/[id]/delete.ts b/pages/api/bookings/[id]/delete.ts index 1196e505eb..62fa836955 100644 --- a/pages/api/bookings/[id]/delete.ts +++ b/pages/api/bookings/[id]/delete.ts @@ -2,7 +2,7 @@ import prisma from "@calcom/prisma"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { @@ -12,24 +12,23 @@ type ResponseData = { export async function booking(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - const safe = await schemaQueryId.safeParse(query); - if (safe.success) { - if (method === "DELETE") { - // DELETE WILL DELETE THE EVENT TYPE - prisma.booking - .delete({ where: { id: safe.data.id } }) - .then(() => { - // We only remove the booking type from the database if there's an existing resource. - res.status(200).json({ message: `booking-type with id: ${safe.data.id} deleted successfully` }); - }) - .catch((error) => { - // This catches the error thrown by prisma.booking.delete() if the resource is not found. - res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); - }); - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only DELETE Method allowed in /booking-types/[id]/delete endpoint" }); - } + const safe = await schemaQueryIdParseInt.safeParse(query); + + if (method === "DELETE" && safe.success) { + // DELETE WILL DELETE THE EVENT TYPE + prisma.booking + .delete({ where: { id: safe.data.id } }) + .then(() => { + // We only remove the booking type from the database if there's an existing resource. + res.status(200).json({ message: `booking-type with id: ${safe.data.id} deleted successfully` }); + }) + .catch((error) => { + // This catches the error thrown by prisma.booking.delete() if the resource is not found. + res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); + }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only DELETE Method allowed in /booking-types/[id]/delete endpoint" }); } } diff --git a/pages/api/bookings/[id]/edit.ts b/pages/api/bookings/[id]/edit.ts index 1fc77f9c25..9d78fbaa00 100644 --- a/pages/api/bookings/[id]/edit.ts +++ b/pages/api/bookings/[id]/edit.ts @@ -4,7 +4,7 @@ import { Booking } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { schemaBooking, withValidBooking } from "@lib/validations/booking"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: Booking; @@ -14,7 +14,7 @@ type ResponseData = { export async function editBooking(req: NextApiRequest, res: NextApiResponse) { const { query, body, method } = req; - const safeQuery = await schemaQueryId.safeParse(query); + const safeQuery = await schemaQueryIdParseInt.safeParse(query); const safeBody = await schemaBooking.safeParse(body); if (method === "PATCH") { diff --git a/pages/api/bookings/[id]/index.ts b/pages/api/bookings/[id]/index.ts index 11c84ba5c8..dbe47a5f91 100644 --- a/pages/api/bookings/[id]/index.ts +++ b/pages/api/bookings/[id]/index.ts @@ -3,7 +3,7 @@ import prisma from "@calcom/prisma"; import { Booking } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: Booking; @@ -13,17 +13,16 @@ type ResponseData = { export async function booking(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - const safe = await schemaQueryId.safeParse(query); - if (safe.success) { - if (method === "GET") { - const booking = await prisma.booking.findUnique({ where: { id: safe.data.id } }); + const safe = await schemaQueryIdParseInt.safeParse(query); - if (booking) res.status(200).json({ data: booking }); - if (!booking) res.status(404).json({ message: "Event type not found" }); - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only GET Method allowed" }); - } + if (method === "GET" && safe.success) { + const booking = await prisma.booking.findUnique({ where: { id: safe.data.id } }); + + if (booking) res.status(200).json({ data: booking }); + if (!booking) res.status(404).json({ message: "Event type not found" }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only GET Method allowed" }); } } diff --git a/pages/api/event-types/[id]/delete.ts b/pages/api/event-types/[id]/delete.ts index f6341df094..72bca44879 100644 --- a/pages/api/event-types/[id]/delete.ts +++ b/pages/api/event-types/[id]/delete.ts @@ -2,7 +2,7 @@ import prisma from "@calcom/prisma"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { @@ -12,24 +12,22 @@ type ResponseData = { export async function eventType(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - const safe = await schemaQueryId.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({ message: `Resource with id:${safe.data.id} was not found`, error: error }); - }); - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only DELETE Method allowed in /event-types/[id]/delete endpoint" }); - } + const safe = await schemaQueryIdParseInt.safeParse(query); + if (method === "DELETE" && safe.success) { + // 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({ message: `Resource with id:${safe.data.id} was not found`, error: error }); + }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only DELETE Method allowed in /event-types/[id]/delete endpoint" }); } } diff --git a/pages/api/event-types/[id]/edit.ts b/pages/api/event-types/[id]/edit.ts index 276b0ba34a..becd6b5255 100644 --- a/pages/api/event-types/[id]/edit.ts +++ b/pages/api/event-types/[id]/edit.ts @@ -4,7 +4,7 @@ import { EventType } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { schemaEventType, withValidEventType } from "@lib/validations/eventType"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: EventType; @@ -14,7 +14,7 @@ type ResponseData = { export async function editEventType(req: NextApiRequest, res: NextApiResponse) { const { query, body, method } = req; - const safeQuery = await schemaQueryId.safeParse(query); + const safeQuery = await schemaQueryIdParseInt.safeParse(query); const safeBody = await schemaEventType.safeParse(body); if (method === "PATCH") { diff --git a/pages/api/event-types/[id]/index.ts b/pages/api/event-types/[id]/index.ts index 44eaa46c77..9b8a159ab3 100644 --- a/pages/api/event-types/[id]/index.ts +++ b/pages/api/event-types/[id]/index.ts @@ -3,7 +3,7 @@ import prisma from "@calcom/prisma"; import { EventType } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: EventType; @@ -13,17 +13,16 @@ type ResponseData = { export async function eventType(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - const safe = await schemaQueryId.safeParse(query); - if (safe.success) { - if (method === "GET") { - const event = await prisma.eventType.findUnique({ where: { id: safe.data.id } }); + const safe = await schemaQueryIdParseInt.safeParse(query); - if (event) res.status(200).json({ data: event }); - if (!event) res.status(404).json({ message: "Event type not found" }); - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only GET Method allowed" }); - } + if (method === "GET" && safe.success) { + 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({ message: "Event type not found" }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only GET Method allowed" }); } } diff --git a/pages/api/teams/[id]/delete.ts b/pages/api/teams/[id]/delete.ts index 26a5df4919..935d24c12f 100644 --- a/pages/api/teams/[id]/delete.ts +++ b/pages/api/teams/[id]/delete.ts @@ -2,7 +2,7 @@ import prisma from "@calcom/prisma"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { @@ -12,24 +12,22 @@ type ResponseData = { export async function team(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - const safe = await schemaQueryId.safeParse(query); - if (safe.success) { - if (method === "DELETE") { - // DELETE WILL DELETE THE EVENT TYPE - prisma.team - .delete({ where: { id: safe.data.id } }) - .then(() => { - // We only remove the team type from the database if there's an existing resource. - res.status(200).json({ message: `team-type with id: ${safe.data.id} deleted successfully` }); - }) - .catch((error) => { - // This catches the error thrown by prisma.team.delete() if the resource is not found. - res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); - }); - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only DELETE Method allowed in /team-types/[id]/delete endpoint" }); - } + const safe = await schemaQueryIdParseInt.safeParse(query); + if (method === "DELETE" && safe.success) { + // DELETE WILL DELETE THE EVENT TYPE + prisma.team + .delete({ where: { id: safe.data.id } }) + .then(() => { + // We only remove the team type from the database if there's an existing resource. + res.status(200).json({ message: `team-type with id: ${safe.data.id} deleted successfully` }); + }) + .catch((error) => { + // This catches the error thrown by prisma.team.delete() if the resource is not found. + res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); + }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only DELETE Method allowed in /team-types/[id]/delete endpoint" }); } } diff --git a/pages/api/teams/[id]/edit.ts b/pages/api/teams/[id]/edit.ts index eb3636aac4..c046c97da4 100644 --- a/pages/api/teams/[id]/edit.ts +++ b/pages/api/teams/[id]/edit.ts @@ -4,7 +4,7 @@ import { Team } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { schemaTeam, withValidTeam } from "@lib/validations/team"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: Team; @@ -14,7 +14,7 @@ type ResponseData = { export async function editTeam(req: NextApiRequest, res: NextApiResponse) { const { query, body, method } = req; - const safeQuery = await schemaQueryId.safeParse(query); + const safeQuery = await schemaQueryIdParseInt.safeParse(query); const safeBody = await schemaTeam.safeParse(body); if (method === "PATCH") { diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index d21c33e266..cd9cd660d4 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -3,7 +3,7 @@ import prisma from "@calcom/prisma"; import { Team } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: Team; @@ -13,17 +13,16 @@ type ResponseData = { export async function team(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - const safe = await schemaQueryId.safeParse(query); - if (safe.success) { - if (method === "GET") { - const team = await prisma.team.findUnique({ where: { id: safe.data.id } }); + const safe = await schemaQueryIdParseInt.safeParse(query); + + if (method === "GET" && safe.success) { + const team = await prisma.team.findUnique({ where: { id: safe.data.id } }); - if (team) res.status(200).json({ data: team }); - if (!team) res.status(404).json({ message: "Event type not found" }); - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only GET Method allowed" }); - } + if (team) res.status(200).json({ data: team }); + if (!team) res.status(404).json({ message: "Event type not found" }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only GET Method allowed" }); } } diff --git a/pages/api/users/[id]/delete.ts b/pages/api/users/[id]/delete.ts index 860b684a64..3c9b608ef3 100644 --- a/pages/api/users/[id]/delete.ts +++ b/pages/api/users/[id]/delete.ts @@ -2,7 +2,7 @@ import prisma from "@calcom/prisma"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { @@ -12,24 +12,22 @@ type ResponseData = { export async function user(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - const safe = await schemaQueryId.safeParse(query); - if (safe.success) { - if (method === "DELETE") { - // DELETE WILL DELETE THE EVENT TYPE - prisma.user - .delete({ where: { id: safe.data.id } }) - .then(() => { - // We only remove the user type from the database if there's an existing resource. - res.status(200).json({ message: `user-type with id: ${safe.data.id} deleted successfully` }); - }) - .catch((error) => { - // This catches the error thrown by prisma.user.delete() if the resource is not found. - res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); - }); - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only DELETE Method allowed in /user-types/[id]/delete endpoint" }); - } + const safe = await schemaQueryIdParseInt.safeParse(query); + if (method === "DELETE" && safe.success) { + // DELETE WILL DELETE THE EVENT TYPE + prisma.user + .delete({ where: { id: safe.data.id } }) + .then(() => { + // We only remove the user type from the database if there's an existing resource. + res.status(200).json({ message: `user-type with id: ${safe.data.id} deleted successfully` }); + }) + .catch((error) => { + // This catches the error thrown by prisma.user.delete() if the resource is not found. + res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); + }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only DELETE Method allowed in /user-types/[id]/delete endpoint" }); } } diff --git a/pages/api/users/[id]/edit.ts b/pages/api/users/[id]/edit.ts index 7b26f306f8..1e5780fc35 100644 --- a/pages/api/users/[id]/edit.ts +++ b/pages/api/users/[id]/edit.ts @@ -4,7 +4,7 @@ import { User } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { schemaUser, withValidUser } from "@lib/validations/user"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: User; @@ -14,7 +14,7 @@ type ResponseData = { export async function editUser(req: NextApiRequest, res: NextApiResponse) { const { query, body, method } = req; - const safeQuery = await schemaQueryId.safeParse(query); + const safeQuery = await schemaQueryIdParseInt.safeParse(query); const safeBody = await schemaUser.safeParse(body); if (method === "PATCH") { diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index a5fcf369fa..8852ad6e6f 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -3,7 +3,7 @@ import prisma from "@calcom/prisma"; import { User } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; +import { schemaQueryIdParseInt, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: User; @@ -13,17 +13,15 @@ type ResponseData = { export async function user(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - const safe = await schemaQueryId.safeParse(query); - if (safe.success) { - if (method === "GET") { - const user = await prisma.user.findUnique({ where: { id: safe.data.id } }); + const safe = await schemaQueryIdParseInt.safeParse(query); + if (method === "GET" && safe.success) { + const user = await prisma.user.findUnique({ where: { id: safe.data.id } }); - if (user) res.status(200).json({ data: user }); - if (!user) res.status(404).json({ message: "Event type not found" }); - } else { - // Reject any other HTTP method than POST - res.status(405).json({ message: "Only GET Method allowed" }); - } + if (user) res.status(200).json({ data: user }); + if (!user) res.status(404).json({ message: "Event type not found" }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only GET Method allowed" }); } } diff --git a/tests/bookings/[id]/booking.id.edit.test.ts b/tests/bookings/[id]/booking.id.edit.test.ts new file mode 100644 index 0000000000..5b7c1ec73f --- /dev/null +++ b/tests/bookings/[id]/booking.id.edit.test.ts @@ -0,0 +1,92 @@ +import handleBookingEdit from "@api/bookings/[id]/edit"; +import { createMocks } from "node-mocks-http"; + +import prisma from "@calcom/prisma"; + +describe("PATCH /api/bookings/[id]/edit with valid id and body updates an booking", () => { + it("returns a message with the specified bookings", async () => { + const { req, res } = createMocks({ + method: "PATCH", + query: { + id: "2", + }, + body: { + title: "Updated title", + slug: "updated-slug", + length: 1, + }, + }); + const booking = await prisma.booking.findUnique({ where: { id: parseInt(req.query.id) } }); + await handleBookingEdit(req, res); + + expect(res._getStatusCode()).toBe(200); + if (booking) booking.title = "Updated title"; + expect(JSON.parse(res._getData())).toStrictEqual({ data: booking }); + }); +}); + +describe("PATCH /api/bookings/[id]/edit with invalid id returns 404", () => { + it("returns a message with the specified bookings", async () => { + const { req, res } = createMocks({ + method: "PATCH", + query: { + id: "0", + }, + body: { + title: "Updated title", + slug: "updated-slug", + length: 1, + }, + }); + const booking = await prisma.booking.findUnique({ where: { id: parseInt(req.query.id) } }); + await handleBookingEdit(req, res); + + expect(res._getStatusCode()).toBe(404); + if (booking) booking.title = "Updated title"; + expect(JSON.parse(res._getData())).toStrictEqual({ "error": { + "clientVersion": "3.10.0", + "code": "P2025", + "meta": { + "cause": "Record to update not found.", + }, + }, + "message": "Event type with ID 0 not found and wasn't updated", }); + }); +}); + +describe("PATCH /api/bookings/[id]/edit with valid id and no body returns 400 error and zod validation errors", () => { + it("returns a message with the specified bookings", async () => { + const { req, res } = createMocks({ + method: "PATCH", + query: { + id: "2", + }, + }); + await handleBookingEdit(req, res); + + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toStrictEqual([{"code": "invalid_type", "expected": "string", "message": "Required", "path": ["title"], "received": "undefined"}, {"code": "invalid_type", "expected": "string", "message": "Required", "path": ["slug"], "received": "undefined"}, {"code": "invalid_type", "expected": "number", "message": "Required", "path": ["length"], "received": "undefined"}]); + }); +}); + +describe("POST /api/bookings/[id]/edit fails, only PATCH allowed", () => { + it("returns a message with the specified bookings", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + query: { + id: "1", + }, + body: { + title: "Updated title", + slug: "updated-slug", + length: 1, + }, + }); + await handleBookingEdit(req, res); + + expect(res._getStatusCode()).toBe(405); + expect(JSON.parse(res._getData())).toStrictEqual({ message: "Only PATCH Method allowed for updating bookings" }); + }); +}); + + diff --git a/tests/bookings/[id]/booking.id.index.test.ts b/tests/bookings/[id]/booking.id.index.test.ts new file mode 100644 index 0000000000..d4edede123 --- /dev/null +++ b/tests/bookings/[id]/booking.id.index.test.ts @@ -0,0 +1,85 @@ +import handleBooking from "@api/bookings/[id]"; +import { createMocks } from "node-mocks-http"; + +import prisma from "@calcom/prisma"; +import { stringifyISODate } from "@lib/utils/stringifyISODate"; + +describe("GET /api/bookings/[id] with valid id as string returns an booking", () => { + it("returns a message with the specified events", async () => { + const { req, res } = createMocks({ + method: "GET", + query: { + id: "1", + }, + }); + const booking = await prisma.booking.findUnique({ where: { id: 1 } }); + await handleBooking(req, res); + + expect(res._getStatusCode()).toBe(200); + expect(JSON.parse(res._getData())).toEqual({ + data: { + ...booking, + createdAt: stringifyISODate(booking?.createdAt), + startTime: stringifyISODate(booking?.startTime), + endTime: stringifyISODate(booking?.endTime) + } + }); + }); +}); + +// 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("GET /api/bookings/[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", + query: { + id: 1, // passing query as a number, which should fail as nextjs will try to parse it as a string + }, + }); + await handleBooking(req, res); + + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toStrictEqual([ + { + code: "invalid_type", + expected: "string", + received: "number", + path: ["id"], + message: "Expected string, received number", + }, + ]); + }); +}); + +describe("GET /api/bookings/[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", + query: { + id: "0", // There's no booking type with id 0 + }, + }); + await handleBooking(req, res); + + expect(res._getStatusCode()).toBe(404); + expect(JSON.parse(res._getData())).toStrictEqual({ message: "Event type not found" }); + }); +}); + +describe("POST /api/bookings/[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 + query: { + id: "1", + }, + }); + await handleBooking(req, res); + + expect(res._getStatusCode()).toBe(405); + expect(JSON.parse(res._getData())).toStrictEqual({ message: "Only GET Method allowed" }); + }); +}); + + diff --git a/tests/bookings/booking.index.test.ts b/tests/bookings/booking.index.test.ts new file mode 100644 index 0000000000..87e1bd24b5 --- /dev/null +++ b/tests/bookings/booking.index.test.ts @@ -0,0 +1,31 @@ +import handleApiKeys from "@api/api-keys"; +import { createMocks } from "node-mocks-http"; + +import prisma from "@calcom/prisma"; +import {stringifyISODate} from "@lib/utils/stringifyISODate"; + +describe("GET /api/api-keys without any params", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "GET", + query: {}, + }); + let apiKeys = await prisma.apiKey.findMany(); + await handleApiKeys(req, res); + + expect(res._getStatusCode()).toBe(200); + apiKeys = apiKeys.map(apiKey => (apiKey = {...apiKey, createdAt: stringifyISODate(apiKey?.createdAt), expiresAt: stringifyISODate(apiKey?.expiresAt)})); + expect(JSON.parse(res._getData())).toStrictEqual(JSON.parse(JSON.stringify({ data: {...apiKeys} }))); + }); +}); + +describe("POST /api/api-keys/ fails, only GET allowed", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + }); + await handleApiKeys(req, res); + expect(res._getStatusCode()).toBe(405); + expect(JSON.parse(res._getData())).toStrictEqual({ message: "Only GET Method allowed" }); + }); +}); diff --git a/tests/bookings/booking.new.test.ts b/tests/bookings/booking.new.test.ts new file mode 100644 index 0000000000..9507bdbd71 --- /dev/null +++ b/tests/bookings/booking.new.test.ts @@ -0,0 +1,71 @@ +import handleNewApiKey from "@api/api-keys/new"; +import { createMocks } from "node-mocks-http"; + +describe("POST /api/api-keys/new with a note", () => { + it("returns a 201, and the created api key", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + body: { + note: "Updated note", + }, + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(201); + expect(JSON.parse(res._getData()).data.note).toStrictEqual("Updated note"); + }); +}); + +describe("POST /api/api-keys/new with a slug param", () => { + it("returns error 400, and the details about invalid slug body param", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + body: { + note: "Updated note", + slug: "slug", + }, + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toStrictEqual( + [{"code": "unrecognized_keys", "keys": ["slug"], "message": "Unrecognized key(s) in object: 'slug'", "path": []}] + ); + }); +}); + + +describe("GET /api/api-keys/new fails, only POST allowed", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "GET", // This POST method is not allowed + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(405); + expect(JSON.parse(res._getData())).toStrictEqual({ error: "Only POST Method allowed" }); + }); +}); + + +// FIXME: test 405 when prisma fails look for how to test prisma errors +describe("GET /api/api-keys/new fails, only POST allowed", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + body: { + nonExistentParam: true + // note: '123', + // slug: 12, + }, + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toStrictEqual([{ + "code": "unrecognized_keys", + "keys": ["nonExistentParam"], + "message": "Unrecognized key(s) in object: 'nonExistentParam'", "path": [] + }]); + }); +}); \ No newline at end of file diff --git a/tests/teams/[id]/team.id.test.edit.ts b/tests/teams/[id]/team.id.edit.test.ts similarity index 100% rename from tests/teams/[id]/team.id.test.edit.ts rename to tests/teams/[id]/team.id.edit.test.ts diff --git a/tests/teams/[id]/team.id.test.index.ts b/tests/teams/[id]/team.id.index.test.ts similarity index 100% rename from tests/teams/[id]/team.id.test.index.ts rename to tests/teams/[id]/team.id.index.test.ts