perf: bookings query improvement (#10687)

This commit is contained in:
Hariom Balhara 2023-08-24 14:44:10 +05:30 committed by GitHub
parent 43b3d68447
commit 5503d9d432
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 765 additions and 266 deletions

View File

@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

View File

@ -1,8 +1,8 @@
import type { PrismaClient } from "@prisma/client";
import type { App_RoutingForms_Form } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

View File

@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { getSerializableForm } from "../lib/getSerializableForm";

View File

@ -1,7 +1,8 @@
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
import logger from "@calcom/lib/logger";
import type { PrismaClient, Prisma } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { entries } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

View File

@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";

View File

@ -1,7 +1,7 @@
import type { PrismaClient } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { z } from "zod";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import { getSerializableForm } from "../lib/getSerializableForm";

View File

@ -1,8 +1,9 @@
import type { Prisma, PrismaClient } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import type { z } from "zod";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import slugify from "@calcom/lib/slugify";
import type { PrismaClient } from "@calcom/prisma";
import prisma from "@calcom/prisma";
type BookingSelect = {

View File

@ -1,4 +1,4 @@
import type { Prisma, PrismaClient, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
import type { Prisma, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import type { EventManagerUser } from "@calcom/core/EventManager";
@ -11,6 +11,7 @@ import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";

View File

@ -1,8 +1,8 @@
import type { PrismaClient } from "@prisma/client";
import cache from "memory-cache";
import { z } from "zod";
import { CONSOLE_URL } from "@calcom/lib/constants";
import type { PrismaClient } from "@calcom/prisma";
const CACHING_TIME = 86400000; // 24 hours in milliseconds

View File

@ -1,4 +1,4 @@
import type { PrismaClient } from "@prisma/client";
import type { PrismaClient } from "@calcom/prisma";
export async function getDeploymentKey(prisma: PrismaClient) {
const deployment = await prisma.deployment.findUnique({

View File

@ -1,9 +1,10 @@
import type { PrismaClient, Prisma } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import { sendSlugReplacementEmail } from "@calcom/emails/email-manager";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { PrismaClient } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { _EventTypeModel } from "@calcom/prisma/zod";
import { allManagedEventTypeProps, unlockedManagedEventTypeProps } from "@calcom/prisma/zod-utils";

View File

@ -1,8 +1,8 @@
import type { SAMLSSORecord, OIDCSSORecord } from "@boxyhq/saml-jackson";
import type { PrismaClient } from "@prisma/client";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || "";

View File

@ -9,7 +9,7 @@ import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/or
import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
import {
bookerLayoutOptions,

View File

@ -1,4 +1,4 @@
import type { PrismaClient } from "@prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { AppFlags } from "../config";

View File

@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import defaultPrisma from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma";
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
export type GetSubscriberOptions = {

View File

@ -1,8 +1,9 @@
import type { Prisma, PrismaClient } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import type { z } from "zod";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import slugify from "@calcom/lib/slugify";
import type { PrismaClient } from "@calcom/prisma";
type BookingSelect = {
description: true;

View File

@ -1,4 +1,3 @@
import type { PrismaClient } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { getLocationGroupedOptions } from "@calcom/app-store/server";
@ -10,6 +9,7 @@ import { parseBookingLimit, parseDurationLimit, parseRecurringEvent } from "@cal
import { CAL_URL } from "@calcom/lib/constants";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { PrismaClient } from "@calcom/prisma";
import { SchedulingType, MembershipRole, AppCategories } from "@calcom/prisma/enums";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";

View File

@ -1,4 +1,4 @@
import type { PrismaClient } from "@prisma/client";
import type { PrismaClient } from "@calcom/prisma";
export async function maybeGetBookingUidFromSeat(prisma: PrismaClient, uid: string) {
// Look bookingUid in bookingSeat

View File

@ -1,28 +1,65 @@
import type { Prisma } from "@prisma/client";
import { PrismaClient } from "@prisma/client";
import { PrismaClient as PrismaClientWithoutExtension } from "@prisma/client";
import { bookingReferenceMiddleware } from "./middleware";
declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
var prisma: typeof prismaWithClientExtensions;
}
const prismaOptions: Prisma.PrismaClientOptions = {};
if (!!process.env.NEXT_PUBLIC_DEBUG) prismaOptions.log = ["query", "error", "warn"];
export const prisma = globalThis.prisma || new PrismaClient(prismaOptions);
const prismaWithoutClientExtensions = new PrismaClientWithoutExtension(prismaOptions);
export const customPrisma = (options: Prisma.PrismaClientOptions) =>
new PrismaClient({ ...prismaOptions, ...options });
new PrismaClientWithoutExtension({ ...prismaOptions, ...options });
// If any changed on middleware server restart is required
// TODO: Migrate it to $extends
bookingReferenceMiddleware(prismaWithoutClientExtensions);
// FIXME: Due to some reason, there are types failing in certain places due to the $extends. Fix it and then enable it
// Specifically we get errors like `Type 'string | Date | null | undefined' is not assignable to type 'Exact<string | Date | null | undefined, string | Date | null | undefined>'`
// const prismaWithClientExtensions = prismaWithoutClientExtensions.$extends({
// query: {
// $allModels: {
// async $allOperations({ model, operation, args, query }) {
// const start = performance.now();
// /* your custom logic here */
// const res = await query(args);
// const end = performance.now();
// logger.debug("Query Perf: ", `${model}.${operation} took ${(end - start).toFixed(2)}ms\n`);
// return res;
// },
// },
// },
// });
// .$extends({
// name: "teamUpdateWithMetadata",
// query: {
// team: {
// async update({ model, operation, args, query }) {
// if (args.data.metadata) {
// // Prepare args.data with merged metadata
// }
// return query(args);
// },
// },
// },
// })
const prismaWithClientExtensions = prismaWithoutClientExtensions;
export const prisma = (globalThis.prisma as typeof prismaWithClientExtensions) || prismaWithClientExtensions;
if (process.env.NODE_ENV !== "production") {
globalThis.prisma = prisma;
}
// If any changed on middleware server restart is required
bookingReferenceMiddleware(prisma);
export type PrismaClient = typeof prisma;
export default prisma;
export * from "./selects";

View File

@ -1,4 +1,4 @@
import { PrismaClient } from "@prisma/client";
import type { PrismaClient } from "@prisma/client";
function middleware(prisma: PrismaClient) {
/***********************************/

View File

@ -0,0 +1,325 @@
/**
* This script can be used to seed the database with a lot of data for performance testing.
* TODO: Make it more structured and configurable from CLI
* Run it as `npx ts-node --transpile-only ./seed-performance-testing.ts`
*/
import { uuid } from "short-uuid";
import dailyMeta from "@calcom/app-store/dailyvideo/_metadata";
import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
import dayjs from "@calcom/dayjs";
import { BookingStatus } from "@calcom/prisma/enums";
import { createUserAndEventType } from "./seed-utils";
async function createManyDifferentUsersWithDifferentEventTypesAndBookings({
tillUser,
startFrom = 0,
}: {
tillUser: number;
startFrom?: number;
}) {
for (let i = startFrom; i < tillUser; i++) {
await createUserAndEventType({
user: {
email: `pro${i}@example.com`,
name: "Pro Example",
password: "1111",
username: `pro${i}`,
theme: "light",
},
eventTypes: [
{
title: "30min",
slug: "30min",
length: 30,
_bookings: [
{
uid: uuid(),
title: "30min",
startTime: dayjs().add(1, "day").toDate(),
endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
},
{
uid: uuid(),
title: "30min",
startTime: dayjs().add(2, "day").toDate(),
endTime: dayjs().add(2, "day").add(30, "minutes").toDate(),
status: BookingStatus.PENDING,
},
],
},
{
title: "60min",
slug: "60min",
length: 60,
},
{
title: "Multiple duration",
slug: "multiple-duration",
length: 75,
metadata: {
multipleDuration: [30, 75, 90],
},
},
{
title: "paid",
slug: "paid",
length: 60,
price: 100,
},
{
title: "In person meeting",
slug: "in-person",
length: 60,
locations: [{ type: "inPerson", address: "London" }],
},
{
title: "Zoom Event",
slug: "zoom",
length: 60,
locations: [{ type: zoomMeta.appData?.location?.type }],
},
{
title: "Daily Event",
slug: "daily",
length: 60,
locations: [{ type: dailyMeta.appData?.location?.type }],
},
{
title: "Google Meet",
slug: "google-meet",
length: 60,
locations: [{ type: googleMeetMeta.appData?.location?.type }],
},
{
title: "Yoga class",
slug: "yoga-class",
length: 30,
recurringEvent: { freq: 2, count: 12, interval: 1 },
_bookings: [
{
uid: uuid(),
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").toDate(),
endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(1, "week").toDate(),
endTime: dayjs().add(1, "day").add(1, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(2, "week").toDate(),
endTime: dayjs().add(1, "day").add(2, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(3, "week").toDate(),
endTime: dayjs().add(1, "day").add(3, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(4, "week").toDate(),
endTime: dayjs().add(1, "day").add(4, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Yoga class",
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(5, "week").toDate(),
endTime: dayjs().add(1, "day").add(5, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").toDate(),
endTime: dayjs().subtract(4, "day").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").add(1, "week").toDate(),
endTime: dayjs().subtract(4, "day").add(1, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").add(2, "week").toDate(),
endTime: dayjs().subtract(4, "day").add(2, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").add(3, "week").toDate(),
endTime: dayjs().subtract(4, "day").add(3, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
],
},
{
title: "Tennis class",
slug: "tennis-class",
length: 60,
recurringEvent: { freq: 2, count: 10, interval: 2 },
requiresConfirmation: true,
_bookings: [
{
uid: uuid(),
title: "Tennis class",
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").toDate(),
endTime: dayjs().add(2, "day").add(60, "minutes").toDate(),
status: BookingStatus.PENDING,
},
{
uid: uuid(),
title: "Tennis class",
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(2, "week").toDate(),
endTime: dayjs().add(2, "day").add(2, "week").add(60, "minutes").toDate(),
status: BookingStatus.PENDING,
},
{
uid: uuid(),
title: "Tennis class",
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(4, "week").toDate(),
endTime: dayjs().add(2, "day").add(4, "week").add(60, "minutes").toDate(),
status: BookingStatus.PENDING,
},
{
uid: uuid(),
title: "Tennis class",
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(8, "week").toDate(),
endTime: dayjs().add(2, "day").add(8, "week").add(60, "minutes").toDate(),
status: BookingStatus.PENDING,
},
{
uid: uuid(),
title: "Tennis class",
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(10, "week").toDate(),
endTime: dayjs().add(2, "day").add(10, "week").add(60, "minutes").toDate(),
status: BookingStatus.PENDING,
},
],
},
],
});
}
}
async function createAUserWithManyBookings() {
const random = Math.random();
await createUserAndEventType({
user: {
email: `pro-${random}@example.com`,
name: "Pro Example",
password: "1111",
username: `pro-${random}`,
theme: "light",
},
eventTypes: [
{
title: "30min",
slug: "30min",
length: 30,
_numBookings: 100,
},
{
title: "60min",
slug: "60min",
length: 60,
_numBookings: 100,
},
{
title: "Multiple duration",
slug: "multiple-duration",
length: 75,
metadata: {
multipleDuration: [30, 75, 90],
},
_numBookings: 100,
},
{
title: "paid",
slug: "paid",
length: 60,
price: 100,
_numBookings: 100,
},
{
title: "Zoom Event",
slug: "zoom",
length: 60,
locations: [{ type: zoomMeta.appData?.location?.type }],
_numBookings: 100,
},
{
title: "Daily Event",
slug: "daily",
length: 60,
locations: [{ type: dailyMeta.appData?.location?.type }],
_numBookings: 100,
},
{
title: "Google Meet",
slug: "google-meet",
length: 60,
locations: [{ type: googleMeetMeta.appData?.location?.type }],
_numBookings: 100,
},
{
title: "Yoga class",
slug: "yoga-class",
length: 30,
_numBookings: 100,
},
{
title: "Tennis class",
slug: "tennis-class",
length: 60,
recurringEvent: { freq: 2, count: 10, interval: 2 },
requiresConfirmation: true,
_numBookings: 100,
},
],
});
}
// createManyDifferentUsersWithDifferentEventTypesAndBookings({
// tillUser: 20000,
// startFrom: 10000,
// });
createAUserWithManyBookings();

View File

@ -0,0 +1,147 @@
import type { Prisma, UserPermissionRole } from "@prisma/client";
import { uuid } from "short-uuid";
import dayjs from "@calcom/dayjs";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import prisma from ".";
export async function createUserAndEventType({
user,
eventTypes = [],
}: {
user: {
email: string;
password: string;
username: string;
name: string;
completedOnboarding?: boolean;
timeZone?: string;
role?: UserPermissionRole;
theme?: "dark" | "light";
};
eventTypes?: Array<
Prisma.EventTypeUncheckedCreateInput & {
_bookings?: Prisma.BookingCreateInput[];
_numBookings?: number;
}
>;
}) {
const userData = {
...user,
password: await hashPassword(user.password),
emailVerified: new Date(),
completedOnboarding: user.completedOnboarding ?? true,
locale: "en",
schedules:
user.completedOnboarding ?? true
? {
create: {
name: "Working Hours",
availability: {
createMany: {
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
},
},
},
}
: undefined,
};
const theUser = await prisma.user.upsert({
where: { email_username: { email: user.email, username: user.username } },
update: userData,
create: userData,
});
console.log(
`👤 Upserted '${user.username}' with email "${user.email}" & password "${user.password}". Booking page 👉 ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}`
);
for (const eventTypeInput of eventTypes) {
const { _bookings, _numBookings, ...eventTypeData } = eventTypeInput;
let bookingFields;
if (_bookings && _numBookings) {
throw new Error("You can't set both _bookings and _numBookings");
} else if (_numBookings) {
bookingFields = [...Array(_numBookings).keys()].map((i) => ({
startTime: dayjs()
.add(1, "day")
.add(i * 5 + 0, "minutes")
.toDate(),
endTime: dayjs()
.add(1, "day")
.add(i * 5 + 30, "minutes")
.toDate(),
title: `${eventTypeInput.title}:${i + 1}`,
uid: uuid(),
}));
} else {
bookingFields = _bookings || [];
}
eventTypeData.userId = theUser.id;
eventTypeData.users = { connect: { id: theUser.id } };
const eventType = await prisma.eventType.findFirst({
where: {
slug: eventTypeData.slug,
users: {
some: {
id: eventTypeData.userId,
},
},
},
select: {
id: true,
},
});
if (eventType) {
console.log(
`\t📆 Event type ${eventTypeData.slug} already seems seeded - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
);
continue;
}
const { id } = await prisma.eventType.create({
data: eventTypeData,
});
console.log(
`\t📆 Event type ${eventTypeData.slug} with id ${id}, length ${eventTypeData.length}min - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
);
for (const bookingInput of bookingFields) {
await prisma.booking.create({
data: {
...bookingInput,
user: {
connect: {
email: user.email,
},
},
attendees: {
create: {
email: user.email,
name: user.name,
timeZone: "Europe/London",
},
},
eventType: {
connect: {
id,
},
},
status: bookingInput.status,
},
});
console.log(
`\t\t☎ Created booking ${bookingInput.title} at ${new Date(
bookingInput.startTime
).toLocaleDateString()}`
);
}
}
console.log("👤 User with it's event-types and bookings created", theUser.email);
return theUser;
}

View File

@ -1,4 +1,4 @@
import type { Prisma, UserPermissionRole } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import { uuid } from "short-uuid";
import type z from "zod";
@ -6,132 +6,13 @@ import dailyMeta from "@calcom/app-store/dailyvideo/_metadata";
import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
import dayjs from "@calcom/dayjs";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
import prisma from ".";
import mainAppStore from "./seed-app-store";
import { createUserAndEventType } from "./seed-utils";
import type { teamMetadataSchema } from "./zod-utils";
async function createUserAndEventType({
user,
eventTypes = [],
}: {
user: {
email: string;
password: string;
username: string;
name: string;
completedOnboarding?: boolean;
timeZone?: string;
role?: UserPermissionRole;
theme?: "dark" | "light";
};
eventTypes?: Array<
Prisma.EventTypeUncheckedCreateInput & {
_bookings?: Prisma.BookingCreateInput[];
}
>;
}) {
const userData = {
...user,
password: await hashPassword(user.password),
emailVerified: new Date(),
completedOnboarding: user.completedOnboarding ?? true,
locale: "en",
schedules:
user.completedOnboarding ?? true
? {
create: {
name: "Working Hours",
availability: {
createMany: {
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
},
},
},
}
: undefined,
};
const theUser = await prisma.user.upsert({
where: { email_username: { email: user.email, username: user.username } },
update: userData,
create: userData,
});
console.log(
`👤 Upserted '${user.username}' with email "${user.email}" & password "${user.password}". Booking page 👉 ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}`
);
for (const eventTypeInput of eventTypes) {
const { _bookings: bookingFields = [], ...eventTypeData } = eventTypeInput;
eventTypeData.userId = theUser.id;
eventTypeData.users = { connect: { id: theUser.id } };
const eventType = await prisma.eventType.findFirst({
where: {
slug: eventTypeData.slug,
users: {
some: {
id: eventTypeData.userId,
},
},
},
select: {
id: true,
},
});
if (eventType) {
console.log(
`\t📆 Event type ${eventTypeData.slug} already seems seeded - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
);
continue;
}
const { id } = await prisma.eventType.create({
data: eventTypeData,
});
console.log(
`\t📆 Event type ${eventTypeData.slug} with id ${id}, length ${eventTypeData.length}min - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
);
for (const bookingInput of bookingFields) {
await prisma.booking.create({
data: {
...bookingInput,
user: {
connect: {
email: user.email,
},
},
attendees: {
create: {
email: user.email,
name: user.name,
timeZone: "Europe/London",
},
},
eventType: {
connect: {
id,
},
},
status: bookingInput.status,
},
});
console.log(
`\t\t☎ Created booking ${bookingInput.title} at ${new Date(
bookingInput.startTime
).toLocaleDateString()}`
);
}
}
return theUser;
}
async function createTeamAndAddUsers(
teamInput: Prisma.TeamCreateInput,
users: { id: number; username: string; role?: MembershipRole }[] = []

View File

@ -1,6 +1,7 @@
import type { Session } from "next-auth";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
@ -131,17 +132,25 @@ const getUserSession = async (ctx: TRPCContextInner) => {
return { user, session };
};
const sessionMiddleware = middleware(async ({ ctx, next }) => {
const { user, session } = await getUserSession(ctx);
const sessionMiddleware = middleware(async ({ ctx, next }) => {
const middlewareStart = performance.now();
const { user, session } = await getUserSession(ctx);
const middlewareEnd = performance.now();
logger.debug("Perf:t.sessionMiddleware", middlewareEnd - middlewareStart);
return next({
ctx: { user, session },
});
});
export const isAuthed = middleware(async ({ ctx, next }) => {
const middlewareStart = performance.now();
const { user, session } = await getUserSession(ctx);
const middlewareEnd = performance.now();
logger.debug("Perf:t.isAuthed", middlewareEnd - middlewareStart);
if (!user || !session) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

View File

@ -1,7 +1,6 @@
import { type PrismaClient } from "@prisma/client";
import { isOrganization, withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils";
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";

View File

@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import type { PrismaClient } from "@calcom/prisma";
import type { TEventInputSchema } from "./event.schema";

View File

@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
import type { PrismaClient } from "@calcom/prisma";
import type { TSamlTenantProductInputSchema } from "./samlTenantProduct.schema";

View File

@ -1,7 +1,8 @@
import type { Prisma, PrismaClient } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
import { getLocalAppMetadata } from "@calcom/app-store/utils";
import type { PrismaClient } from "@calcom/prisma";
import { AppCategories } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "../../../trpc";

View File

@ -1,8 +1,8 @@
import type { Prisma } from "@prisma/client";
import type { PrismaClient } from "@prisma/client";
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
import { getLocalAppMetadata } from "@calcom/app-store/utils";
import type { PrismaClient } from "@calcom/prisma";
import type { AppCategories } from "@calcom/prisma/enums";
// import prisma from "@calcom/prisma";

View File

@ -1,8 +1,7 @@
import type { PrismaClient } from "@prisma/client";
import { getLocalAppMetadata } from "@calcom/app-store/utils";
import { sendDisabledAppEmail } from "@calcom/emails";
import { getTranslation } from "@calcom/lib/server";
import type { PrismaClient } from "@calcom/prisma";
import { AppCategories } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";

View File

@ -1,6 +1,6 @@
import type { Availability as AvailabilityModel, Schedule as ScheduleModel, User } from "@prisma/client";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { Schedule } from "@calcom/types/schedule";
export const getDefaultScheduleId = async (userId: number, prisma: PrismaClient) => {

View File

@ -1,6 +1,7 @@
import { parseRecurringEvent } from "@calcom/lib";
import type { PrismaClient } from "@calcom/prisma";
import { bookingMinimalSelect } from "@calcom/prisma";
import type { Prisma, PrismaClient } from "@calcom/prisma/client";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@ -72,6 +73,62 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
unconfirmed: { startTime: "asc" },
};
const passedBookingsStatusFilter = bookingListingFilters[bookingListingByStatus];
const orderBy = bookingListingOrderby[bookingListingByStatus];
const { bookings, recurringInfo } = await getBookings({
user,
prisma,
passedBookingsStatusFilter,
filters: input.filters,
orderBy,
take,
skip,
});
const bookingsFetched = bookings.length;
let nextCursor: typeof skip | null = skip;
if (bookingsFetched > take) {
nextCursor += bookingsFetched;
} else {
nextCursor = null;
}
return {
bookings,
recurringInfo,
nextCursor,
};
};
const set = new Set();
const getUniqueBookings = <T extends { uid: string }>(arr: T[]) => {
const unique = arr.filter((booking) => {
const duplicate = set.has(booking.uid);
set.add(booking.uid);
return !duplicate;
});
set.clear();
return unique;
};
async function getBookings({
user,
prisma,
passedBookingsStatusFilter,
filters,
orderBy,
take,
skip,
}: {
user: { id: number; email: string };
filters: TGetInputSchema["filters"];
prisma: PrismaClient;
passedBookingsStatusFilter: Prisma.BookingWhereInput;
orderBy: Prisma.BookingOrderByWithAggregationInput;
take: number;
skip: number;
}) {
// TODO: Fix record typing
const bookingWhereInputFilters: Record<string, Prisma.BookingWhereInput> = {
teamIds: {
@ -80,7 +137,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
eventType: {
team: {
id: {
in: input.filters?.teamIds,
in: filters?.teamIds,
},
},
},
@ -94,7 +151,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
users: {
some: {
id: {
in: input.filters?.userIds,
in: filters?.userIds,
},
},
},
@ -106,7 +163,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
AND: [
{
eventTypeId: {
in: input.filters?.eventTypeIds,
in: filters?.eventTypeIds,
},
},
],
@ -114,21 +171,100 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
};
const filtersCombined: Prisma.BookingWhereInput[] =
input.filters &&
Object.keys(input.filters).map((key) => {
filters &&
Object.keys(filters).map((key) => {
return bookingWhereInputFilters[key];
});
const passedBookingsStatusFilter = bookingListingFilters[bookingListingByStatus];
const orderBy = bookingListingOrderby[bookingListingByStatus];
const bookingSelect = {
...bookingMinimalSelect,
uid: true,
recurringEventId: true,
location: true,
eventType: {
select: {
slug: true,
id: true,
eventName: true,
price: true,
recurringEvent: true,
currency: true,
metadata: true,
seatsShowAttendees: true,
team: {
select: {
id: true,
name: true,
},
},
},
},
status: true,
paid: true,
payment: {
select: {
paymentOption: true,
amount: true,
currency: true,
success: true,
},
},
user: {
select: {
id: true,
name: true,
email: true,
},
},
rescheduled: true,
references: true,
isRecorded: true,
seatsReferences: {
where: {
attendee: {
email: user.email,
},
},
select: {
referenceUid: true,
attendee: {
select: {
email: true,
},
},
},
},
};
const [bookingsQuery, recurringInfoBasic, recurringInfoExtended] = await Promise.all([
const [
// Quering these in parallel to save time.
// Note that because we are applying `take` to individual queries, we will usually get more bookings then we need. It is okay to have more bookings faster than having what we need slower
bookingsQueryUserId,
bookingsQueryAttendees,
bookingsQueryTeamMember,
bookingsQuerySeatReference,
//////////////////////////
recurringInfoBasic,
recurringInfoExtended,
// We need all promises to be successful, so we are not using Promise.allSettled
] = await Promise.all([
prisma.booking.findMany({
where: {
OR: [
{
userId: user.id,
},
],
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
},
orderBy,
take: take + 1,
skip,
}),
prisma.booking.findMany({
where: {
OR: [
{
attendees: {
some: {
@ -136,6 +272,16 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
},
},
},
],
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
},
orderBy,
take: take + 1,
skip,
}),
prisma.booking.findMany({
where: {
OR: [
{
eventType: {
team: {
@ -150,6 +296,16 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
},
},
},
],
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
},
orderBy,
take: take + 1,
skip,
}),
prisma.booking.findMany({
where: {
OR: [
{
seatsReferences: {
some: {
@ -162,65 +318,6 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
],
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
},
select: {
...bookingMinimalSelect,
uid: true,
recurringEventId: true,
location: true,
eventType: {
select: {
slug: true,
id: true,
eventName: true,
price: true,
recurringEvent: true,
currency: true,
metadata: true,
seatsShowAttendees: true,
team: {
select: {
id: true,
name: true,
},
},
},
},
status: true,
paid: true,
payment: {
select: {
paymentOption: true,
amount: true,
currency: true,
success: true,
},
},
user: {
select: {
id: true,
name: true,
email: true,
},
},
rescheduled: true,
references: true,
isRecorded: true,
seatsReferences: {
where: {
attendee: {
email: user.email,
},
},
select: {
referenceUid: true,
attendee: {
select: {
email: true,
},
},
},
},
},
orderBy,
take: take + 1,
skip,
@ -285,7 +382,25 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
}
);
const bookings = bookingsQuery.map((booking) => {
const plainBookings = getUniqueBookings(
bookingsQueryUserId
.concat(bookingsQueryAttendees)
.concat(bookingsQueryTeamMember)
.concat(bookingsQuerySeatReference)
);
// Now enrich bookings with relation data. We could have queried the relation data along with the bookings, but that would cause unnecessary queries to the database.
// Because Prisma is also going to query the select relation data sequentially, we are fine querying it separately here as it would be just 1 query instead of 4
const bookings = (
await prisma.booking.findMany({
where: {
id: {
in: plainBookings.map((booking) => booking.id),
},
},
select: bookingSelect,
})
).map((booking) => {
// If seats are enabled and the event is not set to show attendees, filter out attendees that are not the current user
if (booking.seatsReferences.length && !booking.eventType?.seatsShowAttendees) {
booking.attendees = booking.attendees.filter((attendee) => attendee.email === user.email);
@ -303,18 +418,5 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
endTime: booking.endTime.toISOString(),
};
});
const bookingsFetched = bookings.length;
let nextCursor: typeof skip | null = skip;
if (bookingsFetched > take) {
nextCursor += bookingsFetched;
} else {
nextCursor = null;
}
return {
bookings,
recurringInfo,
nextCursor,
};
};
return { bookings, recurringInfo };
}

View File

@ -5,7 +5,7 @@ import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { DailyLocationType } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";

View File

@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import getEventTypeById from "@calcom/lib/getEventTypeById";
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc";
import type { TGetInputSchema } from "./get.schema";

View File

@ -1,4 +1,4 @@
import { type PrismaClient, Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client";
// eslint-disable-next-line no-restricted-imports
import { orderBy } from "lodash";
@ -7,6 +7,7 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE
import { CAL_URL } from "@calcom/lib/constants";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
import type { PrismaClient } from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";

View File

@ -1,4 +1,3 @@
import type { PrismaClient } from "@prisma/client";
import { Prisma } from "@prisma/client";
import type { NextApiResponse, GetServerSidePropsContext } from "next";
@ -8,6 +7,7 @@ import { validateIntervalLimitOrder } from "@calcom/lib";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server";
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
import type { PrismaClient } from "@calcom/prisma";
import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";

View File

@ -1,4 +1,4 @@
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

View File

@ -2,7 +2,7 @@ import appStore from "@calcom/app-store";
import dayjs from "@calcom/dayjs";
import { sendNoShowFeeChargedEmail } from "@calcom/emails";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";

View File

@ -4,7 +4,7 @@ import { v4 as uuid } from "uuid";
import dayjs from "@calcom/dayjs";
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@trpc/server";

View File

@ -1,5 +1,4 @@
import type { PrismaClient } from "@prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TListMembersInputSchema } from "./listMembers.schema";

View File

@ -1,9 +1,8 @@
import type { PrismaClient } from "@prisma/client";
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams";
import { closeComDeleteTeamMembership } from "@calcom/lib/sync/SyncServiceManager";
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";

View File

@ -3,8 +3,8 @@ import type { Workflow } from "@prisma/client";
import emailReminderTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailReminderTemplate";
import { SENDER_NAME } from "@calcom/lib/constants";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import type { PrismaClient } from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma/client";
import {
MembershipRole,
TimeUnit,

View File

@ -1,6 +1,7 @@
import type { WorkflowType } from "@calcom/ee/workflows/components/WorkflowListPage";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import type { Prisma, PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/client";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

View File

@ -19,7 +19,7 @@ import {
import { IS_SELF_HOSTED, SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import { BookingStatus, WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

View File

@ -8,7 +8,7 @@ import {
} from "@calcom/features/bookings/lib/getBookingFields";
import { removeBookingField, upsertBookingField } from "@calcom/features/eventtypes/lib/bookingFieldsManager";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import type PrismaType from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma";
import type { WorkflowStep } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
@ -20,7 +20,7 @@ export function getSender(
export async function isAuthorized(
workflow: Pick<Workflow, "id" | "teamId" | "userId"> | null,
prisma: typeof PrismaType,
prisma: PrismaClient,
currentUserId: number,
readOnly?: boolean
) {

View File

@ -1,7 +1,7 @@
import type { IncomingMessage } from "http";
import type { Session } from "next-auth";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import "./next-auth";

View File

@ -1,7 +1,8 @@
import type { PrismaClient } from "@prisma/client";
import { beforeEach, vi } from "vitest";
import { mockDeep, mockReset } from "vitest-mock-extended";
import type { PrismaClient } from "@calcom/prisma";
vi.mock("@calcom/prisma", () => ({
default: prisma,
availabilityUserSelect: vi.fn(),