Created HttpEndpoint as a top-level concept, it produces triggers using onRequest

This commit is contained in:
Matt Aitken
2023-10-23 14:40:24 +01:00
parent 504733e35c
commit 6cd7518720
6 changed files with 134 additions and 127 deletions
+7
View File
@@ -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,
]);
+117
View File
@@ -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<TEventSpecification extends EventSpecification<any>> = {
id: string;
event: TEventSpecification;
};
export type RequestOptions = {
filter?: EventFilter;
};
class HttpEndpoint<TEventSpecification extends EventSpecification<any>> {
constructor(private readonly options: HttpEndpointOptions<TEventSpecification>) {}
onRequest(options: RequestOptions): HttpTrigger<TEventSpecification> {
return new HttpTrigger({
endpointId: this.options.id,
event: this.options.event,
filter: options.filter,
});
}
}
type TriggerOptions<TEventSpecification extends EventSpecification<any>> = {
endpointId: string;
event: TEventSpecification;
filter?: EventFilter;
};
class HttpTrigger<TEventSpecification extends EventSpecification<any>>
implements Trigger<TEventSpecification>
{
constructor(private readonly options: TriggerOptions<TEventSpecification>) {}
toJSON(): TriggerMetadata {
return {
type: "httpendpoint",
endpointId: this.options.endpointId,
filter: this.options.filter,
};
}
get event() {
return this.options.event;
}
attachToJob(triggerClient: TriggerClient, job: Job<Trigger<TEventSpecification>, 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<typeof HttpEndpointPayloadSchema>;
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<Response>;
};
verify: (request: Request, context: RequestContext) => Promise<boolean>;
};
export function httpEndpoint(
options: EndpointOptions
): HttpEndpoint<EventSpecification<HttpEndpointPayload>> {
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;
},
},
});
}
+2 -2
View File
@@ -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<boolean> {
const headerValue = request.headers.get(headerName);
if (!headerValue) {
+6 -2
View File
@@ -55,6 +55,7 @@ import type {
TriggerPreprocessContext,
} from "./types";
import { HttpTriggerOptions, httpTrigger } from "./triggers/httpTrigger";
import { EndpointOptions, httpEndpoint } from "./httpEndpoint";
const registerSourceEvent: EventSpecification<RegisterSourceEventV2> = {
name: REGISTER_SOURCE_EVENT_V2,
@@ -472,8 +473,11 @@ export class TriggerClient {
return new DynamicTrigger(this, options);
}
defineHttpTrigger<TEvent extends any = any>(options: HttpTriggerOptions<TEvent>) {
return httpTrigger<TEvent>(options);
defineHttpEndpoint(options: EndpointOptions) {
const endpoint = httpEndpoint(options);
//todo have a record<string, HttpEndpoint>
//todo it so they're indexed
return endpoint;
}
attach(job: Job<Trigger<any>, any>): void {
@@ -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<TEventSpecification extends EventSpecification<any>> = {
id: string;
event: TEventSpecification;
};
export class HttpTrigger<TEventSpecification extends EventSpecification<any>>
implements Trigger<TEventSpecification>
{
constructor(private readonly options: Options<TEventSpecification>) {}
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<Trigger<TEventSpecification>, 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<TEvent> = {
id: string;
/** The hostname of the webhook, e.g. whatsapp.com */
hostname: string;
title?: string;
icon?: string;
bodySchema?: SchemaParser<TEvent>;
filter?: EventFilter;
examples?: EventSpecificationExample[];
properties?: DisplayProperty[];
respondWith?: {
filter: RequestFilter;
handler: (request: Request, context: RequestContext) => Promise<Response>;
};
verify: (request: Request, context: RequestContext) => Promise<boolean>;
/** Use this if you want to control the events created. */
transform?: (request: Request, context: RequestContext) => Promise<SendEvent[]>;
};
type HttpRequest<TBody> = {
headers: Record<string, string>;
method: HttpMethod;
searchParams: Record<string, string>;
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<TEvent extends any = any>(
options: HttpTriggerOptions<TEvent>
): Trigger<EventSpecification<Prettify<HttpRequest<TEvent>>>> {
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;
},
},
});
}
+2 -5
View File
@@ -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}`);
},