adapt-authoring-server 1.4.2 → 2.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 CHANGED
@@ -2,4 +2,5 @@
2
2
  * HTTP server functionality using Express.js
3
3
  * @namespace server
4
4
  */
5
+ export { addExistenceProps, cacheRouteConfig, generateRouterMap, getAllRoutes, mapHandler } from './lib/utils.js'
5
6
  export { default } from './lib/ServerModule.js'
package/lib/Router.js CHANGED
@@ -2,6 +2,9 @@ import _ from 'lodash'
2
2
  import { App } from 'adapt-authoring-core'
3
3
  import express from 'express'
4
4
  import ServerUtils from './ServerUtils.js'
5
+ import { addExistenceProps } from './utils/addExistenceProps.js'
6
+ import { cacheRouteConfig } from './utils/cacheRouteConfig.js'
7
+ import { generateRouterMap } from './utils/generateRouterMap.js'
5
8
  /**
6
9
  * Handles the Express routing functionality
7
10
  * @memberof server
@@ -54,7 +57,7 @@ class Router {
54
57
  * @type {Array<Function>}
55
58
  */
56
59
  this.handlerMiddleware = [
57
- ServerUtils.addExistenceProps,
60
+ addExistenceProps,
58
61
  ServerUtils.handleInternalRoutes,
59
62
  ...handlerMiddleware.filter(_.isFunction)
60
63
  ]
@@ -66,7 +69,7 @@ class Router {
66
69
  * @type {Object}
67
70
  */
68
71
  get map () {
69
- return ServerUtils.generateRouterMap(this)
72
+ return generateRouterMap(this)
70
73
  }
71
74
 
72
75
  /**
@@ -226,7 +229,7 @@ class Router {
226
229
  if (this.routes.length) {
227
230
  this.routes.forEach(r => {
228
231
  Object.entries(r.handlers).forEach(([method, handler]) => {
229
- this.expressRouter[method](r.route, ServerUtils.cacheRouteConfig(r), ...this.getHandlerMiddleware(), handler)
232
+ this.expressRouter[method](r.route, cacheRouteConfig(r), ...this.getHandlerMiddleware(), handler)
230
233
  this.log('verbose', 'ADD_ROUTE', method.toUpperCase(), `${this.path !== '/' ? this.path : ''}${r.route}`)
231
234
  })
232
235
  })
@@ -1,5 +1,6 @@
1
- import _ from 'lodash'
2
1
  import { App } from 'adapt-authoring-core'
2
+ import { getAllRoutes } from './utils/getAllRoutes.js'
3
+
3
4
  /**
4
5
  * Server-related utilities
5
6
  * @memberof server
@@ -23,7 +24,7 @@ class ServerUtils {
23
24
  */
24
25
  static methodNotAllowedHandler (router) {
25
26
  const routePatterns = []
26
- const routeMap = ServerUtils.getAllRoutes(router)
27
+ const routeMap = getAllRoutes(router)
27
28
 
28
29
  for (const [path, methods] of routeMap.entries()) {
29
30
  const pathPattern = path
@@ -75,55 +76,6 @@ class ServerUtils {
75
76
  res.status(App.instance.errors.NOT_FOUND.statusCode).end()
76
77
  }
77
78
 
78
- /**
79
- * Handler for returning an API map
80
- * @param {Router} topRouter
81
- * @return {Function} Middleware function
82
- */
83
- static mapHandler (topRouter) {
84
- return (req, res) => res.json(topRouter.map)
85
- }
86
-
87
- /**
88
- * Collects all registered routes with their methods across the router hierarchy
89
- * @param {Router} router The router to traverse
90
- * @return {Map<string, Set<string>>} Map of route paths to sets of allowed methods
91
- */
92
- static getAllRoutes (router) {
93
- const routeMap = new Map()
94
-
95
- router.flattenRouters().forEach(r => {
96
- r.routes.forEach(route => {
97
- const fullPath = `${r.path !== '/' ? r.path : ''}${route.route}`
98
-
99
- if (!routeMap.has(fullPath)) {
100
- routeMap.set(fullPath, new Set())
101
- }
102
-
103
- Object.keys(route.handlers).forEach(method => {
104
- routeMap.get(fullPath).add(method.toUpperCase())
105
- })
106
- })
107
- })
108
-
109
- return routeMap
110
- }
111
-
112
- /**
113
- * Generates a map for a given router
114
- * @param {Router} topRouter
115
- * @return {Object} The route map
116
- */
117
- static generateRouterMap (topRouter) {
118
- return topRouter.flattenRouters()
119
- .sort((a, b) => a.root.localeCompare(b.root))
120
- .reduce((m, r) => {
121
- const key = `${getRelativeRoute(topRouter, r)}endpoints`
122
- const endpoints = getEndpoints(r)
123
- return endpoints.length ? { ...m, [key]: endpoints } : m
124
- }, {})
125
- }
126
-
127
79
  /**
128
80
  * Adds extra properties to the request object to allow for easy translations
129
81
  * @param {Function} next
@@ -160,47 +112,6 @@ class ServerUtils {
160
112
  next()
161
113
  }
162
114
 
163
- /**
164
- * Adds extra properties to the request object to allow for easy existence checking of common request objects
165
- * @param {external:ExpressRequest} req
166
- * @param {external:ExpressResponse} res
167
- * @param {Function} next
168
- * @example
169
- * "IMPORTANT NOTE: body data is completely ignored for GET requests, any code
170
- * requiring it should switch to use POST."
171
- *
172
- * let req = { 'params': { 'foo':'bar' }, 'query': {}, 'body': {} };
173
- * req.hasParams // true
174
- * req.hasQuery // false
175
- * req.hasBody // false
176
- */
177
- static addExistenceProps (req, res, next) {
178
- if (req.method === 'GET') {
179
- req.body = {}
180
- }
181
- const storeVal = (key, exists) => {
182
- req[`has${_.capitalize(key)}`] = exists
183
- }
184
- ['body', 'params', 'query'].forEach(attr => {
185
- if (!req[attr]) {
186
- return storeVal(attr, false)
187
- }
188
- const entries = Object.entries(req[attr])
189
- let deleted = 0
190
- if (entries.length === 0) {
191
- return storeVal(attr, false)
192
- }
193
- entries.forEach(([key, val]) => {
194
- if (val === undefined || val === null) {
195
- delete req[attr][key]
196
- deleted++
197
- }
198
- })
199
- storeVal(attr, deleted < entries.length)
200
- })
201
- next()
202
- }
203
-
204
115
  /**
205
116
  * Handles restriction of routes marked as internal
206
117
  * @param {external:ExpressRequest} req
@@ -215,39 +126,6 @@ class ServerUtils {
215
126
  }
216
127
  next()
217
128
  }
218
-
219
- /**
220
- * Caches the route config on the incoming request
221
- * @param {Route} routeConfig
222
- * @return {Function}
223
- */
224
- static cacheRouteConfig (routeConfig) {
225
- return (req, res, next) => {
226
- req.routeConfig = routeConfig
227
- next()
228
- }
229
- }
230
- }
231
- /** @ignore */ function getEndpoints (r) {
232
- return r.routes.map(route => {
233
- return {
234
- url: `${r.url}${route.route}`,
235
- accepted_methods: Object.keys(route.handlers).reduce((memo, method) => {
236
- return {
237
- ...memo,
238
- [method]: route?.meta?.[method] ?? {}
239
- }
240
- }, {})
241
- }
242
- })
243
- }
244
- /** @ignore */ function getRelativeRoute (relFrom, relTo) {
245
- if (relFrom === relTo) {
246
- return `${relFrom.route}_`
247
- }
248
- let route = ''
249
- for (let r = relTo; r !== relFrom; r = r.parentRouter) route = `${r.root}_${route}`
250
- return route
251
129
  }
252
130
 
253
131
  export default ServerUtils
@@ -0,0 +1,43 @@
1
+ import _ from 'lodash'
2
+
3
+ /**
4
+ * Adds extra properties to the request object to allow for easy existence checking of common request objects
5
+ * @param {external:ExpressRequest} req
6
+ * @param {external:ExpressResponse} res
7
+ * @param {Function} next
8
+ * @memberof server
9
+ * @example
10
+ * "IMPORTANT NOTE: body data is completely ignored for GET requests, any code
11
+ * requiring it should switch to use POST."
12
+ *
13
+ * let req = { 'params': { 'foo':'bar' }, 'query': {}, 'body': {} };
14
+ * req.hasParams // true
15
+ * req.hasQuery // false
16
+ * req.hasBody // false
17
+ */
18
+ export function addExistenceProps (req, res, next) {
19
+ if (req.method === 'GET') {
20
+ req.body = {}
21
+ }
22
+ const storeVal = (key, exists) => {
23
+ req[`has${_.capitalize(key)}`] = exists
24
+ }
25
+ ;['body', 'params', 'query'].forEach(attr => {
26
+ if (!req[attr]) {
27
+ return storeVal(attr, false)
28
+ }
29
+ const entries = Object.entries(req[attr])
30
+ let deleted = 0
31
+ if (entries.length === 0) {
32
+ return storeVal(attr, false)
33
+ }
34
+ entries.forEach(([key, val]) => {
35
+ if (val === undefined || val === null) {
36
+ delete req[attr][key]
37
+ deleted++
38
+ }
39
+ })
40
+ storeVal(attr, deleted < entries.length)
41
+ })
42
+ next()
43
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Caches the route config on the incoming request
3
+ * @param {Route} routeConfig
4
+ * @return {Function}
5
+ * @memberof server
6
+ */
7
+ export function cacheRouteConfig (routeConfig) {
8
+ return (req, res, next) => {
9
+ req.routeConfig = routeConfig
10
+ next()
11
+ }
12
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Generates a map for a given router
3
+ * @param {Router} topRouter
4
+ * @return {Object} The route map
5
+ * @memberof server
6
+ */
7
+ export function generateRouterMap (topRouter) {
8
+ return topRouter.flattenRouters()
9
+ .sort((a, b) => a.root.localeCompare(b.root))
10
+ .reduce((m, r) => {
11
+ const key = `${getRelativeRoute(topRouter, r)}endpoints`
12
+ const endpoints = getEndpoints(r)
13
+ return endpoints.length ? { ...m, [key]: endpoints } : m
14
+ }, {})
15
+ }
16
+
17
+ /** @ignore */
18
+ function getEndpoints (r) {
19
+ return r.routes.map(route => {
20
+ return {
21
+ url: `${r.url}${route.route}`,
22
+ accepted_methods: Object.keys(route.handlers).reduce((memo, method) => {
23
+ return {
24
+ ...memo,
25
+ [method]: route?.meta?.[method] ?? {}
26
+ }
27
+ }, {})
28
+ }
29
+ })
30
+ }
31
+
32
+ /** @ignore */
33
+ function getRelativeRoute (relFrom, relTo) {
34
+ if (relFrom === relTo) {
35
+ return `${relFrom.route}_`
36
+ }
37
+ let route = ''
38
+ for (let r = relTo; r !== relFrom; r = r.parentRouter) route = `${r.root}_${route}`
39
+ return route
40
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Collects all registered routes with their methods across the router hierarchy
3
+ * @param {Router} router The router to traverse
4
+ * @return {Map<string, Set<string>>} Map of route paths to sets of allowed methods
5
+ * @memberof server
6
+ */
7
+ export function getAllRoutes (router) {
8
+ const routeMap = new Map()
9
+
10
+ router.flattenRouters().forEach(r => {
11
+ r.routes.forEach(route => {
12
+ const fullPath = `${r.path !== '/' ? r.path : ''}${route.route}`
13
+
14
+ if (!routeMap.has(fullPath)) {
15
+ routeMap.set(fullPath, new Set())
16
+ }
17
+
18
+ Object.keys(route.handlers).forEach(method => {
19
+ routeMap.get(fullPath).add(method.toUpperCase())
20
+ })
21
+ })
22
+ })
23
+
24
+ return routeMap
25
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Handler for returning an API map
3
+ * @param {Router} topRouter
4
+ * @return {Function} Middleware function
5
+ * @memberof server
6
+ */
7
+ export function mapHandler (topRouter) {
8
+ return (req, res) => res.json(topRouter.map)
9
+ }
package/lib/utils.js ADDED
@@ -0,0 +1,5 @@
1
+ export { addExistenceProps } from './utils/addExistenceProps.js'
2
+ export { cacheRouteConfig } from './utils/cacheRouteConfig.js'
3
+ export { generateRouterMap } from './utils/generateRouterMap.js'
4
+ export { getAllRoutes } from './utils/getAllRoutes.js'
5
+ export { mapHandler } from './utils/mapHandler.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-server",
3
- "version": "1.4.2",
3
+ "version": "2.0.0",
4
4
  "description": "Provides an Express application routing and more",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-server",
6
6
  "license": "GPL-3.0",
@@ -11,7 +11,7 @@
11
11
  "test": "node --test tests/**/*.spec.js"
12
12
  },
13
13
  "dependencies": {
14
- "adapt-authoring-core": "^1.7.0",
14
+ "adapt-authoring-core": "^2.0.0",
15
15
  "express": "^5.1.0",
16
16
  "hbs": "^4.2.0",
17
17
  "lodash": "^4.17.21"
@@ -16,104 +16,6 @@ describe('ServerUtils', () => {
16
16
  ServerUtils = (await import('../lib/ServerUtils.js')).default
17
17
  })
18
18
 
19
- describe('#addExistenceProps()', () => {
20
- it('should set hasBody, hasParams, hasQuery to false for empty objects', () => {
21
- const req = { method: 'POST', body: {}, params: {}, query: {} }
22
- const res = {}
23
- let nextCalled = false
24
- const next = () => { nextCalled = true }
25
-
26
- ServerUtils.addExistenceProps(req, res, next)
27
-
28
- assert.equal(req.hasBody, false)
29
- assert.equal(req.hasParams, false)
30
- assert.equal(req.hasQuery, false)
31
- assert.equal(nextCalled, true)
32
- })
33
-
34
- it('should set hasBody, hasParams, hasQuery to true for populated objects', () => {
35
- const req = { method: 'POST', body: { foo: 'bar' }, params: { id: '123' }, query: { search: 'test' } }
36
- const res = {}
37
- let nextCalled = false
38
- const next = () => { nextCalled = true }
39
-
40
- ServerUtils.addExistenceProps(req, res, next)
41
-
42
- assert.equal(req.hasBody, true)
43
- assert.equal(req.hasParams, true)
44
- assert.equal(req.hasQuery, true)
45
- assert.equal(nextCalled, true)
46
- })
47
-
48
- it('should remove undefined and null values from request objects', () => {
49
- const req = { method: 'POST', body: { foo: 'bar', baz: undefined, qux: null }, params: {}, query: {} }
50
- const res = {}
51
- const next = () => {}
52
-
53
- ServerUtils.addExistenceProps(req, res, next)
54
-
55
- assert.equal(req.body.foo, 'bar')
56
- assert.equal(req.body.baz, undefined)
57
- assert.equal(req.body.qux, undefined)
58
- assert.equal(req.hasBody, true)
59
- })
60
-
61
- it('should clear body for GET requests', () => {
62
- const req = { method: 'GET', body: { foo: 'bar' }, params: {}, query: {} }
63
- const res = {}
64
- const next = () => {}
65
-
66
- ServerUtils.addExistenceProps(req, res, next)
67
-
68
- assert.deepEqual(req.body, {})
69
- assert.equal(req.hasBody, false)
70
- })
71
-
72
- it('should handle falsy attr values (missing body/params/query)', () => {
73
- const req = { method: 'POST', body: null, params: undefined, query: false }
74
- const res = {}
75
- const next = () => {}
76
-
77
- ServerUtils.addExistenceProps(req, res, next)
78
-
79
- assert.equal(req.hasBody, false)
80
- assert.equal(req.hasParams, false)
81
- assert.equal(req.hasQuery, false)
82
- })
83
-
84
- it('should mark has* false when all entries are null or undefined', () => {
85
- const req = { method: 'POST', body: { a: null, b: undefined }, params: {}, query: {} }
86
- const res = {}
87
- const next = () => {}
88
-
89
- ServerUtils.addExistenceProps(req, res, next)
90
-
91
- assert.equal(req.hasBody, false)
92
- })
93
- })
94
-
95
- describe('#cacheRouteConfig()', () => {
96
- it('should cache route config on request object', () => {
97
- const routeConfig = { route: '/test', handlers: {}, internal: false }
98
- const middleware = ServerUtils.cacheRouteConfig(routeConfig)
99
- const req = {}
100
- const res = {}
101
- let nextCalled = false
102
- const next = () => { nextCalled = true }
103
-
104
- middleware(req, res, next)
105
-
106
- assert.equal(req.routeConfig, routeConfig)
107
- assert.equal(nextCalled, true)
108
- })
109
-
110
- it('should return a middleware function', () => {
111
- const middleware = ServerUtils.cacheRouteConfig({})
112
-
113
- assert.equal(typeof middleware, 'function')
114
- })
115
- })
116
-
117
19
  describe('#addErrorHandler()', () => {
118
20
  it('should add sendError method to response object', () => {
119
21
  const req = {}
@@ -251,100 +153,6 @@ describe('ServerUtils', () => {
251
153
  })
252
154
  })
253
155
 
254
- describe('#mapHandler()', () => {
255
- it('should return a function', () => {
256
- const mockRouter = { map: { test: 'data' } }
257
- const handler = ServerUtils.mapHandler(mockRouter)
258
-
259
- assert.equal(typeof handler, 'function')
260
- })
261
-
262
- it('should respond with router map', () => {
263
- const mockRouter = { map: { test: 'data' } }
264
- const handler = ServerUtils.mapHandler(mockRouter)
265
- const req = {}
266
- let responseData = null
267
- const res = {
268
- json: (data) => { responseData = data }
269
- }
270
-
271
- handler(req, res)
272
-
273
- assert.deepEqual(responseData, { test: 'data' })
274
- })
275
- })
276
-
277
- describe('#getAllRoutes()', () => {
278
- it('should collect routes from router hierarchy', () => {
279
- const mockRouter = {
280
- path: '/api',
281
- routes: [
282
- { route: '/users', handlers: { get: () => {}, post: () => {} } },
283
- { route: '/posts', handlers: { get: () => {} } }
284
- ],
285
- flattenRouters: () => [mockRouter]
286
- }
287
-
288
- const routeMap = ServerUtils.getAllRoutes(mockRouter)
289
-
290
- assert.ok(routeMap instanceof Map)
291
- assert.equal(routeMap.size, 2)
292
- assert.ok(routeMap.has('/api/users'))
293
- assert.ok(routeMap.has('/api/posts'))
294
- assert.ok(routeMap.get('/api/users').has('GET'))
295
- assert.ok(routeMap.get('/api/users').has('POST'))
296
- assert.ok(routeMap.get('/api/posts').has('GET'))
297
- })
298
-
299
- it('should collect routes from multiple routers in hierarchy', () => {
300
- const childRouter = {
301
- path: '/api/v1',
302
- routes: [
303
- { route: '/items', handlers: { get: () => {} } }
304
- ]
305
- }
306
- const parentRouter = {
307
- path: '/api',
308
- routes: [
309
- { route: '/health', handlers: { get: () => {} } }
310
- ],
311
- flattenRouters: () => [parentRouter, childRouter]
312
- }
313
-
314
- const routeMap = ServerUtils.getAllRoutes(parentRouter)
315
-
316
- assert.equal(routeMap.size, 2)
317
- assert.ok(routeMap.has('/api/health'))
318
- assert.ok(routeMap.has('/api/v1/items'))
319
- })
320
-
321
- it('should handle root path "/" by omitting it from the prefix', () => {
322
- const mockRouter = {
323
- path: '/',
324
- routes: [
325
- { route: '/status', handlers: { get: () => {} } }
326
- ],
327
- flattenRouters: () => [mockRouter]
328
- }
329
-
330
- const routeMap = ServerUtils.getAllRoutes(mockRouter)
331
-
332
- assert.ok(routeMap.has('/status'))
333
- })
334
-
335
- it('should return empty map for router with no routes', () => {
336
- const mockRouter = {
337
- path: '/api',
338
- routes: [],
339
- flattenRouters: () => [mockRouter]
340
- }
341
-
342
- const routeMap = ServerUtils.getAllRoutes(mockRouter)
343
-
344
- assert.equal(routeMap.size, 0)
345
- })
346
- })
347
-
348
156
  describe('#methodNotAllowedHandler()', () => {
349
157
  it('should return a middleware function', () => {
350
158
  const mockRouter = {
@@ -420,103 +228,6 @@ describe('ServerUtils', () => {
420
228
  })
421
229
  })
422
230
 
423
- describe('#generateRouterMap()', () => {
424
- it('should generate a router map', () => {
425
- const mockRouter = {
426
- root: 'api',
427
- path: '/api',
428
- url: 'http://localhost:5000/api',
429
- routes: [
430
- { route: '/test', handlers: { get: () => {} } }
431
- ],
432
- childRouters: [],
433
- flattenRouters: function () {
434
- return [this]
435
- }
436
- }
437
-
438
- const map = ServerUtils.generateRouterMap(mockRouter)
439
-
440
- assert.equal(typeof map, 'object')
441
- })
442
-
443
- it('should return empty object for router with no routes', () => {
444
- const mockRouter = {
445
- root: 'api',
446
- path: '/api',
447
- url: 'http://localhost:5000/api',
448
- routes: [],
449
- childRouters: [],
450
- flattenRouters: function () {
451
- return [this]
452
- }
453
- }
454
-
455
- const map = ServerUtils.generateRouterMap(mockRouter)
456
-
457
- assert.deepEqual(map, {})
458
- })
459
-
460
- it('should include endpoint URLs and accepted methods', () => {
461
- const mockRouter = {
462
- root: 'api',
463
- route: '/api',
464
- path: '/api',
465
- url: 'http://localhost:5000/api',
466
- routes: [
467
- { route: '/users', handlers: { get: () => {}, post: () => {} }, meta: { get: { description: 'list users' } } }
468
- ],
469
- childRouters: [],
470
- parentRouter: null,
471
- flattenRouters: function () {
472
- return [this]
473
- }
474
- }
475
-
476
- const map = ServerUtils.generateRouterMap(mockRouter)
477
- const keys = Object.keys(map)
478
-
479
- assert.ok(keys.length > 0)
480
- const endpoints = map[keys[0]]
481
- assert.ok(Array.isArray(endpoints))
482
- assert.equal(endpoints[0].url, 'http://localhost:5000/api/users')
483
- assert.ok('get' in endpoints[0].accepted_methods)
484
- assert.ok('post' in endpoints[0].accepted_methods)
485
- assert.deepEqual(endpoints[0].accepted_methods.get, { description: 'list users' })
486
- assert.deepEqual(endpoints[0].accepted_methods.post, {})
487
- })
488
-
489
- it('should use relative route keys for child routers', () => {
490
- const childRouter = {
491
- root: 'users',
492
- path: '/api/users',
493
- url: 'http://localhost:5000/api/users',
494
- routes: [
495
- { route: '/:id', handlers: { get: () => {} } }
496
- ],
497
- childRouters: [],
498
- parentRouter: null
499
- }
500
- const mockRouter = {
501
- root: 'api',
502
- route: '/api',
503
- path: '/api',
504
- url: 'http://localhost:5000/api',
505
- routes: [],
506
- childRouters: [childRouter],
507
- flattenRouters: function () {
508
- return [this, childRouter]
509
- }
510
- }
511
- childRouter.parentRouter = mockRouter
512
-
513
- const map = ServerUtils.generateRouterMap(mockRouter)
514
- const keys = Object.keys(map)
515
-
516
- assert.ok(keys.some(k => k.includes('users')))
517
- })
518
- })
519
-
520
231
  describe('#rootNotFoundHandler()', () => {
521
232
  it('should respond with NOT_FOUND status code', () => {
522
233
  App.instance.errors = App.instance.errors || {}
@@ -0,0 +1,90 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { addExistenceProps } from '../lib/utils/addExistenceProps.js'
4
+
5
+ describe('addExistenceProps()', () => {
6
+ it('should set hasBody, hasParams, hasQuery to false for empty objects', () => {
7
+ const req = { method: 'POST', body: {}, params: {}, query: {} }
8
+ const res = {}
9
+ let nextCalled = false
10
+ const next = () => { nextCalled = true }
11
+
12
+ addExistenceProps(req, res, next)
13
+
14
+ assert.equal(req.hasBody, false)
15
+ assert.equal(req.hasParams, false)
16
+ assert.equal(req.hasQuery, false)
17
+ assert.equal(nextCalled, true)
18
+ })
19
+
20
+ it('should set hasBody, hasParams, hasQuery to true for populated objects', () => {
21
+ const req = { method: 'POST', body: { foo: 'bar' }, params: { id: '123' }, query: { search: 'test' } }
22
+ const res = {}
23
+ let nextCalled = false
24
+ const next = () => { nextCalled = true }
25
+
26
+ addExistenceProps(req, res, next)
27
+
28
+ assert.equal(req.hasBody, true)
29
+ assert.equal(req.hasParams, true)
30
+ assert.equal(req.hasQuery, true)
31
+ assert.equal(nextCalled, true)
32
+ })
33
+
34
+ it('should remove undefined and null values from request objects', () => {
35
+ const req = { method: 'POST', body: { foo: 'bar', baz: undefined, qux: null }, params: {}, query: {} }
36
+ const res = {}
37
+ const next = () => {}
38
+
39
+ addExistenceProps(req, res, next)
40
+
41
+ assert.equal(req.body.foo, 'bar')
42
+ assert.equal(req.body.baz, undefined)
43
+ assert.equal(req.body.qux, undefined)
44
+ assert.equal(req.hasBody, true)
45
+ })
46
+
47
+ it('should clear body for GET requests', () => {
48
+ const req = { method: 'GET', body: { foo: 'bar' }, params: {}, query: {} }
49
+ const res = {}
50
+ const next = () => {}
51
+
52
+ addExistenceProps(req, res, next)
53
+
54
+ assert.deepEqual(req.body, {})
55
+ assert.equal(req.hasBody, false)
56
+ })
57
+
58
+ it('should handle falsy attr values (missing body/params/query)', () => {
59
+ const req = { method: 'POST', body: null, params: undefined, query: false }
60
+ const res = {}
61
+ const next = () => {}
62
+
63
+ addExistenceProps(req, res, next)
64
+
65
+ assert.equal(req.hasBody, false)
66
+ assert.equal(req.hasParams, false)
67
+ assert.equal(req.hasQuery, false)
68
+ })
69
+
70
+ it('should mark has* false when all entries are null or undefined', () => {
71
+ const req = { method: 'POST', body: { a: null, b: undefined }, params: {}, query: {} }
72
+ const res = {}
73
+ const next = () => {}
74
+
75
+ addExistenceProps(req, res, next)
76
+
77
+ assert.equal(req.hasBody, false)
78
+ })
79
+
80
+ it('should always call next()', () => {
81
+ const req = { method: 'POST', body: {}, params: {}, query: {} }
82
+ const res = {}
83
+ let nextCalled = false
84
+ const next = () => { nextCalled = true }
85
+
86
+ addExistenceProps(req, res, next)
87
+
88
+ assert.equal(nextCalled, true)
89
+ })
90
+ })
@@ -0,0 +1,48 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { cacheRouteConfig } from '../lib/utils/cacheRouteConfig.js'
4
+
5
+ describe('cacheRouteConfig()', () => {
6
+ it('should return a middleware function', () => {
7
+ const middleware = cacheRouteConfig({})
8
+
9
+ assert.equal(typeof middleware, 'function')
10
+ })
11
+
12
+ it('should cache route config on request object', () => {
13
+ const routeConfig = { route: '/test', handlers: {}, internal: false }
14
+ const middleware = cacheRouteConfig(routeConfig)
15
+ const req = {}
16
+ const res = {}
17
+ let nextCalled = false
18
+ const next = () => { nextCalled = true }
19
+
20
+ middleware(req, res, next)
21
+
22
+ assert.equal(req.routeConfig, routeConfig)
23
+ assert.equal(nextCalled, true)
24
+ })
25
+
26
+ it('should always call next()', () => {
27
+ const middleware = cacheRouteConfig({})
28
+ const req = {}
29
+ const res = {}
30
+ let nextCalled = false
31
+ const next = () => { nextCalled = true }
32
+
33
+ middleware(req, res, next)
34
+
35
+ assert.equal(nextCalled, true)
36
+ })
37
+
38
+ it('should store the exact config reference', () => {
39
+ const config = { route: '/specific', internal: true }
40
+ const middleware = cacheRouteConfig(config)
41
+ const req = {}
42
+
43
+ middleware(req, {}, () => {})
44
+
45
+ assert.equal(req.routeConfig, config)
46
+ assert.equal(req.routeConfig.internal, true)
47
+ })
48
+ })
@@ -0,0 +1,138 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { generateRouterMap } from '../lib/utils/generateRouterMap.js'
4
+
5
+ describe('generateRouterMap()', () => {
6
+ it('should generate a router map', () => {
7
+ const mockRouter = {
8
+ root: 'api',
9
+ path: '/api',
10
+ url: 'http://localhost:5000/api',
11
+ routes: [
12
+ { route: '/test', handlers: { get: () => {} } }
13
+ ],
14
+ childRouters: [],
15
+ flattenRouters: function () {
16
+ return [this]
17
+ }
18
+ }
19
+
20
+ const map = generateRouterMap(mockRouter)
21
+
22
+ assert.equal(typeof map, 'object')
23
+ })
24
+
25
+ it('should return empty object for router with no routes', () => {
26
+ const mockRouter = {
27
+ root: 'api',
28
+ path: '/api',
29
+ url: 'http://localhost:5000/api',
30
+ routes: [],
31
+ childRouters: [],
32
+ flattenRouters: function () {
33
+ return [this]
34
+ }
35
+ }
36
+
37
+ const map = generateRouterMap(mockRouter)
38
+
39
+ assert.deepEqual(map, {})
40
+ })
41
+
42
+ it('should include endpoint URLs and accepted methods', () => {
43
+ const mockRouter = {
44
+ root: 'api',
45
+ route: '/api',
46
+ path: '/api',
47
+ url: 'http://localhost:5000/api',
48
+ routes: [
49
+ { route: '/users', handlers: { get: () => {}, post: () => {} }, meta: { get: { description: 'list users' } } }
50
+ ],
51
+ childRouters: [],
52
+ parentRouter: null,
53
+ flattenRouters: function () {
54
+ return [this]
55
+ }
56
+ }
57
+
58
+ const map = generateRouterMap(mockRouter)
59
+ const keys = Object.keys(map)
60
+
61
+ assert.ok(keys.length > 0)
62
+ const endpoints = map[keys[0]]
63
+ assert.ok(Array.isArray(endpoints))
64
+ assert.equal(endpoints[0].url, 'http://localhost:5000/api/users')
65
+ assert.ok('get' in endpoints[0].accepted_methods)
66
+ assert.ok('post' in endpoints[0].accepted_methods)
67
+ assert.deepEqual(endpoints[0].accepted_methods.get, { description: 'list users' })
68
+ assert.deepEqual(endpoints[0].accepted_methods.post, {})
69
+ })
70
+
71
+ it('should use relative route keys for child routers', () => {
72
+ const childRouter = {
73
+ root: 'users',
74
+ path: '/api/users',
75
+ url: 'http://localhost:5000/api/users',
76
+ routes: [
77
+ { route: '/:id', handlers: { get: () => {} } }
78
+ ],
79
+ childRouters: [],
80
+ parentRouter: null
81
+ }
82
+ const mockRouter = {
83
+ root: 'api',
84
+ route: '/api',
85
+ path: '/api',
86
+ url: 'http://localhost:5000/api',
87
+ routes: [],
88
+ childRouters: [childRouter],
89
+ flattenRouters: function () {
90
+ return [this, childRouter]
91
+ }
92
+ }
93
+ childRouter.parentRouter = mockRouter
94
+
95
+ const map = generateRouterMap(mockRouter)
96
+ const keys = Object.keys(map)
97
+
98
+ assert.ok(keys.some(k => k.includes('users')))
99
+ })
100
+
101
+ it('should sort routers alphabetically by root', () => {
102
+ const routerB = {
103
+ root: 'b-router',
104
+ path: '/api/b-router',
105
+ url: 'http://localhost:5000/api/b-router',
106
+ routes: [{ route: '/data', handlers: { get: () => {} } }],
107
+ childRouters: [],
108
+ parentRouter: null
109
+ }
110
+ const routerA = {
111
+ root: 'a-router',
112
+ path: '/api/a-router',
113
+ url: 'http://localhost:5000/api/a-router',
114
+ routes: [{ route: '/data', handlers: { get: () => {} } }],
115
+ childRouters: [],
116
+ parentRouter: null
117
+ }
118
+ const mockRouter = {
119
+ root: 'api',
120
+ route: '/api',
121
+ path: '/api',
122
+ url: 'http://localhost:5000/api',
123
+ routes: [],
124
+ childRouters: [routerB, routerA],
125
+ flattenRouters: function () {
126
+ return [routerB, routerA]
127
+ }
128
+ }
129
+ routerA.parentRouter = mockRouter
130
+ routerB.parentRouter = mockRouter
131
+
132
+ const map = generateRouterMap(mockRouter)
133
+ const keys = Object.keys(map)
134
+
135
+ assert.ok(keys[0].includes('a-router'))
136
+ assert.ok(keys[1].includes('b-router'))
137
+ })
138
+ })
@@ -0,0 +1,113 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { getAllRoutes } from '../lib/utils/getAllRoutes.js'
4
+
5
+ describe('getAllRoutes()', () => {
6
+ it('should collect routes from a single router', () => {
7
+ const mockRouter = {
8
+ path: '/api',
9
+ routes: [
10
+ { route: '/users', handlers: { get: () => {}, post: () => {} } },
11
+ { route: '/posts', handlers: { get: () => {} } }
12
+ ],
13
+ flattenRouters: () => [mockRouter]
14
+ }
15
+
16
+ const routeMap = getAllRoutes(mockRouter)
17
+
18
+ assert.ok(routeMap instanceof Map)
19
+ assert.equal(routeMap.size, 2)
20
+ assert.ok(routeMap.has('/api/users'))
21
+ assert.ok(routeMap.has('/api/posts'))
22
+ assert.ok(routeMap.get('/api/users').has('GET'))
23
+ assert.ok(routeMap.get('/api/users').has('POST'))
24
+ assert.ok(routeMap.get('/api/posts').has('GET'))
25
+ })
26
+
27
+ it('should collect routes from multiple routers in hierarchy', () => {
28
+ const childRouter = {
29
+ path: '/api/v1',
30
+ routes: [
31
+ { route: '/items', handlers: { get: () => {} } }
32
+ ]
33
+ }
34
+ const parentRouter = {
35
+ path: '/api',
36
+ routes: [
37
+ { route: '/health', handlers: { get: () => {} } }
38
+ ],
39
+ flattenRouters: () => [parentRouter, childRouter]
40
+ }
41
+
42
+ const routeMap = getAllRoutes(parentRouter)
43
+
44
+ assert.equal(routeMap.size, 2)
45
+ assert.ok(routeMap.has('/api/health'))
46
+ assert.ok(routeMap.has('/api/v1/items'))
47
+ })
48
+
49
+ it('should handle root path "/" by omitting it from the prefix', () => {
50
+ const mockRouter = {
51
+ path: '/',
52
+ routes: [
53
+ { route: '/status', handlers: { get: () => {} } }
54
+ ],
55
+ flattenRouters: () => [mockRouter]
56
+ }
57
+
58
+ const routeMap = getAllRoutes(mockRouter)
59
+
60
+ assert.ok(routeMap.has('/status'))
61
+ })
62
+
63
+ it('should return empty map for router with no routes', () => {
64
+ const mockRouter = {
65
+ path: '/api',
66
+ routes: [],
67
+ flattenRouters: () => [mockRouter]
68
+ }
69
+
70
+ const routeMap = getAllRoutes(mockRouter)
71
+
72
+ assert.equal(routeMap.size, 0)
73
+ })
74
+
75
+ it('should uppercase method names', () => {
76
+ const mockRouter = {
77
+ path: '/api',
78
+ routes: [
79
+ { route: '/data', handlers: { get: () => {}, post: () => {}, delete: () => {} } }
80
+ ],
81
+ flattenRouters: () => [mockRouter]
82
+ }
83
+
84
+ const routeMap = getAllRoutes(mockRouter)
85
+ const methods = routeMap.get('/api/data')
86
+
87
+ assert.ok(methods.has('GET'))
88
+ assert.ok(methods.has('POST'))
89
+ assert.ok(methods.has('DELETE'))
90
+ })
91
+
92
+ it('should merge methods for duplicate paths across routers', () => {
93
+ const router1 = {
94
+ path: '/api',
95
+ routes: [{ route: '/users', handlers: { get: () => {} } }]
96
+ }
97
+ const router2 = {
98
+ path: '/api',
99
+ routes: [{ route: '/users', handlers: { post: () => {} } }]
100
+ }
101
+ const parentRouter = {
102
+ path: '/api',
103
+ routes: [],
104
+ flattenRouters: () => [router1, router2]
105
+ }
106
+
107
+ const routeMap = getAllRoutes(parentRouter)
108
+
109
+ assert.equal(routeMap.size, 1)
110
+ assert.ok(routeMap.get('/api/users').has('GET'))
111
+ assert.ok(routeMap.get('/api/users').has('POST'))
112
+ })
113
+ })
@@ -0,0 +1,42 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mapHandler } from '../lib/utils/mapHandler.js'
4
+
5
+ describe('mapHandler()', () => {
6
+ it('should return a function', () => {
7
+ const mockRouter = { map: { test: 'data' } }
8
+ const handler = mapHandler(mockRouter)
9
+
10
+ assert.equal(typeof handler, 'function')
11
+ })
12
+
13
+ it('should respond with router map as JSON', () => {
14
+ const mockRouter = { map: { test: 'data' } }
15
+ const handler = mapHandler(mockRouter)
16
+ const req = {}
17
+ let responseData = null
18
+ const res = {
19
+ json: (data) => { responseData = data }
20
+ }
21
+
22
+ handler(req, res)
23
+
24
+ assert.deepEqual(responseData, { test: 'data' })
25
+ })
26
+
27
+ it('should return the current map value on each call', () => {
28
+ const mockRouter = { map: { initial: true } }
29
+ const handler = mapHandler(mockRouter)
30
+ const res = { json: () => {} }
31
+
32
+ let captured
33
+ res.json = (data) => { captured = data }
34
+
35
+ handler({}, res)
36
+ assert.deepEqual(captured, { initial: true })
37
+
38
+ mockRouter.map = { updated: true }
39
+ handler({}, res)
40
+ assert.deepEqual(captured, { updated: true })
41
+ })
42
+ })