teamdigitale/italia-ts-commons

View on GitHub
src/request_middleware.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import * as express from "express";

import * as E from "fp-ts/lib/Either";
import * as TE from "fp-ts/lib/TaskEither";
import { pipe } from "fp-ts/lib/function";

import { IResponse, ResponseErrorInternal } from "./responses";
import { Head, Tail, HasTail } from "./types";

export type RequestHandler<R> = (
  request: express.Request
) => Promise<IResponse<R>>;

/**
 * Transforms a typesafe RequestHandler into an Express Request Handler.
 *
 * Failed promises will be mapped to 500 errors handled by ResponseErrorGeneric.
 */
export const wrapRequestHandler =
  <R>(handler: RequestHandler<R>) =>
  (
    request: express.Request,
    response: express.Response,
    _: express.NextFunction
  ): Promise<void> =>
    handler(request).then(
      (r) => {
        r.apply(response);
      },
      (e) => {
        ResponseErrorInternal(e).apply(response);
      }
    );

/**
 * Interface for implementing a request middleware.
 *
 * A RequestMiddleware is just a function that validates a request or
 * extracts some object out of it.
 * The middleware returns a promise that will resolve to a value that gets
 * passed to the handler.
 * In case the validation fails, the middleware rejects the promise (the
 * value of the error is discarded). In this case the processing of the
 * following middlewares will not happen.
 * Finally, when called, the middleware has full access to the request and
 * the response objects. Access to the response object is particulary useful
 * for returning error messages when the validation fails.
 */
export type IRequestMiddleware<R, T> = (
  request: express.Request
) => Promise<E.Either<IResponse<R>, T>>;

export type MiddlewareFailureResult<T> = T extends IRequestMiddleware<
  infer R,
  unknown
>
  ? R extends IResponse<infer Res>
    ? Res
    : R
  : never;

export type MiddlewareResult<T> = T extends IRequestMiddleware<unknown, infer R>
  ? R
  : never;

export type MiddlewareFailure<M extends IRequestMiddleware<unknown, unknown>> =
  M extends IRequestMiddleware<infer F, unknown> ? F : never;

export type MiddlewareFailures<
  T extends ReadonlyArray<IRequestMiddleware<unknown, unknown>>
> = {
  // eslint-disable-next-line @typescript-eslint/naming-convention
  readonly 0: readonly [MiddlewareFailure<Head<T>>];
  // eslint-disable-next-line @typescript-eslint/naming-convention
  readonly 1: readonly [
    MiddlewareFailure<Head<T>>,
    ...MiddlewareFailures<Tail<T>>
  ];
}[HasTail<T> extends true ? 1 : 0];

export type MiddlewareResults<
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  T extends ReadonlyArray<IRequestMiddleware<any, any>>
> = {
  // eslint-disable-next-line @typescript-eslint/naming-convention
  readonly 0: readonly [MiddlewareResult<Head<T>>];
  // eslint-disable-next-line @typescript-eslint/naming-convention
  readonly 1: readonly [
    MiddlewareResult<Head<T>>,
    ...MiddlewareResults<Tail<T>>
  ];
}[HasTail<T> extends true ? 1 : 0];

export type TypeOfArray<T extends ReadonlyArray<unknown>> = {
  // eslint-disable-next-line @typescript-eslint/naming-convention
  readonly 0: Head<T>;
  // eslint-disable-next-line @typescript-eslint/naming-convention
  readonly 1: Head<T> | TypeOfArray<Tail<T>>;
}[HasTail<T> extends true ? 1 : 0];

export type WithRequestMiddlewaresT = <
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  M extends ReadonlyArray<IRequestMiddleware<any, any>>
>(
  ...middlewares: M
) => <RH>(
  handler: (...values: MiddlewareResults<M>) => Promise<IResponse<RH>>
) => RequestHandler<
  | RH
  | "IResponseErrorInternal"
  | TypeOfArray<MiddlewareFailures<typeof middlewares>>
>;

export const withRequestMiddlewares: WithRequestMiddlewaresT =
  (...middlewares) =>
  (handler) =>
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
  (request) =>
    pipe(
      middlewares.map((middleware) =>
        pipe(
          TE.tryCatch(
            () => middleware(request),
            (_) => ResponseErrorInternal(`error executing middleware`)
          ),
          TE.chain(TE.fromEither)
        )
      ),
      TE.sequenceSeqArray,
      TE.chain((params) =>
        pipe(
          TE.tryCatch(
            () => handler(...(params as MiddlewareResults<typeof middlewares>)),
            E.toError
          ),
          TE.mapLeft((err) =>
            ResponseErrorInternal(
              `Error executing endpoint handler: ${err.message}`
            )
          )
        )
      ),
      TE.toUnion
    )();