diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx
index 3862480656..95a814a987 100644
--- a/apps/web/components/booking/pages/BookingPage.tsx
+++ b/apps/web/components/booking/pages/BookingPage.tsx
@@ -826,22 +826,8 @@ const BookingPage = ({
- {mutation.isError && (
-
-
-
-
-
-
-
- {rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
- {(mutation.error as HttpError)?.message}
-
-
-
-
+ {(mutation.isError || recurringMutation.isError) && (
+
)}
@@ -853,3 +839,24 @@ const BookingPage = ({
};
export default BookingPage;
+
+function ErrorMessage({ error }: { error: unknown }) {
+ const { t } = useLocale();
+ const { query: { rescheduleUid } = {} } = useRouter();
+
+ return (
+
+
+
+
+
+
+
+ {rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
+ {error instanceof HttpError || error instanceof Error ? error.message : "Unknown error"}
+
+
+
+
+ );
+}
diff --git a/apps/web/lib/mutations/event-types/create-event-type.ts b/apps/web/lib/mutations/event-types/create-event-type.ts
deleted file mode 100644
index fb3deaecab..0000000000
--- a/apps/web/lib/mutations/event-types/create-event-type.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as fetch from "@lib/core/http/fetch-wrapper";
-import { CreateEventType, CreateEventTypeResponse } from "@lib/types/event-type";
-
-/**
- * @deprecated Use `trpc.useMutation("viewer.eventTypes.create")` instead.
- */
-const createEventType = async (data: CreateEventType) => {
- const response = await fetch.post(
- "/api/availability/eventtype",
- data
- );
- return response;
-};
-
-export default createEventType;
diff --git a/apps/web/lib/mutations/event-types/delete-event-type.ts b/apps/web/lib/mutations/event-types/delete-event-type.ts
deleted file mode 100644
index 82f77c45df..0000000000
--- a/apps/web/lib/mutations/event-types/delete-event-type.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as fetch from "@lib/core/http/fetch-wrapper";
-
-/**
- * @deprecated Use `trpc.useMutation("viewer.eventTypes.delete")` instead.
- */
-const deleteEventType = async (data: { id: number }) => {
- const response = await fetch.remove<{ id: number }, Record>(
- "/api/availability/eventtype",
- data
- );
- return response;
-};
-
-export default deleteEventType;
diff --git a/apps/web/lib/mutations/event-types/update-event-type.ts b/apps/web/lib/mutations/event-types/update-event-type.ts
deleted file mode 100644
index 02deaf5ca7..0000000000
--- a/apps/web/lib/mutations/event-types/update-event-type.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { EventType } from "@prisma/client";
-
-import * as fetch from "@lib/core/http/fetch-wrapper";
-import { EventTypeInput } from "@lib/types/event-type";
-
-type EventTypeResponse = {
- eventType: EventType;
-};
-
-/**
- * @deprecated Use `trpc.useMutation("viewer.eventTypes.update")` instead.
- */
-const updateEventType = async (data: EventTypeInput) => {
- const response = await fetch.patch("/api/availability/eventtype", data);
- return response;
-};
-
-export default updateEventType;
diff --git a/apps/web/lib/queries/availability/index.ts b/apps/web/lib/queries/availability/index.ts
deleted file mode 100644
index a3c88c0dec..0000000000
--- a/apps/web/lib/queries/availability/index.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Prisma } from "@prisma/client";
-import dayjs from "dayjs";
-
-import { asStringOrNull } from "@lib/asStringOrNull";
-import { getWorkingHours } from "@lib/availability";
-import getBusyTimes from "@lib/getBusyTimes";
-import prisma from "@lib/prisma";
-
-export async function getUserAvailability(query: {
- username: string;
- dateFrom: string;
- dateTo: string;
- eventTypeId?: number;
- timezone?: string;
-}) {
- const username = asStringOrNull(query.username);
- const dateFrom = dayjs(asStringOrNull(query.dateFrom));
- const dateTo = dayjs(asStringOrNull(query.dateTo));
-
- if (!username) throw new Error("Missing username");
- if (!dateFrom.isValid() || !dateTo.isValid()) throw new Error("Invalid time range given.");
-
- const rawUser = await prisma.user.findUnique({
- where: {
- username: username,
- },
- select: {
- credentials: true,
- timeZone: true,
- bufferTime: true,
- availability: true,
- id: true,
- startTime: true,
- endTime: true,
- selectedCalendars: true,
- },
- });
-
- const getEventType = (id: number) =>
- prisma.eventType.findUnique({
- where: { id },
- select: {
- timeZone: true,
- availability: {
- select: {
- startTime: true,
- endTime: true,
- days: true,
- },
- },
- },
- });
-
- type EventType = Prisma.PromiseReturnType;
- let eventType: EventType | null = null;
- if (query.eventTypeId) eventType = await getEventType(query.eventTypeId);
-
- if (!rawUser) throw new Error("No user found");
-
- const { selectedCalendars, ...currentUser } = rawUser;
-
- const busyTimes = await getBusyTimes({
- credentials: currentUser.credentials,
- startTime: dateFrom.format(),
- endTime: dateTo.format(),
- eventTypeId: query.eventTypeId,
- userId: currentUser.id,
- selectedCalendars,
- });
-
- const bufferedBusyTimes = busyTimes.map((a) => ({
- start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
- end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
- }));
-
- const timeZone = query.timezone || eventType?.timeZone || currentUser.timeZone;
- const workingHours = getWorkingHours(
- { timeZone },
- eventType?.availability.length ? eventType.availability : currentUser.availability
- );
-
- return {
- busy: bufferedBusyTimes,
- timeZone,
- workingHours,
- };
-}
diff --git a/apps/web/modules/common/api/defaultResponder.ts b/apps/web/modules/common/api/defaultResponder.ts
deleted file mode 100644
index f230928d41..0000000000
--- a/apps/web/modules/common/api/defaultResponder.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { NextApiRequest, NextApiResponse } from "next";
-import Stripe from "stripe";
-import { ZodError } from "zod";
-
-import { HttpError } from "@calcom/lib/http-error";
-
-type Handle = (req: NextApiRequest, res: NextApiResponse) => Promise;
-
-/** Allows us to get type inference from API handler responses */
-function defaultResponder(f: Handle) {
- return async (req: NextApiRequest, res: NextApiResponse) => {
- try {
- const result = await f(req, res);
- res.json(result);
- } catch (err) {
- if (err instanceof HttpError) {
- res.statusCode = err.statusCode;
- res.json({ message: err.message });
- } else if (err instanceof Stripe.errors.StripeInvalidRequestError) {
- console.error("err", err);
- res.statusCode = err.statusCode || 500;
- res.json({ message: "Stripe error: " + err.message });
- } else if (err instanceof ZodError && err.name === "ZodError") {
- console.error("err", JSON.parse(err.message)[0].message);
- res.statusCode = 400;
- res.json({
- message: "Validation errors: " + err.issues.map((i) => `—'${i.path}' ${i.message}`).join(". "),
- });
- } else {
- console.error("err", err);
- res.statusCode = 500;
- res.json({ message: "Unknown error" });
- }
- }
- };
-}
-
-export default defaultResponder;
diff --git a/apps/web/modules/common/api/index.ts b/apps/web/modules/common/api/index.ts
deleted file mode 100644
index db67b1334c..0000000000
--- a/apps/web/modules/common/api/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as defaultHandler } from "./defaultHandler";
-export { default as defaultResponder } from "./defaultResponder";
diff --git a/apps/web/modules/common/index.ts b/apps/web/modules/common/index.ts
deleted file mode 100644
index d158c57640..0000000000
--- a/apps/web/modules/common/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./api";
diff --git a/apps/web/pages/api/availability/[user].ts b/apps/web/pages/api/availability/[user].ts
index e9ecc51068..1074b7b8d1 100644
--- a/apps/web/pages/api/availability/[user].ts
+++ b/apps/web/pages/api/availability/[user].ts
@@ -1,141 +1,25 @@
-import { Prisma } from "@prisma/client";
-import dayjs from "dayjs";
-import timezone from "dayjs/plugin/timezone";
-import utc from "dayjs/plugin/utc";
-import type { NextApiRequest, NextApiResponse } from "next";
+import type { NextApiRequest } from "next";
+import { z } from "zod";
-import { asStringOrNull } from "@lib/asStringOrNull";
-import { getWorkingHours } from "@lib/availability";
-import getBusyTimes from "@lib/getBusyTimes";
-import prisma from "@lib/prisma";
+import { getUserAvailability } from "@calcom/core/getUserAvailability";
+import { defaultResponder } from "@calcom/lib/server";
+import { stringOrNumber } from "@calcom/prisma/zod-utils";
-dayjs.extend(utc);
-dayjs.extend(timezone);
+const availabilitySchema = z.object({
+ user: z.string(),
+ dateFrom: z.string(),
+ dateTo: z.string(),
+ eventTypeId: stringOrNumber.optional(),
+});
-export default async function handler(req: NextApiRequest, res: NextApiResponse) {
- const user = asStringOrNull(req.query.user);
- const dateFrom = dayjs(asStringOrNull(req.query.dateFrom));
- const dateTo = dayjs(asStringOrNull(req.query.dateTo));
- const eventTypeId = typeof req.query.eventTypeId === "string" ? parseInt(req.query.eventTypeId) : undefined;
-
- if (!dateFrom.isValid() || !dateTo.isValid()) {
- return res.status(400).json({ message: "Invalid time range given." });
- }
-
- const rawUser = await prisma.user.findUnique({
- where: {
- username: user as string,
- },
- select: {
- credentials: true,
- timeZone: true,
- bufferTime: true,
- availability: true,
- id: true,
- startTime: true,
- endTime: true,
- selectedCalendars: true,
- schedules: {
- select: {
- availability: true,
- timeZone: true,
- id: true,
- },
- },
- defaultScheduleId: true,
- },
- });
-
- const getEventType = (id: number) =>
- prisma.eventType.findUnique({
- where: { id },
- select: {
- seatsPerTimeSlot: true,
- timeZone: true,
- schedule: {
- select: {
- availability: true,
- timeZone: true,
- },
- },
- availability: {
- select: {
- startTime: true,
- endTime: true,
- days: true,
- },
- },
- },
- });
-
- type EventType = Prisma.PromiseReturnType;
- let eventType: EventType | null = null;
- if (eventTypeId) eventType = await getEventType(eventTypeId);
-
- if (!rawUser) throw new Error("No user found");
-
- const { selectedCalendars, ...currentUser } = rawUser;
-
- const busyTimes = await getBusyTimes({
- credentials: currentUser.credentials,
- startTime: dateFrom.format(),
- endTime: dateTo.format(),
+async function handler(req: NextApiRequest) {
+ const { user: username, eventTypeId, dateTo, dateFrom } = availabilitySchema.parse(req.query);
+ return getUserAvailability({
+ username,
+ dateFrom,
+ dateTo,
eventTypeId,
- userId: currentUser.id,
- selectedCalendars,
- });
-
- const bufferedBusyTimes = busyTimes.map((a) => ({
- start: dayjs(a.start).subtract(currentUser.bufferTime, "minute"),
- end: dayjs(a.end).add(currentUser.bufferTime, "minute"),
- }));
-
- const schedule = eventType?.schedule
- ? { ...eventType?.schedule }
- : {
- ...currentUser.schedules.filter(
- (schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
- )[0],
- };
-
- const timeZone = schedule.timeZone || eventType?.timeZone || currentUser.timeZone;
-
- const workingHours = getWorkingHours(
- {
- timeZone,
- },
- schedule.availability ||
- (eventType?.availability.length ? eventType.availability : currentUser.availability)
- );
-
- /* Current logic is if a booking is in a time slot mark it as busy, but seats can have more than one attendee so grab
- current bookings with a seats event type and display them on the calendar, even if they are full */
- let currentSeats;
- if (eventType?.seatsPerTimeSlot) {
- currentSeats = await prisma.booking.findMany({
- where: {
- eventTypeId: eventTypeId,
- startTime: {
- gte: dateFrom.format(),
- lte: dateTo.format(),
- },
- },
- select: {
- uid: true,
- startTime: true,
- _count: {
- select: {
- attendees: true,
- },
- },
- },
- });
- }
-
- res.status(200).json({
- busy: bufferedBusyTimes,
- timeZone,
- workingHours,
- currentSeats,
});
}
+
+export default defaultResponder(handler);
diff --git a/apps/web/pages/api/book/confirm.ts b/apps/web/pages/api/book/confirm.ts
index d40a5b2680..02cf8334da 100644
--- a/apps/web/pages/api/book/confirm.ts
+++ b/apps/web/pages/api/book/confirm.ts
@@ -6,6 +6,7 @@ import EventManager from "@calcom/core/EventManager";
import { sendDeclinedEmails, sendScheduledEmails } from "@calcom/emails";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import logger from "@calcom/lib/logger";
+import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
@@ -15,8 +16,6 @@ import { HttpError } from "@lib/core/http/error";
import { getTranslation } from "@server/lib/i18n";
-import { defaultHandler, defaultResponder } from "~/common";
-
const authorized = async (
currentUser: Pick,
booking: Pick
diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts
index 774d040f53..a56ca2e051 100644
--- a/apps/web/pages/api/book/event.ts
+++ b/apps/web/pages/api/book/event.ts
@@ -5,33 +5,35 @@ import dayjsBusinessTime from "dayjs-business-days2";
import isBetween from "dayjs/plugin/isBetween";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
-import type { NextApiRequest, NextApiResponse } from "next";
+import type { NextApiRequest } from "next";
import rrule from "rrule";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import EventManager from "@calcom/core/EventManager";
+import { getUserAvailability } from "@calcom/core/getUserAvailability";
import {
sendAttendeeRequestEmail,
sendOrganizerRequestEmail,
sendRescheduledEmails,
sendScheduledEmails,
} from "@calcom/emails";
-import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
+import { getLuckyUsers, isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
+import { defaultResponder } from "@calcom/lib/server";
+import prisma, { userSelect } from "@calcom/prisma";
+import { extendedBookingCreateBody } from "@calcom/prisma/zod-utils";
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import { handlePayment } from "@ee/lib/stripe/server";
+import { HttpError } from "@lib/core/http/error";
import { ensureArray } from "@lib/ensureArray";
import { getEventName } from "@lib/event";
-import getBusyTimes from "@lib/getBusyTimes";
import isOutOfBounds from "@lib/isOutOfBounds";
-import prisma from "@lib/prisma";
-import { BookingCreateBody } from "@lib/types/booking";
import sendPayload from "@lib/webhooks/sendPayload";
import getSubscribers from "@lib/webhooks/subscriptions";
@@ -81,45 +83,38 @@ function isAvailable(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType, lengt
// Check for conflicts
let t = true;
- if (Array.isArray(busyTimes) && busyTimes.length > 0) {
- busyTimes.forEach((busyTime) => {
- const startTime = dayjs(busyTime.start);
- const endTime = dayjs(busyTime.end);
+ // Early return
+ if (!Array.isArray(busyTimes) || busyTimes.length < 1) return t;
- // Check if time is between start and end times
- if (dayjs(time).isBetween(startTime, endTime, null, "[)")) {
- t = false;
- }
+ let i = 0;
+ while (t === true && i < busyTimes.length) {
+ const busyTime = busyTimes[i];
+ i++;
+ const startTime = dayjs(busyTime.start);
+ const endTime = dayjs(busyTime.end);
- // Check if slot end time is between start and end time
- if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
- t = false;
- }
+ // Check if time is between start and end times
+ if (dayjs(time).isBetween(startTime, endTime, null, "[)")) {
+ t = false;
+ break;
+ }
- // Check if startTime is between slot
- if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) {
- t = false;
- }
- });
+ // Check if slot end time is between start and end time
+ if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
+ t = false;
+ break;
+ }
+
+ // Check if startTime is between slot
+ if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) {
+ t = false;
+ break;
+ }
}
return t;
}
-const userSelect = Prisma.validator()({
- select: {
- id: true,
- email: true,
- name: true,
- username: true,
- timeZone: true,
- credentials: true,
- bufferTime: true,
- destinationCalendar: true,
- locale: true,
- },
-});
-
const getUserNameWithBookingCounts = async (eventTypeId: number, selectedUserNames: string[]) => {
const users = await prisma.user.findMany({
where: {
@@ -191,6 +186,20 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
seatsPerTimeSlot: true,
recurringEvent: true,
locations: true,
+ timeZone: true,
+ schedule: {
+ select: {
+ availability: true,
+ timeZone: true,
+ },
+ },
+ availability: {
+ select: {
+ startTime: true,
+ endTime: true,
+ days: true,
+ },
+ },
},
});
@@ -202,23 +211,15 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
type User = Prisma.UserGetPayload;
-type ExtendedBookingCreateBody = BookingCreateBody & {
- noEmail?: boolean;
- recurringCount?: number;
- rescheduleReason?: string;
-};
-
-export default async function handler(req: NextApiRequest, res: NextApiResponse) {
- const { recurringCount, noEmail, ...reqBody } = req.body as ExtendedBookingCreateBody;
+async function handler(req: NextApiRequest) {
+ const { recurringCount, noEmail, eventTypeSlug, eventTypeId, hasHashedBookingLink, language, ...reqBody } =
+ extendedBookingCreateBody.parse(req.body);
// handle dynamic user
const dynamicUserList = Array.isArray(reqBody.user)
- ? getGroupName(req.body.user)
- : getUsernameList(reqBody.user as string);
- const hasHashedBookingLink = reqBody.hasHashedBookingLink;
- const eventTypeSlug = reqBody.eventTypeSlug;
- const eventTypeId = reqBody.eventTypeId;
- const tAttendees = await getTranslation(reqBody.language ?? "en", "common");
+ ? getGroupName(reqBody.user)
+ : getUsernameList(reqBody.user);
+ const tAttendees = await getTranslation(language ?? "en", "common");
const tGuests = await getTranslation("en", "common");
log.debug(`Booking eventType ${eventTypeId} started`);
@@ -233,11 +234,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
};
log.error(`Booking ${eventTypeId} failed`, error);
- return res.status(400).json(error);
+ throw new HttpError({ statusCode: 400, message: error.message });
}
const eventType = !eventTypeId ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId);
- if (!eventType) return res.status(404).json({ message: "eventType.notFound" });
+ if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
let users = !eventTypeId
? await prisma.user.findMany({
@@ -258,7 +259,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
...userSelect,
});
- if (!eventTypeUser) return res.status(404).json({ message: "eventTypeUser.notFound" });
+ if (!eventTypeUser) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
users.push(eventTypeUser);
}
const [organizerUser] = users;
@@ -287,23 +288,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
email: reqBody.email,
name: reqBody.name,
timeZone: reqBody.timeZone,
- language: { translate: tAttendees, locale: reqBody.language ?? "en" },
+ language: { translate: tAttendees, locale: language ?? "en" },
},
];
- const guests = (reqBody.guests || []).map((guest) => {
- const g = {
- email: guest,
- name: "",
- timeZone: reqBody.timeZone,
- language: { translate: tGuests, locale: "en" },
- };
- return g;
- });
+ const guests = (reqBody.guests || []).map((guest) => ({
+ email: guest,
+ name: "",
+ timeZone: reqBody.timeZone,
+ language: { translate: tGuests, locale: "en" },
+ }));
// For seats, if the booking already exists then we want to add the new attendee to the existing booking
if (reqBody.bookingUid) {
if (!eventType.seatsPerTimeSlot)
- return res.status(404).json({ message: "Event type does not have seats" });
+ throw new HttpError({ statusCode: 404, message: "Event type does not have seats" });
const booking = await prisma.booking.findUnique({
where: {
@@ -313,13 +311,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
attendees: true,
},
});
- if (!booking) return res.status(404).json({ message: "Booking not found" });
+ if (!booking) throw new HttpError({ statusCode: 404, message: "Booking not found" });
if (eventType.seatsPerTimeSlot <= booking.attendees.length)
- return res.status(409).json({ message: "Booking seats are full" });
+ throw new HttpError({ statusCode: 409, message: "Booking seats are full" });
if (booking.attendees.some((attendee) => attendee.email === invitee[0].email))
- return res.status(409).json({ message: "Already signed up for time slot" });
+ throw new HttpError({ statusCode: 409, message: "Already signed up for time slot" });
await prisma.booking.update({
where: {
@@ -336,7 +334,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
},
});
- return res.status(201).json(booking);
+ req.statusCode = 201;
+ return booking;
}
const teamMemberPromises =
@@ -358,7 +357,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const attendeesList = [...invitee, ...guests, ...teamMembers];
- const seed = `${organizerUser.username}:${dayjs(req.body.start).utc().format()}:${new Date().getTime()}`;
+ const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`;
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
const location = !!eventType.locations ? (eventType.locations as Array<{ type: string }>)[0] : "";
@@ -456,8 +455,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
async function createBooking() {
// @TODO: check as metadata
- if (req.body.web3Details) {
- const { web3Details } = req.body;
+ if (reqBody.web3Details) {
+ const { web3Details } = reqBody;
await verifyAccount(web3Details.userSignature, web3Details.userWallet);
}
@@ -477,7 +476,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null;
const dynamicGroupSlugRef = !eventTypeId ? (reqBody.user as string).toLowerCase() : null;
-
const isConfirmedByDefault = (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid;
const newBookingData: Prisma.BookingCreateInput = {
uid,
@@ -548,25 +546,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
- /* Validate if there is any stripe_payment credential for this user */
- const stripePaymentCredential = await prisma.credential.findFirst({
- where: {
- type: "stripe_payment",
- userId: organizerUser.id,
- },
- select: {
- id: true,
- },
- });
- /** eventType doesn’t require payment then we create a booking
- * OR
- * stripePaymentCredential is found and price is higher than 0 then we create a booking
- */
- if (!eventType.price || (stripePaymentCredential && eventType.price > 0)) {
- return prisma.booking.create(createBookingObj);
+ if (typeof eventType.price === "number" && eventType.price > 0) {
+ /* Validate if there is any stripe_payment credential for this user */
+ await prisma.credential.findFirst({
+ rejectOnNotFound(err) {
+ throw new HttpError({ statusCode: 400, message: "Missing stripe credentials", cause: err });
+ },
+ where: {
+ type: "stripe_payment",
+ userId: organizerUser.id,
+ },
+ select: {
+ id: true,
+ },
+ });
}
- // stripePaymentCredential not found and eventType requires payment we return null
- return null;
+
+ return prisma.booking.create(createBookingObj);
}
let results: EventResult[] = [];
@@ -577,31 +573,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
for (const currentUser of users) {
if (!currentUser) {
console.error(`currentUser not found`);
- return;
+ continue;
}
if (!user) user = currentUser;
- const selectedCalendars = await prisma.selectedCalendar.findMany({
- where: {
+ const { busy: bufferedBusyTimes } = await getUserAvailability(
+ {
userId: currentUser.id,
+ dateFrom: reqBody.start,
+ dateTo: reqBody.end,
+ eventTypeId,
},
- });
+ { user, eventType }
+ );
- const busyTimes = await getBusyTimes({
- credentials: currentUser.credentials,
- startTime: reqBody.start,
- endTime: reqBody.end,
- eventTypeId,
- userId: currentUser.id,
- selectedCalendars,
- });
-
- console.log("calendarBusyTimes==>>>", busyTimes);
-
- const bufferedBusyTimes = busyTimes.map((a) => ({
- start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
- end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
- }));
+ console.log("calendarBusyTimes==>>>", bufferedBusyTimes);
let isAvailableToBeBooked = true;
try {
@@ -609,9 +595,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const recurringEvent = parseRecurringEvent(eventType.recurringEvent);
const allBookingDates = new rrule({ dtstart: new Date(reqBody.start), ...recurringEvent }).all();
// Go through each date for the recurring event and check if each one's availability
- isAvailableToBeBooked = allBookingDates
- .map((aDate) => isAvailable(bufferedBusyTimes, aDate, eventType.length)) // <-- array of booleans
- .reduce((acc, value) => acc && value, true); // <-- checks boolean array applying "AND" to each value and the current one, starting in true
+ // DONE: Decreased computational complexity from O(2^n) to O(n) by refactoring this loop to stop
+ // running at the first unavailable time.
+ let i = 0;
+ while (isAvailableToBeBooked === true && i < allBookingDates.length) {
+ const aDate = allBookingDates[i];
+ i++;
+ isAvailableToBeBooked = isAvailable(bufferedBusyTimes, aDate, eventType.length);
+ /* We bail at the first false, we don't need to keep checking */
+ if (!isAvailableToBeBooked) break;
+ }
} else {
isAvailableToBeBooked = isAvailable(bufferedBusyTimes, reqBody.start, eventType.length);
}
@@ -628,8 +621,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
};
log.debug(`Booking ${currentUser.name} failed`, error);
- res.status(409).json(error);
- return;
+ throw new HttpError({ statusCode: 409, message: error.message });
}
let timeOutOfBounds = false;
@@ -655,8 +647,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
};
log.debug(`Booking ${currentUser.name} failed`, error);
- res.status(400).json(error);
- return;
+ throw new HttpError({ statusCode: 409, message: error.message });
}
}
@@ -669,14 +660,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const err = getErrorFromUnknown(_err);
log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
if (err.code === "P2002") {
- res.status(409).json({ message: "booking.conflict" });
- return;
+ throw new HttpError({ statusCode: 409, message: "booking.conflict" });
}
- res.status(500).end();
- return;
+ throw err;
}
- if (!user) throw Error("Can't continue, user not found.");
+ if (!user) throw new HttpError({ statusCode: 404, message: "Can't continue, user not found." });
// After polling videoBusyTimes, credentials might have been changed due to refreshment, so query them again.
const credentials = await refreshCredentials(user.credentials);
@@ -777,21 +766,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
!originalRescheduledBooking?.paid &&
!!booking
) {
- try {
- const [firstStripeCredential] = user.credentials.filter((cred) => cred.type == "stripe_payment");
+ const [firstStripeCredential] = user.credentials.filter((cred) => cred.type == "stripe_payment");
- if (!firstStripeCredential) return res.status(500).json({ message: "Missing payment credentials" });
+ if (!firstStripeCredential)
+ throw new HttpError({ statusCode: 400, message: "Missing payment credentials" });
- if (!booking.user) booking.user = user;
- const payment = await handlePayment(evt, eventType, firstStripeCredential, booking);
+ if (!booking.user) booking.user = user;
+ const payment = await handlePayment(evt, eventType, firstStripeCredential, booking);
- res.status(201).json({ ...booking, message: "Payment required", paymentUid: payment.uid });
- return;
- } catch (e) {
- log.error(`Creating payment failed`, e);
- res.status(500).json({ message: "Payment Failed" });
- return;
- }
+ req.statusCode = 201;
+ return { ...booking, message: "Payment required", paymentUid: payment.uid };
}
log.debug(`Booking ${user.username} completed`);
@@ -821,7 +805,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await Promise.all(promises);
// Avoid passing referencesToCreate with id unique constrain values
// refresh hashed link if used
- const urlSeed = `${organizerUser.username}:${dayjs(req.body.start).utc().format()}`;
+ const urlSeed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}`;
const hashedUid = translator.fromUUID(uuidv5(urlSeed, uuidv5.URL));
if (hasHashedBookingLink) {
@@ -834,32 +818,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
}
- if (booking) {
- await prisma.booking.update({
- where: {
- uid: booking.uid,
- },
- data: {
- references: {
- createMany: {
- data: referencesToCreate,
- },
+ if (!booking) throw new HttpError({ statusCode: 400, message: "Booking failed" });
+ await prisma.booking.update({
+ where: {
+ uid: booking.uid,
+ },
+ data: {
+ references: {
+ createMany: {
+ data: referencesToCreate,
},
},
- });
- // booking successful
- return res.status(201).json(booking);
- }
- return res.status(400).json({ message: "There is not a stripe_payment credential" });
+ },
+ });
+ // booking successful
+ req.statusCode = 201;
+ return booking;
}
-export function getLuckyUsers(
- users: User[],
- bookingCounts: Prisma.PromiseReturnType
-) {
- if (!bookingCounts.length) users.slice(0, 1);
-
- const [firstMostAvailableUser] = bookingCounts.sort((a, b) => (a.bookingCount > b.bookingCount ? 1 : -1));
- const luckyUser = users.find((user) => user.username === firstMostAvailableUser?.username);
- return luckyUser ? [luckyUser] : users;
-}
+export default defaultResponder(handler);
diff --git a/apps/web/pages/getting-started.tsx b/apps/web/pages/getting-started.tsx
index 1df4c2a53f..809d37a10b 100644
--- a/apps/web/pages/getting-started.tsx
+++ b/apps/web/pages/getting-started.tsx
@@ -12,7 +12,7 @@ import { NextPageContext } from "next";
import { useSession } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
-import React, { useEffect, useRef, useState, useCallback } from "react";
+import React, { useCallback, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
@@ -120,21 +120,7 @@ export default function Onboarding(props: inferSSRProps {
- const res = await fetch(`/api/availability/eventtype`, {
- method: "POST",
- body: JSON.stringify(data),
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- if (!res.ok) {
- throw new Error((await res.json()).message);
- }
- const responseData = await res.json();
- return responseData.data;
- };
+ const createEventType = trpc.useMutation("viewer.eventTypes.create");
const createSchedule = trpc.useMutation("viewer.availability.schedule.create", {
onError: (err) => {
@@ -229,7 +215,7 @@ export default function Onboarding(props: inferSSRProps {
- return await createEventType(event);
+ return createEventType.mutate(event);
})
);
}
diff --git a/apps/web/server/routers/viewer/teams.tsx b/apps/web/server/routers/viewer/teams.tsx
index f9e9744a07..7a32ae2c98 100644
--- a/apps/web/server/routers/viewer/teams.tsx
+++ b/apps/web/server/routers/viewer/teams.tsx
@@ -2,7 +2,9 @@ import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
import { randomBytes } from "crypto";
import { z } from "zod";
+import { getUserAvailability } from "@calcom/core/getUserAvailability";
import { sendTeamInviteEmail } from "@calcom/emails";
+import { availabilityUserSelect } from "@calcom/prisma";
import {
addSeat,
downgradeTeamMembers,
@@ -13,7 +15,6 @@ import {
} from "@calcom/stripe/team-billing";
import { BASE_URL, HOSTED_CAL_FEATURES } from "@lib/config/constants";
-import { getUserAvailability } from "@lib/queries/availability";
import { getTeamWithMembers, isTeamAdmin, isTeamOwner } from "@lib/queries/teams";
import slugify from "@lib/slugify";
@@ -408,7 +409,14 @@ export const viewerTeamsRouter = createProtectedRouter()
// verify member is in team
const members = await ctx.prisma.membership.findMany({
where: { teamId: input.teamId },
- include: { user: true },
+ include: {
+ user: {
+ select: {
+ username: true,
+ ...availabilityUserSelect,
+ },
+ },
+ },
});
const member = members?.find((m) => m.userId === input.memberId);
if (!member) throw new TRPCError({ code: "NOT_FOUND", message: "Member not found" });
@@ -416,12 +424,15 @@ export const viewerTeamsRouter = createProtectedRouter()
throw new TRPCError({ code: "BAD_REQUEST", message: "Member doesn't have a username" });
// get availability for this member
- return await getUserAvailability({
- username: member.user.username,
- timezone: input.timezone,
- dateFrom: input.dateFrom,
- dateTo: input.dateTo,
- });
+ return await getUserAvailability(
+ {
+ username: member.user.username,
+ timezone: input.timezone,
+ dateFrom: input.dateFrom,
+ dateTo: input.dateTo,
+ },
+ { user: member.user }
+ );
},
})
.mutation("upgradeTeam", {
diff --git a/apps/web/test/lib/team-event-types.test.ts b/apps/web/test/lib/team-event-types.test.ts
index 241574113f..4262ce90c3 100644
--- a/apps/web/test/lib/team-event-types.test.ts
+++ b/apps/web/test/lib/team-event-types.test.ts
@@ -1,6 +1,6 @@
import { UserPlan } from "@prisma/client";
-import { getLuckyUsers } from "../../pages/api/book/event";
+import { getLuckyUsers } from "@calcom/lib";
const baseUser = {
id: 0,
diff --git a/apps/web/lib/getBusyTimes.ts b/packages/core/getBusyTimes.ts
similarity index 87%
rename from apps/web/lib/getBusyTimes.ts
rename to packages/core/getBusyTimes.ts
index 8b14e6582c..3f4d829e77 100644
--- a/apps/web/lib/getBusyTimes.ts
+++ b/packages/core/getBusyTimes.ts
@@ -3,11 +3,10 @@ import { BookingStatus, Credential, SelectedCalendar } from "@prisma/client";
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
import { getBusyVideoTimes } from "@calcom/core/videoClient";
import notEmpty from "@calcom/lib/notEmpty";
+import prisma from "@calcom/prisma";
import type { EventBusyDate } from "@calcom/types/Calendar";
-import prisma from "@lib/prisma";
-
-async function getBusyTimes(params: {
+export async function getBusyTimes(params: {
credentials: Credential[];
userId: number;
eventTypeId?: number;
@@ -32,7 +31,7 @@ async function getBusyTimes(params: {
endTime: true,
},
})
- .then((bookings) => bookings.map((booking) => ({ end: booking.endTime, start: booking.startTime })));
+ .then((bookings) => bookings.map(({ startTime, endTime }) => ({ end: endTime, start: startTime })));
if (credentials) {
const calendarBusyTimes = await getBusyCalendarTimes(credentials, startTime, endTime, selectedCalendars);
diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts
new file mode 100644
index 0000000000..ab9d16cacc
--- /dev/null
+++ b/packages/core/getUserAvailability.ts
@@ -0,0 +1,145 @@
+import { Prisma } from "@prisma/client";
+import dayjs from "dayjs";
+import { z } from "zod";
+
+import { getWorkingHours } from "@calcom/lib/availability";
+import { HttpError } from "@calcom/lib/http-error";
+import prisma, { availabilityUserSelect } from "@calcom/prisma";
+import { stringToDayjs } from "@calcom/prisma/zod-utils";
+
+import { getBusyTimes } from "./getBusyTimes";
+
+const availabilitySchema = z
+ .object({
+ dateFrom: stringToDayjs,
+ dateTo: stringToDayjs,
+ eventTypeId: z.number().optional(),
+ timezone: z.string().optional(),
+ username: z.string().optional(),
+ userId: z.number().optional(),
+ })
+ .refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
+
+const getEventType = (id: number) =>
+ prisma.eventType.findUnique({
+ where: { id },
+ select: {
+ seatsPerTimeSlot: true,
+ timeZone: true,
+ schedule: {
+ select: {
+ availability: true,
+ timeZone: true,
+ },
+ },
+ availability: {
+ select: {
+ startTime: true,
+ endTime: true,
+ days: true,
+ },
+ },
+ },
+ });
+
+type EventType = Awaited>;
+
+const getUser = (where: Prisma.UserWhereUniqueInput) =>
+ prisma.user.findUnique({
+ where,
+ select: availabilityUserSelect,
+ });
+
+type User = Awaited>;
+
+export async function getUserAvailability(
+ query: ({ username: string } | { userId: number }) & {
+ dateFrom: string;
+ dateTo: string;
+ eventTypeId?: number;
+ timezone?: string;
+ },
+ initialData?: {
+ user?: User;
+ eventType?: EventType;
+ }
+) {
+ const { username, userId, dateFrom, dateTo, eventTypeId, timezone } = availabilitySchema.parse(query);
+
+ if (!dateFrom.isValid() || !dateTo.isValid())
+ throw new HttpError({ statusCode: 400, message: "Invalid time range given." });
+
+ const where: Prisma.UserWhereUniqueInput = {};
+ if (username) where.username = username;
+ if (userId) where.id = userId;
+
+ let user: User | null = initialData?.user || null;
+ if (!user) user = await getUser(where);
+ if (!user) throw new HttpError({ statusCode: 404, message: "No user found" });
+
+ let eventType: EventType | null = initialData?.eventType || null;
+ if (!eventType && eventTypeId) eventType = await getEventType(eventTypeId);
+
+ const { selectedCalendars, ...currentUser } = user;
+
+ const busyTimes = await getBusyTimes({
+ credentials: currentUser.credentials,
+ startTime: dateFrom.toISOString(),
+ endTime: dateTo.toISOString(),
+ eventTypeId,
+ userId: currentUser.id,
+ selectedCalendars,
+ });
+
+ const bufferedBusyTimes = busyTimes.map((a) => ({
+ start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toISOString(),
+ end: dayjs(a.end).add(currentUser.bufferTime, "minute").toISOString(),
+ }));
+
+ const timeZone = timezone || eventType?.timeZone || currentUser.timeZone;
+
+ const schedule = eventType?.schedule
+ ? { ...eventType?.schedule }
+ : {
+ ...currentUser.schedules.filter(
+ (schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
+ )[0],
+ };
+
+ const workingHours = getWorkingHours(
+ { timeZone },
+ schedule.availability ||
+ (eventType?.availability.length ? eventType.availability : currentUser.availability)
+ );
+
+ /* Current logic is if a booking is in a time slot mark it as busy, but seats can have more than one attendee so grab
+ current bookings with a seats event type and display them on the calendar, even if they are full */
+ let currentSeats;
+ if (eventType?.seatsPerTimeSlot) {
+ currentSeats = await prisma.booking.findMany({
+ where: {
+ eventTypeId: eventTypeId,
+ startTime: {
+ gte: dateFrom.format(),
+ lte: dateTo.format(),
+ },
+ },
+ select: {
+ uid: true,
+ startTime: true,
+ _count: {
+ select: {
+ attendees: true,
+ },
+ },
+ },
+ });
+ }
+
+ return {
+ busy: bufferedBusyTimes,
+ timeZone,
+ workingHours,
+ currentSeats,
+ };
+}
diff --git a/packages/core/index.ts b/packages/core/index.ts
index b2e810437b..3f4526787a 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -1,3 +1,4 @@
export * from "./CalendarManager";
export * from "./EventManager";
+export { default as getBusyTimes } from "./getBusyTimes";
export * from "./videoClient";
diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts
index f6809cac8b..56d8e03446 100644
--- a/packages/lib/defaultEvents.ts
+++ b/packages/lib/defaultEvents.ts
@@ -1,5 +1,10 @@
import type { EventTypeCustomInput } from "@prisma/client";
-import { PeriodType, SchedulingType, UserPlan } from "@prisma/client";
+import { PeriodType, Prisma, SchedulingType, UserPlan } from "@prisma/client";
+
+import { baseUserSelect } from "@calcom/prisma/selects";
+
+const userSelectData = Prisma.validator()({ select: baseUserSelect });
+type User = Prisma.UserGetPayload;
const availability = [
{
@@ -81,7 +86,13 @@ const commons = {
theme: null,
brandColor: "#292929",
darkBrandColor: "#fafafa",
- },
+ availability: [],
+ selectedCalendars: [],
+ startTime: 0,
+ endTime: 0,
+ schedules: [],
+ defaultScheduleId: null,
+ } as User,
],
};
diff --git a/packages/lib/getLuckyUsers.ts b/packages/lib/getLuckyUsers.ts
new file mode 100644
index 0000000000..0f31359063
--- /dev/null
+++ b/packages/lib/getLuckyUsers.ts
@@ -0,0 +1,19 @@
+import { Prisma } from "@prisma/client";
+
+import { userSelect } from "@calcom/prisma";
+
+type User = Prisma.UserGetPayload;
+
+export function getLuckyUsers(
+ users: User[],
+ bookingCounts: {
+ username: string | null;
+ bookingCount: number;
+ }[]
+) {
+ if (!bookingCounts.length) users.slice(0, 1);
+
+ const [firstMostAvailableUser] = bookingCounts.sort((a, b) => (a.bookingCount > b.bookingCount ? 1 : -1));
+ const luckyUser = users.find((user) => user.username === firstMostAvailableUser?.username);
+ return luckyUser ? [luckyUser] : users;
+}
diff --git a/packages/lib/index.ts b/packages/lib/index.ts
index 78836ecdd1..00ae0059e8 100644
--- a/packages/lib/index.ts
+++ b/packages/lib/index.ts
@@ -1,2 +1,3 @@
+export { getLuckyUsers } from "./getLuckyUsers";
export { default as isPrismaObj, isPrismaObjOrUndefined } from "./isPrismaObj";
export * from "./isRecurringEvent";
diff --git a/apps/web/modules/common/api/defaultHandler.ts b/packages/lib/server/defaultHandler.ts
similarity index 100%
rename from apps/web/modules/common/api/defaultHandler.ts
rename to packages/lib/server/defaultHandler.ts
diff --git a/packages/lib/server/defaultResponder.ts b/packages/lib/server/defaultResponder.ts
new file mode 100644
index 0000000000..e2167db87f
--- /dev/null
+++ b/packages/lib/server/defaultResponder.ts
@@ -0,0 +1,29 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { performance } from "perf_hooks";
+
+import { perfObserver } from ".";
+import { getServerErrorFromUnkown } from "./getServerErrorFromUnkown";
+
+type Handle = (req: NextApiRequest, res: NextApiResponse) => Promise;
+
+perfObserver.observe({ entryTypes: ["measure"], buffered: true });
+
+/** Allows us to get type inference from API handler responses */
+function defaultResponder(f: Handle) {
+ return async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ performance.mark("Start");
+ const result = await f(req, res);
+ res.json(result);
+ } catch (err) {
+ const error = getServerErrorFromUnkown(err);
+ res.statusCode = error.statusCode;
+ res.json({ message: error.message });
+ } finally {
+ performance.mark("End");
+ performance.measure("Measuring endpoint: " + req.url, "Start", "End");
+ }
+ };
+}
+
+export default defaultResponder;
diff --git a/packages/lib/server/getServerErrorFromUnkown.ts b/packages/lib/server/getServerErrorFromUnkown.ts
new file mode 100644
index 0000000000..7e90d2b06a
--- /dev/null
+++ b/packages/lib/server/getServerErrorFromUnkown.ts
@@ -0,0 +1,29 @@
+import { Prisma } from "@prisma/client";
+import Stripe from "stripe";
+import { ZodError } from "zod";
+
+import { HttpError } from "../http-error";
+
+export function getServerErrorFromUnkown(cause: unknown): HttpError {
+ if (cause instanceof Prisma.PrismaClientKnownRequestError) {
+ return new HttpError({ statusCode: 400, message: cause.message, cause });
+ }
+ if (cause instanceof Error) {
+ return new HttpError({ statusCode: 500, message: cause.message, cause });
+ }
+ if (cause instanceof HttpError) {
+ return cause;
+ }
+ if (cause instanceof Stripe.errors.StripeInvalidRequestError) {
+ return new HttpError({ statusCode: 400, message: cause.message, cause });
+ }
+ if (cause instanceof ZodError) {
+ return new HttpError({ statusCode: 400, message: cause.message, cause });
+ }
+ if (typeof cause === "string") {
+ // @ts-expect-error https://github.com/tc39/proposal-error-cause
+ return new Error(cause, { cause });
+ }
+
+ return new HttpError({ statusCode: 500, message: `Unhandled error of type '${typeof cause}'` });
+}
diff --git a/packages/lib/server/index.ts b/packages/lib/server/index.ts
new file mode 100644
index 0000000000..532895f110
--- /dev/null
+++ b/packages/lib/server/index.ts
@@ -0,0 +1,5 @@
+export { default as defaultHandler } from "./defaultHandler";
+export { default as defaultResponder } from "./defaultResponder";
+export { getServerErrorFromUnkown } from "./getServerErrorFromUnkown";
+export { getTranslation } from "./i18n";
+export { default as perfObserver } from "./perfObserver";
diff --git a/packages/lib/server/perfObserver.ts b/packages/lib/server/perfObserver.ts
new file mode 100644
index 0000000000..4076aca994
--- /dev/null
+++ b/packages/lib/server/perfObserver.ts
@@ -0,0 +1,20 @@
+import { PerformanceObserver } from "perf_hooks";
+
+declare global {
+ // eslint-disable-next-line no-var
+ var perfObserver: PerformanceObserver | undefined;
+}
+
+export const perfObserver =
+ globalThis.perfObserver ||
+ new PerformanceObserver((items) => {
+ items.getEntries().forEach((entry) => {
+ console.log(entry); // fake call to our custom logging solution
+ });
+ });
+
+if (process.env.NODE_ENV !== "production") {
+ globalThis.perfObserver = perfObserver;
+}
+
+export default perfObserver;
diff --git a/packages/prisma/selects/index.ts b/packages/prisma/selects/index.ts
index b59bed2e77..8376ce7e82 100644
--- a/packages/prisma/selects/index.ts
+++ b/packages/prisma/selects/index.ts
@@ -1,2 +1,3 @@
export * from "./booking";
export * from "./event-types";
+export * from "./user";
diff --git a/packages/prisma/selects/user.ts b/packages/prisma/selects/user.ts
new file mode 100644
index 0000000000..153a71d45d
--- /dev/null
+++ b/packages/prisma/selects/user.ts
@@ -0,0 +1,52 @@
+import { Prisma } from "@prisma/client";
+
+export const availabilityUserSelect = Prisma.validator()({
+ credentials: true,
+ timeZone: true,
+ bufferTime: true,
+ availability: true,
+ id: true,
+ startTime: true,
+ endTime: true,
+ selectedCalendars: true,
+ schedules: {
+ select: {
+ availability: true,
+ timeZone: true,
+ id: true,
+ },
+ },
+ defaultScheduleId: true,
+});
+
+export const baseUserSelect = Prisma.validator()({
+ email: true,
+ name: true,
+ username: true,
+ destinationCalendar: true,
+ locale: true,
+ plan: true,
+ avatar: true,
+ hideBranding: true,
+ theme: true,
+ brandColor: true,
+ darkBrandColor: true,
+ ...availabilityUserSelect,
+});
+
+export const userSelect = Prisma.validator()({
+ select: {
+ email: true,
+ name: true,
+ username: true,
+ destinationCalendar: true,
+ locale: true,
+ plan: true,
+ avatar: true,
+ hideBranding: true,
+ theme: true,
+ brandColor: true,
+ darkBrandColor: true,
+ ...availabilityUserSelect,
+ },
+});
diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts
index 9ac22ed62e..162e760ef4 100644
--- a/packages/prisma/zod-utils.ts
+++ b/packages/prisma/zod-utils.ts
@@ -56,3 +56,39 @@ export const stringOrNumber = z.union([
]);
export const stringToDayjs = z.string().transform((val) => dayjs(val));
+
+export const bookingCreateBodySchema = z.object({
+ email: z.string(),
+ end: z.string(),
+ web3Details: z
+ .object({
+ userWallet: z.string(),
+ userSignature: z.string(),
+ })
+ .optional(),
+ eventTypeId: z.number(),
+ eventTypeSlug: z.string(),
+ guests: z.array(z.string()).optional(),
+ location: z.string(),
+ name: z.string(),
+ notes: z.string().optional(),
+ rescheduleUid: z.string().optional(),
+ recurringEventId: z.string().optional(),
+ start: z.string(),
+ timeZone: z.string(),
+ user: z.union([z.string(), z.array(z.string())]).optional(),
+ language: z.string(),
+ bookingUid: z.string().optional(),
+ customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })),
+ metadata: z.record(z.string()),
+ hasHashedBookingLink: z.boolean(),
+ hashedLink: z.string().nullish(),
+});
+
+export const extendedBookingCreateBody = bookingCreateBodySchema.merge(
+ z.object({
+ noEmail: z.boolean().optional(),
+ recurringCount: z.number().optional(),
+ rescheduleReason: z.string().optional(),
+ })
+);