From ad4b7e2ecd01945e4cac6016611e9005aa2ceffb Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Fri, 25 Jan 2019 12:53:27 -0500 Subject: [PATCH] Introduce separately testable param parser --- src/tests/mongooseApiQuery.spec.js | 58 --------- src/tests/paramParser.spec.js | 72 +++++++++++ src/utils/mongooseApiQuery.js | 9 +- src/utils/paramParser.js | 185 +++++++++++++++++++++++++++++ 4 files changed, 262 insertions(+), 62 deletions(-) create mode 100644 src/tests/paramParser.spec.js create mode 100644 src/utils/paramParser.js diff --git a/src/tests/mongooseApiQuery.spec.js b/src/tests/mongooseApiQuery.spec.js index fc8550f9ed..5d7ae7e27e 100644 --- a/src/tests/mongooseApiQuery.spec.js +++ b/src/tests/mongooseApiQuery.spec.js @@ -18,62 +18,4 @@ describe('mongooseApiQuery', () => { it('Should not blow up', () => { expect(mongooseApiQuery).not.toBeNull(); }); - - describe('Parameter Parsing', () => { - it('Property Equals', () => { - let parsed = TestUserSchema.statics.apiQueryParams({name: 'john'}); - expect(parsed.searchParams).toEqual({name: {'$regex': 'john', '$options': '-i'}}); - }); - - it('Greater than or equal', () => { - let parsed = TestUserSchema.statics.apiQueryParams({age: '{gte}21'}); - expect(parsed.searchParams).toEqual({age: {'$gte': '21'}}); - }); - - it('Greater than, less than', () => { - let parsed = TestUserSchema.statics.apiQueryParams({age: '{gte}0{lt}20'}); - expect(parsed.searchParams).toEqual({age: {'$gte': '0', '$lt': '20'}}); - }); - - describe('Pagination / Limits', () => { - it('Page number', () => { - let parsed = TestUserSchema.statics.apiQueryParams({page: '2'}); - expect(parsed).toEqual({searchParams: {}, page: '2', per_page: 100, sort: false}); - }); - - it('Page number with per page', () => { - let parsed = TestUserSchema.statics.apiQueryParams({page: '2', per_page: '1'}); - expect(parsed).toEqual({searchParams: {}, page: '2', per_page: 1, sort: false}); - }); - - it('Per page', () => { - let parsed = TestUserSchema.statics.apiQueryParams({per_page: '1'}); - expect(parsed).toEqual({searchParams: {}, page: 1, per_page: 1, sort: false}); - }); - - it('Limit', () => { - let parsed = TestUserSchema.statics.apiQueryParams({limit: '1'}); - expect(parsed).toEqual({searchParams: {}, page: 1, per_page: 1, sort: false}); - }) - }); - - describe('Sorting', () => { - it('Sort ascending', () => { - let parsed = TestUserSchema.statics.apiQueryParams({sort_by: 'title'}); - expect(parsed).toEqual({searchParams: {}, page: 1, per_page: 100, sort: {title: 1}}); - }); - - it('Sort descending', () => { - let parsed = TestUserSchema.statics.apiQueryParams({sort_by: 'title,desc'}); - expect(parsed).toEqual({searchParams: {}, page: 1, per_page: 100, sort: {title: 'desc'}}); - }) - }); - - // describe('Boolean', () => { - // it('Y is true', () => { - // let parsed = TestUserSchema.statics.apiQueryParams({published: 'Y'}); - // expect(parsed.searchParams).toEqual({published: {'$eq': true}}); - // }) - // }) - }); }); diff --git a/src/tests/paramParser.spec.js b/src/tests/paramParser.spec.js new file mode 100644 index 0000000000..063da2d7b2 --- /dev/null +++ b/src/tests/paramParser.spec.js @@ -0,0 +1,72 @@ +import mongoose from 'mongoose'; +const Schema = mongoose.Schema; +import {paramParser} from '../utils/paramParser'; + +const TestUserSchema = new Schema({ + name: {type: String}, + description: {type: String}, + married: {type: Boolean}, + age: {type: Number} + } +); + +const TestUser = mongoose.model('TestUser', TestUserSchema); + +describe('param parser', () => { + + it('Should parse', () => { + let result = paramParser(TestUser); + expect(result).not.toBeNull(); + }); + + describe('Parameter Parsing', () => { + it('Property Equals', () => { + let parsed = paramParser(TestUser, {name: 'john'}); + expect(parsed.searchParams).toEqual({name: {'$regex': 'john', '$options': '-i'}}); + }); + + it('Greater than or equal', () => { + let parsed = paramParser(TestUser, {age: '{gte}21'}); + expect(parsed.searchParams).toEqual({age: {'$gte': '21'}}); + }); + + it('Greater than, less than', () => { + let parsed = paramParser(TestUser, {age: '{gte}0{lt}20'}); + expect(parsed.searchParams).toEqual({age: {'$gte': '0', '$lt': '20'}}); + }); + }); + + describe('Pagination / Limits', () => { + it('Page number', () => { + let parsed = paramParser(TestUser, {page: '2'}); + expect(parsed).toEqual({searchParams: {}, page: '2', per_page: 100, sort: false}); + }); + + it('Page number with per page', () => { + let parsed = paramParser(TestUser, {page: '2', per_page: '1'}); + expect(parsed).toEqual({searchParams: {}, page: '2', per_page: 1, sort: false}); + }); + + it('Per page', () => { + let parsed = paramParser(TestUser, {per_page: '1'}); + expect(parsed).toEqual({searchParams: {}, page: 1, per_page: 1, sort: false}); + }); + + it('Limit', () => { + let parsed = paramParser(TestUser, {limit: '1'}); + expect(parsed).toEqual({searchParams: {}, page: 1, per_page: 1, sort: false}); + }) + }); + + describe('Sorting', () => { + it('Sort ascending', () => { + let parsed = paramParser(TestUser, {sort_by: 'title'}); + expect(parsed).toEqual({searchParams: {}, page: 1, per_page: 100, sort: {title: 1}}); + }); + + it('Sort descending', () => { + let parsed = paramParser(TestUser, {sort_by: 'title,desc'}); + expect(parsed).toEqual({searchParams: {}, page: 1, per_page: 100, sort: {title: 'desc'}}); + }) + }); +}); diff --git a/src/utils/mongooseApiQuery.js b/src/utils/mongooseApiQuery.js index 364d75842c..2a3ed6a2a3 100644 --- a/src/utils/mongooseApiQuery.js +++ b/src/utils/mongooseApiQuery.js @@ -1,9 +1,11 @@ +import {paramParser} from './paramParser'; + export default function apiQueryPlugin(schema) { schema.statics.apiQuery = function (rawParams, cb) { console.log(rawParams); const model = this; - const params = model.apiQueryParams(rawParams); + const params = paramParser(this, rawParams); let // Create the Mongoose Query object. query = model @@ -11,7 +13,8 @@ export default function apiQueryPlugin(schema) { .limit(params.per_page) .skip((params.page - 1) * params.per_page); - if (params.sort) query = query.sort(params.sort); + if (params.sort) + query = query.sort(params.sort); if (cb) { query.exec(cb); @@ -64,7 +67,6 @@ export default function apiQueryPlugin(schema) { } } else if (typeof schema === 'undefined') { - console.log('schema undefined, lckey: '+ lcKey); paramType = 'String'; } else if (typeof schema.paths[lcKey] === 'undefined') { @@ -74,7 +76,6 @@ export default function apiQueryPlugin(schema) { } else if (schema.paths[lcKey].constructor.name === 'SchemaBoolean') { paramType = 'Boolean'; } else if (schema.paths[lcKey].constructor.name === 'SchemaString') { - console.log('schema string'); paramType = 'String'; } else if (schema.paths[lcKey].constructor.name === 'SchemaNumber') { paramType = 'Number'; diff --git a/src/utils/paramParser.js b/src/utils/paramParser.js new file mode 100644 index 0000000000..4c55b9b151 --- /dev/null +++ b/src/utils/paramParser.js @@ -0,0 +1,185 @@ +export function paramParser(model, rawParams) { + + const convertToBoolean = str => { + return str.toLowerCase() === 'true' || + str.toLowerCase() === 't' || + str.toLowerCase() === 'yes' || + str.toLowerCase() === 'y' || + str === '1'; + }; + + //changed + const searchParams = {}; + + let query; + let page = 1; + let per_page = 100; + let sort = false; + + const parseSchemaForKey = (schema, keyPrefix, lcKey, val, operator) => { + + let paramType; + + const addSearchParam = val => { + const key = keyPrefix + lcKey; + + if (typeof searchParams[key] !== 'undefined') { + for (let i in val) { + searchParams[key][i] = val[i]; + } + } else { + searchParams[key] = val; + } + }; + + let matches = lcKey.match(/(.+)\.(.+)/); + if (matches) { + // parse subschema + if (schema.paths[matches[1]].constructor.name === 'DocumentArray' || + schema.paths[matches[1]].constructor.name === 'Mixed') { + parseSchemaForKey(schema.paths[matches[1]].schema, `${matches[1]}.`, matches[2], val, operator) + } + + } else if (typeof schema === 'undefined') { + paramType = 'String'; + + } else if (typeof schema.paths[lcKey] === 'undefined') { + // nada, not found + } else if (operator === 'near') { + paramType = 'Near'; + } else if (schema.paths[lcKey].constructor.name === 'SchemaBoolean') { + paramType = 'Boolean'; + } else if (schema.paths[lcKey].constructor.name === 'SchemaString') { + paramType = 'String'; + } else if (schema.paths[lcKey].constructor.name === 'SchemaNumber') { + paramType = 'Number'; + } else if (schema.paths[lcKey].constructor.name === 'ObjectId') { + paramType = 'ObjectId'; + }//changed + else if (schema.paths[lcKey].constructor.name === 'SchemaArray') { + paramType = 'Array'; + } + + console.log('Param Type: ' + paramType); + + if (paramType === 'Boolean') { + addSearchParam(convertToBoolean(val)); + } else if (paramType === 'Number') { + if (val.match(/([0-9]+,?)/) && val.match(',')) { + if (operator === 'all') { + addSearchParam({$all: val.split(',')}); + } else if (operator === 'nin') { + addSearchParam({$nin: val.split(',')}); + } else if (operator === 'mod') { + addSearchParam({$mod: [val.split(',')[0], val.split(',')[1]]}); + } else { + addSearchParam({$in: val.split(',')}); + } + } else if (val.match(/([0-9]+)/)) { + if (operator === 'gt' || + operator === 'gte' || + operator === 'lt' || + operator === 'lte' || + operator === 'ne') { + let newParam = {}; + newParam[`$${operator}`] = val; + addSearchParam(newParam); + } else {//changed + addSearchParam(parseInt(val)); + } + } + } else if (paramType === 'String') { + if (val.match(',')) { + const options = val.split(',').map(str => new RegExp(str, 'i')); + + if (operator === 'all') { + addSearchParam({$all: options}); + } else if (operator === 'nin') { + addSearchParam({$nin: options}); + } else { + addSearchParam({$in: options}); + } + } else if (val.match(/([0-9]+)/)) { + if (operator === 'gt' || + operator === 'gte' || + operator === 'lt' || + operator === 'lte') { + let newParam = {}; + newParam[`$${operator}`] = val; + addSearchParam(newParam); + } else { + addSearchParam(val); + } + } else if (operator === 'ne' || operator === 'not') { + const neregex = new RegExp(val, 'i'); + addSearchParam({'$not': neregex}); + } else if (operator === 'exact') { + addSearchParam(val); + } else { + addSearchParam({$regex: val, $options: '-i'}); + } + } else if (paramType === 'Near') { + // divide by 69 to convert miles to degrees + const latlng = val.split(','); + const distObj = {$near: [parseFloat(latlng[0]), parseFloat(latlng[1])]}; + if (typeof latlng[2] !== 'undefined') { + distObj.$maxDistance = parseFloat(latlng[2]) / 69; + } + addSearchParam(distObj); + } else if (paramType === 'ObjectId') { + addSearchParam(val); + } else if (paramType === 'Array') { + addSearchParam(val); + console.log(lcKey) + + } + + }; + + const parseParam = (key, val) => { + console.log(key, val); + const lcKey = key; + let operator = val.match(/\{(.*)\}/); + val = val.replace(/\{(.*)\}/, ''); + + if (operator) operator = operator[1]; + + if (val === '') { + return; + } else if (lcKey === 'page') { + page = val; + } else if (lcKey === 'per_page' || lcKey === 'limit') { + per_page = parseInt(val); + } else if (lcKey === 'sort_by') { + const parts = val.split(','); + sort = {}; + sort[parts[0]] = parts.length > 1 ? parts[1] : 1; + } else { + parseSchemaForKey(model.schema, '', lcKey, val, operator); + } + }; + + // Construct searchParams + for (const key in rawParams) { + const separatedParams = rawParams[key].match(/\{\w+\}(.[^\{\}]*)/g); + + if (separatedParams === null) { + parseParam(key, rawParams[key]); + } else { + for (let i = 0; i < separatedParams.length; ++i) { + parseParam(key, separatedParams[i]); + } + } + } + + let returnVal = { + searchParams, + page, + per_page, + sort + }; + + console.log(returnVal); + + return returnVal; +}