XPBytes/express-routes-archive

View on GitHub
src/RoutesArchive.ts

Summary

Maintainability
A
0 mins
Test Coverage
import * as nodeUrl from 'url'

import { Request } from 'express'
import { RouteNotRegistered } from './RouteNotRegistered'
export { RouteNotRegistered }

export type PathGenerator =
  | string
  | ((mountedAt: string, ...args: unknown[]) => string)
type RouteGenerator = (args: unknown[]) => string

export interface RoutesArchiveBase {
  mountedAt: undefined | string
  routes: { [P: string]: RouteGenerator }
}

const READ_ONLY_ROUTES = {}
const EMPTY_BASE: RoutesArchiveBase = {
  mountedAt: undefined,
  routes: READ_ONLY_ROUTES
}

export class RoutesArchive implements RoutesArchiveBase {
  public mountedAt: string
  public routes: { [P: string]: RouteGenerator }

  private ssl: boolean
  private serverUrl?: string

  /**
   * Creates an instance of RoutesArchive.
   *
   * @param {RoutesArchiveBase} [base=EMPTY_BASE]
   * @param {string} [mountedAt] path this is mounted at
   * @memberof RoutesArchive
   */
  constructor(
    mountedAt?: string,
    base: RoutesArchiveBase = EMPTY_BASE,
    ssl: string | boolean | undefined = process.env.SSL_ENABLED,
    serverUrl: string | undefined = process.env.SERVER_URL
  ) {
    this.mountedAt = [base.mountedAt, mountedAt].filter(Boolean).join('')
    this.routes = base.routes === READ_ONLY_ROUTES ? {} : base.routes

    this.ssl = !!ssl
    this.serverUrl = serverUrl
  }

  /**
   * Register a route
   *
   * @param {string} route name of the route
   * @param {PathGenerator} generatePath the function that generates the correct path, given a set of arguments. If the
   *  path is static, may also be a static string. When called, will prefix with all the {mountedAt} up the chain.
   *
   * @memberof RoutesArchive
   */
  public register(route: string, generatePath: PathGenerator): void {
    this.routes[route] = (args: unknown[]) => {
      return this.callOrReturn(generatePath, this.mountedAt, ...args).toString()
    }
  }

  /**
   * Generates the URL for a route
   *
   * @param {string} route the route to generate for
   * @param {Request} req the current request (used if serverUrl is not set)
   * @param {...any[]} args the arguments to pass to the route generation
   * @returns {nodeUrl.URL} the url
   * @memberof RoutesArchive
   */
  public url(route: string, req: Request, ...args: unknown[]): nodeUrl.URL {
    const baseUrl =
      this.serverUrl ||
      nodeUrl.format({
        hostname: req.hostname,
        port: req.socket.localPort,
        protocol: this.ssl ? 'https' : 'http'
      })

    return new nodeUrl.URL(`.${this.path(route, ...args)}`, baseUrl)
  }

  /**
   * Generates the path for a route
   *
   * @param {string} route the route to generate for
   * @param {...any[]} args the arguments to pass to the route generation
   * @returns {string} the path
   * @memberof RoutesArchive
   */
  public path(route: string, ...args: unknown[]): string {
    this.assertExists(route)
    return this.routes[route](args)
  }

  private assertExists(route: string) {
    if (!this.routes[route]) {
      throw new RouteNotRegistered(route, Object.keys(this.routes))
    }
  }

  private callOrReturn(opt: PathGenerator, mountedAt: string, ...args: unknown[]) {
    if (typeof opt === 'string' || typeof opt === 'object') {
      return mountedAt + opt
    }

    return opt(mountedAt, ...args)
  }
}