diff --git a/agents/knowledge-base.md b/agents/knowledge-base.md index 13bfe08e7cea20..6ff4ddf7426b1a 100644 --- a/agents/knowledge-base.md +++ b/agents/knowledge-base.md @@ -730,4 +730,210 @@ When doing logic that depends on Browser locale, use i18n.language (prefer to de Note that with Date, you’re dealing with System time, so it’s not suited to everywhere (such as in the Booker, where instead we’ll likely migrate to Temporal) - but in most cases the above are suitable. -The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It’s a non-ideal solution to a problem that unfortunately exists due to Dayjs. +The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It's a non-ideal solution to a problem that unfortunately exists due to Dayjs. + +## Dependency Injection (DI) Pattern + +We use a Dependency Injection pattern powered by `@evyweb/ioctopus` to manage service and repository dependencies. This pattern ensures proper dependency management, testability, and consistent instantiation of services throughout the codebase. + +### Core Concepts + +The DI system consists of three main components: + +**Tokens** (`packages/features/di/tokens.ts`): Unique symbols that identify each service or repository in the DI container. Every injectable class needs a corresponding token. + +**Modules** (`packages/features/di/modules/*.ts`): Define how classes are instantiated and what dependencies they require. Modules bind tokens to class implementations. + +**Containers** (`packages/features/di/containers/*.ts`): Assemble modules together and expose getter functions that consumers use to obtain service instances. + +### How It Works + +Here's the flow using `BusyTimesService` as an example: + +**Step 1: Define the service class with constructor injection** + +```typescript +// packages/features/busyTimes/services/getBusyTimes.ts +export interface IBusyTimesService { + bookingRepo: BookingRepository; +} + +export class BusyTimesService { + constructor(public readonly dependencies: IBusyTimesService) {} + + async getBusyTimes(params: {...}) { + // Use dependencies via this.dependencies.bookingRepo + const bookings = await this.dependencies.bookingRepo.findAllExistingBookingsForEventTypeBetween({...}); + // ... + } +} +``` + +**Step 2: Create a module that binds the service to its token** + +```typescript +// packages/features/di/modules/BusyTimes.ts +import { BusyTimesService } from "@calcom/features/busyTimes/services/getBusyTimes"; +import { createModule } from "../di"; +import { DI_TOKENS } from "../tokens"; + +export const busyTimesModule = createModule(); +busyTimesModule.bind(DI_TOKENS.BUSY_TIMES_SERVICE).toClass(BusyTimesService, { + bookingRepo: DI_TOKENS.BOOKING_REPOSITORY, +} satisfies Record); +``` + +**Step 3: Create a container that loads all required modules and exposes a getter** + +```typescript +// packages/features/di/containers/BusyTimes.ts +import type { BusyTimesService } from "@calcom/features/busyTimes/services/getBusyTimes"; +import { DI_TOKENS } from "@calcom/features/di/tokens"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; +import { createContainer } from "../di"; +import { bookingRepositoryModule } from "../modules/Booking"; +import { busyTimesModule } from "../modules/BusyTimes"; + +const container = createContainer(); +container.load(DI_TOKENS.PRISMA_MODULE, prismaModule); +container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule); +container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule); + +export function getBusyTimesService() { + return container.get(DI_TOKENS.BUSY_TIMES_SERVICE); +} +``` + +**Step 4: Use the service via the container's getter function** + +```typescript +// Anywhere in the codebase +import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes"; + +const busyTimesService = getBusyTimesService(); +const busyTimes = await busyTimesService.getBusyTimes({...}); +``` + +### Common Mistakes to Avoid + +**Mistake 1: Creating a repository or service class with all static methods** + +Static methods bypass the DI system entirely, making the code harder to test and breaking the dependency chain. + +```typescript +// ❌ Bad - Static methods bypass DI +export class BookingRepository { + static async findById(id: string) { + return prisma.booking.findUnique({ where: { id } }); + } + + static async create(data: BookingCreateInput) { + return prisma.booking.create({ data }); + } +} + +// Usage (wrong - no DI) +const booking = await BookingRepository.findById("123"); +``` + +```typescript +// ✅ Good - Instance methods with constructor injection +export class BookingRepository { + constructor(private prismaClient: PrismaClient) {} + + async findById(id: string) { + return this.prismaClient.booking.findUnique({ where: { id } }); + } + + async create(data: BookingCreateInput) { + return this.prismaClient.booking.create({ data }); + } +} + +// Usage (correct - via DI container) +const bookingRepo = getBookingRepository(); +const booking = await bookingRepo.findById("123"); +``` + +**Mistake 2: Manually instantiating a class instead of using the DI container** + +Even if you define a class with constructor injection, manually calling `new` bypasses the DI system and its benefits. + +```typescript +// ❌ Bad - Manual instantiation bypasses DI +import { BusyTimesService } from "@calcom/features/busyTimes/services/getBusyTimes"; +import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; +import prisma from "@calcom/prisma"; + +// Wrong: manually creating instances +const bookingRepo = new BookingRepository(prisma); +const busyTimesService = new BusyTimesService({ bookingRepo }); +const busyTimes = await busyTimesService.getBusyTimes({...}); +``` + +```typescript +// ✅ Good - Use the DI container's getter function +import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes"; + +// Correct: let the container manage instantiation +const busyTimesService = getBusyTimesService(); +const busyTimes = await busyTimesService.getBusyTimes({...}); +``` + +**Mistake 3: Importing Prisma directly in a service instead of using repository injection** + +Services should depend on repositories, not directly on Prisma. This maintains proper separation of concerns. + +```typescript +// ❌ Bad - Service imports Prisma directly +import prisma from "@calcom/prisma"; + +export class MyService { + async doSomething() { + const bookings = await prisma.booking.findMany({...}); // Wrong! + } +} +``` + +```typescript +// ✅ Good - Service depends on repository via DI +export interface IMyService { + bookingRepo: BookingRepository; +} + +export class MyService { + constructor(public readonly dependencies: IMyService) {} + + async doSomething() { + const bookings = await this.dependencies.bookingRepo.findMany({...}); + } +} +``` + +### Adding a New Service to the DI System + +When creating a new service or repository that should use DI: + +1. **Add tokens** to `packages/features/di/tokens.ts`: + ```typescript + export const DI_TOKENS = { + // ...existing tokens + MY_SERVICE: Symbol("MyService"), + MY_SERVICE_MODULE: Symbol("MyServiceModule"), + }; + ``` + +2. **Create the service class** with a dependencies interface and constructor injection. + +3. **Create a module** in `packages/features/di/modules/MyService.ts` that binds the service to its token. + +4. **Create a container** in `packages/features/di/containers/MyService.ts` that loads all required modules and exports a getter function. + +5. **Use the getter function** everywhere you need the service - never manually instantiate. + +### Why Use DI? + +- **Testability**: Dependencies can be easily mocked in tests by providing alternative implementations +- **Consistency**: All instances are created the same way with proper dependencies +- **Maintainability**: Changing a dependency only requires updating the module binding, not every usage site +- **Explicit dependencies**: The dependency graph is clear and documented in the module definitions