@radatek/microserver 3.0.3 → 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
@@ -87,10 +87,15 @@ export declare class ServerRequest<T = any> extends http.IncomingMessage {
87
87
  /** Request raw body size */
88
88
  rawBodySize: number;
89
89
  private constructor();
90
+ /** Extend http.IncomingMessage */
90
91
  static extend(req: http.IncomingMessage, res: http.ServerResponse, server: MicroServer): ServerRequest;
92
+ /** Check if request is ready */
91
93
  get isReady(): boolean;
94
+ /** Wait for request to be ready, usualy used to wait for file upload */
92
95
  waitReady(): Promise<void>;
96
+ /** Signal request as ready */
93
97
  setReady(err?: Error): void;
98
+ /** Set request body */
94
99
  setBody(body: ServerRequestBody<T>): void;
95
100
  /** Update request url */
96
101
  updateUrl(url: string): this;
@@ -108,9 +113,10 @@ export declare class ServerRequest<T = any> extends http.IncomingMessage {
108
113
  /** Extends http.ServerResponse */
109
114
  export declare class ServerResponse<T = any> extends http.ServerResponse {
110
115
  readonly req: ServerRequest<T>;
116
+ /** Should response be json */
111
117
  isJson: boolean;
112
- headersOnly: boolean;
113
118
  private constructor();
119
+ /** Extends http.ServerResponse */
114
120
  static extend(res: http.ServerResponse): void;
115
121
  /** Send error reponse */
116
122
  error(error: string | number | Error): void;
@@ -167,11 +173,13 @@ interface MicroServerEvents {
167
173
  }
168
174
  /** Lighweight HTTP server */
169
175
  export declare class MicroServer extends EventEmitter {
176
+ /** MicroServer configuration */
170
177
  config: MicroServerConfig;
178
+ /** Authorization object */
171
179
  auth?: Auth;
172
- /** all sockets */
180
+ /** All sockets */
173
181
  sockets: Set<net.Socket> | undefined;
174
- /** server instances */
182
+ /** Server instances */
175
183
  servers: Set<net.Server> | undefined;
176
184
  /** @param {MicroServerConfig} [config] MicroServer configuration */
177
185
  constructor(config?: MicroServerConfig);
@@ -184,40 +192,39 @@ export declare class MicroServer extends EventEmitter {
184
192
  waitReady(): Promise<void>;
185
193
  /** Listen server, should be used only if config.listen is not set */
186
194
  listen(config: ListenConfig): Promise<void>;
187
- /** 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' */
188
- _bind(fn: string | Function | object): Function;
189
195
  /** Default server handler */
190
196
  handler(req: ServerRequest, res: ServerResponse): void;
191
- /** Last request handler */
192
- handlerLast(req: ServerRequest, res: ServerResponse, next?: Function): any;
193
197
  /** Clear routes and middlewares */
194
198
  clear(): this;
195
199
  /**
196
- * Add middleware route.
200
+ * Add middleware, plugin or routes to server.
197
201
  * Middlewares may return promises for res.jsonSuccess(...), throw errors for res.error(...), return string or {} for res.send(...)
198
202
  * RouteURL: 'METHOD /suburl', 'METHOD', '* /suburl'
199
203
  */
200
204
  use(...args: [
201
205
  Middleware | Plugin | ControllerClass | RoutesSet
202
206
  ] | [Promise<Middleware | Plugin | ControllerClass | RoutesSet>] | RoutesList | [RouteURL, RoutesSet] | [PluginClass | Promise<PluginClass>, options?: any]): Promise<void>;
207
+ /** Add middleware to stack, with optional priority */
203
208
  addStack(middleware: Middleware): void;
209
+ /** Get plugin */
204
210
  getPlugin(id: string): Plugin | undefined;
211
+ /** Wait for plugin */
205
212
  waitPlugin(id: string): Promise<Plugin>;
206
- /** Add route, alias to `server.router.use(url, ...args)` */
213
+ /** Add route, alias to `server.use(url, ...args)` */
207
214
  all(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
208
- /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
215
+ /** Add route, alias to `server.use('GET ' + url, ...args)` */
209
216
  get(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
210
- /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
217
+ /** Add route, alias to `server.use('POST ' + url, ...args)` */
211
218
  post(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
212
- /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
219
+ /** Add route, alias to `server.use('PUT ' + url, ...args)` */
213
220
  put(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
214
- /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
221
+ /** Add route, alias to `server.use('PATCH ' + url, ...args)` */
215
222
  patch(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
216
- /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
223
+ /** Add route, alias to `server.use('DELETE ' + url, ...args)` */
217
224
  delete(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
218
- /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
225
+ /** Add websocket handler, alias to `server.use('WEBSOCKET ' + url, ...args)` */
219
226
  websocket(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
220
- /** Add router hook, alias to `server.router.hook(url, ...args)` */
227
+ /** Add router hook, alias to `server.hook(url, ...args)` */
221
228
  hook(url: RouteURL, ...args: RoutesMiddleware[]): MicroServer;
222
229
  /** Check if middleware allready added */
223
230
  has(mid: Middleware): boolean;
@@ -263,8 +270,6 @@ export declare class CorsPlugin extends Plugin {
263
270
  export declare class MethodsPlugin extends Plugin {
264
271
  priority: number;
265
272
  name: string;
266
- _methods: string;
267
- _methodsIdx: Record<string, boolean>;
268
273
  constructor(methods?: string);
269
274
  handler(req: ServerRequest, res: ServerResponse, next: Function): any;
270
275
  }
@@ -274,7 +279,6 @@ export interface BodyOptions {
274
279
  export declare class BodyPlugin extends Plugin {
275
280
  priority: number;
276
281
  name: string;
277
- _maxBodySize: number;
278
282
  constructor(options?: BodyOptions);
279
283
  handler(req: ServerRequest, res: ServerResponse, next: () => void): void;
280
284
  }
@@ -292,8 +296,6 @@ export interface UploadFile {
292
296
  export declare class UploadPlugin extends Plugin {
293
297
  priority: number;
294
298
  name: string;
295
- _maxFileSize: number;
296
- _uploadDir?: string;
297
299
  constructor(options?: UploadOptions);
298
300
  handler(req: ServerRequest, res: ServerResponse, next: () => void): void;
299
301
  }
@@ -335,7 +337,6 @@ export declare class WebSocket extends EventEmitter {
335
337
  ping(buffer?: Buffer): void;
336
338
  /** Send pong frame */
337
339
  pong(buffer?: Buffer): void;
338
- protected _sendFrame(opcode: number, data: Buffer, cb?: () => void): void;
339
340
  on<K extends keyof WebSocketEvents>(event: K, listener: WebSocketEvents[K]): this;
340
341
  addListener<K extends keyof WebSocketEvents>(event: K, listener: WebSocketEvents[K]): this;
341
342
  once<K extends keyof WebSocketEvents>(event: K, listener: WebSocketEvents[K]): this;
@@ -344,9 +345,8 @@ export declare class WebSocket extends EventEmitter {
344
345
  }
345
346
  export declare class WebSocketPlugin extends Plugin {
346
347
  name: string;
347
- _handler: (req: ServerRequest, socket: net.Socket, head: any) => void;
348
348
  constructor(options?: any, server?: MicroServer);
349
- addUpgradeHandler(srv: http.Server): void;
349
+ private addUpgradeHandler;
350
350
  upgradeHandler(server: MicroServer, req: ServerRequest, socket: net.Socket, head: any): void;
351
351
  }
352
352
  /** Trust proxy plugin, adds `req.ip` and `req.localip` */
@@ -447,6 +447,7 @@ export declare class StaticFilesPlugin extends Plugin {
447
447
  constructor(options?: StaticFilesOptions | string, server?: MicroServer);
448
448
  /** Default static files handler */
449
449
  handler(req: ServerRequest, res: ServerResponse, next: Function): any;
450
+ /** Send static file */
450
451
  serveFile(req: ServerRequest, res: ServerResponse, options: ServeFileOptions): void;
451
452
  }
452
453
  /** Proxy plugin options */
@@ -705,7 +706,9 @@ export declare class FileStore {
705
706
  constructor(options?: FileStoreOptions);
706
707
  /** cleanup cache */
707
708
  cleanup(): void;
709
+ /** close store */
708
710
  close(): Promise<void>;
711
+ /** sync data to disk */
709
712
  sync(): Promise<void>;
710
713
  /** load json file data */
711
714
  load(name: string, autosave?: boolean): Promise<any>;
@@ -923,10 +926,12 @@ export declare class MicroCollection<TSchema extends ModelSchema = any> {
923
926
  deleteOne(query: Query): Promise<number>;
924
927
  /** Delete all matching documents */
925
928
  deleteMany(query: Query): Promise<number>;
929
+ /** Update one matching document */
926
930
  updateOne(query: Query, update: Record<string, any>, options?: FindOptions): Promise<{
927
931
  upsertedId: any;
928
932
  modifiedCount: number;
929
933
  }>;
934
+ /** Update many matching documents */
930
935
  updateMany(query: Query, update: Record<string, any>, options?: FindOptions): Promise<{
931
936
  upsertedId: any;
932
937
  modifiedCount: number;
package/microserver.js CHANGED
@@ -122,6 +122,7 @@ export class ServerRequest extends http.IncomingMessage {
122
122
  super(new net.Socket());
123
123
  ServerRequest.extend(this, res, server);
124
124
  }
125
+ /** Extend http.IncomingMessage */
125
126
  static extend(req, res, server) {
126
127
  const reqNew = Object.setPrototypeOf(req, ServerRequest.prototype);
127
128
  let ip = req.socket.remoteAddress || '::1';
@@ -153,9 +154,11 @@ export class ServerRequest extends http.IncomingMessage {
153
154
  reqNew.updateUrl(req.url || '/');
154
155
  return reqNew;
155
156
  }
157
+ /** Check if request is ready */
156
158
  get isReady() {
157
159
  return this._isReady === undefined;
158
160
  }
161
+ /** Wait for request to be ready, usualy used to wait for file upload */
159
162
  async waitReady() {
160
163
  if (this._isReady === undefined)
161
164
  return;
@@ -163,9 +166,11 @@ export class ServerRequest extends http.IncomingMessage {
163
166
  if (res && res instanceof Error)
164
167
  throw res;
165
168
  }
169
+ /** Signal request as ready */
166
170
  setReady(err) {
167
171
  this._isReady?.resolve(err);
168
172
  }
173
+ /** Set request body */
169
174
  setBody(body) {
170
175
  this._body = body;
171
176
  }
@@ -205,19 +210,19 @@ export class ServerRequest extends http.IncomingMessage {
205
210
  }
206
211
  /** Extends http.ServerResponse */
207
212
  export class ServerResponse extends http.ServerResponse {
213
+ /** Should response be json */
208
214
  isJson;
209
- headersOnly;
210
215
  constructor(server) {
211
216
  super(new http.IncomingMessage(new net.Socket()));
212
217
  ServerRequest.extend(this.req, this, server);
213
218
  ServerResponse.extend(this);
214
219
  }
220
+ /** Extends http.ServerResponse */
215
221
  static extend(res) {
216
222
  Object.setPrototypeOf(res, ServerResponse.prototype);
217
223
  Object.assign(res, {
218
224
  statusCode: 200,
219
- isJson: false,
220
- headersOnly: false
225
+ isJson: false
221
226
  });
222
227
  }
223
228
  /** Send error reponse */
@@ -282,7 +287,7 @@ export class ServerResponse extends http.ServerResponse {
282
287
  return (data.pipe(this, { end: true }), void 0);
283
288
  if (data instanceof Buffer) {
284
289
  this.setHeader('Content-Length', data.byteLength);
285
- if (this.headersOnly)
290
+ if (this.statusCode === 304 || this.req.method === 'HEAD')
286
291
  this.end();
287
292
  else
288
293
  this.end(data);
@@ -301,7 +306,7 @@ export class ServerResponse extends http.ServerResponse {
301
306
  this.setHeader('Content-Type', 'text/plain');
302
307
  }
303
308
  this.setHeader('Content-Length', Buffer.byteLength(data, 'utf8'));
304
- if (this.headersOnly)
309
+ if (this.statusCode === 304 || this.req.method === 'HEAD')
305
310
  this.end();
306
311
  else
307
312
  this.end(data, 'utf8');
@@ -356,15 +361,17 @@ export class ServerResponse extends http.ServerResponse {
356
361
  }
357
362
  /** Lighweight HTTP server */
358
363
  export class MicroServer extends EventEmitter {
364
+ /** MicroServer configuration */
359
365
  config;
366
+ /** Authorization object */
360
367
  auth;
361
368
  _plugins = {};
362
369
  _stack = [];
363
370
  _router = new RouterPlugin();
364
371
  _worker = new Worker();
365
- /** all sockets */
372
+ /** All sockets */
366
373
  sockets;
367
- /** server instances */
374
+ /** Server instances */
368
375
  servers;
369
376
  /** @param {MicroServerConfig} [config] MicroServer configuration */
370
377
  constructor(config) {
@@ -491,7 +498,7 @@ export class MicroServer extends EventEmitter {
491
498
  });
492
499
  return this._worker.wait('listen');
493
500
  }
494
- /** 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' */
495
502
  _bind(fn) {
496
503
  if (typeof fn === 'string') {
497
504
  let name = fn;
@@ -591,15 +598,6 @@ export class MicroServer extends EventEmitter {
591
598
  ServerRequest.extend(req, res, this);
592
599
  ServerResponse.extend(res);
593
600
  this._router.walk(this._stack, req, res, () => res.error(404));
594
- //this.handlerRouter(req, res, () => this.handlerLast(req, res))
595
- }
596
- /** Last request handler */
597
- handlerLast(req, res, next) {
598
- if (res.headersSent || res.closed)
599
- return;
600
- if (!next)
601
- next = () => res.error(404);
602
- return next();
603
601
  }
604
602
  /** Clear routes and middlewares */
605
603
  clear() {
@@ -610,7 +608,7 @@ export class MicroServer extends EventEmitter {
610
608
  return this;
611
609
  }
612
610
  /**
613
- * Add middleware route.
611
+ * Add middleware, plugin or routes to server.
614
612
  * Middlewares may return promises for res.jsonSuccess(...), throw errors for res.error(...), return string or {} for res.send(...)
615
613
  * RouteURL: 'METHOD /suburl', 'METHOD', '* /suburl'
616
614
  */
@@ -704,6 +702,7 @@ export class MicroServer extends EventEmitter {
704
702
  this._worker.endJob('plugin:' + plugin.name);
705
703
  }
706
704
  }
705
+ /** Add middleware to stack, with optional priority */
707
706
  addStack(middleware) {
708
707
  if (middleware.plugin?.name && this.getPlugin(middleware.plugin.name))
709
708
  throw new Error(`Plugin ${middleware.plugin.name} already added`);
@@ -711,9 +710,11 @@ export class MicroServer extends EventEmitter {
711
710
  const idx = this._stack.findLastIndex(f => (f.priority || 0) <= priority);
712
711
  this._stack.splice(idx + 1, 0, middleware);
713
712
  }
713
+ /** Get plugin */
714
714
  getPlugin(id) {
715
715
  return this._plugins[id];
716
716
  }
717
+ /** Wait for plugin */
717
718
  async waitPlugin(id) {
718
719
  const p = this.getPlugin(id);
719
720
  if (p)
@@ -722,42 +723,42 @@ export class MicroServer extends EventEmitter {
722
723
  await this._worker.wait('plugin:' + id);
723
724
  return this.getPlugin(id);
724
725
  }
725
- /** Add route, alias to `server.router.use(url, ...args)` */
726
+ /** Add route, alias to `server.use(url, ...args)` */
726
727
  all(url, ...args) {
727
728
  this.use('* ' + url, ...args);
728
729
  return this;
729
730
  }
730
- /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
731
+ /** Add route, alias to `server.use('GET ' + url, ...args)` */
731
732
  get(url, ...args) {
732
733
  this.use('GET ' + url, ...args);
733
734
  return this;
734
735
  }
735
- /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
736
+ /** Add route, alias to `server.use('POST ' + url, ...args)` */
736
737
  post(url, ...args) {
737
738
  this.use('POST ' + url, ...args);
738
739
  return this;
739
740
  }
740
- /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
741
+ /** Add route, alias to `server.use('PUT ' + url, ...args)` */
741
742
  put(url, ...args) {
742
743
  this.use('PUT ' + url, ...args);
743
744
  return this;
744
745
  }
745
- /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
746
+ /** Add route, alias to `server.use('PATCH ' + url, ...args)` */
746
747
  patch(url, ...args) {
747
748
  this.use('PATCH ' + url, ...args);
748
749
  return this;
749
750
  }
750
- /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
751
+ /** Add route, alias to `server.use('DELETE ' + url, ...args)` */
751
752
  delete(url, ...args) {
752
753
  this.use('DELETE ' + url, ...args);
753
754
  return this;
754
755
  }
755
- /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
756
+ /** Add websocket handler, alias to `server.use('WEBSOCKET ' + url, ...args)` */
756
757
  websocket(url, ...args) {
757
758
  this.use('WEBSOCKET ' + url, ...args);
758
759
  return this;
759
760
  }
760
- /** Add router hook, alias to `server.router.hook(url, ...args)` */
761
+ /** Add router hook, alias to `server.hook(url, ...args)` */
761
762
  hook(url, ...args) {
762
763
  const m = url.match(/^([A-Z]+) (.*)/) || ['', 'hook', url];
763
764
  this._router.add(m[1], m[2], args.filter(m => m).map(m => this._bind(m)), false);
@@ -800,7 +801,6 @@ class RouterItem {
800
801
  class RouterPlugin extends Plugin {
801
802
  priority = 100;
802
803
  name = 'router';
803
- //stack: Middleware[] = []
804
804
  _tree = {};
805
805
  constructor() {
806
806
  super();
@@ -923,7 +923,8 @@ class RouterPlugin extends Plugin {
923
923
  this._tree = {};
924
924
  }
925
925
  handler(req, res, next) {
926
- 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);
927
928
  }
928
929
  }
929
930
  export class CorsPlugin extends Plugin {
@@ -980,10 +981,6 @@ export class MethodsPlugin extends Plugin {
980
981
  res.setHeader('Allow', this._methods);
981
982
  return res.status(405).end();
982
983
  }
983
- if (req.method === 'HEAD') {
984
- req.method = 'GET';
985
- res.headersOnly = true;
986
- }
987
984
  return next();
988
985
  }
989
986
  }
@@ -996,7 +993,7 @@ export class BodyPlugin extends Plugin {
996
993
  this._maxBodySize = options?.maxBodySize || defaultMaxBodySize;
997
994
  }
998
995
  handler(req, res, next) {
999
- if (req.complete || req.method === 'GET') {
996
+ if (req.complete || req.method === 'GET' || req.method === 'HEAD') {
1000
997
  if (!req.body)
1001
998
  req.setBody({});
1002
999
  return next();
@@ -1052,7 +1049,7 @@ export class UploadPlugin extends Plugin {
1052
1049
  this._uploadDir = options?.uploadDir;
1053
1050
  }
1054
1051
  handler(req, res, next) {
1055
- if (!req.readable || req.method === 'GET')
1052
+ if (!req.readable || req.method === 'GET' || req.method === 'HEAD')
1056
1053
  return next();
1057
1054
  const contentType = req.headers['content-type'] || '';
1058
1055
  if (!contentType.startsWith('multipart/form-data'))
@@ -1710,7 +1707,7 @@ export class StaticFilesPlugin extends Plugin {
1710
1707
  }
1711
1708
  /** Default static files handler */
1712
1709
  handler(req, res, next) {
1713
- if (req.method !== 'GET')
1710
+ if (req.method !== 'GET' && req.method !== 'HEAD')
1714
1711
  return next();
1715
1712
  if (!('path' in req.params)) { // global handler
1716
1713
  if (req.path.startsWith(this.prefix) && (req.path === this.prefix || req.path[this.prefix.length] === '/')) {
@@ -1751,6 +1748,7 @@ export class StaticFilesPlugin extends Plugin {
1751
1748
  });
1752
1749
  });
1753
1750
  }
1751
+ /** Send static file */
1754
1752
  serveFile(req, res, options) {
1755
1753
  const filePath = path.isAbsolute(options.path) ? options.path : path.join(options.root || this.root, options.path);
1756
1754
  const statRes = (err, stats) => {
@@ -1773,14 +1771,12 @@ export class StaticFilesPlugin extends Plugin {
1773
1771
  res.setHeader('Content-Length', stats.size);
1774
1772
  if (options.etag !== false) {
1775
1773
  const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1776
- 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())
1777
1775
  res.statusCode = 304;
1778
- res.headersOnly = true;
1779
- }
1780
1776
  }
1781
1777
  if (options.maxAge)
1782
1778
  res.setHeader('Cache-Control', 'max-age=' + options.maxAge);
1783
- if (res.headersOnly) {
1779
+ if (res.statusCode === 304 || req.method === 'HEAD') {
1784
1780
  res.end();
1785
1781
  return;
1786
1782
  }
@@ -2577,11 +2573,13 @@ export class FileStore {
2577
2573
  });
2578
2574
  return p;
2579
2575
  }
2576
+ /** close store */
2580
2577
  async close() {
2581
2578
  await this.sync();
2582
2579
  this._iter = 0;
2583
2580
  this._cache = {};
2584
2581
  }
2582
+ /** sync data to disk */
2585
2583
  async sync() {
2586
2584
  for (const name in this._cache) {
2587
2585
  for (const key in this._cache)
@@ -3313,10 +3311,12 @@ export class MicroCollection {
3313
3311
  async deleteMany(query) {
3314
3312
  return (await this.updateMany(query, {}, { delete: true })).modifiedCount;
3315
3313
  }
3314
+ /** Update one matching document */
3316
3315
  async updateOne(query, update, options) {
3317
3316
  const res = await this.updateMany(query, update, { ...options, limit: 1 });
3318
3317
  return res;
3319
3318
  }
3319
+ /** Update many matching documents */
3320
3320
  async updateMany(query, update, options) {
3321
3321
  let res = { upsertedId: undefined, modifiedCount: 0 };
3322
3322
  if (!query)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radatek/microserver",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "HTTP MicroServer",
5
5
  "author": "Darius Kisonas",
6
6
  "license": "MIT",