From 2aa3d1d4e05514827d7fd1c75bd6f3af194e91ef Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:33:22 +0200 Subject: [PATCH] feat: get org ooo entries and filters/sort (#18645) * feat: get org ooo entries and filters/sort * remove console.log --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> --- .../v2/src/modules/ooo/inputs/ooo.input.ts | 46 +++++++ .../ooo/repositories/ooo.repository.ts | 9 +- .../src/modules/ooo/services/ooo.service.ts | 11 +- .../ooo/organizations-users-ooo-controller.ts | 48 ++++++-- .../ooo/organizations-users-ooo.e2e-spec.ts | 25 ++++ .../organizations-users-ooo.repository.ts | 33 +++++ .../organization-users-ooo.service.ts | 26 ++++ .../organizations/organizations.module.ts | 4 + .../modules/users/inputs/get-users.input.ts | 21 +++- apps/api/v2/swagger/documentation.json | 113 ++++++++++++++++++ docs/api-reference/v2/openapi.json | 99 +++++++++++++++ 11 files changed, 418 insertions(+), 17 deletions(-) create mode 100644 apps/api/v2/src/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository.ts create mode 100644 apps/api/v2/src/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service.ts diff --git a/apps/api/v2/src/modules/ooo/inputs/ooo.input.ts b/apps/api/v2/src/modules/ooo/inputs/ooo.input.ts index f29e935e5a0ab3..94c94333ccf0a0 100644 --- a/apps/api/v2/src/modules/ooo/inputs/ooo.input.ts +++ b/apps/api/v2/src/modules/ooo/inputs/ooo.input.ts @@ -3,6 +3,8 @@ import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger"; import { Transform } from "class-transformer"; import { IsDate, IsInt, IsOptional, IsString, IsEnum, isDate } from "class-validator"; +import { SkipTakePagination } from "@calcom/platform-types"; + export enum OutOfOfficeReason { UNSPECIFIED = "unspecified", VACATION = "vacation", @@ -80,3 +82,47 @@ export class CreateOutOfOfficeEntryDto { } export class UpdateOutOfOfficeEntryDto extends PartialType(CreateOutOfOfficeEntryDto) {} + +export enum SortOrder { + asc = "asc", + desc = "desc", +} +type SortOrderType = keyof typeof SortOrder; + +export class GetOutOfOfficeEntryFiltersDTO extends SkipTakePagination { + @IsOptional() + @IsEnum(SortOrder, { + message: 'SortStart must be either "asc" or "desc".', + }) + @ApiProperty({ + required: false, + description: "Sort results by their start time in ascending or descending order.", + example: "?sortStart=asc OR ?sortStart=desc", + enum: SortOrder, + }) + sortStart?: SortOrderType; + + @IsOptional() + @IsEnum(SortOrder, { + message: 'SortEnd must be either "asc" or "desc".', + }) + @ApiProperty({ + required: false, + description: "Sort results by their end time in ascending or descending order.", + example: "?sortEnd=asc OR ?sortEnd=desc", + enum: SortOrder, + }) + sortEnd?: SortOrderType; +} + +export class GetOrgUsersOutOfOfficeEntryFiltersDTO extends GetOutOfOfficeEntryFiltersDTO { + @IsString() + @IsOptional() + @ApiProperty({ + type: String, + required: false, + description: "Filter ooo entries by the user email address. user must be within your organization.", + example: "example@domain.com", + }) + email?: string; +} diff --git a/apps/api/v2/src/modules/ooo/repositories/ooo.repository.ts b/apps/api/v2/src/modules/ooo/repositories/ooo.repository.ts index c2363ccded3d42..64077339fc1127 100644 --- a/apps/api/v2/src/modules/ooo/repositories/ooo.repository.ts +++ b/apps/api/v2/src/modules/ooo/repositories/ooo.repository.ts @@ -46,12 +46,19 @@ export class UserOOORepository { }); } - async getUserOOOPaginated(userId: number, skip: number, take: number) { + async getUserOOOPaginated( + userId: number, + skip: number, + take: number, + sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" } + ) { return this.dbRead.prisma.outOfOfficeEntry.findMany({ where: { userId }, skip, take, include: { reason: true }, + ...(sort?.sortStart && { orderBy: { start: sort.sortStart } }), + ...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }), }); } diff --git a/apps/api/v2/src/modules/ooo/services/ooo.service.ts b/apps/api/v2/src/modules/ooo/services/ooo.service.ts index 8905065bdaff87..0bc489b9b60a36 100644 --- a/apps/api/v2/src/modules/ooo/services/ooo.service.ts +++ b/apps/api/v2/src/modules/ooo/services/ooo.service.ts @@ -2,6 +2,8 @@ import { CreateOutOfOfficeEntryDto, UpdateOutOfOfficeEntryDto, OutOfOfficeReason, + GetOutOfOfficeEntryFiltersDTO, + SortOrder, } from "@/modules/ooo/inputs/ooo.input"; import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository"; import { UsersRepository } from "@/modules/users/users.repository"; @@ -128,8 +130,13 @@ export class UserOOOService { return this.formatOooReason(ooo); } - async getUserOOOPaginated(userId: number, skip: number, take: number) { - const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take); + async getUserOOOPaginated( + userId: number, + skip: number, + take: number, + sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" } + ) { + const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take, sort); return ooos.map((ooo) => this.formatOooReason(ooo)); } } diff --git a/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller.ts b/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller.ts index 90f6f604d6bc83..d44c5d39541225 100644 --- a/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller.ts +++ b/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller.ts @@ -8,13 +8,19 @@ 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 { IsUserOOO } from "@/modules/ooo/guards/is-user-ooo"; -import { CreateOutOfOfficeEntryDto, UpdateOutOfOfficeEntryDto } from "@/modules/ooo/inputs/ooo.input"; +import { + CreateOutOfOfficeEntryDto, + UpdateOutOfOfficeEntryDto, + GetOutOfOfficeEntryFiltersDTO, + GetOrgUsersOutOfOfficeEntryFiltersDTO, +} from "@/modules/ooo/inputs/ooo.input"; import { UserOooOutputDto, UserOooOutputResponseDto, UserOoosOutputResponseDto, } from "@/modules/ooo/outputs/ooo.output"; import { UserOOOService } from "@/modules/ooo/services/ooo.service"; +import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service"; import { Controller, UseGuards, @@ -33,10 +39,9 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; import { plainToInstance } from "class-transformer"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; @Controller({ - path: "/v2/organizations/:orgId/users/:userId/ooo", + path: "/v2/organizations/:orgId", version: API_VERSIONS_VALUES, }) @UseInterceptors(ClassSerializerInterceptor) @@ -44,18 +49,22 @@ import { SkipTakePagination } from "@calcom/platform-types"; @UseGuards(IsOrgGuard) @DocsTags("Orgs / Users / OOO") export class OrganizationsUsersOOOController { - constructor(private readonly userOOOService: UserOOOService) {} + constructor( + private readonly userOOOService: UserOOOService, + private readonly orgUsersOOOService: OrgUsersOOOService + ) {} - @Get() + @Get("/users/:userId/ooo") @Roles("ORG_ADMIN") @PlatformPlan("ESSENTIALS") @UseGuards(IsUserInOrg) @ApiOperation({ summary: "Get all ooo entries of a user" }) async getOrganizationUserOOO( @Param("userId", ParseIntPipe) userId: number, - @Query() query: SkipTakePagination + @Query() query: GetOutOfOfficeEntryFiltersDTO ): Promise { - const ooos = await this.userOOOService.getUserOOOPaginated(userId, query.skip ?? 0, query.take ?? 250); + const { skip, take, ...rest } = query ?? { skip: 0, take: 250 }; + const ooos = await this.userOOOService.getUserOOOPaginated(userId, skip ?? 0, take ?? 250, rest); return { status: SUCCESS_STATUS, @@ -63,7 +72,7 @@ export class OrganizationsUsersOOOController { }; } - @Post() + @Post("/users/:userId/ooo") @Roles("ORG_ADMIN") @PlatformPlan("ESSENTIALS") @UseGuards(IsUserInOrg) @@ -79,7 +88,7 @@ export class OrganizationsUsersOOOController { }; } - @Patch("/:oooId") + @Patch("/users/:userId/ooo/:oooId") @Roles("ORG_ADMIN") @PlatformPlan("ESSENTIALS") @UseGuards(IsUserInOrg, IsUserOOO) @@ -97,7 +106,7 @@ export class OrganizationsUsersOOOController { }; } - @Delete("/:oooId") + @Delete("/users/:userId/ooo/:oooId") @Roles("ORG_ADMIN") @PlatformPlan("ESSENTIALS") @UseGuards(IsUserInOrg, IsUserOOO) @@ -111,4 +120,23 @@ export class OrganizationsUsersOOOController { data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }), }; } + + @Get("/ooo") + @Roles("ORG_ADMIN") + @PlatformPlan("ESSENTIALS") + @ApiOperation({ summary: "Get all OOO entries of org users" }) + async getOrganizationUsersOOO( + @Param("orgId", ParseIntPipe) orgId: number, + @Query() query: GetOrgUsersOutOfOfficeEntryFiltersDTO + ): Promise { + const { skip, take, email, ...rest } = query ?? { skip: 0, take: 250 }; + const ooos = await this.orgUsersOOOService.getOrgUsersOOOPaginated(orgId, skip ?? 0, take ?? 250, rest, { + email, + }); + + return { + status: SUCCESS_STATUS, + data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })), + }; + } } diff --git a/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo.e2e-spec.ts index ab8252b7032718..1e3c511925aea9 100644 --- a/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo.e2e-spec.ts @@ -366,6 +366,31 @@ describe("Organizations User OOO Endpoints", () => { }); }); + it("should get 2 ooo entries", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/ooo?sortEnd=desc&email=${teammate1Email}`) + .expect(200) + .then((response) => { + const responseBody = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + + const data = responseBody.data as UserOooOutputDto[]; + expect(data.length).toEqual(2); + const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId); + expect(oooUno).toBeDefined(); + if (oooUno) { + expect(oooUno.reason).toEqual("vacation"); + expect(oooUno.toUserId).toEqual(teammate2.id); + expect(oooUno.userId).toEqual(teammate1.id); + expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z"); + expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z"); + } + // test sort + expect(data[1].id).toEqual(oooCreatedViaApiId); + + }); + }); + it("should get 2 ooo entries", async () => { return request(app.getHttpServer()) .get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`) diff --git a/apps/api/v2/src/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository.ts b/apps/api/v2/src/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository.ts new file mode 100644 index 00000000000000..c00ecd27f87a47 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository.ts @@ -0,0 +1,33 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class OrgUsersOOORepository { + constructor(private readonly dbRead: PrismaReadService) {} + async getOrgUsersOOOPaginated( + orgId: number, + skip: number, + take: number, + sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }, + filters?: { email?: string } + ) { + console.log({ sort, filters }); + return this.dbRead.prisma.outOfOfficeEntry.findMany({ + where: { + user: { + ...(filters?.email && { email: filters.email }), + profiles: { + some: { + organizationId: orgId, + }, + }, + }, + }, + skip, + take, + include: { reason: true }, + ...(sort?.sortStart && { orderBy: { start: sort.sortStart } }), + ...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }), + }); + } +} diff --git a/apps/api/v2/src/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service.ts b/apps/api/v2/src/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service.ts new file mode 100644 index 00000000000000..ccf3e7c4d20c8d --- /dev/null +++ b/apps/api/v2/src/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service.ts @@ -0,0 +1,26 @@ +import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository"; +import { UserOOOService } from "@/modules/ooo/services/ooo.service"; +import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository"; +import { UsersRepository } from "@/modules/users/users.repository"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class OrgUsersOOOService { + constructor( + private readonly oooRepository: UserOOORepository, + private readonly oooUserService: UserOOOService, + private readonly usersRepository: UsersRepository, + private readonly orgUsersOOORepository: OrgUsersOOORepository + ) {} + + async getOrgUsersOOOPaginated( + orgId: number, + skip: number, + take: number, + sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }, + filters?: { email?: string } + ) { + const ooos = await this.orgUsersOOORepository.getOrgUsersOOOPaginated(orgId, skip, take, sort, filters); + return ooos.map((ooo) => this.oooUserService.formatOooReason(ooo)); + } +} diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index d741a89866193a..4991f1e9acaaee 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -15,6 +15,8 @@ import { OrganizationsTeamsMembershipsController } from "@/modules/organizations import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller"; import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller"; import { OrganizationsUsersOOOController } from "@/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller"; +import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository"; +import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service"; import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller"; import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller"; import { OrganizationsRepository } from "@/modules/organizations/organizations.repository"; @@ -91,6 +93,8 @@ import { Module } from "@nestjs/common"; OutputTeamEventTypesResponsePipe, UserOOOService, UserOOORepository, + OrgUsersOOOService, + OrgUsersOOORepository, ], exports: [ OrganizationsService, diff --git a/apps/api/v2/src/modules/users/inputs/get-users.input.ts b/apps/api/v2/src/modules/users/inputs/get-users.input.ts index 5818d62775f5f5..5d7908bb3c6c33 100644 --- a/apps/api/v2/src/modules/users/inputs/get-users.input.ts +++ b/apps/api/v2/src/modules/users/inputs/get-users.input.ts @@ -1,12 +1,25 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Transform } from "class-transformer"; -import { IsOptional, Validate } from "class-validator"; - -import { SkipTakePagination } from "@calcom/platform-types"; +import { IsNumber, IsOptional, Max, Min, Validate } from "class-validator"; import { IsEmailStringOrArray } from "../validators/isEmailStringOrArray"; -export class GetUsersInput extends SkipTakePagination { +export class GetUsersInput { + @ApiProperty({ required: false, description: "The number of items to return", example: 10 }) + @Transform(({ value }: { value: string }) => value && parseInt(value)) + @IsNumber() + @Min(1) + @Max(1000) + @IsOptional() + take?: number; + + @ApiProperty({ required: false, description: "The number of items to skip", example: 0 }) + @Transform(({ value }: { value: string }) => value && parseInt(value)) + @IsNumber() + @Min(0) + @IsOptional() + skip?: number; + @IsOptional() @Validate(IsEmailStringOrArray) @Transform(({ value }: { value: string | string[] }) => { diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index d3ef3223296ed7..d261eb50bbed64 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2516,6 +2516,8 @@ "description": "The number of items to return", "example": 10, "schema": { + "minimum": 1, + "maximum": 1000, "type": "number" } }, @@ -2526,6 +2528,7 @@ "description": "The number of items to skip", "example": 0, "schema": { + "minimum": 0, "type": "number" } }, @@ -2716,6 +2719,34 @@ "schema": { "type": "number" } + }, + { + "name": "sortStart", + "required": false, + "in": "query", + "description": "Sort results by their start time in ascending or descending order.", + "example": "?sortStart=asc OR ?sortStart=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortEnd", + "required": false, + "in": "query", + "description": "Sort results by their end time in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } } ], "responses": { @@ -2824,6 +2855,88 @@ ] } }, + "/v2/organizations/{orgId}/ooo": { + "get": { + "operationId": "OrganizationsUsersOOOController_getOrganizationUsersOOO", + "summary": "Get all OOO entries of org users", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "sortStart", + "required": false, + "in": "query", + "description": "Sort results by their start time in ascending or descending order.", + "example": "?sortStart=asc OR ?sortStart=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortEnd", + "required": false, + "in": "query", + "description": "Sort results by their end time in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "email", + "required": false, + "in": "query", + "description": "Filter ooo entries by the user email address. user must be within your organization.", + "example": "example@domain.com", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Orgs / Users / OOO" + ] + } + }, "/v2/organizations/{orgId}/webhooks": { "get": { "operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks", diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index f2c7847f8a3b94..1dcefb7117f483 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -2389,6 +2389,8 @@ "description": "The number of items to return", "example": 10, "schema": { + "minimum": 1, + "maximum": 1000, "type": "number" } }, @@ -2399,6 +2401,7 @@ "description": "The number of items to skip", "example": 0, "schema": { + "minimum": 0, "type": "number" } }, @@ -2581,6 +2584,28 @@ "schema": { "type": "number" } + }, + { + "name": "sortStart", + "required": false, + "in": "query", + "description": "Sort results by their start time in ascending or descending order.", + "example": "?sortStart=asc OR ?sortStart=desc", + "schema": { + "enum": ["asc", "desc"], + "type": "string" + } + }, + { + "name": "sortEnd", + "required": false, + "in": "query", + "description": "Sort results by their end time in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": ["asc", "desc"], + "type": "string" + } } ], "responses": { @@ -2681,6 +2706,80 @@ "tags": ["Orgs / Users / OOO"] } }, + "/v2/organizations/{orgId}/ooo": { + "get": { + "operationId": "OrganizationsUsersOOOController_getOrganizationUsersOOO", + "summary": "Get all OOO entries of org users", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "sortStart", + "required": false, + "in": "query", + "description": "Sort results by their start time in ascending or descending order.", + "example": "?sortStart=asc OR ?sortStart=desc", + "schema": { + "enum": ["asc", "desc"], + "type": "string" + } + }, + { + "name": "sortEnd", + "required": false, + "in": "query", + "description": "Sort results by their end time in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": ["asc", "desc"], + "type": "string" + } + }, + { + "name": "email", + "required": false, + "in": "query", + "description": "Filter ooo entries by the user email address. user must be within your organization.", + "example": "example@domain.com", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": ["Orgs / Users / OOO"] + } + }, "/v2/organizations/{orgId}/webhooks": { "get": { "operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks",