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
JavaScript
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, []);
}
}
};
}
}