Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d0dc3f546 | ||
|
|
e4baf15b53 | ||
|
|
a1bd8b6259 | ||
|
|
7987c9f45d | ||
|
|
781d0d92df | ||
|
|
b71d8baccc | ||
|
|
a71d949a0c | ||
|
|
e55cee443b | ||
|
|
c3634b3aba | ||
|
|
cd576f5378 |
@@ -22,6 +22,7 @@ env:
|
|||||||
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
|
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.286",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
|
|||||||
@@ -106,8 +106,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||||
const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy;
|
const rescheduledByEmail = databaseBooking.rescheduled
|
||||||
|
? rescheduledToInfo?.rescheduledBy
|
||||||
|
: databaseBooking.rescheduledBy;
|
||||||
|
|
||||||
const booking = {
|
const booking = {
|
||||||
id: databaseBooking.id,
|
id: databaseBooking.id,
|
||||||
@@ -159,11 +160,10 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
||||||
return {
|
return {
|
||||||
uid: rescheduledTo?.uid,
|
uid: rescheduledTo?.uid,
|
||||||
rescheduledBy: rescheduledTo?.rescheduledBy
|
rescheduledBy: rescheduledTo?.rescheduledBy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||||
if (databaseMetadata === null) return {};
|
if (databaseMetadata === null) return {};
|
||||||
|
|
||||||
@@ -281,7 +281,9 @@ export class OutputBookingsService_2024_08_13 {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||||
const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy;
|
const rescheduledByEmail = databaseBooking.rescheduled
|
||||||
|
? rescheduledToInfo?.rescheduledBy
|
||||||
|
: databaseBooking.rescheduledBy;
|
||||||
|
|
||||||
const booking = {
|
const booking = {
|
||||||
id: databaseBooking.id,
|
id: databaseBooking.id,
|
||||||
|
|||||||
+4
-1
@@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
|||||||
import { CacheService } from "@/lib/services/cache.service";
|
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 { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
@@ -24,10 +26,12 @@ import { Module } from "@nestjs/common";
|
|||||||
PrismaEventTypeRepository,
|
PrismaEventTypeRepository,
|
||||||
PrismaRoutingFormResponseRepository,
|
PrismaRoutingFormResponseRepository,
|
||||||
PrismaTeamRepository,
|
PrismaTeamRepository,
|
||||||
|
RedisService,
|
||||||
PrismaFeaturesRepository,
|
PrismaFeaturesRepository,
|
||||||
CheckBookingLimitsService,
|
CheckBookingLimitsService,
|
||||||
CacheService,
|
CacheService,
|
||||||
AvailableSlotsService,
|
AvailableSlotsService,
|
||||||
|
UserAvailabilityService
|
||||||
],
|
],
|
||||||
exports: [AvailableSlotsService],
|
exports: [AvailableSlotsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"
|
|||||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||||
import { CacheService } from "@/lib/services/cache.service";
|
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 { RedisService } from "@/modules/redis/redis.service";
|
||||||
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 {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -24,6 +27,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
selectedSlotRepository: PrismaSelectedSlotRepository,
|
selectedSlotRepository: PrismaSelectedSlotRepository,
|
||||||
eventTypeRepository: PrismaEventTypeRepository,
|
eventTypeRepository: PrismaEventTypeRepository,
|
||||||
userRepository: PrismaUserRepository,
|
userRepository: PrismaUserRepository,
|
||||||
|
redisService: RedisService,
|
||||||
featuresRepository: PrismaFeaturesRepository
|
featuresRepository: PrismaFeaturesRepository
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
@@ -35,8 +39,15 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
selectedSlotRepo: selectedSlotRepository,
|
selectedSlotRepo: selectedSlotRepository,
|
||||||
eventTypeRepo: eventTypeRepository,
|
eventTypeRepo: eventTypeRepository,
|
||||||
userRepo: userRepository,
|
userRepo: userRepository,
|
||||||
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository) as any,
|
redisClient: redisService,
|
||||||
|
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository),
|
||||||
cacheService: new CacheService(featuresRepository),
|
cacheService: new CacheService(featuresRepository),
|
||||||
|
userAvailabilityService: new UserAvailabilityService(
|
||||||
|
oooRepoDependency,
|
||||||
|
bookingRepository,
|
||||||
|
eventTypeRepository,
|
||||||
|
redisService
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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 { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
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,
|
||||||
|
redisService: RedisService
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
oooRepo: oooRepoDependency,
|
||||||
|
bookingRepo: bookingRepository,
|
||||||
|
eventTypeRepo: eventTypeRepository,
|
||||||
|
redisClient: redisService,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -7,12 +7,32 @@ import { Redis } from "ioredis";
|
|||||||
export class RedisService implements OnModuleDestroy {
|
export class RedisService implements OnModuleDestroy {
|
||||||
public redis: Redis;
|
public redis: Redis;
|
||||||
private readonly logger = new Logger("RedisService");
|
private readonly logger = new Logger("RedisService");
|
||||||
|
private isReady = false; // Track connection status
|
||||||
|
|
||||||
constructor(readonly configService: ConfigService<AppConfig>) {
|
constructor(readonly configService: ConfigService<AppConfig>) {
|
||||||
const dbUrl = configService.get<string>("db.redisUrl", { infer: true });
|
const dbUrl = configService.get<string>("db.redisUrl", { infer: true });
|
||||||
if (!dbUrl) throw new Error("Misconfigured Redis, halting.");
|
if (!dbUrl) throw new Error("Misconfigured Redis, halting.");
|
||||||
|
|
||||||
this.redis = new Redis(dbUrl);
|
this.redis = new Redis(dbUrl);
|
||||||
|
|
||||||
|
this.redis.on("error", (err) => {
|
||||||
|
this.logger.error(`IoRedis connection error: ${err.message}`);
|
||||||
|
this.isReady = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redis.on("connect", () => {
|
||||||
|
this.logger.log("IoRedis connected!");
|
||||||
|
this.isReady = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redis.on("reconnecting", (delay: string) => {
|
||||||
|
this.logger.warn(`IoRedis reconnecting... next retry in ${delay}ms`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redis.on("end", () => {
|
||||||
|
this.logger.warn("IoRedis connection ended.");
|
||||||
|
this.isReady = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
@@ -22,4 +42,99 @@ export class RedisService implements OnModuleDestroy {
|
|||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get<TData>(key: string): Promise<TData | null> {
|
||||||
|
let data = null;
|
||||||
|
if (!this.isReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = await this.redis.get(key);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) this.logger.error(`IoRedis get failed: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(data) as TData;
|
||||||
|
} catch (e) {
|
||||||
|
return data as TData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(key: string): Promise<number> {
|
||||||
|
if (!this.isReady) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return this.redis.del(key);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) this.logger.error(`IoRedis del failed: ${err.message}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<TData>(key: string, value: TData, opts?: { ttl?: number }): Promise<"OK" | TData | null> {
|
||||||
|
if (!this.isReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stringifiedValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||||
|
if (opts?.ttl) {
|
||||||
|
await this.redis.set(key, stringifiedValue, "PX", opts.ttl);
|
||||||
|
} else {
|
||||||
|
await this.redis.set(key, stringifiedValue);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) this.logger.error(`IoRedis set failed: ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
async expire(key: string, seconds: number): Promise<0 | 1> {
|
||||||
|
if (!this.isReady) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return this.redis.expire(key, seconds) as Promise<0 | 1>;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) this.logger.error(`IoRedis expire failed: ${err.message}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async lrange<TResult = string>(key: string, start: number, end: number): Promise<TResult[]> {
|
||||||
|
if (!this.isReady) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const results = await this.redis.lrange(key, start, end);
|
||||||
|
return results.map((item) => JSON.parse(item) as TResult);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) this.logger.error(`IoRedis lrange failed: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async lpush<TData>(key: string, ...elements: TData[]): Promise<number> {
|
||||||
|
if (!this.isReady) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const stringifiedElements = elements.map((element) =>
|
||||||
|
typeof element === "object" ? JSON.stringify(element) : String(element)
|
||||||
|
);
|
||||||
|
return this.redis.lpush(key, ...stringifiedElements);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) this.logger.error(`IoRedis lpush failed: ${err.message}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,4 +36,5 @@ process.env = {
|
|||||||
CALENDSO_ENCRYPTION_KEY: "22gfxhWUlcKliUeXcu8xNah2+HP/29ZX",
|
CALENDSO_ENCRYPTION_KEY: "22gfxhWUlcKliUeXcu8xNah2+HP/29ZX",
|
||||||
INTEGRATION_TEST_MODE: "true",
|
INTEGRATION_TEST_MODE: "true",
|
||||||
e2e: "true",
|
e2e: "true",
|
||||||
|
SLOTS_CACHE_TTL: "1"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||||
|
|
||||||
|
import { handleQueuedFormResponseCleanup } from "@calcom/app-store/routing-forms/cron/queuedFormResponseCleanup";
|
||||||
|
|
||||||
|
export const POST = defaultResponderForAppDir(handleQueuedFormResponseCleanup);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.5.11",
|
"version": "5.5.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"@team-plain/typescript-sdk": "^5.9.0",
|
"@team-plain/typescript-sdk": "^5.9.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
"@unkey/ratelimit": "^0.1.1",
|
"@unkey/ratelimit": "^0.1.1",
|
||||||
"@upstash/redis": "^1.21.0",
|
"@upstash/redis": "^1.35.2",
|
||||||
"@vercel/edge-config": "^0.1.1",
|
"@vercel/edge-config": "^0.1.1",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
"@vercel/og": "^0.6.3",
|
"@vercel/og": "^0.6.3",
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
"path": "/api/cron/calendar-cache-cleanup",
|
"path": "/api/cron/calendar-cache-cleanup",
|
||||||
"schedule": "0 5 * * *"
|
"schedule": "0 5 * * *"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "/api/cron/queuedFormResponseCleanup",
|
||||||
|
"schedule": "0 */12 * * *"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "/api/tasks/cron",
|
"path": "/api/tasks/cron",
|
||||||
"schedule": "* * * * *"
|
"schedule": "* * * * *"
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
"@daily-co/daily-js": "^0.76.0",
|
"@daily-co/daily-js": "^0.76.0",
|
||||||
"@evyweb/ioctopus": "^1.2.0",
|
"@evyweb/ioctopus": "^1.2.0",
|
||||||
"@next/third-parties": "^14.2.5",
|
"@next/third-parties": "^14.2.5",
|
||||||
|
"@unkey/ratelimit": "^2.1.2",
|
||||||
"@vercel/functions": "^1.4.0",
|
"@vercel/functions": "^1.4.0",
|
||||||
"city-timezones": "^1.2.1",
|
"city-timezones": "^1.2.1",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
|
|||||||
@@ -1114,4 +1114,18 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMainTimeZone(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const primaryCalendar = await this.getPrimaryCalendar();
|
||||||
|
if (!primaryCalendar?.timeZone) {
|
||||||
|
this.log.warn("No timezone found in primary calendar, defaulting to UTC");
|
||||||
|
return "UTC";
|
||||||
|
}
|
||||||
|
return primaryCalendar.timeZone;
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error("Error getting main timezone from Google Calendar", { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -625,4 +625,21 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async getMainTimeZone(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await this.fetcher(`${await this.getUserEndpoint()}/mailboxSettings/timeZone`);
|
||||||
|
const timezone = await handleErrorsJson<string>(response);
|
||||||
|
|
||||||
|
if (!timezone) {
|
||||||
|
this.log.warn("No timezone found in mailbox settings, defaulting to Europe/London");
|
||||||
|
return "Europe/London";
|
||||||
|
}
|
||||||
|
|
||||||
|
return timezone;
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error("Error getting main timezone from Office365 Calendar", { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import logger from "@calcom/lib/logger";
|
||||||
|
import { PrismaQueuedFormResponseRepository } from "@calcom/lib/server/repository/routingForm/queuedFormResponse/PrismaQueuedFormResponseRepository";
|
||||||
|
import { QueuedFormResponseService } from "@calcom/lib/server/service/routingForm/queuedFormResponse/QueuedFormResponseService";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
function validateRequest(request: NextRequest) {
|
||||||
|
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
|
||||||
|
|
||||||
|
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
|
||||||
|
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleQueuedFormResponseCleanup(request: NextRequest) {
|
||||||
|
const authError = validateRequest(request);
|
||||||
|
if (authError) {
|
||||||
|
return authError;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const queuedFormResponseRepo = new PrismaQueuedFormResponseRepository(prisma);
|
||||||
|
const queuedFormResponseService = new QueuedFormResponseService({
|
||||||
|
logger,
|
||||||
|
queuedFormResponseRepo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await queuedFormResponseService.cleanupExpiredResponses({
|
||||||
|
batchSize: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
status: "success",
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during queued form response cleanup:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ status: "error", message: "Failed to cleanup queued form responses" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/eslint-plugin-eslint": "*",
|
"@calcom/eslint-plugin-eslint": "*",
|
||||||
"@todesktop/tailwind-variants": "^1.0.0",
|
"@todesktop/tailwind-variants": "^1.0.0",
|
||||||
"eslint-config-next": "^15.4.5",
|
"eslint-config-next": "^13.2.1",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-config-turbo": "^0.0.7",
|
"eslint-config-turbo": "^0.0.7",
|
||||||
"eslint-plugin-playwright": "^0.12.0",
|
"eslint-plugin-playwright": "^0.12.0",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
export interface IRedisService {
|
export interface IRedisService {
|
||||||
get: <TData>(key: string) => Promise<TData | null>;
|
get: <TData>(key: string) => Promise<TData | null>;
|
||||||
|
|
||||||
set: <TData>(key: string, value: TData) => Promise<"OK" | TData | null>;
|
set: <TData>(key: string, value: TData, opts?: { ttl?: number }) => Promise<"OK" | TData | null>;
|
||||||
|
|
||||||
expire: (key: string, seconds: number) => Promise<0 | 1>;
|
expire: (key: string, seconds: number) => Promise<0 | 1>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { IRedisService } from "./IRedisService";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Noop implementation of IRedisService for testing or fallback scenarios.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class NoopRedisService implements IRedisService {
|
||||||
|
async get<TData>(_key: string): Promise<TData | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(_key: string): Promise<number> {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<TData>(_key: string, _value: TData, _opts?: { ttl?: number }): Promise<"OK" | TData | null> {
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
async expire(_key: string, _seconds: number): Promise<0 | 1> {
|
||||||
|
// Implementation for setting expiration time for key in Redis
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async lrange<TResult = string>(_key: string, _start: number, _end: number): Promise<TResult[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async lpush<TData>(_key: string, ..._elements: TData[]): Promise<number> {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,13 @@ export class RedisService implements IRedisService {
|
|||||||
private redis: Redis;
|
private redis: Redis;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.redis = Redis.fromEnv();
|
// Ensure we throw an Error to mimick old behavior
|
||||||
|
if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) {
|
||||||
|
throw new Error("Attempted to initialize Upstash Redis client without url or token.");
|
||||||
|
}
|
||||||
|
this.redis = Redis.fromEnv({
|
||||||
|
signal: () => AbortSignal.timeout(2000),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<TData>(key: string): Promise<TData | null> {
|
async get<TData>(key: string): Promise<TData | null> {
|
||||||
@@ -17,9 +23,16 @@ export class RedisService implements IRedisService {
|
|||||||
return this.redis.del(key);
|
return this.redis.del(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async set<TData>(key: string, value: TData): Promise<"OK" | TData | null> {
|
async set<TData>(key: string, value: TData, opts?: { ttl?: number }): Promise<"OK" | TData | null> {
|
||||||
// Implementation for setting value in Redis
|
return this.redis.set(
|
||||||
return this.redis.set(key, value);
|
key,
|
||||||
|
value,
|
||||||
|
opts?.ttl
|
||||||
|
? {
|
||||||
|
px: opts.ttl,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async expire(key: string, seconds: number): Promise<0 | 1> {
|
async expire(key: string, seconds: number): Promise<0 | 1> {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { createModule } from "@evyweb/ioctopus";
|
||||||
|
|
||||||
|
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||||
|
|
||||||
|
import { NoopRedisService } from "../NoopRedisService";
|
||||||
|
import { RedisService } from "../RedisService";
|
||||||
|
|
||||||
|
const redisModule = createModule();
|
||||||
|
|
||||||
|
redisModule.bind(DI_TOKENS.REDIS_CLIENT).toFactory(() => {
|
||||||
|
if (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) {
|
||||||
|
return new RedisService();
|
||||||
|
}
|
||||||
|
return new NoopRedisService();
|
||||||
|
}, "singleton");
|
||||||
|
|
||||||
|
export { redisModule };
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createContainer } from "@evyweb/ioctopus";
|
import { createContainer } from "@evyweb/ioctopus";
|
||||||
|
|
||||||
|
import { redisModule } from "@calcom/features/redis/di/redisModule";
|
||||||
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||||
import { prismaModule } from "@calcom/prisma/prisma.module";
|
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||||
import type { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util";
|
import type { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||||
@@ -16,8 +17,10 @@ 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.REDIS_CLIENT, redisModule);
|
||||||
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
|
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
|
||||||
container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
|
container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
|
||||||
container.load(DI_TOKENS.SCHEDULE_REPOSITORY_MODULE, scheduleRepositoryModule);
|
container.load(DI_TOKENS.SCHEDULE_REPOSITORY_MODULE, scheduleRepositoryModule);
|
||||||
@@ -31,7 +34,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,26 @@
|
|||||||
|
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 { redisModule } from "@calcom/features/redis/di/redisModule";
|
||||||
|
|
||||||
|
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);
|
||||||
|
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
|
||||||
|
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
|
|||||||
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
|
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
|
||||||
eventTypeRepo: DI_TOKENS.EVENT_TYPE_REPOSITORY,
|
eventTypeRepo: DI_TOKENS.EVENT_TYPE_REPOSITORY,
|
||||||
routingFormResponseRepo: DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY,
|
routingFormResponseRepo: DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY,
|
||||||
|
redisClient: DI_TOKENS.REDIS_CLIENT,
|
||||||
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,12 @@
|
|||||||
|
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,
|
||||||
|
redisClient: DI_TOKENS.REDIS_CLIENT,
|
||||||
|
} 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>);
|
||||||
@@ -2,6 +2,7 @@ export const DI_TOKENS = {
|
|||||||
PRISMA_CLIENT: Symbol("PrismaClient"),
|
PRISMA_CLIENT: Symbol("PrismaClient"),
|
||||||
READ_ONLY_PRISMA_CLIENT: Symbol("ReadOnlyPrismaClient"),
|
READ_ONLY_PRISMA_CLIENT: Symbol("ReadOnlyPrismaClient"),
|
||||||
PRISMA_MODULE: Symbol("PrismaModule"),
|
PRISMA_MODULE: Symbol("PrismaModule"),
|
||||||
|
REDIS_CLIENT: Symbol("RedisClient"),
|
||||||
OOO_REPOSITORY: Symbol("OOORepository"),
|
OOO_REPOSITORY: Symbol("OOORepository"),
|
||||||
OOO_REPOSITORY_MODULE: Symbol("OOORepositoryModule"),
|
OOO_REPOSITORY_MODULE: Symbol("OOORepositoryModule"),
|
||||||
SCHEDULE_REPOSITORY: Symbol("ScheduleRepository"),
|
SCHEDULE_REPOSITORY: Symbol("ScheduleRepository"),
|
||||||
@@ -22,6 +23,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 +33,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"),
|
||||||
};
|
};
|
||||||
|
|||||||
+204
-266
@@ -9,8 +9,10 @@ import type {
|
|||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import type { IRedisService } from "@calcom/features/redis/IRedisService";
|
||||||
import { getWorkingHours } from "@calcom/lib/availability";
|
import { getWorkingHours } from "@calcom/lib/availability";
|
||||||
import type { DateOverride, WorkingHours } from "@calcom/lib/date-ranges";
|
import type { DateOverride, WorkingHours } from "@calcom/lib/date-ranges";
|
||||||
import { buildDateRanges, subtract } from "@calcom/lib/date-ranges";
|
import { buildDateRanges, subtract } from "@calcom/lib/date-ranges";
|
||||||
@@ -26,10 +28,10 @@ import {
|
|||||||
import logger from "@calcom/lib/logger";
|
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 type { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||||
import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository";
|
import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository";
|
||||||
import prisma from "@calcom/prisma";
|
import type { PrismaOOORepository } from "@calcom/lib/server/repository/ooo";
|
||||||
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";
|
||||||
@@ -55,106 +57,9 @@ 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<ReturnType<(typeof UserAvailabilityService)["prototype"]["_getEventType"]>>;
|
||||||
const eventType = await prisma.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),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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,133 @@ 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;
|
||||||
|
redisClient: IRedisService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserAvailabilityService {
|
||||||
|
constructor(public readonly dependencies: IUserAvailabilityService) {}
|
||||||
|
|
||||||
|
// Fetch timezones from outlook or google using delegated credentials (formely known as domain wide delegatiion)
|
||||||
|
async getTimezoneFromDelegatedCalendars(user: GetAvailabilityUser): Promise<string | null> {
|
||||||
|
if (!user.credentials || user.credentials.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delegatedCredentials = user.credentials.filter(
|
||||||
|
(credential) => credential.type.endsWith("_calendar") && Boolean(credential.delegatedToId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!delegatedCredentials || delegatedCredentials.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `user-timezone:${user.id}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cachedTimezone = await this.dependencies.redisClient.get<string>(cacheKey);
|
||||||
|
|
||||||
|
if (cachedTimezone) {
|
||||||
|
log.debug(`Got timezone ${cachedTimezone} from Redis cache for user ${user.id}`);
|
||||||
|
return cachedTimezone;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.warn(`Failed to get timezone from Redis cache for user ${user.id}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delegatedCredentials.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const credential of delegatedCredentials) {
|
||||||
|
try {
|
||||||
|
const calendar = await getCalendar(credential);
|
||||||
|
if (calendar && "getMainTimeZone" in calendar && typeof calendar.getMainTimeZone === "function") {
|
||||||
|
const timezone = await calendar.getMainTimeZone();
|
||||||
|
if (timezone && timezone !== "UTC") {
|
||||||
|
log.debug(`Got timezone ${timezone} from calendar service ${credential.type}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.dependencies.redisClient.set<string>(cacheKey, timezone, { ttl: 3600 * 6 * 1000 }); // 6 hours ttl in ms;
|
||||||
|
log.debug(`Cached timezone ${timezone} in Redis for user ${user.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
log.warn(`Failed to set timezone in Redis cache for user ${user.id}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timezone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.warn(`Failed to get timezone from calendar service ${credential.type}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +250,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,24 +258,10 @@ 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({
|
||||||
where: {
|
|
||||||
eventTypeId: id,
|
eventTypeId: id,
|
||||||
startTime: {
|
dateFrom: dateFrom.format(),
|
||||||
gte: dateFrom.format(),
|
dateTo: dateTo.format(),
|
||||||
lte: dateTo.format(),
|
|
||||||
},
|
|
||||||
status: BookingStatus.ACCEPTED,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
uid: true,
|
|
||||||
startTime: true,
|
|
||||||
attendees: {
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return bookings.map((booking) => {
|
return bookings.map((booking) => {
|
||||||
@@ -260,19 +277,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 +311,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(
|
||||||
@@ -340,10 +350,15 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
|||||||
timeZone: fallbackTimezoneIfScheduleIsMissing,
|
timeZone: fallbackTimezoneIfScheduleIsMissing,
|
||||||
};
|
};
|
||||||
|
|
||||||
const schedule =
|
// possible timezones that have been set by or for a user
|
||||||
(eventType?.schedule ? eventType.schedule : hostSchedule ? hostSchedule : userSchedule) ??
|
const potentialSchedule = eventType?.schedule
|
||||||
fallbackSchedule;
|
? eventType.schedule
|
||||||
const timeZone = schedule?.timeZone || fallbackTimezoneIfScheduleIsMissing;
|
: hostSchedule
|
||||||
|
? hostSchedule
|
||||||
|
: userSchedule;
|
||||||
|
|
||||||
|
// if no schedules set by or for a user, use fallbackSchedule
|
||||||
|
const schedule = potentialSchedule ?? fallbackSchedule;
|
||||||
|
|
||||||
const bookingLimits =
|
const bookingLimits =
|
||||||
eventType?.bookingLimits &&
|
eventType?.bookingLimits &&
|
||||||
@@ -359,6 +374,25 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
|||||||
? parseDurationLimit(eventType.durationLimits)
|
? parseDurationLimit(eventType.durationLimits)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// TODO: only query what we need after applying limits (shrink date range)
|
||||||
|
const getBusyTimesStart = dateFrom.toISOString();
|
||||||
|
const getBusyTimesEnd = dateTo.toISOString();
|
||||||
|
|
||||||
|
const selectedCalendars = eventType?.useEventLevelSelectedCalendars
|
||||||
|
? EventTypeRepository.getSelectedCalendarsFromUser({ user, eventTypeId: eventType.id })
|
||||||
|
: user.userLevelSelectedCalendars;
|
||||||
|
|
||||||
|
const isTimezoneSet = Boolean(potentialSchedule && potentialSchedule.timeZone !== null);
|
||||||
|
|
||||||
|
// this timezone is synced with google/outlook calendars timezone usingg delegated credentials
|
||||||
|
// it's a fallback for delegated credentials users who want to sync their timezone with third party calendars
|
||||||
|
const calendarTimezone = !isTimezoneSet ? await this.getTimezoneFromDelegatedCalendars(user) : null;
|
||||||
|
|
||||||
|
const finalTimezone =
|
||||||
|
!isTimezoneSet && calendarTimezone
|
||||||
|
? calendarTimezone
|
||||||
|
: schedule?.timeZone || fallbackTimezoneIfScheduleIsMissing;
|
||||||
|
|
||||||
let busyTimesFromLimits: EventBusyDetails[] = [];
|
let busyTimesFromLimits: EventBusyDetails[] = [];
|
||||||
|
|
||||||
if (initialData?.busyTimesFromLimits && initialData?.eventTypeForLimits) {
|
if (initialData?.busyTimesFromLimits && initialData?.eventTypeForLimits) {
|
||||||
@@ -368,12 +402,12 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
|||||||
busyTimesFromLimits = await getBusyTimesFromLimits(
|
busyTimesFromLimits = await getBusyTimesFromLimits(
|
||||||
bookingLimits,
|
bookingLimits,
|
||||||
durationLimits,
|
durationLimits,
|
||||||
dateFrom.tz(timeZone),
|
dateFrom.tz(finalTimezone),
|
||||||
dateTo.tz(timeZone),
|
dateTo.tz(finalTimezone),
|
||||||
duration,
|
duration,
|
||||||
eventType,
|
eventType,
|
||||||
initialData?.busyTimesFromLimitsBookings ?? [],
|
initialData?.busyTimesFromLimitsBookings ?? [],
|
||||||
timeZone,
|
finalTimezone,
|
||||||
initialData?.rescheduleUid ?? undefined
|
initialData?.rescheduleUid ?? undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -394,23 +428,15 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
|||||||
busyTimesFromTeamLimits = await getBusyTimesFromTeamLimits(
|
busyTimesFromTeamLimits = await getBusyTimesFromTeamLimits(
|
||||||
user,
|
user,
|
||||||
teamBookingLimits,
|
teamBookingLimits,
|
||||||
dateFrom.tz(timeZone),
|
dateFrom.tz(finalTimezone),
|
||||||
dateTo.tz(timeZone),
|
dateTo.tz(finalTimezone),
|
||||||
teamForBookingLimits.id,
|
teamForBookingLimits.id,
|
||||||
teamForBookingLimits.includeManagedEventsInLimits,
|
teamForBookingLimits.includeManagedEventsInLimits,
|
||||||
timeZone,
|
finalTimezone,
|
||||||
initialData?.rescheduleUid ?? undefined
|
initialData?.rescheduleUid ?? undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: only query what we need after applying limits (shrink date range)
|
|
||||||
const getBusyTimesStart = dateFrom.toISOString();
|
|
||||||
const getBusyTimesEnd = dateTo.toISOString();
|
|
||||||
|
|
||||||
const selectedCalendars = eventType?.useEventLevelSelectedCalendars
|
|
||||||
? EventTypeRepository.getSelectedCalendarsFromUser({ user, eventTypeId: eventType.id })
|
|
||||||
: user.userLevelSelectedCalendars;
|
|
||||||
|
|
||||||
let busyTimes = [];
|
let busyTimes = [];
|
||||||
try {
|
try {
|
||||||
busyTimes = await getBusyTimes({
|
busyTimes = await getBusyTimes({
|
||||||
@@ -435,7 +461,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
|||||||
log.error(`Error fetching busy times for user ${username}:`, error);
|
log.error(`Error fetching busy times for user ${username}:`, error);
|
||||||
return {
|
return {
|
||||||
busy: [],
|
busy: [],
|
||||||
timeZone,
|
timeZone: finalTimezone,
|
||||||
dateRanges: [],
|
dateRanges: [],
|
||||||
oooExcludedDateRanges: [],
|
oooExcludedDateRanges: [],
|
||||||
workingHours: [],
|
workingHours: [],
|
||||||
@@ -469,7 +495,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 });
|
||||||
}
|
}
|
||||||
@@ -481,7 +510,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const workingHours = getWorkingHours({ timeZone }, availability);
|
const workingHours = getWorkingHours({ timeZone: finalTimezone }, availability);
|
||||||
|
|
||||||
const dateOverrides: TimeRange[] = [];
|
const dateOverrides: TimeRange[] = [];
|
||||||
// NOTE: getSchedule is currently calling this function for every user in a team event
|
// NOTE: getSchedule is currently calling this function for every user in a team event
|
||||||
@@ -513,78 +542,19 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
|||||||
|
|
||||||
const outOfOfficeDays =
|
const outOfOfficeDays =
|
||||||
initialData?.outOfOfficeDays ??
|
initialData?.outOfOfficeDays ??
|
||||||
(await prisma.outOfOfficeEntry.findMany({
|
(await this.dependencies.oooRepo.findUserOOODays({
|
||||||
where: {
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
OR: [
|
dateFrom: dateFrom.toISOString(),
|
||||||
// outside of range
|
dateTo: dateTo.toISOString(),
|
||||||
// (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: {
|
|
||||||
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 datesOutOfOffice: IOutOfOfficeData = this.calculateOutOfOfficeRanges(outOfOfficeDays, availability);
|
||||||
|
|
||||||
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
|
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
availability,
|
availability,
|
||||||
timeZone,
|
timeZone: finalTimezone,
|
||||||
travelSchedules: isDefaultSchedule
|
travelSchedules: isDefaultSchedule
|
||||||
? user.travelSchedules.map((schedule) => {
|
? user.travelSchedules.map((schedule) => {
|
||||||
return {
|
return {
|
||||||
@@ -607,7 +577,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
|||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
busy: detailedBusyTimes,
|
busy: detailedBusyTimes,
|
||||||
timeZone,
|
timeZone: finalTimezone,
|
||||||
dateRanges: dateRangesInWhichUserIsAvailable,
|
dateRanges: dateRangesInWhichUserIsAvailable,
|
||||||
oooExcludedDateRanges: dateRangesInWhichUserIsAvailableWithoutOOO,
|
oooExcludedDateRanges: dateRangesInWhichUserIsAvailableWithoutOOO,
|
||||||
workingHours,
|
workingHours,
|
||||||
@@ -621,16 +591,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 +610,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 +659,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 +670,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 +687,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;
|
||||||
|
|||||||
@@ -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 {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+432
@@ -0,0 +1,432 @@
|
|||||||
|
import { describe, expect, beforeAll, afterAll, beforeEach, it } from "vitest";
|
||||||
|
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { PrismaQueuedFormResponseRepository } from "./PrismaQueuedFormResponseRepository";
|
||||||
|
|
||||||
|
const repository = new PrismaQueuedFormResponseRepository(prisma);
|
||||||
|
|
||||||
|
// Test Data Builders
|
||||||
|
const createTestUser = async (overrides?: { email?: string; username?: string }) => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
return prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: overrides?.email || `test-qfr-${timestamp}@example.com`,
|
||||||
|
username: overrides?.username || `test-qfr-${timestamp}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTestForm = async (userId: number, overrides?: { name?: string }) => {
|
||||||
|
return prisma.app_RoutingForms_Form.create({
|
||||||
|
data: {
|
||||||
|
name: overrides?.name || `Test Form ${Date.now()}`,
|
||||||
|
userId,
|
||||||
|
fields: [],
|
||||||
|
routes: [],
|
||||||
|
settings: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFormResponse = async (
|
||||||
|
formId: string,
|
||||||
|
overrides?: {
|
||||||
|
response?: Record<string, unknown>;
|
||||||
|
chosenRouteId?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return prisma.app_RoutingForms_FormResponse.create({
|
||||||
|
data: {
|
||||||
|
formId,
|
||||||
|
response: overrides?.response || { test: "response" },
|
||||||
|
chosenRouteId: overrides?.chosenRouteId || "test-route-id",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createQueuedResponse = async (
|
||||||
|
formId: string,
|
||||||
|
overrides?: {
|
||||||
|
response?: Record<string, unknown>;
|
||||||
|
chosenRouteId?: string;
|
||||||
|
actualResponseId?: number | null;
|
||||||
|
createdAt?: Date;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return prisma.app_RoutingForms_QueuedFormResponse.create({
|
||||||
|
data: {
|
||||||
|
formId,
|
||||||
|
response: overrides?.response || { field1: "value" },
|
||||||
|
chosenRouteId: overrides?.chosenRouteId || "test-route-id",
|
||||||
|
actualResponseId: overrides?.actualResponseId ?? null,
|
||||||
|
...(overrides?.createdAt && { createdAt: overrides.createdAt }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createQueuedResponsesBatch = async (
|
||||||
|
formId: string,
|
||||||
|
count: number,
|
||||||
|
baseOverrides?: Parameters<typeof createQueuedResponse>[1]
|
||||||
|
) => {
|
||||||
|
const responses = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const response = await createQueuedResponse(formId, {
|
||||||
|
...baseOverrides,
|
||||||
|
response: { field1: `value${i}` },
|
||||||
|
});
|
||||||
|
responses.push(response);
|
||||||
|
}
|
||||||
|
return responses;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Date Helpers
|
||||||
|
const daysAgo = (days: number) => new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Assertion Helpers
|
||||||
|
const expectQueuedResponseCount = async (formId: string, expectedCount: number) => {
|
||||||
|
const count = await prisma.app_RoutingForms_QueuedFormResponse.count({
|
||||||
|
where: { formId },
|
||||||
|
});
|
||||||
|
expect(count).toBe(expectedCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectQueuedResponseExists = async (responseId: string) => {
|
||||||
|
const response = await prisma.app_RoutingForms_QueuedFormResponse.findUnique({
|
||||||
|
where: { id: responseId },
|
||||||
|
});
|
||||||
|
expect(response).not.toBeNull();
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectQueuedResponseNotExists = async (responseId: string) => {
|
||||||
|
const response = await prisma.app_RoutingForms_QueuedFormResponse.findUnique({
|
||||||
|
where: { id: responseId },
|
||||||
|
});
|
||||||
|
expect(response).toBeNull();
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectResponseFields = (response: unknown) => {
|
||||||
|
const expectedFields = [
|
||||||
|
"id",
|
||||||
|
"formId",
|
||||||
|
"response",
|
||||||
|
"chosenRouteId",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"actualResponseId",
|
||||||
|
];
|
||||||
|
expectedFields.forEach((field) => {
|
||||||
|
expect(response).toHaveProperty(field);
|
||||||
|
});
|
||||||
|
expect(Object.keys(response as object).sort()).toEqual(expectedFields.sort());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup Helpers
|
||||||
|
const cleanupQueuedResponses = async (formId: string) => {
|
||||||
|
await prisma.app_RoutingForms_QueuedFormResponse.deleteMany({
|
||||||
|
where: { formId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("PrismaQueuedFormResponseRepository Integration Tests", () => {
|
||||||
|
let testForm: { id: string };
|
||||||
|
let testUser: { id: number };
|
||||||
|
let testFormResponse: { id: number };
|
||||||
|
let testResponses: Array<{ id: string; actualResponseId: number | null; createdAt: Date }> = [];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Create test user
|
||||||
|
testUser = await createTestUser();
|
||||||
|
|
||||||
|
// Create test form
|
||||||
|
testForm = await createTestForm(testUser.id);
|
||||||
|
|
||||||
|
// Create test form response for actualResponseId references
|
||||||
|
testFormResponse = await createFormResponse(testForm.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Clean up all test data
|
||||||
|
await prisma.app_RoutingForms_QueuedFormResponse.deleteMany({
|
||||||
|
where: { formId: testForm.id },
|
||||||
|
});
|
||||||
|
await prisma.app_RoutingForms_FormResponse.deleteMany({
|
||||||
|
where: { formId: testForm.id },
|
||||||
|
});
|
||||||
|
await prisma.app_RoutingForms_Form.delete({
|
||||||
|
where: { id: testForm.id },
|
||||||
|
});
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: { id: testUser.id },
|
||||||
|
});
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clear queued responses before each test
|
||||||
|
await cleanupQueuedResponses(testForm.id);
|
||||||
|
testResponses = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findMany", () => {
|
||||||
|
it("should find queued responses by actualResponseId", async () => {
|
||||||
|
// Create additional form responses for testing
|
||||||
|
const formResponse1 = await createFormResponse(testForm.id, {
|
||||||
|
response: { field1: "form-response-1" },
|
||||||
|
});
|
||||||
|
const formResponse2 = await createFormResponse(testForm.id, {
|
||||||
|
response: { field1: "form-response-2" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create test queued responses
|
||||||
|
const queuedResponse1 = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "value1" },
|
||||||
|
actualResponseId: formResponse1.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queuedResponse2 = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "value2" },
|
||||||
|
actualResponseId: formResponse2.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queuedResponse3 = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "value3" },
|
||||||
|
actualResponseId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
testResponses.push(queuedResponse1, queuedResponse2, queuedResponse3);
|
||||||
|
|
||||||
|
// Test finding by specific actualResponseId
|
||||||
|
const result = await repository.findMany({
|
||||||
|
where: { actualResponseId: formResponse1.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].id).toBe(queuedResponse1.id);
|
||||||
|
expect(result[0].actualResponseId).toBe(formResponse1.id);
|
||||||
|
expect(result[0].formId).toBe(testForm.id);
|
||||||
|
expect(result[0].response).toEqual({ field1: "value1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find queued responses by actualResponseId null", async () => {
|
||||||
|
// Create test queued responses
|
||||||
|
await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "value1" },
|
||||||
|
actualResponseId: testFormResponse.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queuedResponse2 = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "value2" },
|
||||||
|
actualResponseId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queuedResponse3 = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "value3" },
|
||||||
|
actualResponseId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test finding by actualResponseId null
|
||||||
|
const result = await repository.findMany({
|
||||||
|
where: { actualResponseId: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
const resultIds = result.map((r) => r.id).sort();
|
||||||
|
const expectedIds = [queuedResponse2.id, queuedResponse3.id].sort();
|
||||||
|
expect(resultIds).toEqual(expectedIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find queued responses created before a specific date", async () => {
|
||||||
|
// Create test queued responses with different timestamps
|
||||||
|
const now = new Date();
|
||||||
|
const oneDayAgo = daysAgo(1);
|
||||||
|
const twoDaysAgo = daysAgo(2);
|
||||||
|
|
||||||
|
// Create responses with specific timestamps
|
||||||
|
const oldResponse1 = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "old1" },
|
||||||
|
createdAt: twoDaysAgo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldResponse2 = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "old2" },
|
||||||
|
createdAt: twoDaysAgo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentResponse = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "recent" },
|
||||||
|
createdAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
testResponses.push(oldResponse1, oldResponse2, recentResponse);
|
||||||
|
|
||||||
|
// Test finding responses created before oneDayAgo
|
||||||
|
const result = await repository.findMany({
|
||||||
|
where: { createdAt: { lt: oneDayAgo } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
const resultIds = result.map((r) => r.id).sort();
|
||||||
|
const expectedIds = [oldResponse1.id, oldResponse2.id].sort();
|
||||||
|
expect(resultIds).toEqual(expectedIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should apply take parameter to limit results", async () => {
|
||||||
|
// Create multiple test queued responses
|
||||||
|
const responses = await createQueuedResponsesBatch(testForm.id, 5, { actualResponseId: null });
|
||||||
|
testResponses.push(...responses);
|
||||||
|
|
||||||
|
// Test with take parameter
|
||||||
|
const result = await repository.findMany({
|
||||||
|
where: { actualResponseId: null },
|
||||||
|
params: { take: 3 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle combined where conditions", async () => {
|
||||||
|
// Create test queued responses
|
||||||
|
const now = new Date();
|
||||||
|
const oneDayAgo = daysAgo(1);
|
||||||
|
const twoDaysAgo = daysAgo(2);
|
||||||
|
|
||||||
|
const oldWithNull = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "old-null" },
|
||||||
|
actualResponseId: null,
|
||||||
|
createdAt: twoDaysAgo,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "old-with-id" },
|
||||||
|
actualResponseId: testFormResponse.id,
|
||||||
|
createdAt: twoDaysAgo,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "recent-null" },
|
||||||
|
actualResponseId: null,
|
||||||
|
createdAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test finding old responses with null actualResponseId
|
||||||
|
const result = await repository.findMany({
|
||||||
|
where: {
|
||||||
|
actualResponseId: null,
|
||||||
|
createdAt: { lt: oneDayAgo },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].id).toBe(oldWithNull.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when where is empty", async () => {
|
||||||
|
await expect(
|
||||||
|
repository.findMany({
|
||||||
|
where: {},
|
||||||
|
})
|
||||||
|
).rejects.toThrow("where is empty");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when where has only undefined values", async () => {
|
||||||
|
await expect(
|
||||||
|
repository.findMany({
|
||||||
|
where: {
|
||||||
|
actualResponseId: undefined,
|
||||||
|
createdAt: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).rejects.toThrow("where is empty");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when no matches found", async () => {
|
||||||
|
const result = await repository.findMany({
|
||||||
|
where: { actualResponseId: 999999 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return all fields defined in safeSelect", async () => {
|
||||||
|
const formResponseForTest = await createFormResponse(testForm.id, {
|
||||||
|
response: { field1: "form-response-test" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await createQueuedResponse(testForm.id, {
|
||||||
|
response: { field1: "test-value" },
|
||||||
|
actualResponseId: formResponseForTest.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await repository.findMany({
|
||||||
|
where: { actualResponseId: formResponseForTest.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
const foundResponse = result[0];
|
||||||
|
|
||||||
|
// Verify all fields from safeSelect are present
|
||||||
|
expect(foundResponse).toHaveProperty("id", response.id);
|
||||||
|
expect(foundResponse).toHaveProperty("formId", response.formId);
|
||||||
|
expect(foundResponse).toHaveProperty("response", response.response);
|
||||||
|
expect(foundResponse).toHaveProperty("chosenRouteId", response.chosenRouteId);
|
||||||
|
expect(foundResponse).toHaveProperty("createdAt");
|
||||||
|
expect(foundResponse).toHaveProperty("updatedAt");
|
||||||
|
expect(foundResponse).toHaveProperty("actualResponseId", response.actualResponseId);
|
||||||
|
|
||||||
|
// Verify no extra fields are included
|
||||||
|
expectResponseFields(foundResponse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteByIds", () => {
|
||||||
|
it("should delete queued responses by their ids", async () => {
|
||||||
|
// Create test queued responses
|
||||||
|
const response1 = await createQueuedResponse(testForm.id, { response: { field1: "value1" } });
|
||||||
|
const response2 = await createQueuedResponse(testForm.id, { response: { field1: "value2" } });
|
||||||
|
const response3 = await createQueuedResponse(testForm.id, { response: { field1: "value3" } });
|
||||||
|
|
||||||
|
// Delete response1 and response2
|
||||||
|
const result = await repository.deleteByIds([response1.id, response2.id]);
|
||||||
|
|
||||||
|
expect(result.count).toBe(2);
|
||||||
|
|
||||||
|
// Verify only response3 remains
|
||||||
|
await expectQueuedResponseCount(testForm.id, 1);
|
||||||
|
await expectQueuedResponseExists(response3.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return count 0 when deleting non-existent ids", async () => {
|
||||||
|
const result = await repository.deleteByIds(["non-existent-id-1", "non-existent-id-2"]);
|
||||||
|
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty array of ids", async () => {
|
||||||
|
const result = await repository.deleteByIds([]);
|
||||||
|
|
||||||
|
expect(result.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle single id deletion", async () => {
|
||||||
|
const response = await createQueuedResponse(testForm.id, { response: { field1: "single-delete" } });
|
||||||
|
|
||||||
|
const result = await repository.deleteByIds([response.id]);
|
||||||
|
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
|
||||||
|
// Verify it's deleted
|
||||||
|
await expectQueuedResponseNotExists(response.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mix of existing and non-existing ids", async () => {
|
||||||
|
const response = await createQueuedResponse(testForm.id, { response: { field1: "mixed-delete" } });
|
||||||
|
|
||||||
|
const result = await repository.deleteByIds([response.id, "non-existent-id"]);
|
||||||
|
|
||||||
|
expect(result.count).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
QueuedFormResponseRepositoryFindManyArgs,
|
||||||
|
QueuedFormResponseRepositoryFindManyWhere,
|
||||||
|
QueuedFormResponseRepositoryInterface,
|
||||||
|
} from "./QueuedFormResponseRepository.interface";
|
||||||
|
|
||||||
|
const safeSelect = {
|
||||||
|
id: true,
|
||||||
|
formId: true,
|
||||||
|
response: true,
|
||||||
|
chosenRouteId: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
actualResponseId: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PrismaQueuedFormResponseRepository implements QueuedFormResponseRepositoryInterface {
|
||||||
|
constructor(private readonly prismaClient: PrismaClient = prisma) {}
|
||||||
|
|
||||||
|
private assertWhereIsNotEmpty(where: QueuedFormResponseRepositoryFindManyWhere) {
|
||||||
|
let found = false;
|
||||||
|
Object.entries(where).forEach(([_, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!found) {
|
||||||
|
throw new Error("where is empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findMany({ where, params }: QueuedFormResponseRepositoryFindManyArgs) {
|
||||||
|
this.assertWhereIsNotEmpty(where);
|
||||||
|
return await this.prismaClient.app_RoutingForms_QueuedFormResponse.findMany({
|
||||||
|
where,
|
||||||
|
select: safeSelect,
|
||||||
|
...(params?.take && { take: params.take }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteByIds(ids: string[]) {
|
||||||
|
return await this.prismaClient.app_RoutingForms_QueuedFormResponse.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: ids,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
export type QueuedFormResponseRepositoryFindManyWhere = {
|
||||||
|
actualResponseId?: number | null;
|
||||||
|
createdAt?: {
|
||||||
|
lt: Date;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueuedFormResponseRepositoryFindManyArgs = {
|
||||||
|
where: QueuedFormResponseRepositoryFindManyWhere;
|
||||||
|
params?: {
|
||||||
|
take?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueuedFormResponseRepositoryFindManyResult = {
|
||||||
|
id: string;
|
||||||
|
formId: string;
|
||||||
|
response: unknown;
|
||||||
|
chosenRouteId: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date | null;
|
||||||
|
actualResponseId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface QueuedFormResponseRepositoryInterface {
|
||||||
|
findMany(
|
||||||
|
args: QueuedFormResponseRepositoryFindManyArgs
|
||||||
|
): Promise<QueuedFormResponseRepositoryFindManyResult[]>;
|
||||||
|
deleteByIds(ids: string[]): Promise<{ count: number }>;
|
||||||
|
}
|
||||||
+1
-1
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -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`;
|
||||||
|
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import type logger from "@calcom/lib/logger";
|
||||||
|
|
||||||
|
import type { QueuedFormResponseRepositoryInterface } from "../../repository/routingForm/queuedFormResponse/QueuedFormResponseRepository.interface";
|
||||||
|
import { QueuedFormResponseService } from "./QueuedFormResponseService";
|
||||||
|
|
||||||
|
describe("QueuedFormResponseService", () => {
|
||||||
|
let service: QueuedFormResponseService;
|
||||||
|
let mockRepo: jest.Mocked<QueuedFormResponseRepositoryInterface>;
|
||||||
|
let mockLogger: ReturnType<typeof createMockLogger>;
|
||||||
|
|
||||||
|
function createMockLogger() {
|
||||||
|
const subLogger = {
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSubLogger: vi.fn().mockReturnValue(subLogger),
|
||||||
|
subLogger,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
mockRepo = {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
deleteByIds: vi.fn(),
|
||||||
|
} as unknown as jest.Mocked<QueuedFormResponseRepositoryInterface>;
|
||||||
|
|
||||||
|
mockLogger = createMockLogger();
|
||||||
|
|
||||||
|
service = new QueuedFormResponseService({
|
||||||
|
logger: mockLogger as unknown as typeof logger,
|
||||||
|
queuedFormResponseRepo: mockRepo,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cleanupExpiredResponses", () => {
|
||||||
|
it("should clean up expired responses in batches", async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
vi.spyOn(Date, "now").mockReturnValue(now);
|
||||||
|
|
||||||
|
// Mock first batch
|
||||||
|
mockRepo.findMany
|
||||||
|
.mockResolvedValueOnce([{ id: "1" }, { id: "2" }] as any)
|
||||||
|
.mockResolvedValueOnce([{ id: "3" }] as any)
|
||||||
|
.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
mockRepo.deleteByIds.mockResolvedValueOnce({ count: 2 }).mockResolvedValueOnce({ count: 1 });
|
||||||
|
|
||||||
|
const resultPromise = service.cleanupExpiredResponses({
|
||||||
|
batchSize: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Advance timers to handle delays
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
const result = await resultPromise;
|
||||||
|
|
||||||
|
// Verify cutoff time calculation (7 days is hardcoded)
|
||||||
|
const expectedCutoffTime = new Date(now - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
expect(mockRepo.findMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
actualResponseId: null,
|
||||||
|
createdAt: {
|
||||||
|
lt: expectedCutoffTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
take: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify deletion calls
|
||||||
|
expect(mockRepo.deleteByIds).toHaveBeenCalledWith(["1", "2"]);
|
||||||
|
expect(mockRepo.deleteByIds).toHaveBeenCalledWith(["3"]);
|
||||||
|
|
||||||
|
// Verify result
|
||||||
|
expect(result).toEqual({
|
||||||
|
count: 3,
|
||||||
|
batches: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use default values when config is not provided", async () => {
|
||||||
|
mockRepo.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const result = await service.cleanupExpiredResponses();
|
||||||
|
const expectedCutoffTime = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Verify default batch size and older than days
|
||||||
|
expect(mockRepo.findMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
actualResponseId: null,
|
||||||
|
createdAt: {
|
||||||
|
lt: expectedCutoffTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
take: 1000, // default batch size
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
count: 0,
|
||||||
|
batches: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle no expired responses", async () => {
|
||||||
|
mockRepo.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const result = await service.cleanupExpiredResponses();
|
||||||
|
|
||||||
|
expect(mockRepo.deleteByIds).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
count: 0,
|
||||||
|
batches: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should stop when batch is not full", async () => {
|
||||||
|
// Return partial batch
|
||||||
|
mockRepo.findMany.mockResolvedValueOnce([{ id: "1" }, { id: "2" }, { id: "3" }]);
|
||||||
|
mockRepo.deleteByIds.mockResolvedValueOnce({ count: 3 });
|
||||||
|
|
||||||
|
const result = await service.cleanupExpiredResponses({ batchSize: 5 });
|
||||||
|
|
||||||
|
expect(mockRepo.findMany).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockRepo.deleteByIds).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toEqual({
|
||||||
|
count: 3,
|
||||||
|
batches: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors during batch processing", async () => {
|
||||||
|
mockRepo.findMany.mockRejectedValueOnce(new Error("Database error"));
|
||||||
|
|
||||||
|
await expect(service.cleanupExpiredResponses()).rejects.toThrow("Database error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
import type logger from "@calcom/lib/logger";
|
||||||
|
|
||||||
|
import type { QueuedFormResponseRepositoryInterface } from "../../../repository/routingForm/queuedFormResponse/QueuedFormResponseRepository.interface";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
logger: typeof logger;
|
||||||
|
queuedFormResponseRepo: QueuedFormResponseRepositoryInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CleanupConfig {
|
||||||
|
batchSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueuedFormResponseService {
|
||||||
|
constructor(private readonly deps: Dependencies) {}
|
||||||
|
|
||||||
|
private getExpiredResponses({ batchSize }: { batchSize: number }) {
|
||||||
|
const cutoffTime = this.getExpiryCutoffTime();
|
||||||
|
return this.deps.queuedFormResponseRepo.findMany({
|
||||||
|
where: {
|
||||||
|
actualResponseId: null,
|
||||||
|
createdAt: {
|
||||||
|
lt: cutoffTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
take: batchSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Calculate the cutoff time. All responses older than this time could be considered expired.
|
||||||
|
*/
|
||||||
|
private getExpiryCutoffTime(): Date {
|
||||||
|
const olderThanDays = 7;
|
||||||
|
return new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired queued form responses in batches
|
||||||
|
*/
|
||||||
|
async cleanupExpiredResponses(config: CleanupConfig = {}): Promise<{
|
||||||
|
count: number;
|
||||||
|
batches: number;
|
||||||
|
}> {
|
||||||
|
const { batchSize = 1000 } = config;
|
||||||
|
|
||||||
|
const log = this.deps.logger.getSubLogger({
|
||||||
|
prefix: ["[QueuedFormResponseService]", "cleanupExpiredResponses"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let totalDeleted = 0;
|
||||||
|
let batchCount = 0;
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
try {
|
||||||
|
const expiredResponses = await this.getExpiredResponses({ batchSize });
|
||||||
|
|
||||||
|
if (expiredResponses.length === 0) {
|
||||||
|
hasMore = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idsToDelete = expiredResponses.map((r: { id: string }) => r.id);
|
||||||
|
|
||||||
|
const deleteResult = await this.deps.queuedFormResponseRepo.deleteByIds(idsToDelete);
|
||||||
|
|
||||||
|
totalDeleted += deleteResult.count;
|
||||||
|
batchCount++;
|
||||||
|
|
||||||
|
log.info(`Batch ${batchCount}: Deleted ${deleteResult.count} records. Total: ${totalDeleted}`);
|
||||||
|
|
||||||
|
// If we found fewer records than the batch size, we're done
|
||||||
|
if (expiredResponses.length < batchSize) {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a small delay between batches to reduce database load
|
||||||
|
if (hasMore) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error processing batch ${batchCount + 1}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Cleanup completed. Total deleted: ${totalDeleted}, Batches: ${batchCount}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: totalDeleted,
|
||||||
|
batches: batchCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
|
|||||||
import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts";
|
import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts";
|
||||||
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
|
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
|
||||||
import type { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
import type { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||||
|
import type { IRedisService } from "@calcom/features/redis/IRedisService";
|
||||||
import { findQualifiedHostsWithDelegationCredentials } from "@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials";
|
import { findQualifiedHostsWithDelegationCredentials } from "@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials";
|
||||||
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
|
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
|
||||||
import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
|
import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
|
||||||
@@ -20,10 +21,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";
|
||||||
@@ -64,6 +65,7 @@ import { handleNotificationWhenNoSlots } from "./handleNotificationWhenNoSlots";
|
|||||||
import type { GetScheduleOptions } from "./types";
|
import type { GetScheduleOptions } from "./types";
|
||||||
|
|
||||||
const log = logger.getSubLogger({ prefix: ["[slots/util]"] });
|
const log = logger.getSubLogger({ prefix: ["[slots/util]"] });
|
||||||
|
const DEFAULT_SLOTS_CACHE_TTL = 2000;
|
||||||
|
|
||||||
type GetAvailabilityUserWithDelegationCredentials = Omit<GetAvailabilityUser, "credentials"> & {
|
type GetAvailabilityUserWithDelegationCredentials = Omit<GetAvailabilityUser, "credentials"> & {
|
||||||
credentials: CredentialForCalendarService[];
|
credentials: CredentialForCalendarService[];
|
||||||
@@ -101,6 +103,47 @@ export interface IAvailableSlotsService {
|
|||||||
routingFormResponseRepo: RoutingFormResponseRepository;
|
routingFormResponseRepo: RoutingFormResponseRepository;
|
||||||
cacheService: CacheService;
|
cacheService: CacheService;
|
||||||
checkBookingLimitsService: CheckBookingLimitsService;
|
checkBookingLimitsService: CheckBookingLimitsService;
|
||||||
|
userAvailabilityService: UserAvailabilityService;
|
||||||
|
redisClient: IRedisService;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withSlotsCache(
|
||||||
|
redisClient: IRedisService,
|
||||||
|
func: (args: GetScheduleOptions) => Promise<IGetAvailableSlots>
|
||||||
|
) {
|
||||||
|
return async (args: GetScheduleOptions): Promise<IGetAvailableSlots> => {
|
||||||
|
const cacheKey = `${JSON.stringify(args.input)}`;
|
||||||
|
let success = false;
|
||||||
|
let cachedResult: IGetAvailableSlots | null = null;
|
||||||
|
const startTime = process.hrtime();
|
||||||
|
try {
|
||||||
|
cachedResult = await redisClient.get(cacheKey);
|
||||||
|
success = true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error && err.name === "TimeoutError") {
|
||||||
|
const endTime = process.hrtime(startTime);
|
||||||
|
log.error(`Redis request timed out after ${endTime[0]}${endTime[1] / 1e6}ms`);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
// If the cache request fails, we proceed to call the function directly
|
||||||
|
return await func(args);
|
||||||
|
}
|
||||||
|
if (cachedResult) {
|
||||||
|
log.info("[CACHE HIT] Available slots", { cacheKey });
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
const result = await func(args);
|
||||||
|
const ttl = parseInt(process.env.SLOTS_CACHE_TTL ?? "", 10) || DEFAULT_SLOTS_CACHE_TTL;
|
||||||
|
// we do not wait for the cache to complete setting; we fire and forget, and hope it'll finish.
|
||||||
|
// this is to already start responding to the client.
|
||||||
|
redisClient.set(cacheKey, result, { ttl });
|
||||||
|
log.info("[CACHE MISS] Available slots", { cacheKey, ttl });
|
||||||
|
return result;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AvailableSlotsService {
|
export class AvailableSlotsService {
|
||||||
@@ -357,7 +400,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 +435,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 +486,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 +586,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 +634,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 +861,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(),
|
||||||
@@ -883,7 +926,10 @@ export class AvailableSlotsService {
|
|||||||
"getRegularOrDynamicEventType"
|
"getRegularOrDynamicEventType"
|
||||||
);
|
);
|
||||||
|
|
||||||
getAvailableSlots = withReporting(this._getAvailableSlots.bind(this), "getAvailableSlots");
|
getAvailableSlots = withReporting(
|
||||||
|
withSlotsCache(this.dependencies.redisClient, this._getAvailableSlots.bind(this)),
|
||||||
|
"getAvailableSlots"
|
||||||
|
);
|
||||||
|
|
||||||
async _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<IGetAvailableSlots> {
|
async _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<IGetAvailableSlots> {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+2
-1
@@ -490,6 +490,7 @@
|
|||||||
"NEXT_PUBLIC_SINGLE_ORG_MODE_ENABLED",
|
"NEXT_PUBLIC_SINGLE_ORG_MODE_ENABLED",
|
||||||
"CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY",
|
"CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY",
|
||||||
"CAL_VIDEO_RECORDING_TOKEN_SECRET",
|
"CAL_VIDEO_RECORDING_TOKEN_SECRET",
|
||||||
"ORGANIZER_EMAIL_EXEMPT_DOMAINS"
|
"ORGANIZER_EMAIL_EXEMPT_DOMAINS",
|
||||||
|
"SLOTS_CACHE_TTL"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.286"
|
||||||
"@calcom/platform-types": "*"
|
"@calcom/platform-types": "*"
|
||||||
"@calcom/platform-utils": "*"
|
"@calcom/platform-utils": "*"
|
||||||
"@calcom/prisma": "*"
|
"@calcom/prisma": "*"
|
||||||
@@ -2836,7 +2836,7 @@ __metadata:
|
|||||||
"@typescript-eslint/eslint-plugin": ^5.52.0
|
"@typescript-eslint/eslint-plugin": ^5.52.0
|
||||||
"@typescript-eslint/parser": ^5.52.0
|
"@typescript-eslint/parser": ^5.52.0
|
||||||
eslint: ^8.34.0
|
eslint: ^8.34.0
|
||||||
eslint-config-next: ^15.4.5
|
eslint-config-next: ^13.2.1
|
||||||
eslint-config-prettier: ^8.6.0
|
eslint-config-prettier: ^8.6.0
|
||||||
eslint-config-turbo: ^0.0.7
|
eslint-config-turbo: ^0.0.7
|
||||||
eslint-plugin-playwright: ^0.12.0
|
eslint-plugin-playwright: ^0.12.0
|
||||||
@@ -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.286":
|
||||||
version: 0.0.276
|
version: 0.0.286
|
||||||
resolution: "@calcom/platform-libraries@npm:0.0.276"
|
resolution: "@calcom/platform-libraries@npm:0.0.286"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@calcom/features": "*"
|
"@calcom/features": "*"
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "*"
|
||||||
checksum: 4effc186c0301ec35496ebffbe32280f26261667b60057d18207b30636b038cdcc3783b95390eecc8460f3203d0cfe5d732e3b3f89031ff064b1a556321779a8
|
checksum: b6402b589456fdc7f0aa86368e75dfcab4ec2c9c813c6bd30efcb7b2c58634441908856a9893788055ca5a71b655a9632ed9f559af7d23bd59293924adcbfe4d
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -4076,7 +4076,7 @@ __metadata:
|
|||||||
"@types/turndown": ^5.0.1
|
"@types/turndown": ^5.0.1
|
||||||
"@types/uuid": 8.3.1
|
"@types/uuid": 8.3.1
|
||||||
"@unkey/ratelimit": ^0.1.1
|
"@unkey/ratelimit": ^0.1.1
|
||||||
"@upstash/redis": ^1.21.0
|
"@upstash/redis": ^1.35.2
|
||||||
"@vercel/edge-config": ^0.1.1
|
"@vercel/edge-config": ^0.1.1
|
||||||
"@vercel/edge-functions-ui": ^0.2.1
|
"@vercel/edge-functions-ui": ^0.2.1
|
||||||
"@vercel/og": ^0.6.3
|
"@vercel/og": ^0.6.3
|
||||||
@@ -8943,6 +8943,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@next/eslint-plugin-next@npm:13.5.11":
|
||||||
|
version: 13.5.11
|
||||||
|
resolution: "@next/eslint-plugin-next@npm:13.5.11"
|
||||||
|
dependencies:
|
||||||
|
glob: 7.1.7
|
||||||
|
checksum: ee960ee89b5bc056c00ba667bcdab69469d792ebf682996a5a7abc34366cf350c45f0cfbbac8311e28ec86766d087c968b40d51a84f1eb9f589a4ff1104a708c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@next/eslint-plugin-next@npm:14.0.4":
|
"@next/eslint-plugin-next@npm:14.0.4":
|
||||||
version: 14.0.4
|
version: 14.0.4
|
||||||
resolution: "@next/eslint-plugin-next@npm:14.0.4"
|
resolution: "@next/eslint-plugin-next@npm:14.0.4"
|
||||||
@@ -8961,15 +8970,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/eslint-plugin-next@npm:15.4.5":
|
|
||||||
version: 15.4.5
|
|
||||||
resolution: "@next/eslint-plugin-next@npm:15.4.5"
|
|
||||||
dependencies:
|
|
||||||
fast-glob: 3.3.1
|
|
||||||
checksum: b377bbff604a3aa0a5c1c480077cc2430186abe18f9d0de3b192dca44106f06407509a2c8744f49788a09222f950c4cf67fd08beaad011af5053f410692616c3
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@npm:13.5.7":
|
"@next/swc-darwin-arm64@npm:13.5.7":
|
||||||
version: 13.5.7
|
version: 13.5.7
|
||||||
resolution: "@next/swc-darwin-arm64@npm:13.5.7"
|
resolution: "@next/swc-darwin-arm64@npm:13.5.7"
|
||||||
@@ -18756,6 +18756,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@unkey/api@npm:2.0.3":
|
||||||
|
version: 2.0.3
|
||||||
|
resolution: "@unkey/api@npm:2.0.3"
|
||||||
|
dependencies:
|
||||||
|
zod: ^3.20.0
|
||||||
|
peerDependencies:
|
||||||
|
"@modelcontextprotocol/sdk": ">=1.5.0 <1.10.0"
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@modelcontextprotocol/sdk":
|
||||||
|
optional: true
|
||||||
|
bin:
|
||||||
|
mcp: bin/mcp-server.js
|
||||||
|
checksum: 24b876dfc2d83f17889c95891eeb8c18945df6fe75a3fb4582a90c23c9171e1711b48e876c7cf76f596ac66c20424bbe16b1ba810eb450da9f5b654713d02f49
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@unkey/api@npm:^0.19.4":
|
"@unkey/api@npm:^0.19.4":
|
||||||
version: 0.19.4
|
version: 0.19.4
|
||||||
resolution: "@unkey/api@npm:0.19.4"
|
resolution: "@unkey/api@npm:0.19.4"
|
||||||
@@ -18774,6 +18790,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@unkey/ratelimit@npm:^2.1.2":
|
||||||
|
version: 2.1.2
|
||||||
|
resolution: "@unkey/ratelimit@npm:2.1.2"
|
||||||
|
dependencies:
|
||||||
|
"@unkey/api": 2.0.3
|
||||||
|
checksum: 645959a8f2e458fc671cc4ea3d51497e76ee9ff967a9c233dacd51caaa7d0fa20386cf47ec138026113c18527cd2a24f6f941d0b0c0bd7723c8bf9e17c87baa5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@unkey/rbac@npm:^0.1.11":
|
"@unkey/rbac@npm:^0.1.11":
|
||||||
version: 0.1.11
|
version: 0.1.11
|
||||||
resolution: "@unkey/rbac@npm:0.1.11"
|
resolution: "@unkey/rbac@npm:0.1.11"
|
||||||
@@ -18784,12 +18809,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@upstash/redis@npm:^1.21.0":
|
"@upstash/redis@npm:^1.35.2":
|
||||||
version: 1.30.1
|
version: 1.35.2
|
||||||
resolution: "@upstash/redis@npm:1.30.1"
|
resolution: "@upstash/redis@npm:1.35.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
crypto-js: ^4.2.0
|
uncrypto: ^0.1.3
|
||||||
checksum: 1d8083650fbc1760fe4c58d263c62f9c658d08035480927316932df2757f4e987799f7ce752579e7dd871155e96996bb4caf8ad1034af3ab18965d7ca76421cf
|
checksum: 1d54217589418873702f08bdc27d9455ea44df0fce30ace612b5637a1c4c261b762a80fb85d7b3c72d3213e82b8938bb57c413ae44f9f1e713eeab3f31d5539a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -21550,6 +21575,7 @@ __metadata:
|
|||||||
"@snaplet/copycat": ^4.1.0
|
"@snaplet/copycat": ^4.1.0
|
||||||
"@testing-library/jest-dom": ^5.16.5
|
"@testing-library/jest-dom": ^5.16.5
|
||||||
"@testing-library/react": ^16.0.1
|
"@testing-library/react": ^16.0.1
|
||||||
|
"@unkey/ratelimit": ^2.1.2
|
||||||
"@vercel/functions": ^1.4.0
|
"@vercel/functions": ^1.4.0
|
||||||
"@vitest/ui": ^2.1.1
|
"@vitest/ui": ^2.1.1
|
||||||
c8: ^7.13.0
|
c8: ^7.13.0
|
||||||
@@ -23319,13 +23345,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"crypto-js@npm:^4.2.0":
|
|
||||||
version: 4.2.0
|
|
||||||
resolution: "crypto-js@npm:4.2.0"
|
|
||||||
checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"crypto-random-string@npm:^2.0.0":
|
"crypto-random-string@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "crypto-random-string@npm:2.0.0"
|
resolution: "crypto-random-string@npm:2.0.0"
|
||||||
@@ -25896,27 +25915,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"eslint-config-next@npm:^15.4.5":
|
"eslint-config-next@npm:^13.2.1":
|
||||||
version: 15.4.5
|
version: 13.5.11
|
||||||
resolution: "eslint-config-next@npm:15.4.5"
|
resolution: "eslint-config-next@npm:13.5.11"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@next/eslint-plugin-next": 15.4.5
|
"@next/eslint-plugin-next": 13.5.11
|
||||||
"@rushstack/eslint-patch": ^1.10.3
|
"@rushstack/eslint-patch": ^1.3.3
|
||||||
"@typescript-eslint/eslint-plugin": ^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
"@typescript-eslint/parser": ^5.4.2 || ^6.0.0
|
||||||
"@typescript-eslint/parser": ^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
|
||||||
eslint-import-resolver-node: ^0.3.6
|
eslint-import-resolver-node: ^0.3.6
|
||||||
eslint-import-resolver-typescript: ^3.5.2
|
eslint-import-resolver-typescript: ^3.5.2
|
||||||
eslint-plugin-import: ^2.31.0
|
eslint-plugin-import: ^2.28.1
|
||||||
eslint-plugin-jsx-a11y: ^6.10.0
|
eslint-plugin-jsx-a11y: ^6.7.1
|
||||||
eslint-plugin-react: ^7.37.0
|
eslint-plugin-react: ^7.33.2
|
||||||
eslint-plugin-react-hooks: ^5.0.0
|
eslint-plugin-react-hooks: ^4.5.0 || 5.0.0-canary-7118f5dd7-20230705
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
|
eslint: ^7.23.0 || ^8.0.0
|
||||||
typescript: ">=3.3.1"
|
typescript: ">=3.3.1"
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: c4f61ee00fc29d669477037481557a282ab080e0276da3e02e187ad54ae543bd2ca22668ee73fbf7212f69ed9d1866cc6fb648a9e5726d7b2c33875b4dd82722
|
checksum: 6694d86f779f2a6e820873db6641d43cdf446aaa2e9c97b13c0019627369538d217f41e4b7349d7a9308a5378a454c51201e47e8eac2e873fb3f0309985a72d6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -45439,6 +45457,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"uncrypto@npm:^0.1.3":
|
||||||
|
version: 0.1.3
|
||||||
|
resolution: "uncrypto@npm:0.1.3"
|
||||||
|
checksum: 07160e08806dd6cea16bb96c3fd54cd70fc801e02fc3c6f86980144d15c9ebbd1c55587f7280a207b3af6cd34901c0d0b77ada5a02c2f7081a033a05acf409e2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"undici-types@npm:~6.19.2":
|
"undici-types@npm:~6.19.2":
|
||||||
version: 6.19.8
|
version: 6.19.8
|
||||||
resolution: "undici-types@npm:6.19.8"
|
resolution: "undici-types@npm:6.19.8"
|
||||||
@@ -47646,6 +47671,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"zod@npm:^3.20.0":
|
||||||
|
version: 3.25.76
|
||||||
|
resolution: "zod@npm:3.25.76"
|
||||||
|
checksum: c9a403a62b329188a5f6bd24d5d935d2bba345f7ab8151d1baa1505b5da9f227fb139354b043711490c798e91f3df75991395e40142e6510a4b16409f302b849
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"zod@npm:^3.22.2":
|
"zod@npm:^3.22.2":
|
||||||
version: 3.24.4
|
version: 3.24.4
|
||||||
resolution: "zod@npm:3.24.4"
|
resolution: "zod@npm:3.24.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user