Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple middleware option #695

Open
tg44 opened this issue Jun 29, 2022 · 2 comments
Open

Multiple middleware option #695

tg44 opened this issue Jun 29, 2022 · 2 comments
Labels
Type: Feature New feature or request

Comments

@tg44
Copy link

tg44 commented Jun 29, 2022

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}
      )
}
@tg44 tg44 added the Type: Feature New feature or request label Jun 29, 2022
@wolfy1339
Copy link
Member

You can see here for some other middleware examples: https://github.com/octokit/oauth-app.js/tree/master/src/middleware

@tg44
Copy link
Author

tg44 commented Jun 30, 2022

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...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Feature New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants