Skip to content

Commit 2ac3104

Browse files
authored
fix: Selected calendar delegation credentials (calcom#24190)
* Fix selected calendar delegation credentials
1 parent ff264d6 commit 2ac3104

9 files changed

Lines changed: 97 additions & 60 deletions

File tree

packages/features/calendar-subscription/lib/CalendarSubscriptionService.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import type {
99
import type { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
1010
import type { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
1111
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
12-
import { getCredentialForCalendarCache } from "@calcom/lib/delegationCredential/server";
12+
import { getCredentialForSelectedCalendar } from "@calcom/lib/delegationCredential/server";
1313
import logger from "@calcom/lib/logger";
1414
import type { ISelectedCalendarRepository } from "@calcom/lib/server/repository/SelectedCalendarRepository.interface";
15-
import type { SelectedCalendar } from "@calcom/prisma/client";
15+
import { SelectedCalendar } from "@calcom/prisma/client";
1616

1717
const log = logger.getSubLogger({ prefix: ["CalendarSubscriptionService"] });
1818

@@ -35,15 +35,18 @@ export class CalendarSubscriptionService {
3535
*/
3636
async subscribe(selectedCalendarId: string): Promise<void> {
3737
log.debug("subscribe", { selectedCalendarId });
38-
const selectedCalendar = await this.deps.selectedCalendarRepository.findByIdWithCredentials(
39-
selectedCalendarId
40-
);
41-
if (!selectedCalendar?.credentialId) {
38+
const selectedCalendar = await this.deps.selectedCalendarRepository.findById(selectedCalendarId);
39+
if (!selectedCalendar) {
4240
log.debug("Selected calendar not found", { selectedCalendarId });
4341
return;
4442
}
4543

46-
const credential = await this.getCredential(selectedCalendar.credentialId);
44+
if (!selectedCalendar.credentialId && !selectedCalendar.delegationCredentialId) {
45+
log.debug("Selected calendar doesn't have credentials", { selectedCalendarId });
46+
return;
47+
}
48+
49+
const credential = await this.getCredential(selectedCalendar);
4750
if (!credential) {
4851
log.debug("Calendar credential not found", { selectedCalendarId });
4952
return;
@@ -72,12 +75,18 @@ export class CalendarSubscriptionService {
7275
*/
7376
async unsubscribe(selectedCalendarId: string): Promise<void> {
7477
log.debug("unsubscribe", { selectedCalendarId });
75-
const selectedCalendar = await this.deps.selectedCalendarRepository.findByIdWithCredentials(
76-
selectedCalendarId
77-
);
78-
if (!selectedCalendar?.credentialId) return;
78+
const selectedCalendar = await this.deps.selectedCalendarRepository.findById(selectedCalendarId);
79+
if (!selectedCalendar) {
80+
log.debug("Selected calendar not found", { selectedCalendarId });
81+
return;
82+
}
83+
84+
if (!selectedCalendar.credentialId && !selectedCalendar.delegationCredentialId) {
85+
log.debug("Selected calendar doesn't have credentials", { selectedCalendarId });
86+
return;
87+
}
7988

80-
const credential = await this.getCredential(selectedCalendar.credentialId);
89+
const credential = await this.getCredential(selectedCalendar);
8190
if (!credential) return;
8291

8392
const calendarSubscriptionAdapter = this.deps.adapterFactory.get(
@@ -133,10 +142,11 @@ export class CalendarSubscriptionService {
133142
selectedCalendar.integration as CalendarSubscriptionProvider
134143
);
135144

136-
if (!selectedCalendar.credentialId) {
137-
log.debug("Selected calendar credential not found", { channelId: selectedCalendar.channelId });
145+
if (!selectedCalendar.credentialId && !selectedCalendar.delegationCredentialId) {
146+
log.debug("Selected Calendar doesn't have credentials", { selectedCalendarId: selectedCalendar.id });
138147
return;
139148
}
149+
140150
// for cache the feature should be enabled globally and by user/team features
141151
const [cacheEnabled, syncEnabled, cacheEnabledForUser] = await Promise.all([
142152
this.isCacheEnabled(),
@@ -150,7 +160,7 @@ export class CalendarSubscriptionService {
150160
}
151161

152162
log.debug("Processing events", { channelId: selectedCalendar.channelId });
153-
const credential = await this.getCredential(selectedCalendar.credentialId);
163+
const credential = await this.getCredential(selectedCalendar);
154164
if (!credential) return;
155165

156166
let events: CalendarSubscriptionEvent | null = null;
@@ -236,8 +246,12 @@ export class CalendarSubscriptionService {
236246
/**
237247
* Get credential with delegation if available
238248
*/
239-
private async getCredential(credentialId: number): Promise<CalendarCredential | null> {
240-
const credential = await getCredentialForCalendarCache({ credentialId });
249+
private async getCredential(selectedCalendar: SelectedCalendar): Promise<CalendarCredential | null> {
250+
if (!selectedCalendar.credentialId && !selectedCalendar.delegationCredentialId) {
251+
log.debug("Selected calendar doesn't have any credential");
252+
return null;
253+
}
254+
const credential = await getCredentialForSelectedCalendar(selectedCalendar);
241255
if (!credential) return null;
242256
return {
243257
...credential,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { vi } from "vitest";
22

3-
export const getCredentialForCalendarCache = vi.fn().mockResolvedValue({
3+
export const getCredentialForSelectedCalendar = vi.fn().mockResolvedValue({
44
id: 1,
55
key: { access_token: "test-token" },
66
user: { email: "test@example.com" },
77
delegatedTo: null,
88
});
99

1010
vi.doMock("@calcom/lib/delegationCredential/server", () => ({
11-
getCredentialForCalendarCache,
11+
getCredentialForSelectedCalendar,
1212
}));

packages/features/calendar-subscription/lib/__tests__/CalendarSubscriptionService.test.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe("CalendarSubscriptionService", () => {
115115
};
116116

117117
mockSelectedCalendarRepository = {
118-
findByIdWithCredentials: vi.fn().mockResolvedValue(mockSelectedCalendar),
118+
findById: vi.fn().mockResolvedValue(mockSelectedCalendar),
119119
findByChannelId: vi.fn().mockResolvedValue(mockSelectedCalendar),
120120
findNextSubscriptionBatch: vi.fn().mockResolvedValue([mockSelectedCalendar]),
121121
updateSyncStatus: vi.fn().mockResolvedValue(mockSelectedCalendar),
@@ -145,15 +145,15 @@ describe("CalendarSubscriptionService", () => {
145145
calendarSyncService: mockCalendarSyncService,
146146
});
147147

148-
const { getCredentialForCalendarCache } = await import("../__mocks__/delegationCredential");
149-
getCredentialForCalendarCache.mockResolvedValue(mockCredential);
148+
const { getCredentialForSelectedCalendar } = await import("../__mocks__/delegationCredential");
149+
getCredentialForSelectedCalendar.mockResolvedValue(mockCredential);
150150
});
151151

152152
describe("subscribe", () => {
153153
test("should successfully subscribe to a calendar", async () => {
154154
await service.subscribe("test-calendar-id");
155155

156-
expect(mockSelectedCalendarRepository.findByIdWithCredentials).toHaveBeenCalledWith("test-calendar-id");
156+
expect(mockSelectedCalendarRepository.findById).toHaveBeenCalledWith("test-calendar-id");
157157
expect(mockAdapterFactory.get).toHaveBeenCalledWith("google_calendar");
158158
expect(mockAdapter.subscribe).toHaveBeenCalledWith(mockSelectedCalendar, mockCredential);
159159
expect(mockSelectedCalendarRepository.updateSubscription).toHaveBeenCalledWith("test-calendar-id", {
@@ -167,18 +167,19 @@ describe("CalendarSubscriptionService", () => {
167167
});
168168

169169
test("should return early if selected calendar not found", async () => {
170-
mockSelectedCalendarRepository.findByIdWithCredentials.mockResolvedValue(null);
170+
mockSelectedCalendarRepository.findById.mockResolvedValue(null);
171171

172172
await service.subscribe("non-existent-id");
173173

174174
expect(mockAdapter.subscribe).not.toHaveBeenCalled();
175175
expect(mockSelectedCalendarRepository.updateSubscription).not.toHaveBeenCalled();
176176
});
177177

178-
test("should return early if selected calendar has no credentialId", async () => {
179-
mockSelectedCalendarRepository.findByIdWithCredentials.mockResolvedValue({
178+
test("should return early if selected calendar has no credentialId or delegationCredentialId", async () => {
179+
mockSelectedCalendarRepository.findById.mockResolvedValue({
180180
...mockSelectedCalendar,
181181
credentialId: null,
182+
delegationCredentialId: null,
182183
});
183184

184185
await service.subscribe("test-calendar-id");
@@ -194,7 +195,7 @@ describe("CalendarSubscriptionService", () => {
194195

195196
await service.unsubscribe("test-calendar-id");
196197

197-
expect(mockSelectedCalendarRepository.findByIdWithCredentials).toHaveBeenCalledWith("test-calendar-id");
198+
expect(mockSelectedCalendarRepository.findById).toHaveBeenCalledWith("test-calendar-id");
198199
expect(mockAdapter.unsubscribe).toHaveBeenCalledWith(mockSelectedCalendar, mockCredential);
199200
expect(mockSelectedCalendarRepository.updateSubscription).toHaveBeenCalledWith("test-calendar-id", {
200201
syncSubscribedAt: null,
@@ -211,7 +212,7 @@ describe("CalendarSubscriptionService", () => {
211212
});
212213

213214
test("should return early if selected calendar not found", async () => {
214-
mockSelectedCalendarRepository.findByIdWithCredentials.mockResolvedValue(null);
215+
mockSelectedCalendarRepository.findById.mockResolvedValue(null);
215216

216217
await service.unsubscribe("non-existent-id");
217218

@@ -356,10 +357,11 @@ describe("CalendarSubscriptionService", () => {
356357
expect(mockCalendarSyncService.handleEvents).not.toHaveBeenCalled();
357358
});
358359

359-
test("should return early when selected calendar has no credentialId", async () => {
360+
test("should return early when selected calendar has no credentialId or delegationCredentialId", async () => {
360361
const calendarWithoutCredential = {
361362
...mockSelectedCalendar,
362363
credentialId: null,
364+
delegationCredentialId: null,
363365
};
364366

365367
await service.processEvents(calendarWithoutCredential);

packages/features/calendar-subscription/lib/cache/CalendarCacheWrapper.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,10 @@ export class CalendarCacheWrapper implements Calendar {
6060
async getAvailability(
6161
dateFrom: string,
6262
dateTo: string,
63-
selectedCalendars: IntegrationCalendar[],
64-
shouldServeCache?: boolean
63+
selectedCalendars: IntegrationCalendar[]
64+
// _shouldServeCache?: boolean
6565
// _fallbackToPrimary?: boolean
6666
): Promise<EventBusyDate[]> {
67-
if (!shouldServeCache) {
68-
return this.deps.originalCalendar.getAvailability(dateFrom, dateTo, selectedCalendars);
69-
}
70-
7167
log.debug("getAvailability from cache", { dateFrom, dateTo, selectedCalendars });
7268
const selectedCalendarIds = selectedCalendars.map((e) => e.id).filter((id): id is string => Boolean(id));
7369
if (!selectedCalendarIds.length) {

packages/lib/delegationCredential/server.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { safeStringify } from "@calcom/lib/safeStringify";
88
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
99
import type { ServiceAccountKey } from "@calcom/lib/server/repository/delegationCredential";
1010
import { DelegationCredentialRepository } from "@calcom/lib/server/repository/delegationCredential";
11-
import prisma from "@calcom/prisma";
11+
import { prisma } from "@calcom/prisma";
12+
import type { SelectedCalendar } from "@calcom/prisma/client";
1213
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
1314

1415
import { UserRepository } from "../server/repository/user";
@@ -650,3 +651,32 @@ export async function getCredentialForCalendarCache({ credentialId }: { credenti
650651
}
651652
return credentialForCalendarService;
652653
}
654+
655+
/**
656+
* Find the credential for a selected calendar
657+
* @param selectedCalendar
658+
*/
659+
export async function getCredentialForSelectedCalendar({
660+
credentialId,
661+
delegationCredentialId,
662+
userId,
663+
}: Partial<SelectedCalendar>) {
664+
if (credentialId) {
665+
const credentialRepository = new CredentialRepository(prisma);
666+
const credential = await credentialRepository.findByIdWithDelegationCredential(credentialId);
667+
if (credential?.delegationCredential?.id && credential.userId) {
668+
return findUniqueDelegationCalendarCredential({
669+
userId: credential.userId,
670+
delegationCredentialId: credential.delegationCredential.id,
671+
});
672+
}
673+
return credential ? buildNonDelegationCredential(credential) : undefined;
674+
}
675+
if (delegationCredentialId && userId) {
676+
return findUniqueDelegationCalendarCredential({
677+
userId,
678+
delegationCredentialId,
679+
});
680+
}
681+
return undefined;
682+
}

packages/lib/server/repository/SelectedCalendarRepository.interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import type { Prisma, SelectedCalendar } from "@calcom/prisma/client";
22

33
export interface ISelectedCalendarRepository {
44
/**
5-
* Find selected calendar by id with credentials
5+
* Find selected calendar by id
66
*
77
* @param id
88
*/
9-
findByIdWithCredentials(id: string): Promise<SelectedCalendar | null>;
9+
findById(id: string): Promise<SelectedCalendar | null>;
1010

1111
/**
1212
* Find selected calendar by channel id

packages/lib/server/repository/SelectedCalendarRepository.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,9 @@ import type { Prisma } from "@calcom/prisma/client";
55
export class SelectedCalendarRepository implements ISelectedCalendarRepository {
66
constructor(private prismaClient: PrismaClient) {}
77

8-
async findByIdWithCredentials(id: string) {
8+
async findById(id: string) {
99
return this.prismaClient.selectedCalendar.findUnique({
1010
where: { id },
11-
include: {
12-
credential: {
13-
select: {
14-
delegationCredential: true,
15-
},
16-
},
17-
},
1811
});
1912
}
2013

packages/lib/server/repository/__tests__/SelectedCalendarRepository.test.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe("SelectedCalendarRepository", () => {
5555
vi.clearAllMocks();
5656
});
5757

58-
describe("findByIdWithCredentials", () => {
58+
describe("findById", () => {
5959
test("should find selected calendar by id with credential delegation", async () => {
6060
const mockCalendarWithCredential = {
6161
...mockSelectedCalendar,
@@ -69,17 +69,10 @@ describe("SelectedCalendarRepository", () => {
6969

7070
vi.mocked(mockPrismaClient.selectedCalendar.findUnique).mockResolvedValue(mockCalendarWithCredential);
7171

72-
const result = await repository.findByIdWithCredentials("test-calendar-id");
72+
const result = await repository.findById("test-calendar-id");
7373

7474
expect(mockPrismaClient.selectedCalendar.findUnique).toHaveBeenCalledWith({
7575
where: { id: "test-calendar-id" },
76-
include: {
77-
credential: {
78-
select: {
79-
delegationCredential: true,
80-
},
81-
},
82-
},
8376
});
8477

8578
expect(result).toEqual(mockCalendarWithCredential);
@@ -88,7 +81,7 @@ describe("SelectedCalendarRepository", () => {
8881
test("should return null when calendar not found", async () => {
8982
vi.mocked(mockPrismaClient.selectedCalendar.findUnique).mockResolvedValue(null);
9083

91-
const result = await repository.findByIdWithCredentials("non-existent-id");
84+
const result = await repository.findById("non-existent-id");
9285

9386
expect(result).toBeNull();
9487
});

packages/lib/server/repository/credential.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logger from "@calcom/lib/logger";
22
import { prisma } from "@calcom/prisma";
3-
import type { Prisma } from "@calcom/prisma/client";
3+
import type { Prisma, PrismaClient } from "@calcom/prisma/client";
44
import { safeCredentialSelect } from "@calcom/prisma/selects/credential";
55
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
66

@@ -10,22 +10,31 @@ const log = logger.getSubLogger({ prefix: ["CredentialRepository"] });
1010

1111
type CredentialCreateInput = {
1212
type: string;
13-
key: any;
13+
key: object;
1414
userId: number;
1515
appId: string;
1616
delegationCredentialId?: string | null;
1717
};
1818

1919
type CredentialUpdateInput = {
2020
type?: string;
21-
key?: any;
21+
key?: object;
2222
userId?: number;
2323
appId?: string;
2424
delegationCredentialId?: string | null;
2525
invalid?: boolean;
2626
};
2727

2828
export class CredentialRepository {
29+
constructor(private primaClient: PrismaClient) {}
30+
31+
async findByIdWithDelegationCredential(id: number) {
32+
return this.primaClient.credential.findUnique({
33+
where: { id },
34+
select: { ...credentialForCalendarServiceSelect, delegationCredential: true },
35+
});
36+
}
37+
2938
static async create(data: CredentialCreateInput) {
3039
const credential = await prisma.credential.create({ data: { ...data } });
3140
return buildNonDelegationCredential(credential);
@@ -157,7 +166,7 @@ export class CredentialRepository {
157166
return {
158167
...rest,
159168
// We queried only those where delegationCredentialId is not null
160-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
169+
161170
delegationCredentialId: delegationCredentialId!,
162171
};
163172
});

0 commit comments

Comments
 (0)