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.
- package/lib/AbstractApiModule.js +99 -107
- package/package.json +1 -1
package/lib/AbstractApiModule.js
CHANGED
|
@@ -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
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
476
|
+
await this.requestHook.invoke(req)
|
|
483
477
|
|
|
484
|
-
|
|
478
|
+
await this.setUpPagination(req, res, mongoOpts)
|
|
485
479
|
|
|
486
|
-
|
|
480
|
+
let results = await this.find(req.apiData.query, opts, mongoOpts)
|
|
487
481
|
|
|
488
|
-
|
|
482
|
+
results = await this.checkAccess(req, results)
|
|
489
483
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
498
|
+
results = await this.sanitise(req.apiData.schemaName, results, { isInternal: true, strict: false })
|
|
505
499
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
/**
|