From 7ef10509469b6a01228cc7012b34c90f28e062a8 Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Wed, 16 Apr 2025 17:29:47 +0200 Subject: [PATCH] feat: v2 get slots using teamSlug + eventTypeSlug + optional organizationSlug (#20554) * feat: allow teamSlug + eventTypeSlug + optional organizationSlug as input * feat: return team event slots * test: get slots using teamSlug + eventTypeSlug + optional organizationalSlug * docs: how to get team event slots --- .../organizations-event-types.controller.ts | 2 +- .../index/organizations-teams.repository.ts | 12 +++ .../services/routing-forms.service.ts | 3 +- ...am-event-type-slots.controller.e2e-spec.ts | 36 ++++++++- ...am-event-type-slots.controller.e2e-spec.ts | 25 ++++++- .../controllers/slots.controller.ts | 25 ++++++- .../services/slots-input.service.ts | 75 ++++++++++++++----- .../slots/slots-2024-09-04/slots.module.ts | 12 ++- .../modules/teams/teams/teams.repository.ts | 10 +++ apps/api/v2/swagger/documentation.json | 26 +++++-- docs/api-reference/v2/openapi.json | 26 +++++-- .../inputs/get-slots-input.pipe.ts | 52 +++++++++++-- .../inputs/get-slots.input.ts | 47 +++++++++++- 13 files changed, 302 insertions(+), 49 deletions(-) diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.controller.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.controller.ts index 0336924b00..640c09b97b 100644 --- a/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.controller.ts +++ b/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.controller.ts @@ -152,7 +152,7 @@ export class OrganizationsEventTypesController { @UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard) @Get("/teams/:teamId/event-types") - @ApiOperation({ summary: "Get a team event type" }) + @ApiOperation({ summary: "Get team event types" }) async getTeamEventTypes( @Param("teamId", ParseIntPipe) teamId: number, @Query() queryParams: GetTeamEventTypesQuery_2024_06_14 diff --git a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.repository.ts b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.repository.ts index aec6615b7d..9acf452aea 100644 --- a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.repository.ts +++ b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.repository.ts @@ -18,6 +18,18 @@ export class OrganizationsTeamsRepository { }); } + async findOrgTeamBySlug(organizationId: number, teamSlug: string) { + return this.dbRead.prisma.team.findUnique({ + where: { + slug_parentId: { + slug: teamSlug, + parentId: organizationId, + }, + isOrganization: false, + }, + }); + } + async findOrgTeams(organizationId: number) { return this.dbRead.prisma.team.findMany({ where: { diff --git a/apps/api/v2/src/modules/routing-forms/services/routing-forms.service.ts b/apps/api/v2/src/modules/routing-forms/services/routing-forms.service.ts index 22085db47a..045a559c62 100644 --- a/apps/api/v2/src/modules/routing-forms/services/routing-forms.service.ts +++ b/apps/api/v2/src/modules/routing-forms/services/routing-forms.service.ts @@ -4,7 +4,7 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { Request } from "express"; import { getRoutedUrl } from "@calcom/platform-libraries"; -import { GetAvailableSlotsInput_2024_09_04 } from "@calcom/platform-types"; +import { ById_2024_09_04_type, GetAvailableSlotsInput_2024_09_04 } from "@calcom/platform-types"; @Injectable() export class RoutingFormsService { @@ -25,6 +25,7 @@ export class RoutingFormsService { } const slots = await this.slotsService.getAvailableSlots({ + type: ById_2024_09_04_type, eventTypeId, ...slotsQuery, }); diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/org-team-event-type-slots.controller.e2e-spec.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/org-team-event-type-slots.controller.e2e-spec.ts index 2fc6f7e962..f3f66ef680 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/org-team-event-type-slots.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/org-team-event-type-slots.controller.e2e-spec.ts @@ -49,11 +49,14 @@ describe("Slots 2024-09-04 Endpoints", () => { const nonOrgUserEmailOne = `slots-2024-09-04-non-org-user-one-${randomString()}@api.com`; + const orgSlug = `slots-2024-09-04-organization-${randomString()}`; let organization: Team; + const teamSlug = `slots-2024-09-04-organization-team-${randomString()}`; let team: Team; let orgUserOne: User; let orgUserTwo: User; let collectiveEventTypeId: number; + let collectiveEventTypeSlug: string; let roundRobinEventTypeId: number; let collectiveBookingId: number; let roundRobinBookingId: number; @@ -91,7 +94,6 @@ describe("Slots 2024-09-04 Endpoints", () => { membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - const orgSlug = `slots-2024-09-04-organization-${randomString()}`; organization = await organizationsRepositoryFixture.create({ name: orgSlug, isOrganization: true, @@ -143,7 +145,8 @@ describe("Slots 2024-09-04 Endpoints", () => { }); team = await teamRepositoryFixture.create({ - name: `slots-2024-09-04-team-${randomString()}`, + name: teamSlug, + slug: teamSlug, isOrganization: false, parent: { connect: { id: organization.id } }, }); @@ -178,6 +181,7 @@ describe("Slots 2024-09-04 Endpoints", () => { }, }); collectiveEventTypeId = collectiveEventType.id; + collectiveEventTypeSlug = collectiveEventType.slug; const roundRobinEventType = await eventTypesRepositoryFixture.createTeamEventType({ schedulingType: "ROUND_ROBIN", @@ -282,6 +286,34 @@ describe("Slots 2024-09-04 Endpoints", () => { }); }); + it("should get collective team event slots in UTC using teamSlug, eventTypeSlug and organizationSlug", async () => { + return request(app.getHttpServer()) + .get( + `/v2/slots?organizationSlug=${orgSlug}&teamSlug=${teamSlug}&eventTypeSlug=${collectiveEventTypeSlug}&start=2050-09-05&end=2050-09-09` + ) + .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) + .expect(200) + .then(async (response) => { + const responseBody: GetSlotsOutput_2024_09_04 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const slots = responseBody.data; + + expect(slots).toBeDefined(); + const days = Object.keys(slots); + expect(days.length).toEqual(5); + expect(slots).toEqual(expectedSlotsUTC); + }); + }); + + it("should not get collective team event slots in UTC using teamSlug, eventTypeSlug if organizationSlug is missing", async () => { + return request(app.getHttpServer()) + .get( + `/v2/slots?teamSlug=${teamSlug}&eventTypeSlug=${collectiveEventTypeSlug}&start=2050-09-05&end=2050-09-09` + ) + .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) + .expect(404); + }); + it("should get round robin team event slots in UTC", async () => { return request(app.getHttpServer()) .get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`) diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/team-event-type-slots.controller.e2e-spec.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/team-event-type-slots.controller.e2e-spec.ts index 0c069a4f09..1b4181f8b1 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/team-event-type-slots.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/e2e/team-event-type-slots.controller.e2e-spec.ts @@ -54,10 +54,12 @@ describe("Slots 2024-09-04 Endpoints", () => { let outsider: User; let outsiderApiKeyString: string; + const teamSlug = `slots-2024-09-04-team-${randomString()}`; let team: Team; let teammateOne: User; let teammateTwo: User; let collectiveEventTypeId: number; + let collectiveEventTypeSlug: string; let collectiveEventTypeWithoutHostsId: number; let roundRobinEventTypeId: number; let collectiveBookingId: number; @@ -121,7 +123,8 @@ describe("Slots 2024-09-04 Endpoints", () => { outsiderApiKeyString = unrelatedUserKeyString; team = await teamRepositoryFixture.create({ - name: `slots-2024-09-04-team-${randomString()}`, + name: teamSlug, + slug: teamSlug, isOrganization: false, }); @@ -167,6 +170,7 @@ describe("Slots 2024-09-04 Endpoints", () => { }, }); collectiveEventTypeId = collectiveEventType.id; + collectiveEventTypeSlug = collectiveEventType.slug; const collectiveEventTypeWithoutHosts = await eventTypesRepositoryFixture.createTeamEventType({ schedulingType: "COLLECTIVE", @@ -246,6 +250,25 @@ describe("Slots 2024-09-04 Endpoints", () => { }); }); + it("should get collective team event slots in UTC using teamSlug and eventTypeSlug", async () => { + return request(app.getHttpServer()) + .get( + `/v2/slots?teamSlug=${teamSlug}&eventTypeSlug=${collectiveEventTypeSlug}&start=2050-09-05&end=2050-09-09` + ) + .set(CAL_API_VERSION_HEADER, VERSION_2024_09_04) + .expect(200) + .then(async (response) => { + const responseBody: GetSlotsOutput_2024_09_04 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const slots = responseBody.data; + + expect(slots).toBeDefined(); + const days = Object.keys(slots); + expect(days.length).toEqual(5); + expect(slots).toEqual(expectedSlotsUTC); + }); + }); + it("should get round robin team event slots in UTC", async () => { return request(app.getHttpServer()) .get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`) diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts index a658e312b0..f4b6108cdb 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts @@ -60,7 +60,7 @@ export class SlotsController_2024_09_04 { @ApiOperation({ summary: "Find out when is an event type ready to be booked.", description: ` - There are 4 ways to get available slots: + There are 4 ways to get available slots for event type of an individual user: 1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome' @@ -70,6 +70,14 @@ export class SlotsController_2024_09_04 { 4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs. + And 3 ways to get available slots for team event type: + + 1. By team event type id. Example '/v2/slots?teamEventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome' + + 2. By team event type slug + team slug. Example '/v2/slots?eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06' + + 3. By team event type slug + team slug + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06' + All of them require "start" and "end" query parameters which define the time range for which available slots should be checked. Optional parameters are: - timeZone: Time zone in which the available slots should be returned. Defaults to UTC. @@ -117,19 +125,28 @@ export class SlotsController_2024_09_04 { name: "eventTypeSlug", required: false, description: - "The slug of the event type for which available slots should be checked. If slug is provided then username must be provided too and if relevant organizationSlug too.", + "The slug of the event type for which available slots should be checked. If slug is provided then username or teamSlug must be provided too and if relevant organizationSlug too.", example: "event-type-slug", }) @ApiQuery({ name: "username", required: false, - description: "The username of the user to get event types for.", + description: + "The username of the user who owns event type with eventTypeSlug - used when slots are checked for individual user event type.", example: "bob", }) + @ApiQuery({ + name: "teamSlug", + required: false, + description: + "The slug of the team who owns event type with eventTypeSlug - used when slots are checked for team event type.", + example: "team-slug", + }) @ApiQuery({ name: "organizationSlug", required: false, - description: "The slug of the organization to which user with username belongs.", + description: + "The slug of the organization to which user with username belongs or team with teamSlug belongs.", example: "org-slug", }) @ApiQuery({ diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts index b54dc4e172..3f3a0be0d0 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots-input.service.ts @@ -1,12 +1,23 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; +import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; +import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; import { UsersRepository } from "@/modules/users/users.repository"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { DateTime } from "luxon"; import { dynamicEvent } from "@calcom/platform-libraries"; -import { GetSlotsInput_2024_09_04 } from "@calcom/platform-types"; +import { + ById_2024_09_04, + ByUsernameAndEventTypeSlug_2024_09_04, + ByTeamSlugAndEventTypeSlug_2024_09_04, + GetSlotsInput_2024_09_04, + ById_2024_09_04_type, + ByUsernameAndEventTypeSlug_2024_09_04_type, + ByTeamSlugAndEventTypeSlug_2024_09_04_type, +} from "@calcom/platform-types"; @Injectable() export class SlotsInputService_2024_09_04 { @@ -14,7 +25,10 @@ export class SlotsInputService_2024_09_04 { private readonly eventTypeRepository: EventTypesRepository_2024_06_14, private readonly usersRepository: UsersRepository, private readonly organizationsUsersRepository: OrganizationsUsersRepository, - private readonly organizationsRepository: OrganizationsRepository + private readonly organizationsTeamsRepository: OrganizationsTeamsRepository, + private readonly organizationsRepository: OrganizationsRepository, + private readonly teamsRepository: TeamsRepository, + private readonly teamsEventTypesRepository: TeamsEventTypesRepository ) {} async transformGetSlotsQuery(query: GetSlotsInput_2024_09_04) { @@ -47,11 +61,11 @@ export class SlotsInputService_2024_09_04 { } private async getEventType(input: GetSlotsInput_2024_09_04) { - if ("eventTypeId" in input) { + if (input.type === ById_2024_09_04_type) { return this.eventTypeRepository.getEventTypeById(input.eventTypeId); } - if ("eventTypeSlug" in input) { + if (input.type === ByUsernameAndEventTypeSlug_2024_09_04_type) { const user = await this.getEventTypeUser(input); if (!user) { throw new NotFoundException(`User with username ${input.username} not found`); @@ -59,27 +73,48 @@ export class SlotsInputService_2024_09_04 { return this.eventTypeRepository.getUserEventTypeBySlug(user.id, input.eventTypeSlug); } + if (input.type === ByTeamSlugAndEventTypeSlug_2024_09_04_type) { + const team = await this.getEventTypeTeam(input); + if (!team) { + throw new NotFoundException(`Team with slug ${input.teamSlug} not found`); + } + return this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug(team.id, input.eventTypeSlug); + } + return input.duration ? { ...dynamicEvent, length: input.duration } : dynamicEvent; } - private async getEventTypeUser(input: GetSlotsInput_2024_09_04) { - if ("eventTypeSlug" in input) { - if ("organizationSlug" in input && input.organizationSlug) { - const organization = await this.organizationsRepository.findOrgBySlug(input.organizationSlug); - if (!organization) { - throw new NotFoundException( - `slots-input.service.ts: Organization with slug ${input.organizationSlug} not found` - ); - } - - return await this.organizationsUsersRepository.getOrganizationUserByUsername( - organization.id, - input.username - ); - } - + private async getEventTypeUser(input: ByUsernameAndEventTypeSlug_2024_09_04) { + if (!input.organizationSlug) { return await this.usersRepository.findByUsername(input.username); } + + const organization = await this.organizationsRepository.findOrgBySlug(input.organizationSlug); + if (!organization) { + throw new NotFoundException( + `slots-input.service.ts: Organization with slug ${input.organizationSlug} not found` + ); + } + + return await this.organizationsUsersRepository.getOrganizationUserByUsername( + organization.id, + input.username + ); + } + + private async getEventTypeTeam(input: ByTeamSlugAndEventTypeSlug_2024_09_04) { + if (!input.organizationSlug) { + return await this.teamsRepository.findTeamBySlug(input.teamSlug); + } + + const organization = await this.organizationsRepository.findOrgBySlug(input.organizationSlug); + if (!organization) { + throw new NotFoundException( + `slots-input.service.ts: Organization with slug ${input.organizationSlug} not found` + ); + } + + return await this.organizationsTeamsRepository.findOrgTeamBySlug(organization.id, input.teamSlug); } private adjustStartTime(startTime: string) { diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/slots.module.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/slots.module.ts index 9d022a8014..214bd44e67 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/slots.module.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/slots.module.ts @@ -1,6 +1,7 @@ import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; +import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { SlotsController_2024_09_04 } from "@/modules/slots/slots-2024-09-04/controllers/slots.controller"; @@ -9,12 +10,20 @@ import { SlotsOutputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/ import { SlotsService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots.service"; import { SlotsRepository_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.repository"; import { StripeModule } from "@/modules/stripe/stripe.module"; +import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; import { TeamsModule } from "@/modules/teams/teams/teams.module"; import { UsersRepository } from "@/modules/users/users.repository"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, EventTypesModule_2024_06_14, StripeModule, TeamsModule, MembershipsModule], + imports: [ + PrismaModule, + EventTypesModule_2024_06_14, + StripeModule, + TeamsModule, + MembershipsModule, + TeamsEventTypesModule, + ], providers: [ SlotsRepository_2024_09_04, SlotsService_2024_09_04, @@ -23,6 +32,7 @@ import { Module } from "@nestjs/common"; SlotsOutputService_2024_09_04, OrganizationsUsersRepository, OrganizationsRepository, + OrganizationsTeamsRepository, ], controllers: [SlotsController_2024_09_04], exports: [SlotsService_2024_09_04], diff --git a/apps/api/v2/src/modules/teams/teams/teams.repository.ts b/apps/api/v2/src/modules/teams/teams/teams.repository.ts index f92bbec334..8d25133132 100644 --- a/apps/api/v2/src/modules/teams/teams/teams.repository.ts +++ b/apps/api/v2/src/modules/teams/teams/teams.repository.ts @@ -110,4 +110,14 @@ export class TeamsRepository { where: { id: teamId }, }); } + + async findTeamBySlug(slug: string) { + return this.dbRead.prisma.team.findFirst({ + where: { + slug, + isOrganization: false, + parentId: null, + }, + }); + } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 22bc436197..fe07e035a8 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -3470,7 +3470,7 @@ }, "get": { "operationId": "OrganizationsEventTypesController_getTeamEventTypes", - "summary": "Get a team event type", + "summary": "Get team event types", "parameters": [ { "name": "Authorization", @@ -7377,6 +7377,14 @@ "type": "string" } }, + { + "name": "isDryRun", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "redir", "required": false, @@ -9511,7 +9519,7 @@ "get": { "operationId": "SlotsController_2024_09_04_getAvailableSlots", "summary": "Find out when is an event type ready to be booked.", - "description": "\n There are 4 ways to get available slots:\n \n 1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/v2/slots?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By event type slug + username + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs.\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ", + "description": "\n There are 4 ways to get available slots for event type of an individual user:\n \n 1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/v2/slots?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By event type slug + username + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs.\n\n And 3 ways to get available slots for team event type:\n\n 1. By team event type id. Example '/v2/slots?teamEventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By team event type slug + team slug. Example '/v2/slots?eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n 3. By team event type slug + team slug + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ", "parameters": [ { "name": "cal-api-version", @@ -9543,15 +9551,23 @@ "name": "organizationSlug", "required": false, "in": "query", - "description": "The slug of the organization to which user with username belongs.", + "description": "The slug of the organization to which user with username belongs or team with teamSlug belongs.", "example": "org-slug", "schema": {} }, + { + "name": "teamSlug", + "required": false, + "in": "query", + "description": "The slug of the team who owns event type with eventTypeSlug - used when slots are checked for team event type.", + "example": "team-slug", + "schema": {} + }, { "name": "username", "required": false, "in": "query", - "description": "The username of the user to get event types for.", + "description": "The username of the user who owns event type with eventTypeSlug - used when slots are checked for individual user event type.", "example": "bob", "schema": {} }, @@ -9559,7 +9575,7 @@ "name": "eventTypeSlug", "required": false, "in": "query", - "description": "The slug of the event type for which available slots should be checked. If slug is provided then username must be provided too and if relevant organizationSlug too.", + "description": "The slug of the event type for which available slots should be checked. If slug is provided then username or teamSlug must be provided too and if relevant organizationSlug too.", "example": "event-type-slug", "schema": {} }, diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index cab1189550..72245aa09d 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -3304,7 +3304,7 @@ }, "get": { "operationId": "OrganizationsEventTypesController_getTeamEventTypes", - "summary": "Get a team event type", + "summary": "Get team event types", "parameters": [ { "name": "Authorization", @@ -7046,6 +7046,14 @@ "type": "string" } }, + { + "name": "isDryRun", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "redir", "required": false, @@ -9057,7 +9065,7 @@ "get": { "operationId": "SlotsController_2024_09_04_getAvailableSlots", "summary": "Find out when is an event type ready to be booked.", - "description": "\n There are 4 ways to get available slots:\n \n 1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/v2/slots?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By event type slug + username + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs.\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ", + "description": "\n There are 4 ways to get available slots for event type of an individual user:\n \n 1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/v2/slots?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By event type slug + username + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs.\n\n And 3 ways to get available slots for team event type:\n\n 1. By team event type id. Example '/v2/slots?teamEventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By team event type slug + team slug. Example '/v2/slots?eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n 3. By team event type slug + team slug + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ", "parameters": [ { "name": "cal-api-version", @@ -9089,15 +9097,23 @@ "name": "organizationSlug", "required": false, "in": "query", - "description": "The slug of the organization to which user with username belongs.", + "description": "The slug of the organization to which user with username belongs or team with teamSlug belongs.", "example": "org-slug", "schema": {} }, + { + "name": "teamSlug", + "required": false, + "in": "query", + "description": "The slug of the team who owns event type with eventTypeSlug - used when slots are checked for team event type.", + "example": "team-slug", + "schema": {} + }, { "name": "username", "required": false, "in": "query", - "description": "The username of the user to get event types for.", + "description": "The username of the user who owns event type with eventTypeSlug - used when slots are checked for individual user event type.", "example": "bob", "schema": {} }, @@ -9105,7 +9121,7 @@ "name": "eventTypeSlug", "required": false, "in": "query", - "description": "The slug of the event type for which available slots should be checked. If slug is provided then username must be provided too and if relevant organizationSlug too.", + "description": "The slug of the event type for which available slots should be checked. If slug is provided then username or teamSlug must be provided too and if relevant organizationSlug too.", "example": "event-type-slug", "schema": {} }, diff --git a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots-input.pipe.ts b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots-input.pipe.ts index 390deaca7a..6b5175e391 100644 --- a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots-input.pipe.ts +++ b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots-input.pipe.ts @@ -4,10 +4,18 @@ import { plainToClass } from "class-transformer"; import type { ValidationError } from "class-validator"; import { validateSync } from "class-validator"; -import { ById_2024_09_04, BySlug_2024_09_04 } from "./get-slots.input"; +import { + ById_2024_09_04, + ByTeamSlugAndEventTypeSlug_2024_09_04, + ByUsernameAndEventTypeSlug_2024_09_04, +} from "./get-slots.input"; import { ByUsernames_2024_09_04 } from "./get-slots.input"; -export type GetSlotsInput_2024_09_04 = ById_2024_09_04 | BySlug_2024_09_04 | ByUsernames_2024_09_04; +export type GetSlotsInput_2024_09_04 = + | ById_2024_09_04 + | ByUsernameAndEventTypeSlug_2024_09_04 + | ByTeamSlugAndEventTypeSlug_2024_09_04 + | ByUsernames_2024_09_04; @Injectable() export class GetSlotsInputPipe implements PipeTransform { @@ -29,8 +37,12 @@ export class GetSlotsInputPipe implements PipeTransform { return this.validateById(value); } - if (this.isBySlug(value)) { - return this.validateBySlug(value); + if (this.isByUsernameAndEventTypeSlug(value)) { + return this.validateByUsernameAndEventTypeSlug(value); + } + + if (this.isByTeamSlugAndEventTypeSlug(value)) { + return this.validateByTeamSlugAndEventTypeSlug(value); } return this.validateByUsernames(value); @@ -52,8 +64,24 @@ export class GetSlotsInputPipe implements PipeTransform { return object; } - validateBySlug(value: BySlug_2024_09_04) { - const object = plainToClass(BySlug_2024_09_04, value); + validateByUsernameAndEventTypeSlug(value: ByUsernameAndEventTypeSlug_2024_09_04) { + const object = plainToClass(ByUsernameAndEventTypeSlug_2024_09_04, value); + + const errors = validateSync(object, { + whitelist: true, + forbidNonWhitelisted: true, + skipMissingProperties: false, + }); + + if (errors.length > 0) { + throw new BadRequestException(this.formatErrors(errors)); + } + + return object; + } + + validateByTeamSlugAndEventTypeSlug(value: ByTeamSlugAndEventTypeSlug_2024_09_04) { + const object = plainToClass(ByTeamSlugAndEventTypeSlug_2024_09_04, value); const errors = validateSync(object, { whitelist: true, @@ -99,7 +127,15 @@ export class GetSlotsInputPipe implements PipeTransform { return value.hasOwnProperty("eventTypeId"); } - private isBySlug(value: GetSlotsInput_2024_09_04): value is BySlug_2024_09_04 { - return value.hasOwnProperty("eventTypeSlug"); + private isByUsernameAndEventTypeSlug( + value: GetSlotsInput_2024_09_04 + ): value is ByUsernameAndEventTypeSlug_2024_09_04 { + return value.hasOwnProperty("username") && value.hasOwnProperty("eventTypeSlug"); + } + + private isByTeamSlugAndEventTypeSlug( + value: GetSlotsInput_2024_09_04 + ): value is ByTeamSlugAndEventTypeSlug_2024_09_04 { + return value.hasOwnProperty("teamSlug") && value.hasOwnProperty("eventTypeSlug"); } } diff --git a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts index 1edd3fa6ef..8efc8e2493 100644 --- a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts +++ b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts @@ -80,7 +80,11 @@ export class GetAvailableSlotsInput_2024_09_04 { format?: SlotFormat; } +export const ById_2024_09_04_type = "byEventTypeId"; export class ById_2024_09_04 extends GetAvailableSlotsInput_2024_09_04 { + @IsString() + type: typeof ById_2024_09_04_type = ById_2024_09_04_type; + @Transform(({ value }: { value: string }) => value && parseInt(value)) @IsNumber() @ApiProperty({ @@ -91,7 +95,11 @@ export class ById_2024_09_04 extends GetAvailableSlotsInput_2024_09_04 { eventTypeId!: number; } -export class BySlug_2024_09_04 extends GetAvailableSlotsInput_2024_09_04 { +export const ByUsernameAndEventTypeSlug_2024_09_04_type = "byUsernameAndEventTypeSlug"; +export class ByUsernameAndEventTypeSlug_2024_09_04 extends GetAvailableSlotsInput_2024_09_04 { + @IsString() + type: typeof ByUsernameAndEventTypeSlug_2024_09_04_type = ByUsernameAndEventTypeSlug_2024_09_04_type; + @IsString() @ApiProperty({ type: String, @@ -120,7 +128,44 @@ export class BySlug_2024_09_04 extends GetAvailableSlotsInput_2024_09_04 { organizationSlug?: string; } +export const ByTeamSlugAndEventTypeSlug_2024_09_04_type = "byTeamSlugAndEventTypeSlug"; +export class ByTeamSlugAndEventTypeSlug_2024_09_04 extends GetAvailableSlotsInput_2024_09_04 { + @IsString() + type: typeof ByTeamSlugAndEventTypeSlug_2024_09_04_type = ByTeamSlugAndEventTypeSlug_2024_09_04_type; + + @IsString() + @ApiProperty({ + type: String, + description: "The slug of the event type for which available slots should be checked.", + example: "event-type-slug", + }) + eventTypeSlug!: string; + + @IsString() + @ApiProperty({ + type: String, + description: + "When searching by eventTypeSlug a teamSlug must be provided too aka team who owns the the event type.", + example: "bob", + }) + teamSlug!: string; + + @IsString() + @IsOptional() + @ApiProperty({ + type: String, + description: + "Organzation slug in which the slots of event type belonging to the specified teamSlug should be checked.", + example: "org-slug", + }) + organizationSlug?: string; +} + +export const ByUsernames_2024_09_04_type = "byUsernames"; export class ByUsernames_2024_09_04 extends GetAvailableSlotsInput_2024_09_04 { + @IsString() + type: typeof ByUsernames_2024_09_04_type = ByUsernames_2024_09_04_type; + @Transform(({ value }) => { if (typeof value === "string") { return value.split(",").map((username: string) => username.trim());