webhooks.js
webhooks.js copied to clipboard
Multiple middleware option
What’s missing?
The lib should handle/integrate more http frameworks/toolkits. (like node, express, deno, sunder) Or at least it should allow the users to write integrations easily to it.
Why?
To use and deploy in multiple cloud infrastructures (like cloudflare workers, aws lambda, etc.)
Alternatives you tried
We started to discuss this in #693 .
I started to play with my idea described here, it would look sth like this;
//neccess boilerplate for later compat.
enum HttpMethod {
GET = "GET",
POST = "POST",
DELETE = "DELETE",
OPTION = "OPTION",
PUT = "PUT",
UNKNOWN = "UNKNOWN",
}
type HttpMethodStrings = keyof typeof HttpMethod;
//typeclass definition (RRP as request-response-pair)
interface MiddlewareIOHelper<RRP> {
getHeader(i: RRP, name: string): string;
getBodyAsJson(i: RRP): Promise<unknown>;
getUrlPath(i: RRP): string;
getMethod(i: RRP): HttpMethodStrings;
respondWith(o: RRP, body?: string, status?: number, headers?: Record<string, string>): void;
handleUnknownRoute(o: RRP): void;
}
//this handles the application logic
export async function middleware<RRP>(
webhooks: Webhooks<RRP>,
options: Required<MiddlewareOptions>,
handler: NodeMiddlewareIOHelper<RRP>,
rrp: RRP
) {
...
}
Implemented to node;
type RRP<I, O> = {
request: I,
response: O,
next?: Function,
}
type NodeRRP = RRP<IncomingMessage, ServerResponse>
//typeclass instance for node middleware
function NodeMiddlewareIOHelper(onUnhandledRequest: (request: IncomingMessage, response: ServerResponse) => void = onUnhandledRequestDefault): MiddlewareIOHelper<NodeRRP> {
return {
getBodyAsJson(i: NodeRRP): Promise<unknown> {
return new Promise((resolve, reject) => {
let data = "";
i.request.setEncoding("utf8");
// istanbul ignore next
i.request.on("error", (error: Error) => reject(new AggregateError([error])));
i.request.on("data", (chunk: string) => (data += chunk));
i.request.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (error: any) {
error.message = "Invalid JSON";
error.status = 400;
reject(new AggregateError([error]));
}
});
});
},
getHeader(i: NodeRRP, name: string): string {
return i.request.headers[name] as string;
},
getMethod(i: NodeRRP): HttpMethodStrings {
const indexOf = Object.values(HttpMethod).indexOf((i.request.method || "") as unknown as HttpMethod);
const key = (indexOf >= 0) ? Object.keys(HttpMethod)[indexOf] : "UNKNOWN";
return key as HttpMethodStrings;
},
getUrlPath(i: NodeRRP): string {
return new URL(i.request.url as string, "http://localhost").pathname;
},
respondWith(o: NodeRRP, body: string | undefined, status: number | undefined, headers: Record<string, string> | undefined): void {
if (!headers && status) {
o.response.statusCode = status;
} else if (status) {
o.response.writeHead(status, headers);
}
o.response.end(body || "");
},
handleUnknownRoute(rrp: NodeRRP) {
const isExpressMiddleware = typeof rrp.next === "function";
if (isExpressMiddleware) {
rrp.next!();
} else {
onUnhandledRequest(rrp.request, rrp.response);
}
}
};
}
//this connects the dots for node
export function createNodeMiddleware(
webhooks: Webhooks<IncomingMessage>,
{
path = "/api/github/webhooks",
onUnhandledRequest = onUnhandledRequestDefault,
log = createLogger(),
}: MiddlewareOptions = {}
) {
const handler = NodeMiddlewareIOHelper(onUnhandledRequest)
const fun = (request: IncomingMessage, response: ServerResponse, next?: Function) =>
middleware<NodeRRP>(
webhooks,
{
path,
log,
} as Required<MiddlewareOptions>,
handler,
{request, response, next}
)
}
Implement to sunder would be sth like;
type SunderRRP = Context
function SunderMiddlewareIOHelper(onUnhandledRequest: (context: Context) => void = onUnhandledRequestDefaultSunder): MiddlewareIOHelper<SunderRRP> {
...
}
//this connects the dots for node
export function createSunderMiddleware(
webhooks: Webhooks<IncomingMessage>,
{
path = "/api/github/webhooks",
onUnhandledRequest = onUnhandledRequestDefaultSunder,
log = createLogger(),
}: MiddlewareOptions = {}
) {
const handler = SunderMiddlewareIOHelper(onUnhandledRequest)
const fun = (context: Context, next?: Function) =>
middleware<SunderRRP>(
webhooks,
{
path,
log,
} as Required<MiddlewareOptions>,
handler,
{context, next}
)
}
You can see here for some other middleware examples: https://github.com/octokit/oauth-app.js/tree/master/src/middleware
Yapp, this is basically the same idea, the linked one is the OOP method (repack the object to a common object/interface), mine is more FP (generic type the thing, and give in the functions which can provide the common interface to the given type).
Both have pros and cons. Probably the OOP method is faster to understand, but the FP is usually more flexible (which is some cases not matters at all). In this specific case we could type-safely drag the request between layers without loosing any information which is a big plus if you asks me. When you call function parseRequest(request: APIGatewayProxyEventV2): OctokitRequest you instantly loose a lot of inner data from APIGatewayProxyEventV2, you can add an original: any to OctokitRequest but in that case we loose the typesafety...