From c6ad767565b780bcb7148a488f4505195352b608 Mon Sep 17 00:00:00 2001 From: Volnei Munhoz Date: Tue, 4 Nov 2025 06:51:13 -0300 Subject: [PATCH] refactor: decouple @calcom/prisma from @calcom/features, @calcom/ee, and @calcom/lib (#24802) * refactor: decouple @calcom/prisma from @calcom/features, @calcom/ee, and @calcom/lib - Inline helper functions in zod-utils.ts (emailSchema, slugify, getValidRhfFieldName, isPasswordValid, intervalLimitsType, zodAttributesQueryValue) - Update schema.prisma @zod.import comments to reference zod-utils instead of @calcom/lib - Inline idempotency key generation in booking-idempotency-key extension using uuid v5 - Move usage-tracking extension to @calcom/ee/prisma-extensions/ - Remove usage-tracking extension from packages/prisma/index.ts - Move Prisma DI module from @calcom/prisma to @calcom/features/di/modules/Prisma.ts - Update 20 import paths in @calcom/features to use new Prisma DI module - Remove @calcom/lib dependency from @calcom/prisma package.json - Add uuid dependency to @calcom/prisma package.json This reduces the dependency footprint of @calcom/prisma and breaks circular dependencies between packages. Co-Authored-By: Volnei Munhoz * feat: add EE-specific Prisma DI module with usage tracking Create packages/ee/di/modules/PrismaEE.ts to apply the usage-tracking extension in EE contexts. This module: - Imports the base prisma client from @calcom/prisma - Applies the usageTrackingExtention from @calcom/ee/prisma-extensions/usage-tracking - Exports the same DI tokens as the base Prisma module for override in EE containers This ensures usage tracking functionality is preserved in EE builds while keeping the base @calcom/prisma package free of EE dependencies. Note: EE containers should load this module after the base Prisma module to override the bindings. Co-Authored-By: Volnei Munhoz --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/ee/di/modules/PrismaEE.ts | 23 +++ .../prisma-extensions}/usage-tracking.ts | 0 .../di/BookingCancelService.module.ts | 2 +- .../di/InstantBookingCreateService.module.ts | 2 +- .../di/RegularBookingService.module.ts | 2 +- .../features/di/containers/AvailableSlots.ts | 2 +- .../features/di/containers/BookingLimits.ts | 2 +- packages/features/di/containers/BusyTimes.ts | 2 +- packages/features/di/containers/Cache.ts | 2 +- .../features/di/containers/FilterHosts.ts | 2 +- .../di/containers/GetUserAvailability.ts | 2 +- .../features/di/containers/InsightsBooking.ts | 2 +- .../features/di/containers/InsightsRouting.ts | 2 +- .../di/containers/NoSlotsNotification.ts | 2 +- .../features/di/containers/QualifiedHosts.ts | 2 +- packages/features/di/modules/Attribute.ts | 2 +- packages/features/di/modules/Host.ts | 2 +- packages/features/di/modules/Ooo.ts | 2 +- .../di/modules/Prisma.ts} | 3 +- packages/features/di/modules/User.ts | 2 +- .../di/watchlist/containers/watchlist.ts | 2 +- .../di/HashedLinkRepository.module.ts | 2 +- .../users/di/MembershipRepository.module.ts | 2 +- .../extensions/booking-idempotency-key.ts | 22 ++- packages/prisma/index.ts | 3 - packages/prisma/package.json | 2 +- packages/prisma/schema.prisma | 12 +- packages/prisma/zod-utils.ts | 175 +++++++++++++++++- 28 files changed, 238 insertions(+), 42 deletions(-) create mode 100644 packages/ee/di/modules/PrismaEE.ts rename packages/{prisma/extensions => ee/prisma-extensions}/usage-tracking.ts (100%) rename packages/{prisma/prisma.module.ts => features/di/modules/Prisma.ts} (91%) diff --git a/packages/ee/di/modules/PrismaEE.ts b/packages/ee/di/modules/PrismaEE.ts new file mode 100644 index 0000000000..08f79e4f8c --- /dev/null +++ b/packages/ee/di/modules/PrismaEE.ts @@ -0,0 +1,23 @@ +import { type Container, createModule } from "@calcom/features/di/di"; +import { DI_TOKENS } from "@calcom/features/di/tokens"; +import { prisma, readonlyPrisma } from "@calcom/prisma"; + +import { usageTrackingExtention } from "../../prisma-extensions/usage-tracking"; + +export const prismaEEModule = createModule(); +const token = DI_TOKENS.PRISMA_CLIENT; +const readOnlyToken = DI_TOKENS.READ_ONLY_PRISMA_CLIENT; +const moduleToken = DI_TOKENS.PRISMA_MODULE; + +const prismaWithUsageTracking = prisma.$extends(usageTrackingExtention(prisma)); + +prismaEEModule.bind(token).toFactory(() => prismaWithUsageTracking, "singleton"); +prismaEEModule.bind(readOnlyToken).toFactory(() => readonlyPrisma, "singleton"); + +export const moduleLoader = { + token, + readOnlyToken, + loadModule: (container: Container) => { + container.load(moduleToken, prismaEEModule); + }, +}; diff --git a/packages/prisma/extensions/usage-tracking.ts b/packages/ee/prisma-extensions/usage-tracking.ts similarity index 100% rename from packages/prisma/extensions/usage-tracking.ts rename to packages/ee/prisma-extensions/usage-tracking.ts diff --git a/packages/features/bookings/di/BookingCancelService.module.ts b/packages/features/bookings/di/BookingCancelService.module.ts index a4e6ea5f25..8781e6e195 100644 --- a/packages/features/bookings/di/BookingCancelService.module.ts +++ b/packages/features/bookings/di/BookingCancelService.module.ts @@ -1,7 +1,7 @@ import { BookingCancelService } from "@calcom/features/bookings/lib/handleCancelBooking"; import { bindModuleToClassOnToken, createModule } from "@calcom/lib/di/di"; import { DI_TOKENS } from "@calcom/lib/di/tokens"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; const thisModule = createModule(); const token = DI_TOKENS.BOOKING_CANCEL_SERVICE; diff --git a/packages/features/bookings/di/InstantBookingCreateService.module.ts b/packages/features/bookings/di/InstantBookingCreateService.module.ts index 54d2c1042f..741615330d 100644 --- a/packages/features/bookings/di/InstantBookingCreateService.module.ts +++ b/packages/features/bookings/di/InstantBookingCreateService.module.ts @@ -1,7 +1,7 @@ import { InstantBookingCreateService } from "@calcom/features/bookings/lib/service/InstantBookingCreateService"; import { createModule, bindModuleToClassOnToken } from "@calcom/features/di/di"; import { DI_TOKENS } from "@calcom/features/di/tokens"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; export const instantBookingCreateServiceModule = createModule(); const token = DI_TOKENS.INSTANT_BOOKING_CREATE_SERVICE; diff --git a/packages/features/bookings/di/RegularBookingService.module.ts b/packages/features/bookings/di/RegularBookingService.module.ts index c78155e2f1..d916478ac7 100644 --- a/packages/features/bookings/di/RegularBookingService.module.ts +++ b/packages/features/bookings/di/RegularBookingService.module.ts @@ -7,7 +7,7 @@ import { moduleLoader as luckyUserServiceModuleLoader } from "@calcom/features/d import { moduleLoader as userRepositoryModuleLoader } from "@calcom/features/di/modules/User"; import { DI_TOKENS } from "@calcom/features/di/tokens"; import { moduleLoader as hashedLinkServiceModuleLoader } from "@calcom/features/hashedLink/di/HashedLinkService.module"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; const thisModule = createModule(); const token = DI_TOKENS.REGULAR_BOOKING_SERVICE; diff --git a/packages/features/di/containers/AvailableSlots.ts b/packages/features/di/containers/AvailableSlots.ts index 2ddd55c996..3eff9920fe 100644 --- a/packages/features/di/containers/AvailableSlots.ts +++ b/packages/features/di/containers/AvailableSlots.ts @@ -1,6 +1,6 @@ import { DI_TOKENS } from "@calcom/features/di/tokens"; import { redisModule } from "@calcom/features/redis/di/redisModule"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import type { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; import { membershipRepositoryModule } from "@calcom/features/users/di/MembershipRepository.module"; import { createContainer } from "../di"; diff --git a/packages/features/di/containers/BookingLimits.ts b/packages/features/di/containers/BookingLimits.ts index e2d1f39d7e..a9c56c2869 100644 --- a/packages/features/di/containers/BookingLimits.ts +++ b/packages/features/di/containers/BookingLimits.ts @@ -1,6 +1,6 @@ import type { CheckBookingLimitsService } from "@calcom/features/bookings/lib/checkBookingLimits"; import { DI_TOKENS } from "@calcom/features/di/tokens"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import type { CheckBookingAndDurationLimitsService } from "../../bookings/lib/handleNewBooking/checkBookingAndDurationLimits"; import { createContainer } from "../di"; diff --git a/packages/features/di/containers/BusyTimes.ts b/packages/features/di/containers/BusyTimes.ts index 0c14211087..a029341357 100644 --- a/packages/features/di/containers/BusyTimes.ts +++ b/packages/features/di/containers/BusyTimes.ts @@ -1,6 +1,6 @@ import type { BusyTimesService } from "@calcom/features/busyTimes/services/getBusyTimes"; import { DI_TOKENS } from "@calcom/features/di/tokens"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import { createContainer } from "../di"; import { bookingRepositoryModule } from "../modules/Booking"; diff --git a/packages/features/di/containers/Cache.ts b/packages/features/di/containers/Cache.ts index bb77b2260d..5c33085034 100644 --- a/packages/features/di/containers/Cache.ts +++ b/packages/features/di/containers/Cache.ts @@ -1,5 +1,5 @@ import { DI_TOKENS } from "@calcom/features/di/tokens"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import type { CacheService } from "../../../features/calendar-cache/lib/getShouldServeCache"; import { createContainer } from "../di"; diff --git a/packages/features/di/containers/FilterHosts.ts b/packages/features/di/containers/FilterHosts.ts index b274855316..d91e342e67 100644 --- a/packages/features/di/containers/FilterHosts.ts +++ b/packages/features/di/containers/FilterHosts.ts @@ -1,6 +1,6 @@ import type { FilterHostsService } from "@calcom/features/bookings/lib/host-filtering/filterHostsBySameRoundRobinHost"; import { DI_TOKENS } from "@calcom/features/di/tokens"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import { createContainer } from "../di"; import { bookingRepositoryModule } from "../modules/Booking"; diff --git a/packages/features/di/containers/GetUserAvailability.ts b/packages/features/di/containers/GetUserAvailability.ts index 6545f546cb..6bd9b34613 100644 --- a/packages/features/di/containers/GetUserAvailability.ts +++ b/packages/features/di/containers/GetUserAvailability.ts @@ -1,7 +1,7 @@ import type { UserAvailabilityService } from "@calcom/features/availability/lib/getUserAvailability"; import { DI_TOKENS } from "@calcom/features/di/tokens"; import { redisModule } from "@calcom/features/redis/di/redisModule"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import { createContainer } from "../di"; import { bookingRepositoryModule } from "../modules/Booking"; diff --git a/packages/features/di/containers/InsightsBooking.ts b/packages/features/di/containers/InsightsBooking.ts index 189b321367..4d3ffe4583 100644 --- a/packages/features/di/containers/InsightsBooking.ts +++ b/packages/features/di/containers/InsightsBooking.ts @@ -5,7 +5,7 @@ import type { InsightsBookingBaseService, } from "@calcom/features/insights/services/InsightsBookingBaseService"; import type { InsightsBookingService } from "@calcom/features/insights/services/InsightsBookingDIService"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import { createContainer } from "../di"; import { insightsBookingModule } from "../modules/InsightsBooking"; diff --git a/packages/features/di/containers/InsightsRouting.ts b/packages/features/di/containers/InsightsRouting.ts index 1a018a9b20..aa04bf6eec 100644 --- a/packages/features/di/containers/InsightsRouting.ts +++ b/packages/features/di/containers/InsightsRouting.ts @@ -5,7 +5,7 @@ import type { InsightsRoutingBaseService, } from "@calcom/features/insights/services/InsightsRoutingBaseService"; import type { InsightsRoutingService } from "@calcom/features/insights/services/InsightsRoutingDIService"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import { createContainer } from "../di"; import { insightsRoutingModule } from "../modules/InsightsRouting"; diff --git a/packages/features/di/containers/NoSlotsNotification.ts b/packages/features/di/containers/NoSlotsNotification.ts index 534bc08bb0..569a600cd1 100644 --- a/packages/features/di/containers/NoSlotsNotification.ts +++ b/packages/features/di/containers/NoSlotsNotification.ts @@ -1,7 +1,7 @@ import { DI_TOKENS } from "@calcom/features/di/tokens"; import { redisModule } from "@calcom/features/redis/di/redisModule"; import { membershipRepositoryModule } from "@calcom/features/users/di/MembershipRepository.module"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import type { NoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots"; import { createContainer } from "../di"; diff --git a/packages/features/di/containers/QualifiedHosts.ts b/packages/features/di/containers/QualifiedHosts.ts index 67e4e3e71a..96ef6d8f4f 100644 --- a/packages/features/di/containers/QualifiedHosts.ts +++ b/packages/features/di/containers/QualifiedHosts.ts @@ -1,6 +1,6 @@ import type { QualifiedHostsService } from "@calcom/features/bookings/lib/host-filtering/findQualifiedHostsWithDelegationCredentials"; import { DI_TOKENS } from "@calcom/features/di/tokens"; -import { prismaModule } from "@calcom/prisma/prisma.module"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; import { createContainer } from "../di"; import { bookingRepositoryModule } from "../modules/Booking"; diff --git a/packages/features/di/modules/Attribute.ts b/packages/features/di/modules/Attribute.ts index 581d446ca0..7dc7f0f9ff 100644 --- a/packages/features/di/modules/Attribute.ts +++ b/packages/features/di/modules/Attribute.ts @@ -1,6 +1,6 @@ import { DI_TOKENS } from "@calcom/features/di/tokens"; import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; import { createModule, bindModuleToClassOnToken, type ModuleLoader } from "../di"; diff --git a/packages/features/di/modules/Host.ts b/packages/features/di/modules/Host.ts index cf6c9c1581..3d56a32361 100644 --- a/packages/features/di/modules/Host.ts +++ b/packages/features/di/modules/Host.ts @@ -1,6 +1,6 @@ import { DI_TOKENS } from "@calcom/features/di/tokens"; import { HostRepository } from "@calcom/lib/server/repository/host"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; import { createModule, bindModuleToClassOnToken, type ModuleLoader } from "../di"; diff --git a/packages/features/di/modules/Ooo.ts b/packages/features/di/modules/Ooo.ts index e2652fc177..eddf5d1052 100644 --- a/packages/features/di/modules/Ooo.ts +++ b/packages/features/di/modules/Ooo.ts @@ -1,6 +1,6 @@ import { DI_TOKENS } from "@calcom/features/di/tokens"; import { PrismaOOORepository } from "@calcom/lib/server/repository/ooo"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; import { createModule, bindModuleToClassOnToken, type ModuleLoader } from "../di"; diff --git a/packages/prisma/prisma.module.ts b/packages/features/di/modules/Prisma.ts similarity index 91% rename from packages/prisma/prisma.module.ts rename to packages/features/di/modules/Prisma.ts index 0519c4e8ce..33114f0d74 100644 --- a/packages/prisma/prisma.module.ts +++ b/packages/features/di/modules/Prisma.ts @@ -1,7 +1,6 @@ import { type Container, createModule } from "@calcom/features/di/di"; import { DI_TOKENS } from "@calcom/features/di/tokens"; - -import { prisma, readonlyPrisma } from "./index"; +import { prisma, readonlyPrisma } from "@calcom/prisma"; export const prismaModule = createModule(); const token = DI_TOKENS.PRISMA_CLIENT; diff --git a/packages/features/di/modules/User.ts b/packages/features/di/modules/User.ts index e2eaf74eb5..f12156063c 100644 --- a/packages/features/di/modules/User.ts +++ b/packages/features/di/modules/User.ts @@ -1,6 +1,6 @@ import { DI_TOKENS } from "@calcom/features/di/tokens"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; import { createModule, bindModuleToClassOnToken, type ModuleLoader } from "../di"; diff --git a/packages/features/di/watchlist/containers/watchlist.ts b/packages/features/di/watchlist/containers/watchlist.ts index 718824662d..82c123f750 100644 --- a/packages/features/di/watchlist/containers/watchlist.ts +++ b/packages/features/di/watchlist/containers/watchlist.ts @@ -13,7 +13,7 @@ import type { GlobalBlockingService } from "@calcom/features/watchlist/lib/servi import type { OrganizationBlockingService } from "@calcom/features/watchlist/lib/service/OrganizationBlockingService"; import type { WatchlistAuditService } from "@calcom/features/watchlist/lib/service/WatchlistAuditService"; import type { WatchlistService } from "@calcom/features/watchlist/lib/service/WatchlistService"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; import { WATCHLIST_DI_TOKENS } from "../Watchlist.tokens"; import { watchlistModule } from "../modules/Watchlist.module"; diff --git a/packages/features/hashedLink/di/HashedLinkRepository.module.ts b/packages/features/hashedLink/di/HashedLinkRepository.module.ts index d31f17c4e2..8199e6b232 100644 --- a/packages/features/hashedLink/di/HashedLinkRepository.module.ts +++ b/packages/features/hashedLink/di/HashedLinkRepository.module.ts @@ -1,6 +1,6 @@ import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di"; import { DI_TOKENS } from "@calcom/features/di/tokens"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; import { HashedLinkRepository } from "../lib/repository/HashedLinkRepository"; diff --git a/packages/features/users/di/MembershipRepository.module.ts b/packages/features/users/di/MembershipRepository.module.ts index 96b2363473..d60a4b68c3 100644 --- a/packages/features/users/di/MembershipRepository.module.ts +++ b/packages/features/users/di/MembershipRepository.module.ts @@ -1,7 +1,7 @@ import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di"; import { DI_TOKENS } from "@calcom/features/di/tokens"; import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository"; -import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; export const membershipRepositoryModule = createModule(); const token = DI_TOKENS.MEMBERSHIP_REPOSITORY; diff --git a/packages/prisma/extensions/booking-idempotency-key.ts b/packages/prisma/extensions/booking-idempotency-key.ts index aebc422dd9..8e3378c979 100644 --- a/packages/prisma/extensions/booking-idempotency-key.ts +++ b/packages/prisma/extensions/booking-idempotency-key.ts @@ -1,14 +1,32 @@ -import { IdempotencyKeyService } from "@calcom/lib/idempotencyKey/idempotencyKeyService"; +import { v5 as uuidv5 } from "uuid"; + import { Prisma } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; +function generateIdempotencyKey({ + startTime, + endTime, + userId, + reassignedById, +}: { + startTime: Date | string; + endTime: Date | string; + userId?: number; + reassignedById?: number | null; +}) { + return uuidv5( + `${startTime.valueOf()}.${endTime.valueOf()}.${userId}${reassignedById ? `.${reassignedById}` : ""}`, + uuidv5.URL + ); +} + export function bookingIdempotencyKeyExtension() { return Prisma.defineExtension({ query: { booking: { async create({ args, query }) { if (args.data.status === BookingStatus.ACCEPTED) { - const idempotencyKey = IdempotencyKeyService.generate({ + const idempotencyKey = generateIdempotencyKey({ startTime: args.data.startTime, endTime: args.data.endTime, userId: args.data.user?.connect?.id, diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index cebded00bf..1c7a844a55 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -5,7 +5,6 @@ import { bookingIdempotencyKeyExtension } from "./extensions/booking-idempotency import { disallowUndefinedDeleteUpdateManyExtension } from "./extensions/disallow-undefined-delete-update-many"; import { excludeLockedUsersExtension } from "./extensions/exclude-locked-users"; import { excludePendingPaymentsExtension } from "./extensions/exclude-pending-payment-teams"; -import { usageTrackingExtention } from "./extensions/usage-tracking"; import { PrismaClient, type Prisma } from "./generated/prisma/client"; const connectionString = process.env.DATABASE_URL || ""; @@ -66,7 +65,6 @@ export const customPrisma = (options?: Prisma.PrismaClientOptions) => { } return new PrismaClient(finalOptions) - .$extends(usageTrackingExtention(baseClient)) .$extends(excludeLockedUsersExtension()) .$extends(excludePendingPaymentsExtension()) .$extends(bookingIdempotencyKeyExtension()) @@ -79,7 +77,6 @@ export const customPrisma = (options?: Prisma.PrismaClientOptions) => { // Explanation why we cast as PrismaClient. When we leave Prisma to its devices it tries to infer logic based on the extensions, but this is not a simple extends. // this makes the PrismaClient export type-hint impossible and it also is a massive hit on Prisma type hinting performance. export const prisma: PrismaClient = baseClient - .$extends(usageTrackingExtention(baseClient)) .$extends(excludeLockedUsersExtension()) .$extends(excludePendingPaymentsExtension()) .$extends(bookingIdempotencyKeyExtension()) diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 2ac0763853..ed386ea3e3 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -23,7 +23,6 @@ "seed-pbac": "ts-node --transpile-only ../../scripts/seed-pbac-organization.ts" }, "dependencies": { - "@calcom/lib": "workspace:*", "@prisma/adapter-pg": "^6.16.0", "@prisma/client": "^6.16.0", "@prisma/extension-accelerate": "^1.3.0", @@ -31,6 +30,7 @@ "prisma": "^6.16.0", "prisma-kysely": "2.2.0", "ts-node": "^10.9.1", + "uuid": "^8.3.2", "zod": "^3.22.4", "zod-prisma-types": "^3.2.4" }, diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 29725cff5a..5b2b5460e5 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -223,7 +223,7 @@ model EventType { hideOrganizerEmail Boolean @default(false) maxActiveBookingsPerBooker Int? maxActiveBookingPerBookerOfferReschedule Boolean @default(false) - /// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema) + /// @zod.import(["import { emailSchema } from '../../zod-utils'"]).custom.use(emailSchema) customReplyToEmail String? calVideoSettings CalVideoSettings? @@ -300,7 +300,7 @@ model DestinationCalendar { id Int @id @default(autoincrement()) integration String externalId String - /// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema) + /// @zod.import(["import { emailSchema } from '../../zod-utils'"]).custom.use(emailSchema) primaryEmail String? user User? @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int? @unique @@ -352,7 +352,7 @@ model User { uuid String @unique @default(uuid()) @db.Uuid username String? name String? - /// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema) + /// @zod.import(["import { emailSchema } from '../../zod-utils'"]).custom.use(emailSchema) email String emailVerified DateTime? password UserPassword? @@ -796,7 +796,7 @@ model Booking { user User? @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int? // User's email at the time of booking - /// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema) + /// @zod.import(["import { emailSchema } from '../../zod-utils'"]).custom.use(emailSchema) userPrimaryEmail String? references BookingReference[] eventType EventType? @relation(fields: [eventTypeId], references: [id]) @@ -842,9 +842,9 @@ model Booking { noShowHost Boolean? @default(false) scheduledTriggers WebhookScheduledTriggers[] oneTimePassword String? @unique @default(uuid()) - /// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema) + /// @zod.import(["import { emailSchema } from '../../zod-utils'"]).custom.use(emailSchema) cancelledBy String? - /// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema) + /// @zod.import(["import { emailSchema } from '../../zod-utils'"]).custom.use(emailSchema) rescheduledBy String? // Ah, made a typo here. Should have been routedFromRoutingFormRe"s"ponse. Live with it :( routedFromRoutingFormReponse App_RoutingForms_FormResponse? diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 402a7beb3a..87224533f3 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -11,16 +11,177 @@ import type { ZodTypeAny, } from "zod"; -import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid"; -import { emailSchema as emailRegexSchema, emailRegex } from "@calcom/lib/emailSchema"; -import { getValidRhfFieldName } from "@calcom/lib/getValidRhfFieldName"; -import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema"; -import { zodAttributesQueryValue } from "@calcom/lib/raqb/zod"; -import { slugify } from "@calcom/lib/slugify"; import { EventTypeCustomInputType } from "@calcom/prisma/enums"; import type { Prisma } from "./client"; + +/** @see https://github.com/colinhacks/zod/issues/3155#issuecomment-2060045794 */ +export const emailRegex = + /* eslint-disable-next-line no-useless-escape */ + /^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.']*)[A-Z0-9_+'-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; + +/** + * RFC 5321 Section 4.5.3.1.3 specifies: + * - Maximum email address length: 254 characters + * - Local part (before @): max 64 characters + * - Domain part (after @): max 253 characters + */ +const MAX_EMAIL_LENGTH = 254; + +const emailRegexSchema = z + .string() + .max(MAX_EMAIL_LENGTH, { message: "Email address is too long" }) + .regex(emailRegex); + +const slugify = (str: string, forDisplayingInput?: boolean) => { + if (!str) { + return ""; + } + + const s = str + .toLowerCase() // Convert to lowercase + .trim() // Remove whitespace from both sides + .normalize("NFD") // Normalize to decomposed form for handling accents + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + .replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + .replace(/[^.\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode and except "." period) with a dash + .replace(/[\s_#]+/g, "-") // Replace whitespace, # and underscores with a single dash + .replace(/^-+/, "") // Remove dashes from start + .replace(/\.{2,}/g, ".") // Replace consecutive periods with a single period + .replace(/^\.+/, "") // Remove periods from the start + .replace( + /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, + "" + ) // Removes emojis + .replace(/\s+/g, " ") + .replace(/-+/g, "-"); // Replace consecutive dashes with a single dash + + return forDisplayingInput ? s : s.replace(/-+$/, "").replace(/\.*$/, ""); // Remove dashes and period from end +}; + +const getValidRhfFieldName = (fieldName: string) => { + // Remember that any transformation that you do here would run on System Field names as well. So, be careful and avoiding doing anything here that would modify the SystemField names. + // e.g. SystemField name currently have uppercases in them. So, no need to lowercase unless absolutely needed. + return fieldName.replace(/[^a-zA-Z0-9-_]/g, "-"); +}; + +function isPasswordValid(password: string): boolean; +function isPasswordValid( + password: string, + breakdown: boolean, + strict?: boolean +): { caplow: boolean; num: boolean; min: boolean; admin_min: boolean }; +function isPasswordValid(password: string, breakdown?: boolean, strict?: boolean) { + let cap = false, // Has uppercase characters + low = false, // Has lowercase characters + num = false, // At least one number + min = false, // Eight characters, or fifteen in strict mode. + admin_min = false; + if (password.length >= 7 && (!strict || password.length > 14)) min = true; + if (strict && password.length > 14) admin_min = true; + if (password.match(/\d/)) num = true; + if (password.match(/[a-z]/)) low = true; + if (password.match(/[A-Z]/)) cap = true; + + if (!breakdown) return cap && low && num && min && (strict ? admin_min : true); + + let errors: Record = { caplow: cap && low, num, min }; + // Only return the admin key if strict mode is enabled. + if (strict) errors = { ...errors, admin_min }; + + return errors; +} + +export type IntervalLimitUnit = "day" | "week" | "month" | "year"; +export type IntervalLimit = Partial}`, number | undefined>>; +export type IntervalLimitKey = keyof IntervalLimit; + +export const intervalLimitsType: z.Schema = z + .object({ + PER_DAY: z.number().optional(), + PER_WEEK: z.number().optional(), + PER_MONTH: z.number().optional(), + PER_YEAR: z.number().optional(), + }) + .nullable(); + +const raqbChildSchema = z.object({ + type: z.string().optional(), + properties: z + .object({ + field: z.any().optional(), + operator: z.any().optional(), + value: z.any().optional(), + valueSrc: z.any().optional(), + valueError: z.array(z.union([z.string(), z.null()])).optional(), + valueType: z.any().optional(), + }) + .optional(), +}); + +const raqbChildren1Schema = z + .record(raqbChildSchema) + .superRefine((children1, ctx) => { + if (!children1) return; + const isObject = (value: unknown): value is Record => + typeof value === "object" && value !== null; + Object.entries(children1).forEach(([, _rule]) => { + const rule = _rule as unknown; + if (!isObject(rule) || rule.type !== "rule") return; + if (!isObject(rule.properties)) return; + + const value = rule.properties.value || []; + const valueSrc = rule.properties.valueSrc; + if (!(value instanceof Array) || !(valueSrc instanceof Array)) { + return; + } + + if (!valueSrc.length) { + // If valueSrc is empty, value could be empty for operators like is_empty, is_not_empty + return; + } + + // MultiSelect array can be 2D array + const flattenedValues = value.flat(); + + const validValues = flattenedValues.filter((value: unknown) => { + // Might want to restrict it to filter out null and empty string as well. But for now we know that Prisma errors only for undefined values when saving it in JSON field + // Also, it is possible that RAQB has some requirements to support null or empty string values. + if (value === undefined) return false; + return true; + }); + + if (!validValues.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Looks like you are trying to create a rule with no value", + }); + } + }); + }); + +const raqbQueryValueSchema = z.union([ + z.object({ + id: z.string().optional(), + type: z.literal("group"), + children1: raqbChildren1Schema.optional(), + properties: z.any(), + }), + z.object({ + id: z.string().optional(), + type: z.literal("switch_group"), + children1: raqbChildren1Schema.optional(), + properties: z.any(), + }), +]); + +const zodAttributesQueryValue = raqbQueryValueSchema; + + // Let's not import 118kb just to get an enum export enum Frequency { YEARLY = 0, @@ -220,8 +381,6 @@ export const eventTypeColor = z export type IntervalLimitsType = IntervalLimit | null; -export { intervalLimitsType } from "@calcom/lib/intervalLimits/intervalLimitSchema"; - export const eventTypeSlug = z .string() .trim()