@radatek/microserver 3.0.2 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ ## HTTP MicroServer
2
+
3
+ Lightweight all-in-one http web server without dependencies
4
+
5
+ Features:
6
+ - fast REST API router
7
+ - form/json body decoder
8
+ - file upload
9
+ - websockets
10
+ - authentication
11
+ - plain/hashed passwords
12
+ - virtual hosts
13
+ - static files
14
+ - rewrite
15
+ - redirect
16
+ - reverse proxy routes
17
+ - trust ip for reverse proxy
18
+ - json file storage with autosave
19
+ - tls with automatic certificate reload
20
+ - data model with validation and mongodb interface
21
+ - typescript model schema translation
22
+ - simple file/memory storage
23
+ - promises as middleware
24
+ - controller class
25
+ - access rights per route
26
+ - access rights per model field
27
+
28
+ ### Usage examples:
29
+
30
+ Simple router:
31
+
32
+ ```ts
33
+ import { MicroServer, AccessDenied, StandardPlugins, StaticPlugin } from '@radatek/microserver'
34
+
35
+ const server = new MicroServer({
36
+ listen: 8080,
37
+ auth: {
38
+ users: {
39
+ usr: {
40
+ password: 'secret'
41
+ }
42
+ }
43
+ }
44
+ })
45
+ server.use(StandardPlugins)
46
+ server.use('GET /api/hello/:id',
47
+ (req: ServerRequset, res: ServerResponse) =>
48
+ ({message:'Hello ' + req.params.id + '!'}))
49
+ server.use('POST /api/login',
50
+ (req: ServerRequset, res: ServerResponse) =>
51
+ {
52
+ const user = await req.auth.login(req.body.user, req.body.password)
53
+ return user ? {user} : new AccessDenied()
54
+ })
55
+ server.use('GET /api/protected', 'acl:auth',
56
+ (req: ServerRequset, res: ServerResponse) =>
57
+ ({message:'Secret resource'}))
58
+ server.use(StaticPlugin, {root:'public'})
59
+ ```
60
+
61
+ Using WebSockets
62
+
63
+ ```ts
64
+ import { MicroServer, WebSocketsPlugin } from '@radatek/microserver'
65
+
66
+ const server = new MicroServer({
67
+ listen: 8080
68
+ })
69
+ server.use(WebSocketsPlugin)
70
+ server.use('WEBSOCKET /ws', (req: ServerRequset, res: ServerResponse) => {
71
+ req.websocket.on('message', (data) => console.log(data))
72
+ req.websocket.on('close', () => console.log('closed'))
73
+ })
74
+ ```
75
+
76
+ Using data schema:
77
+
78
+ ```js
79
+ import { MicroServer, Model, MicroCollection } from '@radatek/microserver'
80
+
81
+ const db = new MicroCollectionStore('./data')
82
+ // or using MicroDB collection
83
+ //const db = new MicroDB('microdb://data')
84
+
85
+ const usersCollection = await db.collection('users')
86
+
87
+ const userProfile = new Model({
88
+ _id: 'string',
89
+ name: { type: 'string', required: true },
90
+ email: { type: 'string', format: 'email' },
91
+ password: { type: 'string', canRead: false },
92
+ role: { type: 'string' },
93
+ acl: { type: 'object' },
94
+ }, { collection: usersCollection, name: 'user' })
95
+
96
+ const server = new MicroServer({
97
+ listen:8080,
98
+ auth: {
99
+ users: (user, password) => userProfile.get(password ? {_id: user, password } : {_id: user})
100
+ }
101
+ })
102
+
103
+ await userProfile.insert({name: 'admin', password: 'secret', role: 'admin', acl: {'user/*': true}})
104
+
105
+ server.use('POST /login', async (req) => {
106
+ const user = await req.auth.login(req.body.user, req.body.password)
107
+ return user ? { user } : 403
108
+ })
109
+ // authenticated user allways has auth access
110
+ server.use('GET /profile', 'acl:auth', req => ({ user: req.user }))
111
+ // get all users if role='admin'
112
+ server.use('GET /admin/users', 'role:admin', userProfile)
113
+ // get user by id if has acl 'user/get'
114
+ server.use('GET /admin/user/:id', 'acl:user/get', userProfile)
115
+ // insert new user if role='admin' and has acl 'user/insert'
116
+ server.use('POST /admin/user', 'role:admin', 'acl:user/insert', userProfile)
117
+ // update user if has acl 'user/update'
118
+ server.use('PUT /admin/user/:id', 'acl:user/update', userProfile)
119
+ // delete user if has acl 'user/update'
120
+ server.use('DELETE /admin/user/:id', 'acl:user/delete', userProfile)
121
+ ```
122
+
123
+ Using controller:
124
+
125
+ ```ts
126
+ const server = new MicroServer({
127
+ listen: 8080,
128
+ auth: {
129
+ users: {
130
+ usr: {
131
+ password: 'secret',
132
+ acl: {
133
+ user: true,
134
+ 'user/insert': true
135
+ }
136
+ }
137
+ }
138
+ }
139
+ })
140
+ server.use(StandardPlugins)
141
+
142
+ new MicroCollectionStore('data') // initialize simple file store for models
143
+ //new MicroCollectionStore() // initialize simple memory store for models
144
+
145
+ class RestApi extends Controller {
146
+ static acl = '' // default acl
147
+
148
+ gethello(id) { // same as 'GET /hello'
149
+ return {message:'Hello ' + id + '!'}
150
+ }
151
+
152
+ async post_login() { // same as 'POST /login'
153
+ const user = await this.auth.login(this.body.user, this.body.password)
154
+ return user ? {user} : 403
155
+ }
156
+
157
+ static 'acl:protected' = 'user'
158
+ static 'url:protected' = 'GET /protected'
159
+ protected() {
160
+ return {message:'Protected'}
161
+ }
162
+ }
163
+
164
+ server.use('/api', RestApi)
165
+
166
+ const AddressModel = new Model({
167
+ street: String,
168
+ city: String
169
+ })
170
+
171
+ const UserModel = Model.define({
172
+ name: { type: String, require: true },
173
+ address: AddressModel
174
+ })
175
+
176
+ class UserController extends Controller<typeof UserModel> {
177
+ static model = UserModel
178
+ static name = 'user'
179
+
180
+ static 'acl:get' = 'user/get'
181
+ async get(id: string) { // same as 'GET user'
182
+ return {user: await this.model.findOne({id})}
183
+ }
184
+
185
+ static 'acl:insert' = 'user/insert'
186
+ async insert() { // same as 'POST user'
187
+ await this.model.insert(this.req.body)
188
+ return {}
189
+ }
190
+ }
191
+
192
+ server.use('/api/user', UserController)
193
+ ```
package/microserver.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 3.0.2
3
+ * @version 3.0.3
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -32,9 +32,6 @@ export declare class WebSocketError extends Error {
32
32
  statusCode: number;
33
33
  constructor(text?: string, code?: number);
34
34
  }
35
- type DeferPromise<T = void> = Promise<T> & {
36
- resolve: (res?: T | Error) => void;
37
- };
38
35
  /** Middleware */
39
36
  export interface Middleware {
40
37
  (req: ServerRequest, res: ServerResponse, next: Function): any;
@@ -50,7 +47,7 @@ export declare abstract class Plugin {
50
47
  constructor();
51
48
  }
52
49
  interface PluginClass {
53
- new (options?: any, server?: MicroServer): Plugin;
50
+ new (options: any, server: MicroServer): Plugin;
54
51
  }
55
52
  export type ServerRequestBody<T = any> = T extends Model<infer U extends ModelSchema> ? ModelDocument<U> : Record<string, any>;
56
53
  /** Extended http.IncomingMessage */
@@ -89,12 +86,17 @@ export declare class ServerRequest<T = any> extends http.IncomingMessage {
89
86
  rawBody: Buffer[];
90
87
  /** Request raw body size */
91
88
  rawBodySize: number;
92
- _body?: ServerRequestBody<T>;
93
- _isReady: DeferPromise | undefined;
94
89
  private constructor();
95
- static extend(req: http.IncomingMessage, server: MicroServer): ServerRequest;
90
+ /** Extend http.IncomingMessage */
91
+ static extend(req: http.IncomingMessage, res: http.ServerResponse, server: MicroServer): ServerRequest;
92
+ /** Check if request is ready */
96
93
  get isReady(): boolean;
94
+ /** Wait for request to be ready, usualy used to wait for file upload */
97
95
  waitReady(): Promise<void>;
96
+ /** Signal request as ready */
97
+ setReady(err?: Error): void;
98
+ /** Set request body */
99
+ setBody(body: ServerRequestBody<T>): void;
98
100
  /** Update request url */
99
101
  updateUrl(url: string): this;
100
102
  /** Rewrite request url */
@@ -111,9 +113,10 @@ export declare class ServerRequest<T = any> extends http.IncomingMessage {
111
113
  /** Extends http.ServerResponse */
112
114
  export declare class ServerResponse<T = any> extends http.ServerResponse {
113
115
  readonly req: ServerRequest<T>;
116
+ /** Should response be json */
114
117
  isJson: boolean;
115
- headersOnly: boolean;
116
118
  private constructor();
119
+ /** Extends http.ServerResponse */
117
120
  static extend(res: http.ServerResponse): void;
118
121
  /** Send error reponse */
119
122
  error(error: string | number | Error): void;
@@ -170,11 +173,13 @@ interface MicroServerEvents {
170
173
  }
171
174
  /** Lighweight HTTP server */
172
175
  export declare class MicroServer extends EventEmitter {
176
+ /** MicroServer configuration */
173
177
  config: MicroServerConfig;
178
+ /** Authorization object */
174
179
  auth?: Auth;
175
- /** all sockets */
180
+ /** All sockets */
176
181
  sockets: Set<net.Socket> | undefined;
177
- /** server instances */
182
+ /** Server instances */
178
183
  servers: Set<net.Server> | undefined;
179
184
  /** @param {MicroServerConfig} [config] MicroServer configuration */
180
185
  constructor(config?: MicroServerConfig);
@@ -187,40 +192,39 @@ export declare class MicroServer extends EventEmitter {
187
192
  waitReady(): Promise<void>;
188
193
  /** Listen server, should be used only if config.listen is not set */
189
194
  listen(config: ListenConfig): Promise<void>;
190
- /** 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' */
191
- _bind(fn: string | Function | object): Function;
192
195
  /** Default server handler */
193
196
  handler(req: ServerRequest, res: ServerResponse): void;
194
- /** Last request handler */
195
- handlerLast(req: ServerRequest, res: ServerResponse, next?: Function): any;
196
197
  /** Clear routes and middlewares */
197
198
  clear(): this;
198
199
  /**
199
- * Add middleware route.
200
+ * Add middleware, plugin or routes to server.
200
201
  * Middlewares may return promises for res.jsonSuccess(...), throw errors for res.error(...), return string or {} for res.send(...)
201
202
  * RouteURL: 'METHOD /suburl', 'METHOD', '* /suburl'
202
203
  */
203
204
  use(...args: [
204
205
  Middleware | Plugin | ControllerClass | RoutesSet
205
206
  ] | [Promise<Middleware | Plugin | ControllerClass | RoutesSet>] | RoutesList | [RouteURL, RoutesSet] | [PluginClass | Promise<PluginClass>, options?: any]): Promise<void>;
207
+ /** Add middleware to stack, with optional priority */
206
208
  addStack(middleware: Middleware): void;
209
+ /** Get plugin */
207
210
  getPlugin(id: string): Plugin | undefined;
211
+ /** Wait for plugin */
208
212
  waitPlugin(id: string): Promise<Plugin>;
209
- /** Add route, alias to `server.router.use(url, ...args)` */
213
+ /** Add route, alias to `server.use(url, ...args)` */
210
214
  all(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
211
- /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
215
+ /** Add route, alias to `server.use('GET ' + url, ...args)` */
212
216
  get(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
213
- /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
217
+ /** Add route, alias to `server.use('POST ' + url, ...args)` */
214
218
  post(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
215
- /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
219
+ /** Add route, alias to `server.use('PUT ' + url, ...args)` */
216
220
  put(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
217
- /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
221
+ /** Add route, alias to `server.use('PATCH ' + url, ...args)` */
218
222
  patch(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
219
- /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
223
+ /** Add route, alias to `server.use('DELETE ' + url, ...args)` */
220
224
  delete(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
221
- /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
225
+ /** Add websocket handler, alias to `server.use('WEBSOCKET ' + url, ...args)` */
222
226
  websocket(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
223
- /** Add router hook, alias to `server.router.hook(url, ...args)` */
227
+ /** Add router hook, alias to `server.hook(url, ...args)` */
224
228
  hook(url: RouteURL, ...args: RoutesMiddleware[]): MicroServer;
225
229
  /** Check if middleware allready added */
226
230
  has(mid: Middleware): boolean;
@@ -266,8 +270,6 @@ export declare class CorsPlugin extends Plugin {
266
270
  export declare class MethodsPlugin extends Plugin {
267
271
  priority: number;
268
272
  name: string;
269
- _methods: string;
270
- _methodsIdx: Record<string, boolean>;
271
273
  constructor(methods?: string);
272
274
  handler(req: ServerRequest, res: ServerResponse, next: Function): any;
273
275
  }
@@ -277,7 +279,6 @@ export interface BodyOptions {
277
279
  export declare class BodyPlugin extends Plugin {
278
280
  priority: number;
279
281
  name: string;
280
- _maxBodySize: number;
281
282
  constructor(options?: BodyOptions);
282
283
  handler(req: ServerRequest, res: ServerResponse, next: () => void): void;
283
284
  }
@@ -295,8 +296,6 @@ export interface UploadFile {
295
296
  export declare class UploadPlugin extends Plugin {
296
297
  priority: number;
297
298
  name: string;
298
- _maxFileSize: number;
299
- _uploadDir?: string;
300
299
  constructor(options?: UploadOptions);
301
300
  handler(req: ServerRequest, res: ServerResponse, next: () => void): void;
302
301
  }
@@ -338,7 +337,6 @@ export declare class WebSocket extends EventEmitter {
338
337
  ping(buffer?: Buffer): void;
339
338
  /** Send pong frame */
340
339
  pong(buffer?: Buffer): void;
341
- protected _sendFrame(opcode: number, data: Buffer, cb?: () => void): void;
342
340
  on<K extends keyof WebSocketEvents>(event: K, listener: WebSocketEvents[K]): this;
343
341
  addListener<K extends keyof WebSocketEvents>(event: K, listener: WebSocketEvents[K]): this;
344
342
  once<K extends keyof WebSocketEvents>(event: K, listener: WebSocketEvents[K]): this;
@@ -347,11 +345,9 @@ export declare class WebSocket extends EventEmitter {
347
345
  }
348
346
  export declare class WebSocketPlugin extends Plugin {
349
347
  name: string;
350
- _handler: (req: ServerRequest, socket: net.Socket, head: any) => void;
351
348
  constructor(options?: any, server?: MicroServer);
352
- addUpgradeHandler(srv: http.Server): void;
349
+ private addUpgradeHandler;
353
350
  upgradeHandler(server: MicroServer, req: ServerRequest, socket: net.Socket, head: any): void;
354
- static create(req: ServerRequest, options?: WebSocketOptions): WebSocket;
355
351
  }
356
352
  /** Trust proxy plugin, adds `req.ip` and `req.localip` */
357
353
  export declare class TrustProxyPlugin extends Plugin {
@@ -451,6 +447,7 @@ export declare class StaticFilesPlugin extends Plugin {
451
447
  constructor(options?: StaticFilesOptions | string, server?: MicroServer);
452
448
  /** Default static files handler */
453
449
  handler(req: ServerRequest, res: ServerResponse, next: Function): any;
450
+ /** Send static file */
454
451
  serveFile(req: ServerRequest, res: ServerResponse, options: ServeFileOptions): void;
455
452
  }
456
453
  /** Proxy plugin options */
@@ -709,7 +706,9 @@ export declare class FileStore {
709
706
  constructor(options?: FileStoreOptions);
710
707
  /** cleanup cache */
711
708
  cleanup(): void;
709
+ /** close store */
712
710
  close(): Promise<void>;
711
+ /** sync data to disk */
713
712
  sync(): Promise<void>;
714
713
  /** load json file data */
715
714
  load(name: string, autosave?: boolean): Promise<any>;
@@ -927,10 +926,12 @@ export declare class MicroCollection<TSchema extends ModelSchema = any> {
927
926
  deleteOne(query: Query): Promise<number>;
928
927
  /** Delete all matching documents */
929
928
  deleteMany(query: Query): Promise<number>;
929
+ /** Update one matching document */
930
930
  updateOne(query: Query, update: Record<string, any>, options?: FindOptions): Promise<{
931
931
  upsertedId: any;
932
932
  modifiedCount: number;
933
933
  }>;
934
+ /** Update many matching documents */
934
935
  updateMany(query: Query, update: Record<string, any>, options?: FindOptions): Promise<{
935
936
  upsertedId: any;
936
937
  modifiedCount: number;
package/microserver.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 3.0.2
3
+ * @version 3.0.3
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -118,11 +118,12 @@ export class ServerRequest extends http.IncomingMessage {
118
118
  rawBodySize;
119
119
  _body;
120
120
  _isReady;
121
- constructor(server) {
121
+ constructor(res, server) {
122
122
  super(new net.Socket());
123
- ServerRequest.extend(this, server);
123
+ ServerRequest.extend(this, res, server);
124
124
  }
125
- static extend(req, server) {
125
+ /** Extend http.IncomingMessage */
126
+ static extend(req, res, server) {
126
127
  const reqNew = Object.setPrototypeOf(req, ServerRequest.prototype);
127
128
  let ip = req.socket.remoteAddress || '::1';
128
129
  if (ip.startsWith('::ffff:'))
@@ -141,12 +142,23 @@ export class ServerRequest extends http.IncomingMessage {
141
142
  rawBody: [],
142
143
  rawBodySize: 0
143
144
  });
145
+ if (req.readable && !req.complete) {
146
+ reqNew._isReady = deferPromise((err) => {
147
+ reqNew._isReady = undefined;
148
+ if (err && res && !res.headersSent) {
149
+ res.statusCode = 'statusCode' in err ? err.statusCode : 400;
150
+ res.end(http.STATUS_CODES[res.statusCode] || 'Error');
151
+ }
152
+ });
153
+ }
144
154
  reqNew.updateUrl(req.url || '/');
145
155
  return reqNew;
146
156
  }
157
+ /** Check if request is ready */
147
158
  get isReady() {
148
159
  return this._isReady === undefined;
149
160
  }
161
+ /** Wait for request to be ready, usualy used to wait for file upload */
150
162
  async waitReady() {
151
163
  if (this._isReady === undefined)
152
164
  return;
@@ -154,6 +166,14 @@ export class ServerRequest extends http.IncomingMessage {
154
166
  if (res && res instanceof Error)
155
167
  throw res;
156
168
  }
169
+ /** Signal request as ready */
170
+ setReady(err) {
171
+ this._isReady?.resolve(err);
172
+ }
173
+ /** Set request body */
174
+ setBody(body) {
175
+ this._body = body;
176
+ }
157
177
  /** Update request url */
158
178
  updateUrl(url) {
159
179
  this.url = url;
@@ -190,18 +210,19 @@ export class ServerRequest extends http.IncomingMessage {
190
210
  }
191
211
  /** Extends http.ServerResponse */
192
212
  export class ServerResponse extends http.ServerResponse {
213
+ /** Should response be json */
193
214
  isJson;
194
- headersOnly;
195
215
  constructor(server) {
196
- super(ServerRequest.extend(new http.IncomingMessage(new net.Socket()), server));
216
+ super(new http.IncomingMessage(new net.Socket()));
217
+ ServerRequest.extend(this.req, this, server);
197
218
  ServerResponse.extend(this);
198
219
  }
220
+ /** Extends http.ServerResponse */
199
221
  static extend(res) {
200
222
  Object.setPrototypeOf(res, ServerResponse.prototype);
201
223
  Object.assign(res, {
202
224
  statusCode: 200,
203
- isJson: false,
204
- headersOnly: false
225
+ isJson: false
205
226
  });
206
227
  }
207
228
  /** Send error reponse */
@@ -266,7 +287,7 @@ export class ServerResponse extends http.ServerResponse {
266
287
  return (data.pipe(this, { end: true }), void 0);
267
288
  if (data instanceof Buffer) {
268
289
  this.setHeader('Content-Length', data.byteLength);
269
- if (this.headersOnly)
290
+ if (this.statusCode === 304 || this.req.method === 'HEAD')
270
291
  this.end();
271
292
  else
272
293
  this.end(data);
@@ -285,7 +306,7 @@ export class ServerResponse extends http.ServerResponse {
285
306
  this.setHeader('Content-Type', 'text/plain');
286
307
  }
287
308
  this.setHeader('Content-Length', Buffer.byteLength(data, 'utf8'));
288
- if (this.headersOnly)
309
+ if (this.statusCode === 304 || this.req.method === 'HEAD')
289
310
  this.end();
290
311
  else
291
312
  this.end(data, 'utf8');
@@ -340,15 +361,17 @@ export class ServerResponse extends http.ServerResponse {
340
361
  }
341
362
  /** Lighweight HTTP server */
342
363
  export class MicroServer extends EventEmitter {
364
+ /** MicroServer configuration */
343
365
  config;
366
+ /** Authorization object */
344
367
  auth;
345
368
  _plugins = {};
346
369
  _stack = [];
347
370
  _router = new RouterPlugin();
348
371
  _worker = new Worker();
349
- /** all sockets */
372
+ /** All sockets */
350
373
  sockets;
351
- /** server instances */
374
+ /** Server instances */
352
375
  servers;
353
376
  /** @param {MicroServerConfig} [config] MicroServer configuration */
354
377
  constructor(config) {
@@ -475,7 +498,7 @@ export class MicroServer extends EventEmitter {
475
498
  });
476
499
  return this._worker.wait('listen');
477
500
  }
478
- /** 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' */
501
+ /* 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' */
479
502
  _bind(fn) {
480
503
  if (typeof fn === 'string') {
481
504
  let name = fn;
@@ -572,27 +595,9 @@ export class MicroServer extends EventEmitter {
572
595
  }
573
596
  /** Default server handler */
574
597
  handler(req, res) {
575
- ServerRequest.extend(req, this);
598
+ ServerRequest.extend(req, res, this);
576
599
  ServerResponse.extend(res);
577
- if (req.readable) {
578
- req._isReady = deferPromise((err) => {
579
- req._isReady = undefined;
580
- if (err) {
581
- if (!res.headersSent)
582
- res.error('statusCode' in err ? err.statusCode : 400);
583
- }
584
- });
585
- }
586
600
  this._router.walk(this._stack, req, res, () => res.error(404));
587
- //this.handlerRouter(req, res, () => this.handlerLast(req, res))
588
- }
589
- /** Last request handler */
590
- handlerLast(req, res, next) {
591
- if (res.headersSent || res.closed)
592
- return;
593
- if (!next)
594
- next = () => res.error(404);
595
- return next();
596
601
  }
597
602
  /** Clear routes and middlewares */
598
603
  clear() {
@@ -603,7 +608,7 @@ export class MicroServer extends EventEmitter {
603
608
  return this;
604
609
  }
605
610
  /**
606
- * Add middleware route.
611
+ * Add middleware, plugin or routes to server.
607
612
  * Middlewares may return promises for res.jsonSuccess(...), throw errors for res.error(...), return string or {} for res.send(...)
608
613
  * RouteURL: 'METHOD /suburl', 'METHOD', '* /suburl'
609
614
  */
@@ -697,6 +702,7 @@ export class MicroServer extends EventEmitter {
697
702
  this._worker.endJob('plugin:' + plugin.name);
698
703
  }
699
704
  }
705
+ /** Add middleware to stack, with optional priority */
700
706
  addStack(middleware) {
701
707
  if (middleware.plugin?.name && this.getPlugin(middleware.plugin.name))
702
708
  throw new Error(`Plugin ${middleware.plugin.name} already added`);
@@ -704,9 +710,11 @@ export class MicroServer extends EventEmitter {
704
710
  const idx = this._stack.findLastIndex(f => (f.priority || 0) <= priority);
705
711
  this._stack.splice(idx + 1, 0, middleware);
706
712
  }
713
+ /** Get plugin */
707
714
  getPlugin(id) {
708
715
  return this._plugins[id];
709
716
  }
717
+ /** Wait for plugin */
710
718
  async waitPlugin(id) {
711
719
  const p = this.getPlugin(id);
712
720
  if (p)
@@ -715,42 +723,42 @@ export class MicroServer extends EventEmitter {
715
723
  await this._worker.wait('plugin:' + id);
716
724
  return this.getPlugin(id);
717
725
  }
718
- /** Add route, alias to `server.router.use(url, ...args)` */
726
+ /** Add route, alias to `server.use(url, ...args)` */
719
727
  all(url, ...args) {
720
728
  this.use('* ' + url, ...args);
721
729
  return this;
722
730
  }
723
- /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
731
+ /** Add route, alias to `server.use('GET ' + url, ...args)` */
724
732
  get(url, ...args) {
725
733
  this.use('GET ' + url, ...args);
726
734
  return this;
727
735
  }
728
- /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
736
+ /** Add route, alias to `server.use('POST ' + url, ...args)` */
729
737
  post(url, ...args) {
730
738
  this.use('POST ' + url, ...args);
731
739
  return this;
732
740
  }
733
- /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
741
+ /** Add route, alias to `server.use('PUT ' + url, ...args)` */
734
742
  put(url, ...args) {
735
743
  this.use('PUT ' + url, ...args);
736
744
  return this;
737
745
  }
738
- /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
746
+ /** Add route, alias to `server.use('PATCH ' + url, ...args)` */
739
747
  patch(url, ...args) {
740
748
  this.use('PATCH ' + url, ...args);
741
749
  return this;
742
750
  }
743
- /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
751
+ /** Add route, alias to `server.use('DELETE ' + url, ...args)` */
744
752
  delete(url, ...args) {
745
753
  this.use('DELETE ' + url, ...args);
746
754
  return this;
747
755
  }
748
- /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
756
+ /** Add websocket handler, alias to `server.use('WEBSOCKET ' + url, ...args)` */
749
757
  websocket(url, ...args) {
750
758
  this.use('WEBSOCKET ' + url, ...args);
751
759
  return this;
752
760
  }
753
- /** Add router hook, alias to `server.router.hook(url, ...args)` */
761
+ /** Add router hook, alias to `server.hook(url, ...args)` */
754
762
  hook(url, ...args) {
755
763
  const m = url.match(/^([A-Z]+) (.*)/) || ['', 'hook', url];
756
764
  this._router.add(m[1], m[2], args.filter(m => m).map(m => this._bind(m)), false);
@@ -793,7 +801,6 @@ class RouterItem {
793
801
  class RouterPlugin extends Plugin {
794
802
  priority = 100;
795
803
  name = 'router';
796
- //stack: Middleware[] = []
797
804
  _tree = {};
798
805
  constructor() {
799
806
  super();
@@ -916,7 +923,8 @@ class RouterPlugin extends Plugin {
916
923
  this._tree = {};
917
924
  }
918
925
  handler(req, res, next) {
919
- this.walk(this._getStack(req.pathname, ['hook', req.method, '*']), req, res, next);
926
+ const method = req.method === 'HEAD' ? 'GET' : req.method || 'GET';
927
+ this.walk(this._getStack(req.pathname, ['hook', method, '*']), req, res, next);
920
928
  }
921
929
  }
922
930
  export class CorsPlugin extends Plugin {
@@ -973,10 +981,6 @@ export class MethodsPlugin extends Plugin {
973
981
  res.setHeader('Allow', this._methods);
974
982
  return res.status(405).end();
975
983
  }
976
- if (req.method === 'HEAD') {
977
- req.method = 'GET';
978
- res.headersOnly = true;
979
- }
980
984
  return next();
981
985
  }
982
986
  }
@@ -989,22 +993,11 @@ export class BodyPlugin extends Plugin {
989
993
  this._maxBodySize = options?.maxBodySize || defaultMaxBodySize;
990
994
  }
991
995
  handler(req, res, next) {
992
- if (req.complete || req.method === 'GET') {
996
+ if (req.complete || req.method === 'GET' || req.method === 'HEAD') {
993
997
  if (!req.body)
994
- req._body = {};
998
+ req.setBody({});
995
999
  return next();
996
1000
  }
997
- req._isReady = deferPromise((err) => {
998
- req._isReady = undefined;
999
- if (err) {
1000
- if (!req.complete)
1001
- req.pause();
1002
- if (!res.headersSent)
1003
- res.error('statusCode' in err ? err.statusCode : 400);
1004
- }
1005
- else if (req.complete)
1006
- res.removeHeader('Connection');
1007
- });
1008
1001
  const contentType = req.headers['content-type'] || '';
1009
1002
  if (contentType.startsWith('multipart/form-data')) {
1010
1003
  req.pause();
@@ -1012,44 +1005,35 @@ export class BodyPlugin extends Plugin {
1012
1005
  return next();
1013
1006
  }
1014
1007
  if (parseInt(req.headers['content-length'] || '-1') > this._maxBodySize) {
1015
- return req._isReady?.resolve(new ResponseError("too big", 413));
1008
+ return req.setReady(new ResponseError("too big", 413));
1016
1009
  }
1017
1010
  req.once('error', () => { })
1018
1011
  .on('data', chunk => {
1019
1012
  req.rawBodySize += chunk.length;
1020
1013
  if (req.rawBodySize >= this._maxBodySize)
1021
- req._isReady?.resolve(new ResponseError("too big", 413));
1014
+ req.setReady(new ResponseError("too big", 413));
1022
1015
  else
1023
1016
  req.rawBody.push(chunk);
1024
1017
  })
1025
1018
  .once('end', () => {
1026
- req._isReady?.resolve();
1027
- Object.defineProperty(req, 'body', {
1028
- get: () => {
1029
- if (!req._body) {
1030
- let charset = contentType.match(/charset=(\S+)/)?.[1];
1031
- if (charset !== 'utf8' && charset !== 'latin1' && charset !== 'ascii')
1032
- charset = 'utf8';
1033
- const bodyString = Buffer.concat(req.rawBody).toString(charset);
1034
- if (contentType.startsWith('application/x-www-form-urlencoded')) {
1035
- req._body = querystring.parse(bodyString);
1036
- }
1037
- else if (bodyString.startsWith('{') || bodyString.startsWith('[')) {
1038
- try {
1039
- req._body = JSON.parse(bodyString);
1040
- }
1041
- catch {
1042
- return res.jsonError(405);
1043
- }
1044
- }
1045
- else
1046
- req._body = {};
1047
- }
1048
- return req._body;
1049
- },
1050
- configurable: true,
1051
- enumerable: true
1052
- });
1019
+ let charset = contentType.match(/charset=(\S+)/)?.[1];
1020
+ if (charset !== 'utf8' && charset !== 'latin1' && charset !== 'ascii')
1021
+ charset = 'utf8';
1022
+ const bodyString = Buffer.concat(req.rawBody).toString(charset);
1023
+ if (contentType.startsWith('application/x-www-form-urlencoded')) {
1024
+ req.setBody(querystring.parse(bodyString));
1025
+ }
1026
+ else if (bodyString.startsWith('{') || bodyString.startsWith('[')) {
1027
+ try {
1028
+ req.setBody(JSON.parse(bodyString));
1029
+ }
1030
+ catch {
1031
+ return res.jsonError(405);
1032
+ }
1033
+ }
1034
+ else
1035
+ req.setBody({});
1036
+ req.setReady();
1053
1037
  return next();
1054
1038
  });
1055
1039
  }
@@ -1065,24 +1049,11 @@ export class UploadPlugin extends Plugin {
1065
1049
  this._uploadDir = options?.uploadDir;
1066
1050
  }
1067
1051
  handler(req, res, next) {
1068
- if (!req.readable || req.method === 'GET')
1052
+ if (!req.readable || req.method === 'GET' || req.method === 'HEAD')
1069
1053
  return next();
1070
1054
  const contentType = req.headers['content-type'] || '';
1071
1055
  if (!contentType.startsWith('multipart/form-data'))
1072
1056
  return next();
1073
- if (!req._isReady) {
1074
- req._isReady = deferPromise((err) => {
1075
- req._isReady = undefined;
1076
- if (err) {
1077
- req.pause();
1078
- if (!res.headersSent)
1079
- res.setHeader('Connection', 'close');
1080
- res.error('statusCode' in err ? err.statusCode : 400);
1081
- }
1082
- else
1083
- res.removeHeader('Connection');
1084
- });
1085
- }
1086
1057
  req.pause();
1087
1058
  res.setHeader('Connection', 'close');
1088
1059
  if (!this._uploadDir)
@@ -1152,7 +1123,7 @@ export class UploadPlugin extends Plugin {
1152
1123
  fileStream = undefined;
1153
1124
  if (buffer[nextBoundaryIndexEnd] === 45) {
1154
1125
  res.removeHeader('Connection');
1155
- req._isReady?.resolve();
1126
+ req.setReady();
1156
1127
  }
1157
1128
  buffer = buffer.subarray(nextBoundaryIndex);
1158
1129
  }
@@ -1170,7 +1141,7 @@ export class UploadPlugin extends Plugin {
1170
1141
  };
1171
1142
  const _removeTempFiles = () => {
1172
1143
  if (!req.isReady)
1173
- req._isReady?.resolve(new Error('Upload error'));
1144
+ req.setReady(new Error('Upload error'));
1174
1145
  if (fileStream) {
1175
1146
  fileStream.close();
1176
1147
  fileStream = undefined;
@@ -1181,12 +1152,12 @@ export class UploadPlugin extends Plugin {
1181
1152
  delete f.filePath;
1182
1153
  });
1183
1154
  files.splice(0);
1184
- req._isReady?.resolve();
1155
+ req.setReady();
1185
1156
  };
1186
1157
  next();
1187
- req.once('error', () => req._isReady?.resolve(new Error('Upload error')))
1158
+ req.once('error', () => req.setReady(new Error('Upload error')))
1188
1159
  .on('data', chunk => chunkParse(chunk))
1189
- .once('end', () => req._isReady?.resolve(new Error('Upload error')));
1160
+ .once('end', () => req.setReady(new Error('Upload error')));
1190
1161
  res.once('finish', () => _removeTempFiles());
1191
1162
  res.once('error', () => _removeTempFiles());
1192
1163
  res.once('close', () => _removeTempFiles());
@@ -1230,9 +1201,12 @@ export class WebSocket extends EventEmitter {
1230
1201
  this._options.deflate = true;
1231
1202
  }
1232
1203
  this.ready = true;
1233
- this._upgrade(key, headers);
1204
+ this._upgrade(key, headers, () => {
1205
+ req.setReady();
1206
+ this.emit('open');
1207
+ });
1234
1208
  }
1235
- _upgrade(key, headers = []) {
1209
+ _upgrade(key, headers = [], cb) {
1236
1210
  const digest = crypto.createHash('sha1')
1237
1211
  .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
1238
1212
  .digest('base64');
@@ -1245,7 +1219,7 @@ export class WebSocket extends EventEmitter {
1245
1219
  '',
1246
1220
  ''
1247
1221
  ];
1248
- this._socket.write(headers.join('\r\n'));
1222
+ this._socket.write(headers.join('\r\n'), cb);
1249
1223
  this._socket.on('error', this._errorHandler.bind(this));
1250
1224
  this._socket.on('data', this._dataHandler.bind(this));
1251
1225
  this._socket.on('close', () => this.emit('close'));
@@ -1511,7 +1485,6 @@ export class WebSocketPlugin extends Plugin {
1511
1485
  srv.on('upgrade', this._handler);
1512
1486
  }
1513
1487
  upgradeHandler(server, req, socket, head) {
1514
- ServerRequest.extend(req, server);
1515
1488
  const host = req.headers.host || '';
1516
1489
  const vhostPlugin = server.getPlugin('vhost');
1517
1490
  const vserver = vhostPlugin?.vhosts?.[host] || server;
@@ -1524,7 +1497,7 @@ export class WebSocketPlugin extends Plugin {
1524
1497
  statusCode: 200,
1525
1498
  socket,
1526
1499
  server,
1527
- write(data) {
1500
+ end(data) {
1528
1501
  if (res.headersSent)
1529
1502
  throw new Error('Headers already sent');
1530
1503
  let code = res.statusCode || 403;
@@ -1538,7 +1511,7 @@ export class WebSocketPlugin extends Plugin {
1538
1511
  const headers = [
1539
1512
  `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
1540
1513
  'Connection: close',
1541
- 'Content-Type: text/html',
1514
+ 'Content-Type: text/plain',
1542
1515
  `Content-Length: ${Buffer.byteLength(data)}`,
1543
1516
  '',
1544
1517
  data
@@ -1547,22 +1520,25 @@ export class WebSocketPlugin extends Plugin {
1547
1520
  },
1548
1521
  error(code) {
1549
1522
  res.statusCode = code || 403;
1550
- res.write();
1551
- },
1552
- end(data) {
1553
- res.write(data);
1523
+ res.end();
1554
1524
  },
1555
1525
  send(data) {
1556
- res.write(data);
1526
+ res.end(data);
1557
1527
  },
1558
1528
  getHeader() { },
1559
1529
  setHeader() { }
1560
1530
  };
1531
+ ServerRequest.extend(req, res, server);
1532
+ let _ws;
1533
+ Object.defineProperty(req, 'websocket', {
1534
+ get: () => {
1535
+ if (!_ws)
1536
+ _ws = new WebSocket(req, server.config.websocket);
1537
+ return _ws;
1538
+ },
1539
+ enumerable: true
1540
+ });
1561
1541
  vserver.handler(req, res);
1562
- //vserver.handlerRouter(req, res as unknown as ServerResponse, () => res.error(404))
1563
- }
1564
- static create(req, options) {
1565
- return new WebSocket(req, options);
1566
1542
  }
1567
1543
  }
1568
1544
  // #endregion WebSocket
@@ -1731,7 +1707,7 @@ export class StaticFilesPlugin extends Plugin {
1731
1707
  }
1732
1708
  /** Default static files handler */
1733
1709
  handler(req, res, next) {
1734
- if (req.method !== 'GET')
1710
+ if (req.method !== 'GET' && req.method !== 'HEAD')
1735
1711
  return next();
1736
1712
  if (!('path' in req.params)) { // global handler
1737
1713
  if (req.path.startsWith(this.prefix) && (req.path === this.prefix || req.path[this.prefix.length] === '/')) {
@@ -1772,6 +1748,7 @@ export class StaticFilesPlugin extends Plugin {
1772
1748
  });
1773
1749
  });
1774
1750
  }
1751
+ /** Send static file */
1775
1752
  serveFile(req, res, options) {
1776
1753
  const filePath = path.isAbsolute(options.path) ? options.path : path.join(options.root || this.root, options.path);
1777
1754
  const statRes = (err, stats) => {
@@ -1794,14 +1771,12 @@ export class StaticFilesPlugin extends Plugin {
1794
1771
  res.setHeader('Content-Length', stats.size);
1795
1772
  if (options.etag !== false) {
1796
1773
  const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1797
- if (req.headers['if-none-match'] === etag || req.headers['if-modified-since'] === stats.mtime.toUTCString()) {
1774
+ if (req.headers['if-none-match'] === etag || req.headers['if-modified-since'] === stats.mtime.toUTCString())
1798
1775
  res.statusCode = 304;
1799
- res.headersOnly = true;
1800
- }
1801
1776
  }
1802
1777
  if (options.maxAge)
1803
1778
  res.setHeader('Cache-Control', 'max-age=' + options.maxAge);
1804
- if (res.headersOnly) {
1779
+ if (res.statusCode === 304 || req.method === 'HEAD') {
1805
1780
  res.end();
1806
1781
  return;
1807
1782
  }
@@ -2598,11 +2573,13 @@ export class FileStore {
2598
2573
  });
2599
2574
  return p;
2600
2575
  }
2576
+ /** close store */
2601
2577
  async close() {
2602
2578
  await this.sync();
2603
2579
  this._iter = 0;
2604
2580
  this._cache = {};
2605
2581
  }
2582
+ /** sync data to disk */
2606
2583
  async sync() {
2607
2584
  for (const name in this._cache) {
2608
2585
  for (const key in this._cache)
@@ -3334,10 +3311,12 @@ export class MicroCollection {
3334
3311
  async deleteMany(query) {
3335
3312
  return (await this.updateMany(query, {}, { delete: true })).modifiedCount;
3336
3313
  }
3314
+ /** Update one matching document */
3337
3315
  async updateOne(query, update, options) {
3338
3316
  const res = await this.updateMany(query, update, { ...options, limit: 1 });
3339
3317
  return res;
3340
3318
  }
3319
+ /** Update many matching documents */
3341
3320
  async updateMany(query, update, options) {
3342
3321
  let res = { upsertedId: undefined, modifiedCount: 0 };
3343
3322
  if (!query)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radatek/microserver",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "HTTP MicroServer",
5
5
  "author": "Darius Kisonas",
6
6
  "license": "MIT",