chore: Add initial webhook wiring (TRPC handlers) to the new architecture (#27170)

* --init

* clean up

* unnecessary comment

* remove comment

* fix factory resolver

* Add active filter to webhook query
This commit is contained in:
Syed Ali Shahbaz
2026-01-23 16:50:34 +04:00
committed by GitHub
parent 646d18e286
commit 4115eaacbd
14 changed files with 142 additions and 77 deletions
@@ -13,6 +13,8 @@ export const WEBHOOK_TOKENS = {
// Repositories
WEBHOOK_REPOSITORY: Symbol("IWebhookRepository"),
WEBHOOK_EVENT_TYPE_REPOSITORY: Symbol("IEventTypeRepository"),
WEBHOOK_USER_REPOSITORY: Symbol("IUserRepository"),
// Producer/Consumer
WEBHOOK_PRODUCER_SERVICE: Symbol("IWebhookProducerService"),
@@ -32,7 +32,9 @@ loggerModuleLoader.loadModule(webhookContainer);
prismaModuleLoader.loadModule(webhookContainer);
webhookContainer.load(SHARED_TOKENS.TASKER, taskerServiceModule);
// Load webhook module
// Load webhook module (includes cross-table repositories + all webhook services)
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_EVENT_TYPE_REPOSITORY, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_USER_REPOSITORY, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_SERVICE, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE, webhookModule);
@@ -1,5 +1,5 @@
import { createModule } from "@evyweb/ioctopus";
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
import { UsersRepository } from "@calcom/features/users/users.repository";
import { createPayloadBuilderFactory } from "@calcom/features/webhooks/lib/factory/versioned/registry";
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
import { BookingWebhookService } from "@calcom/features/webhooks/lib/service/BookingWebhookService";
@@ -9,17 +9,28 @@ import { RecordingWebhookService } from "@calcom/features/webhooks/lib/service/R
import { WebhookNotificationHandler } from "@calcom/features/webhooks/lib/service/WebhookNotificationHandler";
import { WebhookNotifier } from "@calcom/features/webhooks/lib/service/WebhookNotifier";
import { WebhookService } from "@calcom/features/webhooks/lib/service/WebhookService";
import { DI_TOKENS } from "../../tokens";
import { createModule } from "@evyweb/ioctopus";
import { SHARED_TOKENS } from "../../shared/shared.tokens";
import { DI_TOKENS } from "../../tokens";
import { WEBHOOK_TOKENS } from "../Webhooks.tokens";
export const webhookModule = createModule();
const webhookModule = createModule();
// Bind repository with Prisma dependency
// Bind cross-table repositories (used by WebhookRepository)
webhookModule
.bind(WEBHOOK_TOKENS.WEBHOOK_EVENT_TYPE_REPOSITORY)
.toClass(EventTypeRepository, [DI_TOKENS.PRISMA_CLIENT]);
webhookModule.bind(WEBHOOK_TOKENS.WEBHOOK_USER_REPOSITORY).toClass(UsersRepository, []);
// Bind webhook repository with dependencies (Repository Pattern compliance)
webhookModule
.bind(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY)
.toClass(WebhookRepository, [DI_TOKENS.PRISMA_CLIENT]);
.toClass(WebhookRepository, [
DI_TOKENS.PRISMA_CLIENT,
WEBHOOK_TOKENS.WEBHOOK_EVENT_TYPE_REPOSITORY,
WEBHOOK_TOKENS.WEBHOOK_USER_REPOSITORY,
]);
// Bind services
webhookModule
@@ -68,3 +79,5 @@ webhookModule
webhookModule
.bind(WEBHOOK_TOKENS.WEBHOOK_NOTIFIER)
.toClass(WebhookNotifier, [WEBHOOK_TOKENS.WEBHOOK_NOTIFICATION_HANDLER, SHARED_TOKENS.LOGGER]);
export { webhookModule };
@@ -2,7 +2,7 @@ import type { IWebhookDataFetcher } from "@calcom/features/webhooks/lib/interfac
import type { IWebhookRepository } from "@calcom/features/webhooks/lib/interface/IWebhookRepository";
import type { ILogger } from "@calcom/features/webhooks/lib/interface/infrastructure";
import { WebhookTaskConsumer } from "@calcom/features/webhooks/lib/service/WebhookTaskConsumer";
import type { Container } from "@evyweb/ioctopus";
import type { ResolveFunction } from "@evyweb/ioctopus";
import { createModule } from "@evyweb/ioctopus";
import { SHARED_TOKENS } from "../../shared/shared.tokens";
import { WEBHOOK_TOKENS } from "../Webhooks.tokens";
@@ -18,16 +18,16 @@ import { WEBHOOK_TOKENS } from "../Webhooks.tokens";
*/
export const webhookTaskConsumerModule = createModule();
webhookTaskConsumerModule.bind(WEBHOOK_TOKENS.WEBHOOK_TASK_CONSUMER).toFactory((container: Container) => {
const webhookRepository = container.get<IWebhookRepository>(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY);
webhookTaskConsumerModule.bind(WEBHOOK_TOKENS.WEBHOOK_TASK_CONSUMER).toFactory((resolve: ResolveFunction) => {
const webhookRepository = resolve(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY) as IWebhookRepository;
const dataFetchers: IWebhookDataFetcher[] = [
container.get<IWebhookDataFetcher>(WEBHOOK_TOKENS.BOOKING_DATA_FETCHER),
container.get<IWebhookDataFetcher>(WEBHOOK_TOKENS.PAYMENT_DATA_FETCHER),
container.get<IWebhookDataFetcher>(WEBHOOK_TOKENS.FORM_DATA_FETCHER),
container.get<IWebhookDataFetcher>(WEBHOOK_TOKENS.RECORDING_DATA_FETCHER),
container.get<IWebhookDataFetcher>(WEBHOOK_TOKENS.OOO_DATA_FETCHER),
resolve(WEBHOOK_TOKENS.BOOKING_DATA_FETCHER) as IWebhookDataFetcher,
resolve(WEBHOOK_TOKENS.PAYMENT_DATA_FETCHER) as IWebhookDataFetcher,
resolve(WEBHOOK_TOKENS.FORM_DATA_FETCHER) as IWebhookDataFetcher,
resolve(WEBHOOK_TOKENS.RECORDING_DATA_FETCHER) as IWebhookDataFetcher,
resolve(WEBHOOK_TOKENS.OOO_DATA_FETCHER) as IWebhookDataFetcher,
];
const logger = container.get<ILogger>(SHARED_TOKENS.LOGGER);
const logger = resolve(SHARED_TOKENS.LOGGER) as ILogger;
return new WebhookTaskConsumer(webhookRepository, dataFetchers, logger);
});
@@ -0,0 +1,13 @@
/**
* EventTypes Repository Interface
*
* Central interface for EventType repository methods.
*/
export interface IEventTypesRepository {
/**
* @param eventTypeId - The event type to check
* @returns Parent ID if this is a managed child event type, null otherwise
*/
findParentEventTypeId(eventTypeId: number): Promise<number | null>;
}
@@ -1,3 +1,4 @@
import type { IEventTypesRepository } from "@calcom/features/eventtypes/eventtypes.repository.interface";
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import { LookupTarget, ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import type { UserWithLegacySelectedCalendars } from "@calcom/features/users/repositories/UserRepository";
@@ -9,8 +10,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
import { eventTypeSelect } from "@calcom/lib/server/eventTypeSelect";
import type { PrismaClient } from "@calcom/prisma";
import { availabilityUserSelect, userSelect as userSelectWithSelectedCalendars } from "@calcom/prisma";
import type { EventType as PrismaEventType } from "@calcom/prisma/client";
import type { Prisma } from "@calcom/prisma/client";
import type { Prisma, EventType as PrismaEventType } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { EventTypeMetaDataSchema, rrSegmentQueryValueSchema } from "@calcom/prisma/zod-utils";
@@ -78,9 +78,25 @@ function usersWithSelectedCalendars<
return users.map((user) => withSelectedCalendars(user));
}
export class EventTypeRepository {
export class EventTypeRepository implements IEventTypesRepository {
constructor(private prismaClient: PrismaClient) {}
async findParentEventTypeId(eventTypeId: number): Promise<number | null> {
const managedChildEventType = await this.prismaClient.eventType.findFirst({
where: {
id: eventTypeId,
parentId: {
not: null,
},
},
select: {
parentId: true,
},
});
return managedChildEventType?.parentId ?? null;
}
private generateCreateEventTypeData = (eventTypeCreateData: IEventType) => {
const {
userId,
@@ -2,4 +2,10 @@ import type { User } from "@calcom/prisma/client";
export interface IUsersRepository {
updateLastActiveAt(userId: number): Promise<User>;
/**
* @param userId - The user ID to query
* @returns User with team memberships, or null if user not found
*/
findUserTeams(userId: number): Promise<{ teams: { teamId: number }[] } | null>;
}
+20 -3
View File
@@ -1,7 +1,5 @@
import db, { prisma } from "@calcom/prisma";
import { captureException } from "@sentry/nextjs";
import db from "@calcom/prisma";
import type { IUsersRepository } from "./users.repository.interface";
export class UsersRepository implements IUsersRepository {
@@ -17,4 +15,23 @@ export class UsersRepository implements IUsersRepository {
throw err;
}
}
async findUserTeams(userId: number): Promise<{ teams: { teamId: number }[] } | null> {
try {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
teams: {
select: {
teamId: true,
},
},
},
});
return user;
} catch (err) {
captureException(err);
throw err;
}
}
}
@@ -1,6 +1,5 @@
import type { WebhookTriggerEvents, UserPermissionRole } from "@calcom/prisma/enums";
import type { Webhook, WebhookSubscriber, WebhookGroup } from "../dto/types";
import type { UserPermissionRole, WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { Webhook, WebhookGroup, WebhookSubscriber } from "../dto/types";
/**
* Webhook Version enum - defines the payload format versions.
@@ -8,20 +7,20 @@ import type { Webhook, WebhookSubscriber, WebhookGroup } from "../dto/types";
* This is a TypeScript-only enum (not Prisma).
* DB operations go through the repository which enforces these values.
*/
export const WebhookVersion = {
const WebhookVersion = {
V_2021_10_20: "2021-10-20",
} as const;
export type WebhookVersion = (typeof WebhookVersion)[keyof typeof WebhookVersion];
type WebhookVersion = (typeof WebhookVersion)[keyof typeof WebhookVersion];
/**
* Default webhook version - used for new webhooks and as fallback
*/
export const DEFAULT_WEBHOOK_VERSION = WebhookVersion.V_2021_10_20;
const VALID_WEBHOOK_VERSIONS = new Set<string>(Object.values(WebhookVersion));
const DEFAULT_WEBHOOK_VERSION: WebhookVersion = WebhookVersion.V_2021_10_20;
const VALID_WEBHOOK_VERSIONS: Set<string> = new Set<string>(Object.values(WebhookVersion));
export { WebhookVersion, DEFAULT_WEBHOOK_VERSION, VALID_WEBHOOK_VERSIONS };
export function isValidWebhookVersion(value: string): value is WebhookVersion {
return VALID_WEBHOOK_VERSIONS.has(value);
}
@@ -71,6 +70,10 @@ export interface IWebhookRepository {
timeUnit: string | null;
version: WebhookVersion;
}>;
findByOrgIdAndTrigger(options: {
orgId: number;
triggerEvent: WebhookTriggerEvents;
}): Promise<WebhookSubscriber[]>;
getFilteredWebhooksForUser(options: { userId: number; userRole?: UserPermissionRole }): Promise<{
webhookGroups: WebhookGroup[];
profiles: {
@@ -84,4 +87,3 @@ export interface IWebhookRepository {
}>;
listWebhooks(options: ListWebhooksOptions): Promise<Webhook[]>;
}
@@ -1,4 +1,8 @@
import type { IEventTypesRepository } from "@calcom/features/eventtypes/eventtypes.repository.interface";
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { UsersRepository } from "@calcom/features/users/users.repository";
import type { IUsersRepository } from "@calcom/features/users/users.repository.interface";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import type { PrismaClient } from "@calcom/prisma";
@@ -30,7 +34,7 @@ interface WebhookQueryResult {
priority: number; // This field is added by the query and removed before returning
}
const filterWebhooks = (webhook: { appId: string | null }) => {
const filterWebhooks = (webhook: { appId: string | null }): boolean => {
const appIds = [
"zapier",
"make",
@@ -41,17 +45,30 @@ const filterWebhooks = (webhook: { appId: string | null }) => {
};
export class WebhookRepository implements IWebhookRepository {
private static _instance: WebhookRepository;
private static _instance: WebhookRepository | undefined;
constructor(private readonly prisma: typeof defaultPrisma = defaultPrisma) {}
constructor(
private readonly prisma: PrismaClient,
private readonly eventTypeRepository: IEventTypesRepository,
private readonly userRepository: IUsersRepository
) {}
/**
* Singleton accessor for backward compatibility.
* @deprecated Use DI container (getWebhookFeature().repository) instead
*
* @deprecated Use DI container instead:
* ```typescript
* import { getWebhookFeature } from "@calcom/features/di/webhooks/containers/webhook";
* const { repository } = getWebhookFeature();
* ```
*
*/
static getInstance(): WebhookRepository {
if (!WebhookRepository._instance) {
WebhookRepository._instance = new WebhookRepository(defaultPrisma);
WebhookRepository._instance = new WebhookRepository(
defaultPrisma,
new EventTypeRepository(defaultPrisma),
new UsersRepository()
);
}
return WebhookRepository._instance;
}
@@ -66,18 +83,8 @@ export class WebhookRepository implements IWebhookRepository {
let managedParentEventTypeId: number | undefined;
if (eventTypeId) {
const managedChildEventType = await this.prisma.eventType.findFirst({
where: {
id: eventTypeId,
parentId: {
not: null,
},
},
select: {
parentId: true,
},
});
managedParentEventTypeId = managedChildEventType?.parentId ?? undefined;
managedParentEventTypeId =
(await this.eventTypeRepository.findParentEventTypeId(eventTypeId)) ?? undefined;
}
const webhooks = await this.getSubscribersRaw({
@@ -277,6 +284,7 @@ export class WebhookRepository implements IWebhookRepository {
teamId: orgId,
platform: false,
eventTriggers: { has: triggerEvent },
active: true,
},
select: {
id: true,
@@ -508,26 +516,15 @@ export class WebhookRepository implements IWebhookRepository {
{ appId: appId ?? null },
];
// Get user's teams
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { teams: true },
});
const user = await this.userRepository.findUserTeams(userId);
if (eventTypeId) {
// Check for managed event type parent
const managedParentEvt = await this.prisma.eventType.findFirst({
where: {
id: eventTypeId,
parentId: { not: null },
},
select: { parentId: true },
});
const managedParentId = await this.eventTypeRepository.findParentEventTypeId(eventTypeId);
if (managedParentEvt?.parentId) {
if (managedParentId) {
// Include webhooks from both the event type and its parent (if active)
whereConditions.push({
OR: [{ eventTypeId }, { eventTypeId: managedParentEvt.parentId, active: true }],
OR: [{ eventTypeId }, { eventTypeId: managedParentId, active: true }],
});
} else {
whereConditions.push({ eventTypeId });
@@ -1,10 +1,9 @@
import { DelegationCredentialRepository } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
import { DelegationCredentialErrorPayloadType } from "@calcom/features/webhooks/lib/dto/types";
import { getWebhookFeature } from "@calcom/features/di/webhooks/containers/webhook";
import type { DelegationCredentialErrorPayloadType } from "@calcom/features/webhooks/lib/dto/types";
import type { CalendarAppDelegationCredentialError } from "@calcom/lib/CalendarAppError";
import logger from "@calcom/lib/logger";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { WebhookRepository } from "./repository/WebhookRepository";
import sendPayload from "./sendPayload";
const log = logger.getSubLogger({ prefix: ["triggerDelegationCredentialErrorWebhook"] });
@@ -30,7 +29,7 @@ export async function triggerDelegationCredentialErrorWebhook(params: {
return;
}
const webhookRepository = WebhookRepository.getInstance();
const { repository: webhookRepository } = getWebhookFeature();
const webhooks = await webhookRepository.findByOrgIdAndTrigger({
orgId: delegationCredential.organizationId,
triggerEvent: WebhookTriggerEvents.DELEGATION_CREDENTIAL_ERROR,
@@ -1,6 +1,5 @@
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
import { getWebhookFeature } from "@calcom/features/di/webhooks/containers/webhook";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import type { TGetInputSchema } from "./get.schema";
type GetOptions = {
@@ -11,6 +10,6 @@ type GetOptions = {
};
export const getHandler = async ({ ctx: _ctx, input }: GetOptions) => {
const webhookRepository = WebhookRepository.getInstance();
const { repository: webhookRepository } = getWebhookFeature();
return await webhookRepository.findByWebhookId(input.webhookId);
};
@@ -1,5 +1,5 @@
import { getWebhookFeature } from "@calcom/features/di/webhooks/containers/webhook";
import type { WebhookGroup } from "@calcom/features/webhooks/lib/dto/types";
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
type GetByViewerOptions = {
@@ -21,7 +21,7 @@ export type WebhooksByViewer = {
export const getByViewerHandler = async ({ ctx }: GetByViewerOptions): Promise<WebhooksByViewer> => {
// Use the singleton instance to avoid creating new instances repeatedly
const webhookRepository = WebhookRepository.getInstance();
const { repository: webhookRepository } = getWebhookFeature();
return await webhookRepository.getFilteredWebhooksForUser({
userId: ctx.user.id,
userRole: ctx.user.role,
@@ -1,7 +1,6 @@
import { getWebhookFeature } from "@calcom/features/di/webhooks/containers/webhook";
import type { Webhook } from "@calcom/features/webhooks/lib/dto/types";
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import type { TListInputSchema } from "./list.schema";
type ListOptions = {
@@ -12,7 +11,7 @@ type ListOptions = {
};
export const listHandler = async ({ ctx, input }: ListOptions): Promise<Webhook[]> => {
const repository = WebhookRepository.getInstance();
const { repository } = getWebhookFeature();
return repository.listWebhooks({
userId: ctx.user.id,