adapt-authoring-api 2.1.4 → 2.2.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/lib/AbstractApiModule.js
CHANGED
|
@@ -2,6 +2,7 @@ import _ from 'lodash'
|
|
|
2
2
|
import { AbstractModule, Hook, stringifyValues } from 'adapt-authoring-core'
|
|
3
3
|
import { argsFromReq, generateApiMetadata, httpMethodToDBFunction } from './utils.js'
|
|
4
4
|
import DataCache from './DataCache.js'
|
|
5
|
+
import { loadRouteConfig } from 'adapt-authoring-server'
|
|
5
6
|
/**
|
|
6
7
|
* Abstract module for creating APIs
|
|
7
8
|
* @memberof api
|
|
@@ -149,6 +150,17 @@ class AbstractApiModule extends AbstractModule {
|
|
|
149
150
|
* @type {String}
|
|
150
151
|
*/
|
|
151
152
|
this.schemaName = undefined
|
|
153
|
+
|
|
154
|
+
const config = await loadRouteConfig(this.rootDir, this, {
|
|
155
|
+
schema: 'apiroutes',
|
|
156
|
+
handlerAliases: {
|
|
157
|
+
default: this.requestHandler(),
|
|
158
|
+
query: this.queryHandler(),
|
|
159
|
+
serveSchema: this.serveSchema.bind(this)
|
|
160
|
+
},
|
|
161
|
+
defaults: new URL('./default-routes.json', import.meta.url).pathname
|
|
162
|
+
})
|
|
163
|
+
if (config) this.applyRouteConfig(config)
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
/**
|
|
@@ -189,6 +201,7 @@ class AbstractApiModule extends AbstractModule {
|
|
|
189
201
|
|
|
190
202
|
/**
|
|
191
203
|
* Checks required values have been set
|
|
204
|
+
* @deprecated Validation is handled by the routes schema for modules using routes.json
|
|
192
205
|
*/
|
|
193
206
|
validateValues () {
|
|
194
207
|
if (!this.root && !this.router) {
|
|
@@ -226,6 +239,30 @@ class AbstractApiModule extends AbstractModule {
|
|
|
226
239
|
Object.values(uniqueRoutes).forEach(r => this.addRoute(r, auth))
|
|
227
240
|
}
|
|
228
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Applies route configuration loaded from routes.json.
|
|
244
|
+
* Expands `${scope}` permission placeholders with `this.permissionsScope || this.root`.
|
|
245
|
+
* @param {Object} config The route config object returned by loadRouteConfig
|
|
246
|
+
*/
|
|
247
|
+
applyRouteConfig (config) {
|
|
248
|
+
/** @ignore */ this.root = config.root
|
|
249
|
+
if (config.schemaName !== undefined) this.schemaName = config.schemaName
|
|
250
|
+
if (config.collectionName !== undefined) this.collectionName = config.collectionName
|
|
251
|
+
const scope = this.permissionsScope || this.root
|
|
252
|
+
this.routes = config.routes.map(r => {
|
|
253
|
+
if (!r.permissions) return r
|
|
254
|
+
return {
|
|
255
|
+
...r,
|
|
256
|
+
permissions: Object.fromEntries(
|
|
257
|
+
Object.entries(r.permissions).map(([method, perms]) => [
|
|
258
|
+
method,
|
|
259
|
+
Array.isArray(perms) ? perms.map(p => p.replace('${scope}', scope)) : perms // eslint-disable-line no-template-curly-in-string
|
|
260
|
+
])
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
229
266
|
/**
|
|
230
267
|
* Adds a single route definition
|
|
231
268
|
* @param {Route} config The route config
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"routes": [
|
|
3
|
+
{
|
|
4
|
+
"route": "/",
|
|
5
|
+
"handlers": { "post": "default", "get": "default" },
|
|
6
|
+
"permissions": { "post": ["write:${scope}"], "get": ["read:${scope}"] }
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"route": "/schema",
|
|
10
|
+
"handlers": { "get": "serveSchema" },
|
|
11
|
+
"permissions": { "get": ["read:schema"] }
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"route": "/:_id",
|
|
15
|
+
"handlers": { "put": "default", "get": "default", "patch": "default", "delete": "default" },
|
|
16
|
+
"permissions": { "put": ["write:${scope}"], "get": ["read:${scope}"], "patch": ["write:${scope}"], "delete": ["write:${scope}"] }
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"route": "/query",
|
|
20
|
+
"validate": false,
|
|
21
|
+
"modifying": false,
|
|
22
|
+
"handlers": { "post": "query" },
|
|
23
|
+
"permissions": { "post": ["read:${scope}"] }
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Generates REST API metadata and stores on route config
|
|
3
3
|
* @param {AbstractApiModule} instance The current AbstractApiModule instance
|
|
4
4
|
* @memberof api
|
|
5
|
+
* @deprecated For modules with routes.json, define metadata in the meta field of each route entry instead
|
|
5
6
|
*/
|
|
6
7
|
export function generateApiMetadata (instance) {
|
|
7
8
|
const getData = isList => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-api",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Abstract module for creating APIs",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-api",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
"adapt-authoring-auth": "^2.0.0",
|
|
19
19
|
"adapt-authoring-jsonschema": "^1.2.0",
|
|
20
20
|
"adapt-authoring-mongodb": "^3.0.0",
|
|
21
|
-
"adapt-authoring-server": "^2.
|
|
21
|
+
"adapt-authoring-server": "^2.1.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@semantic-release/git": "^10.0.1",
|
|
25
|
+
"adapt-authoring-server": "^2.1.0",
|
|
25
26
|
"conventional-changelog-eslint": "^6.0.0",
|
|
26
27
|
"semantic-release": "^25.0.2",
|
|
27
28
|
"standard": "^17.1.0"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$anchor": "apiroutes",
|
|
4
|
+
"$merge": {
|
|
5
|
+
"source": { "$ref": "routes" },
|
|
6
|
+
"with": {
|
|
7
|
+
"properties": {
|
|
8
|
+
"schemaName": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Schema name for the module's data model"
|
|
11
|
+
},
|
|
12
|
+
"collectionName": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "MongoDB collection name"
|
|
15
|
+
},
|
|
16
|
+
"useDefaultRoutes": {
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"description": "Whether to generate default CRUD routes",
|
|
19
|
+
"default": true
|
|
20
|
+
},
|
|
21
|
+
"routes": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": { "$ref": "routeitem" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -2,6 +2,20 @@ import { describe, it } from 'node:test'
|
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import AbstractApiModule from '../lib/AbstractApiModule.js'
|
|
4
4
|
|
|
5
|
+
function createInstance (overrides = {}) {
|
|
6
|
+
const instance = Object.create(AbstractApiModule.prototype)
|
|
7
|
+
instance.root = 'test'
|
|
8
|
+
instance.permissionsScope = undefined
|
|
9
|
+
instance.schemaName = undefined
|
|
10
|
+
instance.collectionName = undefined
|
|
11
|
+
instance.routes = []
|
|
12
|
+
instance.requestHandler = () => function defaultRequestHandler () {}
|
|
13
|
+
instance.queryHandler = () => function queryHandler () {}
|
|
14
|
+
instance.serveSchema = function serveSchema () {}
|
|
15
|
+
Object.assign(instance, overrides)
|
|
16
|
+
return instance
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
describe('AbstractApiModule', () => {
|
|
6
20
|
describe('#mapStatusCode()', () => {
|
|
7
21
|
const instance = Object.create(AbstractApiModule.prototype)
|
|
@@ -56,4 +70,112 @@ describe('AbstractApiModule', () => {
|
|
|
56
70
|
assert.equal(options, undefined)
|
|
57
71
|
})
|
|
58
72
|
})
|
|
73
|
+
|
|
74
|
+
describe('#applyRouteConfig()', () => {
|
|
75
|
+
it('should set root, schemaName, and collectionName from config', async () => {
|
|
76
|
+
const instance = createInstance()
|
|
77
|
+
await instance.applyRouteConfig({
|
|
78
|
+
root: 'content',
|
|
79
|
+
schemaName: 'content',
|
|
80
|
+
collectionName: 'content',
|
|
81
|
+
useDefaultRoutes: false,
|
|
82
|
+
routes: []
|
|
83
|
+
})
|
|
84
|
+
assert.equal(instance.root, 'content')
|
|
85
|
+
assert.equal(instance.schemaName, 'content')
|
|
86
|
+
assert.equal(instance.collectionName, 'content')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should not override schemaName or collectionName when not in config', async () => {
|
|
90
|
+
const instance = createInstance({ schemaName: 'existing', collectionName: 'existing' })
|
|
91
|
+
await instance.applyRouteConfig({ root: 'content', useDefaultRoutes: false, routes: [] })
|
|
92
|
+
assert.equal(instance.schemaName, 'existing')
|
|
93
|
+
assert.equal(instance.collectionName, 'existing')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should set routes to custom routes only when useDefaultRoutes is false', async () => {
|
|
97
|
+
const instance = createInstance()
|
|
98
|
+
const customRoute = { route: '/custom', handlers: { get: () => {} } }
|
|
99
|
+
await instance.applyRouteConfig({ root: 'test', useDefaultRoutes: false, routes: [customRoute] })
|
|
100
|
+
assert.equal(instance.routes.length, 1)
|
|
101
|
+
assert.equal(instance.routes[0].route, '/custom')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should set routes to empty array when config has no routes', async () => {
|
|
105
|
+
const instance = createInstance()
|
|
106
|
+
await instance.applyRouteConfig({ root: 'test', routes: [] })
|
|
107
|
+
assert.deepEqual(instance.routes, [])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should expand ${scope} placeholders in permissions using root', async () => { // eslint-disable-line no-template-curly-in-string
|
|
111
|
+
const instance = createInstance({ root: 'content' })
|
|
112
|
+
await instance.applyRouteConfig({
|
|
113
|
+
root: 'content',
|
|
114
|
+
routes: [{ route: '/', permissions: { get: ['read:${scope}'], post: ['write:${scope}'] } }] // eslint-disable-line no-template-curly-in-string
|
|
115
|
+
})
|
|
116
|
+
assert.deepEqual(instance.routes[0].permissions.get, ['read:content'])
|
|
117
|
+
assert.deepEqual(instance.routes[0].permissions.post, ['write:content'])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should prefer permissionsScope over root for ${scope} expansion', async () => { // eslint-disable-line no-template-curly-in-string
|
|
121
|
+
const instance = createInstance({ root: 'content', permissionsScope: 'custom' })
|
|
122
|
+
await instance.applyRouteConfig({
|
|
123
|
+
root: 'content',
|
|
124
|
+
routes: [{ route: '/', permissions: { get: ['read:${scope}'] } }] // eslint-disable-line no-template-curly-in-string
|
|
125
|
+
})
|
|
126
|
+
assert.deepEqual(instance.routes[0].permissions.get, ['read:custom'])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should pass through null permissions unchanged', async () => {
|
|
130
|
+
const instance = createInstance({ root: 'content' })
|
|
131
|
+
await instance.applyRouteConfig({
|
|
132
|
+
root: 'content',
|
|
133
|
+
routes: [{ route: '/', permissions: { get: null } }]
|
|
134
|
+
})
|
|
135
|
+
assert.equal(instance.routes[0].permissions.get, null)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('#DEFAULT_ROUTES', () => {
|
|
140
|
+
it('should return an array of route objects', async () => {
|
|
141
|
+
const instance = createInstance()
|
|
142
|
+
const routes = await instance.DEFAULT_ROUTES
|
|
143
|
+
assert.ok(Array.isArray(routes))
|
|
144
|
+
assert.ok(routes.length > 0)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should include routes for /, /schema, /:_id, and /query', async () => {
|
|
148
|
+
const instance = createInstance()
|
|
149
|
+
const routes = await instance.DEFAULT_ROUTES
|
|
150
|
+
const routePaths = routes.map(r => r.route)
|
|
151
|
+
assert.ok(routePaths.includes('/'))
|
|
152
|
+
assert.ok(routePaths.includes('/schema'))
|
|
153
|
+
assert.ok(routePaths.includes('/:_id'))
|
|
154
|
+
assert.ok(routePaths.includes('/query'))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should use permissionsScope when set', async () => {
|
|
158
|
+
const instance = createInstance({ root: 'content', permissionsScope: 'custom' })
|
|
159
|
+
const routes = await instance.DEFAULT_ROUTES
|
|
160
|
+
const rootRoute = routes.find(r => r.route === '/')
|
|
161
|
+
assert.ok(rootRoute.permissions.post.includes('write:custom'))
|
|
162
|
+
assert.ok(rootRoute.permissions.get.includes('read:custom'))
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('should fall back to root for permissions when permissionsScope is not set', async () => {
|
|
166
|
+
const instance = createInstance({ root: 'content', permissionsScope: undefined })
|
|
167
|
+
const routes = await instance.DEFAULT_ROUTES
|
|
168
|
+
const rootRoute = routes.find(r => r.route === '/')
|
|
169
|
+
assert.ok(rootRoute.permissions.post.includes('write:content'))
|
|
170
|
+
assert.ok(rootRoute.permissions.get.includes('read:content'))
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should set validate: false and modifying: false on /query route', async () => {
|
|
174
|
+
const instance = createInstance()
|
|
175
|
+
const routes = await instance.DEFAULT_ROUTES
|
|
176
|
+
const queryRoute = routes.find(r => r.route === '/query')
|
|
177
|
+
assert.equal(queryRoute.validate, false)
|
|
178
|
+
assert.equal(queryRoute.modifying, false)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
59
181
|
})
|