From 52c7a89785c485198d02e2938bbbaa767420554f Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Thu, 27 Jun 2024 21:04:26 +0200 Subject: [PATCH] feat: organization schedules endpoints (#15584) * feat: organization schedules endpoints * refactor: add IsUserInOrg guard * refactor: separate schedules controller folder * tests * chore: add roles guard and test it * refactor: dont use Promise.all * refactor: rely on profiles instead of user.organizationId * feat: add pagination to getOrganizationSchedules * refactor: default skip=0 and take=250 * chore: regenerate swagger doc --- .../schedules_2024_06_11/schedules.module.ts | 2 +- .../organizations-schedules.controller.ts | 155 ++++ .../organizations-schedules.e2e-spec.ts | 338 ++++++++ .../organizations/organizations.module.ts | 11 +- .../organizations-schedules.repository.ts | 23 + .../organizations-schedules.service.ts | 30 + .../v2/src/modules/users/users.repository.ts | 12 + apps/api/v2/swagger/documentation.json | 754 +++++++++++------- .../repository/profiles.repository.fixture.ts | 26 + packages/prisma/schema.prisma | 1 + 10 files changed, 1078 insertions(+), 274 deletions(-) create mode 100644 apps/api/v2/src/modules/organizations/controllers/schedules/organizations-schedules.controller.ts create mode 100644 apps/api/v2/src/modules/organizations/controllers/schedules/organizations-schedules.e2e-spec.ts create mode 100644 apps/api/v2/src/modules/organizations/repositories/organizations-schedules.repository.ts create mode 100644 apps/api/v2/src/modules/organizations/services/organizations-schedules.service.ts create mode 100644 apps/api/v2/test/fixtures/repository/profiles.repository.fixture.ts diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts index 0f92ac4daf953f..abd2f7cf985a9f 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts +++ b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts @@ -17,6 +17,6 @@ import { Module } from "@nestjs/common"; OutputSchedulesService_2024_06_11, ], controllers: [SchedulesController_2024_06_11], - exports: [SchedulesService_2024_06_11, SchedulesRepository_2024_06_11], + exports: [SchedulesService_2024_06_11, SchedulesRepository_2024_06_11, OutputSchedulesService_2024_06_11], }) export class SchedulesModule_2024_06_11 {} diff --git a/apps/api/v2/src/modules/organizations/controllers/schedules/organizations-schedules.controller.ts b/apps/api/v2/src/modules/organizations/controllers/schedules/organizations-schedules.controller.ts new file mode 100644 index 00000000000000..92924c9cdf4ac5 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/controllers/schedules/organizations-schedules.controller.ts @@ -0,0 +1,155 @@ +import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service"; +import { API_VERSIONS_VALUES } from "@/lib/api-versions"; +import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; +import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; +import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard"; +import { OrganizationsSchedulesService } from "@/modules/organizations/services/organizations-schedules.service"; +import { + Controller, + UseGuards, + Get, + Post, + Param, + ParseIntPipe, + Body, + Patch, + Delete, + HttpCode, + HttpStatus, + Query, +} from "@nestjs/common"; +import { ApiTags as DocsTags } from "@nestjs/swagger"; +import { Transform } from "class-transformer"; +import { IsNumber, Min, Max, IsOptional } from "class-validator"; + +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { + CreateScheduleInput_2024_06_11, + CreateScheduleOutput_2024_06_11, + DeleteScheduleOutput_2024_06_11, + GetScheduleOutput_2024_06_11, + GetSchedulesOutput_2024_06_11, + UpdateScheduleInput_2024_06_11, + UpdateScheduleOutput_2024_06_11, +} from "@calcom/platform-types"; + +class SkipTakePagination { + @Transform(({ value }: { value: string }) => value && parseInt(value)) + @IsNumber() + @Min(1) + @Max(250) + @IsOptional() + take?: number; + + @Transform(({ value }: { value: string }) => value && parseInt(value)) + @IsNumber() + @Min(0) + @IsOptional() + skip?: number; +} + +@Controller({ + path: "/v2/organizations/:orgId", + version: API_VERSIONS_VALUES, +}) +@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard) +@DocsTags("Organizations Schedules") +export class OrganizationsSchedulesController { + constructor( + private schedulesService: SchedulesService_2024_06_11, + private organizationScheduleService: OrganizationsSchedulesService + ) {} + + @Roles("ORG_ADMIN") + @Get("/schedules") + async getOrganizationSchedules( + @Param("orgId", ParseIntPipe) orgId: number, + @Query() queryParams: SkipTakePagination + ): Promise { + const { skip, take } = queryParams; + + const schedules = await this.organizationScheduleService.getOrganizationSchedules(orgId, skip, take); + + return { + status: SUCCESS_STATUS, + data: schedules, + }; + } + + @Roles("ORG_ADMIN") + @UseGuards(IsUserInOrg) + @Post("/users/:userId/schedules") + async createUserSchedule( + @Param("userId", ParseIntPipe) userId: number, + @Body() bodySchedule: CreateScheduleInput_2024_06_11 + ): Promise { + const schedule = await this.schedulesService.createUserSchedule(userId, bodySchedule); + + return { + status: SUCCESS_STATUS, + data: schedule, + }; + } + + @Roles("ORG_ADMIN") + @UseGuards(IsUserInOrg) + @Get("/users/:userId/schedules/:scheduleId") + async getUserSchedule( + @Param("userId", ParseIntPipe) userId: number, + @Param("scheduleId") scheduleId: number + ): Promise { + const schedule = await this.schedulesService.getUserSchedule(userId, scheduleId); + + return { + status: SUCCESS_STATUS, + data: schedule, + }; + } + + @Roles("ORG_ADMIN") + @UseGuards(IsUserInOrg) + @Get("/users/:userId/schedules") + async getUserSchedules( + @Param("userId", ParseIntPipe) userId: number + ): Promise { + const schedules = await this.schedulesService.getUserSchedules(userId); + + return { + status: SUCCESS_STATUS, + data: schedules, + }; + } + + @Roles("ORG_ADMIN") + @UseGuards(IsUserInOrg) + @Patch("/users/:userId/schedules/:scheduleId") + async updateUserSchedule( + @Param("userId", ParseIntPipe) userId: number, + @Param("scheduleId", ParseIntPipe) scheduleId: number, + @Body() bodySchedule: UpdateScheduleInput_2024_06_11 + ): Promise { + const updatedSchedule = await this.schedulesService.updateUserSchedule(userId, scheduleId, bodySchedule); + + return { + status: SUCCESS_STATUS, + data: updatedSchedule, + }; + } + + @Roles("ORG_ADMIN") + @UseGuards(IsUserInOrg) + @Delete("/users/:userId/schedules/:scheduleId") + @HttpCode(HttpStatus.OK) + async deleteUserSchedule( + @Param("userId", ParseIntPipe) userId: number, + @Param("scheduleId", ParseIntPipe) scheduleId: number + ): Promise { + await this.schedulesService.deleteUserSchedule(userId, scheduleId); + + return { + status: SUCCESS_STATUS, + }; + } +} diff --git a/apps/api/v2/src/modules/organizations/controllers/schedules/organizations-schedules.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/schedules/organizations-schedules.e2e-spec.ts new file mode 100644 index 00000000000000..9b972baaa2e76c --- /dev/null +++ b/apps/api/v2/src/modules/organizations/controllers/schedules/organizations-schedules.e2e-spec.ts @@ -0,0 +1,338 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import * as request from "supertest"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { + CreateScheduleInput_2024_06_11, + CreateScheduleOutput_2024_06_11, + GetScheduleOutput_2024_06_11, + GetSchedulesOutput_2024_06_11, + ScheduleAvailabilityInput_2024_06_11, + ScheduleOutput_2024_06_11, + UpdateScheduleInput_2024_06_11, + UpdateScheduleOutput_2024_06_11, +} from "@calcom/platform-types"; +import { User, Team, Membership, Profile } from "@calcom/prisma/client"; + +describe("Organizations Schedules Endpoints", () => { + describe("User lacks required role", () => { + let app: INestApplication; + + let userRepositoryFixture: UserRepositoryFixture; + let organizationsRepositoryFixture: TeamRepositoryFixture; + let membershipFixtures: MembershipRepositoryFixture; + + const userEmail = "mr-robot@schedules-api.com"; + let user: User; + let org: Team; + let membership: Membership; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }) + ).compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + membershipFixtures = new MembershipRepositoryFixture(moduleRef); + + org = await organizationsRepositoryFixture.create({ + name: "Ecorp", + isOrganization: true, + }); + + user = await userRepositoryFixture.create({ + email: userEmail, + username: userEmail, + organization: { connect: { id: org.id } }, + }); + + membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should be defined", () => { + expect(userRepositoryFixture).toBeDefined(); + expect(organizationsRepositoryFixture).toBeDefined(); + expect(user).toBeDefined(); + expect(org).toBeDefined(); + }); + + it("should not be able to create schedule for org user", async () => { + return request(app.getHttpServer()) + .post(`/v2/organizations/${org.id}/users/${user.id}/schedules`) + .send({ + name: "work", + timeZone: "Europe/Rome", + isDefault: true, + }) + .expect(403); + }); + + it("should not be able to get org schedules", async () => { + return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/schedules`).expect(403); + }); + + it("should mot be able to get user schedules", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/users/${user.id}/schedules/`) + .expect(403); + }); + + afterAll(async () => { + await membershipFixtures.delete(membership.id); + await userRepositoryFixture.deleteByEmail(user.email); + await organizationsRepositoryFixture.delete(org.id); + await app.close(); + }); + }); + + describe("User has required role", () => { + let app: INestApplication; + + let userRepositoryFixture: UserRepositoryFixture; + let organizationsRepositoryFixture: TeamRepositoryFixture; + let membershipFixtures: MembershipRepositoryFixture; + let profileRepositoryFixture: ProfileRepositoryFixture; + + const userEmail = "mr-robot@schedules-api.com"; + const username = "mr-robot"; + let user: User; + let org: Team; + let profile: Profile; + let membership: Membership; + + let createdSchedule: ScheduleOutput_2024_06_11; + + const createScheduleInput: CreateScheduleInput_2024_06_11 = { + name: "work", + timeZone: "Europe/Rome", + isDefault: true, + }; + + const defaultAvailability: ScheduleAvailabilityInput_2024_06_11[] = [ + { + days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], + startTime: "09:00", + endTime: "17:00", + }, + ]; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }) + ).compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + membershipFixtures = new MembershipRepositoryFixture(moduleRef); + profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + + org = await organizationsRepositoryFixture.create({ + name: "Ecorp", + isOrganization: true, + }); + + user = await userRepositoryFixture.create({ + email: userEmail, + username: userEmail, + organization: { connect: { id: org.id } }, + }); + + profile = await profileRepositoryFixture.create({ + uid: `usr-${user.id}`, + username: username, + organization: { + connect: { + id: org.id, + }, + }, + user: { + connect: { + id: user.id, + }, + }, + }); + + membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should be defined", () => { + expect(userRepositoryFixture).toBeDefined(); + expect(organizationsRepositoryFixture).toBeDefined(); + expect(user).toBeDefined(); + expect(org).toBeDefined(); + }); + + it("should create schedule for org user", async () => { + return request(app.getHttpServer()) + .post(`/v2/organizations/${org.id}/users/${user.id}/schedules`) + .send(createScheduleInput) + .expect(201) + .then(async (response) => { + const responseBody: CreateScheduleOutput_2024_06_11 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + createdSchedule = response.body.data; + + const expectedSchedule = { + ...createScheduleInput, + availability: defaultAvailability, + overrides: [], + }; + outputScheduleMatchesExpected(createdSchedule, expectedSchedule, 1); + + const scheduleOwner = createdSchedule.ownerId + ? await userRepositoryFixture.get(createdSchedule.ownerId) + : null; + expect(scheduleOwner?.defaultScheduleId).toEqual(createdSchedule.id); + }); + }); + + function outputScheduleMatchesExpected( + outputSchedule: ScheduleOutput_2024_06_11 | null, + expected: CreateScheduleInput_2024_06_11 & { + availability: CreateScheduleInput_2024_06_11["availability"]; + } & { + overrides: CreateScheduleInput_2024_06_11["overrides"]; + }, + expectedAvailabilityLength: number + ) { + expect(outputSchedule).toBeTruthy(); + expect(outputSchedule?.name).toEqual(expected.name); + expect(outputSchedule?.timeZone).toEqual(expected.timeZone); + expect(outputSchedule?.isDefault).toEqual(expected.isDefault); + expect(outputSchedule?.availability.length).toEqual(expectedAvailabilityLength); + + const outputScheduleAvailability = outputSchedule?.availability[0]; + expect(outputScheduleAvailability).toBeDefined(); + expect(outputScheduleAvailability?.days).toEqual(expected.availability?.[0].days); + expect(outputScheduleAvailability?.startTime).toEqual(expected.availability?.[0].startTime); + expect(outputScheduleAvailability?.endTime).toEqual(expected.availability?.[0].endTime); + + expect(JSON.stringify(outputSchedule?.overrides)).toEqual(JSON.stringify(expected.overrides)); + } + + it("should get org schedules", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/schedules`) + .expect(200) + .then(async (response) => { + const responseBody: GetSchedulesOutput_2024_06_11 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const schedules = response.body.data; + expect(schedules.length).toEqual(1); + + const expectedSchedule = { + ...createScheduleInput, + availability: defaultAvailability, + overrides: [], + }; + + outputScheduleMatchesExpected(schedules[0], expectedSchedule, 1); + }); + }); + + it("should get org user schedule", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/users/${user.id}/schedules/${createdSchedule.id}`) + .expect(200) + .then(async (response) => { + const responseBody: GetScheduleOutput_2024_06_11 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const schedule = response.body.data; + + const expectedSchedule = { + ...createScheduleInput, + availability: defaultAvailability, + overrides: [], + }; + + outputScheduleMatchesExpected(schedule, expectedSchedule, 1); + }); + }); + + it("should get user schedules", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/users/${user.id}/schedules/`) + .expect(200) + .then(async (response) => { + const responseBody: GetSchedulesOutput_2024_06_11 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const schedules = response.body.data; + expect(schedules.length).toEqual(1); + + const expectedSchedule = { + ...createScheduleInput, + availability: defaultAvailability, + overrides: [], + }; + + outputScheduleMatchesExpected(schedules[0], expectedSchedule, 1); + }); + }); + + it("should update user schedule name", async () => { + const newScheduleName = "updated-schedule-name"; + + const body: UpdateScheduleInput_2024_06_11 = { + name: newScheduleName, + }; + + return request(app.getHttpServer()) + .patch(`/v2/organizations/${org.id}/users/${user.id}/schedules/${createdSchedule.id}`) + .send(body) + .expect(200) + .then((response: any) => { + const responseData: UpdateScheduleOutput_2024_06_11 = response.body; + expect(responseData.status).toEqual(SUCCESS_STATUS); + const responseSchedule = responseData.data; + + const expectedSchedule = { ...createdSchedule, name: newScheduleName }; + outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1); + + createdSchedule = responseSchedule; + }); + }); + + it("should delete user schedule", async () => { + return request(app.getHttpServer()) + .delete(`/v2/organizations/${org.id}/users/${user.id}/schedules/${createdSchedule.id}`) + .expect(200); + }); + + afterAll(async () => { + await profileRepositoryFixture.delete(profile.id); + await membershipFixtures.delete(membership.id); + await userRepositoryFixture.deleteByEmail(user.email); + await organizationsRepositoryFixture.delete(org.id); + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 99002278e5e379..cc6f958966eba3 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -1,23 +1,30 @@ +import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { OrganizationsTeamsController } from "@/modules/organizations/controllers/organizations-teams.controller"; +import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller"; import { OrganizationsRepository } from "@/modules/organizations/organizations.repository"; +import { OrganizationSchedulesRepository } from "@/modules/organizations/repositories/organizations-schedules.repository"; +import { OrganizationsSchedulesService } from "@/modules/organizations/services/organizations-schedules.service"; import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository"; import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service"; import { OrganizationsService } from "@/modules/organizations/services/organizations.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { StripeModule } from "@/modules/stripe/stripe.module"; +import { UsersModule } from "@/modules/users/users.module"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, StripeModule], + imports: [PrismaModule, StripeModule, SchedulesModule_2024_06_11, UsersModule], providers: [ OrganizationsRepository, OrganizationsTeamsRepository, OrganizationsService, OrganizationsTeamsService, MembershipsRepository, + OrganizationsSchedulesService, + OrganizationSchedulesRepository, ], exports: [OrganizationsService, OrganizationsRepository, OrganizationsTeamsRepository], - controllers: [OrganizationsTeamsController], + controllers: [OrganizationsTeamsController, OrganizationsSchedulesController], }) export class OrganizationsModule {} diff --git a/apps/api/v2/src/modules/organizations/repositories/organizations-schedules.repository.ts b/apps/api/v2/src/modules/organizations/repositories/organizations-schedules.repository.ts new file mode 100644 index 00000000000000..e652a85df79654 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/repositories/organizations-schedules.repository.ts @@ -0,0 +1,23 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class OrganizationSchedulesRepository { + constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + + async getSchedulesByUserIds(userIds: number[], skip: number, take: number) { + return this.dbRead.prisma.schedule.findMany({ + where: { + userId: { + in: userIds, + }, + }, + include: { + availability: true, + }, + skip, + take, + }); + } +} diff --git a/apps/api/v2/src/modules/organizations/services/organizations-schedules.service.ts b/apps/api/v2/src/modules/organizations/services/organizations-schedules.service.ts new file mode 100644 index 00000000000000..7dfd39f0330f1c --- /dev/null +++ b/apps/api/v2/src/modules/organizations/services/organizations-schedules.service.ts @@ -0,0 +1,30 @@ +import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service"; +import { OrganizationSchedulesRepository } from "@/modules/organizations/repositories/organizations-schedules.repository"; +import { UsersRepository } from "@/modules/users/users.repository"; +import { Injectable } from "@nestjs/common"; + +import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types"; + +@Injectable() +export class OrganizationsSchedulesService { + constructor( + private readonly organizationSchedulesService: OrganizationSchedulesRepository, + private readonly outputSchedulesService: OutputSchedulesService_2024_06_11, + private readonly usersRepository: UsersRepository + ) {} + + async getOrganizationSchedules(organizationId: number, skip = 0, take = 250) { + const users = await this.usersRepository.getOrganizationUsers(organizationId); + const usersIds = users.map((user) => user.id); + + const schedules = await this.organizationSchedulesService.getSchedulesByUserIds(usersIds, skip, take); + + const responseSchedules: ScheduleOutput_2024_06_11[] = []; + + for (const schedule of schedules) { + responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule)); + } + + return responseSchedules; + } +} diff --git a/apps/api/v2/src/modules/users/users.repository.ts b/apps/api/v2/src/modules/users/users.repository.ts index cd774690350c89..a175f2309bd11b 100644 --- a/apps/api/v2/src/modules/users/users.repository.ts +++ b/apps/api/v2/src/modules/users/users.repository.ts @@ -181,6 +181,18 @@ export class UsersRepository { return user?.defaultScheduleId; } + + async getOrganizationUsers(organizationId: number) { + const profiles = await this.dbRead.prisma.profile.findMany({ + where: { + organizationId, + }, + include: { + user: true, + }, + }); + return profiles.map((profile) => profile.user); + } } function capitalizeTimezone(timezone: string) { diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 7ce312b35a08d5..1deb21b26f12f0 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -977,17 +977,225 @@ ] } }, + "/v2/organizations/{orgId}/schedules": { + "get": { + "operationId": "OrganizationsSchedulesController_getOrganizationSchedules", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}/schedules": { + "post": { + "operationId": "OrganizationsSchedulesController_createUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "get": { + "operationId": "OrganizationsSchedulesController_getUserSchedules", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}/schedules/{scheduleId}": { + "get": { + "operationId": "OrganizationsSchedulesController_getUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "patch": { + "operationId": "OrganizationsSchedulesController_updateUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "delete": { + "operationId": "OrganizationsSchedulesController_deleteUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, "/v2/schedules": { "post": { - "operationId": "SchedulesController_2024_06_11_createSchedule", + "operationId": "SchedulesController_2024_04_15_createSchedule", "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleInput_2024_04_15" + } + } + } + }, "responses": { "201": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11" + "$ref": "#/components/schemas/CreateScheduleOutput_2024_04_15" } } } @@ -998,7 +1206,7 @@ ] }, "get": { - "operationId": "SchedulesController_2024_06_11_getSchedules", + "operationId": "SchedulesController_2024_04_15_getSchedules", "parameters": [], "responses": { "200": { @@ -1006,7 +1214,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + "$ref": "#/components/schemas/GetSchedulesOutput_2024_04_15" } } } @@ -1019,11 +1227,18 @@ }, "/v2/schedules/default": { "get": { - "operationId": "SchedulesController_2024_06_11_getDefaultSchedule", + "operationId": "SchedulesController_2024_04_15_getDefaultSchedule", "parameters": [], "responses": { "200": { - "description": "Returns the default schedule" + "description": "Returns the default schedule", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetDefaultScheduleOutput_2024_04_15" + } + } + } } }, "tags": [ @@ -1033,7 +1248,7 @@ }, "/v2/schedules/{scheduleId}": { "get": { - "operationId": "SchedulesController_2024_06_11_getSchedule", + "operationId": "SchedulesController_2024_04_15_getSchedule", "parameters": [ { "name": "scheduleId", @@ -1050,7 +1265,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetScheduleOutput_2024_06_11" + "$ref": "#/components/schemas/GetScheduleOutput_2024_04_15" } } } @@ -1061,7 +1276,7 @@ ] }, "patch": { - "operationId": "SchedulesController_2024_06_11_updateSchedule", + "operationId": "SchedulesController_2024_04_15_updateSchedule", "parameters": [ { "name": "scheduleId", @@ -1078,7 +1293,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateScheduleOutput_2024_06_11" + "$ref": "#/components/schemas/UpdateScheduleOutput_2024_04_15" } } } @@ -1089,7 +1304,7 @@ ] }, "delete": { - "operationId": "SchedulesController_2024_06_11_deleteSchedule", + "operationId": "SchedulesController_2024_04_15_deleteSchedule", "parameters": [ { "name": "scheduleId", @@ -1106,7 +1321,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteScheduleOutput_2024_06_11" + "$ref": "#/components/schemas/DeleteScheduleOutput_2024_04_15" } } } @@ -3465,16 +3680,12 @@ "CreateOrgTeamDto": { "type": "object", "properties": { - "id": { - "type": "number" - }, "name": { "type": "string", - "minimum": 1 + "minLength": 1 }, "slug": { - "type": "string", - "minimum": 1 + "type": "string" }, "logoUrl": { "type": "string" @@ -3492,7 +3703,8 @@ "type": "string" }, "hideBranding": { - "type": "boolean" + "type": "boolean", + "default": false }, "isPrivate": { "type": "boolean" @@ -3531,26 +3743,28 @@ "name" ] }, - "CreateAvailabilityInput_2024_04_15": { + "ScheduleAvailabilityInput_2024_06_11": { "type": "object", "properties": { "days": { "example": [ - 1, - 2 + "Monday", + "Tuesday" ], "type": "array", "items": { - "type": "number" + "type": "object" } }, "startTime": { - "format": "date-time", - "type": "string" + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "09:00" }, "endTime": { - "format": "date-time", - "type": "string" + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "10:00" } }, "required": [ @@ -3559,27 +3773,255 @@ "endTime" ] }, - "CreateScheduleInput_2024_04_15": { + "ScheduleOverrideInput_2024_06_11": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "timeZone": { - "type": "string" + "date": { + "type": "string", + "example": "2024-05-20" }, - "availabilities": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CreateAvailabilityInput_2024_04_15" - } + "startTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "12:00" }, - "isDefault": { - "type": "boolean" + "endTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "13:00" } }, "required": [ - "name", + "date", + "startTime", + "endTime" + ] + }, + "ScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 254 + }, + "ownerId": { + "type": "number", + "example": 478 + }, + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "13:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + }, + "required": [ + "id", + "ownerId", + "name", + "timeZone", + "availability", + "isDefault", + "overrides" + ] + }, + "GetSchedulesOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + ] + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "CreateAvailabilityInput_2024_04_15": { + "type": "object", + "properties": { + "days": { + "example": [ + 1, + 2 + ], + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "CreateScheduleInput_2024_04_15": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "availabilities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateAvailabilityInput_2024_04_15" + } + }, + "isDefault": { + "type": "boolean" + } + }, + "required": [ + "name", "timeZone", "isDefault" ] @@ -4073,236 +4515,6 @@ "status" ] }, - "ScheduleAvailabilityInput_2024_06_11": { - "type": "object", - "properties": { - "days": { - "example": [ - "Monday", - "Tuesday" - ], - "type": "array", - "items": { - "type": "object" - } - }, - "startTime": { - "type": "string", - "pattern": "TIME_FORMAT_HH_MM", - "example": "09:00" - }, - "endTime": { - "type": "string", - "pattern": "TIME_FORMAT_HH_MM", - "example": "10:00" - } - }, - "required": [ - "days", - "startTime", - "endTime" - ] - }, - "ScheduleOverrideInput_2024_06_11": { - "type": "object", - "properties": { - "date": { - "type": "string", - "example": "2024-05-20" - }, - "startTime": { - "type": "string", - "pattern": "TIME_FORMAT_HH_MM", - "example": "12:00" - }, - "endTime": { - "type": "string", - "pattern": "TIME_FORMAT_HH_MM", - "example": "13:00" - } - }, - "required": [ - "date", - "startTime", - "endTime" - ] - }, - "ScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 254 - }, - "ownerId": { - "type": "number", - "example": 478 - }, - "name": { - "type": "string", - "example": "One-on-one coaching" - }, - "timeZone": { - "type": "string", - "example": "Europe/Rome" - }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" - } - }, - "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "13:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" - } - } - }, - "required": [ - "id", - "ownerId", - "name", - "timeZone", - "availability", - "isDefault", - "overrides" - ] - }, - "CreateScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" - } - }, - "required": [ - "status", - "data" - ] - }, - "GetScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" - } - ] - }, - "error": { - "type": "object" - } - }, - "required": [ - "status", - "data" - ] - }, - "GetSchedulesOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" - } - }, - "error": { - "type": "object" - } - }, - "required": [ - "status", - "data" - ] - }, - "UpdateScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" - }, - "error": { - "type": "object" - } - }, - "required": [ - "status", - "data" - ] - }, - "DeleteScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - } - }, - "required": [ - "status" - ] - }, "MeOutput": { "type": "object", "properties": { diff --git a/apps/api/v2/test/fixtures/repository/profiles.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/profiles.repository.fixture.ts new file mode 100644 index 00000000000000..d31e6a7939f46f --- /dev/null +++ b/apps/api/v2/test/fixtures/repository/profiles.repository.fixture.ts @@ -0,0 +1,26 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { TestingModule } from "@nestjs/testing"; +import { Prisma } from "@prisma/client"; + +export class ProfileRepositoryFixture { + private primaReadClient: PrismaReadService["prisma"]; + private prismaWriteClient: PrismaWriteService["prisma"]; + + constructor(private readonly module: TestingModule) { + this.primaReadClient = module.get(PrismaReadService).prisma; + this.prismaWriteClient = module.get(PrismaWriteService).prisma; + } + + async get(profileId: number) { + return this.primaReadClient.profile.findFirst({ where: { id: profileId } }); + } + + async create(data: Prisma.ProfileCreateInput) { + return this.prismaWriteClient.profile.create({ data }); + } + + async delete(profileId: number) { + return this.prismaWriteClient.profile.delete({ where: { id: profileId } }); + } +} diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 9df2e32a9ba529..2cd5ebff66ed81 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -301,6 +301,7 @@ model User { verifiedNumbers VerifiedNumber[] verifiedEmails VerifiedEmail[] hosts Host[] + // organizationId is deprecated. Instead, rely on the Profile to search profiles by organizationId and then get user from the profile. organizationId Int? organization Team? @relation("scope", fields: [organizationId], references: [id], onDelete: SetNull) accessCodes AccessCode[]