UNPKG

@cmddevelop/query-parameters

Version:

Translates URL query parameters for Mongoose, Express, and MongoDb

377 lines 14.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var qs = require("querystring"); var Moment = require("moment"); var _ = require("lodash"); var QueryParser = /** @class */ (function () { function QueryParser(options) { var _this = this; if (options === void 0) { options = {}; } this.options = options; this.defaultDateFormat = [Moment.ISO_8601]; this.builtInCaster = { string: function (val) { return String(val); }, date: function (val) { var m = Moment(val, _this.options.dateFormat); if (m.isValid()) { return m.toDate(); } else { throw new Error("Invalid date string: [" + val + "]"); } } }; this.operators = [ { operator: 'select', method: this.castSelect, defaultKey: 'select' }, { operator: 'populate', method: this.castPopulate, defaultKey: 'populate' }, { operator: 'sort', method: this.castSort, defaultKey: 'sort' }, { operator: 'skip', method: this.castSkip, defaultKey: 'skip' }, { operator: 'limit', method: this.castLimit, defaultKey: 'limit' }, { operator: 'filter', method: this.castFilter, defaultKey: 'filter' }, { operator: 'page', method: this.castPage, defaultKey: 'page' } ]; // add default date format as ISO_8601 this.options.dateFormat = options.dateFormat || this.defaultDateFormat; // add builtInCaster this.options.casters = Object.assign(this.builtInCaster, options.casters); // build blacklist this.options.blacklist = options.blacklist || []; this.operators.forEach(function (_a) { var operator = _a.operator, method = _a.method, defaultKey = _a.defaultKey; _this.options.blacklist.push(_this.options[operator + "Key"] || defaultKey); }); } /** * parses query string/object to Mongoose friendly query object/QueryOptions * @param {string | Object} query * @param {Object} [context] * @return {QueryOptions} */ QueryParser.prototype.parse = function (query, context) { var _this = this; var params = _.isString(query) ? qs.parse(query) : query; var options = this.options; var result = {}; this.operators.forEach(function (_a) { var operator = _a.operator, method = _a.method, defaultKey = _a.defaultKey; var key = options[operator + "Key"] || defaultKey; var value = params[key]; if (value || operator === 'filter') { result[operator] = method.call(_this, value, params); } }, this); result = this.parsePredefinedQuery(result, context); return result; }; /** * parses string to typed values * This methods will apply auto type casting on Number, RegExp, Date, Boolean and null * Also, it will apply defined casters in given options of the instance * @param {string} value * @param {string} key * @return {any} typed value */ QueryParser.prototype.parseValue = function (value, key) { var me = this; var options = this.options; // Apply casters // Match type casting operators like: string(true), _caster(123), $('test') var casters = options.casters; var casting = value.match(/^([a-zA-Z_$][0-9a-zA-Z_$]*)\((.*)\)$/); if (casting && casters[casting[1]]) { return casters[casting[1]](casting[2]); } // Apply casters per params if (key && options.castParams && options.castParams[key] && casters[options.castParams[key]]) { return casters[options.castParams[key]](value); } // cast array if (value.includes(',')) { return value.split(',').map(function (val) { return me.parseValue(val, key); }); } // Apply type casting for Number, RegExp, Date, Boolean and null // Match regex operators like /foo_\d+/i var regex = value.match(/^\/(.*)\/(i?)$/); if (regex) { return new RegExp(regex[1], regex[2]); } // Match boolean values if (value === 'true') { return true; } if (value === 'false') { return false; } // Match null if (value === 'null') { return null; } // Match numbers (string padded with zeros are not numbers) if (value !== '' && !isNaN(Number(value)) && !/^0[0-9]+/.test(value)) { return Number(value); } // Match dates var m = Moment(value, options.dateFormat); if (m.isValid()) { return m.toDate(); } return value; }; QueryParser.prototype.castFilter = function (filter, params) { var _this = this; var options = this.options; var parsedFilter = filter ? this.parseFilter(filter) : {}; return Object.keys(params) .map(function (val) { var join = params[val] ? val + "=" + params[val] : val; // Separate key, operators and value var _a = join.match(/(!?)([^><!=]+)([><]=?|!?=|)(.*)/), prefix = _a[1], key = _a[2], op = _a[3], value = _a[4]; return { prefix: prefix, key: key, op: _this.parseOperator(op), value: _this.parseValue(value, key) }; }) .filter(function (_a) { var key = _a.key; return options.blacklist.indexOf(key) === -1; }) .reduce(function (result, _a) { var prefix = _a.prefix, key = _a.key, op = _a.op, value = _a.value; if (!result[key]) { result[key] = {}; } if (Array.isArray(value)) { result[key][op === '$ne' ? '$nin' : '$in'] = value; } else if (op === '$exists') { result[key][op] = prefix !== '!'; } else if (op === '$eq') { result[key] = value; } else if (op === '$ne' && typeof value === 'object') { result[key].$not = value; } else { result[key][op] = value; } return result; }, parsedFilter); }; QueryParser.prototype.parseFilter = function (filter) { try { if (typeof filter === 'object') { return filter; } return JSON.parse(filter); } catch (err) { throw new Error("Invalid JSON string: " + filter); } }; QueryParser.prototype.parseOperator = function (operator) { if (operator === '=') { return '$eq'; } else if (operator === '!=') { return '$ne'; } else if (operator === '>') { return '$gt'; } else if (operator === '>=') { return '$gte'; } else if (operator === '<') { return '$lt'; } else if (operator === '<=') { return '$lte'; } else if (!operator) { return '$exists'; } }; /** * cast select query to object like: * select=a,b or select=-a,-b * => * {select: { a: 1, b: 1 }} or {select: { a: 0, b: 0 }} * @param val */ QueryParser.prototype.castSelect = function (val) { var fields = this.parseUnaries(val, { plus: 1, minus: 0 }); /* From the MongoDB documentation: "A projection cannot contain both include and exclude specifications, except for the exclusion of the _id field." */ var hasMixedValues = Object.keys(fields) .reduce(function (set, key) { if (key !== '_id') { set.add(fields[key]); } return set; }, new Set()).size > 1; if (hasMixedValues) { Object.keys(fields) .forEach(function (key) { if (fields[key] === 1) { delete fields[key]; } }); } return fields; }; /** * cast populate query to object like: * populate=field1.p1,field1.p2,field2 * => * [{path: 'field1', select: 'p1 p2'}, {path: 'field2'}] * @param val */ QueryParser.prototype.castPopulate = function (val) { return val .split(',') .map(function (qry) { var _a = qry.split('.', 2), p = _a[0], s = _a[1]; return s ? { path: p, select: s } : { path: p }; }).reduce(function (prev, curr, key) { // consolidate population array var path = curr.path; var select = curr.select; var found = false; prev.forEach(function (e) { if (e.path === path) { found = true; if (select) { e.select = e.select ? (e.select + ' ' + select) : select; } } }); if (!found) { prev.push(curr); } return prev; }, []); }; /** * cast sort query to object like * sort=-a,b * => * {sort: {a: -1, b: 1}} * @param sort */ QueryParser.prototype.castSort = function (sort) { return this.parseUnaries(sort); }; /** * Map/reduce helper to transform list of unaries * like '+a,-b,c' to {a: 1, b: -1, c: 1} */ QueryParser.prototype.parseUnaries = function (unaries, values) { if (values === void 0) { values = { plus: 1, minus: -1 }; } var unariesAsArray = _.isString(unaries) ? unaries.split(',') : unaries; return unariesAsArray .map(function (x) { return x.match(/^(\+|-)?(.*)/); }) .reduce(function (result, _a) { var val = _a[1], key = _a[2]; result[key.trim()] = val === '-' ? values.minus : values.plus; return result; }, {}); }; /** * cast skip query to object like * skip=100 * => * {skip: 100} * @param skip */ QueryParser.prototype.castSkip = function (skip) { return Number(skip); }; /** * cast limit query to object like * limit=10 * => * {limit: 10} * @param limit */ QueryParser.prototype.castLimit = function (limit) { return Number(limit); }; /** * cast page query to object like * page=1 * => * {page: 1} * @param page */ QueryParser.prototype.castPage = function (page) { return Number(page); }; /** * transform predefined query strings defined in query string to the actual query object out of the given context * @param query * @param context */ QueryParser.prototype.parsePredefinedQuery = function (query, context) { if (context) { // check if given string is the format as predefined query i.e. ${query} var _match_1 = function (str) { var reg = /^\$\{([a-zA-Z_$][0-9a-zA-Z_$]*)\}$/; var match = str.match(reg); var val = undefined; if (match) { val = _.property(match[1])(context); if (val === undefined) { throw new Error("No predefined query found for the provided reference [" + match[1] + "]"); } } return { match: !!match, val: val }; }; var _transform_1 = function (obj) { return _.reduce(obj, function (prev, curr, key) { var _a, _b; var val = undefined, match = undefined; if (_.isString(key)) { (_a = _match_1(key), match = _a.match, val = _a.val); if (match) { if (_.has(curr, '$exists')) { // 1). as a key: {'${qry}': {$exits: true}} => {${qry object}} return _.merge(prev, val); } else if (_.isString(val)) { // 1). as a key: {'${qry}': 'something'} => {'${qry object}': 'something'} key = val; } else { throw new Error("Invalid query string at " + key); } } } if (_.isString(curr)) { (_b = _match_1(curr), match = _b.match, val = _b.val); if (match) { _.isNumber(key) ? prev.push(val) // 3). as an item of array: ['${qry}', ...] => [${qry object}, ...] : (prev[key] = val); // 2). as a value: {prop: '${qry}'} => {prop: ${qry object}} return prev; } } if (_.isObject(curr) && !_.isRegExp(curr) && !_.isDate(curr)) { // iterate all props & keys recursively _.isNumber(key) ? prev.push(_transform_1(curr)) : (prev[key] = _transform_1(curr)); } else { _.isNumber(key) ? prev.push(curr) : (prev[key] = curr); } return prev; }, _.isArray(obj) ? [] : {}); }; return _transform_1(query); } return query; }; return QueryParser; }()); exports.QueryParser = QueryParser; //# sourceMappingURL=index.js.map