Created HttpEndpoint as a top-level concept, it produces triggers using onRequest
This commit is contained in:
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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}`);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user