merest
Version:
Express based REST-full API for Mongoose models
288 lines (242 loc) • 7 kB
JavaScript
const mongoose = require('mongoose');
const _pjoin = require('path').posix.join;
const ModelAPIError = require('../model-api-error');
module.exports = exports = {
ensureMethodObj: ensureMethodObj,
camel2giffen: camel2giffen,
flat: flat,
addController: addController,
readMiddlwares: readMiddlwares,
exposeMethods: exposeMethods,
parseRegExp: parseRegExp,
normalizeReadonly: normalizeReadonly,
validateReadonly: validateReadonly
}
function ensureMethodObj(obj, defMethod) {
if (obj === false) return false;
if (obj == null) return { method: defMethod };
if (typeof obj === 'object') {
obj.method = obj.method || defMethod;
if (obj.hasOwnProperty('readonly')) {
obj.readonly = normalizeReadonly(obj.readonly);
}
return obj;
}
var newObj = {};
if (typeof obj === 'string') {
var val = obj.toLowerCase();
if (ensureMethodObj.methods[val] === true) newObj.method = obj;
else newObj.title = obj;
}
newObj.method = newObj.method || defMethod;
return newObj;
}
ensureMethodObj.methods = {
checkout: true,
copy: true,
delete: true,
get: true,
head: true,
lock: true,
merge: true,
mkactivity: true,
mkcol: true,
move: true,
'm-search': true,
notify: true,
options: true,
patch: true,
post: true,
purge: true,
put: true,
report: true,
search: true,
subscribe: true,
trace: true,
unlock: true,
unsubscribe: true
}
function camel2giffen(str) {
var res = str
.replace(/^([A-Z]+)/, function(m) {return m.toLowerCase();})
.replace(/([A-Z]+)/g, function(m) {return "-"+m.toLowerCase();});
return res;
};
function flat(obj) {
return _flat('', obj, { });
}
function _flat(path, obj, flatted) {
return Object.keys(obj).reduce(
(f, p) => {
let np = path + p;
let v = obj[p];
if (v && !Array.isArray(v) && typeof(v) === 'object') {
return _flat(np + '.', v, f);
}
f[np] = v;
return f;
}, flatted
)
}
function addController(router, endPoint, path, options, controller, description) {
var mountPath = path;
if (mountPath === '/:id') {
mountPath = '/:id(' + router.apiOptions.matchId +')';
}
if (options.path) {
mountPath = _pjoin(mountPath, options.path);
path = _pjoin(path, options.path);
}
if (options.middlewares && options.middlewares.length) {
router[options.method].apply(router, [mountPath].concat(options.middlewares));
}
router[options.method](mountPath, function (req, res, next) {
return controller.call(router, req, res, next);
});
if (description !== false) {
var title = options.title || description;
router._urls.push([
options.method,
_pjoin(router.path, path),
title
]);
var exposedMethod;
if (endPoint.indexOf(':')>=0) {
endPoint = endPoint.split(':');
exposedMethod = endPoint[1];
endPoint = endPoint[0];
}
router._controllers.push({
endPoint: endPoint,
exposedMethod: exposedMethod,
path: _pjoin(router.path, path),
method: options.method,
controller: controller,
title: title,
model: router.model,
router: router,
options: options
});
}
}
function exposeMethods(router, basePath, expose, methods, handler) {
const staticPath = basePath.indexOf(":id") < 0 ? "statics." : "";
let allowed = Object.keys(expose);
let allowedAll = (expose['*'] === true);
if (allowedAll) {
methods = Object.keys(methods).concat(
allowed.filter(m => m !== '*' && !methods[m])
);
} else {
methods = allowed;
}
methods.forEach(methodName => {
if (allowedAll && expose[methodName] === false) return;
if (!allowedAll && !expose[methodName]) return;
let methodTitle, methodPath, methodOptions, endPoint;
expose[methodName] = methodOptions = ensureMethodObj(
expose[methodName], 'post'
);
methodOptions.path = methodOptions.path || camel2giffen(methodName);
endPoint = router.nameSingle + '.' + staticPath + methodName;
methodTitle = methodOptions.title || endPoint;
addController(
router,
handler.name + ':' + endPoint, basePath, methodOptions,
function(req, res, next) {
return handler.call(router, methodName, req, res, next);
},
methodTitle
)
});
};
function isModel(m) {
return (m && m.schema && m.schema instanceof mongoose.Schema);
}
function isPureFunction(f) {
return (f instanceof Function && !isModel(f));
}
function readMiddlwares(args, startIndex) {
var i = startIndex || 0;
var middlewares = [];
while (isPureFunction(args[i])) middlewares.push(args[i++]);
return (middlewares.length)?middlewares:null;
}
function parseRegExp(str) {
var s = str;
var start = str.indexOf('/');
var end = str.lastIndexOf('/');
var options;
if (start >= 0 && start < end) {
options = str.substring(end + 1);
str = str.substring(start + 1, end);
}
try {
return new RegExp(str, options)
} catch(e) {
return s;
}
}
function normalizeReadonly(val) {
if (!val) return null;
if (typeof val === 'string') val = val.split(/\s+/);
if (Array.isArray(val) && val.length) {
val = val.reduce( (o, v) => (o[v] = true) && o, {});
}
if (typeof val !== 'object' || !Object.keys(val).length) {
return null
}
return Object.keys(val).reduce( (a, p) => {
let v = val[p];
if (!v) return a;
let strict = /^[^!]+!$/.test(p);
if (strict) p = p.substr(0, p.length - 1);
let s = p.split('.').map(f => {
if (/^\*\*.+$/.test(f)) {
return f = '([^.]?\\.?)*' + f.substring(2, f.length);
}
switch (f) {
case '*' : return '[^.]+'
case '**': return '([^.]+\\.?)*'
}
return f;
}).join('\\.');
a.push({
name: p,
path: new RegExp(`^${s}$`),
strict: strict,
message: (typeof(v) === 'string') ?
v :
strict && `Couldn't modify path <${p}>` || null
})
return a;
}, [ ]);
}
function validateReadonly(data, rules) {
let errors = { _isError: false };
data = Object.keys(data).reduce( (o, p) => {
for (let rule of rules) {
if (!validatePathWithRule(p, rule, errors)) return o;
}
o[p] = data[p];
return o;
}, { });
if (errors._isError) {
delete errors._isError;
throw new ModelAPIError(422, 'Readonly constraint vialated', errors);
}
return data;
}
function validatePathWithRule(path, rule, errors) {
if (!rule.path.test(path)) return true;
if (rule.strict) {
errors._isError = true;
errors[path] = {
type: 'readonly',
message: rule.message,
path: path
}
}
return false;
}