@radatek/microserver
Version:
HTTP MicroServer
806 lines (804 loc) • 27.4 kB
TypeScript
/**
* MicroServer
* @version 2.1.0
* @package @radatek/microserver
* @copyright Darius Kisonas 2022
* @license MIT
*/
import http from 'http';
import net from 'net';
import { Readable } from 'stream';
import fs from 'fs';
import { EventEmitter } from 'events';
export declare class Warning extends Error {
constructor(text: string);
}
export declare class ResponseError extends Error {
static getStatusCode(text: string | number | undefined): number;
static getStatusText(text: string | number | undefined): string;
statusCode: number;
constructor(text: string | number | undefined, statusCode?: number);
}
export declare class AccessDenied extends ResponseError {
constructor(text?: string);
}
export declare class InvalidData extends ResponseError {
constructor(text?: string, type?: string);
}
export declare class NotFound extends ResponseError {
constructor(text?: string);
}
export declare class WebSocketError extends Error {
statusCode: number;
constructor(text?: string, code?: number);
}
export type Routes = () => {
[key: string]: Array<any>;
} | {
[key: string]: Array<any>;
};
export declare abstract class Plugin {
name?: string;
priority?: number;
handler?(req: ServerRequest, res: ServerResponse, next: Function): void;
routes?: () => Routes | Routes;
constructor(router: Router, ...args: any);
}
interface PluginClass {
new (router: Router, ...args: any): Plugin;
}
/** Extended http.IncomingMessage */
export declare class ServerRequest extends http.IncomingMessage {
/** Request protocol: http or https */
protocol: string;
/** Request client IP */
ip?: string;
/** Request from local network */
localip?: boolean;
/** Request is secure (https) */
secure?: boolean;
/** Request whole path */
path: string;
/** Request pathname */
pathname: string;
/** Base url */
baseUrl: string;
/** Original url */
originalUrl?: string;
/** Query parameters */
query: {
[key: string]: string;
};
/** Router named parameters */
params: {
[key: string]: string;
};
/** Router named parameters list */
paramsList: string[];
/** Router */
router: Router;
/** Authentication object */
auth?: Auth;
/** Authenticated user info */
user?: UserInfo;
/** Model used for request */
model?: Model;
/** Authentication token id */
tokenId?: string;
/** Request raw body */
rawBody: Buffer[];
/** Request raw body size */
rawBodySize: number;
private constructor();
/** Update request url */
updateUrl(url: string): void;
/** Rewrite request url */
rewrite(url: string): void;
/** Request body: JSON or POST parameters */
get body(): {
[key: string]: any;
};
/** Alias to body */
get post(): {
[key: string]: any;
};
/** Get websocket */
get websocket(): WebSocket;
/** get files list in request */
files(): Promise<any[] | undefined>;
/** Decode request body */
bodyDecode(res: ServerResponse, options: any, next: () => void): void;
}
/** Extends http.ServerResponse */
export declare class ServerResponse extends http.ServerResponse {
req: ServerRequest;
router: Router;
isJson: boolean;
headersOnly: boolean;
private constructor();
/** Send error reponse */
error(error: string | number | Error): void;
/** Sets Content-Type acording to data and sends response */
send(data?: string | Buffer | Error | Readable | object): void;
/** Send json response */
json(data: any): void;
/** Send json response in form { success: false, error: err } */
jsonError(error: string | number | object | Error): void;
/** Send json response in form { success: true, ... } */
jsonSuccess(data?: object | string): void;
/** Send redirect response to specified URL with optional status code (default: 302) */
redirect(code: number | string, url?: string): void;
/** Set status code */
status(code: number): this;
download(path: string, filename?: string): void;
}
/** WebSocket options */
export interface WebSocketOptions {
maxPayload?: number;
autoPong?: boolean;
permessageDeflate?: boolean;
maxWindowBits?: number;
deflate?: boolean;
}
/** WebSocket class */
export declare class WebSocket extends EventEmitter {
ready: boolean;
constructor(req: ServerRequest, options?: WebSocketOptions);
/** Close connection */
close(reason?: number, data?: Buffer): void;
/** Generate WebSocket frame from data */
static getFrame(data: number | string | Buffer | undefined, options?: any): Buffer;
/** Send data */
send(data: string | Buffer): void;
/** Send ping frame */
ping(buffer?: Buffer): void;
/** Send pong frame */
pong(buffer?: Buffer): void;
protected _sendFrame(opcode: number, data: Buffer, cb?: () => void): void;
}
/**
* Controller for dynamic routes
*
* @example
* ```js
* class MyController extends Controller {
* static model = MyModel;
* static acl = 'auth';
*
* static 'acl:index' = '';
* static 'url:index' = 'GET /index';
* async index (req, res) {
* res.send('Hello World')
* }
*
* //function name prefixes translated to HTTP methods:
* // all => GET, get => GET, insert => POST, post => POST,
* // update => PUT, put => PUT, delete => DELETE,
* // modify => PATCH, patch => PATCH,
* // websocket => internal WebSocket
* // automatic acl will be: class_name + '/' + function_name_prefix
* // automatic url will be: method + ' /' + class_name + '/' + function_name_without_prefix
*
* //static 'acl:allUsers' = 'MyController/all';
* //static 'url:allUsers' = 'GET /MyController/Users';
* async allUsers () {
* return ['usr1', 'usr2', 'usr3']
* }
*
* //static 'acl:getOrder' = 'MyController/get';
* //static 'url:getOrder' = 'GET /Users/:id/:id1';
* static 'group:getOrder' = 'orders';
* static 'model:getOrder' = OrderModel;
* async getOrder (id: string, id1: string) {
* return {id, extras: id1, type: 'order'}
* }
*
* //static 'acl:insertOrder' = 'MyController/insert';
* //static 'url:insertOrder' = 'POST /Users/:id';
* static 'model:insertOrder' = OrderModel;
* async insertOrder (id: string, id1: string) {
* return {id, extras: id1, type: 'order'}
* }
*
* static 'acl:POST /login' = '';
* async 'POST /login' () {
* return {id, extras: id1, type: 'order'}
* }
* }
* ```
*/
export declare class Controller {
protected req: ServerRequest;
protected res: ServerResponse;
model?: Model;
constructor(req: ServerRequest, res: ServerResponse);
/** Generate routes for this controller */
static routes(): any[];
}
/** Middleware */
export interface Middleware {
(req: ServerRequest, res: ServerResponse, next: Function): any;
/** @default 0 */
priority?: number;
plugin?: Plugin;
}
/** Router */
export declare class Router extends EventEmitter {
server: MicroServer;
auth?: Auth;
plugins: {
[key: string]: Plugin;
};
/** @param {MicroServer} server */
constructor(server: MicroServer);
/** Handler */
handler(req: ServerRequest, res: ServerResponse, next: Function, method?: string): any;
/** Clear routes and middlewares */
clear(): this;
/**
* Add middleware route.
* Middlewares may return promises for res.jsonSuccess(...), throw errors for res.error(...), return string or {} for res.send(...)
*
* @signature add(plugin: Plugin)
* @param {Plugin} plugin plugin module instance
* @return {Router} current router
*
* @signature add(pluginid: string, ...args: any)
* @param {string} pluginid pluginid module
* @param {...any} args arguments passed to constructor
* @return {Router} current router
*
* @signature add(pluginClass: typeof Plugin, ...args: any)
* @param {typeof Plugin} pluginClass plugin class
* @param {...any} args arguments passed to constructor
* @return {Router} current router
*
* @signature add(middleware: Middleware)
* @param {Middleware} middleware
* @return {Router} current router
*
* @signature add(methodUrl: string, ...middlewares: any)
* @param {string} methodUrl 'METHOD /url' or '/url'
* @param {...any} middlewares
* @return {Router} current router
*
* @signature add(methodUrl: string, controllerClass: typeof Controller)
* @param {string} methodUrl 'METHOD /url' or '/url'
* @param {typeof Controller} controllerClass
* @return {Router} current router
*
* @signature add(methodUrl: string, routes: Array<Array<any>>)
* @param {string} methodUrl 'METHOD /url' or '/url'
* @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
* @return {Router} current router
*
* @signature add(methodUrl: string, routes: Array<Array<any>>)
* @param {string} methodUrl 'METHOD /url' or '/url'
* @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
* @return {Router} current router
*
* @signature add(routes: { [key: string]: Array<any> })
* @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
* @return {Router} current router
*
* @signature add(methodUrl: string, routes: { [key: string]: Array<any> })
* @param {string} methodUrl 'METHOD /url' or '/url'
* @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
* @return {Router} current router
*/
use(...args: any): Router;
/** Add hook */
hook(url: string, ...mid: Middleware[]): Router;
/** Check if middleware allready added */
has(mid: Middleware): boolean;
}
export interface HttpHandler {
(req: ServerRequest, res: ServerResponse): void;
}
export interface TcpHandler {
(socket: net.Socket): void;
}
export interface ListenConfig {
/** listen port(s) with optional protocol and host (Ex. 8080 or '0.0.0.0:8080,8180' or 'https://0.0.0.0:8080' or 'tcp://0.0.0.0:8080' or 'tls://0.0.0.0:8080') */
listen?: string | number;
/** tls options */
tls?: {
cert: string;
key: string;
ca?: string;
};
/** custom handler */
handler?: HttpHandler | TcpHandler;
}
export interface CorsOptions {
/** allowed origins (default: '*') */
origin: string;
/** allowed headers (default: '*') */
headers: string;
/** allow credentials (default: false) */
credentials: boolean;
/** Expose headers */
expose?: string;
/** Max age */
maxAge?: number;
}
/** MicroServer configuration */
export interface MicroServerConfig extends ListenConfig {
/** server instance root path */
root?: string;
/** Auth options */
auth?: AuthOptions;
/** routes to add */
routes?: any;
/** Static file options */
static?: StaticOptions;
/** max body size (default: 5MB) */
maxBodySize?: number;
/** allowed HTTP methods */
methods?: string;
/** trust proxy */
trustProxy?: string[];
/** cors options */
cors?: string | CorsOptions | boolean;
/** upload dir (default: './upload') */
uploadDir?: string;
/** allow websocket deflate compression (default: false) */
websocketCompress?: boolean;
/** max websocket payload (default: 1MB) */
websocketMaxPayload?: number;
/** websocket max window bits 8-15 for deflate (default: 10) */
websocketMaxWindowBits?: number;
/** extra options for plugins */
[key: string]: any;
}
export declare class MicroServer extends EventEmitter {
/** server configuration */
config: MicroServerConfig;
/** main router */
router: Router;
/** virtual host routers */
vhosts?: {
[key: string]: Router;
};
/** all sockets */
sockets: Set<net.Socket>;
/** server instances */
servers: Set<net.Server>;
static plugins: {
[key: string]: PluginClass;
};
get plugins(): {
[key: string]: Plugin;
};
constructor(config: MicroServerConfig);
/** Add one time listener or call immediatelly for 'ready' */
once(name: string, cb: Function): this;
/** Add listener and call immediatelly for 'ready' */
on(name: string, cb: Function): this;
/** Listen server, should be used only if config.listen is not set */
listen(config?: ListenConfig): Promise<unknown>;
/** bind middleware or create one from string like: 'redirect:302,https://redirect.to', 'error:422', 'param:name=value', 'acl:users/get', 'model:User', 'group:Users', 'user:admin' */
bind(fn: string | Function | object): Function;
/** Add middleware, routes, etc.. see {router.use} */
use(...args: any): MicroServer;
/** Default server handler */
handler(req: ServerRequest, res: ServerResponse): void;
protected requestInit(req: ServerRequest, res?: ServerResponse): void;
/** Preprocess request, used by {MicroServer.handler} */
handlerInit(req: ServerRequest, res: ServerResponse, next: Function): void;
/** Last request handler */
handlerLast(req: ServerRequest, res: ServerResponse, next?: Function): any;
/** Default upgrade handler, used for WebSockets */
handlerUpgrade(req: ServerRequest, socket: net.Socket, head: any): void;
/** Close server instance */
close(): Promise<void>;
/** Add route, alias to `server.router.use(url, ...args)` */
all(url: string, ...args: any): MicroServer;
/** Add route, alias to `server.router.use('GET ' + url, ...args)` */
get(url: string, ...args: any): MicroServer;
/** Add route, alias to `server.router.use('POST ' + url, ...args)` */
post(url: string, ...args: any): MicroServer;
/** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
put(url: string, ...args: any): MicroServer;
/** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
patch(url: string, ...args: any): MicroServer;
/** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
delete(url: string, ...args: any): MicroServer;
/** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
websocket(url: string, ...args: any): MicroServer;
/** Add router hook, alias to `server.router.hook(url, ...args)` */
hook(url: string, ...args: any): MicroServer;
}
/** Static files options */
export interface StaticOptions {
/** files root directory */
root?: string;
/** url path */
path?: string;
/** additional mime types */
mimeTypes?: {
[key: string]: string;
};
/** file extension handlers */
handlers?: {
[key: string]: Middleware;
};
/** ignore prefixes */
ignore?: string[];
/** index file. default: 'index.html' */
index?: string;
/** Update Last-Modified header. default: true */
lastModified?: boolean;
/** Update ETag header. default: true */
etag?: boolean;
/** Max file age in seconds */
maxAge?: number;
}
export interface ServeFileOptions {
/** path */
path: string;
/** root */
root?: string;
/** file name */
filename?: string;
/** file mime type */
mimeType?: string;
/** last modified date */
lastModified?: boolean;
/** etag */
etag?: boolean;
/** max age */
maxAge?: number;
/** range */
range?: boolean;
/** stat */
stats?: fs.Stats;
}
/** Proxy plugin options */
export interface ProxyPluginOptions {
/** Base path */
path?: string;
/** Remote url */
remote?: string;
/** Match regex filter */
match?: string;
/** Override/set headers for remote */
headers?: {
[key: string]: string;
};
/** Valid headers to forward */
validHeaders?: {
[key: string]: boolean;
};
}
export declare class ProxyPlugin extends Plugin {
/** Default valid headers */
static validHeaders: {
[key: string]: boolean;
};
/** Current valid headers */
validHeaders: {
[key: string]: boolean;
};
/** Override headers to forward to remote */
headers: {
[key: string]: string;
} | undefined;
/** Remote url */
remoteUrl: URL;
/** Match regex filter */
regex?: RegExp;
constructor(router: Router, options?: ProxyPluginOptions | string);
/** Default proxy handler */
proxyHandler(req: ServerRequest, res: ServerResponse, next: Function): any;
/** Proxy plugin handler as middleware */
handler?(req: ServerRequest, res: ServerResponse, next: Function): void;
}
/** User info */
export interface UserInfo {
/** User id */
id: string;
/** User password plain or hash */
password?: string;
/** ACL options */
acl?: {
[key: string]: boolean;
};
/** User group */
group?: string;
/** Custom user data */
[key: string]: any;
}
/** Authentication options */
export interface AuthOptions {
/** Authentication token */
token: string | Buffer;
/** Users */
users?: {
[key: string]: UserInfo;
} | ((usr: string, psw?: string) => Promise<UserInfo | undefined>);
/** Default ACL */
defaultAcl?: {
[key: string]: boolean;
};
/** Expire time in seconds */
expire?: number;
/** Authentication mode */
mode?: 'cookie' | 'token';
/** Authentication realm for basic authentication */
realm?: string;
/** Redirect URL */
redirect?: string;
/** Authentication cache */
cache?: {
[key: string]: {
data: UserInfo;
time: number;
};
};
/** Interal next cache cleanup time */
cacheCleanup?: number;
}
/** Authentication class */
export declare class Auth {
/** Server request */
req: ServerRequest | undefined;
/** Server response */
res: ServerResponse | undefined;
/** Authentication options */
options: AuthOptions;
/** Get user function */
users: ((usr: string, psw?: string, salt?: string) => Promise<UserInfo | undefined>);
constructor(options?: AuthOptions);
/** Decode token */
decode(data: string): {
data: string;
expire: number;
};
/** Encode token */
encode(data: string, expire?: number): string;
/**
* Check acl over authenticated user with: `id`, `group/*`, `*`
* @param {string} id - to authenticate: `id`, `group/id`, `model/action`, comma separated best: true => false => def
* @param {boolean} [def=false] - default access
*/
acl(id: string, def?: boolean): boolean;
/**
* Authenticate user and setup cookie
* @param {string|UserInfo} usr - user id used with options.users to retrieve user object. User object must contain `id` and `acl` object (Ex. usr = {id:'usr', acl:{'users/*':true}})
* @param {string} [psw] - user password (if used for user authentication with options.users)
* @param {number} [expire] - expire time in seconds (default: options.expire)
*/
token(usr: string | UserInfo | undefined, psw: string | undefined, expire?: number): Promise<string | undefined>;
/**
* Authenticate user and setup cookie
*/
login(usr: string | UserInfo | undefined, psw?: string, options?: {
expire?: number;
salt?: string;
}): Promise<UserInfo | undefined>;
/** Logout logged in user */
logout(): void;
/** Get hashed string from user and password */
password(usr: string, psw: string, salt?: string): string;
/** Get hashed string from user and password */
static password(usr: string, psw: string, salt?: string): string;
/** Validate user password */
checkPassword(usr: string, psw: string, storedPsw: string, salt?: string): boolean;
/** Validate user password */
static checkPassword(usr: string, psw: string, storedPsw: string, salt?: string): boolean;
/** Clear user cache if users setting where changed */
clearCache(): void;
}
/** Create microserver */
export declare function create(config: MicroServerConfig): MicroServer;
export interface FileStoreOptions {
/** Base directory */
dir?: string;
/** Cache timeout in milliseconds */
cacheTimeout?: number;
/** Max number of cached items */
cacheItems?: number;
/** Debounce timeout in milliseconds for autosave */
debounceTimeout?: number;
}
/** JSON File store */
export declare class FileStore {
constructor(options?: FileStoreOptions);
/** cleanup cache */
cleanup(): void;
close(): Promise<void>;
sync(): Promise<void>;
/** load json file data */
load(name: string, autosave?: boolean): Promise<any>;
/** save data */
save(name: string, data: any): Promise<any>;
/** load all files in directory */
all(name: string, autosave?: boolean): Promise<{
[key: string]: any;
}>;
/** delete data file */
delete(name: string): Promise<void>;
/** Observe data object */
observe(data: object, cb: (data: object, key: string, value: any) => void): object;
}
/** Model validation options */
interface ModelValidateOptions {
/** User info */
user?: UserInfo;
/** Request params */
params?: Object;
/** is insert */
insert?: boolean;
/** is read-only */
readOnly?: boolean;
/** validate */
validate?: boolean;
/** use default */
default?: boolean;
/** is required */
required?: boolean;
/** projection fields */
projection?: Document;
}
/** Model field validation options */
interface ModelValidateFieldOptions extends ModelValidateOptions {
name: string;
field: FieldDescriptionInternal;
model: Model;
}
export interface ModelCallbackFunc {
(options: any): any;
}
/** Model field description */
export interface FieldDescriptionObject {
/** Field type */
type: string | Function | Model | Array<string | Function | Model>;
/** Is array */
array?: boolean;
/** Is required */
required?: boolean | string | ModelCallbackFunc;
/** Can read */
canRead?: boolean | string | ModelCallbackFunc;
/** Can write */
canWrite?: boolean | string | ModelCallbackFunc;
/** Default value */
default?: number | string | ModelCallbackFunc;
/** Validate function */
validate?: (value: any, options: ModelValidateOptions) => string | number | object | null | Error | typeof Error;
/** Valid values */
enum?: Array<string | number>;
/** Minimum value for string and number */
minimum?: number | string;
/** Maximum value for string and number */
maximum?: number | string;
/** Regex validation or 'email', 'url', 'date', 'time', 'date-time' */
format?: string;
}
type FieldDescription = FieldDescriptionObject | string | Function | Model | FieldDescription[];
interface FieldDescriptionInternal {
type: string;
model?: Model;
required?: ModelCallbackFunc;
canRead: ModelCallbackFunc;
canWrite: ModelCallbackFunc;
default: ModelCallbackFunc;
validate: (value: any, options: ModelValidateFieldOptions) => any;
}
export declare interface ModelCollections {
collection(name: string): Promise<MicroCollection>;
}
export declare class Model {
static collections?: ModelCollections;
static models: {
[key: string]: Model;
};
/** Define model */
static define(name: string, fields: {
[key: string]: FieldDescription;
}, options?: {
collection?: MicroCollection | Promise<MicroCollection>;
class?: typeof Model;
}): Model;
/** Model fields description */
model: {
[key: string]: FieldDescriptionInternal;
};
/** Model name */
name: string;
/** Model collection for persistance */
collection?: MicroCollection | Promise<MicroCollection>;
/** Create model acording to description */
constructor(fields: {
[key: string]: FieldDescription;
}, options?: {
collection?: MicroCollection | Promise<MicroCollection>;
name?: string;
});
/** Validate data over model */
validate(data: Document, options?: ModelValidateOptions): Document;
/** Generate filter for data queries */
getFilter(data: Document, options?: ModelValidateOptions): Document;
/** Find one document */
findOne(query: Query, options?: ModelValidateOptions): Promise<Document | undefined>;
/** Find many documents */
findMany(query: Query, options?: ModelValidateOptions): Promise<Document[]>;
/** Insert a new document */
insert(data: Document, options?: ModelValidateOptions): Promise<void>;
/** Update one matching document */
update(query: Query, options?: ModelValidateOptions): Promise<void>;
/** Delete one matching document */
delete(query: Query, options?: ModelValidateOptions): Promise<void>;
/** Microserver middleware */
handler(req: ServerRequest, res: ServerResponse): any;
}
export declare interface MicroCollectionOptions {
/** Collection name */
name?: string;
/** Collection persistent store */
store?: FileStore;
/** Custom data loader */
load?: (col: MicroCollection) => Promise<object>;
/** Custom data saver */
save?: (id: string, doc: Document | undefined, col: MicroCollection) => Promise<Document>;
/** Preloaded data object */
data?: {
[key: string]: Document;
};
}
export declare interface Query {
[key: string]: any;
}
export declare interface Document {
[key: string]: any;
}
/** Cursor */
export declare interface Cursor {
forEach(cb: Function, self?: any): Promise<number>;
all(): Promise<Document[]>;
}
/** Find options */
export declare interface FindOptions {
/** Query */
query?: Query;
/** is upsert */
upsert?: boolean;
/** is new */
new?: boolean;
/** update object */
update?: Query;
/** maximum number of hits */
limit?: number;
}
/** minimalistic indexed mongo type collection with persistance for usage with Model */
export declare class MicroCollection {
/** Collection name */
name: string;
/** Collection data */
data: {
[key: string]: Document;
};
/** Get collections factory */
static collections(options: MicroCollectionOptions): ModelCollections;
constructor(options?: MicroCollectionOptions);
/** check for collection is ready */
protected checkReady(): Promise<void>;
/** Query document with query filter */
protected queryDocument(query?: Query, data?: Document): Document;
/** Count all documents */
count(): Promise<number>;
/** Find one matching document */
findOne(query: Query): Promise<Document | undefined>;
/** Find all matching documents */
find(query: Query): Cursor;
/** Find and modify one matching document */
findAndModify(options: FindOptions): Promise<number>;
/** Insert one document */
insertOne(doc: Document): Promise<Document>;
/** Insert multiple documents */
insert(docs: Document[]): Promise<Document[]>;
/** Delete one matching document */
deleteOne(query: Query): Promise<void>;
/** Delete all matching documents */
deleteMany(query: Query): Promise<number>;
}