UNPKG

rjweb-server

Version:

Easy and Robust Way to create a Web Server with Many Easy-to-use Features in NodeJS

229 lines (228 loc) 10 kB
import Route from "../Route"; import parseURL from "../../functions/parseURL"; import RateLimit from "./RateLimit"; import path from "path"; import { filesystem, object } from "@rjweb/utils"; import Path from "./Path"; export default class File { computePath(path) { if (path instanceof RegExp) return path; return parseURL(this.prefix.concat('/', path)).path; } /** * Create a new File Loader * @since 9.0.0 */ constructor(prefix, global, validators = [], ratelimits, promises, openApi) { this.validators = validators; this.prefix = prefix; this._global = global; if (ratelimits) { this._httpRatelimit = ratelimits[0]; this._wsRatelimit = ratelimits[1]; } else { this._httpRatelimit = new RateLimit()['data']; this._wsRatelimit = new RateLimit()['data']; } this.promises = promises ?? []; this.openApi = openApi ?? {}; } /** * Add OpenAPI Documentation to all HTTP Endpoints in this Path (and all children) * @since 9.0.0 */ document(item) { this.openApi = object.deepMerge(this.openApi, item); return this; } /** * Add a Ratelimit to all HTTP Endpoints in this Path (and all children) * * When a User requests an Endpoint, that will count against their hit count, if * the hits exceeds the `<maxHits>` in `<timeWindow>ms`, they wont be able to access * the route for `<penalty>ms`. * @example * ``` * import { time } from "@rjweb/utils" * * server.path('/', (path) => path * .httpRateLimit((limit) => limit * .hits(5) * .window(time(20).s()) * .penalty(0) * ) // This will allow 5 requests every 20 seconds * .http('GET', '/hello', (ws) => ws * .onRequest(async(ctr) => { * ctr.print('Hello bro!') * }) * ) * ) * * server.rateLimit('httpRequest', (ctr) => { * ctr.print(`Please wait ${ctr.getRateLimit()?.resetIn}ms!!!!!`) * }) * ``` * @since 9.0.0 */ httpRatelimit(callback) { const limit = new RateLimit(); limit['data'] = Object.assign({}, this._httpRatelimit); callback(limit); this._httpRatelimit = limit['data']; return this; } /** * Add a Ratelimit to all WebSocket Endpoints in this Path (and all children) * * When a User sends a message to a Socket, that will count against their hit count, if * the hits exceeds the `<maxHits>` in `<timeWindow>ms`, they wont be able to access * the route for `<penalty>ms`. * @example * ``` * import { time } from "@rjweb/utils" * * server.path('/', (path) => path * .httpRateLimit((limit) => limit * .hits(5) * .window(time(20).s()) * .penalty(0) * ) // This will allow 5 messages every 20 seconds * .ws('/echo', (ws) => ws * .onMessage(async(ctr) => { * ctr.print(await ctr.rawMessageBytes()) * }) * ) * ) * * server.rateLimit('wsMessage', (ctr) => { * ctr.print(`Please wait ${ctr.getRateLimit()?.resetIn}ms!!!!!`) * }) * ``` * @since 9.0.0 */ wsRatelimit(callback) { const limit = new RateLimit(); limit['data'] = Object.assign({}, this._wsRatelimit); callback(limit); this._wsRatelimit = limit['data']; return this; } /** * Use a Validator on all Endpoints in this Path * * This will attach a Validator to all Endpoints in this Path * @since 9.0.0 */ validate(validator) { this.validators.push(validator); this.openApi = object.deepMerge(this.openApi, validator.openApi); return this; } /** * Load Files from a Directory * @since 9.0.0 */ load(directory, options = {}) { options = { filter: (path) => path.endsWith('js') || path.endsWith('ts'), fileBasedRouting: false, ...options }; this.promises.push(new Promise(async (resolve) => { const resolved = path.resolve(directory); for await (const file of filesystem.walk(resolved, { recursive: true, async: true })) { if (!file.isFIFO() && !file.isFile()) continue; if (!options.filter?.(file.path)) continue; try { const { default: router } = await eval(`import(${JSON.stringify(file.path)})`), // bypass ttsc converting to require() in cjs path = file.path.replace(resolved, '').replaceAll('index', '').replace(/\\/g, '/').split('.').slice(0, -1).join('.').concat('/'); if (router instanceof Path) { const modifiedRoutesHttp = [], modifiedRoutesWS = [], modifiedRoutesStatic = []; for (const route of router['routesHttp']) { route.validators.unshift(...this.validators); if (options.fileBasedRouting) { if (route.urlData.type === 'regexp') { route.urlData.prefix = this.computePath(path.concat(route.urlData.prefix)); route.openApi = object.deepMerge(this.openApi, route.openApi); modifiedRoutesHttp.push(route); } else { const newRoute = new Route('http', route['urlMethod'], this.computePath(path.concat(route.urlData.value)), route.data); newRoute.validators = route.validators; newRoute.ratelimit = Object.assign(this._httpRatelimit, route.ratelimit); newRoute.openApi = object.deepMerge(this.openApi, route.openApi); modifiedRoutesHttp.push(newRoute); } } else { modifiedRoutesHttp.push(route); } } for (const route of router['routesWS']) { route.validators.unshift(...this.validators); if (options.fileBasedRouting) { if (route.urlData.type === 'regexp') { route.urlData.prefix = this.computePath(path.concat(route.urlData.prefix)); route.openApi = object.deepMerge(this.openApi, route.openApi); modifiedRoutesWS.push(route); } else { const newRoute = new Route('ws', route['urlMethod'], this.computePath(path.concat(route.urlData.value)), route.data); newRoute.validators = route.validators; newRoute.ratelimit = Object.assign(this._wsRatelimit, route.ratelimit); newRoute.openApi = object.deepMerge(this.openApi, route.openApi); modifiedRoutesWS.push(newRoute); } } else { modifiedRoutesWS.push(route); } } for (const route of router['routesStatic']) { route.validators.unshift(...this.validators); if (options.fileBasedRouting) { if (route.urlData.type === 'regexp') { route.urlData.prefix = this.computePath(path.concat(route.urlData.prefix)); route.openApi = object.deepMerge(this.openApi, route.openApi); modifiedRoutesStatic.push(route); } else { const newRoute = new Route('static', route['urlMethod'], this.computePath(path.concat(route.urlData.value)), route.data); newRoute.validators = route.validators; newRoute.ratelimit = route.ratelimit; newRoute.openApi = object.deepMerge(this.openApi, route.openApi); modifiedRoutesStatic.push(newRoute); } } else { modifiedRoutesStatic.push(route); } } this._global.routes.http.push(...modifiedRoutesHttp); this._global.routes.ws.push(...modifiedRoutesWS); this._global.routes.static.push(...modifiedRoutesStatic); } else { this._global.logger.error(`Failed to load route file ${file.path} (default export is not a Path)`); } } catch (err) { this._global.logger.error(`Failed to load route file ${file.path}\n${err}`); } } resolve(); })); return this; } /** * Export the File Loader to be used in route files * @since 9.0.0 */ export() { const global = this._global; return { Path: class FakePath extends Path { constructor() { super('/', global, undefined, undefined, []); } } }; } }