feat: add CalendarsTasker with sync and trigger.dev versions (#26854)

* feat: add CalendarsTasker with sync and trigger.dev versions

This PR implements a CalendarsTasker following the same pattern as
PlatformOrganizationBillingTasker to replace the logic in
apps/api/v2/src/ee/calendars/processors/calendars.processor.ts

New files created:
- CalendarsTasker main orchestrator extending Tasker base class
- CalendarsSyncTasker for sync execution
- CalendarsTriggerTasker for async execution via trigger.dev
- CalendarsTaskService with business logic for ensuring default calendars
- trigger.dev task with queue config and retry settings
- DI modules using @evyweb/ioctopus
- NestJS DI modules for API v2

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: import and deasync

* style: format trigger.config.ts dirs array

Co-Authored-By: unknown <>

* refactor: move prisma query to UserRepository and set onboarding to true

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* refactor: remove credential.key from UserRepository method

Co-Authored-By: morgan@cal.com <morgan@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-01-19 16:20:05 +02:00
1 parent e0c637b8ec
commit 62216f2db6
25 files changed
+490 -2

No files matched your search

@@ -0,0 +1,26 @@
import { Module, Scope } from "@nestjs/common";
import { Logger } from "@/lib/logger.bridge";
import { CalendarsSyncTaskerService } from "@/lib/services/tasker/calendars-sync-tasker.service";
import { CalendarsTaskService } from "@/lib/services/tasker/calendars-task.service";
import { CalendarsTasker } from "@/lib/services/tasker/calendars-tasker.service";
import { CalendarsTriggerTaskerService } from "@/lib/services/tasker/calendars-trigger-tasker.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
@Module({
imports: [PrismaModule],
providers: [
{
provide: Logger,
useFactory: () => {
return new Logger();
},
scope: Scope.TRANSIENT,
},
CalendarsTaskService,
CalendarsSyncTaskerService,
CalendarsTriggerTaskerService,
CalendarsTasker,
],
exports: [CalendarsTasker],
})
export class CalendarsTaskerModule {}
@@ -0,0 +1,15 @@
import { Logger } from "@/lib/logger.bridge";
import { CalendarsTaskService } from "@/lib/services/tasker/calendars-task.service";
import { Injectable } from "@nestjs/common";
import { CalendarsSyncTasker as BaseCalendarsSyncTasker } from "@calcom/platform-libraries/calendars";
@Injectable()
export class CalendarsSyncTaskerService extends BaseCalendarsSyncTasker {
constructor(calendarsTaskService: CalendarsTaskService, logger: Logger) {
super({
logger,
calendarsTaskService,
});
}
}
@@ -0,0 +1,15 @@
import { Logger } from "@/lib/logger.bridge";
import { Injectable } from "@nestjs/common";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { CalendarsTaskService as BaseCalendarsTaskService } from "@calcom/platform-libraries/calendars";
@Injectable()
export class CalendarsTaskService extends BaseCalendarsTaskService {
constructor(prismaReadService: PrismaReadService, logger: Logger) {
super({
logger,
prisma: prismaReadService.prisma,
});
}
}
@@ -0,0 +1,21 @@
import { Logger } from "@/lib/logger.bridge";
import { CalendarsSyncTaskerService } from "@/lib/services/tasker/calendars-sync-tasker.service";
import { CalendarsTriggerTaskerService } from "@/lib/services/tasker/calendars-trigger-tasker.service";
import { Injectable } from "@nestjs/common";
import { CalendarsTasker as BaseCalendarsTasker } from "@calcom/platform-libraries/calendars";
@Injectable()
export class CalendarsTasker extends BaseCalendarsTasker {
constructor(
syncTasker: CalendarsSyncTaskerService,
asyncTasker: CalendarsTriggerTaskerService,
logger: Logger
) {
super({
logger,
asyncTasker: asyncTasker,
syncTasker: syncTasker,
});
}
}
@@ -0,0 +1,13 @@
import { Logger } from "@/lib/logger.bridge";
import { Injectable } from "@nestjs/common";
import { CalendarsTriggerTasker as BaseCalendarsTriggerTasker } from "@calcom/platform-libraries/calendars";
@Injectable()
export class CalendarsTriggerTaskerService extends BaseCalendarsTriggerTasker {
constructor(logger: Logger) {
super({
logger,
});
}
}
@@ -0,0 +1,26 @@
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { CalendarsSyncTasker } from "@calcom/features/calendars/lib/tasker/CalendarsSyncTasker";
import { moduleLoader as calendarsTaskServiceModuleLoader } from "./CalendarsTaskService.module";
import { CALENDARS_TASKER_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = CALENDARS_TASKER_DI_TOKENS.CALENDARS_SYNC_TASKER;
const moduleToken = CALENDARS_TASKER_DI_TOKENS.CALENDARS_SYNC_TASKER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: CalendarsSyncTasker,
depsMap: {
logger: loggerServiceModule,
calendarsTaskService: calendarsTaskServiceModuleLoader,
},
});
export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
@@ -0,0 +1,11 @@
import { createContainer } from "@calcom/features/di/di";
import { CalendarsTaskService } from "@calcom/features/calendars/lib/tasker/CalendarsTaskService";
import { moduleLoader as taskServiceModuleLoader } from "./CalendarsTaskService.module";
const container = createContainer();
export function getCalendarsTaskService(): CalendarsTaskService {
taskServiceModuleLoader.loadModule(container);
return container.get<CalendarsTaskService>(taskServiceModuleLoader.token);
}
@@ -0,0 +1,26 @@
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma";
import { CalendarsTaskService } from "@calcom/features/calendars/lib/tasker/CalendarsTaskService";
import { CALENDARS_TASKER_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = CALENDARS_TASKER_DI_TOKENS.CALENDARS_TASK_SERVICE;
const moduleToken = CALENDARS_TASKER_DI_TOKENS.CALENDARS_TASK_SERVICE_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: CalendarsTaskService,
depsMap: {
logger: loggerServiceModule,
prisma: prismaModuleLoader,
},
});
export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
@@ -0,0 +1,11 @@
import { createContainer } from "@calcom/features/di/di";
import { CalendarsTasker } from "@calcom/features/calendars/lib/tasker/CalendarsTasker";
import { moduleLoader as taskerModuleLoader } from "./CalendarsTasker.module";
const container = createContainer();
export function getCalendarsTasker(): CalendarsTasker {
taskerModuleLoader.loadModule(container);
return container.get<CalendarsTasker>(taskerModuleLoader.token);
}
@@ -0,0 +1,28 @@
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { CalendarsTasker } from "@calcom/features/calendars/lib/tasker/CalendarsTasker";
import { moduleLoader as calendarsSyncTaskerModuleLoader } from "./CalendarsSyncTasker.module";
import { moduleLoader as calendarsTriggerTaskerModuleLoader } from "./CalendarsTriggerTasker.module";
import { CALENDARS_TASKER_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = CALENDARS_TASKER_DI_TOKENS.CALENDARS_TASKER;
const moduleToken = CALENDARS_TASKER_DI_TOKENS.CALENDARS_TASKER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: CalendarsTasker,
depsMap: {
logger: loggerServiceModule,
syncTasker: calendarsSyncTaskerModuleLoader,
asyncTasker: calendarsTriggerTaskerModuleLoader,
},
});
export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
@@ -0,0 +1,24 @@
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { CalendarsTriggerTasker } from "@calcom/features/calendars/lib/tasker/CalendarsTriggerTasker";
import { CALENDARS_TASKER_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = CALENDARS_TASKER_DI_TOKENS.CALENDARS_TRIGGER_TASKER;
const moduleToken = CALENDARS_TASKER_DI_TOKENS.CALENDARS_TRIGGER_TASKER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: CalendarsTriggerTasker,
depsMap: {
logger: loggerServiceModule,
},
});
export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
@@ -0,0 +1,10 @@
export const CALENDARS_TASKER_DI_TOKENS = {
CALENDARS_TASK_SERVICE: Symbol("CalendarsTaskService"),
CALENDARS_TASK_SERVICE_MODULE: Symbol("CalendarsTaskServiceModule"),
CALENDARS_SYNC_TASKER: Symbol("CalendarsSyncTasker"),
CALENDARS_SYNC_TASKER_MODULE: Symbol("CalendarsSyncTaskerModule"),
CALENDARS_TRIGGER_TASKER: Symbol("CalendarsTriggerTasker"),
CALENDARS_TRIGGER_TASKER_MODULE: Symbol("CalendarsTriggerTaskerModule"),
CALENDARS_TASKER: Symbol("CalendarsTasker"),
CALENDARS_TASKER_MODULE: Symbol("CalendarsTaskerModule"),
};
@@ -0,0 +1,23 @@
import type { ITaskerDependencies } from "@calcom/lib/tasker/types";
import { nanoid } from "nanoid";
import type { CalendarsTaskService } from "./CalendarsTaskService";
import type { ICalendarsTasker } from "./types";
export interface ICalendarsSyncTaskerDependencies {
calendarsTaskService: CalendarsTaskService;
}
export class CalendarsSyncTasker implements ICalendarsTasker {
constructor(
public readonly dependencies: ITaskerDependencies & ICalendarsSyncTaskerDependencies
) {}
async ensureDefaultCalendars(
payload: Parameters<ICalendarsTasker["ensureDefaultCalendars"]>[0]
): Promise<{ runId: string }> {
const runId = `sync_${nanoid(10)}`;
await this.dependencies.calendarsTaskService.ensureDefaultCalendars(payload);
return { runId };
}
}
@@ -0,0 +1,58 @@
import type { ITaskerDependencies } from "@calcom/lib/tasker/types";
import type { PrismaClient } from "@calcom/prisma";
import { UserRepository } from "../../../users/repositories/UserRepository";
import type { CalendarsTasks } from "./types";
export interface ICalendarsTaskServiceDependencies {
prisma: PrismaClient;
}
export class CalendarsTaskService implements CalendarsTasks {
constructor(
public readonly dependencies: {
logger: ITaskerDependencies["logger"];
} & ICalendarsTaskServiceDependencies
) {}
async ensureDefaultCalendars(
payload: Parameters<CalendarsTasks["ensureDefaultCalendars"]>[0]
): Promise<void> {
const { userId } = payload;
const { prisma, logger } = this.dependencies;
try {
const userRepository = new UserRepository(prisma);
const userWithCalendars = await userRepository.findByIdWithSelectedCalendars({ userId });
if (!userWithCalendars) {
logger.error(`User not found for ensureDefaultCalendars`, { userId });
return;
}
const { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } = await import(
"@calcom/features/calendars/lib/getConnectedDestinationCalendars"
);
await getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
user: {
...userWithCalendars,
allSelectedCalendars: userWithCalendars.selectedCalendars,
userLevelSelectedCalendars: userWithCalendars.selectedCalendars.filter(
(calendar) => !calendar.eventTypeId
),
},
onboarding: true,
eventTypeId: null,
prisma,
});
logger.info("Successfully ensured default calendars for user", { userId });
} catch (err) {
logger.error(`Failed to ensure default calendars for user`, {
userId,
error: err instanceof Error ? err.message : "Unknown error",
});
}
}
}
@@ -0,0 +1,43 @@
import { Tasker } from "@calcom/lib/tasker/Tasker";
import type { ILogger } from "@calcom/lib/tasker/types";
import type { TriggerOptions } from "@trigger.dev/sdk";
import type { CalendarsSyncTasker } from "./CalendarsSyncTasker";
import type { CalendarsTriggerTasker } from "./CalendarsTriggerTasker";
import type { ICalendarsTasker, CalendarsTaskPayload } from "./types";
export interface ICalendarsTaskerDependencies {
asyncTasker: CalendarsTriggerTasker;
syncTasker: CalendarsSyncTasker;
logger: ILogger;
}
export class CalendarsTasker extends Tasker<ICalendarsTasker> {
constructor(public readonly dependencies: ICalendarsTaskerDependencies) {
super(dependencies);
}
public async ensureDefaultCalendars(data: {
payload: CalendarsTaskPayload;
options?: TriggerOptions;
}): Promise<{ runId: string }> {
const { payload } = data;
let taskResponse: {
runId: string;
} = { runId: "task-not-found" };
try {
taskResponse = await this.dispatch("ensureDefaultCalendars", payload, data.options);
this.logger.info(`CalendarsTasker ensureDefaultCalendars success:`, taskResponse, {
userId: payload.userId,
});
} catch {
taskResponse = { runId: "task-failed" };
this.logger.error(`CalendarsTasker ensureDefaultCalendars failed`, taskResponse, {
userId: payload.userId,
});
}
return taskResponse;
}
}
@@ -0,0 +1,16 @@
import type { ITaskerDependencies } from "@calcom/lib/tasker/types";
import type { TriggerOptions } from "@trigger.dev/sdk";
import type { ICalendarsTasker } from "./types";
export class CalendarsTriggerTasker implements ICalendarsTasker {
constructor(public readonly dependencies: ITaskerDependencies) {}
async ensureDefaultCalendars(
payload: Parameters<ICalendarsTasker["ensureDefaultCalendars"]>[0],
options?: TriggerOptions
): Promise<{ runId: string }> {
const { ensureDefaultCalendars } = await import("./trigger/ensure-default-calendars");
const handle = await ensureDefaultCalendars.trigger(payload, options);
return { runId: handle.id };
}
}
@@ -0,0 +1,19 @@
import { type schemaTask, queue } from "@trigger.dev/sdk";
type CalendarsTask = Pick<Parameters<typeof schemaTask>[0], "machine" | "retry" | "queue">;
export const calendarsQueue = queue({
name: "calendars",
concurrencyLimit: 10,
});
export const calendarsTaskConfig: CalendarsTask = {
queue: calendarsQueue,
retry: {
maxAttempts: 3,
factor: 2,
minTimeoutInMs: 1000,
maxTimeoutInMs: 10000,
randomize: true,
},
};
@@ -0,0 +1,24 @@
import { schemaTask, type TaskWithSchema } from "@trigger.dev/sdk";
import type { z } from "zod";
import { calendarsTaskConfig } from "./config";
import { calendarsTaskSchema } from "./schema";
export const ENSURE_DEFAULT_CALENDARS_JOB_ID = "calendars.ensure-default-calendars";
export const ensureDefaultCalendars: TaskWithSchema<
typeof ENSURE_DEFAULT_CALENDARS_JOB_ID,
typeof calendarsTaskSchema
> = schemaTask({
id: ENSURE_DEFAULT_CALENDARS_JOB_ID,
...calendarsTaskConfig,
schema: calendarsTaskSchema,
run: async (payload: z.infer<typeof calendarsTaskSchema>) => {
const { getCalendarsTaskService } = await import(
"@calcom/features/calendars/di/tasker/CalendarsTaskService.container"
);
const calendarsTaskService = getCalendarsTaskService();
await calendarsTaskService.ensureDefaultCalendars(payload);
},
});
@@ -0,0 +1,5 @@
import { z } from "zod";
export const calendarsTaskSchema = z.object({
userId: z.number(),
});
@@ -0,0 +1,15 @@
import type { TriggerOptions } from "@trigger.dev/sdk";
import type { z } from "zod";
import type { calendarsTaskSchema } from "./trigger/schema";
type WithVoidReturns<T> = {
[K in keyof T]: T[K] extends (...args: infer P) => unknown ? (...args: P) => void : T[K];
};
export type CalendarsTaskPayload = z.infer<typeof calendarsTaskSchema>;
export interface ICalendarsTasker {
ensureDefaultCalendars(payload: CalendarsTaskPayload, options?: TriggerOptions): Promise<{ runId: string }>;
}
export type CalendarsTasks = WithVoidReturns<Pick<ICalendarsTasker, "ensureDefaultCalendars">>;
+2
View File
@@ -27,6 +27,7 @@
"city-timezones": "1.2.1",
"class-variance-authority": "0.7.1",
"date-fns-tz": "3.2.0",
"deasync": "^0.1.31",
"framer-motion": "10.12.8",
"p-limit": "6.2.0",
"react-awesome-query-builder": "5.1.2",
@@ -48,6 +49,7 @@
"devDependencies": {
"@calcom/testing": "workspace:*",
"@testing-library/react-hooks": "8.0.1",
"@types/deasync": "^0",
"@types/web-push": "3.6.3",
"trigger.dev": "4.3.2"
}
+15 -2
View File
@@ -16,7 +16,11 @@ export default defineConfig({
project: process.env.TRIGGER_DEV_PROJECT_REF ?? "", // e.g., "proj_abc123"
// Directories containing your tasks
dirs: ["./bookings/lib/tasker/trigger/notifications", "./ee/billing/service/proration/tasker/trigger"], // Customize based on your project structure
dirs: [
"./bookings/lib/tasker/trigger/notifications",
"./calendars/lib/tasker/trigger",
"./ee/billing/service/proration/tasker/trigger",
], // Customize based on your project structure
// Retry configuration
retries: {
@@ -35,7 +39,16 @@ export default defineConfig({
// Build configuration (optional)
build: {
external: ["@prisma/client", "nodemailer", "jsdom", "playwright-core", "playwright", "chromium-bidi"],
external: [
"@prisma/client",
"nodemailer",
"jsdom",
"playwright-core",
"playwright",
"chromium-bidi",
"http-cookie-agent",
"deasync",
],
extensions: canSyncEnvVars
? [
syncVercelEnvVars({
@@ -1487,4 +1487,16 @@ export class UserRepository {
select: userSelect,
});
}
async findByIdWithSelectedCalendars({ userId }: { userId: number }) {
return this.prismaClient.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
selectedCalendars: true,
destinationCalendar: true,
},
});
}
}
+4
View File
@@ -0,0 +1,4 @@
export { CalendarsTaskService } from "@calcom/features/calendars/lib/tasker/CalendarsTaskService";
export { CalendarsSyncTasker } from "@calcom/features/calendars/lib/tasker/CalendarsSyncTasker";
export { CalendarsTriggerTasker } from "@calcom/features/calendars/lib/tasker/CalendarsTriggerTasker";
export { CalendarsTasker } from "@calcom/features/calendars/lib/tasker/CalendarsTasker";
+28
View File
@@ -2607,11 +2607,13 @@ __metadata:
"@tanstack/react-virtual": "npm:3.10.9"
"@testing-library/react-hooks": "npm:8.0.1"
"@trigger.dev/sdk": "npm:4.3.2"
"@types/deasync": "npm:^0"
"@types/web-push": "npm:3.6.3"
"@vercel/functions": "npm:1.4.0"
city-timezones: "npm:1.2.1"
class-variance-authority: "npm:0.7.1"
date-fns-tz: "npm:3.2.0"
deasync: "npm:^0.1.31"
framer-motion: "npm:10.12.8"
p-limit: "npm:6.2.0"
react-awesome-query-builder: "npm:5.1.2"
@@ -15798,6 +15800,13 @@ __metadata:
languageName: node
linkType: hard
"@types/deasync@npm:^0":
version: 0.1.5
resolution: "@types/deasync@npm:0.1.5"
checksum: 10/5a8fec89572abc6fbcce7b4f5b5357ba58b2d6d6860b640eb452eb9e87a8d44f787a40450e456b6e2d7fa12cf52bac2ab0d2f9be431dd51bd40584e316ffaa2c
languageName: node
linkType: hard
"@types/debug@npm:^4.0.0":
version: 4.1.12
resolution: "@types/debug@npm:4.1.12"
@@ -20796,6 +20805,16 @@ __metadata:
languageName: node
linkType: hard
"deasync@npm:^0.1.31":
version: 0.1.31
resolution: "deasync@npm:0.1.31"
dependencies:
bindings: "npm:^1.5.0"
node-addon-api: "npm:^1.7.1"
checksum: 10/e3430cea041d7de593e0cc80a88f9f9b9b8c4f5cc635bcd3a237b7a5371d62dbd50daed0f14ed4d148754a345b993aa7a1e4da848bda69f69c60fb58fd5fac07
languageName: node
linkType: hard
"debounce-fn@npm:^4.0.0":
version: 4.0.0
resolution: "debounce-fn@npm:4.0.0"
@@ -30425,6 +30444,15 @@ __metadata:
languageName: node
linkType: hard
"node-addon-api@npm:^1.7.1":
version: 1.7.2
resolution: "node-addon-api@npm:1.7.2"
dependencies:
node-gyp: "npm:latest"
checksum: 10/6bf8217a8cd8148f4bbfd319b46d33587e9fb2e63e3c856ded67a76715167f7a6b17e1d9b8bbf3b8508befeb6a4adb10d92b8998ed5c19ca8448343f4cea11d6
languageName: node
linkType: hard
"node-addon-api@npm:^7.0.0":
version: 7.1.1
resolution: "node-addon-api@npm:7.1.1"