sleepy-serv 0.1.0
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/package.json +26 -0
- package/src/errors.js +412 -0
- package/src/index.js +301 -0
- package/src/meta.js +65 -0
- package/src/middleware.js +101 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sleepy-serv",
|
|
3
|
+
"description": "A more opinionated web server for building REST-ful applications",
|
|
4
|
+
"author": "Travis J True",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"exports": "./src/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"rest",
|
|
10
|
+
"serv",
|
|
11
|
+
"sleepy",
|
|
12
|
+
"web server",
|
|
13
|
+
"www"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"axios": "^1.7.9",
|
|
20
|
+
"eslint": "9.20.1"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"ajv": "^8.17.1",
|
|
24
|
+
"ajv-formats": "^3.0.1"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
class RequestError extends Error {
|
|
2
|
+
constructor (message) {
|
|
3
|
+
super(message)
|
|
4
|
+
|
|
5
|
+
this.name = 'RequestError'
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 4xx - Client Errors
|
|
10
|
+
|
|
11
|
+
export class BadRequestError extends RequestError {
|
|
12
|
+
static get statusCode () { return 400 }
|
|
13
|
+
|
|
14
|
+
constructor (message) {
|
|
15
|
+
super(message)
|
|
16
|
+
|
|
17
|
+
this.name = 'BadRequestError'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class UnauthorizedError extends RequestError {
|
|
22
|
+
static get statusCode () { return 401 }
|
|
23
|
+
|
|
24
|
+
constructor (message) {
|
|
25
|
+
super(message)
|
|
26
|
+
|
|
27
|
+
this.name = 'UnauthorizedError'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class PaymentRequiredError extends RequestError {
|
|
32
|
+
static get statusCode () { return 402 }
|
|
33
|
+
|
|
34
|
+
constructor (message) {
|
|
35
|
+
super(message)
|
|
36
|
+
|
|
37
|
+
this.name = 'PaymentRequiredError'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ForbiddenError extends RequestError {
|
|
42
|
+
static get statusCode () { return 403 }
|
|
43
|
+
|
|
44
|
+
constructor (message) {
|
|
45
|
+
super(message)
|
|
46
|
+
|
|
47
|
+
this.name = 'ForbiddenError'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class NotFoundError extends RequestError {
|
|
52
|
+
static get statusCode () { return 404 }
|
|
53
|
+
|
|
54
|
+
constructor () {
|
|
55
|
+
super('')
|
|
56
|
+
|
|
57
|
+
this.name = 'NotFoundError'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class MethodNotAllowedError extends RequestError {
|
|
62
|
+
static get statusCode () { return 405 }
|
|
63
|
+
|
|
64
|
+
constructor () {
|
|
65
|
+
super('')
|
|
66
|
+
|
|
67
|
+
this.name = 'MethodNotAllowedError'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class NotAcceptableError extends RequestError {
|
|
72
|
+
static get statusCode () { return 406 }
|
|
73
|
+
|
|
74
|
+
constructor (message) {
|
|
75
|
+
super(message)
|
|
76
|
+
|
|
77
|
+
this.name = 'NotAcceptableError'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class ProxyAuthenticationRequiredError extends RequestError {
|
|
82
|
+
static get statusCode () { return 407 }
|
|
83
|
+
|
|
84
|
+
constructor (message) {
|
|
85
|
+
super(message)
|
|
86
|
+
|
|
87
|
+
this.name = 'ProxyAuthenticationRequiredError'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class RequestTimeoutError extends RequestError {
|
|
92
|
+
static get statusCode () { return 408 }
|
|
93
|
+
|
|
94
|
+
constructor (message) {
|
|
95
|
+
super(message)
|
|
96
|
+
|
|
97
|
+
this.name = 'RequestTimeoutError'
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class ConflictError extends RequestError {
|
|
102
|
+
static get statusCode () { return 409 }
|
|
103
|
+
|
|
104
|
+
constructor (message) {
|
|
105
|
+
super(message)
|
|
106
|
+
|
|
107
|
+
this.name = 'ConflictError'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class GoneError extends RequestError {
|
|
112
|
+
static get statusCode () { return 410 }
|
|
113
|
+
|
|
114
|
+
constructor (message) {
|
|
115
|
+
super(message)
|
|
116
|
+
|
|
117
|
+
this.name = 'GoneError'
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class LengthRequiredError extends RequestError {
|
|
122
|
+
static get statusCode () { return 411 }
|
|
123
|
+
|
|
124
|
+
constructor (message) {
|
|
125
|
+
super(message)
|
|
126
|
+
|
|
127
|
+
this.name = 'LengthRequiredError'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class PreconditionFailedError extends RequestError {
|
|
132
|
+
static get statusCode () { return 412 }
|
|
133
|
+
|
|
134
|
+
constructor (message) {
|
|
135
|
+
super(message)
|
|
136
|
+
|
|
137
|
+
this.name = 'PreconditionFailedError'
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class PayloadTooLargeError extends RequestError {
|
|
142
|
+
static get statusCode () { return 413 }
|
|
143
|
+
|
|
144
|
+
constructor (message) {
|
|
145
|
+
super(message)
|
|
146
|
+
|
|
147
|
+
this.name = 'PayloadTooLargeError'
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class UriTooLongError extends RequestError {
|
|
152
|
+
static get statusCode () { return 414 }
|
|
153
|
+
|
|
154
|
+
constructor (message) {
|
|
155
|
+
super(message)
|
|
156
|
+
|
|
157
|
+
this.name = 'UriTooLongError'
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export class UnsupportedMediaTypeError extends RequestError {
|
|
162
|
+
static get statusCode () { return 415 }
|
|
163
|
+
|
|
164
|
+
constructor (subject) {
|
|
165
|
+
super(`Unsupported ${subject}`)
|
|
166
|
+
|
|
167
|
+
this.name = 'UnsupportedMediaTypeError'
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class RangeNotSatisfiableError extends RequestError {
|
|
172
|
+
static get statusCode () { return 416 }
|
|
173
|
+
|
|
174
|
+
constructor (message) {
|
|
175
|
+
super(message)
|
|
176
|
+
|
|
177
|
+
this.name = 'RangeNotSatisfiableError'
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class ExpectationFailedError extends RequestError {
|
|
182
|
+
static get statusCode () { return 417 }
|
|
183
|
+
|
|
184
|
+
constructor (message) {
|
|
185
|
+
super(message)
|
|
186
|
+
|
|
187
|
+
this.name = 'ExpectationFailedError'
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export class ImATeapotError extends RequestError {
|
|
192
|
+
static get statusCode () { return 418 }
|
|
193
|
+
|
|
194
|
+
constructor (message) {
|
|
195
|
+
super(message)
|
|
196
|
+
|
|
197
|
+
this.name = 'ImATeapotError'
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class MisdirectedRequestError extends RequestError {
|
|
202
|
+
static get statusCode () { return 421 }
|
|
203
|
+
|
|
204
|
+
constructor (message) {
|
|
205
|
+
super(message)
|
|
206
|
+
|
|
207
|
+
this.name = 'MisdirectedRequestError'
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export class UnprocessableContentError extends RequestError {
|
|
212
|
+
static get statusCode () { return 422 }
|
|
213
|
+
|
|
214
|
+
constructor (errors) {
|
|
215
|
+
super(JSON.stringify(errors))
|
|
216
|
+
|
|
217
|
+
this.name = 'UnprocessableContentError'
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export class LockedError extends RequestError {
|
|
222
|
+
static get statusCode () { return 423 }
|
|
223
|
+
|
|
224
|
+
constructor (message) {
|
|
225
|
+
super(message)
|
|
226
|
+
|
|
227
|
+
this.name = 'LockedError'
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export class FailedDependencyError extends RequestError {
|
|
232
|
+
static get statusCode () { return 424 }
|
|
233
|
+
|
|
234
|
+
constructor (message) {
|
|
235
|
+
super(message)
|
|
236
|
+
|
|
237
|
+
this.name = 'FailedDependencyError'
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export class TooEarlyError extends RequestError {
|
|
242
|
+
static get statusCode () { return 425 }
|
|
243
|
+
|
|
244
|
+
constructor (message) {
|
|
245
|
+
super(message)
|
|
246
|
+
|
|
247
|
+
this.name = 'TooEarlyError'
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export class UpgradeRequiredError extends RequestError {
|
|
252
|
+
static get statusCode () { return 426 }
|
|
253
|
+
|
|
254
|
+
constructor (message) {
|
|
255
|
+
super(message)
|
|
256
|
+
|
|
257
|
+
this.name = 'UpgradeRequiredError'
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export class PreconditionRequiredError extends RequestError {
|
|
262
|
+
static get statusCode () { return 428 }
|
|
263
|
+
|
|
264
|
+
constructor (message) {
|
|
265
|
+
super(message)
|
|
266
|
+
|
|
267
|
+
this.name = 'PreconditionRequiredError'
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export class TooManyRequestsError extends RequestError {
|
|
272
|
+
static get statusCode () { return 429 }
|
|
273
|
+
|
|
274
|
+
constructor (message) {
|
|
275
|
+
super(message)
|
|
276
|
+
|
|
277
|
+
this.name = 'TooManyRequestsError'
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export class RequestHeaderFieldsTooLargeError extends RequestError {
|
|
282
|
+
static get statusCode () { return 431 }
|
|
283
|
+
|
|
284
|
+
constructor (message) {
|
|
285
|
+
super(message)
|
|
286
|
+
|
|
287
|
+
this.name = 'RequestHeaderFieldsTooLargeError'
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export class UnavailableForLegalReasonsError extends RequestError {
|
|
292
|
+
static get statusCode () { return 451 }
|
|
293
|
+
|
|
294
|
+
constructor (message) {
|
|
295
|
+
super(message)
|
|
296
|
+
|
|
297
|
+
this.name = 'UnavailableForLegalReasonsError'
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 5xx - Server Errors
|
|
302
|
+
|
|
303
|
+
export class InternalServerError extends RequestError {
|
|
304
|
+
static get statusCode () { return 500 }
|
|
305
|
+
|
|
306
|
+
constructor (message, ctx) {
|
|
307
|
+
super(message)
|
|
308
|
+
|
|
309
|
+
this.name = 'InternalServerError'
|
|
310
|
+
this.ctx = ctx
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export class NotImplementedError extends RequestError {
|
|
315
|
+
static get statusCode () { return 501 }
|
|
316
|
+
|
|
317
|
+
constructor () {
|
|
318
|
+
super('')
|
|
319
|
+
|
|
320
|
+
this.name = 'NotImplementedError'
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export class BadGatewayError extends RequestError {
|
|
325
|
+
static get statusCode () { return 502 }
|
|
326
|
+
|
|
327
|
+
constructor (message) {
|
|
328
|
+
super(message)
|
|
329
|
+
|
|
330
|
+
this.name = 'BadGatewayError'
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export class ServiceUnavailableError extends RequestError {
|
|
335
|
+
static get statusCode () { return 503 }
|
|
336
|
+
|
|
337
|
+
constructor (message) {
|
|
338
|
+
super(message)
|
|
339
|
+
|
|
340
|
+
this.name = 'ServiceUnavailableError'
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export class GatewayTimeoutError extends RequestError {
|
|
345
|
+
static get statusCode () { return 504 }
|
|
346
|
+
|
|
347
|
+
constructor () {
|
|
348
|
+
super('')
|
|
349
|
+
|
|
350
|
+
this.name = 'GatewayTimeoutError'
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export class HTTPVersionNotSupportedError extends RequestError {
|
|
355
|
+
static get statusCode () { return 505 }
|
|
356
|
+
|
|
357
|
+
constructor (message) {
|
|
358
|
+
super(message)
|
|
359
|
+
|
|
360
|
+
this.name = 'HTTPVersionNotSupportedError'
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export class VariantAlsoNegotiatesError extends RequestError {
|
|
365
|
+
static get statusCode () { return 506 }
|
|
366
|
+
|
|
367
|
+
constructor (message) {
|
|
368
|
+
super(message)
|
|
369
|
+
|
|
370
|
+
this.name = 'VariantAlsoNegotiatesError'
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export class InsufficientStorageError extends RequestError {
|
|
375
|
+
static get statusCode () { return 507 }
|
|
376
|
+
|
|
377
|
+
constructor (message) {
|
|
378
|
+
super(message)
|
|
379
|
+
|
|
380
|
+
this.name = 'InsufficientStorageError'
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export class LoopDetectedError extends RequestError {
|
|
385
|
+
static get statusCode () { return 508 }
|
|
386
|
+
|
|
387
|
+
constructor (message) {
|
|
388
|
+
super(message)
|
|
389
|
+
|
|
390
|
+
this.name = 'LoopDetectedError'
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export class NotExtendedError extends RequestError {
|
|
395
|
+
static get statusCode () { return 510 }
|
|
396
|
+
|
|
397
|
+
constructor (message) {
|
|
398
|
+
super(message)
|
|
399
|
+
|
|
400
|
+
this.name = 'NotExtendedError'
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export class NetworkAuthenticationRequiredError extends RequestError {
|
|
405
|
+
static get statusCode () { return 511 }
|
|
406
|
+
|
|
407
|
+
constructor (message) {
|
|
408
|
+
super(message)
|
|
409
|
+
|
|
410
|
+
this.name = 'NetworkAuthenticationRequiredError'
|
|
411
|
+
}
|
|
412
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import querystring from 'querystring'
|
|
4
|
+
import readline from 'node:readline'
|
|
5
|
+
import { stdin, stdout } from 'node:process'
|
|
6
|
+
|
|
7
|
+
import * as _middleware from './middleware.js'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
NotFoundError,
|
|
11
|
+
MethodNotAllowedError,
|
|
12
|
+
} from './errors'
|
|
13
|
+
|
|
14
|
+
export * from './errors'
|
|
15
|
+
|
|
16
|
+
const ALLOWED_FILES_META = ['meta.js']
|
|
17
|
+
|
|
18
|
+
const ALLOWED_FILES_METHODS = [
|
|
19
|
+
'head.js',
|
|
20
|
+
'get.js',
|
|
21
|
+
'put.js',
|
|
22
|
+
'post.js',
|
|
23
|
+
'patch.js',
|
|
24
|
+
'delete.js',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const ALLOWED_FILES_ALL = [
|
|
28
|
+
...ALLOWED_FILES_META,
|
|
29
|
+
...ALLOWED_FILES_METHODS,
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/* istanbul ignore if */
|
|
33
|
+
if (process.stdin.isTTY) {
|
|
34
|
+
process.stdin.setRawMode(true)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const middleware = _middleware
|
|
38
|
+
|
|
39
|
+
const rl = readline.createInterface({
|
|
40
|
+
input: stdin,
|
|
41
|
+
output: stdout,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
function methodNotAllowedHandler (_req) {
|
|
45
|
+
throw new MethodNotAllowedError()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateDirectoryIllegalFiles (targetPath, filenames) {
|
|
49
|
+
const hasInvalidFiles = filenames.some(filename =>
|
|
50
|
+
!ALLOWED_FILES_ALL.includes(filename)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (hasInvalidFiles) {
|
|
54
|
+
throw new TypeError(`
|
|
55
|
+
Directory contains illegal files:
|
|
56
|
+
${targetPath}
|
|
57
|
+
`.trim())
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateLeafDirectory (targetPath, filenames, entries) {
|
|
62
|
+
const hasDirectories = entries.some(entry => entry.stat.isDirectory())
|
|
63
|
+
|
|
64
|
+
if (!hasDirectories) {
|
|
65
|
+
const hasMethodEntry = filenames.some(filename =>
|
|
66
|
+
ALLOWED_FILES_METHODS.includes(filename)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if (!hasMethodEntry) {
|
|
70
|
+
throw new TypeError(`
|
|
71
|
+
Directory is a leaf, but doesn't contain a method file:
|
|
72
|
+
${targetPath}
|
|
73
|
+
`.trim())
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateDirectory (targetPath, entries) {
|
|
79
|
+
const filenames = entries
|
|
80
|
+
.filter(entry => entry.stat.isFile())
|
|
81
|
+
.map(entry => path.basename(entry.path))
|
|
82
|
+
|
|
83
|
+
validateDirectoryIllegalFiles(targetPath, filenames)
|
|
84
|
+
validateLeafDirectory(targetPath, filenames, entries)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getAllFilePathsRec (targetPath, paths) {
|
|
88
|
+
const entries = fs.readdirSync(targetPath)
|
|
89
|
+
|
|
90
|
+
const children = entries.map(item => {
|
|
91
|
+
const fullPath = path.join(targetPath, item)
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
path: fullPath,
|
|
95
|
+
stat: fs.statSync(fullPath),
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
validateDirectory(targetPath, children)
|
|
100
|
+
|
|
101
|
+
return children.reduce((accum, curr) => {
|
|
102
|
+
const result = curr.stat.isDirectory()
|
|
103
|
+
? getAllFilePathsRec(curr.path, paths)
|
|
104
|
+
: [curr.path]
|
|
105
|
+
|
|
106
|
+
return [...accum, ...result]
|
|
107
|
+
}, [])
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getFilteredFilePaths (targetPath, allowedFiles) {
|
|
111
|
+
const allPaths = getAllFilePathsRec(targetPath, [])
|
|
112
|
+
|
|
113
|
+
return allPaths.filter(item =>
|
|
114
|
+
allowedFiles.includes(path.basename(item))
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getMethodFilePaths (targetPath) {
|
|
119
|
+
return getFilteredFilePaths(targetPath, ALLOWED_FILES_METHODS)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getMetaFilePaths (targetPath) {
|
|
123
|
+
return getFilteredFilePaths(targetPath, ALLOWED_FILES_META)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildRoutesPaths (rootPath, mountPath) {
|
|
127
|
+
const metadata = getMetaFilePaths(rootPath)
|
|
128
|
+
const paths = getMethodFilePaths(rootPath)
|
|
129
|
+
|
|
130
|
+
return paths.map(modulePath => {
|
|
131
|
+
const trimmedPath = modulePath.replace(rootPath, '')
|
|
132
|
+
const segments = trimmedPath.split('.')[0].split('/')
|
|
133
|
+
const lastIndex = segments.length - 1
|
|
134
|
+
const basePath = segments.slice(0, segments.length - 1).join('/')
|
|
135
|
+
|
|
136
|
+
const joinedPath = [mountPath, basePath]
|
|
137
|
+
.filter(item => item)
|
|
138
|
+
.join('') || '/'
|
|
139
|
+
|
|
140
|
+
const metaMiddlewarePath = metadata
|
|
141
|
+
.filter(metaPath => {
|
|
142
|
+
const metaBasePath = path.dirname(metaPath)
|
|
143
|
+
|
|
144
|
+
return modulePath.startsWith(metaBasePath)
|
|
145
|
+
})
|
|
146
|
+
.sort((a, b) => a.length - b.length)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
method: segments[lastIndex].toUpperCase(),
|
|
150
|
+
path: joinedPath,
|
|
151
|
+
metaMiddlewarePath,
|
|
152
|
+
modulePath,
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function buildHandlers (route, rootMiddleware) {
|
|
158
|
+
const module = await import(route.modulePath)
|
|
159
|
+
|
|
160
|
+
const middlewareModules = await Promise.all(
|
|
161
|
+
route.metaMiddlewarePath.map(item => import(item))
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const metaMiddleware = middlewareModules
|
|
165
|
+
.map(item => item.middleware)
|
|
166
|
+
.reduce((accum, curr) => [
|
|
167
|
+
...accum,
|
|
168
|
+
...(curr || []),
|
|
169
|
+
], [])
|
|
170
|
+
|
|
171
|
+
if (!module.default) {
|
|
172
|
+
throw new ReferenceError(`
|
|
173
|
+
No default export defined in:
|
|
174
|
+
${route.modulePath}
|
|
175
|
+
`.trim())
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const baseChain = Array.isArray(module.default)
|
|
179
|
+
? module.default
|
|
180
|
+
: [module.default]
|
|
181
|
+
|
|
182
|
+
const middlewareChain = [
|
|
183
|
+
...rootMiddleware,
|
|
184
|
+
...metaMiddleware,
|
|
185
|
+
...baseChain,
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
const handler = async req => {
|
|
189
|
+
const query = req.url.split('?')[1]
|
|
190
|
+
|
|
191
|
+
req.query = query ? querystring.parse(query) : {}
|
|
192
|
+
|
|
193
|
+
for (const fn of middlewareChain) {
|
|
194
|
+
const res = await fn(req)
|
|
195
|
+
|
|
196
|
+
if (res) {
|
|
197
|
+
return res
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
method: route.method,
|
|
204
|
+
path: route.path,
|
|
205
|
+
handler,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildModuleRoutes (routePaths, rootMiddleware) {
|
|
210
|
+
return Promise.all(
|
|
211
|
+
routePaths.map(route => buildHandlers(route, rootMiddleware))
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildServerRoutes (moduleRoutes) {
|
|
216
|
+
return moduleRoutes.reduce((accum, curr) => {
|
|
217
|
+
if (!accum[curr.path]) {
|
|
218
|
+
accum[curr.path] = {
|
|
219
|
+
HEAD: methodNotAllowedHandler,
|
|
220
|
+
GET: methodNotAllowedHandler,
|
|
221
|
+
PUT: methodNotAllowedHandler,
|
|
222
|
+
POST: methodNotAllowedHandler,
|
|
223
|
+
PATCH: methodNotAllowedHandler,
|
|
224
|
+
DELETE: methodNotAllowedHandler,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
accum[curr.path][curr.method] = curr.handler
|
|
229
|
+
|
|
230
|
+
return accum
|
|
231
|
+
}, {})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildOutputRoutes (moduleRoutes) {
|
|
235
|
+
return moduleRoutes.reduce((accum, curr) => {
|
|
236
|
+
accum[curr.path] = accum[curr.path] || []
|
|
237
|
+
accum[curr.path].push(curr.method)
|
|
238
|
+
|
|
239
|
+
return accum
|
|
240
|
+
}, {})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function buildRoutes (rootPath, opts) {
|
|
244
|
+
const basePath = `${rootPath}/api`
|
|
245
|
+
const mountPath = opts.mountPath || ''
|
|
246
|
+
const rootMiddleware = opts.middleware || []
|
|
247
|
+
const routePaths = buildRoutesPaths(basePath, mountPath)
|
|
248
|
+
const moduleRoutes = await buildModuleRoutes(routePaths, rootMiddleware)
|
|
249
|
+
const serverRoutes = buildServerRoutes(moduleRoutes)
|
|
250
|
+
const outputRoutes = buildOutputRoutes(moduleRoutes)
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
server: serverRoutes,
|
|
254
|
+
output: outputRoutes,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildServer (port, routes) {
|
|
259
|
+
return Bun.serve({
|
|
260
|
+
port,
|
|
261
|
+
routes: routes.server,
|
|
262
|
+
fetch (_req) {
|
|
263
|
+
throw new NotFoundError()
|
|
264
|
+
},
|
|
265
|
+
error (err) {
|
|
266
|
+
console.error(err)
|
|
267
|
+
|
|
268
|
+
return new Response(err.message, {
|
|
269
|
+
status: err.constructor.statusCode || 500,
|
|
270
|
+
})
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function processIO (port, server, opts) {
|
|
276
|
+
const onClose = opts.onClose || (() => {})
|
|
277
|
+
|
|
278
|
+
console.info(`Running on port: ${port}`)
|
|
279
|
+
console.info('')
|
|
280
|
+
console.info('Press Ctrl+D to gracefully shutdown')
|
|
281
|
+
console.info('')
|
|
282
|
+
|
|
283
|
+
/* istanbul ignore next */
|
|
284
|
+
rl.on('close', async () => {
|
|
285
|
+
await server.stop()
|
|
286
|
+
await onClose()
|
|
287
|
+
process.exit(0)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function createApp (port, rootPath, opts = {}) {
|
|
292
|
+
const routes = await buildRoutes(rootPath, opts)
|
|
293
|
+
const server = buildServer(port, routes)
|
|
294
|
+
|
|
295
|
+
processIO(port, server, opts)
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
routes: routes.output,
|
|
299
|
+
server,
|
|
300
|
+
}
|
|
301
|
+
}
|
package/src/meta.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function range (count, startIndex = 0) {
|
|
2
|
+
return new Array(count).fill(0).map((_, index) => index + startIndex)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function traverse (obj, onKey, includeRoot = false) {
|
|
6
|
+
const path = ['']
|
|
7
|
+
|
|
8
|
+
const fn = target => {
|
|
9
|
+
Object.entries(target).forEach(([k, v]) => {
|
|
10
|
+
path[path.length - 1] = k
|
|
11
|
+
|
|
12
|
+
const dateType = v instanceof Date
|
|
13
|
+
const clip = onKey([...path], v) === false
|
|
14
|
+
const updatedVal = getValueByPath(obj, path)
|
|
15
|
+
const nonNullObj = updatedVal !== null && typeof updatedVal === 'object'
|
|
16
|
+
|
|
17
|
+
if (!clip && !dateType && nonNullObj) {
|
|
18
|
+
path.push('')
|
|
19
|
+
fn(updatedVal)
|
|
20
|
+
path.pop()
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (includeRoot) {
|
|
26
|
+
onKey([], obj)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn(obj)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function map (obj, onKey) {
|
|
33
|
+
const result = Array.isArray(obj) ? [] : {}
|
|
34
|
+
|
|
35
|
+
traverse(obj, (keyPath, value) => {
|
|
36
|
+
const dateType = value instanceof Date
|
|
37
|
+
|
|
38
|
+
if (!dateType && value !== null && typeof value === 'object') {
|
|
39
|
+
setValueByPath(result, keyPath, Array.isArray(value) ? [] : {})
|
|
40
|
+
} else {
|
|
41
|
+
setValueByPath(result, keyPath, onKey(keyPath, value))
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function deepCopy (obj) {
|
|
49
|
+
return map(obj, (_, value) => value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function setValueByPath (obj, keyPath, value) {
|
|
53
|
+
keyPath.reduce((subObj, key, index) => {
|
|
54
|
+
if (index === keyPath.length - 1) {
|
|
55
|
+
subObj[key] = value
|
|
56
|
+
} else {
|
|
57
|
+
return subObj[key]
|
|
58
|
+
}
|
|
59
|
+
}, obj)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getValueByPath (obj, keyPath) {
|
|
63
|
+
return keyPath.reduce((obj, key) =>
|
|
64
|
+
(typeof obj !== 'undefined' ? obj[key] : undefined), obj)
|
|
65
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import Ajv from 'ajv'
|
|
2
|
+
import addFormats from 'ajv-formats'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
UnsupportedMediaTypeError,
|
|
6
|
+
UnprocessableContentError,
|
|
7
|
+
} from './errors'
|
|
8
|
+
|
|
9
|
+
const SCHEMA_EMPTY = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let _customFormats = {}
|
|
15
|
+
|
|
16
|
+
function formatError (prefix, input) {
|
|
17
|
+
const fixedPath = input.instancePath || '/'
|
|
18
|
+
const suffixPath = fixedPath.replace(/\//g, '.').replace('.', '')
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
path: [prefix, suffixPath].filter(item => item).join('.'),
|
|
22
|
+
message: input.message,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildFormatterSchema (schema) {
|
|
27
|
+
if (!schema) {
|
|
28
|
+
return SCHEMA_EMPTY
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const properties = Object
|
|
32
|
+
.entries(schema)
|
|
33
|
+
.map(([key, config]) => [key, {
|
|
34
|
+
type: 'string',
|
|
35
|
+
[config.type]: config.value,
|
|
36
|
+
},
|
|
37
|
+
])
|
|
38
|
+
.reduce((accum, [key, value]) => ({
|
|
39
|
+
...accum,
|
|
40
|
+
[key]: value,
|
|
41
|
+
}), {})
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function parseJson (req) {
|
|
50
|
+
const contentType = req.headers.get('content-type')
|
|
51
|
+
|
|
52
|
+
if (req.method !== 'GET' && contentType !== 'application/json') {
|
|
53
|
+
throw new UnsupportedMediaTypeError('content-type')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
req.parsedBody = await req.json()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function setValidationFormats (formats) {
|
|
60
|
+
_customFormats = formats
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function validateSchema (schemas) {
|
|
64
|
+
return function (req) {
|
|
65
|
+
const formattedSchemas = {
|
|
66
|
+
headers: buildFormatterSchema(schemas.headers),
|
|
67
|
+
params: buildFormatterSchema(schemas.params),
|
|
68
|
+
query: buildFormatterSchema(schemas.query),
|
|
69
|
+
parsedBody: schemas.parsedBody || SCHEMA_EMPTY,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ajv = new Ajv({
|
|
73
|
+
allErrors: true,
|
|
74
|
+
removeAdditional: 'all',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
addFormats(ajv)
|
|
78
|
+
|
|
79
|
+
Object
|
|
80
|
+
.entries(_customFormats)
|
|
81
|
+
.forEach(([k, v]) => ajv.addFormat(k, v))
|
|
82
|
+
|
|
83
|
+
const errors = Object
|
|
84
|
+
.entries(formattedSchemas)
|
|
85
|
+
.reduce((accum, [key, schema]) => {
|
|
86
|
+
const data = req[key]
|
|
87
|
+
const valid = schema ? ajv.validate(schema, data) : true
|
|
88
|
+
|
|
89
|
+
return !valid
|
|
90
|
+
? [
|
|
91
|
+
...accum,
|
|
92
|
+
...ajv.errors.map(item => formatError(key, item)),
|
|
93
|
+
]
|
|
94
|
+
: accum
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
if (errors.length > 0) {
|
|
98
|
+
throw new UnprocessableContentError(errors)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|