@radatek/microserver 3.0.3 → 3.0.5

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.3
3
+ * @version 3.0.5
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 3.0.3
3
+ * @version 3.0.5
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -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
  */
@@ -697,13 +695,16 @@ export class MicroServer extends EventEmitter {
697
695
  if (routes)
698
696
  await this.use(routes);
699
697
  }
700
- if (plugin.handler && plugin.name) {
698
+ if (plugin.name) {
699
+ if (this._plugins[plugin.name])
700
+ throw new Error(`Plugin ${plugin.name} already added`);
701
701
  this._plugins[plugin.name] = plugin;
702
702
  this.emit('plugin', plugin.name);
703
703
  this.emit('plugin:' + plugin.name);
704
704
  this._worker.endJob('plugin:' + plugin.name);
705
705
  }
706
706
  }
707
+ /** Add middleware to stack, with optional priority */
707
708
  addStack(middleware) {
708
709
  if (middleware.plugin?.name && this.getPlugin(middleware.plugin.name))
709
710
  throw new Error(`Plugin ${middleware.plugin.name} already added`);
@@ -711,9 +712,11 @@ export class MicroServer extends EventEmitter {
711
712
  const idx = this._stack.findLastIndex(f => (f.priority || 0) <= priority);
712
713
  this._stack.splice(idx + 1, 0, middleware);
713
714
  }
715
+ /** Get plugin */
714
716
  getPlugin(id) {
715
717
  return this._plugins[id];
716
718
  }
719
+ /** Wait for plugin */
717
720
  async waitPlugin(id) {
718
721
  const p = this.getPlugin(id);
719
722
  if (p)
@@ -722,42 +725,42 @@ export class MicroServer extends EventEmitter {
722
725
  await this._worker.wait('plugin:' + id);
723
726
  return this.getPlugin(id);
724
727
  }
725
- /** Add route, alias to `server.router.use(url, ...args)` */
728
+ /** Add route, alias to `server.use(url, ...args)` */
726
729
  all(url, ...args) {
727
730
  this.use('* ' + url, ...args);
728
731
  return this;
729
732
  }
730
- /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
733
+ /** Add route, alias to `server.use('GET ' + url, ...args)` */
731
734
  get(url, ...args) {
732
735
  this.use('GET ' + url, ...args);
733
736
  return this;
734
737
  }
735
- /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
738
+ /** Add route, alias to `server.use('POST ' + url, ...args)` */
736
739
  post(url, ...args) {
737
740
  this.use('POST ' + url, ...args);
738
741
  return this;
739
742
  }
740
- /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
743
+ /** Add route, alias to `server.use('PUT ' + url, ...args)` */
741
744
  put(url, ...args) {
742
745
  this.use('PUT ' + url, ...args);
743
746
  return this;
744
747
  }
745
- /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
748
+ /** Add route, alias to `server.use('PATCH ' + url, ...args)` */
746
749
  patch(url, ...args) {
747
750
  this.use('PATCH ' + url, ...args);
748
751
  return this;
749
752
  }
750
- /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
753
+ /** Add route, alias to `server.use('DELETE ' + url, ...args)` */
751
754
  delete(url, ...args) {
752
755
  this.use('DELETE ' + url, ...args);
753
756
  return this;
754
757
  }
755
- /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
758
+ /** Add websocket handler, alias to `server.use('WEBSOCKET ' + url, ...args)` */
756
759
  websocket(url, ...args) {
757
760
  this.use('WEBSOCKET ' + url, ...args);
758
761
  return this;
759
762
  }
760
- /** Add router hook, alias to `server.router.hook(url, ...args)` */
763
+ /** Add router hook, alias to `server.hook(url, ...args)` */
761
764
  hook(url, ...args) {
762
765
  const m = url.match(/^([A-Z]+) (.*)/) || ['', 'hook', url];
763
766
  this._router.add(m[1], m[2], args.filter(m => m).map(m => this._bind(m)), false);
@@ -800,7 +803,6 @@ class RouterItem {
800
803
  class RouterPlugin extends Plugin {
801
804
  priority = 100;
802
805
  name = 'router';
803
- //stack: Middleware[] = []
804
806
  _tree = {};
805
807
  constructor() {
806
808
  super();
@@ -923,7 +925,8 @@ class RouterPlugin extends Plugin {
923
925
  this._tree = {};
924
926
  }
925
927
  handler(req, res, next) {
926
- this.walk(this._getStack(req.pathname, ['hook', req.method, '*']), req, res, next);
928
+ const method = req.method === 'HEAD' ? 'GET' : req.method || 'GET';
929
+ this.walk(this._getStack(req.pathname, ['hook', method, '*']), req, res, next);
927
930
  }
928
931
  }
929
932
  export class CorsPlugin extends Plugin {
@@ -980,10 +983,6 @@ export class MethodsPlugin extends Plugin {
980
983
  res.setHeader('Allow', this._methods);
981
984
  return res.status(405).end();
982
985
  }
983
- if (req.method === 'HEAD') {
984
- req.method = 'GET';
985
- res.headersOnly = true;
986
- }
987
986
  return next();
988
987
  }
989
988
  }
@@ -996,7 +995,7 @@ export class BodyPlugin extends Plugin {
996
995
  this._maxBodySize = options?.maxBodySize || defaultMaxBodySize;
997
996
  }
998
997
  handler(req, res, next) {
999
- if (req.complete || req.method === 'GET') {
998
+ if (req.complete || req.method === 'GET' || req.method === 'HEAD') {
1000
999
  if (!req.body)
1001
1000
  req.setBody({});
1002
1001
  return next();
@@ -1052,7 +1051,7 @@ export class UploadPlugin extends Plugin {
1052
1051
  this._uploadDir = options?.uploadDir;
1053
1052
  }
1054
1053
  handler(req, res, next) {
1055
- if (!req.readable || req.method === 'GET')
1054
+ if (!req.readable || req.method === 'GET' || req.method === 'HEAD')
1056
1055
  return next();
1057
1056
  const contentType = req.headers['content-type'] || '';
1058
1057
  if (!contentType.startsWith('multipart/form-data'))
@@ -1675,9 +1674,6 @@ export class StaticFilesPlugin extends Plugin {
1675
1674
  options = {};
1676
1675
  if (typeof options === 'string')
1677
1676
  options = { root: options };
1678
- // allow multiple instances
1679
- if (server && !server.getPlugin('static'))
1680
- this.name = 'static';
1681
1677
  this.mimeTypes = options.mimeTypes ? { ...StaticFilesPlugin.mimeTypes, ...options.mimeTypes } : Object.freeze(StaticFilesPlugin.mimeTypes);
1682
1678
  this.root = (options.root && path.isAbsolute(options.root) ? options.root : path.resolve(options.root || options?.path || 'public')).replace(/[\/\\]$/, '') + path.sep;
1683
1679
  this.ignore = (options.ignore || []).map((p) => path.normalize(path.join(this.root, p)) + path.sep);
@@ -1689,28 +1685,32 @@ export class StaticFilesPlugin extends Plugin {
1689
1685
  this.errors = options.errors;
1690
1686
  this.prefix = ('/' + (options.path?.replace(/^[.\/]*/, '') || '').replace(/\/$/, '')).replace(/\/$/, '');
1691
1687
  const defSend = ServerResponse.prototype.send;
1692
- ServerResponse.prototype.send = function (data) {
1693
- const plugin = this.req.server.getPlugin('static');
1694
- if (this.statusCode < 400 || this.isJson || typeof data !== 'string' || !plugin?.errors || this.getHeader('Content-Type'))
1688
+ if (server && !server.getPlugin('static')) {
1689
+ this.name = 'static'; // only first plugin instance is registered as
1690
+ const defSend = ServerResponse.prototype.send;
1691
+ ServerResponse.prototype.send = function (data) {
1692
+ const plugin = this.req.server.getPlugin('static');
1693
+ if (this.statusCode < 400 || this.isJson || typeof data !== 'string' || !plugin?.errors || this.getHeader('Content-Type'))
1694
+ return defSend.call(this, data);
1695
+ const errFile = plugin.errors[this.statusCode] || plugin.errors['*'];
1696
+ if (errFile)
1697
+ plugin.serveFile(this.req, this, { path: errFile, mimeType: 'text/html' });
1695
1698
  return defSend.call(this, data);
1696
- const errFile = plugin.errors[this.statusCode] || plugin.errors['*'];
1697
- if (errFile)
1698
- plugin.serveFile(this.req, this, { path: errFile, mimeType: 'text/html' });
1699
- return defSend.call(this, data);
1700
- };
1701
- ServerResponse.prototype.file = function (path) {
1702
- const plugin = this.req.server.getPlugin('static');
1703
- if (!plugin)
1704
- throw new Error('Server error');
1705
- plugin.serveFile(this.req, this, typeof path === 'object' ? path : {
1706
- path,
1707
- mimeType: StaticFilesPlugin.mimeTypes[extname(path)] || 'application/octet-stream'
1708
- });
1709
- };
1699
+ };
1700
+ ServerResponse.prototype.file = function (path) {
1701
+ const plugin = this.req.server.getPlugin('static');
1702
+ if (!plugin)
1703
+ throw new Error('Server error');
1704
+ plugin.serveFile(this.req, this, typeof path === 'object' ? path : {
1705
+ path,
1706
+ mimeType: StaticFilesPlugin.mimeTypes[extname(path)] || 'application/octet-stream'
1707
+ });
1708
+ };
1709
+ }
1710
1710
  }
1711
1711
  /** Default static files handler */
1712
1712
  handler(req, res, next) {
1713
- if (req.method !== 'GET')
1713
+ if (req.method !== 'GET' && req.method !== 'HEAD')
1714
1714
  return next();
1715
1715
  if (!('path' in req.params)) { // global handler
1716
1716
  if (req.path.startsWith(this.prefix) && (req.path === this.prefix || req.path[this.prefix.length] === '/')) {
@@ -1751,6 +1751,7 @@ export class StaticFilesPlugin extends Plugin {
1751
1751
  });
1752
1752
  });
1753
1753
  }
1754
+ /** Send static file */
1754
1755
  serveFile(req, res, options) {
1755
1756
  const filePath = path.isAbsolute(options.path) ? options.path : path.join(options.root || this.root, options.path);
1756
1757
  const statRes = (err, stats) => {
@@ -1773,14 +1774,12 @@ export class StaticFilesPlugin extends Plugin {
1773
1774
  res.setHeader('Content-Length', stats.size);
1774
1775
  if (options.etag !== false) {
1775
1776
  const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1776
- if (req.headers['if-none-match'] === etag || req.headers['if-modified-since'] === stats.mtime.toUTCString()) {
1777
+ if (req.headers['if-none-match'] === etag || req.headers['if-modified-since'] === stats.mtime.toUTCString())
1777
1778
  res.statusCode = 304;
1778
- res.headersOnly = true;
1779
- }
1780
1779
  }
1781
1780
  if (options.maxAge)
1782
1781
  res.setHeader('Cache-Control', 'max-age=' + options.maxAge);
1783
- if (res.headersOnly) {
1782
+ if (res.statusCode === 304 || req.method === 'HEAD') {
1784
1783
  res.end();
1785
1784
  return;
1786
1785
  }
@@ -2577,11 +2576,13 @@ export class FileStore {
2577
2576
  });
2578
2577
  return p;
2579
2578
  }
2579
+ /** close store */
2580
2580
  async close() {
2581
2581
  await this.sync();
2582
2582
  this._iter = 0;
2583
2583
  this._cache = {};
2584
2584
  }
2585
+ /** sync data to disk */
2585
2586
  async sync() {
2586
2587
  for (const name in this._cache) {
2587
2588
  for (const key in this._cache)
@@ -3313,10 +3314,12 @@ export class MicroCollection {
3313
3314
  async deleteMany(query) {
3314
3315
  return (await this.updateMany(query, {}, { delete: true })).modifiedCount;
3315
3316
  }
3317
+ /** Update one matching document */
3316
3318
  async updateOne(query, update, options) {
3317
3319
  const res = await this.updateMany(query, update, { ...options, limit: 1 });
3318
3320
  return res;
3319
3321
  }
3322
+ /** Update many matching documents */
3320
3323
  async updateMany(query, update, options) {
3321
3324
  let res = { upsertedId: undefined, modifiedCount: 0 };
3322
3325
  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.5",
4
4
  "description": "HTTP MicroServer",
5
5
  "author": "Darius Kisonas",
6
6
  "license": "MIT",