Files
calendar/packages/features/watchlist/lib/service/WatchlistService.ts
T
Syed Ali ShahbazandGitHub a2bee76da6 feat: Add spam blocker DI structure (#24040)
* --init

* --

* replace old structure with new DI

* fix type

* --

* moving stuff around

* moving stuff around again

* minor clean up

* --

* resolve conflict

* clea up

* old schema clean up

* further clean up

* removing unwanted merged responsibilities

* --

* improve DI and SOLID

* some type fixes

* --1

* clean up --cont

* fix DI in facade for test

* more fix

* fix

* checking

* o.o

* fix import

* fix failing test --2

* fix failing test --3

* fix failing test --4

* normalization use

* introduce facade container injection

* uniform async telemetry spans

* further improvements

* replace prismock with repo mocks

* ensure we don't pass prisma outside of repo

* fix import

* remove try catch from repo calls

* async await fixes

* using deps pattern

* more clean up and fixes

* more clean up

* address feedback --1

* separation of concern

* clean up

* test clean up

* feedback --2

* remove extra fetch

* remove await

* migrate _post test from prismock

* fix type

* --

* fix tokens path

* rename AuditRepo

* test --1

* update _post to integration test

* fix test

* fix test

* test fix maybe?

* --

* feedback

* feedback

* fixes

* more feedback

* NIT

* use sentry and logger imports as planned

* assertion in test

* add missing test case

* add tests for controllers and services

* NITs

* fix domain normalisation
2025-10-14 10:16:18 +03:00

88 lines
2.8 KiB
TypeScript

import type logger from "@calcom/lib/logger";
import { WatchlistType } from "@calcom/prisma/enums";
import type {
IGlobalWatchlistRepository,
IOrganizationWatchlistRepository,
} from "../interface/IWatchlistRepositories";
import type { IWatchlistService } from "../interface/IWatchlistService";
import type { WatchlistEntry, CreateWatchlistEntryData, UpdateWatchlistEntryData } from "../types";
import { normalizeEmail, normalizeDomain } from "../utils/normalization";
type Deps = {
globalRepo: IGlobalWatchlistRepository;
orgRepo: IOrganizationWatchlistRepository;
logger: ReturnType<typeof logger.getSubLogger>;
};
export class WatchlistService implements IWatchlistService {
private readonly log: ReturnType<typeof logger.getSubLogger>;
constructor(private readonly deps: Deps) {
this.log = deps.logger;
}
async createEntry(data: CreateWatchlistEntryData): Promise<WatchlistEntry> {
const isGlobal = data.isGlobal ?? false;
// Normalize value based on type (service layer responsibility)
const normalizedValue =
data.type === WatchlistType.EMAIL ? normalizeEmail(data.value) : normalizeDomain(data.value);
const payload = {
type: data.type,
value: normalizedValue,
description: data.description,
action: data.action,
source: data.source,
};
// Global path
if (isGlobal) {
return this.deps.globalRepo.createEntry(payload);
}
// Org path (validate, then narrow)
const orgId = data.organizationId;
if (orgId == null) {
this.log.error("organizationId required for non-global entry", { type: data.type, value: data.value });
throw new Error("organizationId is required for organization-scoped entries");
}
return this.deps.orgRepo.createEntry(orgId, payload);
}
async updateEntry(id: string, data: UpdateWatchlistEntryData): Promise<WatchlistEntry> {
// Normalize value if provided (service layer responsibility)
const payload = {
...data,
...(data.value && {
value: data.type === WatchlistType.EMAIL ? normalizeEmail(data.value) : normalizeDomain(data.value),
}),
};
return this.deps.globalRepo.updateEntry(id, payload);
}
async deleteEntry(id: string): Promise<void> {
return this.deps.globalRepo.deleteEntry(id);
}
async getEntry(id: string): Promise<WatchlistEntry | null> {
return this.deps.globalRepo.findById(id);
}
/**
* System admin method to get ALL watchlist entries across the entire system
* Returns both global entries and all organization-specific entries from every organization
*/
async listAllSystemEntries(): Promise<WatchlistEntry[]> {
const [globalEntries, allOrgEntries] = await Promise.all([
this.deps.globalRepo.listBlockedEntries(),
this.deps.orgRepo.listAllOrganizationEntries(),
]);
return [...globalEntries, ...allOrgEntries];
}
}