From 936572e7e149f3fd1c1a36bdd01a01e78cdde656 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sun, 27 Mar 2022 00:58:22 +0100 Subject: [PATCH 1/7] 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 From 396c5b8d8caca612b38078cbc862966b9b49b4e7 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sun, 27 Mar 2022 01:08:00 +0100 Subject: [PATCH 2/7] chore: refactor all delete endpoints to use if/else instead of .catch() and .error() --- pages/api/api-keys/[id]/delete.ts | 24 ++++++++++-------------- pages/api/attendees/[id]/delete.ts | 22 +++++++--------------- pages/api/bookings/[id]/delete.ts | 26 +++++++++----------------- pages/api/event-types/[id]/delete.ts | 25 +++++++++---------------- pages/api/teams/[id]/delete.ts | 25 +++++++++---------------- pages/api/users/[id]/delete.ts | 25 +++++++++---------------- 6 files changed, 53 insertions(+), 94 deletions(-) diff --git a/pages/api/api-keys/[id]/delete.ts b/pages/api/api-keys/[id]/delete.ts index b5644e6ce8..10e6aaef98 100644 --- a/pages/api/api-keys/[id]/delete.ts +++ b/pages/api/api-keys/[id]/delete.ts @@ -1,8 +1,10 @@ import prisma from "@calcom/prisma"; -import { NextApiRequest, NextApiResponse } from "next"; + +import type { NextApiRequest, NextApiResponse } from "next"; import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/shared/queryIdString"; + type ResponseData = { message?: string; error?: unknown; @@ -11,19 +13,13 @@ 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 - .delete({ where: { id: safe.data.id } }) - .then(() => { - // We only remove the api key from the database if there's an existing resource. - res.status(204).json({ message: `api-key with id: ${safe.data.id} deleted successfully` }); - }) - .catch((error) => { - // This catches the error thrown by prisma.apiKey.delete() if the resource is not found. - res.status(404).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); - }); + if (method === "DELETE" && safe.success && safe.data) { + const apiKey = await prisma.apiKey + .delete({ where: { id: safe.data.id } }) + // We only remove the apiKey type from the database if there's an existing resource. + if (apiKey) res.status(200).json({ message: `apiKey with id: ${safe.data.id} deleted successfully` }); + // This catches the error thrown by prisma.apiKey.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" }); } diff --git a/pages/api/attendees/[id]/delete.ts b/pages/api/attendees/[id]/delete.ts index 767860af93..47b4a783b7 100644 --- a/pages/api/attendees/[id]/delete.ts +++ b/pages/api/attendees/[id]/delete.ts @@ -13,23 +13,15 @@ type ResponseData = { export async function attendee(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 + if (method === "DELETE" && safe.success && safe.data) { + const attendee = await 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 { + // We only remove the attendee type from the database if there's an existing resource. + if (attendee) res.status(200).json({ message: `attendee with id: ${safe.data.id} deleted successfully` }); + // This catches the error thrown by prisma.attendee.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 - res.status(405).json({ message: "Only DELETE Method allowed in /attendee-types/[id]/delete endpoint" }); - } + } else res.status(405).json({ message: "Only DELETE Method allowed in /availabilities/[id]/delete endpoint" }); } export default withValidQueryIdTransformParseInt(attendee); diff --git a/pages/api/bookings/[id]/delete.ts b/pages/api/bookings/[id]/delete.ts index 62fa836955..4150a66613 100644 --- a/pages/api/bookings/[id]/delete.ts +++ b/pages/api/bookings/[id]/delete.ts @@ -10,26 +10,18 @@ type ResponseData = { error?: unknown; }; -export async function booking(req: NextApiRequest, res: NextApiResponse) { +export async function deleteBooking(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.booking + if (method === "DELETE" && safe.success && safe.data) { + const booking = await 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 { + // We only remove the booking type from the database if there's an existing resource. + if (booking) res.status(200).json({ message: `booking with id: ${safe.data.id} deleted successfully` }); + // This catches the error thrown by prisma.booking.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 - res.status(405).json({ message: "Only DELETE Method allowed in /booking-types/[id]/delete endpoint" }); - } + } else res.status(405).json({ message: "Only DELETE Method allowed in /availabilities/[id]/delete endpoint" }); } -export default withValidQueryIdTransformParseInt(booking); +export default withValidQueryIdTransformParseInt(deleteBooking); diff --git a/pages/api/event-types/[id]/delete.ts b/pages/api/event-types/[id]/delete.ts index 72bca44879..d94dc611a2 100644 --- a/pages/api/event-types/[id]/delete.ts +++ b/pages/api/event-types/[id]/delete.ts @@ -10,25 +10,18 @@ type ResponseData = { error?: unknown; }; -export async function eventType(req: NextApiRequest, res: NextApiResponse) { +export async function deleteEventType(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.eventType + if (method === "DELETE" && safe.success && safe.data) { + const eventType = await 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 { + // We only remove the eventType type from the database if there's an existing resource. + if (eventType) res.status(200).json({ message: `eventType with id: ${safe.data.id} deleted successfully` }); + // This catches the error thrown by prisma.eventType.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 - res.status(405).json({ message: "Only DELETE Method allowed in /event-types/[id]/delete endpoint" }); - } + } else res.status(405).json({ message: "Only DELETE Method allowed in /availabilities/[id]/delete endpoint" }); } -export default withValidQueryIdTransformParseInt(eventType); +export default withValidQueryIdTransformParseInt(deleteEventType); diff --git a/pages/api/teams/[id]/delete.ts b/pages/api/teams/[id]/delete.ts index 935d24c12f..9ca57af0a4 100644 --- a/pages/api/teams/[id]/delete.ts +++ b/pages/api/teams/[id]/delete.ts @@ -10,25 +10,18 @@ type ResponseData = { error?: unknown; }; -export async function team(req: NextApiRequest, res: NextApiResponse) { +export async function deleteTeam(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.team + if (method === "DELETE" && safe.success && safe.data) { + const team = await 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 { + // We only remove the team type from the database if there's an existing resource. + if (team) res.status(200).json({ message: `team with id: ${safe.data.id} deleted successfully` }); + // This catches the error thrown by prisma.team.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 - res.status(405).json({ message: "Only DELETE Method allowed in /team-types/[id]/delete endpoint" }); - } + } else res.status(405).json({ message: "Only DELETE Method allowed" }); } -export default withValidQueryIdTransformParseInt(team); +export default withValidQueryIdTransformParseInt(deleteTeam); diff --git a/pages/api/users/[id]/delete.ts b/pages/api/users/[id]/delete.ts index 3c9b608ef3..f7bbf96856 100644 --- a/pages/api/users/[id]/delete.ts +++ b/pages/api/users/[id]/delete.ts @@ -10,25 +10,18 @@ type ResponseData = { error?: unknown; }; -export async function user(req: NextApiRequest, res: NextApiResponse) { +export async function deleteUser(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.user + if (method === "DELETE" && safe.success && safe.data) { + const user = await 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 { + // We only remove the user type from the database if there's an existing resource. + if (user) res.status(200).json({ message: `user with id: ${safe.data.id} deleted successfully` }); + // This catches the error thrown by prisma.user.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 - res.status(405).json({ message: "Only DELETE Method allowed in /user-types/[id]/delete endpoint" }); - } + } else res.status(405).json({ message: "Only DELETE Method allowed" }); } -export default withValidQueryIdTransformParseInt(user); +export default withValidQueryIdTransformParseInt(deleteUser); From c561b16f8585a9645707613909021deaad3efa6e Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sun, 27 Mar 2022 01:19:49 +0100 Subject: [PATCH 3/7] chore: rename empty validations, enable relations on user zod object --- lib/validations/availability.ts | 2 +- lib/validations/booking-reference.ts | 64 +++--------------------- lib/validations/booking.ts | 42 +--------------- lib/validations/credential.ts | 64 +++--------------------- lib/validations/daily-event-reference.ts | 64 +++--------------------- lib/validations/destination-calendar.ts | 64 +++--------------------- lib/validations/eventType.ts | 2 +- lib/validations/membership.ts | 62 ++--------------------- lib/validations/payment.ts | 64 +++--------------------- lib/validations/schedule.ts | 64 +++--------------------- lib/validations/selected-calendar.ts | 64 +++--------------------- lib/validations/team.ts | 2 +- lib/validations/user.ts | 37 +++++++------- lib/validations/webhook.ts | 63 +++-------------------- 14 files changed, 76 insertions(+), 582 deletions(-) diff --git a/lib/validations/availability.ts b/lib/validations/availability.ts index b9ecf2fe20..d5fc7bbc25 100644 --- a/lib/validations/availability.ts +++ b/lib/validations/availability.ts @@ -55,7 +55,7 @@ const schemaAvailability = z // metadata: z.object({}).optional(), // verified: z.boolean().default(false), }) - .strict(); // Adding strict so that we can disallow passing in extra fields + .strict(); const withValidAvailability = withValidation({ schema: schemaAvailability, type: "Zod", diff --git a/lib/validations/booking-reference.ts b/lib/validations/booking-reference.ts index fb50c9c9ad..3a7298b9ac 100644 --- a/lib/validations/booking-reference.ts +++ b/lib/validations/booking-reference.ts @@ -1,65 +1,13 @@ 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, +const schemaBookingReference = z + .object({}) + .strict(); +const withValidBookingReference = withValidation({ + schema: schemaBookingReference, type: "Zod", mode: "body", }); -export { schemaBooking, withValidBooking }; +export { schemaBookingReference, withValidBookingReference }; diff --git a/lib/validations/booking.ts b/lib/validations/booking.ts index fb50c9c9ad..3959ad9b24 100644 --- a/lib/validations/booking.ts +++ b/lib/validations/booking.ts @@ -14,48 +14,8 @@ const schemaBooking = z 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 + .strict(); const withValidBooking = withValidation({ schema: schemaBooking, type: "Zod", diff --git a/lib/validations/credential.ts b/lib/validations/credential.ts index fb50c9c9ad..b0b142bfd3 100644 --- a/lib/validations/credential.ts +++ b/lib/validations/credential.ts @@ -1,65 +1,13 @@ 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, +const schemaCredential = z + .object({}) + .strict(); +const withValidCredential = withValidation({ + schema: schemaCredential, type: "Zod", mode: "body", }); -export { schemaBooking, withValidBooking }; +export { schemaCredential, withValidCredential }; diff --git a/lib/validations/daily-event-reference.ts b/lib/validations/daily-event-reference.ts index fb50c9c9ad..48a8a93e62 100644 --- a/lib/validations/daily-event-reference.ts +++ b/lib/validations/daily-event-reference.ts @@ -1,65 +1,13 @@ 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, +const schemaDailyEventReference = z + .object({}) + .strict(); +const withValidDailyEventReference = withValidation({ + schema: schemaDailyEventReference, type: "Zod", mode: "body", }); -export { schemaBooking, withValidBooking }; +export { schemaDailyEventReference, withValidDailyEventReference }; diff --git a/lib/validations/destination-calendar.ts b/lib/validations/destination-calendar.ts index fb50c9c9ad..34fa856897 100644 --- a/lib/validations/destination-calendar.ts +++ b/lib/validations/destination-calendar.ts @@ -1,65 +1,13 @@ 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, +const schemaDestinationCalendar = z + .object({}) + .strict(); +const withValidDestinationCalendar = withValidation({ + schema: schemaDestinationCalendar, type: "Zod", mode: "body", }); -export { schemaBooking, withValidBooking }; +export { schemaDestinationCalendar, withValidDestinationCalendar }; diff --git a/lib/validations/eventType.ts b/lib/validations/eventType.ts index d9be43a1e2..34764a21ee 100644 --- a/lib/validations/eventType.ts +++ b/lib/validations/eventType.ts @@ -8,7 +8,7 @@ const schemaEventType = z 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 + .strict(); const withValidEventType = withValidation({ schema: schemaEventType, type: "Zod", diff --git a/lib/validations/membership.ts b/lib/validations/membership.ts index fb50c9c9ad..4959dafb8a 100644 --- a/lib/validations/membership.ts +++ b/lib/validations/membership.ts @@ -1,65 +1,13 @@ 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), - }) +const schemaMembership = z + .object({}) .strict(); // Adding strict so that we can disallow passing in extra fields -const withValidBooking = withValidation({ - schema: schemaBooking, +const withValidMembership = withValidation({ + schema: schemaMembership, type: "Zod", mode: "body", }); -export { schemaBooking, withValidBooking }; +export { schemaMembership, withValidMembership }; diff --git a/lib/validations/payment.ts b/lib/validations/payment.ts index fb50c9c9ad..b9f019fe0d 100644 --- a/lib/validations/payment.ts +++ b/lib/validations/payment.ts @@ -1,65 +1,13 @@ 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, +const schemaPayment = z + .object({}) + .strict(); +const withValidPayment = withValidation({ + schema: schemaPayment, type: "Zod", mode: "body", }); -export { schemaBooking, withValidBooking }; +export { schemaPayment, withValidPayment }; diff --git a/lib/validations/schedule.ts b/lib/validations/schedule.ts index fb50c9c9ad..9a0b0f286f 100644 --- a/lib/validations/schedule.ts +++ b/lib/validations/schedule.ts @@ -1,65 +1,13 @@ 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, +const schemaSchedule = z + .object({}) + .strict(); +const withValidSchedule = withValidation({ + schema: schemaSchedule, type: "Zod", mode: "body", }); -export { schemaBooking, withValidBooking }; +export { schemaSchedule, withValidSchedule }; diff --git a/lib/validations/selected-calendar.ts b/lib/validations/selected-calendar.ts index fb50c9c9ad..b1646e1047 100644 --- a/lib/validations/selected-calendar.ts +++ b/lib/validations/selected-calendar.ts @@ -1,65 +1,13 @@ 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, +const schemaSelectedCalendar = z + .object({}) + .strict(); +const withValidSelectedCalendar = withValidation({ + schema: schemaSelectedCalendar, type: "Zod", mode: "body", }); -export { schemaBooking, withValidBooking }; +export { schemaSelectedCalendar, withValidSelectedCalendar }; diff --git a/lib/validations/team.ts b/lib/validations/team.ts index 9ed08f90d5..935f49ac1e 100644 --- a/lib/validations/team.ts +++ b/lib/validations/team.ts @@ -9,7 +9,7 @@ const schemaTeam = z bio: z.string().min(3).optional(), logo: z.string().optional(), }) - .strict(); // Adding strict so that we can disallow passing in extra fields + .strict(); const withValidTeam = withValidation({ schema: schemaTeam, type: "Zod", diff --git a/lib/validations/user.ts b/lib/validations/user.ts index 4b24b168af..b0fd00848c 100644 --- a/lib/validations/user.ts +++ b/lib/validations/user.ts @@ -1,15 +1,16 @@ import { withValidation } from "next-validations"; -import { schemaEventType } from "./eventType"; -// import { schemaCredential } from "./credential"; -// import { schemaMembership } from "./membership"; -// import { schemaBooking } from "./booking"; -// import { schemaSchedule } from "./schedule"; -// import { schemaSelectedCalendar } from "./selectedCalendar"; -// import { schemaAvailability } from "./availability"; -// import { schemaWebhook } from "./webhook"; - import { z } from "zod"; + +import { schemaEventType } from "./eventType"; import { schemaApiKey } from "./apiKey"; +import { schemaDestinationCalendar } from "./destination-calendar"; +import { schemaWebhook } from "./webhook"; +import { schemaAvailability } from "./availability"; +import { schemaSelectedCalendar } from "./selected-calendar"; +import { schemaBooking } from "./booking"; +import { schemaMembership } from "./membership"; +import { schemaSchedule } from "./schedule"; +import { schemaCredential } from "./credential"; const schemaUser = z .object({ @@ -27,12 +28,12 @@ const schemaUser = z 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(), + credentials: z.array((schemaCredential)).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(), + selectedCalendars: z.array((schemaSelectedCalendar)).optional(), completedOnboarding: z.boolean().default(false), locale: z.string().optional(), timeFormat: z.number().optional().default(12), @@ -40,19 +41,19 @@ const schemaUser = z twoFactorSecret: z.string().optional(), identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), identityProviderId: z.string().optional(), - // availavility: z.array((schemaAvailavility)).optional(), + availability: z.array((schemaAvailability)).optional(), invitedTo: z.number().optional(), plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), - // webhooks: z.array((schemaWebhook)).optional(), + 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 + destinationCalendar: z.array(schemaDestinationCalendar).optional(), // FIXME: instanceof doesnt work here away: z.boolean().default(false), metadata: z.object({}).optional(), verified: z.boolean().default(false), apiKeys: z.array((schemaApiKey)).optional(), }) - .strict(); // Adding strict so that we can disallow passing in extra fields + .strict(); const withValidUser = withValidation({ schema: schemaUser, type: "Zod", diff --git a/lib/validations/webhook.ts b/lib/validations/webhook.ts index fb50c9c9ad..1b1a35a2da 100644 --- a/lib/validations/webhook.ts +++ b/lib/validations/webhook.ts @@ -1,65 +1,14 @@ 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), +const schemaWebhook = z + .object({}) + .strict(); - // 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, +const withValidWebhook = withValidation({ + schema: schemaWebhook, type: "Zod", mode: "body", }); -export { schemaBooking, withValidBooking }; +export { schemaWebhook, withValidWebhook }; From 0e3131d8665bd17727ef02bb13ba01abf1c571bd Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Sun, 27 Mar 2022 15:15:46 +0200 Subject: [PATCH 4/7] feat: improve validations --- lib/validations/apiKey.ts | 3 +- lib/validations/availability.ts | 60 ++++--------------------- lib/validations/booking.ts | 2 +- lib/validations/membership.ts | 2 +- pages/api/attendees/[id]/delete.ts | 2 +- pages/api/availabilities/[id]/delete.ts | 5 +-- 6 files changed, 15 insertions(+), 59 deletions(-) diff --git a/lib/validations/apiKey.ts b/lib/validations/apiKey.ts index 044e3768f3..a8d89d78f3 100644 --- a/lib/validations/apiKey.ts +++ b/lib/validations/apiKey.ts @@ -9,8 +9,7 @@ const schemaApiKey = z expiresAt: z.date().optional(), // default is 30 days note: z.string().min(1).optional(), }) - .strict(); - + .strict(); // Adding strict so that we can disallow passing in extra fields const withValidApiKey = withValidation({ schema: schemaApiKey, type: "Zod", diff --git a/lib/validations/availability.ts b/lib/validations/availability.ts index d5fc7bbc25..1bffc5acec 100644 --- a/lib/validations/availability.ts +++ b/lib/validations/availability.ts @@ -3,57 +3,15 @@ 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), + id: z.number(), + userId: z.number(), + eventTypeId: z.number(), + scheduleId: z.number(), + + days: z.array(z.number()), + date: z.date().or(z.string()), + startTime: z.string(), + endTime: z.string(), }) .strict(); const withValidAvailability = withValidation({ diff --git a/lib/validations/booking.ts b/lib/validations/booking.ts index 3959ad9b24..7c07aac283 100644 --- a/lib/validations/booking.ts +++ b/lib/validations/booking.ts @@ -10,7 +10,7 @@ const schemaBooking = z endTime: z.date(), location: z.string().min(3).optional(), createdAt: z.date().or(z.string()), - updatedAt: z.date(), + updatedAt: z.date().or(z.string()), confirmed: z.boolean().default(true), rejected: z.boolean().default(false), paid: z.boolean().default(false), diff --git a/lib/validations/membership.ts b/lib/validations/membership.ts index 4959dafb8a..3d52743955 100644 --- a/lib/validations/membership.ts +++ b/lib/validations/membership.ts @@ -3,7 +3,7 @@ import { z } from "zod"; const schemaMembership = z .object({}) - .strict(); // Adding strict so that we can disallow passing in extra fields + .strict(); const withValidMembership = withValidation({ schema: schemaMembership, type: "Zod", diff --git a/pages/api/attendees/[id]/delete.ts b/pages/api/attendees/[id]/delete.ts index 47b4a783b7..21c0efaff4 100644 --- a/pages/api/attendees/[id]/delete.ts +++ b/pages/api/attendees/[id]/delete.ts @@ -21,7 +21,7 @@ export async function attendee(req: NextApiRequest, res: NextApiResponse Date: Mon, 28 Mar 2022 02:51:40 +0200 Subject: [PATCH 5/7] Adds basic api-key auth in users, need to extract out --- lib/utils/stringifyISODate.ts | 5 +---- pages/_middleware.ts | 12 ++++++++++++ pages/api/users/index.ts | 31 ++++++++++++++++++++++++------- pages/api/users/new.ts | 12 ++++-------- tsconfig.json | 3 ++- 5 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 pages/_middleware.ts diff --git a/lib/utils/stringifyISODate.ts b/lib/utils/stringifyISODate.ts index bb2ec71339..17be60bed7 100644 --- a/lib/utils/stringifyISODate.ts +++ b/lib/utils/stringifyISODate.ts @@ -1,7 +1,4 @@ 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] => { - return [key, typeof value === "object" && value instanceof Date ? stringifyISODate(value) : value] -} \ No newline at end of file +// TODO: create a function that takes an object and returns a stringified version of dates of it. \ No newline at end of file diff --git a/pages/_middleware.ts b/pages/_middleware.ts new file mode 100644 index 0000000000..d79b68edb6 --- /dev/null +++ b/pages/_middleware.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from 'next/server' +// Not much useful yet as prisma.client can't be used in the middlewares (client is not available) +// For now we just throw early if no apiKey is passed, +// but we could also check if the apiKey is valid if we had prisma here. +export async function middleware({ nextUrl }: NextRequest, res: NextResponse) { + const response = NextResponse.next() + const apiKey = nextUrl.searchParams.get('apiKey'); + + if (apiKey) return response + // if no apiKey is passed, we throw early + else throw new Error('You need to pass an apiKey as query param: https://api.cal.com/resource?apiKey=') +} diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 33afee9f5e..dd3665cf14 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -7,13 +7,30 @@ type ResponseData = { data?: User[]; error?: unknown; }; +const dateInPast = function (firstDate: Date, secondDate: Date) { + if (firstDate.setHours(0, 0, 0, 0) <= secondDate.setHours(0, 0, 0, 0)) { + return true; + } + + return false; +}; +const today = new Date(); export default async function user(req: NextApiRequest, res: NextApiResponse) { - try { - const users = await prisma.user.findMany(); - res.status(200).json({ data: { ...users } }); - } catch (error) { - // FIXME: Add zod for validation/error handling - res.status(400).json({ error: error }); - } + const apiKey = req.query.apiKey as string; + const apiInDb = await prisma.apiKey.findUnique({ where: { id: apiKey } }); + if (!apiInDb) throw new Error('API key not found'); + const { expiresAt } = apiInDb; + // if (!apiInDb) res.status(400).json({ error: 'Your api key is not valid' }); + if (expiresAt && dateInPast(expiresAt, today)) { + console.log(apiInDb) + try { + const users = await prisma.user.findMany(); + res.status(200).json({ data: { ...users } }); + } catch (error) { + // FIXME: Add zod for validation/error handling + res.status(400).json({ error: error }); + } + } else res.status(400).json({ error: 'Your api key is not valid' }); + } diff --git a/pages/api/users/new.ts b/pages/api/users/new.ts index 4eba163f6b..ae298fdfb3 100644 --- a/pages/api/users/new.ts +++ b/pages/api/users/new.ts @@ -13,18 +13,14 @@ type ResponseData = { async function createUser(req: NextApiRequest, res: NextApiResponse) { const { body, method } = req; - if (method === "POST") { - const safe = schemaUser.safeParse(body); - if (safe.success && safe.data) { + const safe = schemaUser.safeParse(body); + if (method === "POST" && safe.success) { await prisma.user .create({ data: safe.data }) .then((user) => res.status(201).json({ data: user })) .catch((error) => res.status(400).json({ message: "Could not create user type", error: error })); - } - } else { - // Reject any other HTTP method than POST - res.status(405).json({ error: "Only POST Method allowed" }); - } + // Reject any other HTTP method than POST + } else res.status(405).json({ error: "Only POST Method allowed" }); } export default withValidUser(createUser); diff --git a/tsconfig.json b/tsconfig.json index fd13c250c3..93bbf8be81 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,8 @@ "jsx": "preserve", "paths": { "@api/*": ["pages/api/*"], - "@lib/*": ["lib/*"] + "@lib/*": ["lib/*"], + "@/*": ["*"] }, }, From 1241ae6cfc4adfaa1ef474a15b9b84d95a0930dc Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Mon, 28 Mar 2022 16:05:00 +0200 Subject: [PATCH 6/7] feat: move findAll to return arrays --- pages/api/api-keys/index.ts | 4 ++-- pages/api/attendees/index.ts | 4 ++-- pages/api/availabilities/index.ts | 4 ++-- pages/api/bookings/index.ts | 4 ++-- pages/api/event-types/index.ts | 4 ++-- pages/api/teams/index.ts | 4 ++-- pages/api/users/index.ts | 5 ++--- tests/api-keys/[id]/api-key.id.delete.test.ts | 2 -- 8 files changed, 14 insertions(+), 17 deletions(-) diff --git a/pages/api/api-keys/index.ts b/pages/api/api-keys/index.ts index 37a477f1bf..5155c36c59 100644 --- a/pages/api/api-keys/index.ts +++ b/pages/api/api-keys/index.ts @@ -12,7 +12,7 @@ type ResponseData = { export default async function apiKeys(req: NextApiRequest, res: NextApiResponse) { const { method } = req; if (method === "GET") { - const apiKeys = await prisma.apiKey.findMany({}); - res.status(200).json({ data: { ...apiKeys } }); + const data = await prisma.apiKey.findMany({}); + res.status(200).json({ data }); } else res.status(405).json({ message: "Only GET Method allowed" }); } diff --git a/pages/api/attendees/index.ts b/pages/api/attendees/index.ts index 121457759f..1a105dd52d 100644 --- a/pages/api/attendees/index.ts +++ b/pages/api/attendees/index.ts @@ -10,8 +10,8 @@ type ResponseData = { export default async function attendee(req: NextApiRequest, res: NextApiResponse) { try { - const attendees = await prisma.attendee.findMany(); - res.status(200).json({ data: { ...attendees } }); + const data = await prisma.attendee.findMany(); + res.status(200).json({ data }); } catch (error) { // FIXME: Add zod for validation/error handling res.status(400).json({ error: error }); diff --git a/pages/api/availabilities/index.ts b/pages/api/availabilities/index.ts index f529fd6ed2..79f591c06b 100644 --- a/pages/api/availabilities/index.ts +++ b/pages/api/availabilities/index.ts @@ -10,8 +10,8 @@ type ResponseData = { export default async function availability(req: NextApiRequest, res: NextApiResponse) { try { - const availabilities = await prisma.availability.findMany(); - res.status(200).json({ data: { ...availabilities } }); + const data = await prisma.availability.findMany(); + res.status(200).json({ data }); } catch (error) { // FIXME: Add zod for validation/error handling res.status(400).json({ error: error }); diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index 20b9a41a7e..edfc31f1c1 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -10,8 +10,8 @@ type ResponseData = { export default async function booking(req: NextApiRequest, res: NextApiResponse) { try { - const bookings = await prisma.booking.findMany(); - res.status(200).json({ data: { ...bookings } }); + const data = await prisma.booking.findMany(); + res.status(200).json({ data }); } catch (error) { // FIXME: Add zod for validation/error handling res.status(400).json({ error: error }); diff --git a/pages/api/event-types/index.ts b/pages/api/event-types/index.ts index 534ebf828f..348d6aca30 100644 --- a/pages/api/event-types/index.ts +++ b/pages/api/event-types/index.ts @@ -12,8 +12,8 @@ type ResponseData = { export default async function eventType(req: NextApiRequest, res: NextApiResponse) { const { method } = req; if (method === "GET") { - const eventTypes = await prisma.eventType.findMany(); - res.status(200).json({ data: { ...eventTypes } }); + const data = await prisma.eventType.findMany(); + res.status(200).json({ data }); } else { // Reject any other HTTP method than POST res.status(405).json({ message: "Only GET Method allowed" }); diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index d00f092c99..0b57f21604 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -10,8 +10,8 @@ type ResponseData = { export default async function team(req: NextApiRequest, res: NextApiResponse) { try { - const teams = await prisma.team.findMany(); - res.status(200).json({ data: { ...teams } }); + const data = await prisma.team.findMany(); + res.status(200).json({ data }); } catch (error) { // FIXME: Add zod for validation/error handling res.status(400).json({ error: error }); diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index dd3665cf14..1644c3906b 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -23,10 +23,9 @@ export default async function user(req: NextApiRequest, res: NextApiResponse Date: Mon, 28 Mar 2022 16:05:50 +0200 Subject: [PATCH 7/7] remove unused req --- pages/_middleware.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pages/_middleware.ts b/pages/_middleware.ts index d79b68edb6..c631934a91 100644 --- a/pages/_middleware.ts +++ b/pages/_middleware.ts @@ -1,12 +1,16 @@ -import { NextRequest, NextResponse } from 'next/server' -// Not much useful yet as prisma.client can't be used in the middlewares (client is not available) -// For now we just throw early if no apiKey is passed, -// but we could also check if the apiKey is valid if we had prisma here. -export async function middleware({ nextUrl }: NextRequest, res: NextResponse) { - const response = NextResponse.next() - const apiKey = nextUrl.searchParams.get('apiKey'); +import { NextRequest, NextResponse } from "next/server"; - if (apiKey) return response +// Not much useful yet as prisma.client can't be used in the middlewares (client is not available) +// For now we just throw early if no apiKey is passed, +// but we could also check if the apiKey is valid if we had prisma here. +export async function middleware({ nextUrl }: NextRequest) { + const response = NextResponse.next(); + const apiKey = nextUrl.searchParams.get("apiKey"); + + if (apiKey) return response; // if no apiKey is passed, we throw early - else throw new Error('You need to pass an apiKey as query param: https://api.cal.com/resource?apiKey=') + else + throw new Error( + "You need to pass an apiKey as query param: https://api.cal.com/resource?apiKey=" + ); }