Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87cb8b170a | ||
|
|
badd0de33c | ||
|
|
124fd8ebca | ||
|
|
5d0dc3f546 | ||
|
|
e4baf15b53 | ||
|
|
a1bd8b6259 | ||
|
|
7987c9f45d | ||
|
|
781d0d92df | ||
|
|
b71d8baccc | ||
|
|
a71d949a0c | ||
|
|
e55cee443b | ||
|
|
c3634b3aba | ||
|
|
cd576f5378 |
@@ -22,6 +22,7 @@ env:
|
||||
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
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 { 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 { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -191,8 +191,9 @@ const availabilitySchema = z
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { isSystemWideAdmin, userId: reqUserId } = req;
|
||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||
const userAvailabilityService = getUserAvailabilityService()
|
||||
if (!teamId)
|
||||
return getUserAvailability({
|
||||
return userAvailabilityService.getUserAvailability({
|
||||
username,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
@@ -230,7 +231,7 @@ async function handler(req: NextApiRequest) {
|
||||
const availabilities = members.map(async (user) => {
|
||||
return {
|
||||
userId: user.id,
|
||||
availability: await getUserAvailability({
|
||||
availability: await userAvailabilityService.getUserAvailability({
|
||||
userId: user.id,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.276",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.287",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -104,10 +104,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const rescheduledToInfo = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToInfo(databaseBooking.uid)
|
||||
: undefined;
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy;
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled
|
||||
? rescheduledToInfo?.rescheduledBy
|
||||
: databaseBooking.rescheduledBy;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
@@ -157,13 +158,12 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
async getRescheduledToInfo(bookingUid: string): Promise<{ uid?: string; rescheduledBy?: string | null }> {
|
||||
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
||||
return {
|
||||
uid: rescheduledTo?.uid,
|
||||
rescheduledBy: rescheduledTo?.rescheduledBy
|
||||
return {
|
||||
uid: rescheduledTo?.uid,
|
||||
rescheduledBy: rescheduledTo?.rescheduledBy,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||
if (databaseMetadata === null) return {};
|
||||
|
||||
@@ -279,9 +279,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const rescheduledToInfo = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToInfo(databaseBooking.uid)
|
||||
: undefined;
|
||||
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled ? rescheduledToInfo?.rescheduledBy : databaseBooking.rescheduledBy;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled
|
||||
? rescheduledToInfo?.rescheduledBy
|
||||
: databaseBooking.rescheduledBy;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
|
||||
+4
-1
@@ -290,7 +290,10 @@ describe("Event types Endpoints", () => {
|
||||
expect(responseBookingFields).toBeDefined();
|
||||
// note(Lauris): response bookingFields are already existing default bookingFields + the new one
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,9 @@ import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
@@ -24,10 +26,12 @@ import { Module } from "@nestjs/common";
|
||||
PrismaEventTypeRepository,
|
||||
PrismaRoutingFormResponseRepository,
|
||||
PrismaTeamRepository,
|
||||
RedisService,
|
||||
PrismaFeaturesRepository,
|
||||
CheckBookingLimitsService,
|
||||
CacheService,
|
||||
AvailableSlotsService,
|
||||
UserAvailabilityService
|
||||
],
|
||||
exports: [AvailableSlotsService],
|
||||
})
|
||||
|
||||
@@ -9,10 +9,13 @@ import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { AvailableSlotsService as BaseAvailableSlotsService } from "@calcom/platform-libraries/slots";
|
||||
|
||||
import { UserAvailabilityService } from "./user-availability.service";
|
||||
|
||||
@Injectable()
|
||||
export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
constructor(
|
||||
@@ -24,6 +27,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
selectedSlotRepository: PrismaSelectedSlotRepository,
|
||||
eventTypeRepository: PrismaEventTypeRepository,
|
||||
userRepository: PrismaUserRepository,
|
||||
redisService: RedisService,
|
||||
featuresRepository: PrismaFeaturesRepository
|
||||
) {
|
||||
super({
|
||||
@@ -35,8 +39,15 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
selectedSlotRepo: selectedSlotRepository,
|
||||
eventTypeRepo: eventTypeRepository,
|
||||
userRepo: userRepository,
|
||||
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository) as any,
|
||||
redisClient: redisService,
|
||||
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository),
|
||||
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;
|
||||
}
|
||||
) {
|
||||
|
||||
if (this.configService.get("e2e")) {
|
||||
return true;
|
||||
}
|
||||
const { uid, startTime, fromReschedule } = booking;
|
||||
|
||||
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.
|
||||
*/
|
||||
async cancelUsageByBookingUid(bookingUid: string) {
|
||||
if (this.configService.get("e2e")) {
|
||||
return true;
|
||||
}
|
||||
const job = await this.billingQueue.getJob(`increment-${bookingUid}`);
|
||||
if (job) {
|
||||
await job.remove();
|
||||
@@ -458,6 +465,7 @@ export class BillingService implements OnModuleDestroy {
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
|
||||
await this.billingQueue.close();
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
|
||||
@@ -7,12 +7,32 @@ import { Redis } from "ioredis";
|
||||
export class RedisService implements OnModuleDestroy {
|
||||
public redis: Redis;
|
||||
private readonly logger = new Logger("RedisService");
|
||||
private isReady = false; // Track connection status
|
||||
|
||||
constructor(readonly configService: ConfigService<AppConfig>) {
|
||||
const dbUrl = configService.get<string>("db.redisUrl", { infer: true });
|
||||
if (!dbUrl) throw new Error("Misconfigured Redis, halting.");
|
||||
|
||||
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() {
|
||||
@@ -22,4 +42,99 @@ export class RedisService implements OnModuleDestroy {
|
||||
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",
|
||||
INTEGRATION_TEST_MODE: "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",
|
||||
"version": "5.5.11",
|
||||
"version": "5.5.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -78,7 +78,7 @@
|
||||
"@team-plain/typescript-sdk": "^5.9.0",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@unkey/ratelimit": "^0.1.1",
|
||||
"@upstash/redis": "^1.21.0",
|
||||
"@upstash/redis": "^1.35.2",
|
||||
"@vercel/edge-config": "^0.1.1",
|
||||
"@vercel/edge-functions-ui": "^0.2.1",
|
||||
"@vercel/og": "^0.6.3",
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
"path": "/api/cron/calendar-cache-cleanup",
|
||||
"schedule": "0 5 * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/cron/queuedFormResponseCleanup",
|
||||
"schedule": "0 */12 * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/tasks/cron",
|
||||
"schedule": "* * * * *"
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"@daily-co/daily-js": "^0.76.0",
|
||||
"@evyweb/ioctopus": "^1.2.0",
|
||||
"@next/third-parties": "^14.2.5",
|
||||
"@unkey/ratelimit": "^2.1.2",
|
||||
"@vercel/functions": "^1.4.0",
|
||||
"city-timezones": "^1.2.1",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
|
||||
@@ -1114,4 +1114,18 @@ export default class GoogleCalendarService implements Calendar {
|
||||
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();
|
||||
};
|
||||
|
||||
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": {
|
||||
"@calcom/eslint-plugin-eslint": "*",
|
||||
"@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-turbo": "^0.0.7",
|
||||
"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 { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
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 { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits";
|
||||
import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
|
||||
@@ -62,6 +62,7 @@ const _ensureAvailableUsers = async (
|
||||
shouldServeCache?: boolean
|
||||
// ReturnType hint of at least one IsFixedAwareUser, as it's made sure at least one entry exists
|
||||
): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> => {
|
||||
const userAvailabilityService = getUserAvailabilityService()
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
|
||||
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,
|
||||
query: {
|
||||
...input,
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function InsightsPage() {
|
||||
|
||||
## 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
|
||||
// packages/features/insights/server/trpc-router.ts
|
||||
@@ -118,6 +118,7 @@ export const insightsRouter = router({
|
||||
myNewChartData: userBelongsToTeamProcedure
|
||||
.input(bookingRepositoryBaseInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
// `createInsightsBookingService` is defined at the root level in this file
|
||||
const insightsBookingService = createInsightsBookingService(ctx, input);
|
||||
|
||||
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
|
||||
// packages/lib/server/service/insightsBooking.ts
|
||||
export class InsightsBookingService {
|
||||
// packages/lib/server/service/InsightsBookingBaseService.ts
|
||||
export class InsightsBookingBaseService {
|
||||
// ... existing methods
|
||||
|
||||
async getMyNewChartData() {
|
||||
@@ -168,7 +169,7 @@ export class InsightsBookingService {
|
||||
|
||||
## 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
|
||||
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`
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
routingRepositoryBaseInputSchema,
|
||||
bookingRepositoryBaseInputSchema,
|
||||
} 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 { InsightsBookingService } from "@calcom/lib/server/service/insightsBooking";
|
||||
import type { readonlyPrisma } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
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
|
||||
*/
|
||||
function createInsightsBookingService(
|
||||
ctx: { insightsDb: typeof readonlyPrisma; user: { id: number; organizationId: number | null } },
|
||||
ctx: { user: { id: number; organizationId: number | null } },
|
||||
input: z.infer<typeof bookingRepositoryBaseInputSchema>,
|
||||
dateTarget: "createdAt" | "startTime" = "createdAt"
|
||||
) {
|
||||
const { scope, selectedTeamId, startDate, endDate, columnFilters } = input;
|
||||
|
||||
return new InsightsBookingService({
|
||||
prisma: ctx.insightsDb,
|
||||
return getInsightsBookingService({
|
||||
options: {
|
||||
scope,
|
||||
userId: ctx.user.id,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
export interface IRedisService {
|
||||
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>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
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> {
|
||||
@@ -17,9 +23,16 @@ export class RedisService implements IRedisService {
|
||||
return this.redis.del(key);
|
||||
}
|
||||
|
||||
async set<TData>(key: string, value: TData): Promise<"OK" | TData | null> {
|
||||
// Implementation for setting value in Redis
|
||||
return this.redis.set(key, value);
|
||||
async set<TData>(key: string, value: TData, opts?: { ttl?: number }): Promise<"OK" | TData | null> {
|
||||
return this.redis.set(
|
||||
key,
|
||||
value,
|
||||
opts?.ttl
|
||||
? {
|
||||
px: opts.ttl,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
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 };
|
||||
@@ -151,6 +151,7 @@ export const Meeting = ({ title, users = [], profile }: MeetingImageProps) => {
|
||||
src={avatar}
|
||||
alt="Profile picture"
|
||||
width="160"
|
||||
height="160"
|
||||
/>
|
||||
))}
|
||||
{avatars.length > 3 && (
|
||||
@@ -217,7 +218,7 @@ export const App = ({ name, description, slug }: AppImageProps) => (
|
||||
|
||||
<div tw="flex items-center w-full">
|
||||
<div tw="flex">
|
||||
<img src={`${WEBAPP_URL}${slug}`} alt="App icon" width="172" />
|
||||
<img src={`${WEBAPP_URL}${slug}`} alt="App icon" width="172" height="172" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ color: "#111827" }} tw="flex mt-auto w-full flex-col">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContainer } from "@evyweb/ioctopus";
|
||||
|
||||
import { redisModule } from "@calcom/features/redis/di/redisModule";
|
||||
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||
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 { teamRepositoryModule } from "../modules/team";
|
||||
import { userRepositoryModule } from "../modules/user";
|
||||
import { getUserAvailabilityModule } from "../modules/get-user-availability";
|
||||
|
||||
const container = createContainer();
|
||||
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
|
||||
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
|
||||
container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
|
||||
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.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule);
|
||||
container.load(DI_TOKENS.AVAILABLE_SLOTS_SERVICE_MODULE, availableSlotsModule);
|
||||
|
||||
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule)
|
||||
export function getAvailableSlotsService() {
|
||||
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,
|
||||
eventTypeRepo: DI_TOKENS.EVENT_TYPE_REPOSITORY,
|
||||
routingFormResponseRepo: DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY,
|
||||
redisClient: DI_TOKENS.REDIS_CLIENT,
|
||||
cacheService: DI_TOKENS.CACHE_SERVICE,
|
||||
checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE,
|
||||
userAvailabilityService: DI_TOKENS.GET_USER_AVAILABILITY_SERVICE
|
||||
} 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"),
|
||||
READ_ONLY_PRISMA_CLIENT: Symbol("ReadOnlyPrismaClient"),
|
||||
PRISMA_MODULE: Symbol("PrismaModule"),
|
||||
REDIS_CLIENT: Symbol("RedisClient"),
|
||||
OOO_REPOSITORY: Symbol("OOORepository"),
|
||||
OOO_REPOSITORY_MODULE: Symbol("OOORepositoryModule"),
|
||||
SCHEDULE_REPOSITORY: Symbol("ScheduleRepository"),
|
||||
@@ -22,6 +23,8 @@ export const DI_TOKENS = {
|
||||
AVAILABLE_SLOTS_SERVICE_MODULE: Symbol("AvailableSlotsModule"),
|
||||
INSIGHTS_ROUTING_SERVICE: Symbol("InsightsRoutingService"),
|
||||
INSIGHTS_ROUTING_SERVICE_MODULE: Symbol("InsightsRoutingServiceModule"),
|
||||
INSIGHTS_BOOKING_SERVICE: Symbol("InsightsBookingService"),
|
||||
INSIGHTS_BOOKING_SERVICE_MODULE: Symbol("InsightsBookingServiceModule"),
|
||||
FEATURES_REPOSITORY: Symbol("FeaturesRepository"),
|
||||
FEATURES_REPOSITORY_MODULE: Symbol("FeaturesRepositoryModule"),
|
||||
CACHE_SERVICE: Symbol("CacheService"),
|
||||
@@ -30,4 +33,6 @@ export const DI_TOKENS = {
|
||||
CHECK_BOOKING_LIMITS_SERVICE_MODULE: Symbol("CheckBookingLimitsServiceModule"),
|
||||
CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE: Symbol("CheckBookingAndDurationLimitsService"),
|
||||
CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE_MODULE: Symbol("CheckBookingAndDurationLimitsServiceModule"),
|
||||
GET_USER_AVAILABILITY_SERVICE: Symbol("GetUserAvailabilityService"),
|
||||
GET_USER_AVAILABILITY_SERVICE_MODULE: Symbol("GetUserAvailabilityModule"),
|
||||
};
|
||||
|
||||
+532
-609
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import { getCheckBookingLimitsService } from "@calcom/lib/di/containers/booking-limits";
|
||||
import { getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes";
|
||||
import type { EventType } from "@calcom/lib/getUserAvailability";
|
||||
import { getPeriodStartDatesBetween } from "@calcom/lib/getUserAvailability";
|
||||
import { getPeriodStartDatesBetween } from "@calcom/lib/intervalLimits/utils/getPeriodStartDatesBetween";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import { getTotalBookingDuration } from "@calcom/lib/server/queries/booking";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
|
||||
import type { IntervalLimitUnit } from "../intervalLimitSchema";
|
||||
|
||||
function _getPeriodStartDatesBetween(
|
||||
dateFrom: Dayjs,
|
||||
dateTo: Dayjs,
|
||||
period: IntervalLimitUnit,
|
||||
timeZone?: string
|
||||
): Dayjs[] {
|
||||
const dates = [];
|
||||
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);
|
||||
|
||||
while (startDate.isBefore(endDate)) {
|
||||
dates.push(startDate);
|
||||
startDate = startDate.add(1, period);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
export const getPeriodStartDatesBetween = withReporting(_getPeriodStartDatesBetween, "getPeriodStartDatesBetween");
|
||||
+22
-15
@@ -1,12 +1,13 @@
|
||||
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 { fetchWithTimeout } from "../fetchWithTimeout";
|
||||
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 i18nInstanceCache = new Map<string, any>();
|
||||
const SUPPORTED_NAMESPACES = ["common"];
|
||||
@@ -28,22 +29,28 @@ export async function loadTranslations(_locale: string, _ns: string) {
|
||||
}
|
||||
|
||||
const url = `${WEBAPP_URL}/static/locales/${locale}/${ns}.json`;
|
||||
const response = await fetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
|
||||
},
|
||||
3000
|
||||
);
|
||||
try {
|
||||
const response = await fetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
|
||||
},
|
||||
3000
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error(`Failed to fetch translations: ${response.status}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const translations = await response.json();
|
||||
translationCache.set(cacheKey, translations);
|
||||
return translations;
|
||||
} catch (err) {
|
||||
console.error("loadTranslations Error:", err);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error(`Failed to fetch translations: ${response.status}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const translations = await response.json();
|
||||
translationCache.set(cacheKey, translations);
|
||||
return translations;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
export class InsightsBookingService {
|
||||
export class InsightsBookingBaseService {
|
||||
private prisma: typeof readonlyPrisma;
|
||||
private options: InsightsBookingServiceOptions | 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 { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { InsightsBookingService } from "../../service/insightsBooking";
|
||||
import { InsightsBookingBaseService as InsightsBookingService } from "../InsightsBookingBaseService";
|
||||
|
||||
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";
|
||||
|
||||
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 { TUserInputSchema } from "./user.schema";
|
||||
@@ -11,7 +11,8 @@ type UserOptions = {
|
||||
};
|
||||
|
||||
export const userHandler = async ({ input }: UserOptions) => {
|
||||
return getUserAvailability(
|
||||
const userAvailabilityService = getUserAvailabilityService()
|
||||
return userAvailabilityService.getUserAvailability(
|
||||
{ returnDateOverrides: true, bypassBusyCalendarTimes: false, ...input },
|
||||
undefined
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts";
|
||||
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
|
||||
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 { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
|
||||
import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
|
||||
@@ -20,10 +21,10 @@ import type {
|
||||
CurrentSeats,
|
||||
EventType,
|
||||
GetAvailabilityUser,
|
||||
UserAvailabilityService,
|
||||
IFromUser,
|
||||
IToUser,
|
||||
} from "@calcom/lib/getUserAvailability";
|
||||
import { getPeriodStartDatesBetween, getUsersAvailability } from "@calcom/lib/getUserAvailability";
|
||||
import { descendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit";
|
||||
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
||||
import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
|
||||
@@ -64,6 +65,7 @@ import { handleNotificationWhenNoSlots } from "./handleNotificationWhenNoSlots";
|
||||
import type { GetScheduleOptions } from "./types";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[slots/util]"] });
|
||||
const DEFAULT_SLOTS_CACHE_TTL = 2000;
|
||||
|
||||
type GetAvailabilityUserWithDelegationCredentials = Omit<GetAvailabilityUser, "credentials"> & {
|
||||
credentials: CredentialForCalendarService[];
|
||||
@@ -101,6 +103,47 @@ export interface IAvailableSlotsService {
|
||||
routingFormResponseRepo: RoutingFormResponseRepository;
|
||||
cacheService: CacheService;
|
||||
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 {
|
||||
@@ -357,7 +400,7 @@ export class AvailableSlotsService {
|
||||
if (!limit) continue;
|
||||
|
||||
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) {
|
||||
if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
|
||||
@@ -392,7 +435,7 @@ export class AvailableSlotsService {
|
||||
if (!limit) continue;
|
||||
|
||||
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) {
|
||||
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
|
||||
@@ -443,7 +486,7 @@ export class AvailableSlotsService {
|
||||
if (!limit) continue;
|
||||
|
||||
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) {
|
||||
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
|
||||
@@ -543,7 +586,7 @@ export class AvailableSlotsService {
|
||||
if (!limit) continue;
|
||||
|
||||
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) {
|
||||
if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
|
||||
@@ -591,7 +634,7 @@ export class AvailableSlotsService {
|
||||
if (!limit) continue;
|
||||
|
||||
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) {
|
||||
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
|
||||
@@ -818,7 +861,7 @@ export class AvailableSlotsService {
|
||||
const users = enrichUsersWithData();
|
||||
|
||||
// TODO: DI getUsersAvailability
|
||||
const premappedUsersAvailability = await getUsersAvailability({
|
||||
const premappedUsersAvailability = await this.dependencies.userAvailabilityService.getUsersAvailability({
|
||||
users,
|
||||
query: {
|
||||
dateFrom: startTime.format(),
|
||||
@@ -883,7 +926,10 @@ export class AvailableSlotsService {
|
||||
"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> {
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
@@ -16,6 +16,7 @@ type GetMemberAvailabilityOptions = {
|
||||
};
|
||||
|
||||
export const getMemberAvailabilityHandler = async ({ ctx, input }: GetMemberAvailabilityOptions) => {
|
||||
const userAvailabilityService = getUserAvailabilityService()
|
||||
const team = await isTeamMember(ctx.user?.id, input.teamId);
|
||||
if (!team) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
@@ -32,7 +33,7 @@ export const getMemberAvailabilityHandler = async ({ ctx, input }: GetMemberAvai
|
||||
});
|
||||
|
||||
// get availability for this member
|
||||
return await getUserAvailability(
|
||||
return await userAvailabilityService.getUserAvailability(
|
||||
{
|
||||
username: username,
|
||||
dateFrom: input.dateFrom,
|
||||
|
||||
+2
-1
@@ -490,6 +490,7 @@
|
||||
"NEXT_PUBLIC_SINGLE_ORG_MODE_ENABLED",
|
||||
"CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY",
|
||||
"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
|
||||
"@calcom/platform-constants": "*"
|
||||
"@calcom/platform-enums": "*"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.276"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.287"
|
||||
"@calcom/platform-types": "*"
|
||||
"@calcom/platform-utils": "*"
|
||||
"@calcom/prisma": "*"
|
||||
@@ -2836,7 +2836,7 @@ __metadata:
|
||||
"@typescript-eslint/eslint-plugin": ^5.52.0
|
||||
"@typescript-eslint/parser": ^5.52.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-turbo: ^0.0.7
|
||||
eslint-plugin-playwright: ^0.12.0
|
||||
@@ -3556,13 +3556,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.276":
|
||||
version: 0.0.276
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.276"
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.287":
|
||||
version: 0.0.287
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.287"
|
||||
dependencies:
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
checksum: 4effc186c0301ec35496ebffbe32280f26261667b60057d18207b30636b038cdcc3783b95390eecc8460f3203d0cfe5d732e3b3f89031ff064b1a556321779a8
|
||||
checksum: 098a50e1b20f70c0f67a332d5f4f6a13c9aaf03f0c0ac9c9ffe1b74c06c607e6650bc0f08bdc1a9bdd89fa25461c0e153580305b845620a1506b23150334c5bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4076,7 +4076,7 @@ __metadata:
|
||||
"@types/turndown": ^5.0.1
|
||||
"@types/uuid": 8.3.1
|
||||
"@unkey/ratelimit": ^0.1.1
|
||||
"@upstash/redis": ^1.21.0
|
||||
"@upstash/redis": ^1.35.2
|
||||
"@vercel/edge-config": ^0.1.1
|
||||
"@vercel/edge-functions-ui": ^0.2.1
|
||||
"@vercel/og": ^0.6.3
|
||||
@@ -8943,6 +8943,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 14.0.4
|
||||
resolution: "@next/eslint-plugin-next@npm:14.0.4"
|
||||
@@ -8961,15 +8970,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 13.5.7
|
||||
resolution: "@next/swc-darwin-arm64@npm:13.5.7"
|
||||
@@ -18756,6 +18756,22 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.19.4
|
||||
resolution: "@unkey/api@npm:0.19.4"
|
||||
@@ -18774,6 +18790,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.1.11
|
||||
resolution: "@unkey/rbac@npm:0.1.11"
|
||||
@@ -18784,12 +18809,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@upstash/redis@npm:^1.21.0":
|
||||
version: 1.30.1
|
||||
resolution: "@upstash/redis@npm:1.30.1"
|
||||
"@upstash/redis@npm:^1.35.2":
|
||||
version: 1.35.2
|
||||
resolution: "@upstash/redis@npm:1.35.2"
|
||||
dependencies:
|
||||
crypto-js: ^4.2.0
|
||||
checksum: 1d8083650fbc1760fe4c58d263c62f9c658d08035480927316932df2757f4e987799f7ce752579e7dd871155e96996bb4caf8ad1034af3ab18965d7ca76421cf
|
||||
uncrypto: ^0.1.3
|
||||
checksum: 1d54217589418873702f08bdc27d9455ea44df0fce30ace612b5637a1c4c261b762a80fb85d7b3c72d3213e82b8938bb57c413ae44f9f1e713eeab3f31d5539a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -21550,6 +21575,7 @@ __metadata:
|
||||
"@snaplet/copycat": ^4.1.0
|
||||
"@testing-library/jest-dom": ^5.16.5
|
||||
"@testing-library/react": ^16.0.1
|
||||
"@unkey/ratelimit": ^2.1.2
|
||||
"@vercel/functions": ^1.4.0
|
||||
"@vitest/ui": ^2.1.1
|
||||
c8: ^7.13.0
|
||||
@@ -23319,13 +23345,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.0.0
|
||||
resolution: "crypto-random-string@npm:2.0.0"
|
||||
@@ -25896,27 +25915,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-config-next@npm:^15.4.5":
|
||||
version: 15.4.5
|
||||
resolution: "eslint-config-next@npm:15.4.5"
|
||||
"eslint-config-next@npm:^13.2.1":
|
||||
version: 13.5.11
|
||||
resolution: "eslint-config-next@npm:13.5.11"
|
||||
dependencies:
|
||||
"@next/eslint-plugin-next": 15.4.5
|
||||
"@rushstack/eslint-patch": ^1.10.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 || ^7.0.0 || ^8.0.0
|
||||
"@next/eslint-plugin-next": 13.5.11
|
||||
"@rushstack/eslint-patch": ^1.3.3
|
||||
"@typescript-eslint/parser": ^5.4.2 || ^6.0.0
|
||||
eslint-import-resolver-node: ^0.3.6
|
||||
eslint-import-resolver-typescript: ^3.5.2
|
||||
eslint-plugin-import: ^2.31.0
|
||||
eslint-plugin-jsx-a11y: ^6.10.0
|
||||
eslint-plugin-react: ^7.37.0
|
||||
eslint-plugin-react-hooks: ^5.0.0
|
||||
eslint-plugin-import: ^2.28.1
|
||||
eslint-plugin-jsx-a11y: ^6.7.1
|
||||
eslint-plugin-react: ^7.33.2
|
||||
eslint-plugin-react-hooks: ^4.5.0 || 5.0.0-canary-7118f5dd7-20230705
|
||||
peerDependencies:
|
||||
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
|
||||
eslint: ^7.23.0 || ^8.0.0
|
||||
typescript: ">=3.3.1"
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: c4f61ee00fc29d669477037481557a282ab080e0276da3e02e187ad54ae543bd2ca22668ee73fbf7212f69ed9d1866cc6fb648a9e5726d7b2c33875b4dd82722
|
||||
checksum: 6694d86f779f2a6e820873db6641d43cdf446aaa2e9c97b13c0019627369538d217f41e4b7349d7a9308a5378a454c51201e47e8eac2e873fb3f0309985a72d6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -45439,6 +45457,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.19.8
|
||||
resolution: "undici-types@npm:6.19.8"
|
||||
@@ -47646,6 +47671,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 3.24.4
|
||||
resolution: "zod@npm:3.24.4"
|
||||
|
||||
Reference in New Issue
Block a user