koa-mongo-crud
Version:
Base API for CRUD using koa and mongo
336 lines (277 loc) • 8.01 kB
JavaScript
module.exports = function MongoQF(options) {
const opts = options || {};
this.ops = opts.ops || ['!', '^', '$', '~', '>', '<', '$in'];
this.alias = opts.alias || {};
this.blacklist = opts.blacklist || {};
this.whitelist = opts.whitelist || {};
this.custom = opts.custom || {};
// String Value Parsing
opts.string = opts.string || {};
this.string = opts.string || {};
this.string.toBoolean = (typeof opts.string.toBoolean === 'boolean') ? opts.string.toBoolean : true;
this.string.toNumber = (typeof opts.string.toNumber === 'boolean') ? opts.string.toNumber : true;
this.keyRegex = opts.keyRegex || /^[a-zæøå0-9-_.]+$/i;
this.valRegex = opts.valRegex || /[^a-zæøå0-9-_.* ]/i;
this.arrRegex = opts.arrRegex || /^[a-zæøå0-9-_.]+(\[])?$/i;
this.objRegex = opts.objRegex || this.keyRegex;
if (this.custom.bbox) {
this.custom.bbox = this.customBBOX(this.custom.bbox);
}
if (this.custom.near) {
this.custom.near = this.customNear(this.custom.near);
}
if (this.custom.after) {
this.custom.after = this.customAfter(this.custom.after);
}
if (this.custom.before) {
this.custom.before = this.customBefore(this.custom.before);
}
if (this.custom.between) {
this.custom.between = this.customBetween(this.custom.between);
}
return this;
};
module.exports.prototype.customBBOX = (field) => (query, bbox) => {
const bboxArr = bbox.split(',');
if (bboxArr.length === 4) {
// Optimize by unrolling the loop
bboxArr[0] = parseFloat(bboxArr[0], 10);
bboxArr[1] = parseFloat(bboxArr[1], 10);
bboxArr[2] = parseFloat(bboxArr[2], 10);
bboxArr[3] = parseFloat(bboxArr[3], 10);
if (!isNaN(bboxArr.reduce((a, b) => a + b))) {
query[field] = {
$geoWithin: {
$geometry: {
type: 'Polygon',
coordinates: [[
[bboxArr[0], bboxArr[1]],
[bboxArr[2], bboxArr[1]],
[bboxArr[2], bboxArr[3]],
[bboxArr[0], bboxArr[3]],
[bboxArr[0], bboxArr[1]],
]],
},
},
};
}
}
};
module.exports.prototype.customNear = (field) => (query, point) => {
const pointArr = point.split(',').map((p) => parseFloat(p, 10));
if (pointArr.length >= 2) {
if (!isNaN(pointArr.reduce((a, b) => a + b))) {
const max = pointArr[2];
const min = pointArr[3];
query[field] = {
$near: {
$geometry: {
type: 'Point',
coordinates: pointArr.splice(0, 2),
},
},
};
if (!isNaN(max)) {
query[field].$near.$maxDistance = max;
if (!isNaN(min)) {
query[field].$near.$minDistance = min;
}
}
}
}
};
function parseDate(value) {
let date = value;
if (!isNaN(date)) {
if (`${date}`.length === 10) {
date = `${date}000`;
}
date = parseInt(date, 10);
}
return new Date(date);
}
module.exports.prototype.customAfter = (field) => (query, value) => {
const date = parseDate(value);
if (date.toString() !== 'Invalid Date') {
query[field] = {
$gte: date,
};
}
};
module.exports.prototype.customBefore = (field) => (query, value) => {
const date = parseDate(value);
if (date.toString() !== 'Invalid Date') {
query[field] = {
$lt: date,
};
}
};
module.exports.prototype.customBetween = (field) => (query, value) => {
const dates = value.split('|');
const afterValue = dates[0];
const beforeValue = dates[1];
const after = parseDate(afterValue);
const before = parseDate(beforeValue);
if (after.toString() !== 'Invalid Date' && before.toString() !== 'Invalid Date') {
query[field] = {
$gte: after,
$lt: before,
};
}
};
module.exports.prototype.parseString = function parseString(string, array) {
let op = string[0] || '';
const eq = string[1] === '=';
let org = string.substr(eq ? 2 : 1) || '';
const val = this.parseStringVal(org);
const ret = { op, org, value: val };
switch (op) {
case '!':
if (array) {
ret.field = '$nin';
} else if (org === '') {
ret.field = '$exists';
ret.value = false;
} else {
ret.field = '$ne';
}
break;
case '>':
ret.field = eq ? '$gte' : '$gt';
break;
case '<':
ret.field = eq ? '$lte' : '$lt';
break;
case '^':
case '$':
case '~':
ret.field = '$regex';
ret.options = 'i';
ret.value = org.replace(this.valReqex, '');
switch (op) {
case '^':
ret.value = `^${val}`;
break;
case '$':
ret.value = `${val}$`;
break;
default:
break;
}
break;
default:
ret.org = org = op + org;
ret.op = op = '';
ret.value = this.parseStringVal(org);
if (array) {
ret.field = '$in';
} else if (org === '') {
ret.field = '$exists';
ret.value = true;
} else {
ret.field = '$eq';
}
}
ret.parsed = {};
ret.parsed[ret.field] = ret.value;
if (ret.options) {
ret.parsed.$options = ret.options;
}
return ret;
};
module.exports.prototype.parseStringVal = function parseStringVal(string) {
if (this.string.toBoolean && string.toLowerCase() === 'true') {
return true;
} if (this.string.toBoolean && string.toLowerCase() === 'false') {
return false;
} if (this.string.toNumber && !isNaN(parseInt(string, 10))
&& ((+string - +string) + 1) >= 0) {
return parseFloat(string, 10);
}
return string;
};
module.exports.prototype.parse = function parse(query) {
const res = {};
Object.keys(query).forEach((k) => {
let key = k;
const val = query[key];
// normalize array keys
if (val instanceof Array) {
key = key.replace(/\[]$/, '');
}
// whitelist
if (Object.keys(this.whitelist).length && !this.whitelist[key]) {
return;
}
// blacklist
if (this.blacklist[key]) {
return;
}
// alias
if (this.alias[key]) {
key = this.alias[key];
}
// string key
if (typeof val === 'string' && !this.keyRegex.test(key)) {
return;
// array key
} if (val instanceof Array && !this.arrRegex.test(key)) {
return;
// string key and object val
} if (val instanceof Object && !this.objRegex.test(key)) {
return;
}
// custom functions
if (typeof this.custom[key] === 'function') {
this.custom[key].apply(null, [res, val]);
return;
}
// object val
if (val instanceof Object) {
res[key] = val;
}
// array key
if (val instanceof Array) {
if (this.ops.indexOf('$in') >= 0 && val.length > 0) {
res[key] = {};
for (let i = 0; i < val.length; i += 1) {
if (this.ops.indexOf(val[i][0]) >= 0) {
const parsed = this.parseString(val[i], true);
switch (parsed.field) {
case '$in':
case '$nin':
res[key][parsed.field] = res[key][parsed.field] || [];
res[key][parsed.field].push(parsed.value);
break;
case '$regex':
res[key].$regex = parsed.value;
res[key].$options = parsed.options;
break;
default:
res[key][parsed.field] = parsed.value;
}
} else {
res[key].$in = res[key].$in || [];
res[key].$in.push(this.parseStringVal(val[i]));
}
}
}
return;
}
// value must be a string
if (typeof val !== 'string') {
return;
}
// field exists query
if (!val) {
res[key] = { $exists: true };
// query operators
} else if (this.ops.indexOf(val[0]) >= 0) {
res[key] = this.parseString(val).parsed;
// equal operator (no operator)
} else {
res[key] = this.parseStringVal(val);
}
});
return res;
};