From 6cd7518720d15e6011fb55210648e7c2c14abef3 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 23 Oct 2023 14:40:24 +0100 Subject: [PATCH] Created HttpEndpoint as a top-level concept, it produces triggers using onRequest --- packages/core/src/schemas/triggers.ts | 7 ++ packages/trigger-sdk/src/httpEndpoint.ts | 117 +++++++++++++++++ packages/trigger-sdk/src/security.ts | 4 +- packages/trigger-sdk/src/triggerClient.ts | 8 +- .../trigger-sdk/src/triggers/httpTrigger.ts | 118 ------------------ references/job-catalog/src/httpTrigger.ts | 7 +- 6 files changed, 134 insertions(+), 127 deletions(-) create mode 100644 packages/trigger-sdk/src/httpEndpoint.ts delete mode 100644 packages/trigger-sdk/src/triggers/httpTrigger.ts diff --git a/packages/core/src/schemas/triggers.ts b/packages/core/src/schemas/triggers.ts index 4ff5cd0c6..5f248dbc0 100644 --- a/packages/core/src/schemas/triggers.ts +++ b/packages/core/src/schemas/triggers.ts @@ -40,6 +40,12 @@ export const ScheduledTriggerMetadataSchema = z.object({ schedule: ScheduleMetadataSchema, }); +export const HttpEndpointTriggerMetadataSchema = z.object({ + type: z.literal("httpendpoint"), + endpointId: z.string(), + filter: EventFilterSchema.optional(), +}); + export const AssetTriggerMetadataSchema = z.object({ type: z.literal("modular"), key: z.string(), @@ -49,6 +55,7 @@ export const TriggerMetadataSchema = z.discriminatedUnion("type", [ DynamicTriggerMetadataSchema, StaticTriggerMetadataSchema, ScheduledTriggerMetadataSchema, + HttpEndpointTriggerMetadataSchema, AssetTriggerMetadataSchema, ]); diff --git a/packages/trigger-sdk/src/httpEndpoint.ts b/packages/trigger-sdk/src/httpEndpoint.ts new file mode 100644 index 000000000..f830565d2 --- /dev/null +++ b/packages/trigger-sdk/src/httpEndpoint.ts @@ -0,0 +1,117 @@ +import { + DisplayProperty, + EventFilter, + Prettify, + RequestFilter, + TriggerMetadata, +} from "@trigger.dev/core"; +import { EventSpecification, EventSpecificationExample, Trigger } from "./types"; +import { TriggerClient } from "./triggerClient"; +import { Job } from "./job"; +import { z } from "zod"; +import { ParsedPayloadSchemaError } from "./errors"; +import { formatSchemaErrors } from "./utils/formatSchemaErrors"; + +type HttpEndpointOptions> = { + id: string; + event: TEventSpecification; +}; + +export type RequestOptions = { + filter?: EventFilter; +}; + +class HttpEndpoint> { + constructor(private readonly options: HttpEndpointOptions) {} + + onRequest(options: RequestOptions): HttpTrigger { + return new HttpTrigger({ + endpointId: this.options.id, + event: this.options.event, + filter: options.filter, + }); + } +} + +type TriggerOptions> = { + endpointId: string; + event: TEventSpecification; + filter?: EventFilter; +}; + +class HttpTrigger> + implements Trigger +{ + constructor(private readonly options: TriggerOptions) {} + + toJSON(): TriggerMetadata { + return { + type: "httpendpoint", + endpointId: this.options.endpointId, + filter: this.options.filter, + }; + } + + get event() { + return this.options.event; + } + + attachToJob(triggerClient: TriggerClient, job: Job, any>): void { + // triggerClient.attachModularTrigger({ key: this.#key, trigger: this }); + } + + get preprocessRuns() { + return false; + } +} + +type RequestContext = { + secret: string | undefined; +}; + +const HttpEndpointPayloadSchema = z.object({ + headers: z.record(z.string()), + body: z.any(), +}); + +type HttpEndpointPayload = z.infer; + +export type EndpointOptions = { + id: string; + /** The source of the webhook, e.g. whatsapp.com */ + source: string; + title?: string; + icon?: string; + examples?: EventSpecificationExample[]; + properties?: DisplayProperty[]; + respondWith?: { + filter?: RequestFilter; + handler: (request: Request, context: RequestContext) => Promise; + }; + verify: (request: Request, context: RequestContext) => Promise; +}; + +export function httpEndpoint( + options: EndpointOptions +): HttpEndpoint> { + return new HttpEndpoint({ + id: options.id, + event: { + name: options.id, + title: options.title ?? "HTTP Trigger", + source: options.source, + icon: options.icon ?? "world-www", + properties: options.properties, + examples: options.examples, + parsePayload: (rawPayload: any) => { + const result = HttpEndpointPayloadSchema.safeParse(rawPayload); + + if (!result.success) { + throw new ParsedPayloadSchemaError(formatSchemaErrors(result.error.issues)); + } + + return result.data; + }, + }, + }); +} diff --git a/packages/trigger-sdk/src/security.ts b/packages/trigger-sdk/src/security.ts index f04968c9e..e74d8a6c4 100644 --- a/packages/trigger-sdk/src/security.ts +++ b/packages/trigger-sdk/src/security.ts @@ -4,12 +4,12 @@ export async function verifyRequestSignature({ request, headerName, secret, - algorithm = "sha256", + algorithm, }: { request: Request; headerName: string; secret?: string; - algorithm?: "sha256"; + algorithm: "sha256"; }): Promise { const headerValue = request.headers.get(headerName); if (!headerValue) { diff --git a/packages/trigger-sdk/src/triggerClient.ts b/packages/trigger-sdk/src/triggerClient.ts index 0f6709f49..e04122f52 100644 --- a/packages/trigger-sdk/src/triggerClient.ts +++ b/packages/trigger-sdk/src/triggerClient.ts @@ -55,6 +55,7 @@ import type { TriggerPreprocessContext, } from "./types"; import { HttpTriggerOptions, httpTrigger } from "./triggers/httpTrigger"; +import { EndpointOptions, httpEndpoint } from "./httpEndpoint"; const registerSourceEvent: EventSpecification = { name: REGISTER_SOURCE_EVENT_V2, @@ -472,8 +473,11 @@ export class TriggerClient { return new DynamicTrigger(this, options); } - defineHttpTrigger(options: HttpTriggerOptions) { - return httpTrigger(options); + defineHttpEndpoint(options: EndpointOptions) { + const endpoint = httpEndpoint(options); + //todo have a record + //todo it so they're indexed + return endpoint; } attach(job: Job, any>): void { diff --git a/packages/trigger-sdk/src/triggers/httpTrigger.ts b/packages/trigger-sdk/src/triggers/httpTrigger.ts deleted file mode 100644 index f7e8dff11..000000000 --- a/packages/trigger-sdk/src/triggers/httpTrigger.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - DisplayProperty, - EventFilter, - HttpMethod, - Prettify, - RequestFilter, - TriggerMetadata, -} from "@trigger.dev/core"; -import { ParsedPayloadSchemaError } from "../errors"; -import { Job } from "../job"; -import { TriggerClient } from "../triggerClient"; -import { EventSpecification, EventSpecificationExample, SchemaParser, Trigger } from "../types"; -import { formatSchemaErrors } from "../utils/formatSchemaErrors"; -import { SendEvent } from "@trigger.dev/core"; - -type Options> = { - id: string; - event: TEventSpecification; -}; - -export class HttpTrigger> - implements Trigger -{ - constructor(private readonly options: Options) {} - - toJSON(): TriggerMetadata { - return { - type: "modular", - key: this.#key, - }; - } - - get #key() { - return `http-trigger-${this.options.id}`; - } - - get event() { - return this.options.event; - } - - attachToJob(triggerClient: TriggerClient, job: Job, any>): void { - //todo create the actual modular trigger, and pass that through - //the modular trigger is what will be used outside of HttpTriggers as well - // triggerClient.attachModularTrigger({ key: this.#key, trigger: this }); - } - - get preprocessRuns() { - return false; - } -} - -type RequestContext = { - secret: string | undefined; -}; - -/** Configuration options for an EventTrigger */ -export type HttpTriggerOptions = { - id: string; - /** The hostname of the webhook, e.g. whatsapp.com */ - hostname: string; - title?: string; - icon?: string; - bodySchema?: SchemaParser; - filter?: EventFilter; - examples?: EventSpecificationExample[]; - properties?: DisplayProperty[]; - respondWith?: { - filter: RequestFilter; - handler: (request: Request, context: RequestContext) => Promise; - }; - verify: (request: Request, context: RequestContext) => Promise; - /** Use this if you want to control the events created. */ - transform?: (request: Request, context: RequestContext) => Promise; -}; - -type HttpRequest = { - headers: Record; - method: HttpMethod; - searchParams: Record; - body: TBody; -}; - -/** `eventTrigger()` is set as a [Job's trigger](https://trigger.dev/docs/sdk/job) to subscribe to an event a Job from [a sent event](https://trigger.dev/docs/sdk/triggerclient/instancemethods/sendevent) - * @param options options for the EventTrigger - */ -export function httpTrigger( - options: HttpTriggerOptions -): Trigger>>> { - return new HttpTrigger({ - id: options.id, - event: { - name: options.id, - title: options.title ?? "HTTP Trigger", - source: options.hostname, - icon: options.icon ?? "world-www", - properties: options.properties, - examples: options.examples, - parsePayload: (rawPayload: any) => { - if (options.bodySchema) { - const result = options.bodySchema.safeParse(rawPayload.body); - - if (!result.success) { - throw new ParsedPayloadSchemaError(formatSchemaErrors(result.error.issues)); - } - - return { - headers: rawPayload.headers, - method: rawPayload.method, - searchParams: rawPayload.searchParams, - body: result.data, - }; - } - - return rawPayload as any; - }, - }, - }); -} diff --git a/references/job-catalog/src/httpTrigger.ts b/references/job-catalog/src/httpTrigger.ts index 22afd8ad2..99ee94afb 100644 --- a/references/job-catalog/src/httpTrigger.ts +++ b/references/job-catalog/src/httpTrigger.ts @@ -1,6 +1,5 @@ import { createExpressServer } from "@trigger.dev/express"; import { TriggerClient, verifyRequestSignature } from "@trigger.dev/sdk"; -import { z } from "zod"; export const client = new TriggerClient({ id: "job-catalog", @@ -10,7 +9,6 @@ export const client = new TriggerClient({ ioLogLocalEnabled: true, }); -//todo this is a new primitive, it doesn't get register called on it const whatsApp = client.defineHttpEndpoint({ id: "whatsapp", source: "whatsapp.com", @@ -33,13 +31,11 @@ const whatsApp = client.defineHttpEndpoint({ request, secret: context.secret, headerName: "X-Signature-SHA256", + algorithm: "sha256", }); }, }); -//todo it would be nice if a filter could be added to an HttpTrigger -//then a webhook that subscribes to many events could be created and reused - client.defineJob({ id: "event-example-1", name: "Event Example 1", @@ -47,6 +43,7 @@ client.defineJob({ enabled: true, trigger: whatsApp.onRequest({ filter: { body: { event: ["message"] } } }), run: async (payload, io, ctx) => { + // ^? const { message } = payload.body; await io.logger.info(`Received message from ${message.from}`); },