UNPKG

@radatek/microserver

Version:
806 lines (804 loc) 27.4 kB
/** * 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>; }