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.
@@ -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.0",
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.0.0",
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
@@ -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
- })