From 16a7cf6d0483509f711bcab6996cd477fa89e6bf Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:02:19 +0200 Subject: [PATCH] fix: fetching event-types of org users apiv2 (#18004) --- .../event-types.controller.e2e-spec.ts | 126 +++++++++++++++++- .../services/event-types.service.ts | 26 ++-- .../modules/users/services/users.service.ts | 4 +- .../v2/src/modules/users/users.repository.ts | 18 ++- apps/api/v2/swagger/documentation.json | 48 ++++++- docs/api-reference/v2/openapi.json | 48 ++++++- .../inputs/get-event-types-query.input.ts | 21 ++- 7 files changed, 255 insertions(+), 36 deletions(-) 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 3b22bcfe26..d2083999c3 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 @@ -10,10 +10,12 @@ import { UsersModule } from "@/modules/users/users.module"; import { INestApplication } from "@nestjs/common"; import { NestExpressApplication } from "@nestjs/platform-express"; import { Test } from "@nestjs/testing"; -import { PlatformOAuthClient, Team, User, Schedule } from "@prisma/client"; +import { PlatformOAuthClient, Team, User, Schedule, EventType } from "@prisma/client"; import * as request from "supertest"; import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture"; import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; @@ -75,18 +77,22 @@ describe("Event types Endpoints", () => { let teamRepositoryFixture: TeamRepositoryFixture; let eventTypesRepositoryFixture: EventTypesRepositoryFixture; let schedulesRepostoryFixture: SchedulesRepositoryFixture; - + let profileRepositoryFixture: ProfileRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; const userEmail = "event-types-test-e2e@api.com"; const falseTestEmail = "false-event-types@api.com"; const name = "bob-the-builder"; const username = name; let eventType: EventTypeOutput_2024_06_14; let user: User; + let orgUser: User; let falseTestUser: User; let firstSchedule: Schedule; let secondSchedule: Schedule; let falseTestSchedule: Schedule; - + let orgUserEventType1: EventType; + let orgUserEventType2: EventType; + let orgUserEventType3: EventType; beforeAll(async () => { const moduleRef = await withApiAuth( userEmail, @@ -109,8 +115,12 @@ describe("Event types Endpoints", () => { teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); schedulesRepostoryFixture = new SchedulesRepositoryFixture(moduleRef); - - organization = await teamRepositoryFixture.create({ name: "organization" }); + profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + organization = await teamRepositoryFixture.create({ + name: "organization", + slug: "event-type-2024-06-14-org-slug", + }); oAuthClient = await createOAuthClient(organization.id); user = await userRepositoryFixture.create({ email: userEmail, @@ -118,6 +128,49 @@ describe("Event types Endpoints", () => { username, }); + orgUser = await userRepositoryFixture.create({ + email: "event-types-2024-06-14-org-user@example.com", + name: "event-types-2024-06-14-org-user", + username: "event-types-2024-06-14-org-user", + }); + + profileRepositoryFixture.create({ + uid: `usr-${orgUser.id}`, + username: orgUser.username as string, + organization: { + connect: { + id: organization.id, + }, + }, + user: { + connect: { + id: orgUser.id, + }, + }, + }); + + orgUserEventType1 = await eventTypesRepositoryFixture.create( + { title: "orgUserEventType1", slug: "org-event-type-1", length: 60, locations: [] }, + orgUser.id + ); + + orgUserEventType2 = await eventTypesRepositoryFixture.create( + { title: "orgUserEventType2", slug: "org-event-type-2", length: 60, locations: [] }, + orgUser.id + ); + + orgUserEventType3 = await eventTypesRepositoryFixture.create( + { title: "orgUserEventType3", slug: "org-event-type-3", length: 60, locations: [] }, + orgUser.id + ); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: orgUser.id } }, + team: { connect: { id: organization.id } }, + accepted: true, + }); + falseTestUser = await userRepositoryFixture.create({ email: falseTestEmail, name: "false-test", @@ -342,7 +395,7 @@ describe("Event types Endpoints", () => { }); }); - it(`/GET/even-types by username`, async () => { + it(`/GET/event-types by username`, async () => { const response = await request(app.getHttpServer()) .get(`/api/v2/event-types?username=${username}`) .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) @@ -386,6 +439,61 @@ describe("Event types Endpoints", () => { expect(fetchedEventType.color).toEqual(eventType.color); }); + it(`/GET/event-types by username and orgSlug`, async () => { + console.log(organization); + const response = await request(app.getHttpServer()) + .get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data?.length).toEqual(3); + expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined(); + expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined(); + expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined(); + }); + + it(`/GET/event-types by username and orgSlug and eventSlug`, async () => { + const response = await request(app.getHttpServer()) + .get( + `/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}&eventSlug=${orgUserEventType1.slug}` + ) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data?.length).toEqual(1); + expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined(); + }); + + it(`/GET/event-types by username and orgId`, async () => { + const response = await request(app.getHttpServer()) + .get(`/api/v2/event-types?username=${orgUser.username}&orgId=${organization.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data?.length).toEqual(3); + expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined(); + expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined(); + expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined(); + }); + it("should return an error when creating an event type with seats enabled and multiple locations", async () => { const body: CreateEventTypeInput_2024_06_14 = { title: "Coding class 2", @@ -919,6 +1027,12 @@ describe("Event types Endpoints", () => { } catch (e) { // User might have been deleted by the test } + + try { + await userRepositoryFixture.delete(orgUser.id); + } catch (e) { + // User might have been deleted by the test + } await app.close(); }); }); 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 eabbd49c6f..61e7bcf926 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 @@ -74,8 +74,13 @@ export class EventTypesService_2024_06_14 { await this.checkUserOwnsSchedule(userId, body.scheduleId); } - async getEventTypeByUsernameAndSlug(username: string, eventTypeSlug: string) { - const user = await this.usersRepository.findByUsername(username); + async getEventTypeByUsernameAndSlug( + username: string, + eventTypeSlug: string, + orgSlug?: string, + orgId?: number + ) { + const user = await this.usersRepository.findByUsername(username, orgSlug, orgId); if (!user) { return null; } @@ -92,8 +97,8 @@ export class EventTypesService_2024_06_14 { }; } - async getEventTypesByUsername(username: string) { - const user = await this.usersRepository.findByUsername(username); + async getEventTypesByUsername(username: string, orgSlug?: string, orgId?: number) { + const user = await this.usersRepository.findByUsername(username, orgSlug, orgId); if (!user) { return []; } @@ -152,27 +157,26 @@ export class EventTypesService_2024_06_14 { } async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) { - const { username, eventSlug, usernames } = queryParams; - + const { username, eventSlug, usernames, orgSlug, orgId } = queryParams; if (username && eventSlug) { - const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug); + const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug, orgSlug, orgId); return eventType ? [eventType] : []; } if (username) { - return await this.getEventTypesByUsername(username); + return await this.getEventTypesByUsername(username, orgSlug, orgId); } if (usernames) { - const dynamicEventType = await this.getDynamicEventType(usernames); + const dynamicEventType = await this.getDynamicEventType(usernames, orgSlug, orgId); return [dynamicEventType]; } return []; } - async getDynamicEventType(usernames: string[]) { - const users = await this.usersService.getByUsernames(usernames); + async getDynamicEventType(usernames: string[], orgSlug?: string, orgId?: number) { + const users = await this.usersService.getByUsernames(usernames, orgSlug, orgId); const usersFiltered: UserWithProfile[] = []; for (const user of users) { if (user) { diff --git a/apps/api/v2/src/modules/users/services/users.service.ts b/apps/api/v2/src/modules/users/services/users.service.ts index ed541b9500..e10845e20b 100644 --- a/apps/api/v2/src/modules/users/services/users.service.ts +++ b/apps/api/v2/src/modules/users/services/users.service.ts @@ -7,9 +7,9 @@ import { User } from "@calcom/prisma/client"; export class UsersService { constructor(private readonly usersRepository: UsersRepository) {} - async getByUsernames(usernames: string[]) { + async getByUsernames(usernames: string[], orgSlug?: string, orgId?: number) { const users = await Promise.all( - usernames.map((username) => this.usersRepository.findByUsername(username)) + usernames.map((username) => this.usersRepository.findByUsername(username, orgSlug, orgId)) ); const usersFiltered: User[] = []; diff --git a/apps/api/v2/src/modules/users/users.repository.ts b/apps/api/v2/src/modules/users/users.repository.ts index 6d294361ba..7cae2d7011 100644 --- a/apps/api/v2/src/modules/users/users.repository.ts +++ b/apps/api/v2/src/modules/users/users.repository.ts @@ -142,11 +142,21 @@ export class UsersRepository { }); } - async findByUsername(username: string) { + async findByUsername(username: string, orgSlug?: string, orgId?: number) { return this.dbRead.prisma.user.findFirst({ - where: { - username, - }, + where: + orgId || orgSlug + ? { + profiles: { + some: { + organization: orgSlug ? { slug: orgSlug } : { id: orgId }, + username: username, + }, + }, + } + : { + username, + }, }); } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 3e5fcdb136..3a2cf217ad 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -4059,6 +4059,15 @@ "type": "string" } }, + { + "name": "eventTypeId", + "required": false, + "in": "query", + "description": "Id of the event-type, if this is provided no other parameter is required", + "schema": { + "type": "number" + } + }, { "name": "usernames", "required": false, @@ -4067,6 +4076,33 @@ "schema": { "type": "string" } + }, + { + "name": "userId", + "required": false, + "in": "query", + "description": "ID of the user you want the get the event-types of, username is not needed when using this parameter", + "schema": { + "type": "number" + } + }, + { + "name": "orgSlug", + "required": false, + "in": "query", + "description": "slug of the user's organization if he is in one, orgId is not required if using this parameter", + "schema": { + "type": "string" + } + }, + { + "name": "orgId", + "required": false, + "in": "query", + "description": "ID of the organization of the user you want the get the event-types of, orgSlug is not needed when using this parameter", + "schema": { + "type": "number" + } } ], "responses": { @@ -6534,7 +6570,7 @@ "rolling": { "type": "boolean", "example": true, - "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " + "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " } }, "required": [ @@ -6562,7 +6598,7 @@ "rolling": { "type": "boolean", "example": true, - "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " + "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " } }, "required": [ @@ -13450,7 +13486,7 @@ "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Host" + "$ref": "#/components/schemas/BookingHost" } }, "status": { @@ -13571,7 +13607,7 @@ "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Host" + "$ref": "#/components/schemas/BookingHost" } }, "status": { @@ -13737,7 +13773,7 @@ "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Host" + "$ref": "#/components/schemas/BookingHost" } }, "status": { @@ -13853,7 +13889,7 @@ "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Host" + "$ref": "#/components/schemas/BookingHost" } }, "status": { diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 53275302fb..b1191d750f 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -3849,6 +3849,15 @@ "type": "string" } }, + { + "name": "eventTypeId", + "required": false, + "in": "query", + "description": "Id of the event-type, if this is provided no other parameter is required", + "schema": { + "type": "number" + } + }, { "name": "usernames", "required": false, @@ -3857,6 +3866,33 @@ "schema": { "type": "string" } + }, + { + "name": "userId", + "required": false, + "in": "query", + "description": "ID of the user you want the get the event-types of, username is not needed when using this parameter", + "schema": { + "type": "number" + } + }, + { + "name": "orgSlug", + "required": false, + "in": "query", + "description": "slug of the user's organization if he is in one, orgId is not required if using this parameter", + "schema": { + "type": "string" + } + }, + { + "name": "orgId", + "required": false, + "in": "query", + "description": "ID of the organization of the user you want the get the event-types of, orgSlug is not needed when using this parameter", + "schema": { + "type": "number" + } } ], "responses": { @@ -6067,7 +6103,7 @@ "rolling": { "type": "boolean", "example": true, - "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " + "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " } }, "required": ["type", "value"] @@ -6088,7 +6124,7 @@ "rolling": { "type": "boolean", "example": true, - "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " + "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " } }, "required": ["type", "value"] @@ -12100,7 +12136,7 @@ "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Host" + "$ref": "#/components/schemas/BookingHost" } }, "status": { @@ -12216,7 +12252,7 @@ "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Host" + "$ref": "#/components/schemas/BookingHost" } }, "status": { @@ -12371,7 +12407,7 @@ "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Host" + "$ref": "#/components/schemas/BookingHost" } }, "status": { @@ -12482,7 +12518,7 @@ "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Host" + "$ref": "#/components/schemas/BookingHost" } }, "status": { diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/get-event-types-query.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/get-event-types-query.input.ts index 3133d9b8b8..e6c7930f42 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/get-event-types-query.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/get-event-types-query.input.ts @@ -1,6 +1,6 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; import { Transform } from "class-transformer"; -import { IsOptional, IsString } from "class-validator"; +import { IsNumber, IsOptional, IsString } from "class-validator"; export class GetEventTypesQuery_2024_06_14 { @IsOptional() @@ -27,6 +27,25 @@ export class GetEventTypesQuery_2024_06_14 { type: String, }) usernames?: string[]; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: + "slug of the user's organization if he is in one, orgId is not required if using this parameter", + type: String, + }) + orgSlug?: string; + + @Transform(({ value }) => Number(value)) + @IsOptional() + @IsNumber() + @ApiPropertyOptional({ + description: + "ID of the organization of the user you want the get the event-types of, orgSlug is not needed when using this parameter", + type: Number, + }) + orgId?: number; } export class GetTeamEventTypesQuery_2024_06_14 {