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
This commit is contained in:
1 parent
7b8bbaaa86
commit
7ef1050946
13 files changed
+302
-49
No files matched your search
+1
-1
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
+34
-2
@@ -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`)
|
||||
|
||||
+24
-1
@@ -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`)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
},
|
||||
|
||||
@@ -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": {}
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user