Compare commits

...
Author SHA1 Message Date
emrysal e55cee443b chore: release v5.5.11
Post release / release (push) Canceled after 0s
2025-08-05 12:20:27 +00:00
MorganandAlex van Andel c3634b3aba refactor: getUserAvailability into service with DI (#22881)
* refactor: getUserAvailability into service with DI

* chore: bump platform libs

* disable bull queue in e2e for bookings

* chore: bump platform libs

* chore: bump platform libs

* fix: should update event type bookingFields test

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-08-05 13:13:42 +01:00
Eunjae LeeandDevin AI cd576f5378 refactor: use dependency injection for InsightsBookingService (#22790)
* refactor: use dependency injection for InsightsBookingService

- Add DI tokens for InsightsBookingService and module
- Rename InsightsBookingService to InsightsBookingBaseService
- Create InsightsBookingService DI interface with create method
- Add DI module and container for InsightsBookingService
- Update tRPC router to use getInsightsBookingService DI container
- Update test file to use InsightsBookingBaseService
- Update documentation to reflect new DI pattern

Follows the same dependency injection pattern established for InsightsRoutingService in PR #22677

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: restore createInsightsBookingService helper using DI internally

- Keep createInsightsBookingService helper function for cleaner API
- Use getInsightsBookingService DI container internally
- Maintain same function signature and behavior
- All existing calls continue to work unchanged

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: rename service files with proper capitalization

- Rename insightsBookingBase.ts to InsightsBookingBaseService.ts
- Rename insightsBookingDI.ts to InsightsBookingDIService.ts
- Update all import statements to use new file names
- Maintain existing handler functionality using createInsightsBookingService(ctx, input)

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* revert some changes

* rename

* update doc

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-05 11:07:07 +02:00
34 changed files with 867 additions and 663 deletions
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+4 -3
View File
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next"; import type { NextApiRequest } from "next";
import { z } from "zod"; import { z } from "zod";
import { getUserAvailability } from "@calcom/lib/getUserAvailability"; import { getUserAvailabilityService } from "@calcom/lib/di/containers/get-user-availability";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder"; import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
@@ -191,8 +191,9 @@ const availabilitySchema = z
async function handler(req: NextApiRequest) { async function handler(req: NextApiRequest) {
const { isSystemWideAdmin, userId: reqUserId } = req; const { isSystemWideAdmin, userId: reqUserId } = req;
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query); const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
const userAvailabilityService = getUserAvailabilityService()
if (!teamId) if (!teamId)
return getUserAvailability({ return userAvailabilityService.getUserAvailability({
username, username,
dateFrom, dateFrom,
dateTo, dateTo,
@@ -230,7 +231,7 @@ async function handler(req: NextApiRequest) {
const availabilities = members.map(async (user) => { const availabilities = members.map(async (user) => {
return { return {
userId: user.id, userId: user.id,
availability: await getUserAvailability({ availability: await userAvailabilityService.getUserAvailability({
userId: user.id, userId: user.id,
dateFrom, dateFrom,
dateTo, dateTo,
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0", "@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*", "@calcom/platform-constants": "*",
"@calcom/platform-enums": "*", "@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.276", "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.283",
"@calcom/platform-types": "*", "@calcom/platform-types": "*",
"@calcom/platform-utils": "*", "@calcom/platform-utils": "*",
"@calcom/prisma": "*", "@calcom/prisma": "*",
@@ -290,7 +290,10 @@ describe("Event types Endpoints", () => {
expect(responseBookingFields).toBeDefined(); expect(responseBookingFields).toBeDefined();
// note(Lauris): response bookingFields are already existing default bookingFields + the new one // note(Lauris): response bookingFields are already existing default bookingFields + the new one
const responseBookingField = responseBookingFields.find((field) => field.name === bookingFieldName); const responseBookingField = responseBookingFields.find((field) => field.name === bookingFieldName);
expect(responseBookingField).toEqual(bookingFields[0]); const fields = responseBookingField
//@ts-ignore
delete fields.labelAsSafeHtml
expect(fields).toEqual(bookingFields[0]);
eventType.bookingFields = responseBookingFields; eventType.bookingFields = responseBookingFields;
}); });
}); });
@@ -12,6 +12,7 @@ import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service"; import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { PrismaModule } from "@/modules/prisma/prisma.module"; import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],
@@ -28,6 +29,7 @@ import { Module } from "@nestjs/common";
CheckBookingLimitsService, CheckBookingLimitsService,
CacheService, CacheService,
AvailableSlotsService, AvailableSlotsService,
UserAvailabilityService
], ],
exports: [AvailableSlotsService], exports: [AvailableSlotsService],
}) })
@@ -12,6 +12,7 @@ import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.s
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { AvailableSlotsService as BaseAvailableSlotsService } from "@calcom/platform-libraries/slots"; import { AvailableSlotsService as BaseAvailableSlotsService } from "@calcom/platform-libraries/slots";
import { UserAvailabilityService } from "./user-availability.service";
@Injectable() @Injectable()
export class AvailableSlotsService extends BaseAvailableSlotsService { export class AvailableSlotsService extends BaseAvailableSlotsService {
@@ -37,6 +38,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
userRepo: userRepository, userRepo: userRepository,
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository) as any, checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository) as any,
cacheService: new CacheService(featuresRepository), cacheService: new CacheService(featuresRepository),
userAvailabilityService: new UserAvailabilityService(oooRepoDependency, bookingRepository, eventTypeRepository)
}); });
} }
} }
@@ -0,0 +1,27 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
import { Injectable } from "@nestjs/common";
import { UserAvailabilityService as BaseUserAvailabilityService } from "@calcom/platform-libraries/schedules";
@Injectable()
export class UserAvailabilityService extends BaseUserAvailabilityService {
constructor(
oooRepoDependency: PrismaOOORepository,
bookingRepository: PrismaBookingRepository,
eventTypeRepository: PrismaEventTypeRepository,
) {
super({
oooRepo: oooRepoDependency,
bookingRepo: bookingRepository,
eventTypeRepo: eventTypeRepository,
});
}
}
@@ -403,6 +403,10 @@ export class BillingService implements OnModuleDestroy {
fromReschedule?: string | null; fromReschedule?: string | null;
} }
) { ) {
if (this.configService.get("e2e")) {
return true;
}
const { uid, startTime, fromReschedule } = booking; const { uid, startTime, fromReschedule } = booking;
const delay = startTime.getTime() - Date.now(); const delay = startTime.getTime() - Date.now();
@@ -427,6 +431,9 @@ export class BillingService implements OnModuleDestroy {
* Removing an attendee from a booking does not cancel the usage increment job. * Removing an attendee from a booking does not cancel the usage increment job.
*/ */
async cancelUsageByBookingUid(bookingUid: string) { async cancelUsageByBookingUid(bookingUid: string) {
if (this.configService.get("e2e")) {
return true;
}
const job = await this.billingQueue.getJob(`increment-${bookingUid}`); const job = await this.billingQueue.getJob(`increment-${bookingUid}`);
if (job) { if (job) {
await job.remove(); await job.remove();
@@ -458,6 +465,7 @@ export class BillingService implements OnModuleDestroy {
async onModuleDestroy() { async onModuleDestroy() {
try { try {
await this.billingQueue.close(); await this.billingQueue.close();
} catch (err) { } catch (err) {
this.logger.error(err); this.logger.error(err);
@@ -6,7 +6,7 @@ import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker
import { buildDateRanges } from "@calcom/lib/date-ranges"; import { buildDateRanges } from "@calcom/lib/date-ranges";
import { ErrorCode } from "@calcom/lib/errorCodes"; import { ErrorCode } from "@calcom/lib/errorCodes";
import { getBusyTimesForLimitChecks } from "@calcom/lib/getBusyTimes"; import { getBusyTimesForLimitChecks } from "@calcom/lib/getBusyTimes";
import { getUsersAvailability } from "@calcom/lib/getUserAvailability"; import { getUserAvailabilityService } from "@calcom/lib/di/containers/get-user-availability";
import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits"; import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits"; import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits";
import { getPiiFreeUser } from "@calcom/lib/piiFreeData"; import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
@@ -62,6 +62,7 @@ const _ensureAvailableUsers = async (
shouldServeCache?: boolean shouldServeCache?: boolean
// ReturnType hint of at least one IsFixedAwareUser, as it's made sure at least one entry exists // ReturnType hint of at least one IsFixedAwareUser, as it's made sure at least one entry exists
): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> => { ): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> => {
const userAvailabilityService = getUserAvailabilityService()
const availableUsers: IsFixedAwareUser[] = []; const availableUsers: IsFixedAwareUser[] = [];
const startDateTimeUtc = getDateTimeInUtc(input.dateFrom, input.timeZone); const startDateTimeUtc = getDateTimeInUtc(input.dateFrom, input.timeZone);
@@ -86,7 +87,7 @@ const _ensureAvailableUsers = async (
}) })
: []; : [];
const usersAvailability = await getUsersAvailability({ const usersAvailability = await userAvailabilityService.getUsersAvailability({
users: eventType.users, users: eventType.users,
query: { query: {
...input, ...input,
@@ -103,7 +103,7 @@ export default function InsightsPage() {
## Step 4: Create tRPC Handler ## Step 4: Create tRPC Handler
Add the tRPC endpoint in the insights router using the `createInsightsBookingService()` helper: Add the tRPC endpoint in the insights router using the `getInsightsBookingService()` DI container function:
```typescript ```typescript
// packages/features/insights/server/trpc-router.ts // packages/features/insights/server/trpc-router.ts
@@ -118,6 +118,7 @@ export const insightsRouter = router({
myNewChartData: userBelongsToTeamProcedure myNewChartData: userBelongsToTeamProcedure
.input(bookingRepositoryBaseInputSchema) .input(bookingRepositoryBaseInputSchema)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
// `createInsightsBookingService` is defined at the root level in this file
const insightsBookingService = createInsightsBookingService(ctx, input); const insightsBookingService = createInsightsBookingService(ctx, input);
try { try {
@@ -129,13 +130,13 @@ export const insightsRouter = router({
}); });
``` ```
## Step 5: Add Service Method to InsightsBookingService ## Step 5: Add Service Method to InsightsBookingBaseService
Add your new method to the `InsightsBookingService` class: Add your new method to the `InsightsBookingBaseService` class:
```typescript ```typescript
// packages/lib/server/service/insightsBooking.ts // packages/lib/server/service/InsightsBookingBaseService.ts
export class InsightsBookingService { export class InsightsBookingBaseService {
// ... existing methods // ... existing methods
async getMyNewChartData() { async getMyNewChartData() {
@@ -168,7 +169,7 @@ export class InsightsBookingService {
## Best Practices ## Best Practices
1. **Use `createInsightsBookingService()`**: Always use the helper function for consistent service creation 1. **Use `getInsightsBookingService()`**: Always use the DI container function for consistent service creation
2. **Raw SQL for Performance**: Use `$queryRaw` for complex aggregations and better performance 2. **Raw SQL for Performance**: Use `$queryRaw` for complex aggregations and better performance
3. **Base Conditions**: Always use `await this.getBaseConditions()` for proper filtering and permissions 3. **Base Conditions**: Always use `await this.getBaseConditions()` for proper filtering and permissions
4. **Error Handling**: Wrap service calls in try-catch blocks with `TRPCError` 4. **Error Handling**: Wrap service calls in try-catch blocks with `TRPCError`
@@ -9,8 +9,8 @@ import {
routingRepositoryBaseInputSchema, routingRepositoryBaseInputSchema,
bookingRepositoryBaseInputSchema, bookingRepositoryBaseInputSchema,
} from "@calcom/features/insights/server/raw-data.schema"; } from "@calcom/features/insights/server/raw-data.schema";
import { getInsightsBookingService } from "@calcom/lib/di/containers/insights-booking";
import { getInsightsRoutingService } from "@calcom/lib/di/containers/insights-routing"; import { getInsightsRoutingService } from "@calcom/lib/di/containers/insights-routing";
import { InsightsBookingService } from "@calcom/lib/server/service/insightsBooking";
import type { readonlyPrisma } from "@calcom/prisma"; import type { readonlyPrisma } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums";
import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure"; import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure";
@@ -316,14 +316,13 @@ const BATCH_SIZE = 1000; // Adjust based on your needs
* Helper function to create InsightsBookingService with standardized parameters * Helper function to create InsightsBookingService with standardized parameters
*/ */
function createInsightsBookingService( function createInsightsBookingService(
ctx: { insightsDb: typeof readonlyPrisma; user: { id: number; organizationId: number | null } }, ctx: { user: { id: number; organizationId: number | null } },
input: z.infer<typeof bookingRepositoryBaseInputSchema>, input: z.infer<typeof bookingRepositoryBaseInputSchema>,
dateTarget: "createdAt" | "startTime" = "createdAt" dateTarget: "createdAt" | "startTime" = "createdAt"
) { ) {
const { scope, selectedTeamId, startDate, endDate, columnFilters } = input; const { scope, selectedTeamId, startDate, endDate, columnFilters } = input;
return new InsightsBookingService({ return getInsightsBookingService({
prisma: ctx.insightsDb,
options: { options: {
scope, scope,
userId: ctx.user.id, userId: ctx.user.id,
@@ -16,6 +16,7 @@ import { scheduleRepositoryModule } from "../modules/schedule";
import { selectedSlotsRepositoryModule } from "../modules/selectedSlots"; import { selectedSlotsRepositoryModule } from "../modules/selectedSlots";
import { teamRepositoryModule } from "../modules/team"; import { teamRepositoryModule } from "../modules/team";
import { userRepositoryModule } from "../modules/user"; import { userRepositoryModule } from "../modules/user";
import { getUserAvailabilityModule } from "../modules/get-user-availability";
const container = createContainer(); const container = createContainer();
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule); container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
@@ -31,7 +32,7 @@ container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule);
container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule); container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule);
container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule); container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule);
container.load(DI_TOKENS.AVAILABLE_SLOTS_SERVICE_MODULE, availableSlotsModule); container.load(DI_TOKENS.AVAILABLE_SLOTS_SERVICE_MODULE, availableSlotsModule);
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule)
export function getAvailableSlotsService() { export function getAvailableSlotsService() {
return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE); return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE);
} }
@@ -0,0 +1,23 @@
import { createContainer } from "@evyweb/ioctopus";
import { DI_TOKENS } from "@calcom/lib/di/tokens";
import { prismaModule } from "@calcom/prisma/prisma.module";
import { getUserAvailabilityModule } from "../modules/get-user-availability";
import { bookingRepositoryModule } from "../modules/booking";
import { eventTypeRepositoryModule } from "../modules/eventType";
import { oooRepositoryModule } from "../modules/ooo";
import { UserAvailabilityService } from "../../getUserAvailability";
const container = createContainer();
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule);
export function getUserAvailabilityService() {
return container.get<UserAvailabilityService>(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE);
}
@@ -0,0 +1,27 @@
import { createContainer } from "@evyweb/ioctopus";
import { DI_TOKENS } from "@calcom/lib/di/tokens";
import type {
InsightsBookingServicePublicOptions,
InsightsBookingServiceFilterOptions,
InsightsBookingBaseService,
} from "@calcom/lib/server/service/InsightsBookingBaseService";
import type { InsightsBookingService } from "@calcom/lib/server/service/InsightsBookingDIService";
import { prismaModule } from "@calcom/prisma/prisma.module";
import { insightsBookingModule } from "../modules/insights-booking";
export function getInsightsBookingService({
options,
filters,
}: {
options: InsightsBookingServicePublicOptions;
filters?: InsightsBookingServiceFilterOptions;
}): InsightsBookingBaseService {
const container = createContainer();
container.load(DI_TOKENS.READ_ONLY_PRISMA_CLIENT, prismaModule);
container.load(DI_TOKENS.INSIGHTS_BOOKING_SERVICE_MODULE, insightsBookingModule);
const diService = container.get<InsightsBookingService>(DI_TOKENS.INSIGHTS_BOOKING_SERVICE);
return diService.create({ options, filters });
}
@@ -17,4 +17,5 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
routingFormResponseRepo: DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY, routingFormResponseRepo: DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY,
cacheService: DI_TOKENS.CACHE_SERVICE, cacheService: DI_TOKENS.CACHE_SERVICE,
checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE, checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE,
userAvailabilityService: DI_TOKENS.GET_USER_AVAILABILITY_SERVICE
} satisfies Record<keyof IAvailableSlotsService, symbol>); } satisfies Record<keyof IAvailableSlotsService, symbol>);
@@ -0,0 +1,11 @@
import { createModule } from "@evyweb/ioctopus";
import { DI_TOKENS } from "../tokens";
import { IUserAvailabilityService, UserAvailabilityService } from "../../getUserAvailability";
export const getUserAvailabilityModule = createModule();
getUserAvailabilityModule.bind(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE).toClass(UserAvailabilityService, {
oooRepo: DI_TOKENS.OOO_REPOSITORY,
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
eventTypeRepo: DI_TOKENS.EVENT_TYPE_REPOSITORY,
} satisfies Record<keyof IUserAvailabilityService, symbol>);
@@ -0,0 +1,11 @@
import { createModule } from "@evyweb/ioctopus";
import type { IInsightsBookingService } from "@calcom/lib/server/service/InsightsBookingDIService";
import { InsightsBookingService } from "@calcom/lib/server/service/InsightsBookingDIService";
import { DI_TOKENS } from "../tokens";
export const insightsBookingModule = createModule();
insightsBookingModule.bind(DI_TOKENS.INSIGHTS_BOOKING_SERVICE).toClass(InsightsBookingService, {
prisma: DI_TOKENS.READ_ONLY_PRISMA_CLIENT,
} satisfies Record<keyof IInsightsBookingService, symbol>);
+4
View File
@@ -22,6 +22,8 @@ export const DI_TOKENS = {
AVAILABLE_SLOTS_SERVICE_MODULE: Symbol("AvailableSlotsModule"), AVAILABLE_SLOTS_SERVICE_MODULE: Symbol("AvailableSlotsModule"),
INSIGHTS_ROUTING_SERVICE: Symbol("InsightsRoutingService"), INSIGHTS_ROUTING_SERVICE: Symbol("InsightsRoutingService"),
INSIGHTS_ROUTING_SERVICE_MODULE: Symbol("InsightsRoutingServiceModule"), INSIGHTS_ROUTING_SERVICE_MODULE: Symbol("InsightsRoutingServiceModule"),
INSIGHTS_BOOKING_SERVICE: Symbol("InsightsBookingService"),
INSIGHTS_BOOKING_SERVICE_MODULE: Symbol("InsightsBookingServiceModule"),
FEATURES_REPOSITORY: Symbol("FeaturesRepository"), FEATURES_REPOSITORY: Symbol("FeaturesRepository"),
FEATURES_REPOSITORY_MODULE: Symbol("FeaturesRepositoryModule"), FEATURES_REPOSITORY_MODULE: Symbol("FeaturesRepositoryModule"),
CACHE_SERVICE: Symbol("CacheService"), CACHE_SERVICE: Symbol("CacheService"),
@@ -30,4 +32,6 @@ export const DI_TOKENS = {
CHECK_BOOKING_LIMITS_SERVICE_MODULE: Symbol("CheckBookingLimitsServiceModule"), CHECK_BOOKING_LIMITS_SERVICE_MODULE: Symbol("CheckBookingLimitsServiceModule"),
CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE: Symbol("CheckBookingAndDurationLimitsService"), CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE: Symbol("CheckBookingAndDurationLimitsService"),
CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE_MODULE: Symbol("CheckBookingAndDurationLimitsServiceModule"), CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE_MODULE: Symbol("CheckBookingAndDurationLimitsServiceModule"),
GET_USER_AVAILABILITY_SERVICE: Symbol("GetUserAvailabilityService"),
GET_USER_AVAILABILITY_SERVICE_MODULE: Symbol("GetUserAvailabilityModule"),
}; };
+107 -248
View File
@@ -8,6 +8,7 @@ import type {
} from "@prisma/client"; } from "@prisma/client";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { z } from "zod"; import { z } from "zod";
import type { BookingRepository } from "@calcom/lib/server/repository/booking";
import type { Dayjs } from "@calcom/dayjs"; import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
@@ -27,15 +28,14 @@ import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify"; import { safeStringify } from "@calcom/lib/safeStringify";
import { findUsersForAvailabilityCheck } from "@calcom/lib/server/findUsersForAvailabilityCheck"; import { findUsersForAvailabilityCheck } from "@calcom/lib/server/findUsersForAvailabilityCheck";
import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository"; import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository";
import prisma from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums"; import { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar"; import type { EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar";
import type { TimeRange } from "@calcom/types/schedule"; import type { TimeRange } from "@calcom/types/schedule";
import { getBusyTimes } from "./getBusyTimes"; import { getBusyTimes } from "./getBusyTimes";
import { withReporting } from "./sentryWrapper"; import { withReporting } from "./sentryWrapper";
import type { PrismaOOORepository } from "@calcom/lib/server/repository/ooo";
const log = logger.getSubLogger({ prefix: ["getUserAvailability"] }); const log = logger.getSubLogger({ prefix: ["getUserAvailability"] });
const availabilitySchema = z const availabilitySchema = z
@@ -55,106 +55,11 @@ const availabilitySchema = z
}) })
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in."); .refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
const _getEventType = async (id: number) => { export type EventType = Awaited<
const eventType = await prisma.eventType.findUnique({ ReturnType<(typeof UserAvailabilityService)["prototype"]["_getEventType"]>
where: { id }, >;
select: {
id: true,
seatsPerTimeSlot: true,
bookingLimits: true,
useEventLevelSelectedCalendars: true,
parent: {
select: {
team: {
select: {
id: true,
bookingLimits: true,
includeManagedEventsInLimits: true,
},
},
},
},
team: {
select: {
id: true,
bookingLimits: true,
includeManagedEventsInLimits: true,
},
},
hosts: {
select: {
user: {
select: {
email: true,
id: true,
},
},
schedule: {
select: {
availability: {
select: {
date: true,
startTime: true,
endTime: true,
days: true,
},
},
timeZone: true,
id: true,
},
},
},
},
durationLimits: true,
assignAllTeamMembers: true,
schedulingType: true,
timeZone: true,
length: true,
metadata: true,
schedule: {
select: {
id: true,
availability: {
select: {
days: true,
date: true,
startTime: true,
endTime: true,
},
},
timeZone: true,
},
},
availability: {
select: {
startTime: true,
endTime: true,
days: true,
date: true,
},
},
},
});
if (!eventType) {
return eventType;
}
return {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
};
};
export type EventType = Awaited<ReturnType<typeof _getEventType>>; type GetUser = Awaited<ReturnType<(typeof UserAvailabilityService)["prototype"]["_getUser"]>>;
export const getEventType = withReporting(_getEventType, "getEventType");
const _getUser = async (where: Prisma.UserWhereInput) => {
return findUsersForAvailabilityCheck({ where });
};
type GetUser = Awaited<ReturnType<typeof _getUser>>;
const getUser = withReporting(_getUser, "getUser");
export type GetUserAvailabilityInitialData = { export type GetUserAvailabilityInitialData = {
user?: GetUser; user?: GetUser;
@@ -207,7 +112,76 @@ type GetUserAvailabilityQuery = {
shouldServeCache?: boolean; shouldServeCache?: boolean;
}; };
const _getCurrentSeats = async ( export type CurrentSeats = Awaited<
ReturnType<(typeof UserAvailabilityService)["prototype"]["_getCurrentSeats"]>
>;
export type GetUserAvailabilityResult = Awaited<
ReturnType<(typeof UserAvailabilityService)["prototype"]["_getUserAvailability"]>
>;
interface GetUserAvailabilityParamsDTO {
availability: (DateOverride | WorkingHours)[];
}
export interface IFromUser {
id: number;
displayName: string | null;
}
export interface IToUser {
id: number;
username: string | null;
displayName: string | null;
}
export interface IOutOfOfficeData {
[key: string]: {
fromUser: IFromUser | null;
toUser?: IToUser | null;
reason?: string | null;
emoji?: string | null;
};
}
type GetUsersAvailabilityProps = {
users: (GetAvailabilityUser & {
currentBookings?: GetUserAvailabilityInitialData["currentBookings"];
outOfOfficeDays?: GetUserAvailabilityInitialData["outOfOfficeDays"];
})[];
query: Omit<GetUserAvailabilityQuery, "userId" | "username">;
initialData?: Omit<GetUserAvailabilityInitialData, "user">;
};
export interface IUserAvailabilityService {
eventTypeRepo: EventTypeRepository;
oooRepo: PrismaOOORepository;
bookingRepo: BookingRepository;
}
export class UserAvailabilityService {
constructor(public readonly dependencies: IUserAvailabilityService) {}
async _getEventType(id: number) {
const eventType = await this.dependencies.eventTypeRepo.findByIdForUserAvailability({id})
if (!eventType) {
return eventType;
}
return {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
};
}
getEventType = withReporting(this._getEventType.bind(this), "getEventType");
async _getUser(where: Prisma.UserWhereInput) {
return findUsersForAvailabilityCheck({ where });
}
getUser = withReporting(this._getUser.bind(this), "getUser");
async _getCurrentSeats(
eventType: { eventType: {
id?: number; id?: number;
schedulingType?: SchedulingType | null; schedulingType?: SchedulingType | null;
@@ -219,7 +193,7 @@ const _getCurrentSeats = async (
}, },
dateFrom: Dayjs, dateFrom: Dayjs,
dateTo: Dayjs dateTo: Dayjs
) => { ) {
const { schedulingType, hosts, id } = eventType; const { schedulingType, hosts, id } = eventType;
const hostEmails = hosts?.map((host) => host.user.email); const hostEmails = hosts?.map((host) => host.user.email);
const isTeamEvent = const isTeamEvent =
@@ -227,25 +201,9 @@ const _getCurrentSeats = async (
schedulingType === SchedulingType.ROUND_ROBIN || schedulingType === SchedulingType.ROUND_ROBIN ||
schedulingType === SchedulingType.COLLECTIVE; schedulingType === SchedulingType.COLLECTIVE;
const bookings = await prisma.booking.findMany({ const bookings = await this.dependencies.bookingRepo.findAcceptedBookingByEventTypeId({eventTypeId: id, dateFrom: dateFrom.format(), dateTo: dateTo.format()})
where: {
eventTypeId: id,
startTime: {
gte: dateFrom.format(),
lte: dateTo.format(),
},
status: BookingStatus.ACCEPTED,
},
select: {
uid: true,
startTime: true,
attendees: {
select: {
email: true,
},
},
},
});
return bookings.map((booking) => { return bookings.map((booking) => {
const attendees = isTeamEvent const attendees = isTeamEvent
@@ -260,19 +218,12 @@ const _getCurrentSeats = async (
}, },
}; };
}); });
}; }
export type CurrentSeats = Awaited<ReturnType<typeof _getCurrentSeats>>; getCurrentSeats = withReporting(this._getCurrentSeats.bind(this), "getCurrentSeats");
export const getCurrentSeats = withReporting(_getCurrentSeats, "getCurrentSeats");
export type GetUserAvailabilityResult = Awaited<ReturnType<typeof _getUserAvailability>>;
/** This should be called getUsersWorkingHoursAndBusySlots (...and remaining seats, and final timezone) */ /** This should be called getUsersWorkingHoursAndBusySlots (...and remaining seats, and final timezone) */
const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseAndEverythingElse( async _getUserAvailability(query: GetUserAvailabilityQuery, initialData?: GetUserAvailabilityInitialData) {
query: GetUserAvailabilityQuery,
initialData?: GetUserAvailabilityInitialData
) {
const { const {
username, username,
userId, userId,
@@ -301,20 +252,20 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
if (username) where.username = username; if (username) where.username = username;
if (userId) where.id = userId; if (userId) where.id = userId;
const user = initialData?.user || (await getUser(where)); const user = initialData?.user || (await this.getUser(where));
if (!user) { if (!user) {
throw new HttpError({ statusCode: 404, message: "No user found in getUserAvailability" }); throw new HttpError({ statusCode: 404, message: "No user found in getUserAvailability" });
} }
let eventType: EventType | null = initialData?.eventType || null; let eventType: EventType | null = initialData?.eventType || null;
if (!eventType && eventTypeId) eventType = await getEventType(eventTypeId); if (!eventType && eventTypeId) eventType = await this.getEventType(eventTypeId);
/* 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 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 */ current bookings with a seats event type and display them on the calendar, even if they are full */
let currentSeats: CurrentSeats | null = initialData?.currentSeats || null; let currentSeats: CurrentSeats | null = initialData?.currentSeats || null;
if (!currentSeats && eventType?.seatsPerTimeSlot) { if (!currentSeats && eventType?.seatsPerTimeSlot) {
currentSeats = await getCurrentSeats(eventType, dateFrom, dateTo); currentSeats = await this.getCurrentSeats(eventType, dateFrom, dateTo);
} }
const userSchedule = user.schedules.filter( const userSchedule = user.schedules.filter(
@@ -469,7 +420,10 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
); );
if ( if (
!(schedule?.availability || (eventType?.availability.length ? eventType.availability : user.availability)) !(
schedule?.availability ||
(eventType?.availability.length ? eventType.availability : user.availability)
)
) { ) {
throw new HttpError({ statusCode: 400, message: ErrorCode.AvailabilityNotFoundInSchedule }); throw new HttpError({ statusCode: 400, message: ErrorCode.AvailabilityNotFoundInSchedule });
} }
@@ -513,72 +467,9 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
const outOfOfficeDays = const outOfOfficeDays =
initialData?.outOfOfficeDays ?? initialData?.outOfOfficeDays ??
(await prisma.outOfOfficeEntry.findMany({ (await this.dependencies.oooRepo.findUserOOODays({userId: user.id, dateFrom: dateFrom.toISOString(), dateTo: dateTo.toISOString()}));
where: {
userId: user.id,
OR: [
// outside of range
// (start <= 'dateTo' AND end >= 'dateFrom')
{
start: {
lte: dateTo.toISOString(),
},
end: {
gte: dateFrom.toISOString(),
},
},
// start is between dateFrom and dateTo but end is outside of range
// (start <= 'dateTo' AND end >= 'dateTo')
{
start: {
lte: dateTo.toISOString(),
},
end: { const datesOutOfOffice: IOutOfOfficeData = this.calculateOutOfOfficeRanges(outOfOfficeDays, availability);
gte: dateTo.toISOString(),
},
},
// end is between dateFrom and dateTo but start is outside of range
// (start <= 'dateFrom' OR end <= 'dateTo')
{
start: {
lte: dateFrom.toISOString(),
},
end: {
lte: dateTo.toISOString(),
},
},
],
},
select: {
id: true,
start: true,
end: true,
user: {
select: {
id: true,
name: true,
},
},
toUser: {
select: {
id: true,
username: true,
name: true,
},
},
reason: {
select: {
id: true,
emoji: true,
reason: true,
},
},
},
}));
const datesOutOfOffice: IOutOfOfficeData = calculateOutOfOfficeRanges(outOfOfficeDays, availability);
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({ const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
dateFrom, dateFrom,
@@ -621,16 +512,16 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
); );
return result; return result;
}; }
export const getUserAvailability = withReporting(_getUserAvailability, "getUserAvailability"); getUserAvailability = withReporting(this._getUserAvailability.bind(this), "getUserAvailability");
const _getPeriodStartDatesBetween = ( _getPeriodStartDatesBetween(
dateFrom: Dayjs, dateFrom: Dayjs,
dateTo: Dayjs, dateTo: Dayjs,
period: IntervalLimitUnit, period: IntervalLimitUnit,
timeZone?: string timeZone?: string
): Dayjs[] => { ): Dayjs[] {
const dates = []; const dates = [];
let startDate = timeZone ? dayjs(dateFrom).tz(timeZone).startOf(period) : dayjs(dateFrom).startOf(period); let startDate = timeZone ? dayjs(dateFrom).tz(timeZone).startOf(period) : dayjs(dateFrom).startOf(period);
const endDate = timeZone ? dayjs(dateTo).tz(timeZone).endOf(period) : dayjs(dateTo).endOf(period); const endDate = timeZone ? dayjs(dateTo).tz(timeZone).endOf(period) : dayjs(dateTo).endOf(period);
@@ -640,41 +531,17 @@ const _getPeriodStartDatesBetween = (
startDate = startDate.add(1, period); startDate = startDate.add(1, period);
} }
return dates; return dates;
}; }
export const getPeriodStartDatesBetween = withReporting( getPeriodStartDatesBetween = withReporting(
_getPeriodStartDatesBetween, this._getPeriodStartDatesBetween.bind(this),
"getPeriodStartDatesBetween" "getPeriodStartDatesBetween"
); );
interface GetUserAvailabilityParamsDTO { calculateOutOfOfficeRanges(
availability: (DateOverride | WorkingHours)[];
}
export interface IFromUser {
id: number;
displayName: string | null;
}
export interface IToUser {
id: number;
username: string | null;
displayName: string | null;
}
export interface IOutOfOfficeData {
[key: string]: {
fromUser: IFromUser | null;
toUser?: IToUser | null;
reason?: string | null;
emoji?: string | null;
};
}
const calculateOutOfOfficeRanges = (
outOfOfficeDays: GetUserAvailabilityInitialData["outOfOfficeDays"], outOfOfficeDays: GetUserAvailabilityInitialData["outOfOfficeDays"],
availability: GetUserAvailabilityParamsDTO["availability"] availability: GetUserAvailabilityParamsDTO["availability"]
): IOutOfOfficeData => { ): IOutOfOfficeData {
if (!outOfOfficeDays || outOfOfficeDays.length === 0) { if (!outOfOfficeDays || outOfOfficeDays.length === 0) {
return {}; return {};
} }
@@ -713,18 +580,9 @@ const calculateOutOfOfficeRanges = (
return acc; return acc;
}, {}); }, {});
}; }
type GetUsersAvailabilityProps = { async _getUsersAvailability({ users, query, initialData }: GetUsersAvailabilityProps) {
users: (GetAvailabilityUser & {
currentBookings?: GetUserAvailabilityInitialData["currentBookings"];
outOfOfficeDays?: GetUserAvailabilityInitialData["outOfOfficeDays"];
})[];
query: Omit<GetUserAvailabilityQuery, "userId" | "username">;
initialData?: Omit<GetUserAvailabilityInitialData, "user">;
};
const _getUsersAvailability = async ({ users, query, initialData }: GetUsersAvailabilityProps) => {
if (users.length >= 50) { if (users.length >= 50) {
const userIds = users.map(({ id }) => id).join(", "); const userIds = users.map(({ id }) => id).join(", ");
log.warn( log.warn(
@@ -733,7 +591,7 @@ const _getUsersAvailability = async ({ users, query, initialData }: GetUsersAvai
} }
return await Promise.all( return await Promise.all(
users.map((user) => users.map((user) =>
_getUserAvailability( this._getUserAvailability(
{ {
...query, ...query,
userId: user.id, userId: user.id,
@@ -750,6 +608,7 @@ const _getUsersAvailability = async ({ users, query, initialData }: GetUsersAvai
) )
) )
); );
}; }
export const getUsersAvailability = withReporting(_getUsersAvailability, "getUsersAvailability"); getUsersAvailability = withReporting(this._getUsersAvailability.bind(this), "getUsersAvailability");
}
@@ -1,9 +1,9 @@
import type { Dayjs } from "@calcom/dayjs"; import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { getCheckBookingLimitsService } from "@calcom/lib/di/containers/booking-limits"; import { getCheckBookingLimitsService } from "@calcom/lib/di/containers/booking-limits";
import { getUserAvailabilityService } from "@calcom/lib/di/containers/get-user-availability";
import { getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes"; import { getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes";
import type { EventType } from "@calcom/lib/getUserAvailability"; import type { EventType } from "@calcom/lib/getUserAvailability";
import { getPeriodStartDatesBetween } from "@calcom/lib/getUserAvailability";
import { withReporting } from "@calcom/lib/sentryWrapper"; import { withReporting } from "@calcom/lib/sentryWrapper";
import { performance } from "@calcom/lib/server/perfObserver"; import { performance } from "@calcom/lib/server/perfObserver";
import { getTotalBookingDuration } from "@calcom/lib/server/queries/booking"; import { getTotalBookingDuration } from "@calcom/lib/server/queries/booking";
@@ -86,6 +86,7 @@ const _getBusyTimesFromBookingLimits = async (params: {
includeManagedEvents?: boolean; includeManagedEvents?: boolean;
timeZone?: string | null; timeZone?: string | null;
}) => { }) => {
const userAvailabilityService = getUserAvailabilityService();
const { const {
bookings, bookings,
bookingLimits, bookingLimits,
@@ -105,7 +106,7 @@ const _getBusyTimesFromBookingLimits = async (params: {
if (!limit) continue; if (!limit) continue;
const unit = intervalLimitKeyToUnit(key); const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit); const periodStartDates = userAvailabilityService.getPeriodStartDatesBetween(dateFrom, dateTo, unit);
for (const periodStart of periodStartDates) { for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit)) continue; if (limitManager.isAlreadyBusy(periodStart, unit)) continue;
@@ -162,12 +163,14 @@ const _getBusyTimesFromDurationLimits = async (
timeZone: string, timeZone: string,
rescheduleUid?: string rescheduleUid?: string
) => { ) => {
const userAvailabilityService = getUserAvailabilityService();
for (const key of descendingLimitKeys) { for (const key of descendingLimitKeys) {
const limit = durationLimits?.[key]; const limit = durationLimits?.[key];
if (!limit) continue; if (!limit) continue;
const unit = intervalLimitKeyToUnit(key); const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit); const periodStartDates = userAvailabilityService.getPeriodStartDatesBetween(dateFrom, dateTo, unit);
for (const periodStart of periodStartDates) { for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit)) continue; if (limitManager.isAlreadyBusy(periodStart, unit)) continue;
+9 -2
View File
@@ -1,12 +1,13 @@
import { createInstance } from "i18next"; import { createInstance } from "i18next";
/* eslint-disable @typescript-eslint/no-var-requires */
const { i18n } = require("@calcom/config/next-i18next.config");
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { fetchWithTimeout } from "../fetchWithTimeout"; import { fetchWithTimeout } from "../fetchWithTimeout";
import logger from "../logger"; import logger from "../logger";
/* eslint-disable @typescript-eslint/no-var-requires */
const { i18n } = require("@calcom/config/next-i18next.config");
const translationCache = new Map<string, Record<string, string>>(); const translationCache = new Map<string, Record<string, string>>();
const i18nInstanceCache = new Map<string, any>(); const i18nInstanceCache = new Map<string, any>();
const SUPPORTED_NAMESPACES = ["common"]; const SUPPORTED_NAMESPACES = ["common"];
@@ -28,6 +29,7 @@ export async function loadTranslations(_locale: string, _ns: string) {
} }
const url = `${WEBAPP_URL}/static/locales/${locale}/${ns}.json`; const url = `${WEBAPP_URL}/static/locales/${locale}/${ns}.json`;
try {
const response = await fetchWithTimeout( const response = await fetchWithTimeout(
url, url,
{ {
@@ -44,6 +46,11 @@ export async function loadTranslations(_locale: string, _ns: string) {
const translations = await response.json(); const translations = await response.json();
translationCache.set(cacheKey, translations); translationCache.set(cacheKey, translations);
return translations; return translations;
} catch (err) {
console.error("loadTranslations Error:", err);
return {};
}
} }
/** /**
+22
View File
@@ -869,4 +869,26 @@ export class BookingRepository {
}, },
}); });
} }
async findAcceptedBookingByEventTypeId({eventTypeId, dateFrom, dateTo}: {eventTypeId?: number, dateFrom: string, dateTo: string}) {
return this.prismaClient.booking.findMany({
where: {
eventTypeId,
startTime: {
gte: dateFrom,
lte: dateTo,
},
status: BookingStatus.ACCEPTED,
},
select: {
uid: true,
startTime: true,
attendees: {
select: {
email: true,
},
},
},
});
}
} }
@@ -1299,4 +1299,93 @@ export class EventTypeRepository {
}) { }) {
return user.allSelectedCalendars.filter((calendar) => calendar.eventTypeId === eventTypeId); return user.allSelectedCalendars.filter((calendar) => calendar.eventTypeId === eventTypeId);
} }
async findByIdForUserAvailability({ id }: { id: number }) {
const eventType = await this.prismaClient.eventType.findUnique({
where: { id },
select: {
id: true,
seatsPerTimeSlot: true,
bookingLimits: true,
useEventLevelSelectedCalendars: true,
parent: {
select: {
team: {
select: {
id: true,
bookingLimits: true,
includeManagedEventsInLimits: true,
},
},
},
},
team: {
select: {
id: true,
bookingLimits: true,
includeManagedEventsInLimits: true,
},
},
hosts: {
select: {
user: {
select: {
email: true,
id: true,
},
},
schedule: {
select: {
availability: {
select: {
date: true,
startTime: true,
endTime: true,
days: true,
},
},
timeZone: true,
id: true,
},
},
},
},
durationLimits: true,
assignAllTeamMembers: true,
schedulingType: true,
timeZone: true,
length: true,
metadata: true,
schedule: {
select: {
id: true,
availability: {
select: {
days: true,
date: true,
startTime: true,
endTime: true,
},
},
timeZone: true,
},
},
availability: {
select: {
startTime: true,
endTime: true,
days: true,
date: true,
},
},
},
});
if (!eventType) {
return eventType;
}
return {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
};
}
} }
+67
View File
@@ -79,4 +79,71 @@ export class PrismaOOORepository {
}, },
}); });
} }
async findUserOOODays({userId, dateTo, dateFrom}: {userId: number, dateTo: string, dateFrom: string}) {
return this.prismaClient.outOfOfficeEntry.findMany({
where: {
userId,
OR: [
// outside of range
// (start <= 'dateTo' AND end >= 'dateFrom')
{
start: {
lte: dateTo,
},
end: {
gte: dateFrom,
},
},
// start is between dateFrom and dateTo but end is outside of range
// (start <= 'dateTo' AND end >= 'dateTo')
{
start: {
lte: dateTo,
},
end: {
gte: dateTo,
},
},
// end is between dateFrom and dateTo but start is outside of range
// (start <= 'dateFrom' OR end <= 'dateTo')
{
start: {
lte: dateFrom,
},
end: {
lte: dateTo,
},
},
],
},
select: {
id: true,
start: true,
end: true,
user: {
select: {
id: true,
name: true,
},
},
toUser: {
select: {
id: true,
username: true,
name: true,
},
},
reason: {
select: {
id: true,
emoji: true,
reason: true,
},
},
},
});
}
} }
@@ -135,7 +135,7 @@ const NOTHING_CONDITION = Prisma.sql`1=0`;
const bookingDataKeys = new Set(Object.keys(bookingDataSchema.shape)); const bookingDataKeys = new Set(Object.keys(bookingDataSchema.shape));
export class InsightsBookingService { export class InsightsBookingBaseService {
private prisma: typeof readonlyPrisma; private prisma: typeof readonlyPrisma;
private options: InsightsBookingServiceOptions | null; private options: InsightsBookingServiceOptions | null;
private filters: InsightsBookingServiceFilterOptions | null; private filters: InsightsBookingServiceFilterOptions | null;
@@ -0,0 +1,29 @@
import type { readonlyPrisma } from "@calcom/prisma";
import {
InsightsBookingBaseService,
type InsightsBookingServicePublicOptions,
type InsightsBookingServiceFilterOptions,
} from "./InsightsBookingBaseService";
export interface IInsightsBookingService {
prisma: typeof readonlyPrisma;
}
export class InsightsBookingService {
constructor(private readonly dependencies: IInsightsBookingService) {}
create({
options,
filters,
}: {
options: InsightsBookingServicePublicOptions;
filters?: InsightsBookingServiceFilterOptions;
}): InsightsBookingBaseService {
return new InsightsBookingBaseService({
prisma: this.dependencies.prisma,
options,
filters,
});
}
}
@@ -6,7 +6,7 @@ import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums"; import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
import { InsightsBookingService } from "../../service/insightsBooking"; import { InsightsBookingBaseService as InsightsBookingService } from "../InsightsBookingBaseService";
const NOTHING_CONDITION = Prisma.sql`1=0`; const NOTHING_CONDITION = Prisma.sql`1=0`;
+1
View File
@@ -4,3 +4,4 @@ export {
} from "@calcom/lib/server/repository/schedule"; } from "@calcom/lib/server/repository/schedule";
export { updateSchedule, type UpdateScheduleResponse } from "@calcom/lib/schedules/updateSchedule"; export { updateSchedule, type UpdateScheduleResponse } from "@calcom/lib/schedules/updateSchedule";
export {UserAvailabilityService} from "@calcom/lib/getUserAvailability"
Binary file not shown.
@@ -1,4 +1,4 @@
import { getUserAvailability } from "@calcom/lib/getUserAvailability"; import { getUserAvailabilityService } from "@calcom/lib/di/containers/get-user-availability";
import type { TrpcSessionUser } from "../../../types"; import type { TrpcSessionUser } from "../../../types";
import type { TUserInputSchema } from "./user.schema"; import type { TUserInputSchema } from "./user.schema";
@@ -11,7 +11,8 @@ type UserOptions = {
}; };
export const userHandler = async ({ input }: UserOptions) => { export const userHandler = async ({ input }: UserOptions) => {
return getUserAvailability( const userAvailabilityService = getUserAvailabilityService()
return userAvailabilityService.getUserAvailability(
{ returnDateOverrides: true, bypassBusyCalendarTimes: false, ...input }, { returnDateOverrides: true, bypassBusyCalendarTimes: false, ...input },
undefined undefined
); );
@@ -20,10 +20,10 @@ import type {
CurrentSeats, CurrentSeats,
EventType, EventType,
GetAvailabilityUser, GetAvailabilityUser,
UserAvailabilityService,
IFromUser, IFromUser,
IToUser, IToUser,
} from "@calcom/lib/getUserAvailability"; } from "@calcom/lib/getUserAvailability";
import { getPeriodStartDatesBetween, getUsersAvailability } from "@calcom/lib/getUserAvailability";
import { descendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit"; import { descendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema"; import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits"; import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
@@ -101,6 +101,7 @@ export interface IAvailableSlotsService {
routingFormResponseRepo: RoutingFormResponseRepository; routingFormResponseRepo: RoutingFormResponseRepository;
cacheService: CacheService; cacheService: CacheService;
checkBookingLimitsService: CheckBookingLimitsService; checkBookingLimitsService: CheckBookingLimitsService;
userAvailabilityService: UserAvailabilityService
} }
export class AvailableSlotsService { export class AvailableSlotsService {
@@ -357,7 +358,7 @@ export class AvailableSlotsService {
if (!limit) continue; if (!limit) continue;
const unit = intervalLimitKeyToUnit(key); const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); const periodStartDates = this.dependencies.userAvailabilityService.getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) { for (const periodStart of periodStartDates) {
if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
@@ -392,7 +393,7 @@ export class AvailableSlotsService {
if (!limit) continue; if (!limit) continue;
const unit = intervalLimitKeyToUnit(key); const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); const periodStartDates = this.dependencies.userAvailabilityService.getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) { for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
@@ -443,7 +444,7 @@ export class AvailableSlotsService {
if (!limit) continue; if (!limit) continue;
const unit = intervalLimitKeyToUnit(key); const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); const periodStartDates = this.dependencies.userAvailabilityService.getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) { for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
@@ -543,7 +544,7 @@ export class AvailableSlotsService {
if (!limit) continue; if (!limit) continue;
const unit = intervalLimitKeyToUnit(key); const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); const periodStartDates = this.dependencies.userAvailabilityService.getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) { for (const periodStart of periodStartDates) {
if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
@@ -591,7 +592,7 @@ export class AvailableSlotsService {
if (!limit) continue; if (!limit) continue;
const unit = intervalLimitKeyToUnit(key); const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); const periodStartDates = this.dependencies.userAvailabilityService.getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) { for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
@@ -818,7 +819,7 @@ export class AvailableSlotsService {
const users = enrichUsersWithData(); const users = enrichUsersWithData();
// TODO: DI getUsersAvailability // TODO: DI getUsersAvailability
const premappedUsersAvailability = await getUsersAvailability({ const premappedUsersAvailability = await this.dependencies.userAvailabilityService.getUsersAvailability({
users, users,
query: { query: {
dateFrom: startTime.format(), dateFrom: startTime.format(),
@@ -1,5 +1,5 @@
import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server"; import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server";
import { getUserAvailability } from "@calcom/lib/getUserAvailability"; import { getUserAvailabilityService } from "@calcom/lib/di/containers/get-user-availability";
import { isTeamMember } from "@calcom/lib/server/queries/teams"; import { isTeamMember } from "@calcom/lib/server/queries/teams";
import { MembershipRepository } from "@calcom/lib/server/repository/membership"; import { MembershipRepository } from "@calcom/lib/server/repository/membership";
import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import type { TrpcSessionUser } from "@calcom/trpc/server/types";
@@ -16,6 +16,7 @@ type GetMemberAvailabilityOptions = {
}; };
export const getMemberAvailabilityHandler = async ({ ctx, input }: GetMemberAvailabilityOptions) => { export const getMemberAvailabilityHandler = async ({ ctx, input }: GetMemberAvailabilityOptions) => {
const userAvailabilityService = getUserAvailabilityService()
const team = await isTeamMember(ctx.user?.id, input.teamId); const team = await isTeamMember(ctx.user?.id, input.teamId);
if (!team) throw new TRPCError({ code: "UNAUTHORIZED" }); if (!team) throw new TRPCError({ code: "UNAUTHORIZED" });
@@ -32,7 +33,7 @@ export const getMemberAvailabilityHandler = async ({ ctx, input }: GetMemberAvai
}); });
// get availability for this member // get availability for this member
return await getUserAvailability( return await userAvailabilityService.getUserAvailability(
{ {
username: username, username: username,
dateFrom: input.dateFrom, dateFrom: input.dateFrom,
+5 -5
View File
@@ -2500,7 +2500,7 @@ __metadata:
"@axiomhq/winston": ^1.2.0 "@axiomhq/winston": ^1.2.0
"@calcom/platform-constants": "*" "@calcom/platform-constants": "*"
"@calcom/platform-enums": "*" "@calcom/platform-enums": "*"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.276" "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.283"
"@calcom/platform-types": "*" "@calcom/platform-types": "*"
"@calcom/platform-utils": "*" "@calcom/platform-utils": "*"
"@calcom/prisma": "*" "@calcom/prisma": "*"
@@ -3556,13 +3556,13 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.276": "@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.283":
version: 0.0.276 version: 0.0.283
resolution: "@calcom/platform-libraries@npm:0.0.276" resolution: "@calcom/platform-libraries@npm:0.0.283"
dependencies: dependencies:
"@calcom/features": "*" "@calcom/features": "*"
"@calcom/lib": "*" "@calcom/lib": "*"
checksum: 4effc186c0301ec35496ebffbe32280f26261667b60057d18207b30636b038cdcc3783b95390eecc8460f3203d0cfe5d732e3b3f89031ff064b1a556321779a8 checksum: 7d741bd13ac050f8552855658b2348cba0999529c12ca6741e5f802a7bc9ce4e29c9ac69a17306c42beb392f17acb7b5f8a0554b8d2ea9b5fc6d963950d0fca8
languageName: node languageName: node
linkType: hard linkType: hard