@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 +193 -0
- package/microserver.d.ts +30 -25
- package/microserver.js +64 -61
- package/package.json +1 -1
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
|
+
* @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
|
-
/**
|
|
180
|
+
/** All sockets */
|
|
173
181
|
sockets: Set<net.Socket> | undefined;
|
|
174
|
-
/**
|
|
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
|
|
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.
|
|
213
|
+
/** Add route, alias to `server.use(url, ...args)` */
|
|
207
214
|
all(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
|
|
208
|
-
/** Add route, alias to `server.
|
|
215
|
+
/** Add route, alias to `server.use('GET ' + url, ...args)` */
|
|
209
216
|
get(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
|
|
210
|
-
/** Add route, alias to `server.
|
|
217
|
+
/** Add route, alias to `server.use('POST ' + url, ...args)` */
|
|
211
218
|
post(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
|
|
212
|
-
/** Add route, alias to `server.
|
|
219
|
+
/** Add route, alias to `server.use('PUT ' + url, ...args)` */
|
|
213
220
|
put(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
|
|
214
|
-
/** Add route, alias to `server.
|
|
221
|
+
/** Add route, alias to `server.use('PATCH ' + url, ...args)` */
|
|
215
222
|
patch(url: `/${string}`, ...args: RoutesMiddleware[]): MicroServer;
|
|
216
|
-
/** Add route, alias to `server.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
+
* @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.
|
|
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.
|
|
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
|
-
/**
|
|
372
|
+
/** All sockets */
|
|
366
373
|
sockets;
|
|
367
|
-
/**
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
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
|
-
|
|
1697
|
-
|
|
1698
|
-
plugin
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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.
|
|
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)
|