@ditojs/router 2.26.1 → 2.28.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/router",
3
- "version": "2.26.1",
3
+ "version": "2.28.0",
4
4
  "type": "module",
5
5
  "description": "Dito.js Router – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/router",
@@ -21,7 +21,7 @@
21
21
  "node >= 18"
22
22
  ],
23
23
  "dependencies": {
24
- "@ditojs/utils": "^2.26.1"
24
+ "@ditojs/utils": "^2.28.0"
25
25
  },
26
26
  "keywords": [
27
27
  "route",
@@ -30,6 +30,6 @@
30
30
  "koa",
31
31
  "dito"
32
32
  ],
33
- "gitHead": "8f2e07e3774d0e03b24ee78cffe60c5edacb93ae",
33
+ "gitHead": "2bb631b6048da2d1dcb850d89796c87a4787b258",
34
34
  "readme": "# Dito.js Router\n\nDito.js is a declarative and modern web framework with a focus on API driven\ndevelopment, based on Koa.js, Objection.js and Vue.js\n\nReleased in 2018 under the MIT license, with support by https://lineto.com/\n\nDito.js Router is a high performance, tree-based, framework agnostic HTTP\nrouter, based on [trek-router](https://github.com/trekjs/router), which in turn\nis inspired by [Echo](https://github.com/labstack/echo)'s Router.\n\n## How does it work?\n\nThe router relies on a tree structure which makes heavy use of common\nprefixes, essentially a [prefix tree](https://en.wikipedia.org/wiki/Trie).\n\n## Usage\n\n```js\nimport Koa from 'koa'\nimport Router from '@ditojs/router'\n\nconst app = new Koa()\nconst router = new Router()\n\n// static route\nrouter.get('/folders/files/bolt.gif', ctx => {\n ctx.body = `this ain't no GIF!`\n})\n\n// param route\nrouter.get('/users/:id', ctx => {\n ctx.body = `requesting user ${ctx.params.id}`\n})\n\n// match-any route\nrouter.get('/books/*', ctx => {\n ctx.body = `sub-route: ${ctx.params['*']}`\n})\n\n// Handler found\nlet { handler, params } = router.find('get', '/users/233')\nconsole.log(handler) // ctx => { ... }\n\n// Entry not Found\nlet { handler, params } = router.find('get', '/photos/233')\nconsole.log(handler) // null\n\n// Install router middleware\napp.use(async (ctx, next) => {\n const { method, path } = ctx\n const result = router.find(method, path)\n const { handler, params } = result\n if (handler) {\n ctx.params = params || {}\n return handler(ctx, next)\n } else {\n try {\n await next()\n } finally {\n if (ctx.body === undefined && ctx.status === 404) {\n ctx.status = result.status || 404\n if (ctx.status !== 404 && result.allowed) {\n ctx.set('Allow', result.allowed.join(', '))\n }\n }\n }\n }\n})\n\napp.listen(4040, () => console.log('Koa app listening on 4040'))\n```\n"
35
35
  }
package/src/Node.js CHANGED
@@ -26,11 +26,13 @@ export default class Node {
26
26
  this.type = type
27
27
  this.label = prefix[0]
28
28
  this.prefix = prefix
29
- this.children = children
29
+ this.children = []
30
30
  this.parameters = parameters
31
31
  this.handler = handler
32
32
  this.paramKey = null
33
- this.hasMatchAny = false
33
+ for (const child of children) {
34
+ this.addChild(child)
35
+ }
34
36
  }
35
37
 
36
38
  setup(parameters, handler) {
@@ -179,22 +179,27 @@ describe('Router', () => {
179
179
  expect(result.params).toEqual({
180
180
  $: 'hello'
181
181
  })
182
+
183
+ result = router.find('GET', '/prefix/hello/there/suffix')
184
+ expect(result.handler).toBeUndefined()
182
185
  })
183
186
 
184
187
  it('handles match-any nodes with suffixes', () => {
185
- router.add('GET', '/static/**/suffix', handler)
186
- router.add('GET', '/static/**/suffix/**/more', handler)
187
- router.add('GET', '/static/**/suffix/*/1/:param/2', handler)
188
- router.add('GET', '/static/**/suffix/*/1/:param/2/**/end', handler)
188
+ createRoutes(router, [
189
+ ['GET', '/static/**/suffix', 'one'],
190
+ ['GET', '/static/**/suffix/**/more', 'two'],
191
+ ['GET', '/static/**/suffix/*/1/:param/2', 'three'],
192
+ ['GET', '/static/**/suffix/*/1/:param/2/**/end', 'four']
193
+ ])
189
194
 
190
195
  expect(router.toString()).toBe(
191
196
  deindent`
192
197
  / children=1
193
198
  └── static/ children=4
194
- ├── **/suffix handler() children=0
195
- ├── **/suffix/**/more handler() children=0
196
- ├── **/suffix/*/1/:param/2 handler() children=0
197
- └── **/suffix/*/1/:param/2/**/end handler() children=0
199
+ ├── **/suffix one() children=0
200
+ ├── **/suffix/**/more two() children=0
201
+ ├── **/suffix/*/1/:param/2 three() children=0
202
+ └── **/suffix/*/1/:param/2/**/end four() children=0
198
203
  `.trim()
199
204
  )
200
205
 
@@ -211,13 +216,13 @@ describe('Router', () => {
211
216
  expect(result.handler).toBeUndefined()
212
217
 
213
218
  result = router.find('GET', '/static/one/suffix')
214
- expect(result.handler).toBe(handler)
219
+ expect(result.handler?.name).toBe('one')
215
220
  expect(result.params).toEqual({
216
221
  $$: 'one'
217
222
  })
218
223
 
219
224
  result = router.find('GET', '/static/one/two/suffix')
220
- expect(result.handler).toBe(handler)
225
+ expect(result.handler?.name).toBe('one')
221
226
  expect(result.params).toEqual({
222
227
  $$: 'one/two'
223
228
  })
@@ -226,14 +231,14 @@ describe('Router', () => {
226
231
  expect(result.handler).toBeUndefined()
227
232
 
228
233
  result = router.find('GET', '/static/one/two/suffix/three/four/more')
229
- expect(result.handler).toBe(handler)
234
+ expect(result.handler?.name).toBe('two')
230
235
  expect(result.params).toEqual({
231
236
  $$0: 'one/two',
232
237
  $$1: 'three/four'
233
238
  })
234
239
 
235
240
  result = router.find('GET', '/static/one/two/suffix/three/1/bla/2')
236
- expect(result.handler).toBe(handler)
241
+ expect(result.handler?.name).toBe('three')
237
242
  expect(result.params).toEqual({
238
243
  $$: 'one/two',
239
244
  $: 'three',
@@ -247,7 +252,7 @@ describe('Router', () => {
247
252
  'GET',
248
253
  `/static/one/two/suffix/three/1/bla/2/what/ever/end`
249
254
  )
250
- expect(result.handler).toBe(handler)
255
+ expect(result.handler?.name).toBe('four')
251
256
  expect(result.params).toEqual({
252
257
  $: 'three',
253
258
  param: 'bla',
@@ -256,22 +261,49 @@ describe('Router', () => {
256
261
  })
257
262
  })
258
263
 
264
+ it.only('Allow match-any child-routes to coexist with other child-routes', () => {
265
+ createRoutes(router, [
266
+ ['POST', '/api/admin/pages'],
267
+ ['POST', '/api/admin/pages/upload/items/**/file'],
268
+ ['POST', '/api/admin/pages/upload/items/**/files'],
269
+ ['POST', '/api/admin/pages/:id/child-pages']
270
+ ])
271
+ expect(router.toString('POST')).toBe(
272
+ deindent`
273
+ / children=1
274
+ └── api/admin/pages ƒ() children=1
275
+ └── / children=2
276
+ ├── upload/items/ children=2
277
+ │ ├── **/file ƒ() children=0
278
+ │ └── **/files ƒ() children=0
279
+ └── :id children=1
280
+ └── /child-pages ƒ() children=0
281
+ `.trim()
282
+ )
283
+
284
+ result = router.find('POST', '/api/admin/pages/upload/items/1/file')
285
+ expect(result.handler).not.toBeUndefined()
286
+
287
+ result = router.find('POST', '/api/admin/pages/upload/items/1/2/file')
288
+ expect(result.handler).not.toBeUndefined()
289
+ })
290
+
259
291
  it('handles resources', () => {
260
292
  createRoutes(router, [
261
- ['/', 'root'],
262
- ['/geocoder', 'geocoder'],
263
- ['/geocoder/new', 'newGeocoder'],
264
- ['/geocoder/notify', 'notifyGeocoder'],
265
- // ['/geocoder/nnn', 'nnnGeocoder'],
266
- ['/geocoder/edit', 'editGeocoder'],
267
- ['/geocoder/edit/email', 'editEmailGeocoder'],
268
- ['/geocoder/edit/:item', 'editItemGeocoder'],
269
- ['/geocoder/exchange', 'exchangeGeocoder'],
270
- ['/geocoder/exchange/email', 'exchangeEmailGeocoder'],
271
- ['/geocoder/exchange/:item', 'exchangeItemGeocoder'],
272
- ['/geocoder/:id/echo', 'echoGeocoder'],
273
- ['/geocoder/:action', 'actionGeocoder'],
274
- ['/geocoder/**', 'anyGeocoder']
293
+ ['GET', '/', 'root'],
294
+ ['GET', '/geocoder', 'geocoder'],
295
+ ['GET', '/geocoder/new', 'newGeocoder'],
296
+ ['GET', '/geocoder/notify', 'notifyGeocoder'],
297
+ // ['GET', '/geocoder/nnn', 'nnnGeocoder'],
298
+ ['GET', '/geocoder/edit', 'editGeocoder'],
299
+ ['GET', '/geocoder/edit/email', 'editEmailGeocoder'],
300
+ ['GET', '/geocoder/edit/:item', 'editItemGeocoder'],
301
+ ['GET', '/geocoder/exchange', 'exchangeGeocoder'],
302
+ ['GET', '/geocoder/exchange/email', 'exchangeEmailGeocoder'],
303
+ ['GET', '/geocoder/exchange/:item', 'exchangeItemGeocoder'],
304
+ ['GET', '/geocoder/:id/echo', 'echoGeocoder'],
305
+ ['GET', '/geocoder/:action', 'actionGeocoder'],
306
+ ['GET', '/geocoder/**', 'anyGeocoder']
275
307
  ])
276
308
 
277
309
  expect(router.toString()).toBe(
@@ -377,17 +409,17 @@ describe('Router', () => {
377
409
 
378
410
  it('handles nested resources', () => {
379
411
  createRoutes(router, [
380
- ['/users', 'users'],
381
- ['/users/new', 'newUser'],
382
- ['/users/noi', 'newUser'],
383
- ['/users/nei', 'newUser'],
384
- ['/users/:id', 'user'],
385
- ['/users/:id/edit', 'editUser'],
386
- ['/users/:id/:hello/:good/:bad/ddd', 'editUser'],
387
- ['/users/:id/:action', 'actionUser'],
388
- ['/users/:userId/photos/:id', 'photo'],
389
- ['/users/:userId/books/:id', 'book'],
390
- ['/users/**', 'anyUser']
412
+ ['GET', '/users', 'users'],
413
+ ['GET', '/users/new', 'newUser'],
414
+ ['GET', '/users/noi', 'newUser'],
415
+ ['GET', '/users/nei', 'newUser'],
416
+ ['GET', '/users/:id', 'user'],
417
+ ['GET', '/users/:id/edit', 'editUser'],
418
+ ['GET', '/users/:id/:hello/:good/:bad/ddd', 'editUser'],
419
+ ['GET', '/users/:id/:action', 'actionUser'],
420
+ ['GET', '/users/:userId/photos/:id', 'photo'],
421
+ ['GET', '/users/:userId/books/:id', 'book'],
422
+ ['GET', '/users/**', 'anyUser']
391
423
  ])
392
424
 
393
425
  expect(router.toString()).toBe(
@@ -479,27 +511,27 @@ describe('Router', () => {
479
511
 
480
512
  describe('handles multiple resources', () => {
481
513
  const routes = [
482
- ['/users', 'users'],
483
- ['/users/new', 'newUser'],
484
- ['/users/:id', 'user'],
485
- ['/users/:id/:action', 'actionUser'],
486
- ['/users/:id/edit', 'editUser'],
487
- ['/users/:id/change', 'changeUser'],
488
- ['/users/:id/event', 'eventUser'],
489
- ['/photos', 'photos'],
490
- ['/photos/new', 'newPhoto'],
491
- ['/photos/:id', 'photo'],
492
- ['/photos/:id/:action', 'actionPhoto'],
493
- ['/photos/:id/edit', 'editPhoto'],
494
- ['/photos/:id/change', 'changePhoto'],
495
- ['/photos/:id/event', 'eventPhoto'],
496
- ['/books', 'books'],
497
- ['/books/new', 'newBook'],
498
- ['/books/:id', 'book'],
499
- ['/books/:id/:action', 'actionBook'],
500
- ['/books/:id/edit', 'editBook'],
501
- ['/books/:id/change', 'changeBook'],
502
- ['/books/:id/event', 'eventBook']
514
+ ['GET', '/users', 'users'],
515
+ ['GET', '/users/new', 'newUser'],
516
+ ['GET', '/users/:id', 'user'],
517
+ ['GET', '/users/:id/:action', 'actionUser'],
518
+ ['GET', '/users/:id/edit', 'editUser'],
519
+ ['GET', '/users/:id/change', 'changeUser'],
520
+ ['GET', '/users/:id/event', 'eventUser'],
521
+ ['GET', '/photos', 'photos'],
522
+ ['GET', '/photos/new', 'newPhoto'],
523
+ ['GET', '/photos/:id', 'photo'],
524
+ ['GET', '/photos/:id/:action', 'actionPhoto'],
525
+ ['GET', '/photos/:id/edit', 'editPhoto'],
526
+ ['GET', '/photos/:id/change', 'changePhoto'],
527
+ ['GET', '/photos/:id/event', 'eventPhoto'],
528
+ ['GET', '/books', 'books'],
529
+ ['GET', '/books/new', 'newBook'],
530
+ ['GET', '/books/:id', 'book'],
531
+ ['GET', '/books/:id/:action', 'actionBook'],
532
+ ['GET', '/books/:id/edit', 'editBook'],
533
+ ['GET', '/books/:id/change', 'changeBook'],
534
+ ['GET', '/books/:id/event', 'eventBook']
503
535
  ]
504
536
 
505
537
  it('parses routes into the correct tree', () => {
@@ -632,10 +664,10 @@ describe('Router', () => {
632
664
 
633
665
  describe('handles namespaces', () => {
634
666
  const routes = [
635
- ['/admin/articles', 'articles'],
636
- ['/admin/articles/new', 'newArticle'],
637
- ['/admin/articles/:id', 'article'],
638
- ['/admin/articles/:id/edit', 'editArticle']
667
+ ['GET', '/admin/articles', 'articles'],
668
+ ['GET', '/admin/articles/new', 'newArticle'],
669
+ ['GET', '/admin/articles/:id', 'article'],
670
+ ['GET', '/admin/articles/:id/edit', 'editArticle']
639
671
  ]
640
672
 
641
673
  it('parses routes into the correct tree', () => {
@@ -677,14 +709,14 @@ describe('Router', () => {
677
709
 
678
710
  describe('handles multiple nested resources', () => {
679
711
  const routes = [
680
- ['/magazines/:mid/articles', 'articles'],
681
- ['/magazines/:mid/articles/new', 'newArticle'],
682
- ['/magazines/:mid/articles/:id', 'article'],
683
- ['/magazines/:mid/articles/:id/edit', 'editArticle'],
684
- ['/magazines/:m_id/photos', 'photos'],
685
- ['/magazines/:m_id/photos/new', 'newPhoto'],
686
- ['/magazines/:m_id/photos/:id', 'photo'],
687
- ['/magazines/:m_id/photos/:id/edit', 'editPhoto']
712
+ ['GET', '/magazines/:mid/articles', 'articles'],
713
+ ['GET', '/magazines/:mid/articles/new', 'newArticle'],
714
+ ['GET', '/magazines/:mid/articles/:id', 'article'],
715
+ ['GET', '/magazines/:mid/articles/:id/edit', 'editArticle'],
716
+ ['GET', '/magazines/:m_id/photos', 'photos'],
717
+ ['GET', '/magazines/:m_id/photos/new', 'newPhoto'],
718
+ ['GET', '/magazines/:m_id/photos/:id', 'photo'],
719
+ ['GET', '/magazines/:m_id/photos/:id/edit', 'editPhoto']
688
720
  ]
689
721
 
690
722
  it('parses routes into the correct tree', () => {
@@ -781,11 +813,13 @@ describe('Router', () => {
781
813
 
782
814
  function createFunc(name) {
783
815
  // eslint-disable-next-line no-new-func
784
- return new Function(`return function ${name}(){}`)()
816
+ return new Function(
817
+ `return ${name ? `function ${name}(){}` : '() => {}'}`
818
+ )()
785
819
  }
786
820
 
787
821
  function createRoutes(router, routes) {
788
- routes.forEach(([path, name]) => {
789
- router.add('GET', path, createFunc(name))
822
+ routes.forEach(([method, path, name]) => {
823
+ router.add(method, path, createFunc(name))
790
824
  })
791
825
  }