import { IncomingMessage, RequestListener } from "http"; import { Logger, ForbiddenError, routerDefaultLoggerFactory } from "../common.js"; import { Request, Response, ErrorHandler, Handler, Method, HandlerWithOptions, HandlerWithOptionsSchema, MinimalRouter, RouterHandlerOptions, RouterHandlerOptionsSchema, RouterJSONDoc, RouterOptions, isMinimalRouter, normalizePath, PathParams, PathPart, PathPartToken, matchTokenizePath, splitPath, tokenizePath, APIRouterOptions, APIRouterOptionsSchema, APIRoute, APIRouteSchema, MinimalLogger, removeStartingBackSlashes, SchemaProperties, } from "../types.js"; import { ServerOptions } from "https"; import { SessionHandler, GroupPolicyHandler } from "./session.js"; import { DEFAULT_PARSER, assertNotUndefined, ResponseHandler, ResultParser, ParseRequest } from "./body-parser.js"; import { RequestOptions, request } from "@miqro/request"; import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync } from "fs"; import { basename, extname, join, parse, resolve, sep } from "path"; import { parseEncodeHTML } from "@miqro/parser/build/built-in-parsers.js"; /** * Router Public */ export class Router implements MinimalRouter { protected readonly handlers: RouterHandler[] = []; protected readonly errorHandlers: ErrorHandler[] = []; public readonly listener: RequestListener; public serverOptions: ServerOptions = { IncomingMessage: Request, ServerResponse: Response } constructor(protected config?: RouterOptions) { const loggerFactory = this.config && this.config.loggerFactory ? this.config.loggerFactory : routerDefaultLoggerFactory; this.listener = async (req, res) => { req.logger = loggerFactory(req.uuid, req); if (this.config?.etag !== undefined) { res.setETag(this.config?.etag); } req.logger.trace(`request received`); await this.run(req, res); }; } public clear() { this.handlers.splice(0, this.handlers.length); this.errorHandlers.splice(0, this.errorHandlers.length); } public get< const TBody extends SchemaProperties | string | boolean | undefined = undefined, const TParams extends SchemaProperties | string | boolean | undefined = undefined, const TQuery extends SchemaProperties | string | boolean | undefined = undefined >(path: string | undefined, handler: Array | Handler | MinimalRouter | HandlerWithOptions, options?: RouterHandlerOptions): Router { return this.use(handler as any, path, "get", options); } public post< const TBody extends SchemaProperties | string | boolean | undefined = undefined, const TParams extends SchemaProperties | string | boolean | undefined = undefined, const TQuery extends SchemaProperties | string | boolean | undefined = undefined >(path: string | undefined, handler: Array | Handler | MinimalRouter | HandlerWithOptions, options?: RouterHandlerOptions): Router { return this.use(handler as any, path, "post", options); } public patch< const TBody extends SchemaProperties | string | boolean | undefined = undefined, const TParams extends SchemaProperties | string | boolean | undefined = undefined, const TQuery extends SchemaProperties | string | boolean | undefined = undefined >(path: string | undefined, handler: Array | Handler | MinimalRouter | HandlerWithOptions, options?: RouterHandlerOptions): Router { return this.use(handler as any, path, "patch", options); } public delete< const TBody extends SchemaProperties | string | boolean | undefined = undefined, const TParams extends SchemaProperties | string | boolean | undefined = undefined, const TQuery extends SchemaProperties | string | boolean | undefined = undefined >(path: string | undefined, handler: Array | Handler | MinimalRouter | HandlerWithOptions, options?: RouterHandlerOptions): Router { return this.use(handler as any, path, "delete", options); } public put< const TBody extends SchemaProperties | string | boolean | undefined = undefined, const TParams extends SchemaProperties | string | boolean | undefined = undefined, const TQuery extends SchemaProperties | string | boolean | undefined = undefined >(path: string | undefined, handler: Array | Handler | MinimalRouter | HandlerWithOptions, options?: RouterHandlerOptions): Router { return this.use(handler as any, path, "put", options); } public options< const TBody extends SchemaProperties | string | boolean | undefined = undefined, const TParams extends SchemaProperties | string | boolean | undefined = undefined, const TQuery extends SchemaProperties | string | boolean | undefined = undefined >(path: string | undefined, handler: Array | Handler | MinimalRouter | HandlerWithOptions, options?: RouterHandlerOptions): Router { return this.use(handler as any, path, "options", options); } public use< const TBody extends SchemaProperties | string | boolean | undefined = undefined, const TParams extends SchemaProperties | string | boolean | undefined = undefined, const TQuery extends SchemaProperties | string | boolean | undefined = undefined >(handler: Array | HandlerWithOptions | Handler | MinimalRouter, path?: string, method?: Method, options?: RouterHandlerOptions): Router { const nPath = path !== undefined ? normalizePath(path) : undefined; const nMethod = method ? method.toLowerCase() as Method : method; this.handlers.push(new RouterHandler(handler as any, nPath, nMethod, options)); return this; } public catch(errorHandler: Array | ErrorHandler): Router { const errorHandlers = errorHandler instanceof Array ? errorHandler : [errorHandler]; for (const e of errorHandlers) { if (typeof e !== "function") { throw new Error(`errorHandler ${e} not a function!`); } this.errorHandlers.push(e); } return this; } public async run(req: Request, res: Response, pathTokens?: PathPart[], prePathTokens: PathPartToken[] = []): Promise { //console.log("run(%s, %o)", req.path, pathTokens); pathTokens = pathTokens ? pathTokens : splitPath(req.path); //console.log("run(%s, %o)", req.path, pathTokens); try { for (const h of this.handlers) { if (res.headersSent) { break; } const isMatch = h.isMatch(req.method, pathTokens, prePathTokens); //console.log("h.isMatch(%s, %o, %o)=%s", h.path, pathTokens, prePathTokens, isMatch); if (isMatch.match) { if (isMatch.params) { req.params = req.params ? { ...req.params, ...isMatch.params } : isMatch.params; } const shouldContinue = await h.invoke(req, res, pathTokens, prePathTokens); if (!shouldContinue) { break; } } } return !res.headersSent; } catch (e: any) { if (!res.headersSent) { for (const eH of this.errorHandlers) { const ret = await eH(e, req, res); if (ret === false || res.headersSent) { return false; } } throw e; // pass the error along } else { return false; } } } public getJSONDoc(doc: RouterJSONDoc = {}, prePath = "/"): RouterJSONDoc { for (const h of this.handlers) { doc = h.getJSONDoc(doc, prePath); } return doc; } public logPaths(logger: MinimalLogger, filter?: (path: string, method: string) => boolean): void { this.toString(filter).split("\n").forEach(l => logger.info(l)); } public toString(filter?: (path: string, method: string) => boolean): string { let ret = ""; const doc = this.getJSONDoc(); const paths = Object.keys(doc); const tab = " "; for (const path of paths) { if (path) { const pathData = doc[path]; const methods = Object.keys(pathData); for (const method of methods) { if (method) { const show = filter ? filter(path, method) : true; if (show) { const methodDataList = pathData[method]; for (const methodData of methodDataList) { ret += `${ret === "" ? "" : "\n"}${String(method).toUpperCase()}:${path}\n${tab}${methodData.identifier}${methodData.name ? `\n${tab}${tab}${methodData.name}` : ""}${methodData.description ? `\n${tab}${tab}${methodData.description}` : ""}`; } } } } } } return ret; } } class RouterHandler { private readonly tokens: PathPartToken[]; private readonly handler: Array; private readonly isRouter: boolean; private readonly options?: RouterHandlerOptions; public constructor( private readonly originalHandler: Array | HandlerWithOptions | Handler | MinimalRouter, public readonly path: string | undefined, private readonly method: Method | undefined, options?: RouterHandlerOptions) { this.tokens = tokenizePath(path); if (options) { options = assertNotUndefined(DEFAULT_PARSER.parse(options, RouterHandlerOptionsSchema, "options"), "bad options"); } let handlers: Array = [] this.isRouter = isMinimalRouter(originalHandler); //handler instanceof Router; if (!this.isRouter && !(originalHandler instanceof Array) && typeof originalHandler === "object") { // HandlerWithOptions if (options) { throw new Error("bad arguments. cannot send options with HandlerWithOptions"); } //console.dir(handler); const handlerWithOptions = assertNotUndefined(DEFAULT_PARSER.parse(originalHandler, HandlerWithOptionsSchema, "handler"), "bad options"); this.originalHandler = handlerWithOptions.handler; this.options = handlerWithOptions; } else { this.options = options; } const routeHandlers = this.originalHandler instanceof Array ? this.originalHandler : [this.originalHandler] as Array; //if ((!this.isRouter && routeHandlers.length !== routeHandlers.filter(h => typeof h === "function").length) || (this.isRouter && this.options)) { if ((!this.isRouter && routeHandlers.length !== routeHandlers.filter(h => typeof h === "function").length)) { throw new Error(`bad arguments isRouter[${this.isRouter}]`); } if (this.options && this.options.middleware) { handlers = handlers.concat(this.options.middleware); } if (this.options && this.options.session) { handlers.push(typeof this.options.session !== "function" ? SessionHandler(this.options.session) : this.options.session); } if (this.options && this.options.policy) { handlers.push(GroupPolicyHandler(this.options.policy)); } if (this.options && this.options.request) { handlers.push(ParseRequest(this.options.request, this.options.parser)); } handlers = handlers.concat(routeHandlers); if (this.options && this.options.response && !this.isRouter) { const responseMiddleware = typeof this.options.response === "boolean" ? [ResponseHandler({ etag: true })] : this.options.response.middleware ? ( this.options.response.middleware instanceof Array ? this.options.response.middleware : [this.options.response.middleware] ) : [ResponseHandler({ etag: this.options?.response?.etag !== undefined ? this.options?.response?.etag : true })]; if (typeof this.options.response !== "boolean") { handlers.push(ResultParser(this.options.response, this.options.parser)); } handlers = handlers.concat(responseMiddleware); } this.handler = handlers; } public getJSONDoc(doc: RouterJSONDoc = {}, prePath = ""): RouterJSONDoc { const pp = normalizePath(removeStartingBackSlashes(this.path ? this.path : "/")); const nPath = pp.length > 1 ? pp.substring(1) : ""; const nMethod = this.method ? this.method : ""; if (this.isRouter) { const router: MinimalRouter = this.originalHandler as MinimalRouter; router.getJSONDoc(doc, `${prePath}${nPath}`); } else { if (!doc[`${prePath}${nPath}`]) { doc[`${prePath}${nPath}`] = Object.create(null); } if (!doc[`${prePath}${nPath}`][nMethod]) { doc[`${prePath}${nPath}`][nMethod] = []; } if (this.options) { doc[`${prePath}${nPath}`][nMethod].push({ description: this.options.description, name: this.options.name, identifier: this.options.identifier ? this.options.identifier : `${prePath}${nPath}${this.options.name ? this.options.name : ""}${nMethod ? `_${nMethod}` : ""}`.replace(/[^a-z0-9+]+/gi, '_').split("_").map(s => s.trim()).filter(s => s).join("_").toUpperCase(), policy: this.options.policy, request: this.options.request ? { body: this.options.request.body, headers: this.options.request.headers, params: this.options.request.params, query: this.options.request.query, } : undefined, response: typeof this.options.response === "boolean" ? this.options.response : this.options.response ? { body: this.options.response.body, bodyMode: this.options.response.bodyMode, headers: this.options.response.headers, headersMode: this.options.response.headersMode, middleware: this.options.response.middleware, status: this.options.response.status } : undefined }); } else { doc[`${prePath}${nPath}`][nMethod].push({ description: undefined, name: undefined, identifier: `${prePath}${nPath}${nMethod ? `_${nMethod}` : ""}`.replace(/[^a-z0-9+]+/gi, '_').split("_").map(s => s.trim()).filter(s => s).join("_").toUpperCase(), policy: undefined, request: undefined, response: undefined }); } } return doc; } public async invoke(req: Request, res: Response, pathTokens: PathPart[], prePathTokens: PathPartToken[]): Promise { for (let i = 0; i < this.handler.length; i++) { if (res.headersSent) { return false; } const h = this.handler[i]; if (isMinimalRouter(h)) { //if (h instanceof Router) { if (!RouterHandler.pushResults(await (h as MinimalRouter).run(req, res, pathTokens, prePathTokens.concat(this.tokens)), req)) { return false; } } else { if (!RouterHandler.pushResults(await (h as Handler)(req, res), req)) { return false; } } } return true; } public isMatch( method: string | undefined, requestParts: PathPart[], prePathTokens: PathPartToken[]): { match: boolean; params?: PathParams } { if ((this.method === undefined || this.method === method)) { if (this.path === undefined) { //console.log("isMatch(%s, %s, %s)=%s", method, this.method, this.path, true); return { match: true }; } const ret = matchTokenizePath(this.isRouter, prePathTokens.concat(this.tokens), requestParts); //console.log("isMatch(%s, %s, %s)=%s", method, this.method, this.path, ret); return ret; } //console.log("isMatch(%s, %s, %s)=%s", method, this.method, this.path, false); return { match: false }; } private static pushResults(result: any, req: Request): boolean { if (result === false) { // ctx.logger.debug(`avoiding next handlers because handler returned false.`); return false; } else if (result !== true && result !== undefined) { req.logger.trace("pushing to results [%o]", result); req.results.push(result); } return true; } } /** * Proxy */ export interface ProxyRouterOptions extends RouterOptions { url: string; rejectUnauthorized?: boolean; } class ProxyRouter extends Router { private readonly proxyURL: URL; private readonly rejectUnauthorized?: boolean; constructor(options: ProxyRouterOptions) { super(options); const proxyURL = new URL(options.url); proxyURL.pathname = normalizePath(proxyURL.pathname); this.proxyURL = proxyURL; this.rejectUnauthorized = options.rejectUnauthorized; } async run(req: Request, res: Response, pathTokens?: { value: string; lower: string }[], prePathTokens: PathPartToken[] = []): Promise { const level = prePathTokens.length; const shouldContinue = await super.run(req, res, pathTokens, prePathTokens); if (shouldContinue) { const relative = req.path.split("/").map(s => s.trim()).filter(s => s).slice(level).join("/"); const nUrl = new URL(this.proxyURL.toString()); nUrl.pathname = join(nUrl.pathname, relative); const url = nUrl.toString(); const headers = { ...req.headers }; delete headers["connection"]; delete headers["keep-alive"]; const requestArgs: RequestOptions = { url: url.toString(), query: req.query, disableThrow: true, rejectUnauthorized: this.rejectUnauthorized, disableUserAgent: true, method: req.method, headers, onChunk: async function (chunk: Uint8Array, chunkReq: IncomingMessage): Promise { const responseHeaders = { ...chunkReq.headers }; delete responseHeaders["connection"]; delete responseHeaders["keep-alive"]; if (!res.headersSent) { const headerNames = Object.keys(responseHeaders); for (const name of headerNames) { res.setHeader(name, responseHeaders[name] as any); } } if (chunkReq.statusCode !== undefined) { res.statusCode = chunkReq.statusCode } await res.asyncWrite(chunk); }, data: req.method !== "GET" ? req.buffer : undefined }; req.logger.info("proxy to %s", url.toString()); req.logger.debug("proxy with %o", requestArgs); const response = await request(requestArgs); if (!res.headersSent) { const responseHeaders = { ...response.headers }; const headerNames = Object.keys(responseHeaders); for (const name of headerNames) { res.setHeader(name, responseHeaders[name] as any); } } await res.end(); } return !res.headersSent; } } export function Proxy(options: ProxyRouterOptions) { return new ProxyRouter(options); } /** * Static Router */ async function outputFile(path: string, contentType: string, req: Request, res: Response, allowGetJSON: boolean, status = 200) { const buffer = readFileSync(path); return allowGetJSON && req.query.format && req.query.format === "json" ? res.json({ buffer }) : res.asyncEnd({ status, headers: { ["Content-Type"]: contentType }, body: buffer }); } async function generateList(path: string, req: Request, res: Response, allowListJSON: boolean) { const files = readdirSync(path); return allowListJSON && req.query.format && req.query.format === "json" ? res.json({ files: files.map(f => join(req.path, f)) }) : res.html(`
    ${files.map(f => { const escaped = parseEncodeHTML(f, { type: "encodeHTML" }) as string; return `
  • ${escaped}
  • `; }).join("")}
`); } export interface StaticRouterOptions extends RouterOptions { directory: string; list?: boolean; allowListJSON?: boolean; allowGetJSON?: boolean; index?: string; index404?: string; index404Status?: number; contentTypes?: any; defaultContentType?: string; } export function Static(options: StaticRouterOptions): Router { return new StaticRouter(options); } class StaticRouter extends Router { private readonly staticOptions: { directory: string; list: boolean; allowListJSON: boolean; allowGetJSON: boolean; index: string; index404?: string; index404Status: number; contentTypes: any; defaultContentType: string; }; constructor(options: StaticRouterOptions) { super(options); this.staticOptions = assertNotUndefined(DEFAULT_PARSER.parse(options, { type: "object", properties: { loggerFactory: { required: false, type: "any" }, directory: { type: "string", required: true }, list: { type: "boolean", required: false, defaultValue: false }, allowListJSON: { type: "boolean", required: false, defaultValue: false }, allowGetJSON: { type: "boolean", required: false, defaultValue: false }, index: { type: "string", required: false, defaultValue: "index.html" }, index404: { type: "string", required: false }, index404Status: { type: "number", required: false, defaultValue: 404 }, contentTypes: { type: "object", required: false, defaultValue: { ".aac": "audio/aac", ".abw": "application/x-abiword", ".arc": "application/x-freearc", ".avif": "image/avif", ".avi": "video/x-msvideo", ".azw": "application/vnd.amazon.ebook", ".bin": "application/octet-stream", ".bmp": "image/bmp", ".bz": "application/x-bzip", ".bz2": "application/x-bzip2", ".cda": "application/x-cdf", ".csh": "application/x-csh", ".css": "text/css", ".csv": "text/csv", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".eot": "application/vnd.ms-fontobject", ".epub": "application/epub+zip", ".gz": "application/gzip", ".gif": "image/gif", ".htm": "text/html", ".html": "text/html", ".ico": "image/vnd.microsoft.icon", ".ics": "text/calendar", ".jar": "application/java-archive", ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".js": "text/javascript", ".json": "application/json", ".jsonld": "application/ld+json", ".mid": "audio/midi audio/x-midi", ".midi": "audio/midi audio/x-midi", ".mjs": "text/javascript", ".mp3": "audio/mpeg", ".mp4": "video/mp4", ".mpeg": "video/mpeg", ".mpkg": "application/vnd.apple.installer+xml", ".odp": "application/vnd.oasis.opendocument.presentation", ".ods": "application/vnd.oasis.opendocument.spreadsheet", ".odt": "application/vnd.oasis.opendocument.text", ".oga": "audio/ogg", ".ogv": "video/ogg", ".ogx": "application/ogg", ".opus": "audio/opus", ".otf": "font/otf", ".png": "image/png", ".pdf": "application/pdf", ".php": "application/x-httpd-php", ".ppt": "application/vnd.ms-powerpoint", ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".rar": "application/vnd.rar", ".rtf": "application/rtf", ".sh": "application/x-sh", ".svg": "image/svg+xml", ".swf": "application/x-shockwave-flash", ".tar": "application/x-tar", ".tif": "image/tiff", ".tiff": "image/tiff", ".ts": "video/mp2t", ".ttf": "font/ttf", ".txt": "text/plain", ".vsd": "application/vnd.visio", ".wav": "audio/wav", ".weba": "audio/webm", ".webm": "video/webm", ".webp": "image/webp", ".woff": "font/woff", ".woff2": "font/woff2", ".xhtml": "application/xhtml+xml", ".xls": "application/vnd.ms-excel", ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xml": "application/xml", ".xul": "application/vnd.mozilla.xul+xml", ".zip": "application/zip", ".3gp": "video/3gpp; audio/3gpp", ".3g2": "video/3gpp2; audio/3gpp2", ".7z": "application/x-7z-compressed" } }, defaultContentType: { type: "string", required: false, defaultValue: "application/octet-stream" } } }, "options"), "bad options"); if (!statSync(this.staticOptions.directory).isDirectory()) { throw new Error(`options.directory ${this.staticOptions.directory} not a directory`); } if (this.staticOptions.index404 && !existsSync(resolve(this.staticOptions.directory, this.staticOptions.index404))) { throw new Error(`options.index404 ${this.staticOptions.index404} not a found`); } } public async run(req: Request, res: Response, pathTokens?: { value: string; lower: string; }[], prePathTokens: PathPartToken[] = []): Promise { const { directory, allowGetJSON, allowListJSON, list, index, contentTypes, defaultContentType } = this.staticOptions; const level = prePathTokens.length; if (String(req.method).toLowerCase() === "get") { const realPath = resolve(directory, req.path.split("/").map(s => s.trim()).filter(s => s).slice(level).join("/")); // defense-in-depth: ensure resolved path stays within the served directory if (!realPath.startsWith(resolve(directory))) { throw new ForbiddenError(); } req.logger.debug("static get %s", realPath); if (existsSync(realPath)) { const resolvedDir = realpathSync(resolve(directory)); const resolvedPath = realpathSync(realPath); if (!resolvedPath.startsWith(resolvedDir + sep) && resolvedPath !== resolvedDir) { throw new ForbiddenError(); } if (statSync(realPath).isDirectory()) { // directory const indexPath = resolve(realPath, index); if (index && existsSync(indexPath) && statSync(indexPath).isFile()) { // index file exists const resolvedIndex = realpathSync(indexPath); if (!resolvedIndex.startsWith(resolvedDir + sep)) { throw new ForbiddenError(); } const ext = extname(indexPath); const contentType = contentTypes[ext] ? contentTypes[ext] : defaultContentType; await outputFile(indexPath, contentType, req, res, allowGetJSON); } else { // index doesn't exists if (list) { // generate list await generateList(realPath, req, res, allowListJSON); } } } else { // direct file access const ext = extname(realPath); const contentType = contentTypes[ext] ? contentTypes[ext] : defaultContentType; await outputFile(realPath, contentType, req, res, allowGetJSON); } } } if (!res.headersSent && this.staticOptions.index404) { const realPath = resolve(this.staticOptions.directory, this.staticOptions.index404); if (existsSync(realPath)) { const ext = extname(realPath); const contentType = contentTypes[ext] ? contentTypes[ext] : defaultContentType; await outputFile(realPath, contentType, req, res, allowGetJSON, this.staticOptions.index404Status); } } return !res.headersSent ? super.run(req, res, pathTokens, prePathTokens) : false; } } /** * # APIRouter * * src/server.js * ```typescript *import { App, APIRouter } from "@miqro/core"; *const app = new App(); *app.use(APIRouter({ dirname: resolve(__dirname, "api") })); *app.listen(8080); * ``` * * src/api/user/post.js * * ```typescript *import { ReadBuffer, JSONParser, APIRoute } from "@miqro/core"; * *const route: APIRoute = { * middleware: [ReadBuffer(), JSONParser()], * request: { * query: { ... }, * body: { ... }, * ... * }, * response: { * ... * }, * handler: async (ctx: Context) => { * return { * status: 200, * body: { * status: "OK" * } * }; * } *} *export default route; * ``` */ export async function APIRouter(options: APIRouterOptions, logger?: Logger): Promise { const { dirname } = assertNotUndefined(DEFAULT_PARSER.parse(options, APIRouterOptionsSchema, "options"), "bad options"); if (typeof dirname !== "string") { throw new Error(`options.dirname must be defined!`); } if (!existsSync(dirname) || !lstatSync(dirname).isDirectory()) { throw new Error(`${dirname} not a directory!`); } const apiName = options.apiName !== undefined ? options.apiName : basename(dirname); const apiPath = options.path !== undefined ? options.path : ""; return await traverseAPIRouteDir(apiName.toUpperCase(), dirname, apiPath, undefined, logger, options.ignore, options.loader, options.extensions); } type UseCallArg = [Handler | Handler[] | MinimalRouter, string | undefined, Method | undefined, RouterHandlerOptions]; function isIgnored(f: string, ignore?: Array): boolean { if (!ignore) { return false; } for (const i of ignore) { if (i instanceof RegExp && i.test(f)) { return true; } else if (i === f) { return true; } } return false; } const DEFAULTAPIValidExtensions = new Set([".mjsx", ".mts", ".mjs", ".tsx", ".jsx", ".ts", ".js"]); async function traverseAPIRouteDir( featureName: string, dirname: string, basePath = "/", router: Router = new Router(), logger?: Logger, ignore?: Array, importFN?: (path: string) => Promise, extensions?: string[]): Promise { if (logger) { logger.trace("loading routes from [%s]", dirname); } const VALID_EXTENSION = extensions ? new Set(extensions) : DEFAULTAPIValidExtensions; const files = readdirSync(dirname).filter(f => !isIgnored(f, ignore)); const dirs = files.filter(f => statSync(join(dirname, f)).isDirectory()); const methods = files.filter(f => !statSync(join(dirname, f)).isDirectory()); const postFolder: UseCallArg[] = []; for (const f of methods) { const { name, ext } = parse(f); if (VALID_EXTENSION.has(ext) && name.slice(-2) !== ".d") { const filePath = join(dirname, f); //console.log("dirname=%s f=%s", dirname, f); let route: APIRoute = importFN ? await importFN(filePath) : await import(filePath); //if ((route as any).default && (route as any).__esModule === true) { if ((route as any).default) { route = (route as any).default; // ugly hack when import is importing a commonjs that exports default if ((route as any).default) { route = (route as any).default; } } //const ___filePath = filePath + ext; const ___filePath = filePath; //let routeMethods: Method[] = [name] as Method[]; let routeMethod: Method[] | Method | "use" | "USE" = name as Method; if (typeof route === "function") { route = { handler: route } as any; } else { if (typeof route.method !== "undefined" && (!(route.method instanceof Array) && (typeof route.method !== "string"))) { //throw new Error(`${resolve(dirname, name)} doesn't export method as a string or string array`); throw new Error(`${resolve(dirname, name)} doesnt export method as a string`); } else if (typeof route.method !== "undefined") { routeMethod = route.method;//route.method instanceof Array ? route.method : [route.method]; } if (typeof route.path !== "string" && typeof route.path !== "undefined" && route.path !== null && !(route.path instanceof Array)) { //throw new Error(`${resolve(dirname, name)} doesn't export path as a string or a string array`); throw new Error(`${resolve(dirname, name)} doesnt export path as a string`); } if ((!(route.handler instanceof Array) && typeof route.handler !== "function" && !isMinimalRouter(route.handler)) || typeof route.handler === "undefined") { throw new Error(`${resolve(dirname, name)} doesnt export handler as a function or an array`); } if (typeof route.name !== "string" && typeof route.name !== "undefined") { throw new Error(`${resolve(dirname, name)} doesnt export name as a string`); } } try { route = assertNotUndefined(DEFAULT_PARSER.parse(route, APIRouteSchema, ___filePath), `error parsing [${___filePath}]`); } catch (e) { console.error("error parsing " + ___filePath); throw e; } if (route.ignore !== undefined && route.ignore === true) { continue; } //console.dir(route); if (route.init) { await route.init(route); } //console.dir(route); const p = route.path === undefined ? route.method && route.method.indexOf(name as Method) === -1 ? `${name}` : "/" : route.path; /*if (p !== null && (typeof p !== "string" && !p)) { throw new Error(`${resolve(dirname, name)} doesnt export path as a string or a string array`); }*/ const paths = p instanceof Array ? p : [p]; const methods = routeMethod instanceof Array ? routeMethod : [routeMethod]; for (const method of methods) { for (const pathsP of paths) { const path = pathsP === null ? undefined : normalizePath(`${basePath}${pathsP && pathsP !== "/" ? `/${pathsP}` : pathsP}`) if (route.identifier && (route.identifier === "__prototype__" || route.identifier === "__proto__")) { throw new Error(`invalid feature name ${route.identifier}`); } const useCallArgs: UseCallArg = [route.handler as Handler | Handler[] | MinimalRouter, path, method !== "use" && method !== "USE" ? method : undefined, { identifier: route.identifier, apiName: route.apiName, name: route.name, description: route.description, parser: route.parser, middleware: route.middleware, session: route.session, policy: route.policy, request: route.request as any, response: route.response !== undefined ? route.response : true // ResponseHandler added by default to an APIRoute }]; //console.dir(useCallArgs); if (route.postFolder) { postFolder.push(useCallArgs); } else { router.use(...useCallArgs); } } } } } for (const f of dirs) { const { name } = parse(f); router = await traverseAPIRouteDir(`${featureName}_${name}`.toUpperCase(), join(dirname, f), `${basePath}/${name}`, router, logger, ignore, importFN, extensions); } for (const post of postFolder) { router.use(...post); } return router; };