adapt-authoring-api 3.2.3 → 3.4.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.
@@ -538,6 +538,10 @@ class AbstractApiModule extends AbstractModule {
538
538
  * @param {Object} mongoOpts The MongoDB options
539
539
  */
540
540
  async setUpPagination (req, res, mongoOpts) {
541
+ if (mongoOpts.limit === 0) {
542
+ delete mongoOpts.limit
543
+ return
544
+ }
541
545
  const maxPageSize = this.getConfig('maxPageSize') ?? this.app.config.get('adapt-authoring-api.maxPageSize')
542
546
  let pageSize = mongoOpts.limit ?? this.getConfig('defaultPageSize') ?? this.app.config.get('adapt-authoring-api.defaultPageSize')
543
547
 
@@ -658,7 +662,7 @@ class AbstractApiModule extends AbstractModule {
658
662
  if (results.length > 1) {
659
663
  throw this.app.errors.TOO_MANY_RESULTS.setData({ actual: results.length, expected: 1, query })
660
664
  }
661
- if (options.strict !== false && !results.length) {
665
+ if ((options.throwOnMissing ?? options.strict) !== false && !results.length) {
662
666
  throw this.app.errors.NOT_FOUND.setData({ id: query, type: options.schemaName })
663
667
  }
664
668
  return results[0] ?? null
package/lib/typedefs.js CHANGED
@@ -49,6 +49,8 @@
49
49
  * @typedef {Object} FindOptions
50
50
  * @property {String} schemaName Name of the schema to validate against
51
51
  * @property {String} collectionName DB collection to insert document into
52
+ * @property {Boolean} throwOnMissing Whether to throw a NOT_FOUND error when no results are found (default: true)
53
+ * @property {Boolean} [strict] @deprecated Use throwOnMissing instead
52
54
  */
53
55
  /**
54
56
  * @memberof api
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-api",
3
- "version": "3.2.3",
3
+ "version": "3.4.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",
@@ -142,6 +142,56 @@ describe('AbstractApiModule', () => {
142
142
  })
143
143
  })
144
144
 
145
+ describe('#findOne()', () => {
146
+ function createFindOneInstance (findResults) {
147
+ const notFoundError = { setData: () => { const e = new Error('NOT_FOUND'); e.code = 'NOT_FOUND'; return e } }
148
+ const tooManyResultsError = { setData: () => { const e = new Error('TOO_MANY_RESULTS'); e.code = 'TOO_MANY_RESULTS'; return e } }
149
+ const instance = Object.create(AbstractApiModule.prototype)
150
+ instance.find = async () => findResults
151
+ instance.app = { errors: { NOT_FOUND: notFoundError, TOO_MANY_RESULTS: tooManyResultsError } }
152
+ return instance
153
+ }
154
+
155
+ it('should throw NOT_FOUND when no results and throwOnMissing is not set', async () => {
156
+ const instance = createFindOneInstance([])
157
+ await assert.rejects(() => instance.findOne({}), /NOT_FOUND/)
158
+ })
159
+
160
+ it('should throw NOT_FOUND when no results and throwOnMissing is true', async () => {
161
+ const instance = createFindOneInstance([])
162
+ await assert.rejects(() => instance.findOne({}, { throwOnMissing: true }), /NOT_FOUND/)
163
+ })
164
+
165
+ it('should return null when no results and throwOnMissing is false', async () => {
166
+ const instance = createFindOneInstance([])
167
+ const result = await instance.findOne({}, { throwOnMissing: false })
168
+ assert.equal(result, null)
169
+ })
170
+
171
+ it('should return null when no results and strict is false (backward compat)', async () => {
172
+ const instance = createFindOneInstance([])
173
+ const result = await instance.findOne({}, { strict: false })
174
+ assert.equal(result, null)
175
+ })
176
+
177
+ it('should prefer throwOnMissing over strict when both are set', async () => {
178
+ const instance = createFindOneInstance([])
179
+ await assert.rejects(() => instance.findOne({}, { throwOnMissing: true, strict: false }), /NOT_FOUND/)
180
+ })
181
+
182
+ it('should return the single result when found', async () => {
183
+ const doc = { _id: '1', name: 'test' }
184
+ const instance = createFindOneInstance([doc])
185
+ const result = await instance.findOne({})
186
+ assert.deepEqual(result, doc)
187
+ })
188
+
189
+ it('should throw TOO_MANY_RESULTS when more than one result is returned', async () => {
190
+ const instance = createFindOneInstance([{ _id: '1' }, { _id: '2' }])
191
+ await assert.rejects(() => instance.findOne({}), /TOO_MANY_RESULTS/)
192
+ })
193
+ })
194
+
145
195
  describe('default-routes.json', () => {
146
196
  it('should define an array of route objects', () => {
147
197
  assert.ok(Array.isArray(defaultRoutes.routes))
@@ -180,4 +230,69 @@ describe('AbstractApiModule', () => {
180
230
  assert.equal(queryRoute.modifying, false)
181
231
  })
182
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
+ })
183
298
  })