UNPKG

minapi

Version:

Minimum viable API w/ authentication and permissions, CRUD and resource management

363 lines (315 loc) 9.68 kB
const _ = require('underscore') module.exports = (API, { routes }) => { const methods = { _create: { http: 'post', db: 'create' }, _readAll: { http: 'get', db: 'readAll' }, _read: { http: 'get', db: 'read' }, _update: { http: 'put', db: 'update' }, _delete: { http: 'delete', db: 'delete' }, } //getting base information for each router's routes let routers = routes.map(r => { const pattern = RegExp(/([^\/]*)\/([^\(]+)\(([^\(]+)\)/) const [id, parentPath, path, model] = r._id.match(pattern) for (let m in methods) { if (r[m]) { if (!r[m].where) { r[m].url = `/${path}` } else { r[m].params = {} let paramKey let dbKey if (_.isString(r[m].where)) { paramKey = `${model}${r[m].where}` dbKey = `${model}.${r[m].where}` } else if (_.isObject(r[m].where)) { const key = Object.keys(r[m].where)[0] paramKey = `${model}${key}` dbKey = r[m].where[key].map(v => `${model}.${v}`) } r[m].url = `/${path}/:${paramKey}` r[m].params[paramKey] = dbKey } } } return { ...r, model, path, parentPath, parents: [] } }) //chaining parent routers together let processed = 0 while (processed < routers.length) { for (const r of routers) { let cursor = r.parentPath let depth = 0 while (cursor !== '') { const found = routers.find(router => cursor === router.path) r.parents.unshift({ path: found.path, parentPath: found.parentPath, model: found.model, depth, _create: _.omit(found._create, 'allow'), _readAll: _.omit(found._readAll, 'allow'), _read: _.omit(found._read, 'allow'), _update: _.omit(found._update, 'allow'), _delete: _.omit(found._delete, 'allow'), }) cursor = found.parentPath depth-- } processed++ } } //determining full url for each router's routes for (let r of routers) { for (let m in methods) { if (r[m]) { let url = '' for (let parent of r.parents) { const p = parent._read url += p.url } url += r[m].url r[m].url = url } } } //cataloging all parent req.params for each router's routes for (let r of routers) { if (r.parents.length > 0) { r.parentsParams = {} for (let p of r.parents) { r.parentsParams = { ...r.parentsParams, ...p._read.params || {} } } } } // //finding params to find required model rows // let params = {} // _.each(routers, router => { // _.each(models, model => { // if (router.model === model) { // params[model] = router._read.params || {} // } // }) // }) // r[m].allowMiddleware = async (req, res, next) => { // let data = {} // if (models.indexOf('user') > -1) { // data.user = req.user || {} // } // for (let model of models) { // if (params[model]) { // let where = {} // let whereKeys = Object.keys(params[model]) // for (let whereKey of whereKeys) { // let whereValue = req.params[whereKey] // where[whereKey] = whereValue // } // data[model] = await API.DB[model].read(where) // console.log({ where, 'data[model]': data[model] }) // } // } // console.log({ json, models, params, data }) // //this is where the route allow logic gets applied to the found data // console.log(data) // next() // } //loading all controllers and routes onto API for (const router of routers) { for (const m in methods) { if (router[m]) { const { http, db } = methods[m] const r = router[m] const middlewares = [ API.Auth.requireToken, API.Auth.requireUser, ] const processAllowString = str => { let values = [] let operatorPattern = RegExp(/[^\=]+(\=[in]*\=?)/) let operator = str.match(operatorPattern)[1] let parts = str.split(operator) for (let i in parts) { let part = parts[i] let partPattern = RegExp(/^\@([^\.]+)\.(.*)$/) let partMatches = part.match(partPattern) if (!partMatches) { values[i] = part // console.log('values', i, part) } else { let collection = partMatches[1] let field = partMatches[2] let value = modelData[collection][field] if (API.DB.mongodb.ObjectId.isValid(value)) { value = String(value) } if (Array.isArray(value)) { value = value.map(item => { if (API.DB.mongodb.ObjectId.isValid(item)) { return String(item) } else { return item } }) } values[i] = value // console.log('values', i, collection, field, values[i]) if (value === null || value === undefined) { console.log(`-- @${collection}.${field} didn't exist in db!`) return null } } } // console.log({ operator, values, str }) switch (operator) { case '=': // console.log(values[0] == values[1]) return values[0] == values[1] break case '=in=': if (!Array.isArray(values[1])) { return false } // console.log(values[1].indexOf(values[0]) > -1) return values[1].indexOf(values[0]) > -1 break } return false } const traverseAllowCommands = (allow, comparison) => { let result if (_.isString(allow)) { result = processAllowString(allow) } else if (Array.isArray(allow)) { // console.log(' in array ', allow) arrResult = allow.map(item => traverseAllowCommands(item)) // console.log(comparison, { arrResult }) switch (comparison) { case 'and': result = true for (let value of arrResult) { if (value === null) { result = false } else if (value === false) { result = false } } break case 'or': result = false for (let value of arrResult) { if (value === true) { result = true } } break } } else if (_.isObject(allow)) { // console.log(' in object ') if (allow.and) { // console.log(' in object and ', allow.and) result = traverseAllowCommands(allow.and, 'and') } else if (allow.or) { // console.log(' in object or ', allow.or) result = traverseAllowCommands(allow.or, 'or') } } return result } //getting allow logic const allowJSON = JSON.stringify(r.allow) const pattern = RegExp(/\@([a-z0-9\_]+)\./g) const matches = allowJSON.match(pattern) || [] const modelsInAllow = _.unique(matches).map(v => v.substr(1, v.length-2)) //mapping allow db keys with params keyValues (mapping route params with db keys) let allParams = { ...router.parentsParams || {}, ...r.params || {}, } let keys = {} let modelData = {} API[http](r.url, [...middlewares, async function(req, res, next) { try { if (r.allow) { //loading user model if specified and authenticated if (modelsInAllow.indexOf('user') > -1) { modelData.user = await API.DB.user.read({ where: { _id: req.user._id } }) } //loading other models if specified and w/ req.params values for (let routeParam in allParams) { //we're only loading models that are referenced in the url path (including the user object) let [model, key] = allParams[routeParam].split('.') //creating a where object to locate the correct data let where = {} where[key] = req.params[routeParam] || null //pulling data from each route w/ params modelData[model] = await API.DB[model].read({ where }) } console.log({ modelData, allowJSON, modelsInAllow, modelDataJSON: JSON.stringify(modelData) }) const isAuthorized = traverseAllowCommands(r.allow) console.log({ isAuthorized }) if (!isAuthorized) { throw { code: 422, err: `user not authorized! permissions invalid.` } } next() } } catch (err) { API.Utils.errorHandler({ res, err }) } }], async (req, res) => { try { const paramsAllowed = [ ...Object.keys(router.parentsParams || {}), ...Object.keys(r.params || {}) ] const where = _.pick(req.params, paramsAllowed) const values = { ...req.body, ...where } await API.DB.open() const data = await API.DB[router.model][db]({ where, values }) await API.DB.close() let statusCode = 200 if (data === undefined) { statusCode = 500 } else if (data === null) { statusCode = 404 } else if (Array.isArray(data) && data.length === 0) { statusCode = 404 } res.status(statusCode).send({ data }) } catch (err) { API.Utils.errorHandler({ res, err }) } }) let newCheck = { resource: r.url, description: `${m} for accessing ${router.model} items at ${r.url}`, method: http.toUpperCase(), params: ``, bearerToken: ``, body: ``, output: ``, expectedStatusCode: 200, } switch (m) { case '_create': newCheck.body = API.DB[router.model].dummy('c') newCheck.body = `(${JSON.stringify(newCheck.body)})` break } // API.Checks.register(newCheck) } } } API.get('/', (req, res) => { res.status(200).send({ message: 'healthy' }) }) API.Checks.register({ resource: '/', description: 'health checks for api', method: 'GET', params: ``, bearerToken: ``, body: ``, output: ``, expectedStatusCode: 200, }) return API }