src/middleware/createStoreMiddleware.js
import { cloneDeep, mapValues } from 'lodash'
import AsyncBooleanExpressionEvaluator from 'async-boolean-expression-evaluator'
import AutonymError from '../AutonymError'
import Req from '../Req'
import Res from '../Res'
import { Router as createRouter } from 'express'
export default function createStoreMiddleware(model) {
const modelWithHooks = model.withHooks(createPolicyHooks())
const router = createRouter({ mergeParams: true })
router.route('/').post((req, res, next) => callStoreMethod(create, req, res, next))
router.route('/').get((req, res, next) => callStoreMethod(find, req, res, next))
router.route('/:id').get((req, res, next) => callStoreMethod(findOne, req, res, next))
router.route('/:id').patch((req, res, next) => callStoreMethod(findOneAndUpdate, req, res, next))
router.route('/:id').put((req, res, next) => callStoreMethod(findOneAndUpdate, req, res, next))
router.route('/:id').delete((req, res, next) => callStoreMethod(findOneAndDelete, req, res, next))
return router
function createPolicyHooks() {
return mapValues(model.getPolicies(), hooks =>
mapValues(hooks, (expression, hook) => async (req, res, meta, data) => {
if (data && (hook === 'postSchema' || hook === 'preStore')) {
req.setData(data, true)
}
if (hook === 'postSchema') {
req._isValidated = true
}
if (hook === 'postStore') {
res._isPopulated = true
res.setData(data, true)
}
await evaluatePolicies(expression, req, res, meta)
if (data && hook === 'preSchema') {
return req.getCompleteData()
} else if (data && (hook === 'postSchema' || hook === 'preStore')) {
return req.getData()
} else if (data && hook === 'postStore') {
return res.getData()
} else {
return null
}
})
)
}
async function evaluatePolicies(expression, req, res, meta) {
let lastError = null
const evaluator = new AsyncBooleanExpressionEvaluator(async operand => {
if (typeof operand === 'function') {
try {
await operand(req, res, meta)
return true
} catch (err) {
// `err` may be undefined if this is the result of a `not` expression
lastError = err
return false
}
} else if (typeof operand === 'boolean') {
if (operand) {
return true
} else {
// If operand is just false, use generic error
lastError = new AutonymError(AutonymError.FORBIDDEN, 'This action may not be performed.')
return false
}
} else {
throw new TypeError(
`Policy operands for model "${model.getName()}" are invalid. Operands may be functions or booleans, received ${typeof operand}.`
)
}
})
const result = await evaluator.execute(expression)
if (!result) {
throw lastError || new AutonymError(AutonymError.FORBIDDEN, 'This action may not be performed.')
}
}
async function callStoreMethod(method, req, res, next) {
const meta = cloneDeep(model.getInitialMeta())
const autonymReq = new Req(req, model, meta)
const autonymRes = new Res(res, model, meta)
let autonymError = null
try {
const { status } = await method(autonymReq, autonymRes, meta)
if (autonymRes.getStatus() === null) {
autonymRes.setStatus(status)
}
} catch (err) {
autonymError = AutonymError.fromError(err).toClientError()
}
next(autonymError)
}
async function create(req, res, meta) {
await modelWithHooks.create(req.getData(), meta, [req, res, meta])
return { status: Res.CREATED }
}
async function find(req, res, meta) {
await modelWithHooks.find(req.getQuery(), meta, [req, res, meta])
return { status: Res.OK }
}
async function findOne(req, res, meta) {
await modelWithHooks.findOne(req.getId(), meta, [req, res, meta])
return { status: Res.OK }
}
async function findOneAndUpdate(req, res, meta) {
const completeData = await req.getCompleteData()
await modelWithHooks.findOneAndUpdate(req.getId(), req.getData(), completeData, meta, [req, res, meta])
return { status: Res.OK }
}
async function findOneAndDelete(req, res, meta) {
await modelWithHooks.findOneAndDelete(req.getId(), meta, [req, res, meta])
return { status: Res.OK }
}
}