adapt-authoring-api 3.3.0 → 3.5.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 +5 -2
- package/package.json +2 -2
- package/tests/AbstractApiModule.spec.js +65 -0
- package/lib/DataCache.js +0 -45
- package/tests/DataCache.spec.js +0 -47
package/lib/AbstractApiModule.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
|
-
import { AbstractModule, Hook, stringifyValues } from 'adapt-authoring-core'
|
|
2
|
+
import { AbstractModule, DataCache, Hook, stringifyValues } from 'adapt-authoring-core'
|
|
3
3
|
import { argsFromReq, generateApiMetadata, httpMethodToDBFunction } from './utils.js'
|
|
4
|
-
import DataCache from './DataCache.js'
|
|
5
4
|
import { loadRouteConfig } from 'adapt-authoring-server'
|
|
6
5
|
/**
|
|
7
6
|
* Abstract module for creating APIs
|
|
@@ -538,6 +537,10 @@ class AbstractApiModule extends AbstractModule {
|
|
|
538
537
|
* @param {Object} mongoOpts The MongoDB options
|
|
539
538
|
*/
|
|
540
539
|
async setUpPagination (req, res, mongoOpts) {
|
|
540
|
+
if (mongoOpts.limit === 0) {
|
|
541
|
+
delete mongoOpts.limit
|
|
542
|
+
return
|
|
543
|
+
}
|
|
541
544
|
const maxPageSize = this.getConfig('maxPageSize') ?? this.app.config.get('adapt-authoring-api.maxPageSize')
|
|
542
545
|
let pageSize = mongoOpts.limit ?? this.getConfig('defaultPageSize') ?? this.app.config.get('adapt-authoring-api.defaultPageSize')
|
|
543
546
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-api",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.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",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"test": "node --test 'tests/**/*.spec.js'"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"adapt-authoring-core": "^2.
|
|
14
|
+
"adapt-authoring-core": "^2.5.0",
|
|
15
15
|
"adapt-authoring-server": "^2.1.0",
|
|
16
16
|
"lodash": "^4.17.21"
|
|
17
17
|
},
|
|
@@ -230,4 +230,69 @@ describe('AbstractApiModule', () => {
|
|
|
230
230
|
assert.equal(queryRoute.modifying, false)
|
|
231
231
|
})
|
|
232
232
|
})
|
|
233
|
+
|
|
234
|
+
describe('#setUpPagination()', () => {
|
|
235
|
+
function createPaginationInstance (docCount = 0, config = {}) {
|
|
236
|
+
const headers = {}
|
|
237
|
+
const instance = createInstance({
|
|
238
|
+
getConfig: (key) => config[key],
|
|
239
|
+
app: {
|
|
240
|
+
config: {
|
|
241
|
+
get: (key) => {
|
|
242
|
+
const defaults = { 'adapt-authoring-api.defaultPageSize': 100, 'adapt-authoring-api.maxPageSize': 250 }
|
|
243
|
+
return defaults[key]
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
waitForModule: async () => ({
|
|
247
|
+
getCollection: () => ({
|
|
248
|
+
countDocuments: async () => docCount
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
const req = { originalUrl: '/api/content/query', query: {}, apiData: { collectionName: 'content', query: {} } }
|
|
254
|
+
const res = { set: (k, v) => { headers[k] = v } }
|
|
255
|
+
return { instance, req, res, headers }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
it('should skip pagination when limit is 0', async () => {
|
|
259
|
+
const { instance, req, res, headers } = createPaginationInstance(500)
|
|
260
|
+
const mongoOpts = { limit: 0 }
|
|
261
|
+
await instance.setUpPagination(req, res, mongoOpts)
|
|
262
|
+
assert.equal(mongoOpts.limit, undefined)
|
|
263
|
+
assert.equal(headers['X-Adapt-Page'], undefined)
|
|
264
|
+
assert.equal(headers['X-Adapt-PageSize'], undefined)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should apply default pagination when limit is not 0', async () => {
|
|
268
|
+
const { instance, req, res, headers } = createPaginationInstance(50)
|
|
269
|
+
const mongoOpts = {}
|
|
270
|
+
await instance.setUpPagination(req, res, mongoOpts)
|
|
271
|
+
assert.equal(mongoOpts.limit, 100)
|
|
272
|
+
assert.equal(headers['X-Adapt-Page'], 1)
|
|
273
|
+
assert.equal(headers['X-Adapt-PageSize'], 100)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should set Link header when results span multiple pages', async () => {
|
|
277
|
+
const { instance, req, res, headers } = createPaginationInstance(250)
|
|
278
|
+
const mongoOpts = {}
|
|
279
|
+
await instance.setUpPagination(req, res, mongoOpts)
|
|
280
|
+
assert.ok(headers.Link)
|
|
281
|
+
assert.ok(headers.Link.includes('rel="next"'))
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should not set Link header for single-page results', async () => {
|
|
285
|
+
const { instance, req, res, headers } = createPaginationInstance(50)
|
|
286
|
+
const mongoOpts = {}
|
|
287
|
+
await instance.setUpPagination(req, res, mongoOpts)
|
|
288
|
+
assert.equal(headers.Link, undefined)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('should cap pageSize at maxPageSize', async () => {
|
|
292
|
+
const { instance, req, res, headers } = createPaginationInstance(500)
|
|
293
|
+
const mongoOpts = { limit: 999 }
|
|
294
|
+
await instance.setUpPagination(req, res, mongoOpts)
|
|
295
|
+
assert.equal(headers['X-Adapt-PageSize'], 250)
|
|
296
|
+
})
|
|
297
|
+
})
|
|
233
298
|
})
|
package/lib/DataCache.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { App } from 'adapt-authoring-core'
|
|
2
|
-
/**
|
|
3
|
-
* Time-limited data cache
|
|
4
|
-
* @memberof api
|
|
5
|
-
*/
|
|
6
|
-
class DataCache {
|
|
7
|
-
/** @override */
|
|
8
|
-
constructor ({ enable, lifespan }) {
|
|
9
|
-
this.isEnabled = enable === true
|
|
10
|
-
this.lifespan = lifespan ?? App.instance.config.get('adapt-authoring-api.defaultCacheLifespan')
|
|
11
|
-
this.cache = {}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Retrieve cached data, or run fresh query if no cache exists or cache is invalid
|
|
16
|
-
* @param {Object} query
|
|
17
|
-
* @param Object} options
|
|
18
|
-
* @param {Object} mongoOptions
|
|
19
|
-
* @returns {*} The cached data
|
|
20
|
-
*/
|
|
21
|
-
async get (query, options, mongoOptions) {
|
|
22
|
-
const key = JSON.stringify(query) + JSON.stringify(options) + JSON.stringify(mongoOptions)
|
|
23
|
-
this.prune()
|
|
24
|
-
if (this.isEnabled && this.cache[key]) {
|
|
25
|
-
return this.cache[key].data
|
|
26
|
-
}
|
|
27
|
-
const mongodb = await App.instance.waitForModule('mongodb')
|
|
28
|
-
const data = await mongodb.find(options.collectionName, query, mongoOptions)
|
|
29
|
-
this.cache[key] = { data, timestamp: Date.now() }
|
|
30
|
-
return data
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Removes invalid cache data
|
|
35
|
-
*/
|
|
36
|
-
prune () {
|
|
37
|
-
Object.keys(this.cache).forEach(k => {
|
|
38
|
-
if (Date.now() > (this.cache[k].timestamp + this.lifespan)) {
|
|
39
|
-
delete this.cache[k]
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export default DataCache
|
package/tests/DataCache.spec.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, it, before } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
describe('DataCache', () => {
|
|
5
|
-
let DataCache
|
|
6
|
-
|
|
7
|
-
before(async () => {
|
|
8
|
-
// DataCache constructor references App.instance.config, so we need to
|
|
9
|
-
// dynamically import after ensuring no app instance is required for prune tests.
|
|
10
|
-
// We import the module directly and test what we can without the full app.
|
|
11
|
-
DataCache = (await import('../lib/DataCache.js')).default
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
describe('#prune()', () => {
|
|
15
|
-
it('should remove expired entries from the cache', () => {
|
|
16
|
-
const instance = Object.create(DataCache.prototype)
|
|
17
|
-
instance.lifespan = 100
|
|
18
|
-
instance.cache = {
|
|
19
|
-
expired: { data: [1], timestamp: Date.now() - 200 },
|
|
20
|
-
valid: { data: [2], timestamp: Date.now() }
|
|
21
|
-
}
|
|
22
|
-
instance.prune()
|
|
23
|
-
assert.equal(instance.cache.expired, undefined)
|
|
24
|
-
assert.ok(instance.cache.valid)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('should keep entries that have not expired', () => {
|
|
28
|
-
const instance = Object.create(DataCache.prototype)
|
|
29
|
-
instance.lifespan = 10000
|
|
30
|
-
instance.cache = {
|
|
31
|
-
a: { data: [1], timestamp: Date.now() },
|
|
32
|
-
b: { data: [2], timestamp: Date.now() }
|
|
33
|
-
}
|
|
34
|
-
instance.prune()
|
|
35
|
-
assert.ok(instance.cache.a)
|
|
36
|
-
assert.ok(instance.cache.b)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('should handle an empty cache', () => {
|
|
40
|
-
const instance = Object.create(DataCache.prototype)
|
|
41
|
-
instance.lifespan = 100
|
|
42
|
-
instance.cache = {}
|
|
43
|
-
instance.prune()
|
|
44
|
-
assert.deepEqual(instance.cache, {})
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
})
|