microfleet/core

View on GitHub
packages/plugin-router/src/utils.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { sep, resolve } from 'node:path'
import { glob } from 'glob'
import { ValidationError } from 'common-errors'

import { ServiceAction } from './types/router'

export async function readRoutes(directory: string): Promise<[string, ServiceAction][]> {
  const files = await glob('*.{js,ts,mjs,mts,cjs,cts}', {
    cwd: directory,
    matchBase: true,
    ignore: ['*.d.ts', '**/*.d.ts', '*.d.mts', '**/*.d.mts', '*.d.cts', '**/*.d.cts']
  })

  const routes: [string, ServiceAction][] = []

  for (const file of files) {
    // remove .js/.ts from route
    const route = file.slice(0, -3)
    // replace / with . for route
    const routeKey = route.split(sep).join('.')

    routes.push([
      routeKey,
      await requireServiceActionHandler(resolve(directory, file))
    ])
  }

  return routes
}

export const transformFileToAction = (input: any, handler?: any): ServiceAction => {
  const omitProps = ['length', 'name', 'default', '__esModule']
  const props = Object.getOwnPropertyNames(input)
  const action = Object.create(null)
  for (const prop of props) {
    if (!omitProps.includes(prop)) {
      action[prop] = input[prop]
    }
  }

  if (handler) {
    action.handler = handler
    const handlerProps = Object.getOwnPropertyNames(handler)
    for (const prop of handlerProps) {
      if (!omitProps.includes(prop)) {
        action[prop] = handler[prop]
      }
    }
  } else {
    action.handler = input
  }

  return action
}

export async function requireServiceActionHandler(path: string): Promise<ServiceAction> {
  const module = await import(path)
  const baseAction = module.default || module

  // debug
  if (typeof baseAction === 'function') {
    return transformFileToAction(baseAction)
  }

  if (baseAction && typeof baseAction.default === 'function') {
    return transformFileToAction(baseAction, baseAction.default)
  }

  // native ESM or we have specified handler without default export
  if (baseAction && typeof baseAction.handler === 'function') {
    return { ...baseAction } // copy as we can't mutate original exports
  }

  throw new Error(`action from ${path} must be a function`)
}

export function createServiceAction(route: string, action: ServiceAction): ServiceAction {
  const { allowed, auth, schema, transports } = action

  action.actionName = route

  if (action.schema === undefined) {
    action.schema = route
  }

  if (action.responseSchema === undefined) {
    action.responseSchema = `response.${action.actionName}`
  }

  if (allowed !== undefined && typeof allowed !== 'function') {
    throw new ValidationError(`action.allowed in ${String(route)} must be a function`)
  }

  if (auth !== undefined && (typeof auth !== 'string' && typeof auth !== 'object')) {
    throw new ValidationError(`action.auth in ${String(route)} must be a string or an object`)
  }

  if (schema !== undefined && typeof schema !== 'string' && schema !== null && schema !== false) {
    throw new ValidationError(`action.schema in ${String(route)} must be a string`)
  }

  if (transports !== undefined && !Array.isArray(transports)) {
    throw new ValidationError(`action.transports in ${String(route)} must be an array`)
  }

  return action
}