Introduce separately testable param parser

This commit is contained in:
Elliot DeNolf
2019-01-25 12:53:27 -05:00
parent b604b1bcc8
commit ad4b7e2ecd
4 changed files with 262 additions and 62 deletions

View File

@@ -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}});
// })
// })
});
});

View File

@@ -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'}});
})
});
});

View File

@@ -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';

185
src/utils/paramParser.js Normal file
View File

@@ -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;
}