UNPKG

@zeushq/nextjs-zapi

Version:

Next.js SDK for creating a Zeus API

228 lines (176 loc) 5.49 kB
import createDebug from '../zapi/utils/debug'; import { NextApiHandler } from 'next'; const debug = createDebug('router'); export enum TreeNodeType { HTTP_METHOD, STATIC, PARAM } export type Tree = { [method: string]: TreeNode }; export interface Route { method: string; path: string; } export interface TreeNode { value: string; type: TreeNodeType; children: { [key: string]: TreeNode }; handler?: NextApiHandler; } export interface RouterOptions { caseSensitive?: boolean; } export interface RouteLookupResult { params: Record<string, unknown>; handler: NextApiHandler; } export class Router { routes = [] as Route[]; tree = {} as Tree; caseSensitive = false; public constructor(options?: RouterOptions) { if (options !== undefined) { if (options.caseSensitive !== undefined) this.caseSensitive = options.caseSensitive; } } public print(): string | undefined { // const methods = Object.keys(this.tree); let out = ''; for (let i = 0; i < this.routes.length; i++) { const r = this.routes[i]; out += `${r.method.padStart(7, ' ')} ${r.path}\n`; } // for (let i = 0; i < methods.length; i++) { // const method = methods[i]; // const currNode = this.tree[method]; // // out += (`${method}:\n`); // out = this._print(out, currNode, 1); // } // debug(out); debug(out); return out; } public _print(out: string, node: TreeNode, depth = 1): string { const keys = Object.keys(node.children); out += `${Array(depth).join(' ')} - ${node.value}\n`; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const currNode = node.children[key]; out = this._print(out, currNode, depth + 1); } return out; } public on(method: string | string[], path: string, handler: NextApiHandler): void { if (Array.isArray(method)) { for (let i = 0; i < method.length; i++) { this.on(method[i], path, handler); } return; } let methodNode = this.tree[method]; if (methodNode === undefined) { methodNode = this.tree[method] = { value: method, type: TreeNodeType.HTTP_METHOD, children: {} }; } let pathElements = path.split('/'); if (pathElements[0] === '') pathElements = pathElements.splice(1, pathElements.length - 1); this._on(methodNode, pathElements, handler); this.routes.push({ method, path }); } private _on(node: TreeNode, pathElements: string[], handler: NextApiHandler): void { const nextPathEl = pathElements.shift(); if (nextPathEl === undefined) return; const isParam = nextPathEl.startsWith(':'); const elName = isParam ? nextPathEl.substring(1) : nextPathEl; let nextNode = node.children[elName]; if (!nextNode) { nextNode = node.children[nextPathEl] = { value: elName, type: isParam ? TreeNodeType.PARAM : TreeNodeType.STATIC, children: {}, handler: pathElements.length === 0 ? handler : undefined }; } if (pathElements.length > 0) { this._on(nextNode, pathElements, handler); } } public lookup(method: string | undefined, path: string | undefined): RouteLookupResult | undefined { if (!method) return undefined; if (!path) return undefined; const methodNode = this.tree[method]; if (!methodNode) return undefined; let pathElements = path.split('/'); if (pathElements[0] === '') pathElements = pathElements.splice(1, pathElements.length - 1); const params = {}; const handler = this._lookup(methodNode, pathElements, params); if (!handler) return undefined; return { params, handler }; } private parseInt(value: string) { for (let i = 0; i < value.length; i++) { if (value.charAt(i) < '0' || value.charAt(i) > '9') return undefined; } return parseInt(value); } private parseFloat(value: string) { try { return parseFloat(value); } catch { return undefined; } } private _transform(value: string) { if (value === 'true') { return true; } if (value === 'false') { return false; } const intVal = this.parseInt(value); if (!!intVal) { return intVal; } const floatVal = this.parseFloat(value); if (!!floatVal) { return floatVal; } return value; } public _lookup( node: TreeNode, pathElements: string[], params: { [key: string]: string | number | boolean | null } ): NextApiHandler | undefined { const nextPathEl = pathElements.shift(); if (nextPathEl === undefined) return undefined; const nPathLeft = pathElements.length; // Match static const nextNode = node.children[nextPathEl]; if (nextNode !== undefined) { switch (nPathLeft) { case 0: return nextNode.handler; default: return this._lookup(nextNode, pathElements, params); } } // Match dynamic const childrenKeys = Object.keys(node.children); for (let i = 0; i < childrenKeys.length; i++) { const key = childrenKeys[i]; const nextNode = node.children[key]; if (nextNode.type !== TreeNodeType.PARAM) continue; params[nextNode.value] = this._transform(nextPathEl); if (nPathLeft === 0) { return nextNode.handler; } else { return this._lookup(nextNode, pathElements, params); } } return undefined; } }