adapt-authoring-server 2.1.0 → 2.2.1

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.
@@ -1,8 +1,69 @@
1
1
  # Handling server requests
2
- This page gives you some pointers on how to handle incoming requests in a way that will reduce the work needed by you as a developer, and create consistent and easy-to-process responses.
2
+ This page gives you some pointers on how to handle incoming requests in a way that will reduce the work needed by you as a developer, and create consistent and easy-to-process responses.
3
+
4
+ ## Defining routes with `routes.json`
5
+
6
+ For modules that expose HTTP endpoints, the preferred approach is to declare routes in a `routes.json` file in the module root. This keeps route definitions, permissions, and API metadata in one place.
7
+
8
+ ```json
9
+ {
10
+ "root": "mymodule",
11
+ "routes": [
12
+ {
13
+ "route": "/action",
14
+ "handlers": { "post": "actionHandler" },
15
+ "permissions": { "post": ["write:myresource"] },
16
+ "meta": {
17
+ "post": {
18
+ "summary": "Perform an action",
19
+ "requestBody": {
20
+ "content": {
21
+ "application/json": {
22
+ "schema": {
23
+ "type": "object",
24
+ "properties": {
25
+ "name": { "type": "string" }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ },
31
+ "responses": { "204": {} }
32
+ }
33
+ }
34
+ }
35
+ ]
36
+ }
37
+ ```
38
+
39
+ Then in your module code, use `loadRouteConfig` and `registerRoutes` to load and wire up the routes:
40
+
41
+ ```javascript
42
+ import { loadRouteConfig, registerRoutes } from 'adapt-authoring-server'
43
+
44
+ async init () {
45
+ const [auth, server] = await this.app.waitForModule('auth', 'server')
46
+ const config = await loadRouteConfig(this.rootDir, this)
47
+ const router = server.api.createChildRouter(config.root)
48
+ registerRoutes(router, config.routes, auth)
49
+ }
50
+ ```
51
+
52
+ ### Route properties
53
+
54
+ | Property | Required | Description |
55
+ | -------- | -------- | ----------- |
56
+ | `route` | Yes | Express-style route path (e.g. `"/reset/:id"`) |
57
+ | `handlers` | Yes | Object mapping HTTP methods to handler method names on the module |
58
+ | `permissions` | No | Object mapping HTTP methods to scope arrays (secured) or `null` (unsecured) |
59
+ | `meta` | No | Object mapping HTTP methods to OpenAPI operation metadata |
60
+ | `internal` | No | When `true`, restricts the route to localhost |
61
+ | `override` | No | When `true` and defaults are in use, merges this route onto the matching default route instead of adding a duplicate |
62
+
63
+ Handler strings are resolved to bound methods on the module instance automatically.
3
64
 
4
65
  ## Extend the `AbstractApiModule` class
5
- If you're building a non-trivial API (particularly one that uses the database), we highly recommend that you use `AbstractApiModule` as a base, as this includes a lot of boilerplate code and helper functions to make handling HTTP requests much easier. See [this page](writing-an-api) for more info on using the `AbstractApiModule` class.
66
+ If you're building a non-trivial API (particularly one that uses the database), we highly recommend that you use `AbstractApiModule` as a base, as this includes a lot of boilerplate code and helper functions to make handling HTTP requests much easier. See [this page](writing-an-api) for more info on using the `AbstractApiModule` class.
6
67
 
7
68
  ## Use HTTP status codes
8
69
  This may go without saying, but please stick to standardised HTTP response codes; they state your intentions and make it nice and easy for other devs to work with and react to.
@@ -18,12 +79,12 @@ Using the helper function is as simple as:
18
79
  async myHandler(req, res, next) {
19
80
  try {
20
81
  // do some stuff
21
- } catch() {
82
+ } catch (e) {
22
83
  res.sendError(e);
23
84
  }
24
85
  }
25
86
  ```
26
87
 
27
88
  See the Express.js documentation for information on the extra functions available:
28
- - [Request](https://expressjs.com/en/4x/api.html#req)
29
- - [Response](https://expressjs.com/en/4x/api.html#res)
89
+ - [Request](https://expressjs.com/en/5x/api.html#req)
90
+ - [Response](https://expressjs.com/en/5x/api.html#res)
package/index.js CHANGED
@@ -2,5 +2,5 @@
2
2
  * HTTP server functionality using Express.js
3
3
  * @namespace server
4
4
  */
5
- export { addExistenceProps, cacheRouteConfig, generateRouterMap, getAllRoutes, loadRouteConfig, mapHandler } from './lib/utils.js'
5
+ export { addExistenceProps, cacheRouteConfig, generateRouterMap, getAllRoutes, loadRouteConfig, mapHandler, registerRoutes } from './lib/utils.js'
6
6
  export { default } from './lib/ServerModule.js'
@@ -38,6 +38,8 @@ function resolveHandlers (routes, target, aliases) {
38
38
  * @param {Object} [options.handlerAliases] Map of handler string aliases to pre-resolved functions
39
39
  * @param {String} [options.defaults] Path to a default routes template JSON file. When provided and
40
40
  * routes.json is found, the template's routes are resolved and prepended to config.routes.
41
+ * Custom routes with `override: true` are merged onto the matching default (by path) instead of
42
+ * being appended as duplicates.
41
43
  * @return {Promise<Object|null>} Parsed config with resolved handlers, or null if no routes.json
42
44
  * @memberof server
43
45
  */
@@ -68,7 +70,20 @@ export async function loadRouteConfig (rootDir, target, options = {}) {
68
70
  if (options.defaults) {
69
71
  const template = await readJson(options.defaults)
70
72
  const defaultRoutes = resolveHandlers(template.routes || [], target, aliases)
71
- config.routes = [...defaultRoutes, ...customRoutes]
73
+ // Apply override routes onto matching defaults
74
+ const overrides = new Map(
75
+ customRoutes.filter(r => r.override).map(r => [r.route, r])
76
+ )
77
+ const matched = new Set()
78
+ const mergedDefaults = defaultRoutes.map(d => {
79
+ const o = overrides.get(d.route)
80
+ if (!o) return d
81
+ matched.add(d.route)
82
+ const { override, ...rest } = o
83
+ return { ...d, ...rest, handlers: { ...d.handlers, ...rest.handlers } }
84
+ })
85
+ const remaining = customRoutes.filter(r => !r.override || !matched.has(r.route))
86
+ config.routes = [...mergedDefaults, ...remaining]
72
87
  } else {
73
88
  config.routes = customRoutes
74
89
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Registers routes on a router and configures their permissions with auth.
3
+ * @param {Router} router The router to add routes to
4
+ * @param {Array} routes Array of route definition objects
5
+ * @param {Object} auth The auth module instance
6
+ * @memberof server
7
+ */
8
+ export function registerRoutes (router, routes, auth) {
9
+ for (const r of routes) {
10
+ router.addRoute(r)
11
+ if (!r.permissions) continue
12
+ for (const [method, perms] of Object.entries(r.permissions)) {
13
+ if (perms) {
14
+ auth.secureRoute(`${router.path}${r.route}`, method, perms)
15
+ } else {
16
+ auth.unsecureRoute(`${router.path}${r.route}`, method)
17
+ }
18
+ }
19
+ }
20
+ }
package/lib/utils.js CHANGED
@@ -4,3 +4,4 @@ export { generateRouterMap } from './utils/generateRouterMap.js'
4
4
  export { getAllRoutes } from './utils/getAllRoutes.js'
5
5
  export { loadRouteConfig } from './utils/loadRouteConfig.js'
6
6
  export { mapHandler } from './utils/mapHandler.js'
7
+ export { registerRoutes } from './utils/registerRoutes.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-server",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
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",
@@ -33,6 +33,11 @@
33
33
  "type": "object",
34
34
  "description": "Keys are HTTP methods, values are OpenAPI operation objects",
35
35
  "propertyNames": { "enum": ["get", "post", "put", "patch", "delete"] }
36
+ },
37
+ "override": {
38
+ "type": "boolean",
39
+ "description": "When true and a defaults template is in use, merges this route's properties onto the matching default route (by path) instead of adding a duplicate",
40
+ "default": false
36
41
  }
37
42
  },
38
43
  "required": ["route", "handlers"]
@@ -12,5 +12,5 @@
12
12
  "description": "Route definitions"
13
13
  }
14
14
  },
15
- "required": ["root"]
15
+ "required": []
16
16
  }
@@ -383,5 +383,96 @@ describe('loadRouteConfig()', () => {
383
383
  assert.equal(config.routes[0].permissions.post, null)
384
384
  assert.equal(config.routes[0].meta.post.summary, 'Test')
385
385
  })
386
+
387
+ it('should merge override route properties onto matching default route', async () => {
388
+ const dir = path.join(tmpDir, 'override-merge')
389
+ await mkdir(dir, { recursive: true })
390
+ await writeJson(path.join(dir, 'routes.json'), {
391
+ root: 'test',
392
+ routes: [{
393
+ route: '/',
394
+ override: true,
395
+ handlers: { post: 'myHandler' },
396
+ meta: { post: { summary: 'Overridden' } }
397
+ }]
398
+ })
399
+ const defaultsPath = path.join(tmpDir, 'override-defaults.json')
400
+ await writeJson(defaultsPath, {
401
+ routes: [{ route: '/', handlers: { post: 'myHandler' }, permissions: { post: null } }]
402
+ })
403
+ const config = await loadRouteConfig(dir, { myHandler: () => {} }, { defaults: defaultsPath })
404
+ assert.equal(config.routes.length, 1)
405
+ assert.equal(config.routes[0].route, '/')
406
+ assert.equal(config.routes[0].meta.post.summary, 'Overridden')
407
+ assert.equal(config.routes[0].permissions.post, null)
408
+ })
409
+
410
+ it('should keep default handler when override does not replace it', async () => {
411
+ const dir = path.join(tmpDir, 'override-keep-handler')
412
+ await mkdir(dir, { recursive: true })
413
+ await writeJson(path.join(dir, 'routes.json'), {
414
+ root: 'test',
415
+ routes: [{
416
+ route: '/',
417
+ override: true,
418
+ handlers: { post: 'myHandler' },
419
+ meta: { post: { summary: 'Added meta' } }
420
+ }]
421
+ })
422
+ const defaultsPath = path.join(tmpDir, 'override-keep-handler-defaults.json')
423
+ await writeJson(defaultsPath, {
424
+ routes: [{ route: '/', handlers: { post: 'defaultHandler' } }]
425
+ })
426
+ const target = {
427
+ myHandler: () => {},
428
+ defaultHandler () {}
429
+ }
430
+ const config = await loadRouteConfig(dir, target, { defaults: defaultsPath })
431
+ // override handler takes precedence
432
+ assert.equal(typeof config.routes[0].handlers.post, 'function')
433
+ assert.equal(config.routes[0].meta.post.summary, 'Added meta')
434
+ })
435
+
436
+ it('should not remove override route from results when no matching default exists', async () => {
437
+ const dir = path.join(tmpDir, 'override-no-match')
438
+ await mkdir(dir, { recursive: true })
439
+ await writeJson(path.join(dir, 'routes.json'), {
440
+ root: 'test',
441
+ routes: [{
442
+ route: '/nomatch',
443
+ override: true,
444
+ handlers: { get: 'myHandler' },
445
+ meta: { get: { summary: 'Orphan' } }
446
+ }]
447
+ })
448
+ const defaultsPath = path.join(tmpDir, 'override-no-match-defaults.json')
449
+ await writeJson(defaultsPath, {
450
+ routes: [{ route: '/', handlers: { post: 'myHandler' } }]
451
+ })
452
+ const config = await loadRouteConfig(dir, { myHandler: () => {} }, { defaults: defaultsPath })
453
+ assert.equal(config.routes.length, 2)
454
+ assert.equal(config.routes[0].route, '/')
455
+ assert.equal(config.routes[1].route, '/nomatch')
456
+ })
457
+
458
+ it('should strip the override flag from merged route', async () => {
459
+ const dir = path.join(tmpDir, 'override-strip-flag')
460
+ await mkdir(dir, { recursive: true })
461
+ await writeJson(path.join(dir, 'routes.json'), {
462
+ root: 'test',
463
+ routes: [{
464
+ route: '/',
465
+ override: true,
466
+ handlers: { post: 'myHandler' },
467
+ meta: { post: { summary: 'Test' } }
468
+ }]
469
+ })
470
+ const defaultsPath = path.join(tmpDir, 'override-strip-defaults.json')
471
+ await writeJson(defaultsPath, {
472
+ routes: [{ route: '/', handlers: { post: 'myHandler' } }]
473
+ })
474
+ const config = await loadRouteConfig(dir, { myHandler: () => {} }, { defaults: defaultsPath })
475
+ assert.equal(config.routes[0].override, undefined)
476
+ })
386
477
  })
387
478
  })