@pirxpilot/router 1.0.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/index.js ADDED
@@ -0,0 +1,717 @@
1
+ /*!
2
+ * router
3
+ * Copyright(c) 2013 Roman Shtylman
4
+ * Copyright(c) 2014-2022 Douglas Christopher Wilson
5
+ * MIT Licensed
6
+ */
7
+
8
+ 'use strict'
9
+
10
+ /**
11
+ * Module dependencies.
12
+ * @private
13
+ */
14
+
15
+ const Layer = require('./lib/layer')
16
+ const methods = require('methods')
17
+ const parseUrl = require('parseurl')
18
+ const Route = require('./lib/route')
19
+
20
+ /**
21
+ * Module variables.
22
+ * @private
23
+ */
24
+
25
+ /**
26
+ * Expose `Router`.
27
+ */
28
+
29
+ module.exports = Router
30
+
31
+ /**
32
+ * Expose `Route`.
33
+ */
34
+
35
+ module.exports.Route = Route
36
+
37
+ /**
38
+ * Initialize a new `Router` with the given `options`.
39
+ *
40
+ * @param {object} [options]
41
+ * @return {Router} which is a callable function
42
+ * @public
43
+ */
44
+
45
+ function Router (options) {
46
+ if (!(this instanceof Router)) {
47
+ return new Router(options)
48
+ }
49
+
50
+ const opts = options || {}
51
+
52
+ function router (req, res, next) {
53
+ router.handle(req, res, next)
54
+ }
55
+
56
+ // inherit from the correct prototype
57
+ Object.setPrototypeOf(router, this)
58
+
59
+ router.caseSensitive = opts.caseSensitive
60
+ router.mergeParams = opts.mergeParams
61
+ router.params = {}
62
+ router.strict = opts.strict
63
+ router.stack = []
64
+
65
+ return router
66
+ }
67
+
68
+ /**
69
+ * Router prototype inherits from a Function.
70
+ */
71
+
72
+ /* istanbul ignore next */
73
+ Router.prototype = function () {}
74
+
75
+ /**
76
+ * Map the given param placeholder `name`(s) to the given callback.
77
+ *
78
+ * Parameter mapping is used to provide pre-conditions to routes
79
+ * which use normalized placeholders. For example a _:user_id_ parameter
80
+ * could automatically load a user's information from the database without
81
+ * any additional code.
82
+ *
83
+ * The callback uses the same signature as middleware, the only difference
84
+ * being that the value of the placeholder is passed, in this case the _id_
85
+ * of the user. Once the `next()` function is invoked, just like middleware
86
+ * it will continue on to execute the route, or subsequent parameter functions.
87
+ *
88
+ * Just like in middleware, you must either respond to the request or call next
89
+ * to avoid stalling the request.
90
+ *
91
+ * router.param('user_id', function(req, res, next, id){
92
+ * User.find(id, function(err, user){
93
+ * if (err) {
94
+ * return next(err)
95
+ * } else if (!user) {
96
+ * return next(new Error('failed to load user'))
97
+ * }
98
+ * req.user = user
99
+ * next()
100
+ * })
101
+ * })
102
+ *
103
+ * @param {string} name
104
+ * @param {function} fn
105
+ * @public
106
+ */
107
+
108
+ Router.prototype.param = function param (name, fn) {
109
+ if (!name) {
110
+ throw new TypeError('argument name is required')
111
+ }
112
+
113
+ if (typeof name !== 'string') {
114
+ throw new TypeError('argument name must be a string')
115
+ }
116
+
117
+ if (!fn) {
118
+ throw new TypeError('argument fn is required')
119
+ }
120
+
121
+ if (typeof fn !== 'function') {
122
+ throw new TypeError('argument fn must be a function')
123
+ }
124
+
125
+ let params = this.params[name]
126
+
127
+ if (!params) {
128
+ params = this.params[name] = []
129
+ }
130
+
131
+ params.push(fn)
132
+
133
+ return this
134
+ }
135
+
136
+ /**
137
+ * Dispatch a req, res into the router.
138
+ *
139
+ * @private
140
+ */
141
+
142
+ Router.prototype.handle = function handle (req, res, callback) {
143
+ if (!callback) {
144
+ throw new TypeError('argument callback is required')
145
+ }
146
+
147
+ let idx = 0
148
+ let methods
149
+ const protohost = getProtohost(req.url) || ''
150
+ let removed = ''
151
+ const self = this
152
+ let slashAdded = false
153
+ let sync = 0
154
+ const paramcalled = {}
155
+
156
+ // middleware and routes
157
+ const stack = this.stack
158
+
159
+ // manage inter-router variables
160
+ const parentParams = req.params
161
+ const parentUrl = req.baseUrl || ''
162
+ let done = restore(callback, req, 'baseUrl', 'next', 'params')
163
+
164
+ // setup next layer
165
+ req.next = next
166
+
167
+ // for options requests, respond with a default if nothing else responds
168
+ if (req.method === 'OPTIONS') {
169
+ methods = []
170
+ done = wrap(done, generateOptionsResponder(res, methods))
171
+ }
172
+
173
+ // setup basic req values
174
+ req.baseUrl = parentUrl
175
+ req.originalUrl = req.originalUrl || req.url
176
+
177
+ next()
178
+
179
+ function next (err) {
180
+ let layerError = err === 'route'
181
+ ? null
182
+ : err
183
+
184
+ // remove added slash
185
+ if (slashAdded) {
186
+ req.url = req.url.slice(1)
187
+ slashAdded = false
188
+ }
189
+
190
+ // restore altered req.url
191
+ if (removed.length !== 0) {
192
+ req.baseUrl = parentUrl
193
+ req.url = protohost + removed + req.url.slice(protohost.length)
194
+ removed = ''
195
+ }
196
+
197
+ // signal to exit router
198
+ if (layerError === 'router') {
199
+ setImmediate(done, null)
200
+ return
201
+ }
202
+
203
+ // no more matching layers
204
+ if (idx >= stack.length) {
205
+ setImmediate(done, layerError)
206
+ return
207
+ }
208
+
209
+ // max sync stack
210
+ if (++sync > 100) {
211
+ return setImmediate(next, err)
212
+ }
213
+
214
+ // get pathname of request
215
+ const path = getPathname(req)
216
+
217
+ if (path == null) {
218
+ return done(layerError)
219
+ }
220
+
221
+ // find next matching layer
222
+ let layer
223
+ let match
224
+ let route
225
+
226
+ while (match !== true && idx < stack.length) {
227
+ layer = stack[idx++]
228
+ match = matchLayer(layer, path)
229
+ route = layer.route
230
+
231
+ if (typeof match !== 'boolean') {
232
+ // hold on to layerError
233
+ layerError = layerError || match
234
+ }
235
+
236
+ if (match !== true) {
237
+ continue
238
+ }
239
+
240
+ if (!route) {
241
+ // process non-route handlers normally
242
+ continue
243
+ }
244
+
245
+ if (layerError) {
246
+ // routes do not match with a pending error
247
+ match = false
248
+ continue
249
+ }
250
+
251
+ const method = req.method
252
+ const hasMethod = route._handlesMethod(method)
253
+
254
+ // build up automatic options response
255
+ if (!hasMethod && method === 'OPTIONS' && methods) {
256
+ methods.push.apply(methods, route._methods())
257
+ }
258
+
259
+ // don't even bother matching route
260
+ if (!hasMethod && method !== 'HEAD') {
261
+ match = false
262
+ continue
263
+ }
264
+ }
265
+
266
+ // no match
267
+ if (match !== true) {
268
+ return done(layerError)
269
+ }
270
+
271
+ // store route for dispatch on change
272
+ if (route) {
273
+ req.route = route
274
+ }
275
+
276
+ // Capture one-time layer values
277
+ req.params = self.mergeParams
278
+ ? mergeParams(layer.params, parentParams)
279
+ : layer.params
280
+ const layerPath = layer.path
281
+
282
+ // this should be done for the layer
283
+ processParams(self.params, layer, paramcalled, req, res, function (err) {
284
+ if (err) {
285
+ next(layerError || err)
286
+ } else if (route) {
287
+ layer.handleRequest(req, res, next)
288
+ } else {
289
+ trimPrefix(layer, layerError, layerPath, path)
290
+ }
291
+
292
+ sync = 0
293
+ })
294
+ }
295
+
296
+ function trimPrefix (layer, layerError, layerPath, path) {
297
+ if (layerPath.length !== 0) {
298
+ // Validate path is a prefix match
299
+ if (layerPath !== path.substring(0, layerPath.length)) {
300
+ next(layerError)
301
+ return
302
+ }
303
+
304
+ // Validate path breaks on a path separator
305
+ const c = path[layerPath.length]
306
+ if (c && c !== '/') {
307
+ next(layerError)
308
+ return
309
+ }
310
+
311
+ // Trim off the part of the url that matches the route
312
+ // middleware (.use stuff) needs to have the path stripped
313
+ removed = layerPath
314
+ req.url = protohost + req.url.slice(protohost.length + removed.length)
315
+
316
+ // Ensure leading slash
317
+ if (!protohost && req.url[0] !== '/') {
318
+ req.url = '/' + req.url
319
+ slashAdded = true
320
+ }
321
+
322
+ // Setup base URL (no trailing slash)
323
+ req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
324
+ ? removed.substring(0, removed.length - 1)
325
+ : removed)
326
+ }
327
+
328
+ if (layerError) {
329
+ layer.handleError(layerError, req, res, next)
330
+ } else {
331
+ layer.handleRequest(req, res, next)
332
+ }
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Use the given middleware function, with optional path, defaulting to "/".
338
+ *
339
+ * Use (like `.all`) will run for any http METHOD, but it will not add
340
+ * handlers for those methods so OPTIONS requests will not consider `.use`
341
+ * functions even if they could respond.
342
+ *
343
+ * The other difference is that _route_ path is stripped and not visible
344
+ * to the handler function. The main effect of this feature is that mounted
345
+ * handlers can operate without any code changes regardless of the "prefix"
346
+ * pathname.
347
+ *
348
+ * @public
349
+ */
350
+
351
+ Router.prototype.use = function use (handler, ...args) {
352
+ let path = '/'
353
+
354
+ // default path to '/'
355
+ // disambiguate router.use([handler])
356
+ if (typeof handler !== 'function') {
357
+ let arg = handler
358
+
359
+ while (Array.isArray(arg) && arg.length !== 0) {
360
+ arg = arg[0]
361
+ }
362
+
363
+ // first arg is the path
364
+ if (typeof arg !== 'function') {
365
+ path = handler
366
+ handler = undefined
367
+ }
368
+ }
369
+ if (handler !== undefined) {
370
+ args.unshift(handler)
371
+ }
372
+ const callbacks = args.flat(Infinity)
373
+
374
+ if (callbacks.length === 0) {
375
+ throw new TypeError('argument handler is required')
376
+ }
377
+
378
+ for (let i = 0; i < callbacks.length; i++) {
379
+ const fn = callbacks[i]
380
+
381
+ if (typeof fn !== 'function') {
382
+ throw new TypeError('argument handler must be a function')
383
+ }
384
+
385
+ // add the middleware
386
+ const layer = new Layer(path, {
387
+ sensitive: this.caseSensitive,
388
+ strict: false,
389
+ end: false
390
+ }, fn)
391
+
392
+ layer.route = undefined
393
+
394
+ this.stack.push(layer)
395
+ }
396
+
397
+ return this
398
+ }
399
+
400
+ /**
401
+ * Create a new Route for the given path.
402
+ *
403
+ * Each route contains a separate middleware stack and VERB handlers.
404
+ *
405
+ * See the Route api documentation for details on adding handlers
406
+ * and middleware to routes.
407
+ *
408
+ * @param {string} path
409
+ * @return {Route}
410
+ * @public
411
+ */
412
+
413
+ Router.prototype.route = function route (path) {
414
+ const route = new Route(path)
415
+
416
+ const layer = new Layer(path, {
417
+ sensitive: this.caseSensitive,
418
+ strict: this.strict,
419
+ end: true
420
+ }, handle)
421
+
422
+ function handle (req, res, next) {
423
+ route.dispatch(req, res, next)
424
+ }
425
+
426
+ layer.route = route
427
+
428
+ this.stack.push(layer)
429
+ return route
430
+ }
431
+
432
+ // create Router#VERB functions
433
+ methods.concat('all').forEach(function (method) {
434
+ Router.prototype[method] = function (path, ...args) {
435
+ const route = this.route(path)
436
+ route[method].apply(route, args)
437
+ return this
438
+ }
439
+ })
440
+
441
+ /**
442
+ * Generate a callback that will make an OPTIONS response.
443
+ *
444
+ * @param {OutgoingMessage} res
445
+ * @param {array} methods
446
+ * @private
447
+ */
448
+
449
+ function generateOptionsResponder (res, methods) {
450
+ return function onDone (fn, err) {
451
+ if (err || methods.length === 0) {
452
+ return fn(err)
453
+ }
454
+
455
+ trySendOptionsResponse(res, methods, fn)
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Get pathname of request.
461
+ *
462
+ * @param {IncomingMessage} req
463
+ * @private
464
+ */
465
+
466
+ function getPathname (req) {
467
+ try {
468
+ return parseUrl(req).pathname
469
+ } catch (err) {
470
+ return undefined
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Get get protocol + host for a URL.
476
+ *
477
+ * @param {string} url
478
+ * @private
479
+ */
480
+
481
+ function getProtohost (url) {
482
+ if (typeof url !== 'string' || url.length === 0 || url[0] === '/') {
483
+ return undefined
484
+ }
485
+
486
+ const searchIndex = url.indexOf('?')
487
+ const pathLength = searchIndex !== -1
488
+ ? searchIndex
489
+ : url.length
490
+ const fqdnIndex = url.substring(0, pathLength).indexOf('://')
491
+
492
+ return fqdnIndex !== -1
493
+ ? url.substring(0, url.indexOf('/', 3 + fqdnIndex))
494
+ : undefined
495
+ }
496
+
497
+ /**
498
+ * Match path to a layer.
499
+ *
500
+ * @param {Layer} layer
501
+ * @param {string} path
502
+ * @private
503
+ */
504
+
505
+ function matchLayer (layer, path) {
506
+ try {
507
+ return layer.match(path)
508
+ } catch (err) {
509
+ return err
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Merge params with parent params
515
+ *
516
+ * @private
517
+ */
518
+
519
+ function mergeParams (params, parent) {
520
+ if (typeof parent !== 'object' || !parent) {
521
+ return params
522
+ }
523
+
524
+ // make copy of parent for base
525
+ const obj = { ...parent }
526
+
527
+ // simple non-numeric merging
528
+ if (!(0 in params) || !(0 in parent)) {
529
+ return Object.assign(obj, params)
530
+ }
531
+
532
+ let i = 0
533
+ let o = 0
534
+
535
+ // determine numeric gap in params
536
+ while (i in params) {
537
+ i++
538
+ }
539
+
540
+ // determine numeric gap in parent
541
+ while (o in parent) {
542
+ o++
543
+ }
544
+
545
+ // offset numeric indices in params before merge
546
+ for (i--; i >= 0; i--) {
547
+ params[i + o] = params[i]
548
+
549
+ // create holes for the merge when necessary
550
+ if (i < o) {
551
+ delete params[i]
552
+ }
553
+ }
554
+
555
+ return Object.assign(obj, params)
556
+ }
557
+
558
+ /**
559
+ * Process any parameters for the layer.
560
+ *
561
+ * @private
562
+ */
563
+
564
+ function processParams (params, layer, called, req, res, done) {
565
+ // captured parameters from the layer, keys and values
566
+ const keys = layer.keys
567
+
568
+ // fast track
569
+ if (!keys || keys.length === 0) {
570
+ return done()
571
+ }
572
+
573
+ let i = 0
574
+ let paramIndex = 0
575
+ let key
576
+ let paramVal
577
+ let paramCallbacks
578
+ let paramCalled
579
+
580
+ // process params in order
581
+ // param callbacks can be async
582
+ function param (err) {
583
+ if (err) {
584
+ return done(err)
585
+ }
586
+
587
+ if (i >= keys.length) {
588
+ return done()
589
+ }
590
+
591
+ paramIndex = 0
592
+ key = keys[i++]
593
+ paramVal = req.params[key]
594
+ paramCallbacks = params[key]
595
+ paramCalled = called[key]
596
+
597
+ if (paramVal === undefined || !paramCallbacks) {
598
+ return param()
599
+ }
600
+
601
+ // param previously called with same value or error occurred
602
+ if (paramCalled && (paramCalled.match === paramVal ||
603
+ (paramCalled.error && paramCalled.error !== 'route'))) {
604
+ // restore value
605
+ req.params[key] = paramCalled.value
606
+
607
+ // next param
608
+ return param(paramCalled.error)
609
+ }
610
+
611
+ called[key] = paramCalled = {
612
+ error: null,
613
+ match: paramVal,
614
+ value: paramVal
615
+ }
616
+
617
+ paramCallback()
618
+ }
619
+
620
+ // single param callbacks
621
+ function paramCallback (err) {
622
+ const fn = paramCallbacks[paramIndex++]
623
+
624
+ // store updated value
625
+ paramCalled.value = req.params[key]
626
+
627
+ if (err) {
628
+ // store error
629
+ paramCalled.error = err
630
+ param(err)
631
+ return
632
+ }
633
+
634
+ if (!fn) return param()
635
+
636
+ try {
637
+ const ret = fn(req, res, paramCallback, paramVal, key)
638
+ if (ret instanceof Promise) {
639
+ ret.catch((error = new Error('Rejected promise')) => paramCallback(error))
640
+ }
641
+ } catch (e) {
642
+ paramCallback(e)
643
+ }
644
+ }
645
+
646
+ param()
647
+ }
648
+
649
+ /**
650
+ * Restore obj props after function
651
+ *
652
+ * @private
653
+ */
654
+
655
+ function restore (fn, obj, ...props) {
656
+ const vals = props.map(prop => obj[prop])
657
+
658
+ return function () {
659
+ // restore vals
660
+ for (let i = 0; i < props.length; i++) {
661
+ obj[props[i]] = vals[i]
662
+ }
663
+
664
+ return fn.apply(this, arguments)
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Send an OPTIONS response.
670
+ *
671
+ * @private
672
+ */
673
+
674
+ function sendOptionsResponse (res, methods) {
675
+ const options = Object.create(null)
676
+
677
+ // build unique method map
678
+ for (let i = 0; i < methods.length; i++) {
679
+ options[methods[i]] = true
680
+ }
681
+
682
+ // construct the allow list
683
+ const allow = Object.keys(options).sort().join(', ')
684
+
685
+ // send response
686
+ res.setHeader('Allow', allow)
687
+ res.setHeader('Content-Length', Buffer.byteLength(allow))
688
+ res.setHeader('Content-Type', 'text/plain')
689
+ res.setHeader('X-Content-Type-Options', 'nosniff')
690
+ res.end(allow)
691
+ }
692
+
693
+ /**
694
+ * Try to send an OPTIONS response.
695
+ *
696
+ * @private
697
+ */
698
+
699
+ function trySendOptionsResponse (res, methods, next) {
700
+ try {
701
+ sendOptionsResponse(res, methods)
702
+ } catch (err) {
703
+ next(err)
704
+ }
705
+ }
706
+
707
+ /**
708
+ * Wrap a function
709
+ *
710
+ * @private
711
+ */
712
+
713
+ function wrap (old, fn) {
714
+ return function proxy (...args) {
715
+ fn.call(this, old, ...args)
716
+ }
717
+ }