From e07142f08664dfe24ece3990356f43592b03119d Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Thu, 12 Dec 2024 19:18:25 +0100 Subject: [PATCH] Neon webhook create (#368) --- .../v1/integrations/neon/webhooks/route.tsx | 42 +++++++++++++++++++ .../app/api/v1/webhooks/svix-token/route.tsx | 8 +--- apps/backend/src/lib/webhooks.tsx | 7 ++++ .../api/v1/integrations/neon/webhooks.test.ts | 26 ++++++++++++ apps/e2e/tests/snapshot-serializer.ts | 1 + eslint-configs/defaults.js | 2 +- 6 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 apps/backend/src/app/api/v1/integrations/neon/webhooks/route.tsx create mode 100644 apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/webhooks.test.ts diff --git a/apps/backend/src/app/api/v1/integrations/neon/webhooks/route.tsx b/apps/backend/src/app/api/v1/integrations/neon/webhooks/route.tsx new file mode 100644 index 000000000..500089e7b --- /dev/null +++ b/apps/backend/src/app/api/v1/integrations/neon/webhooks/route.tsx @@ -0,0 +1,42 @@ +import { getSvixClient } from "@/lib/webhooks"; +import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; +import { adaptSchema, neonAuthorizationHeaderSchema, urlSchema, yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields"; + +export const POST = createSmartRouteHandler({ + metadata: { + hidden: true, + }, + request: yupObject({ + auth: yupObject({ + project: adaptSchema.defined(), + }).defined(), + body: yupObject({ + url: urlSchema.defined(), + description: yupString().optional(), + }).defined(), + headers: yupObject({ + authorization: yupTuple([neonAuthorizationHeaderSchema.defined()]).defined(), + }).defined(), + }), + response: yupObject({ + statusCode: yupNumber().oneOf([200]).defined(), + bodyType: yupString().oneOf(["json"]).defined(), + body: yupObject({ + secret: yupString().defined(), + }).defined(), + }), + handler: async ({ auth, body }) => { + const svix = getSvixClient(); + await svix.application.getOrCreate({ uid: auth.project.id, name: auth.project.id }); + const endpoint = await svix.endpoint.create(auth.project.id, { url: body.url, description: body.description }); + const secret = await svix.endpoint.getSecret(auth.project.id, endpoint.id); + + return { + statusCode: 200, + bodyType: "json", + body: { + secret: secret.key, + }, + }; + }, +}); diff --git a/apps/backend/src/app/api/v1/webhooks/svix-token/route.tsx b/apps/backend/src/app/api/v1/webhooks/svix-token/route.tsx index 61008d842..3433cac4c 100644 --- a/apps/backend/src/app/api/v1/webhooks/svix-token/route.tsx +++ b/apps/backend/src/app/api/v1/webhooks/svix-token/route.tsx @@ -1,17 +1,13 @@ +import { getSvixClient } from "@/lib/webhooks"; import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { svixTokenCrud } from "@stackframe/stack-shared/dist/interface/crud/svix-token"; import { yupObject } from "@stackframe/stack-shared/dist/schema-fields"; -import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; -import { Svix } from "svix"; const appPortalCrudHandlers = createLazyProxy(() => createCrudHandlers(svixTokenCrud, { paramsSchema: yupObject({}), onCreate: async ({ auth }) => { - const svix = new Svix( - getEnvVariable("STACK_SVIX_API_KEY"), - { serverUrl: getEnvVariable("STACK_SVIX_SERVER_URL", "") || undefined } - ); + const svix = getSvixClient(); await svix.application.getOrCreate({ uid: auth.project.id, name: auth.project.id }); const result = await svix.authentication.appPortalAccess(auth.project.id, {}); return { token: result.token }; diff --git a/apps/backend/src/lib/webhooks.tsx b/apps/backend/src/lib/webhooks.tsx index a18c0aafd..6455a8ad7 100644 --- a/apps/backend/src/lib/webhooks.tsx +++ b/apps/backend/src/lib/webhooks.tsx @@ -8,6 +8,13 @@ import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { Svix } from "svix"; import * as yup from "yup"; +export function getSvixClient() { + return new Svix( + getEnvVariable("STACK_SVIX_API_KEY"), + { serverUrl: getEnvVariable("STACK_SVIX_SERVER_URL", "") || undefined } + ); +} + async function sendWebhooks(options: { type: string, projectId: string, diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/webhooks.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/webhooks.test.ts new file mode 100644 index 000000000..792e807b1 --- /dev/null +++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/webhooks.test.ts @@ -0,0 +1,26 @@ +import { it } from "../../../../../../helpers"; +import { niceBackendFetch } from "../../../../../backend-helpers"; +import { provisionProject } from "./projects/provision.test"; + + +it("should be able to create a webhook", async ({ expect }) => { + await provisionProject(); + const response = await niceBackendFetch("/api/v1/integrations/neon/webhooks", { + method: "POST", + body: { + url: "https://example.com/neon", + description: "Test webhook", + }, + headers: { + "Authorization": "Basic bmVvbi1sb2NhbDpuZW9uLWxvY2FsLXNlY3JldA==", + }, + accessType: "admin", + }); + expect(response).toMatchInlineSnapshot(` + NiceResponse { + "status": 200, + "body": { "secret": }, + "headers": Headers {