Files
512002aaef refactor: replace FeaturesRepository with DI-based feature repositories (#27200)
* refactor: replace FeaturesRepository with DI-based feature repositories

Migrate from the legacy FeaturesRepository pattern to separate DI-based repositories:
- Add checkIfFeatureIsEnabledGlobally to IFeatureRepository
- Add getTeamsWithFeatureEnabled to ITeamFeatureRepository
- Update CalendarSubscriptionService to use featureRepository, teamFeatureRepository, and userFeatureRepository
- Update SelectedCalendarRepository.findNextSubscriptionBatch to filter by teamIds instead of featureIds
- Update OnboardingPathService to use DI via getFeatureRepository()
- Remove prisma parameter from OnboardingPathService callsites

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* test: update tests to use new DI-based feature repositories

Update CalendarSubscriptionService and SelectedCalendarRepository tests
to use the new separate repository interfaces:
- featureRepository for global feature checks
- teamFeatureRepository for team-level feature checks
- userFeatureRepository for user-level feature checks

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: update API routes to use DI-based feature repositories

Update cron and webhook routes to use the new separate repository
interfaces instead of the combined FeaturesRepository.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: remove prisma arg from getGettingStartedPathWithParams call

The OnboardingPathService method no longer requires a prisma argument
as it now uses DI containers internally.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* test: update route tests to expect new DI-based feature repositories

Update service instantiation tests to expect featureRepository,
teamFeatureRepository, and userFeatureRepository instead of the
old featuresRepository.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: optimize global feature check and add guard in checkForNewSubscriptions

- Use targeted select for only `enabled` field in checkIfFeatureIsEnabledGlobally
- Add global feature flag guard in checkForNewSubscriptions to avoid unnecessary
  DB queries and API calls when the cache feature is globally disabled

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* remove redundant comment

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-02-06 09:34:03 +00:00

91 lines
4.4 KiB
TypeScript

import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import type { CalendarSubscriptionProvider } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { DefaultAdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository";
import { getTeamFeatureRepository } from "@calcom/features/di/containers/TeamFeatureRepository";
import { getUserFeatureRepository } from "@calcom/features/di/containers/UserFeatureRepository";
import { SelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository";
import logger from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
import type { Params } from "app/_types";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
const log = logger.getSubLogger({ prefix: ["calendar-webhook"] });
function extractAndValidateProviderFromParams(params: Params): CalendarSubscriptionProvider | null {
if (!("provider" in params)) {
return null;
}
const { provider } = params;
if (provider === "google_calendar" || provider === "office365_calendar") {
return provider;
}
return null;
}
/**
* Handles incoming POST requests for calendar webhooks.
* It processes the webhook based on the calendar provider specified in the URL.
* If the provider is unsupported, it returns a 400 response.
* If the webhook is processed successfully, it returns a 200 response.
* In case of errors during processing, it returns a 500 response with the error message.
* @param {NextRequest} request - The incoming request object.
* @param {Object} context - The context object containing route parameters.
* @param {Promise<Params>} context.params - A promise that resolves to the route parameters.
* @returns {Promise<NextResponse>} - A promise that resolves to the response object.
*/
async function postHandler(request: NextRequest, ctx: { params: Promise<Params> }) {
const providerFromParams = extractAndValidateProviderFromParams(await ctx.params);
if (!providerFromParams) {
return NextResponse.json({ message: "Unsupported provider" }, { status: 400 });
}
try {
// instantiate dependencies
const bookingRepository = new BookingRepository(prisma);
const calendarSyncService = new CalendarSyncService({
bookingRepository,
});
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
const calendarCacheEventService = new CalendarCacheEventService({
calendarCacheEventRepository,
});
const calendarSubscriptionService = new CalendarSubscriptionService({
adapterFactory: new DefaultAdapterFactory(),
selectedCalendarRepository: new SelectedCalendarRepository(prisma),
featureRepository: getFeatureRepository(),
teamFeatureRepository: getTeamFeatureRepository(),
userFeatureRepository: getUserFeatureRepository(),
calendarSyncService,
calendarCacheEventService,
});
// are features globally enabled
const [isCacheEnabled, isSyncEnabled] = await Promise.all([
calendarSubscriptionService.isCacheEnabled(),
calendarSubscriptionService.isSyncEnabled(),
]);
if (!isCacheEnabled && !isSyncEnabled) {
log.debug("No cache or sync enabled");
return NextResponse.json({ message: "No cache or sync enabled" }, { status: 200 });
}
await calendarSubscriptionService.processWebhook(providerFromParams, request);
return NextResponse.json({ message: "Webhook processed" }, { status: 200 });
} catch (error) {
log.error("Error processing webhook", { error });
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ message }, { status: 500 });
}
}
export const POST = defaultResponderForAppDir(postHandler);