diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts index 44e9ddef629128..c1650dcb99780e 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts @@ -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 = 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: "secondary@gmail.com", + }, + ], + }; + + 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 = 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 = 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 = 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 = 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(); + }); + }); }); diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts index 2a1f83cddc1f65..ea83f48e575869 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts @@ -170,12 +170,12 @@ export class EventTypesController_2024_06_14 { ): Promise { 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, }; } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts index 54c9bb006b1141..934a43427d43d0 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts @@ -14,7 +14,7 @@ export class EventTypesRepository_2024_06_14 { async createUserEventType( userId: number, - body: Omit + body: Omit ) { const { calVideoSettings, ...restBody } = body; @@ -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, + }, + }, }, }); } @@ -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 }, + }); + } } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts index 550e12f89cfc7f..08e258c3a56a4e 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts @@ -22,7 +22,11 @@ import { getEventTypesPublic, EventTypesPublic, } from "@calcom/platform-libraries/event-types"; -import type { GetEventTypesQuery_2024_06_14, SortOrderType } from "@calcom/platform-types"; +import type { + GetEventTypesQuery_2024_06_14, + SortOrderType, + SelectedCalendar_2024_06_14, +} from "@calcom/platform-types"; import type { EventType } from "@calcom/prisma/client"; @Injectable() @@ -45,7 +49,7 @@ export class EventTypesService_2024_06_14 { await this.checkCanCreateEventType(user.id, body); const eventTypeUser = await this.getUserToCreateEvent(user); - const { destinationCalendar: _destinationCalendar, ...rest } = body; + const { destinationCalendar: _destinationCalendar, selectedCalendars, ...rest } = body; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -62,7 +66,8 @@ export class EventTypesService_2024_06_14 { await updateEventType({ input: { id: eventTypeCreated.id, - ...body, + ...rest, + destinationCalendar: body.destinationCalendar, }, ctx: { user: eventTypeUser, @@ -72,7 +77,9 @@ export class EventTypesService_2024_06_14 { }, }); - const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeCreated.id); + await this.updateEventTypeSelectedCalendars(eventTypeCreated.id, user.id, selectedCalendars); + + const eventType = await this.eventTypesRepository.getByIdIncludeSelectedCalendars(eventTypeCreated.id); if (!eventType) { throw new NotFoundException(`Event type with id ${eventTypeCreated.id} not found`); @@ -312,8 +319,10 @@ export class EventTypesService_2024_06_14 { await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId); const eventTypeUser = await this.getUserToUpdateEvent(user); + const { selectedCalendars, ...rest } = body; + await updateEventType({ - input: { id: eventTypeId, ...body }, + input: { id: eventTypeId, ...rest }, ctx: { user: eventTypeUser, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -322,7 +331,9 @@ export class EventTypesService_2024_06_14 { }, }); - const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId); + await this.updateEventTypeSelectedCalendars(eventTypeId, user.id, selectedCalendars); + + const eventType = await this.eventTypesRepository.getByIdIncludeSelectedCalendars(eventTypeId); if (!eventType) { throw new NotFoundException(`Event type with id ${eventTypeId} not found`); @@ -385,4 +396,73 @@ export class EventTypesService_2024_06_14 { throw new NotFoundException(`User with ID=${userId} does not own schedule with ID=${scheduleId}`); } } + + async updateEventTypeSelectedCalendars( + eventTypeId: number, + userId: number, + selectedCalendars: SelectedCalendar_2024_06_14[] | undefined + ) { + if (selectedCalendars === undefined) { + return; + } + + const existingEventType = await this.eventTypesRepository.getEventTypeById(eventTypeId); + if (!existingEventType) { + throw new NotFoundException(`Event type with ID=${eventTypeId} not found`); + } + this.checkUserOwnsEventType(userId, existingEventType); + + const shouldUseEventLevelCalendars = selectedCalendars.length > 0; + const hasSelectedCalendars = selectedCalendars.length > 0; + + const userSelectedCalendars = hasSelectedCalendars + ? await this.selectedCalendarsRepository.getUserSelectedCalendars(userId) + : []; + + const userCalendarMap = new Map( + userSelectedCalendars.map((uc) => [`${uc.integration}:${uc.externalId}`, uc]) + ); + + const calendarsToCreate = hasSelectedCalendars + ? selectedCalendars.map((calendar) => { + const matchingUserCalendar = userCalendarMap.get(`${calendar.integration}:${calendar.externalId}`); + return { + eventTypeId, + userId, + integration: calendar.integration, + externalId: calendar.externalId, + credentialId: matchingUserCalendar?.credentialId ?? null, + }; + }) + : []; + + await this.dbWrite.prisma.$transaction(async (tx) => { + await this.eventTypesRepository.updateUseEventLevelSelectedCalendarsWithTx( + tx, + eventTypeId, + shouldUseEventLevelCalendars + ); + + await this.selectedCalendarsRepository.deleteByEventTypeIdWithTx(tx, eventTypeId); + + if (calendarsToCreate.length > 0) { + await this.selectedCalendarsRepository.createManyForEventTypeWithTx(tx, calendarsToCreate); + } + }); + } + + async getEventTypeWithSelectedCalendars(eventTypeId: number, userId: number) { + const eventType = await this.eventTypesRepository.getByIdIncludeSelectedCalendars(eventTypeId); + + if (!eventType) { + return null; + } + + this.checkUserOwnsEventType(userId, eventType); + + return { + ownerId: userId, + ...eventType, + }; + } } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 2b16fa0d31b904..634a4e39129962 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -134,6 +134,7 @@ export class InputEventTypesService_2024_06_14 { disableRescheduling, disableCancelling, calVideoSettings, + selectedCalendars, ...rest } = inputEventType; const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy); @@ -186,6 +187,7 @@ export class InputEventTypesService_2024_06_14 { ...disableReschedulingTransformed, ...disableCancellingTransformed, ...calVideoSettingsTransformed, + selectedCalendars, }; return eventType; @@ -228,6 +230,7 @@ export class InputEventTypesService_2024_06_14 { disableRescheduling, disableCancelling, calVideoSettings, + selectedCalendars, ...rest } = inputEventType; const eventTypeDb = await this.eventTypesRepository.getEventTypeWithMetaData(eventTypeId); @@ -296,6 +299,7 @@ export class InputEventTypesService_2024_06_14 { ...disableReschedulingTransformed, ...disableCancellingTransformed, ...calVideoSettingsTransformed, + selectedCalendars, }; return eventType; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts index 346a4e85ca60d2..5f85ee6ed79538 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts @@ -21,6 +21,7 @@ import type { EventType, Prisma, Schedule, + SelectedCalendar, Team, } from "@calcom/prisma/client"; import { Injectable } from "@nestjs/common"; @@ -43,6 +44,8 @@ import { } from "@/ee/event-types/event-types_2024_06_14/transformers"; import { ProfileMinimal, UsersService } from "@/modules/users/services/users.service"; +type SelectedCalendarFields = Pick; + type EventTypeUser = { id: number; name: string | null; @@ -64,6 +67,7 @@ type EventTypeRelations = { schedule: Schedule | null; destinationCalendar?: DestinationCalendar | null; calVideoSettings?: CalVideoSettings | null; + selectedCalendars?: SelectedCalendarFields[]; }; export type DatabaseEventType = EventType & EventTypeRelations; @@ -127,6 +131,7 @@ type Input = Pick< | "allowReschedulingPastBookings" | "allowReschedulingCancelledBookings" | "showOptimizedSlots" + | "selectedCalendars" >; @Injectable() @@ -224,6 +229,7 @@ export class OutputEventTypesService_2024_06_14 { calVideoSettings, canSendCalVideoTranscriptionEmails ); + const selectedCalendarsOutput = this.transformSelectedCalendars(databaseEventType.selectedCalendars); return { id, @@ -277,6 +283,7 @@ export class OutputEventTypesService_2024_06_14 { allowReschedulingPastBookings, allowReschedulingCancelledBookings, showOptimizedSlots, + selectedCalendars: selectedCalendarsOutput, }; } @@ -475,7 +482,9 @@ export class OutputEventTypesService_2024_06_14 { } getResponseEventTypeWithoutHiddenFields(eventType: EventTypeOutput_2024_06_14): EventTypeOutput_2024_06_14 { - if (!Array.isArray(eventType?.bookingFields) || eventType.bookingFields.length === 0) return eventType; + if (!Array.isArray(eventType?.bookingFields) || eventType.bookingFields.length === 0) { + return eventType; + } const visibleBookingFields: OutputBookingField_2024_06_14[] = []; for (const bookingField of eventType.bookingFields) { @@ -490,6 +499,18 @@ export class OutputEventTypesService_2024_06_14 { }; } + getResponseEventTypesForPublicEndpoint( + eventTypes: EventTypeOutput_2024_06_14[] + ): EventTypeOutput_2024_06_14[] { + return eventTypes.map((eventType) => this.getResponseEventTypeForPublicEndpoint(eventType)); + } + + getResponseEventTypeForPublicEndpoint(eventType: EventTypeOutput_2024_06_14): EventTypeOutput_2024_06_14 { + const withoutHiddenFields = this.getResponseEventTypeWithoutHiddenFields(eventType); + const { selectedCalendars: _selectedCalendars, ...eventTypeForPublic } = withoutHiddenFields; + return eventTypeForPublic; + } + transformDisableRescheduling( disableRescheduling: boolean | null | undefined, minimumRescheduleNotice: number | null | undefined @@ -525,4 +546,15 @@ export class OutputEventTypesService_2024_06_14 { sendTranscriptionEmails: canSendCalVideoTranscriptionEmails ?? true, }; } + + transformSelectedCalendars(selectedCalendars: SelectedCalendarFields[] | undefined) { + if (!selectedCalendars || selectedCalendars.length === 0) { + return undefined; + } + + return selectedCalendars.map((calendar) => ({ + integration: calendar.integration, + externalId: calendar.externalId, + })); + } } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts index e8847662777aa2..28f39b6a802466 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts @@ -12,6 +12,7 @@ import type { z } from "zod"; import type { CreateEventTypeInput_2024_06_14, ConfirmationPolicyTransformedSchema, + SelectedCalendar_2024_06_14, } from "@calcom/platform-types"; export type InputEventTransformed_2024_06_14 = Omit< diff --git a/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts b/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts index dc48db092a4dd8..3fa8bc1692580e 100644 --- a/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts +++ b/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts @@ -14,6 +14,7 @@ import type { Host, DestinationCalendar, CalVideoSettings, + SelectedCalendar, } from "@calcom/prisma/client"; type EventTypeRelations = { @@ -26,6 +27,7 @@ type EventTypeRelations = { "bannerUrl" | "name" | "logoUrl" | "slug" | "weekStart" | "brandColor" | "darkBrandColor" | "theme" > | null; calVideoSettings?: CalVideoSettings | null; + selectedCalendars?: SelectedCalendar[]; }; export type DatabaseTeamEventType = EventType & EventTypeRelations; @@ -97,6 +99,8 @@ type Input = Pick< | "allowReschedulingCancelledBookings" | "showOptimizedSlots" | "rrHostSubsetEnabled" + | "selectedCalendars" + | "useEventLevelSelectedCalendars" >; @Injectable() diff --git a/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts b/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts index 640026144fc103..e887f552e100d7 100644 --- a/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts +++ b/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts @@ -1,6 +1,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@calcom/prisma/client"; // It ensures that we work on userLevel calendars only const ensureUserLevelWhere = { @@ -129,4 +130,93 @@ export class SelectedCalendarsRepository { }, }); } + + getByEventTypeId(eventTypeId: number) { + return this.dbRead.prisma.selectedCalendar.findMany({ + where: { + eventTypeId, + }, + select: { + id: true, + eventTypeId: true, + userId: true, + integration: true, + externalId: true, + }, + }); + } + + async createForEventType( + eventTypeId: number, + userId: number, + integration: string, + externalId: string, + credentialId: number | null + ) { + return this.dbWrite.prisma.selectedCalendar.create({ + data: { + eventTypeId, + userId, + integration, + externalId, + credentialId, + }, + select: { + id: true, + eventTypeId: true, + userId: true, + integration: true, + externalId: true, + }, + }); + } + + async deleteByEventTypeId(eventTypeId: number) { + return this.dbWrite.prisma.selectedCalendar.deleteMany({ + where: { + eventTypeId, + }, + }); + } + + async createManyForEventType( + calendars: { + eventTypeId: number; + userId: number; + integration: string; + externalId: string; + credentialId: number | null; + }[] + ) { + if (calendars.length === 0) { + return { count: 0 }; + } + return this.dbWrite.prisma.selectedCalendar.createMany({ + data: calendars, + }); + } + + async deleteByEventTypeIdWithTx(tx: Prisma.TransactionClient, eventTypeId: number) { + return tx.selectedCalendar.deleteMany({ + where: { eventTypeId }, + }); + } + + async createManyForEventTypeWithTx( + tx: Prisma.TransactionClient, + calendars: { + eventTypeId: number; + userId: number; + integration: string; + externalId: string; + credentialId: number | null; + }[] + ) { + if (calendars.length === 0) { + return { count: 0 }; + } + return tx.selectedCalendar.createMany({ + data: calendars, + }); + } } diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts index 0f39a7d8c896a9..8240f11b6115fd 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts @@ -86,6 +86,7 @@ import { } from "./locations.input"; import { Recurrence_2024_06_14 } from "./recurrence.input"; import { Seats_2024_06_14 } from "./seats.input"; +import { SelectedCalendar_2024_06_14 } from "./selected-calendars.input"; import { CantHaveRecurrenceAndBookerActiveBookingsLimit } from "./validators/CantHaveRecurrenceAndBookerActiveBookingsLimit"; export const CREATE_EVENT_LENGTH_EXAMPLE = 60; @@ -585,6 +586,7 @@ export class BaseCreateEventTypeInput { }) showOptimizedSlots?: boolean; } +@ApiExtraModels(SelectedCalendar_2024_06_14) export class CreateEventTypeInput_2024_06_14 extends BaseCreateEventTypeInput { @IsOptional() @ValidateLocations_2024_06_14() @@ -604,6 +606,17 @@ export class CreateEventTypeInput_2024_06_14 extends BaseCreateEventTypeInput { }) @Type(() => Object) locations?: InputLocation_2024_06_14[]; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SelectedCalendar_2024_06_14) + @DocsPropertyOptional({ + description: + "An array of calendars to be used for conflict checking for this event type. When provided with at least one calendar, the event type will use these calendars instead of the user's default selected calendars. To switch back to user-level calendars, pass an empty array. Refer to the /api/v2/calendars endpoint to retrieve the integration type and external IDs of your connected calendars.", + type: [SelectedCalendar_2024_06_14], + }) + selectedCalendars?: SelectedCalendar_2024_06_14[]; } export enum HostPriority { diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/index.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/index.ts index 79a95a1869cfa1..9ad47f096babab 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/index.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/index.ts @@ -16,3 +16,4 @@ export * from "./destination-calendar.input"; export * from "./disabled.input"; export * from "./disable-rescheduling.input"; export * from "./disable-cancelling.input"; +export * from "./selected-calendars.input"; diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/selected-calendars.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/selected-calendars.input.ts new file mode 100644 index 00000000000000..50bb99f8eb59e5 --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/selected-calendars.input.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + +export class SelectedCalendar_2024_06_14 { + @ApiProperty({ + description: + "The integration type of the selected calendar for conflict checking. Refer to the /api/v2/calendars endpoint to retrieve the integration type of your connected calendars.", + example: "google_calendar", + }) + @IsString() + integration!: string; + + @ApiProperty({ + description: + "The external ID of the selected calendar for conflict checking. Refer to the /api/v2/calendars endpoint to retrieve the external IDs of your connected calendars.", + example: "primary", + }) + @IsString() + externalId!: string; +} diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts index 6be3ed826611e1..d4c4cf47048989 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts @@ -91,6 +91,7 @@ import { } from "./locations.input"; import { Recurrence_2024_06_14 } from "./recurrence.input"; import { Seats_2024_06_14 } from "./seats.input"; +import { SelectedCalendar_2024_06_14 } from "./selected-calendars.input"; import { CantHaveRecurrenceAndBookerActiveBookingsLimit } from "./validators/CantHaveRecurrenceAndBookerActiveBookingsLimit"; @ApiExtraModels( @@ -524,6 +525,7 @@ class BaseUpdateEventTypeInput { }) showOptimizedSlots?: boolean; } +@ApiExtraModels(SelectedCalendar_2024_06_14) export class UpdateEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInput { @IsOptional() @ValidateLocations_2024_06_14() @@ -543,6 +545,17 @@ export class UpdateEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInput { }) @Type(() => Object) locations?: InputLocation_2024_06_14[]; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SelectedCalendar_2024_06_14) + @DocsPropertyOptional({ + description: + "An array of calendars to be used for conflict checking for this event type. When provided with at least one calendar, the event type will use these calendars instead of the user's default selected calendars. To switch back to user-level calendars, pass an empty array. Refer to the /api/v2/calendars endpoint to retrieve the integration type and external IDs of your connected calendars.", + type: [SelectedCalendar_2024_06_14], + }) + selectedCalendars?: SelectedCalendar_2024_06_14[]; } export class UpdateTeamEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInput { diff --git a/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts b/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts index e8748beed4e77b..08144f01734e02 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts @@ -61,6 +61,7 @@ import { import { BookerActiveBookingsLimitOutput_2024_06_14 } from "./booker-active-bookings-limit.output"; import { DisableCancellingOutput_2024_06_14 } from "./disable-cancelling.output"; import { DisableReschedulingOutput_2024_06_14 } from "./disable-rescheduling.output"; +import { SelectedCalendarOutput_2024_06_14 } from "./selected-calendars.output"; import type { OutputBookingField_2024_06_14 } from "./booking-fields.output"; import { ValidateOutputBookingFields_2024_06_14 } from "./booking-fields.output"; import type { OutputLocation_2024_06_14 } from "./locations.output"; @@ -537,6 +538,7 @@ export class TeamEventTypeResponseHost extends TeamEventTypeHostInput { avatarUrl?: string | null; } +@ApiExtraModels(SelectedCalendarOutput_2024_06_14) export class EventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_14 { @IsInt() @DocsProperty({ example: 10 }) @@ -554,6 +556,17 @@ export class EventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_14 { format: "uri", }) bookingUrl!: string; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SelectedCalendarOutput_2024_06_14) + @ApiPropertyOptional({ + description: + "An array of calendars used for conflict checking for this event type. When non-empty, the event type uses these calendars instead of the user's default selected calendars.", + type: [SelectedCalendarOutput_2024_06_14], + }) + selectedCalendars?: SelectedCalendarOutput_2024_06_14[]; } export class TeamEventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_14 { diff --git a/packages/platform/types/event-types/event-types_2024_06_14/outputs/index.ts b/packages/platform/types/event-types/event-types_2024_06_14/outputs/index.ts index 8a1480b7197f24..d42d301e1e22fa 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/outputs/index.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/outputs/index.ts @@ -3,3 +3,4 @@ export * from "./booking-fields.output"; export * from "./locations.output"; export * from "./disable-rescheduling.output"; export * from "./disable-cancelling.output"; +export * from "./selected-calendars.output"; diff --git a/packages/platform/types/event-types/event-types_2024_06_14/outputs/selected-calendars.output.ts b/packages/platform/types/event-types/event-types_2024_06_14/outputs/selected-calendars.output.ts new file mode 100644 index 00000000000000..f701421aa3fd3c --- /dev/null +++ b/packages/platform/types/event-types/event-types_2024_06_14/outputs/selected-calendars.output.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + +export class SelectedCalendarOutput_2024_06_14 { + @ApiProperty({ + description: "The integration type of the selected calendar.", + example: "google_calendar", + }) + @IsString() + integration!: string; + + @ApiProperty({ + description: "The external ID of the selected calendar.", + example: "primary", + }) + @IsString() + externalId!: string; +}