feat: api v2 webhooks for users and event-types (#15996)
* feat: webhooks for users * fixup! feat: webhooks for users * feat: webhooks for user event types * feat: webhooks for user event types * fixup! Merge branch 'main' into feat-webhooks-api-v2 * doc * chore: split webhook service * chore: webhook repo only depends on prisma * chore: split webhook outputs * fixup! chore: split webhook outputs * chore: describe payload template * chore: pipe webhook input and output * chore: use partialType for update dtos * chore: improve dto
This commit is contained in:
@@ -6,9 +6,17 @@ import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { UsersModule } from "./users/users.module";
|
||||
import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
|
||||
@Module({
|
||||
imports: [OAuthClientModule, BillingModule, PlatformEndpointsModule, TimezoneModule, UsersModule],
|
||||
imports: [
|
||||
OAuthClientModule,
|
||||
BillingModule,
|
||||
PlatformEndpointsModule,
|
||||
TimezoneModule,
|
||||
UsersModule,
|
||||
WebhooksModule,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
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 { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
EventTypeWebhookOutputResponseDto,
|
||||
EventTypeWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/event-type-webhook.output";
|
||||
import { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { EventType, Webhook } from "@calcom/prisma/client";
|
||||
|
||||
describe("EventTypes WebhooksController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
const userEmail = "event-types-webhook-controller-e2e@api.com";
|
||||
let user: UserWithProfile;
|
||||
let otherUser: UserWithProfile;
|
||||
let eventType: EventType;
|
||||
let eventType2: EventType;
|
||||
let otherEventType: EventType;
|
||||
|
||||
let eventTypeRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let webhookRepositoryFixture: WebhookRepositoryFixture;
|
||||
|
||||
let webhook: EventTypeWebhookOutputResponseDto["data"];
|
||||
let webhook2: Webhook;
|
||||
let otherWebhook: Webhook;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
|
||||
eventTypeRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
otherUser = await userRepositoryFixture.create({
|
||||
email: "other-user-webhook-controller@api.com",
|
||||
username: "other-user-webhook-controller@api.com",
|
||||
});
|
||||
|
||||
eventType = await eventTypeRepositoryFixture.create(
|
||||
{
|
||||
title: "Event Type 1",
|
||||
slug: "webhook-event-type-1",
|
||||
length: 60,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
eventType2 = await eventTypeRepositoryFixture.create(
|
||||
{
|
||||
title: "Event Type 2",
|
||||
slug: "webhook-event-type-2",
|
||||
length: 60,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
otherEventType = await eventTypeRepositoryFixture.create(
|
||||
{
|
||||
title: "Other Event Type ",
|
||||
slug: "other-webhook-event-type",
|
||||
length: 60,
|
||||
},
|
||||
otherUser.id
|
||||
);
|
||||
|
||||
otherWebhook = await webhookRepositoryFixture.create({
|
||||
id: "2mdfnn24",
|
||||
subscriberUrl: "https://example.com",
|
||||
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
});
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
userRepositoryFixture.deleteByEmail(user.email);
|
||||
userRepositoryFixture.deleteByEmail(otherUser.email);
|
||||
webhookRepositoryFixture.delete(otherWebhook.id);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("/webhooks (POST)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/event-types/${eventType.id}/webhooks`)
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(201)
|
||||
.then(async (res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
} satisfies EventTypeWebhookOutputResponseDto);
|
||||
webhook = res.body.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks (POST)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/event-types/${eventType2.id}/webhooks`)
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(201)
|
||||
.then(async (res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
eventTypeId: eventType2.id,
|
||||
},
|
||||
} satisfies EventTypeWebhookOutputResponseDto);
|
||||
webhook2 = res.body.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks (POST) should fail to create a webhook for an event-type that does not belong to user", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/event-types/${otherEventType.id}/webhooks`)
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (PATCH)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/event-types/${eventType.id}/webhooks/${webhook.id}`)
|
||||
.send({
|
||||
active: false,
|
||||
} satisfies UpdateWebhookInputDto)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.data.active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (PATCH) should fail to patch a webhook for an event-type that does not belong to user", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/event-types/${otherEventType.id}/webhooks/${otherWebhook.id}`)
|
||||
.send({
|
||||
active: false,
|
||||
} satisfies UpdateWebhookInputDto)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${eventType.id}/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
} satisfies EventTypeWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (GET) should fail to get a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/event-types/${eventType.id}/webhooks/90284`).expect(404);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (GET) should fail to get a webhook of an eventType that does not belong to user", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${otherEventType.id}/webhooks/${otherWebhook.id}`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (GET) should fail to get a webhook that does not belong to the eventType", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${eventType.id}/webhooks/${otherWebhook.id}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("/webhooks (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${eventType.id}/webhooks`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
const responseBody = res.body as EventTypeWebhooksOutputResponseDto;
|
||||
responseBody.data.forEach((webhook) => {
|
||||
expect(webhook.eventTypeId).toBe(eventType.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/event-types/${eventType2.id}/webhooks`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
const responseBody = res.body as EventTypeWebhooksOutputResponseDto;
|
||||
responseBody.data.forEach((webhook) => {
|
||||
expect(webhook.eventTypeId).toBe(eventType2.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${eventType.id}/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
} satisfies EventTypeWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks (DELETE)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${eventType2.id}/webhooks`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: "1 webhooks deleted",
|
||||
} satisfies DeleteManyWebhooksOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${eventType.id}/webhooks/1234453`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("/event-types/:eventTypeId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not belong to user", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/event-types/${otherEventType.id}/webhooks/${otherWebhook.id}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
|
||||
import { IsUserEventTypeWebhookGuard } from "@/modules/webhooks/guards/is-user-event-type-webhook-guard";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
EventTypeWebhookOutputResponseDto,
|
||||
EventTypeWebhookOutputDto,
|
||||
EventTypeWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/event-type-webhook.output";
|
||||
import { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output";
|
||||
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
|
||||
import { EventTypeWebhooksService } from "@/modules/webhooks/services/event-type-webhooks.service";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
UseGuards,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
Delete,
|
||||
Patch,
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/event-types/:eventTypeId/webhooks",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsUserEventTypeWebhookGuard)
|
||||
@ApiTags("Users' EventTypes Webhooks")
|
||||
export class EventTypeWebhooksController {
|
||||
constructor(
|
||||
private readonly webhooksService: WebhooksService,
|
||||
private readonly eventTypeWebhooksService: EventTypeWebhooksService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@ApiOperation({ summary: "Create a webhook for an event-type" })
|
||||
async createEventTypeWebhook(
|
||||
@Body() body: CreateWebhookInputDto,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||
): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
const webhook = await this.eventTypeWebhooksService.createEventTypeWebhook(
|
||||
eventTypeId,
|
||||
new WebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:webhookId")
|
||||
@ApiOperation({ summary: "Update a webhook of an event-type" })
|
||||
async updateEventTypeWebhook(
|
||||
@Body() body: UpdateWebhookInputDto,
|
||||
@Param("webhookId") webhookId: string
|
||||
): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
const webhook = await this.webhooksService.updateWebhook(
|
||||
webhookId,
|
||||
new PartialWebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:webhookId")
|
||||
@ApiOperation({ summary: "Get a webhook of an event-type" })
|
||||
async getEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@ApiOperation({ summary: "Get all webhooks of an event-type" })
|
||||
async getEventTypeWebhooks(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Query() pagination: SkipTakePagination
|
||||
): Promise<EventTypeWebhooksOutputResponseDto> {
|
||||
const webhooks = await this.eventTypeWebhooksService.getEventTypeWebhooksPaginated(
|
||||
eventTypeId,
|
||||
pagination.skip ?? 0,
|
||||
pagination.take ?? 250
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: webhooks.map((webhook) =>
|
||||
plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:webhookId")
|
||||
@ApiOperation({ summary: "Delete a webhook of an event-type" })
|
||||
async deleteEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
await this.webhooksService.deleteWebhook(webhook.id);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(EventTypeWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/")
|
||||
@ApiOperation({ summary: "Delete all webhooks of an event-type" })
|
||||
async deleteAllEventTypeWebhooks(
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||
): Promise<DeleteManyWebhooksOutputResponseDto> {
|
||||
const data = await this.eventTypeWebhooksService.deleteAllEventTypeWebhooks(eventTypeId);
|
||||
return { status: SUCCESS_STATUS, data: `${data.count} webhooks deleted` };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
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 { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
UserWebhookOutputResponseDto,
|
||||
UserWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/user-webhook.output";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
|
||||
describe("WebhooksController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
const userEmail = "webhook-controller-e2e@api.com";
|
||||
let user: UserWithProfile;
|
||||
let otherUser: UserWithProfile;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let webhookRepositoryFixture: WebhookRepositoryFixture;
|
||||
|
||||
let webhook: UserWebhookOutputResponseDto["data"];
|
||||
let otherWebhook: Webhook;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
otherUser = await userRepositoryFixture.create({
|
||||
email: "other-user-webhook-controller@api.com",
|
||||
username: "other-user-webhook-controller@api.com",
|
||||
});
|
||||
|
||||
otherWebhook = await webhookRepositoryFixture.create({
|
||||
id: "2mdfnn2",
|
||||
subscriberUrl: "https://example.com",
|
||||
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
});
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
userRepositoryFixture.deleteByEmail(user.email);
|
||||
userRepositoryFixture.deleteByEmail(otherUser.email);
|
||||
webhookRepositoryFixture.delete(otherWebhook.id);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("/webhooks (POST)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/webhooks")
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(201)
|
||||
.then(async (res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
userId: user.id,
|
||||
},
|
||||
} satisfies UserWebhookOutputResponseDto);
|
||||
webhook = res.body.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks (POST) should fail to create a webhook that already has same userId / subcriberUrl combo", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/webhooks")
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (PATCH)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/webhooks/${webhook.id}`)
|
||||
.send({
|
||||
active: false,
|
||||
} satisfies UpdateWebhookInputDto)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.data.active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
userId: user.id,
|
||||
},
|
||||
} satisfies UserWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (GET) should fail to get a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/webhooks/90284`).expect(404);
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (GET) should fail to get a webhook that does not belong to user", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/webhooks/${otherWebhook.id}`).expect(403);
|
||||
});
|
||||
|
||||
it("/webhooks (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/v2/webhooks")
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
const responseBody = res.body as UserWebhooksOutputResponseDto;
|
||||
responseBody.data.forEach((webhook) => {
|
||||
expect(webhook.userId).toBe(user.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (DELETE)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
userId: user.id,
|
||||
},
|
||||
} satisfies UserWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer()).delete(`/v2/webhooks/12993`).expect(404);
|
||||
});
|
||||
|
||||
it("/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not belong to user", () => {
|
||||
return request(app.getHttpServer()).delete(`/v2/webhooks/${otherWebhook.id}`).expect(403);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
|
||||
import { IsUserWebhookGuard } from "@/modules/webhooks/guards/is-user-webhook-guard";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
UserWebhookOutputDto,
|
||||
UserWebhookOutputResponseDto,
|
||||
UserWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/user-webhook.output";
|
||||
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
|
||||
import { UserWebhooksService } from "@/modules/webhooks/services/user-webhooks.service";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import { Controller, Post, Body, UseGuards, Get, Param, Query, Delete, Patch } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/webhooks",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiTags("Users' Webhooks")
|
||||
export class WebhooksController {
|
||||
constructor(
|
||||
private readonly webhooksService: WebhooksService,
|
||||
private readonly userWebhooksService: UserWebhooksService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@ApiOperation({ summary: "Create a webhook" })
|
||||
async createWebhook(
|
||||
@Body() body: CreateWebhookInputDto,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<UserWebhookOutputResponseDto> {
|
||||
const webhook = await this.userWebhooksService.createUserWebhook(
|
||||
user.id,
|
||||
new WebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:webhookId")
|
||||
@ApiOperation({ summary: "Update a webhook" })
|
||||
@UseGuards(IsUserWebhookGuard)
|
||||
async updateWebhook(
|
||||
@Param("webhookId") webhookId: string,
|
||||
@Body() body: UpdateWebhookInputDto
|
||||
): Promise<UserWebhookOutputResponseDto> {
|
||||
const webhook = await this.webhooksService.updateWebhook(
|
||||
webhookId,
|
||||
new PartialWebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:webhookId")
|
||||
@ApiOperation({ summary: "Get a webhook" })
|
||||
@UseGuards(IsUserWebhookGuard)
|
||||
async getWebhook(@GetWebhook() webhook: Webhook): Promise<UserWebhookOutputResponseDto> {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook)),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@ApiOperation({ summary: "Get all user webhooks paginated" })
|
||||
async getWebhooks(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Query() query: SkipTakePagination
|
||||
): Promise<UserWebhooksOutputResponseDto> {
|
||||
const webhooks = await this.userWebhooksService.getUserWebhooksPaginated(
|
||||
user.id,
|
||||
query.skip ?? 0,
|
||||
query.take ?? 250
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: webhooks.map((webhook) =>
|
||||
plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:webhookId")
|
||||
@ApiOperation({ summary: "Delete a webhook" })
|
||||
@UseGuards(IsUserWebhookGuard)
|
||||
async deleteWebhook(@Param("webhookId") webhookId: string): Promise<UserWebhookOutputResponseDto> {
|
||||
const webhook = await this.webhooksService.deleteWebhook(webhookId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(UserWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { createParamDecorator } from "@nestjs/common";
|
||||
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
|
||||
export type GetWebhookReturnType = Webhook;
|
||||
|
||||
export const GetWebhook = createParamDecorator<
|
||||
keyof GetWebhookReturnType | (keyof GetWebhookReturnType)[],
|
||||
ExecutionContext
|
||||
>((data, ctx) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const webhook = request.webhook as GetWebhookReturnType;
|
||||
|
||||
if (!webhook) {
|
||||
throw new Error("GetWebhook decorator : Webhook not found");
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.reduce((prev, curr) => {
|
||||
return {
|
||||
...prev,
|
||||
[curr]: webhook[curr],
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return webhook[data];
|
||||
}
|
||||
|
||||
return webhook;
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import {
|
||||
BadRequestException,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { EventType, Webhook } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class IsUserEventTypeWebhookGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly webhooksService: WebhooksService,
|
||||
private readonly eventtypesRepository: EventTypesRepository_2024_06_14
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { webhook: Webhook } & { eventType: EventType }>();
|
||||
const user = request.user as GetUserReturnType;
|
||||
const webhookId = request.params.webhookId;
|
||||
const eventTypeId = request.params.eventTypeId;
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (eventTypeId) {
|
||||
const eventType = await this.eventtypesRepository.getEventTypeById(parseInt(eventTypeId));
|
||||
if (!eventType) {
|
||||
throw new NotFoundException(`Event type (${eventTypeId}) not found`);
|
||||
}
|
||||
if (eventType.userId !== user.id) {
|
||||
throw new ForbiddenException(`User (${user.id}) is not the owner of event type (${eventTypeId})`);
|
||||
}
|
||||
request.eventType = eventType;
|
||||
}
|
||||
|
||||
if (webhookId) {
|
||||
const webhook = await this.webhooksService.getWebhookById(webhookId);
|
||||
if (!webhook.eventTypeId) {
|
||||
throw new BadRequestException(`Webhook (${webhookId}) is not associated with an event type`);
|
||||
}
|
||||
if (webhook.eventTypeId !== parseInt(eventTypeId)) {
|
||||
throw new ForbiddenException(
|
||||
`Webhook (${webhookId}) is not associated with event type (${eventTypeId})`
|
||||
);
|
||||
}
|
||||
request.webhook = webhook;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsUserWebhookGuard implements CanActivate {
|
||||
constructor(private readonly webhooksService: WebhooksService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { webhook: Webhook }>();
|
||||
const user = request.user as GetUserReturnType;
|
||||
const webhookId = request.params.webhookId;
|
||||
|
||||
if (!user || !webhookId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const webhook = await this.webhooksService.getWebhookById(webhookId);
|
||||
|
||||
if (webhook.userId !== user.id) {
|
||||
return user.isSystemAdmin;
|
||||
}
|
||||
|
||||
request.webhook = webhook;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ApiProperty, PartialType } from "@nestjs/swagger";
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import { IsArray, IsBoolean, IsEnum, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class CreateWebhookInputDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description:
|
||||
"The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
example: JSON.stringify({
|
||||
content: "A new event has been scheduled",
|
||||
type: "{{type}}",
|
||||
name: "{{title}}",
|
||||
organizer: "{{organizer.name}}",
|
||||
booker: "{{attendees.0.name}}",
|
||||
}),
|
||||
})
|
||||
payloadTemplate?: string;
|
||||
|
||||
@IsBoolean()
|
||||
active!: boolean;
|
||||
|
||||
@IsString()
|
||||
subscriberUrl!: string;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty({
|
||||
example: [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_CONFIRMED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_COMPLETED",
|
||||
"BOOKING_NO_SHOW",
|
||||
"BOOKING_REOPENED",
|
||||
],
|
||||
enum: WebhookTriggerEvents,
|
||||
})
|
||||
@IsEnum(WebhookTriggerEvents, { each: true })
|
||||
triggers!: WebhookTriggerEvents[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
export class UpdateWebhookInputDto extends PartialType(CreateWebhookInputDto) {}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
import { WebhookOutputDto } from "./webhook.output";
|
||||
|
||||
export class EventTypeWebhookOutputDto extends WebhookOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly eventTypeId!: number;
|
||||
}
|
||||
|
||||
export class EventTypeWebhookOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: EventTypeWebhookOutputDto;
|
||||
}
|
||||
|
||||
export class EventTypeWebhooksOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: EventTypeWebhookOutputDto[];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
import { WebhookOutputDto } from "./webhook.output";
|
||||
|
||||
export class UserWebhookOutputDto extends WebhookOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly userId!: number;
|
||||
}
|
||||
|
||||
export class UserWebhookOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: UserWebhookOutputDto;
|
||||
}
|
||||
|
||||
export class UserWebhooksOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: UserWebhookOutputDto[];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsBoolean, IsEnum, IsInt, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class WebhookOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly id!: number;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description:
|
||||
"The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
example: JSON.stringify({
|
||||
content: "A new event has been scheduled",
|
||||
type: "{{type}}",
|
||||
name: "{{title}}",
|
||||
organizer: "{{organizer.name}}",
|
||||
booker: "{{attendees.0.name}}",
|
||||
}),
|
||||
})
|
||||
readonly payloadTemplate!: string;
|
||||
|
||||
@IsEnum(WebhookTriggerEvents)
|
||||
@Expose()
|
||||
readonly triggers!: WebhookTriggerEvents[];
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly subscriberUrl!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly active!: boolean;
|
||||
}
|
||||
|
||||
export class DeleteManyWebhooksOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
@Expose()
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => WebhookOutputDto)
|
||||
data!: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import { PipeTransform, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class WebhookInputPipe implements PipeTransform {
|
||||
transform(value: CreateWebhookInputDto) {
|
||||
const { triggers, ...rest } = value;
|
||||
const eventTriggers = triggers;
|
||||
const parsedData = { ...rest, eventTriggers };
|
||||
return parsedData;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PartialWebhookInputPipe implements PipeTransform {
|
||||
transform(value: UpdateWebhookInputDto) {
|
||||
if (value.triggers) {
|
||||
const { triggers, ...rest } = value;
|
||||
const eventTriggers = triggers;
|
||||
const parsedData = { ...rest, eventTriggers };
|
||||
return parsedData;
|
||||
}
|
||||
return { ...value, eventTriggers: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
export type PipedInputWebhookType = ReturnType<WebhookInputPipe["transform"]>;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PipeTransform, Injectable } from "@nestjs/common";
|
||||
import { Webhook } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class WebhookOutputPipe implements PipeTransform {
|
||||
transform(value: Webhook) {
|
||||
if (value?.eventTriggers) {
|
||||
const { eventTriggers, ...rest } = value;
|
||||
const triggers = eventTriggers;
|
||||
const parsedData = { ...rest, triggers };
|
||||
return parsedData;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export type PipedOutputWebhookType = ReturnType<WebhookOutputPipe["transform"]>;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||
import { ConflictException, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class EventTypeWebhooksService {
|
||||
constructor(private readonly webhooksRepository: WebhooksRepository) {}
|
||||
|
||||
async createEventTypeWebhook(eventTypeId: number, body: PipedInputWebhookType) {
|
||||
const existingWebhook = await this.webhooksRepository.getEventTypeWebhookByUrl(
|
||||
eventTypeId,
|
||||
body.subscriberUrl
|
||||
);
|
||||
if (existingWebhook) {
|
||||
throw new ConflictException("Webhook with this subscriber url already exists for this event type");
|
||||
}
|
||||
return this.webhooksRepository.createEventTypeWebhook(eventTypeId, {
|
||||
...body,
|
||||
payloadTemplate: body.payloadTemplate ?? null,
|
||||
secret: body.secret ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
getEventTypeWebhooksPaginated(eventTypeId: number, skip: number, take: number) {
|
||||
return this.webhooksRepository.getEventTypeWebhooksPaginated(eventTypeId, skip, take);
|
||||
}
|
||||
|
||||
async deleteAllEventTypeWebhooks(eventTypeId: number): Promise<{ count: number }> {
|
||||
return this.webhooksRepository.deleteAllEventTypeWebhooks(eventTypeId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||
import { ConflictException, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class UserWebhooksService {
|
||||
constructor(private readonly webhooksRepository: WebhooksRepository) {}
|
||||
|
||||
async createUserWebhook(userId: number, body: PipedInputWebhookType) {
|
||||
const existingWebhook = await this.webhooksRepository.getUserWebhookByUrl(userId, body.subscriberUrl);
|
||||
if (existingWebhook) {
|
||||
throw new ConflictException("Webhook with this subscriber url already exists for this user");
|
||||
}
|
||||
|
||||
return this.webhooksRepository.createUserWebhook(userId, {
|
||||
...body,
|
||||
payloadTemplate: body.payloadTemplate ?? null,
|
||||
secret: body.secret ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserWebhooksPaginated(userId: number, skip: number, take: number) {
|
||||
return this.webhooksRepository.getUserWebhooksPaginated(userId, skip, take);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class WebhooksService {
|
||||
constructor(private readonly webhooksRepository: WebhooksRepository) {}
|
||||
|
||||
async updateWebhook(webhookId: string, body: UpdateWebhookInputDto) {
|
||||
return this.webhooksRepository.updateWebhook(webhookId, body);
|
||||
}
|
||||
|
||||
async getWebhookById(webhookId: string) {
|
||||
const webhook = await this.webhooksRepository.getWebhookById(webhookId);
|
||||
if (!webhook) {
|
||||
throw new NotFoundException(`Webhook (${webhookId}) not found`);
|
||||
}
|
||||
return webhook;
|
||||
}
|
||||
|
||||
async deleteWebhook(webhookId: string) {
|
||||
return this.webhooksRepository.deleteWebhook(webhookId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypeWebhooksController } from "@/modules/event-types/controllers/event-types-webhooks.controller";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { WebhooksController } from "./controllers/webhooks.controller";
|
||||
import { EventTypeWebhooksService } from "./services/event-type-webhooks.service";
|
||||
import { UserWebhooksService } from "./services/user-webhooks.service";
|
||||
import { WebhooksService } from "./services/webhooks.service";
|
||||
import { WebhooksRepository } from "./webhooks.repository";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, UsersModule, EventTypesModule_2024_06_14],
|
||||
controllers: [WebhooksController, EventTypeWebhooksController],
|
||||
providers: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
|
||||
exports: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
|
||||
})
|
||||
export class WebhooksModule {}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
|
||||
import { PrismaWriteService } from "../prisma/prisma-write.service";
|
||||
|
||||
type WebhookInputData = Pick<
|
||||
Webhook,
|
||||
"payloadTemplate" | "eventTriggers" | "subscriberUrl" | "secret" | "active"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
export class WebhooksRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createUserWebhook(userId: number, data: WebhookInputData) {
|
||||
const id = uuidv4();
|
||||
return this.dbWrite.prisma.webhook.create({
|
||||
data: { ...data, id, userId },
|
||||
});
|
||||
}
|
||||
|
||||
async createEventTypeWebhook(eventTypeId: number, data: WebhookInputData) {
|
||||
const id = uuidv4();
|
||||
return this.dbWrite.prisma.webhook.create({
|
||||
data: { ...data, id, eventTypeId },
|
||||
});
|
||||
}
|
||||
|
||||
async updateWebhook(webhookId: string, data: Partial<WebhookInputData>) {
|
||||
return this.dbWrite.prisma.webhook.update({
|
||||
where: { id: webhookId },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async getWebhookById(webhookId: string) {
|
||||
return this.dbRead.prisma.webhook.findFirst({
|
||||
where: { id: webhookId },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserWebhooksPaginated(userId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.webhook.findMany({
|
||||
where: { userId },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeWebhooksPaginated(eventTypeId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.webhook.findMany({
|
||||
where: { eventTypeId },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserWebhookByUrl(userId: number, subscriberUrl: string) {
|
||||
return this.dbRead.prisma.webhook.findFirst({
|
||||
where: { userId, subscriberUrl },
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeWebhookByUrl(eventTypeId: number, subscriberUrl: string) {
|
||||
return this.dbRead.prisma.webhook.findFirst({
|
||||
where: { eventTypeId, subscriberUrl },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteWebhook(webhookId: string) {
|
||||
return this.dbWrite.prisma.webhook.delete({
|
||||
where: { id: webhookId },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAllEventTypeWebhooks(eventTypeId: number) {
|
||||
return this.dbWrite.prisma.webhook.deleteMany({
|
||||
where: { eventTypeId },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1219,16 +1219,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateScheduleInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
@@ -1331,16 +1321,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -2380,16 +2360,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateScheduleInput_2024_04_15"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -3261,6 +3231,369 @@
|
||||
"Timezones"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/webhooks": {
|
||||
"post": {
|
||||
"operationId": "WebhooksController_createWebhook",
|
||||
"summary": "Create a webhook",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "WebhooksController_getWebhooks",
|
||||
"summary": "Get all user webhooks paginated",
|
||||
"parameters": [
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/webhooks/{webhookId}": {
|
||||
"patch": {
|
||||
"operationId": "WebhooksController_updateWebhook",
|
||||
"summary": "Update a webhook",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "webhookId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "WebhooksController_getWebhook",
|
||||
"summary": "Get a webhook",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "WebhooksController_deleteWebhook",
|
||||
"summary": "Delete a webhook",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "webhookId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/event-types/{eventTypeId}/webhooks": {
|
||||
"post": {
|
||||
"operationId": "EventTypeWebhooksController_createEventTypeWebhook",
|
||||
"summary": "Create a webhook for an event-type",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "EventTypeWebhooksController_getEventTypeWebhooks",
|
||||
"summary": "Get all webhooks of an event-type",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "EventTypeWebhooksController_deleteAllEventTypeWebhooks",
|
||||
"summary": "Delete all webhooks of an event-type",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/event-types/{eventTypeId}/webhooks/{webhookId}": {
|
||||
"patch": {
|
||||
"operationId": "EventTypeWebhooksController_updateEventTypeWebhook",
|
||||
"summary": "Update a webhook of an event-type",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "webhookId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "EventTypeWebhooksController_getEventTypeWebhook",
|
||||
"summary": "Get a webhook of an event-type",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "EventTypeWebhooksController_deleteEventTypeWebhook",
|
||||
"summary": "Delete a webhook of an event-type",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@@ -5508,57 +5841,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CreateScheduleInput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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": "14:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"timeZone",
|
||||
"isDefault"
|
||||
]
|
||||
},
|
||||
"CreateScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5607,52 +5889,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateScheduleInput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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": "14:00"
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateScheduleOutput_2024_06_11": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6747,26 +6983,6 @@
|
||||
"userId"
|
||||
]
|
||||
},
|
||||
"GetDefaultScheduleOutput_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"
|
||||
]
|
||||
},
|
||||
"CreateAvailabilityInput_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7064,66 +7280,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateScheduleInput_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"schedule": {
|
||||
"example": [
|
||||
[],
|
||||
[
|
||||
{
|
||||
"start": "2022-01-01T00:00:00.000Z",
|
||||
"end": "2022-01-02T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"items": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dateOverrides": {
|
||||
"example": [
|
||||
[],
|
||||
[
|
||||
{
|
||||
"start": "2022-01-01T00:00:00.000Z",
|
||||
"end": "2022-01-02T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"items": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"timeZone",
|
||||
"name",
|
||||
"isDefault",
|
||||
"schedule"
|
||||
]
|
||||
},
|
||||
"EventTypeModel_2024_04_15": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8425,6 +8581,285 @@
|
||||
"ReserveSlotInput": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"CreateWebhookInputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "string",
|
||||
"example": [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_CONFIRMED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_COMPLETED",
|
||||
"BOOKING_NO_SHOW",
|
||||
"BOOKING_REOPENED"
|
||||
],
|
||||
"enum": [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_PAYMENT_INITIATED",
|
||||
"BOOKING_PAID",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_REQUESTED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_NO_SHOW_UPDATED",
|
||||
"FORM_SUBMITTED",
|
||||
"MEETING_ENDED",
|
||||
"MEETING_STARTED",
|
||||
"RECORDING_READY",
|
||||
"INSTANT_MEETING",
|
||||
"RECORDING_TRANSCRIPTION_GENERATED"
|
||||
]
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"triggers",
|
||||
"active",
|
||||
"subscriberUrl"
|
||||
]
|
||||
},
|
||||
"UserWebhookOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"userId": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"payloadTemplate",
|
||||
"userId",
|
||||
"id",
|
||||
"triggers",
|
||||
"subscriberUrl",
|
||||
"active"
|
||||
]
|
||||
},
|
||||
"UserWebhookOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateWebhookInputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "string",
|
||||
"example": [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_CONFIRMED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_COMPLETED",
|
||||
"BOOKING_NO_SHOW",
|
||||
"BOOKING_REOPENED"
|
||||
],
|
||||
"enum": [
|
||||
"BOOKING_CREATED",
|
||||
"BOOKING_PAYMENT_INITIATED",
|
||||
"BOOKING_PAID",
|
||||
"BOOKING_RESCHEDULED",
|
||||
"BOOKING_REQUESTED",
|
||||
"BOOKING_CANCELLED",
|
||||
"BOOKING_REJECTED",
|
||||
"BOOKING_NO_SHOW_UPDATED",
|
||||
"FORM_SUBMITTED",
|
||||
"MEETING_ENDED",
|
||||
"MEETING_STARTED",
|
||||
"RECORDING_READY",
|
||||
"INSTANT_MEETING",
|
||||
"RECORDING_TRANSCRIPTION_GENERATED"
|
||||
]
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserWebhooksOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UserWebhookOutputDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"EventTypeWebhookOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"payloadTemplate",
|
||||
"eventTypeId",
|
||||
"id",
|
||||
"triggers",
|
||||
"subscriberUrl",
|
||||
"active"
|
||||
]
|
||||
},
|
||||
"EventTypeWebhookOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"EventTypeWebhooksOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeWebhookOutputDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"DeleteManyWebhooksOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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 WebhookRepositoryFixture {
|
||||
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 create(data: Prisma.WebhookCreateInput) {
|
||||
return this.prismaWriteClient.webhook.create({ data });
|
||||
}
|
||||
|
||||
async delete(webhookId: string) {
|
||||
return this.prismaWriteClient.webhook.delete({ where: { id: webhookId } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user