Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b2751c5
feat(api-v2): add event-type level selected calendars for conflict ch…
devin-ai-integration[bot] Jan 12, 2026
9ea142b
test(api-v2): add e2e tests for event-type level selected calendars
devin-ai-integration[bot] Jan 12, 2026
3bb490d
Merge branch 'main' into devin/event-type-selected-calendars-1768203692
ThyMinimalDev Jan 12, 2026
152dff0
fix(api-v2): address review feedback for selected calendars feature
devin-ai-integration[bot] Jan 12, 2026
62f4854
Merge branch 'devin/event-type-selected-calendars-1768203692' of http…
devin-ai-integration[bot] Jan 12, 2026
8eb70de
fix(api-v2): include selectedCalendars in GET event-type endpoint
devin-ai-integration[bot] Jan 12, 2026
646253d
Merge branch 'main' into devin/event-type-selected-calendars-1768203692
devin-ai-integration[bot] Jan 12, 2026
7dc025a
Merge branch 'main' into devin/event-type-selected-calendars-1768203692
ThyMinimalDev Jan 12, 2026
072f5f1
refactor(api-v2): address review feedback for selected calendars
devin-ai-integration[bot] Jan 16, 2026
53ba3e9
fix(api-v2): forward destinationCalendar to updateEventType in create…
devin-ai-integration[bot] Jan 16, 2026
d2f8059
refactor(api-v2): remove useEventLevelSelectedCalendars from API types
devin-ai-integration[bot] Jan 16, 2026
9277dfb
fix(api-v2): remove useEventLevelSelectedCalendars from public endpoi…
devin-ai-integration[bot] Jan 16, 2026
6c29eb0
fix(api-v2): remove remaining useEventLevelSelectedCalendars referenc…
devin-ai-integration[bot] Jan 16, 2026
b1df6e3
Merge branch 'devin/event-type-selected-calendars-1768203692' of http…
devin-ai-integration[bot] Jan 16, 2026
05aa421
fix(api-v2): revert function rename and fix type error in output service
devin-ai-integration[bot] Jan 16, 2026
7d238dd
fix(api-v2): update E2E tests to remove useEventLevelSelectedCalendar…
devin-ai-integration[bot] Jan 16, 2026
fc259b4
refactor(api-v2): separate function for removing selectedCalendars fr…
devin-ai-integration[bot] Jan 16, 2026
8f67055
Merge branch 'main' into devin/event-type-selected-calendars-1768203692
ThyMinimalDev Jan 18, 2026
34cd105
refactor(api-v2): move Prisma calls from service to repositories
devin-ai-integration[bot] Jan 18, 2026
e3aa227
Merge branch 'devin/event-type-selected-calendars-1768203692' of http…
devin-ai-integration[bot] Jan 18, 2026
c504e31
fix(api-v2): restore PrismaWriteService in service constructor
devin-ai-integration[bot] Jan 18, 2026
450ab6a
refactor(api-v2): keep transaction in service, move Prisma calls to r…
devin-ai-integration[bot] Jan 18, 2026
5138284
refactor(api-v2): move selected calendar tx methods to SelectedCalend…
devin-ai-integration[bot] Jan 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3189,4 +3189,206 @@ describe("Event types Endpoints", () => {
await app.close();
});
});

describe("Event-type level selected calendars", () => {
let app: INestApplication;

let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;

const userEmail = `selected-calendars-test-${randomString()}@api.com`;
const name = `selected-calendars-test-${randomString()}`;
const username = name;
let user: User;
let apiKeyString: string;
let createdEventTypeId: number;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();

app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);

oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);

organization = await teamRepositoryFixture.create({
name: `selected-calendars-org-${randomString()}`,
slug: `selected-calendars-org-slug-${randomString()}`,
});

oAuthClient = await createOAuthClient(organization.id);

user = await userRepositoryFixture.create({
email: userEmail,
name,
username,
});

const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = `cal_test_${keyString}`;

await app.init();
});

async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["redirect-uri"],
permissions: 32,
};
const secret = "secret";

const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}

it("should create an event type with selectedCalendars", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Event with selected calendars",
slug: `event-selected-calendars-${randomString()}`,
lengthInMinutes: 30,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
selectedCalendars: [
{
integration: "google_calendar",
externalId: "primary",
},
],
};

const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201);

const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;

expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.selectedCalendars).toBeDefined();
expect(createdEventType.selectedCalendars).toHaveLength(1);
expect(createdEventType.selectedCalendars?.[0].integration).toEqual("google_calendar");
expect(createdEventType.selectedCalendars?.[0].externalId).toEqual("primary");

createdEventTypeId = createdEventType.id;
});

it("should update an event type with selectedCalendars", async () => {
const updateBody: UpdateEventTypeInput_2024_06_14 = {
selectedCalendars: [
{
integration: "google_calendar",
externalId: "primary",
},
{
integration: "google_calendar",
externalId: "[email protected]",
},
],
};

const response = await request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventTypeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(updateBody)
.expect(200);

const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;

expect(updatedEventType.selectedCalendars).toBeDefined();
expect(updatedEventType.selectedCalendars).toHaveLength(2);
});

it("should get an event type with selectedCalendars via authenticated endpoint", async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${createdEventTypeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);

const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const eventType = responseBody.data;

expect(eventType.selectedCalendars).toBeDefined();
expect(eventType.selectedCalendars).toHaveLength(2);
});

it("should NOT include selectedCalendars in public endpoint response", async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200);

const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
const eventTypes = responseBody.data;

const eventType = eventTypes.find((et) => et.id === createdEventTypeId);
expect(eventType).toBeDefined();
expect(eventType?.selectedCalendars).toBeUndefined();
});

it("should clear selectedCalendars when empty array is provided", async () => {
const updateBody: UpdateEventTypeInput_2024_06_14 = {
selectedCalendars: [],
};

const response = await request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventTypeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(updateBody)
.expect(200);

const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;

expect(
updatedEventType.selectedCalendars === undefined || updatedEventType.selectedCalendars?.length === 0
).toBe(true);
});

afterAll(async () => {
try {
await eventTypesRepositoryFixture.delete(createdEventTypeId);
} catch (e) {
console.log(e);
}
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
console.log(e);
}
await app.close();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,12 @@ export class EventTypesController_2024_06_14 {
): Promise<GetEventTypesOutput_2024_06_14> {
const eventTypes = await this.eventTypesService.getEventTypes(queryParams, authUser);
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
const eventTypesWithoutHiddenFields =
this.outputEventTypesService.getResponseEventTypesWithoutHiddenFields(eventTypesFormatted);
const eventTypesForPublic =
this.outputEventTypesService.getResponseEventTypesForPublicEndpoint(eventTypesFormatted);

return {
status: SUCCESS_STATUS,
data: eventTypesWithoutHiddenFields,
data: eventTypesForPublic,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class EventTypesRepository_2024_06_14 {

async createUserEventType(
userId: number,
body: Omit<InputEventTransformed_2024_06_14, "destinationCalendar">
body: Omit<InputEventTransformed_2024_06_14, "destinationCalendar" | "selectedCalendars" | "useEventLevelSelectedCalendars">
) {
const { calVideoSettings, ...restBody } = body;

Expand Down Expand Up @@ -158,6 +158,15 @@ export class EventTypesRepository_2024_06_14 {
destinationCalendar: true,
calVideoSettings: true,
hosts: true,
selectedCalendars: {
select: {
id: true,
eventTypeId: true,
userId: true,
integration: true,
externalId: true,
},
},
},
});
}
Expand Down Expand Up @@ -234,4 +243,43 @@ export class EventTypesRepository_2024_06_14 {
});
return !!eventType;
}

async updateUseEventLevelSelectedCalendars(eventTypeId: number, useEventLevelSelectedCalendars: boolean) {
return this.dbWrite.prisma.eventType.update({
where: { id: eventTypeId },
data: { useEventLevelSelectedCalendars },
});
}

async getByIdIncludeSelectedCalendars(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: {
users: true,
schedule: true,
destinationCalendar: true,
calVideoSettings: true,
selectedCalendars: {
select: {
id: true,
eventTypeId: true,
userId: true,
integration: true,
externalId: true,
},
},
},
});
}

async updateUseEventLevelSelectedCalendarsWithTx(
tx: Prisma.TransactionClient,
eventTypeId: number,
useEventLevelSelectedCalendars: boolean
) {
return tx.eventType.update({
where: { id: eventTypeId },
data: { useEventLevelSelectedCalendars },
});
}
}
Loading
Loading