Skip to content

Commit 056922b

Browse files
feat: add webhook versioning (calcom#23861)
* add webhook version schema * version the code * update version from numeric to date val * migration * update schema and build factory * update string * move version picker * tooltip instead of infobadge * -- * fix type * -- * fix type * fix type * -- * fix messed up merge * improvements to payloadfactory * extract version off of DB and instead keep it in IWebhookRepository * fix webhookform * fix type safety and routing ambiguity * scalable with easier factory extensions and base definition * fix types * -- * -- * clean up prisma/client type imports * fix * type fix * type fix * cleanup * add tests and registry changes * unintended file inclusion * type-fix * select in repo * -- * explicit return type * -- * fix type * fixes * feedback 1 * feedback 2 * use enum instead of string * fixes
1 parent 42bd4b4 commit 056922b

71 files changed

Lines changed: 3307 additions & 990 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/api/v1/lib/validations/webhook.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from "zod";
22

33
import { WEBHOOK_TRIGGER_EVENTS } from "@calcom/features/webhooks/lib/constants";
4+
import { WebhookVersion } from "@calcom/features/webhooks/lib/interface/IWebhookRepository";
45
import { WebhookSchema } from "@calcom/prisma/zod/modelSchema/WebhookSchema";
56

67
const schemaWebhookBaseBodyParams = WebhookSchema.pick({
@@ -21,6 +22,7 @@ export const schemaWebhookCreateParams = z
2122
eventTypeId: z.number().optional(),
2223
userId: z.number().optional(),
2324
secret: z.string().optional().nullable(),
25+
version: z.nativeEnum(WebhookVersion).optional(),
2426
// API shouldn't mess with Apps webhooks yet (ie. Zapier)
2527
// appId: z.string().optional().nullable(),
2628
})
@@ -33,6 +35,7 @@ export const schemaWebhookEditBodyParams = schemaWebhookBaseBodyParams
3335
z.object({
3436
eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(),
3537
secret: z.string().optional().nullable(),
38+
version: z.nativeEnum(WebhookVersion).optional(),
3639
})
3740
)
3841
.partial()
@@ -44,6 +47,7 @@ export const schemaWebhookReadPublic = WebhookSchema.pick({
4447
eventTypeId: true,
4548
payloadTemplate: true,
4649
eventTriggers: true,
50+
version: true,
4751
// FIXME: We have some invalid urls saved in the DB
4852
// subscriberUrl: true,
4953
/** @todo: find out how to properly add back and validate those. */

apps/api/v2/src/modules/webhooks/inputs/webhook.input.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger";
22
import { IsArray, IsBoolean, IsEnum, IsOptional, IsString } from "class-validator";
33

4-
import { WebhookTriggerEvents } from "@calcom/platform-libraries";
4+
import { WebhookTriggerEvents, WebhookVersion } from "@calcom/platform-libraries";
55

66
export class CreateWebhookInputDto {
77
@IsString()
@@ -48,6 +48,15 @@ export class CreateWebhookInputDto {
4848
@IsOptional()
4949
@ApiPropertyOptional()
5050
secret?: string;
51+
52+
@IsOptional()
53+
@IsEnum(WebhookVersion)
54+
@ApiPropertyOptional({
55+
description: "The version of the webhook",
56+
example: WebhookVersion.V_2021_10_20,
57+
enum: WebhookVersion,
58+
})
59+
version?: WebhookVersion;
5160
}
5261

5362
export class UpdateWebhookInputDto extends PartialType(CreateWebhookInputDto) {}
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { PageProps } from "app/_types";
2-
import { _generateMetadata, getTranslate } from "app/_utils";
2+
import { _generateMetadata } from "app/_utils";
33

4-
import SettingsHeaderWithBackButton from "@calcom/features/settings/appDir/SettingsHeaderWithBackButton";
54
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
65
import { EditWebhookView } from "@calcom/features/webhooks/pages/webhook-edit-view";
76
import { APP_NAME } from "@calcom/lib/constants";
@@ -16,21 +15,13 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
1615
);
1716

1817
const Page = async ({ params: _params }: PageProps) => {
19-
const t = await getTranslate();
2018
const params = await _params;
2119
const id = typeof params?.id === "string" ? params.id : undefined;
2220

2321
const webhookRepository = WebhookRepository.getInstance();
2422
const webhook = await webhookRepository.findByWebhookId(id);
2523

26-
return (
27-
<SettingsHeaderWithBackButton
28-
title={t("edit_webhook")}
29-
description={t("add_webhook_description", { appName: APP_NAME })}
30-
borderInShellHeader={true}>
31-
<EditWebhookView webhook={webhook} />
32-
</SettingsHeaderWithBackButton>
33-
);
24+
return <EditWebhookView webhook={webhook} />;
3425
};
3526

3627
export default Page;

apps/web/lib/hooks/settings/platform/oauth-clients/useOAuthClientWebhooks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
22

33
import type { ApiSuccessResponse } from "@calcom/platform-types";
44
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
5+
import type { WebhookVersion } from "@calcom/features/webhooks/lib/interface/IWebhookRepository";
56

67
type Input = {
78
active: boolean;
@@ -19,6 +20,7 @@ type Output = {
1920
triggers: WebhookTriggerEvents[];
2021
secret?: string;
2122
payloadTemplate: string | undefined | null;
23+
version?: WebhookVersion;
2224
};
2325

2426
export const useOAuthClientWebhooks = (clientId: string) => {

apps/web/modules/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
66
import Shell from "@calcom/features/shell/Shell";
77
import { WebhookForm } from "@calcom/features/webhooks/components";
88
import { useLocale } from "@calcom/lib/hooks/useLocale";
9+
import { DEFAULT_WEBHOOK_VERSION } from "@calcom/features/webhooks/lib/interface/IWebhookRepository";
910
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
1011
import { showToast } from "@calcom/ui/components/toast";
1112

@@ -82,6 +83,7 @@ export default function EditOAuthClientWebhooks() {
8283
subscriberUrl: data.subscriberUrl,
8384
triggers: data.eventTriggers,
8485
secret: data.secret ?? undefined,
86+
version: data.version,
8587
};
8688
if (webhook) {
8789
await updateWebhook({
@@ -95,7 +97,7 @@ export default function EditOAuthClientWebhooks() {
9597
}
9698
await refetchWebhooks();
9799
router.push("/settings/platform/");
98-
} catch (err) {
100+
} catch {
99101
showToast(t(webhookId ? "webhook_update_failed" : "webhook_create_failed"), "error");
100102
}
101103
}}
@@ -105,7 +107,12 @@ export default function EditOAuthClientWebhooks() {
105107
noRoutingFormTriggers={true}
106108
webhook={
107109
webhook
108-
? { ...webhook, eventTriggers: webhook.triggers, secret: webhook.secret ?? null }
110+
? {
111+
...webhook,
112+
eventTriggers: webhook.triggers,
113+
secret: webhook.secret ?? null,
114+
version: webhook.version ?? DEFAULT_WEBHOOK_VERSION,
115+
}
109116
: undefined
110117
}
111118
/>

apps/web/public/static/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3835,6 +3835,7 @@
38353835
"webhook_attendee_email": "Email of the first attendee",
38363836
"webhook_attendee_timezone": "Timezone of the first attendee",
38373837
"webhook_attendee_locale": "Locale of the first attendee",
3838+
"webhook_version": "Webhook Version",
38383839
"webhook_team_name": "Name of the team booked",
38393840
"webhook_team_members": "Members of the team booked",
38403841
"webhook_video_call_url": "Video call URL for the meeting",

packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { describe, it, vi, expect, beforeEach, afterEach } from "vitest";
55

66
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
77
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
8-
import type { GetWebhooksReturnType } from "@calcom/features/webhooks/lib/getWebhooks";
8+
import type { WebhookSubscriber } from "@calcom/features/webhooks/lib/dto/types";
99
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
1010
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
1111
import {
@@ -15,6 +15,7 @@ import {
1515
WorkflowTemplates,
1616
TimeUnit,
1717
} from "@calcom/prisma/enums";
18+
import { WebhookVersion as WebhookVersionEnum } from "@calcom/features/webhooks/lib/interface/IWebhookRepository";
1819

1920
import type { FormResponse, Field } from "../types/types";
2021
import { _onFormSubmission } from "./formSubmissionUtils";
@@ -100,7 +101,7 @@ describe("_onFormSubmission", () => {
100101

101102
describe("Webhooks", () => {
102103
it("should call FORM_SUBMITTED webhooks", async () => {
103-
const mockWebhook: GetWebhooksReturnType[number] = {
104+
const mockWebhook: WebhookSubscriber = {
104105
id: "wh-1",
105106
secret: "secret",
106107
subscriberUrl: "https://example.com/webhook",
@@ -109,6 +110,7 @@ describe("_onFormSubmission", () => {
109110
eventTriggers: [WebhookTriggerEvents.FORM_SUBMITTED],
110111
time: null,
111112
timeUnit: null,
113+
version: WebhookVersionEnum.V_2021_10_20,
112114
};
113115
vi.mocked(getWebhooks).mockResolvedValueOnce([mockWebhook]);
114116

@@ -141,7 +143,7 @@ describe("_onFormSubmission", () => {
141143
"field-1": { label: "Attendee Name", value: "John Doe" },
142144
};
143145

144-
const mockWebhook: GetWebhooksReturnType[number] = {
146+
const mockWebhook: WebhookSubscriber = {
145147
id: "wh-1",
146148
secret: "secret",
147149
subscriberUrl: "https://example.com/webhook",
@@ -150,6 +152,7 @@ describe("_onFormSubmission", () => {
150152
eventTriggers: [WebhookTriggerEvents.FORM_SUBMITTED],
151153
time: null,
152154
timeUnit: null,
155+
version: WebhookVersionEnum.V_2021_10_20,
153156
};
154157
vi.mocked(getWebhooks).mockResolvedValueOnce([mockWebhook]);
155158

packages/features/di/webhooks/Webhooks.tokens.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,8 @@ export const WEBHOOK_TOKENS = {
88
OOO_WEBHOOK_SERVICE: Symbol("OOO_WEBHOOK_SERVICE"),
99
WEBHOOK_NOTIFICATION_HANDLER: Symbol("WebhookNotificationHandler"),
1010

11-
// Payload builders
12-
BOOKING_PAYLOAD_BUILDER: Symbol("BookingPayloadBuilder"),
13-
FORM_PAYLOAD_BUILDER: Symbol("FormPayloadBuilder"),
14-
OOO_PAYLOAD_BUILDER: Symbol("OOOPayloadBuilder"),
15-
RECORDING_PAYLOAD_BUILDER: Symbol("RecordingPayloadBuilder"),
16-
MEETING_PAYLOAD_BUILDER: Symbol("MeetingPayloadBuilder"),
17-
INSTANT_MEETING_BUILDER: Symbol("InstantMeetingBuilder"),
11+
// Payload builder factory (versioning)
12+
PAYLOAD_BUILDER_FACTORY: Symbol("PayloadBuilderFactory"),
1813

1914
// Repositories
2015
WEBHOOK_REPOSITORY: Symbol("IWebhookRepository"),

packages/features/di/webhooks/containers/webhook.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@ webhookContainer.load(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE, webhookModule);
1919
webhookContainer.load(WEBHOOK_TOKENS.FORM_WEBHOOK_SERVICE, webhookModule);
2020
webhookContainer.load(WEBHOOK_TOKENS.RECORDING_WEBHOOK_SERVICE, webhookModule);
2121
webhookContainer.load(WEBHOOK_TOKENS.OOO_WEBHOOK_SERVICE, webhookModule);
22-
webhookContainer.load(WEBHOOK_TOKENS.BOOKING_PAYLOAD_BUILDER, webhookModule);
23-
webhookContainer.load(WEBHOOK_TOKENS.FORM_PAYLOAD_BUILDER, webhookModule);
24-
webhookContainer.load(WEBHOOK_TOKENS.OOO_PAYLOAD_BUILDER, webhookModule);
25-
webhookContainer.load(WEBHOOK_TOKENS.RECORDING_PAYLOAD_BUILDER, webhookModule);
26-
webhookContainer.load(WEBHOOK_TOKENS.MEETING_PAYLOAD_BUILDER, webhookModule);
27-
webhookContainer.load(WEBHOOK_TOKENS.INSTANT_MEETING_BUILDER, webhookModule);
22+
webhookContainer.load(WEBHOOK_TOKENS.PAYLOAD_BUILDER_FACTORY, webhookModule);
2823
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_NOTIFICATION_HANDLER, webhookModule);
2924
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_NOTIFIER, webhookModule);
3025

packages/features/di/webhooks/modules/Webhook.module.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { createModule } from "@evyweb/ioctopus";
22

3-
import { BookingPayloadBuilder } from "@calcom/features/webhooks/lib/factory/BookingPayloadBuilder";
4-
import { FormPayloadBuilder } from "@calcom/features/webhooks/lib/factory/FormPayloadBuilder";
5-
import { InstantMeetingBuilder } from "@calcom/features/webhooks/lib/factory/InstantMeetingBuilder";
6-
import { MeetingPayloadBuilder } from "@calcom/features/webhooks/lib/factory/MeetingPayloadBuilder";
7-
import { OOOPayloadBuilder } from "@calcom/features/webhooks/lib/factory/OOOPayloadBuilder";
8-
import { RecordingPayloadBuilder } from "@calcom/features/webhooks/lib/factory/RecordingPayloadBuilder";
3+
import { createPayloadBuilderFactory } from "@calcom/features/webhooks/lib/factory/versioned/registry";
94
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
105
import { BookingWebhookService } from "@calcom/features/webhooks/lib/service/BookingWebhookService";
116
import { FormWebhookService } from "@calcom/features/webhooks/lib/service/FormWebhookService";
@@ -54,25 +49,15 @@ webhookModule
5449
SHARED_TOKENS.LOGGER,
5550
]);
5651

57-
// Bind payload builders
58-
webhookModule.bind(WEBHOOK_TOKENS.BOOKING_PAYLOAD_BUILDER).toClass(BookingPayloadBuilder);
59-
webhookModule.bind(WEBHOOK_TOKENS.FORM_PAYLOAD_BUILDER).toClass(FormPayloadBuilder);
60-
webhookModule.bind(WEBHOOK_TOKENS.OOO_PAYLOAD_BUILDER).toClass(OOOPayloadBuilder);
61-
webhookModule.bind(WEBHOOK_TOKENS.RECORDING_PAYLOAD_BUILDER).toClass(RecordingPayloadBuilder);
62-
webhookModule.bind(WEBHOOK_TOKENS.MEETING_PAYLOAD_BUILDER).toClass(MeetingPayloadBuilder);
63-
webhookModule.bind(WEBHOOK_TOKENS.INSTANT_MEETING_BUILDER).toClass(InstantMeetingBuilder);
52+
// Bind payload builder factory (composition root for versioning)
53+
webhookModule.bind(WEBHOOK_TOKENS.PAYLOAD_BUILDER_FACTORY).toFactory(() => createPayloadBuilderFactory());
6454

65-
// Bind notification handler
55+
// Bind notification handler with factory
6656
webhookModule
6757
.bind(WEBHOOK_TOKENS.WEBHOOK_NOTIFICATION_HANDLER)
6858
.toClass(WebhookNotificationHandler, [
6959
WEBHOOK_TOKENS.WEBHOOK_SERVICE,
70-
WEBHOOK_TOKENS.BOOKING_PAYLOAD_BUILDER,
71-
WEBHOOK_TOKENS.FORM_PAYLOAD_BUILDER,
72-
WEBHOOK_TOKENS.OOO_PAYLOAD_BUILDER,
73-
WEBHOOK_TOKENS.RECORDING_PAYLOAD_BUILDER,
74-
WEBHOOK_TOKENS.MEETING_PAYLOAD_BUILDER,
75-
WEBHOOK_TOKENS.INSTANT_MEETING_BUILDER,
60+
WEBHOOK_TOKENS.PAYLOAD_BUILDER_FACTORY,
7661
SHARED_TOKENS.LOGGER,
7762
]);
7863

0 commit comments

Comments
 (0)