adapt-authoring-api 3.2.1 → 3.2.2

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.
@@ -142,8 +142,6 @@ class AbstractApiModule extends AbstractModule {
142
142
  const config = await loadRouteConfig(this.rootDir, this, {
143
143
  schema: 'apiroutes',
144
144
  handlerAliases: {
145
- default: this.requestHandler(),
146
- query: this.queryHandler(),
147
145
  serveSchema: this.serveSchema.bind(this)
148
146
  },
149
147
  defaults: new URL('../default-routes.json', import.meta.url).pathname
@@ -380,135 +378,129 @@ class AbstractApiModule extends AbstractModule {
380
378
  * Middleware to handle a generic API request. Supports POST, GET, PUT and DELETE of items in the database.
381
379
  * @return {Function} Express middleware function
382
380
  */
383
- requestHandler () {
384
- const requestHandler = async (req, res, next) => {
385
- const method = req.method.toLowerCase()
386
- const func = this[httpMethodToDBFunction(method)]
387
- if (!func) {
388
- return next(this.app.errors.HTTP_METHOD_NOT_SUPPORTED.setData({ method }))
389
- }
390
- let data
391
- try {
392
- await this.requestHook.invoke(req)
393
- const preCheck = method !== 'get' && method !== 'post'
394
- const postCheck = method === 'get'
395
- if (preCheck) {
396
- await this.checkAccess(req, req.apiData.query)
397
- }
398
- data = await func.apply(this, argsFromReq(req))
399
- if (postCheck) {
400
- data = await this.checkAccess(req, data)
401
- }
402
- data = await this.sanitise(req.apiData.schemaName, data, { isInternal: true, strict: false })
403
- } catch (e) {
404
- return next(e)
381
+ async requestHandler (req, res, next) {
382
+ const method = req.method.toLowerCase()
383
+ const func = this[httpMethodToDBFunction(method)]
384
+ if (!func) {
385
+ return next(this.app.errors.HTTP_METHOD_NOT_SUPPORTED.setData({ method }))
386
+ }
387
+ let data
388
+ try {
389
+ await this.requestHook.invoke(req)
390
+ const preCheck = method !== 'get' && method !== 'post'
391
+ const postCheck = method === 'get'
392
+ if (preCheck) {
393
+ await this.checkAccess(req, req.apiData.query)
405
394
  }
406
- if (Array.isArray(data) && req.params._id) { // special case for when _id param is present
407
- if (!data.length) {
408
- return next(this.app.errors.NOT_FOUND.setData({ id: req.params._id, type: req.apiData.schemaName }))
409
- }
410
- data = data[0]
395
+ data = await func.apply(this, argsFromReq(req))
396
+ if (postCheck) {
397
+ data = await this.checkAccess(req, data)
411
398
  }
412
- if (method !== 'get') {
413
- const resource = Array.isArray(data) ? req.apiData.query : data._id.toString()
414
- this.log('debug', `API_${func.name.toUpperCase()}`, resource, 'by', req.auth.user._id.toString())
399
+ data = await this.sanitise(req.apiData.schemaName, data, { isInternal: true, strict: false })
400
+ } catch (e) {
401
+ return next(e)
402
+ }
403
+ if (Array.isArray(data) && req.params._id) { // special case for when _id param is present
404
+ if (!data.length) {
405
+ return next(this.app.errors.NOT_FOUND.setData({ id: req.params._id, type: req.apiData.schemaName }))
415
406
  }
416
- res.status(this.mapStatusCode(method)).json(data)
407
+ data = data[0]
408
+ }
409
+ if (method !== 'get') {
410
+ const resource = Array.isArray(data) ? req.apiData.query : data._id.toString()
411
+ this.log('debug', `API_${func.name.toUpperCase()}`, resource, 'by', req.auth.user._id.toString())
417
412
  }
418
- return requestHandler
413
+ res.status(this.mapStatusCode(method)).json(data)
419
414
  }
420
415
 
421
416
  /**
422
417
  * Express request handler for advanced API queries. Supports collation/limit/page/skip/sort and pagination. For incoming query data to be correctly parsed, it must be sent as body data using a POST request.
423
418
  * @return {function}
424
419
  */
425
- queryHandler () {
426
- const queryHandler = async (req, res, next) => {
427
- try {
428
- const opts = {
429
- schemaName: req.apiData.schemaName,
430
- collectionName: req.apiData.collectionName
431
- }
432
- const mongoOpts = {}
433
- // find and remove mongo options from the query
434
- Object.entries(req.apiData.query).forEach(([key, val]) => {
435
- if (['collation', 'limit', 'page', 'skip', 'sort'].includes(key)) {
436
- try {
437
- mongoOpts[key] = JSON.parse(req.apiData.query[key])
438
- } catch (e) {
439
- this.log('warn', `failed to parse query ${key} param '${mongoOpts[key]}', ${e}`)
440
- }
441
- delete req.apiData.query[key]
442
- } else {
443
- // otherwise assume we have a query field or option and store for later processing
444
- opts[key] = val
445
- }
446
- })
447
- // handle search parameter
448
- const search = req.apiData.query.search
449
- if (search) {
450
- delete req.apiData.query.search
420
+ async queryHandler (req, res, next) {
421
+ try {
422
+ const opts = {
423
+ schemaName: req.apiData.schemaName,
424
+ collectionName: req.apiData.collectionName
425
+ }
426
+ const mongoOpts = {}
427
+ // find and remove mongo options from the query
428
+ Object.entries(req.apiData.query).forEach(([key, val]) => {
429
+ if (['collation', 'limit', 'page', 'skip', 'sort'].includes(key)) {
451
430
  try {
452
- const schema = await this.getSchema(req.apiData.schemaName)
453
- if (schema && schema.built && schema.built.properties) {
454
- const searchableFields = Object.keys(schema.built.properties).filter(
455
- field => schema.built.properties[field].isSearchable === true
456
- )
457
- if (searchableFields.length) {
458
- // escape special regex characters to prevent ReDoS attacks
459
- const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
460
- const regex = { $regex: escapedSearch, $options: 'i' }
461
- const searchConditions = searchableFields.map(f => ({ [f]: regex }))
462
- // merge with existing $or if present
463
- if (req.apiData.query.$or) {
464
- req.apiData.query.$and = [
465
- { $or: req.apiData.query.$or },
466
- { $or: searchConditions }
467
- ]
468
- delete req.apiData.query.$or
469
- } else {
470
- req.apiData.query.$or = searchConditions
471
- }
431
+ mongoOpts[key] = JSON.parse(req.apiData.query[key])
432
+ } catch (e) {
433
+ this.log('warn', `failed to parse query ${key} param '${mongoOpts[key]}', ${e}`)
434
+ }
435
+ delete req.apiData.query[key]
436
+ } else {
437
+ // otherwise assume we have a query field or option and store for later processing
438
+ opts[key] = val
439
+ }
440
+ })
441
+ // handle search parameter
442
+ const search = req.apiData.query.search
443
+ if (search) {
444
+ delete req.apiData.query.search
445
+ try {
446
+ const schema = await this.getSchema(req.apiData.schemaName)
447
+ if (schema && schema.built && schema.built.properties) {
448
+ const searchableFields = Object.keys(schema.built.properties).filter(
449
+ field => schema.built.properties[field].isSearchable === true
450
+ )
451
+ if (searchableFields.length) {
452
+ // escape special regex characters to prevent ReDoS attacks
453
+ const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
454
+ const regex = { $regex: escapedSearch, $options: 'i' }
455
+ const searchConditions = searchableFields.map(f => ({ [f]: regex }))
456
+ // merge with existing $or if present
457
+ if (req.apiData.query.$or) {
458
+ req.apiData.query.$and = [
459
+ { $or: req.apiData.query.$or },
460
+ { $or: searchConditions }
461
+ ]
462
+ delete req.apiData.query.$or
463
+ } else {
464
+ req.apiData.query.$or = searchConditions
472
465
  }
473
466
  }
474
- } catch (e) {
475
- this.log('warn', `failed to process search parameter, ${e.message}`)
476
467
  }
468
+ } catch (e) {
469
+ this.log('warn', `failed to process search parameter, ${e.message}`)
477
470
  }
478
- req.apiData.query = await this.parseQuery(req.apiData.schemaName, req.body, mongoOpts)
479
- // remove any valid query keys from the options
480
- Object.keys(req.apiData.query).forEach(key => delete opts[key])
471
+ }
472
+ req.apiData.query = await this.parseQuery(req.apiData.schemaName, req.body, mongoOpts)
473
+ // remove any valid query keys from the options
474
+ Object.keys(req.apiData.query).forEach(key => delete opts[key])
481
475
 
482
- await this.requestHook.invoke(req)
476
+ await this.requestHook.invoke(req)
483
477
 
484
- await this.setUpPagination(req, res, mongoOpts)
478
+ await this.setUpPagination(req, res, mongoOpts)
485
479
 
486
- let results = await this.find(req.apiData.query, opts, mongoOpts)
480
+ let results = await this.find(req.apiData.query, opts, mongoOpts)
487
481
 
488
- results = await this.checkAccess(req, results)
482
+ results = await this.checkAccess(req, results)
489
483
 
490
- // If checkAccess filtered some results, fetch more to fill the page
491
- const pageSize = mongoOpts.limit
492
- if (pageSize && results.length < pageSize) {
493
- let fetchSkip = mongoOpts.skip + pageSize
494
- while (results.length < pageSize) {
495
- const extra = await this.find(req.apiData.query, opts, { ...mongoOpts, skip: fetchSkip })
496
- if (!extra.length) break
497
- const filtered = await this.checkAccess(req, extra)
498
- results = results.concat(filtered)
499
- fetchSkip += extra.length
500
- }
501
- if (results.length > pageSize) results = results.slice(0, pageSize)
484
+ // If checkAccess filtered some results, fetch more to fill the page
485
+ const pageSize = mongoOpts.limit
486
+ if (pageSize && results.length < pageSize) {
487
+ let fetchSkip = mongoOpts.skip + pageSize
488
+ while (results.length < pageSize) {
489
+ const extra = await this.find(req.apiData.query, opts, { ...mongoOpts, skip: fetchSkip })
490
+ if (!extra.length) break
491
+ const filtered = await this.checkAccess(req, extra)
492
+ results = results.concat(filtered)
493
+ fetchSkip += extra.length
502
494
  }
495
+ if (results.length > pageSize) results = results.slice(0, pageSize)
496
+ }
503
497
 
504
- results = await this.sanitise(req.apiData.schemaName, results, { isInternal: true, strict: false })
498
+ results = await this.sanitise(req.apiData.schemaName, results, { isInternal: true, strict: false })
505
499
 
506
- res.status(this.mapStatusCode('get')).json(results)
507
- } catch (e) {
508
- return next(e)
509
- }
500
+ res.status(this.mapStatusCode('get')).json(results)
501
+ } catch (e) {
502
+ return next(e)
510
503
  }
511
- return queryHandler
512
504
  }
513
505
 
514
506
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-api",
3
- "version": "3.2.1",
3
+ "version": "3.2.2",
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",