Fully configured response structure

This commit is contained in:
Elliot DeNolf
2019-02-16 18:01:22 -05:00
parent 9545fe060d
commit ad5d8040b8
9 changed files with 364 additions and 39 deletions

189
src/plugins/buildQuery.js Normal file
View File

@@ -0,0 +1,189 @@
/* eslint-disable no-use-before-define */
export default function buildQuery(schema) {
schema.statics.apiQuery = function (rawParams) {
const model = this;
const params = paramParser(this, rawParams);
// Create the Mongoose Query object.
let query = model
.find(params.searchParams);
if (params.sort)
query = query.sort(params.sort);
return query;
};
}
function paramParser(model, rawParams) {
let query = {
searchParams: {},
sort: false
};
// Construct searchParams
for (const key in rawParams) {
const separatedParams = rawParams[key]
.match(/{\w+}(.[^{}]*)/g);
if (separatedParams === null) {
query = parseParam(key, rawParams[key], model, query);
} else {
for (let i = 0; i < separatedParams.length; ++i) {
query = parseParam(key, separatedParams[i], model, query);
}
}
}
return query;
}
function convertToBoolean(str) {
return str.toLowerCase() === 'true' ||
str.toLowerCase() === 't' ||
str.toLowerCase() === 'yes' ||
str.toLowerCase() === 'y' ||
str === '1';
}
function addSearchParam(query, key, value) {
if (typeof query.searchParams[key] !== 'undefined') {
for (let i in value) {
query.searchParams[key][i] = value[i];
}
} else {
query.searchParams[key] = value;
}
return query;
}
function parseParam(key, val, model, query) {
const lcKey = key;
let operator = val.match(/\{(.*)\}/);
val = val.replace(/\{(.*)\}/, '');
if (operator) operator = operator[1];
if (val === '') {
return {};
} else if (lcKey === 'sort_by' || lcKey === 'order_by') {
const parts = val.split(',');
query.sort = {};
query.sort[parts[0]] = parts[1] === 'asc' || parts.length <= 1 ? 1 : parts[1];
} else if (lcKey === 'include') {
if (val.match(',')) {
let orArray = [];
val.split(',').map(id => orArray.push({_id: id}));
query = addSearchParam(query, '$or', orArray);
} else
query.searchParams['_id'] = val;
} else if (lcKey === 'exclude') {
if (val.match(',')) {
let andArray = [];
val.split(',').map(id => andArray.push({_id: {$ne: id}}));
query = addSearchParam(query, '$and', andArray);
} else
query.searchParams['_id'] = {$ne: val};
} else {
query = parseSchemaForKey(model.schema, query, '', lcKey, val, operator);
}
return query;
}
function parseSchemaForKey(schema, query, keyPrefix, lcKey, val, operator) {
let paramType;
const key = keyPrefix + lcKey;
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 (schema.paths[matches[1]].constructor.name === 'SchemaType'){
// This wasn't handled in the original package but seems to work
paramType = schema.paths[matches[1]].schema.paths.name.instance;
}
} else if (typeof schema === 'undefined') {
paramType = 'String';
} else if (typeof schema.paths[lcKey] === 'undefined') {
// nada, not found
} 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';
} else if (schema.paths[lcKey].constructor.name === 'SchemaArray') {
paramType = 'Array';
}
if (paramType === 'Boolean') {
addSearchParam(query, key, convertToBoolean(val));
} else if (paramType === 'Number') {
if (val.match(/([0-9]+,?)/) && val.match(',')) {
if (operator === 'all') {
addSearchParam(query, key, {$all: val.split(',')});
} else if (operator === 'nin') {
addSearchParam(query, key, {$nin: val.split(',')});
} else if (operator === 'mod') {
addSearchParam(query, key, {$mod: [val.split(',')[0], val.split(',')[1]]});
} else {
addSearchParam(query, key, {$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(query, key, newParam);
} else {
addSearchParam(query, key, parseInt(val));
}
}
} else if (paramType === 'String') {
if (val.match(',')) {
const options = val.split(',').map(str => new RegExp(str, 'i'));
if (operator === 'all') {
addSearchParam(query, key, {$all: options});
} else if (operator === 'nin') {
addSearchParam(query, key, {$nin: options});
} else {
addSearchParam(query, key, {$in: options});
}
} else if (val.match(/([0-9]+)/)) {
if (operator === 'gt' ||
operator === 'gte' ||
operator === 'lt' ||
operator === 'lte') {
let newParam = {};
newParam['$' + operator] = val;
addSearchParam(query, key, newParam);
} else {
addSearchParam(query, key, val);
}
} else if (operator === 'ne' || operator === 'not') {
const neregex = new RegExp(val, 'i');
addSearchParam(query, key, {'$not': neregex});
} else if (operator === 'like') {
addSearchParam(query, key, {$regex: val, $options: '-i'});
} else {
addSearchParam(query, key, val);
}
} else if (paramType === 'ObjectId') {
addSearchParam(query, key, val);
} else if (paramType === 'Array') {
addSearchParam(query, key, val);
}
return query;
}

155
src/plugins/paginate.js Normal file
View File

@@ -0,0 +1,155 @@
/**
* @param {Object} [query={}]
* @param {Object} [options={}]
* @param {Object|String} [options.select]
* @param {Object|String} [options.sort]
* @param {Object|String} [options.customLabels]
* @param {Object} [options.collation]
* @param {Array|Object|String} [options.populate]
* @param {Boolean} [options.lean=false]
* @param {Boolean} [options.leanWithId=true]
* @param {Number} [options.offset=0] - Use offset or page to set skip position
* @param {Number} [options.page=1]
* @param {Number} [options.limit=10]
* @param {Function} [callback]
*
* @returns {Promise}
*/
function paginate(query, options, callback) {
query = query || {};
options = Object.assign({}, paginate.options, options);
options.customLabels = options.customLabels ? options.customLabels : {};
var defaultLimit = 10;
var select = options.select;
var sort = options.sort;
var collation = options.collation || {};
var populate = options.populate;
var lean = options.lean || false;
var leanWithId = options.hasOwnProperty('leanWithId') ? options.leanWithId : true;
var limit = options.hasOwnProperty('limit') ? parseInt(options.limit) : defaultLimit;
var skip;
var offset;
var page;
// Custom Labels
var labelTotal = options.customLabels.totalDocs ? options.customLabels.totalDocs : 'totalDocs';
var labelLimit = options.customLabels.limit ? options.customLabels.limit : 'limit';
var labelPage = options.customLabels.page ? options.customLabels.page : 'page';
var labelTotalPages = options.customLabels.totalPages ? options.customLabels.totalPages : 'totalPages';
var labelDocs = options.customLabels.docs ? options.customLabels.docs : 'docs';
var labelNextPage = options.customLabels.nextPage ? options.customLabels.nextPage : 'nextPage';
var labelPrevPage = options.customLabels.prevPage ? options.customLabels.prevPage : 'prevPage';
var labelPagingCounter = options.customLabels.pagingCounter ? options.customLabels.pagingCounter : 'pagingCounter';
if (options.hasOwnProperty('offset')) {
offset = parseInt(options.offset);
skip = offset;
} else if (options.hasOwnProperty('page')) {
page = parseInt(options.page);
skip = (page - 1) * limit;
} else {
offset = 0;
page = 1;
skip = offset;
}
const count = this.countDocuments(query).exec();
const model = this.find(query);
model.select(select);
model.sort(sort);
model.lean(lean);
// Hack for mongo < v3.4
if (Object.keys(collation).length > 0) {
model.collation(collation);
}
if (limit) {
model.skip(skip);
model.limit(limit);
}
if (populate) {
model.populate(populate);
}
var docs = model.exec();
if (lean && leanWithId) {
docs = docs.then(function (docs) {
docs.forEach(function (doc) {
doc.id = String(doc._id);
});
return docs;
});
}
return Promise.all([count, docs])
.then(function (values) {
var result = {
[labelDocs]: values[1],
[labelTotal]: values[0],
[labelLimit]: limit
};
if (offset !== undefined) {
result.offset = offset;
}
if (page !== undefined) {
const pages = Math.ceil(values[0] / limit) || 1;
result.hasPrevPage = false;
result.hasNextPage = false;
result[labelPage] = page;
result[labelTotalPages] = pages;
result[labelPagingCounter] = ((page - 1) * limit) + 1;
// Set prev page
if (page > 1) {
result.hasPrevPage = true;
result[labelPrevPage] = (page - 1);
} else {
result[labelPrevPage] = null;
}
// Set next page
if (page < pages) {
result.hasNextPage = true;
result[labelNextPage] = (page + 1);
} else {
result[labelNextPage] = null;
}
}
// Adding support for callbacks if specified.
if (typeof callback === 'function') {
return callback(null, result);
} else {
return Promise.resolve(result);
}
}).catch(function (reject) {
if (typeof callback === 'function') {
return callback(reject);
} else {
return Promise.reject(reject);
}
});
}
/**
* @param {Schema} schema
*/
module.exports = function (schema) {
schema.statics.paginate = paginate;
};
module.exports.paginate = paginate;

View File

@@ -1,6 +1,5 @@
/* eslint-disable no-use-before-define,camelcase */
export function paramParser(model, rawParams) {
console.log('raw params', rawParams);
let query = {
searchParams: {},
@@ -23,7 +22,6 @@ export function paramParser(model, rawParams) {
}
}
console.log('query object', query);
return query;
}
@@ -57,8 +55,8 @@ function parseParam(key, val, model, query) {
return {};
} else if (lcKey === 'page') {
query.page = val;
} else if (lcKey === 'per_page' || lcKey === 'limit') {
query.per_page = parseInt(val);
} else if (lcKey === 'limit') {
query.limit = parseInt(val);
} else if (lcKey === 'sort_by' || lcKey === 'order_by') {
const parts = val.split(',');
query.sort = {};

View File

@@ -1,7 +1,7 @@
import httpStatus from 'http-status';
const findOne = (req, res) => {
req.model.setDefaultLanguage(req.locale);
// req.model.setDefaultLanguage(req.locale);
req.model.findOne({ _id: req.params._id }, (err, doc) => {
if (err)

View File

@@ -1,15 +1,20 @@
import httpStatus from 'http-status';
const query = (req, res) => {
if (req.query.locale)
if (req.query.locale) {
req.model.setDefaultLanguage(req.query.locale);
}
req.model.apiQuery(req.query, (err, pages) => {
if (err)
req.model.paginate(req.model.apiQuery(req.query), req.query, (err, result) => {
if (err) {
return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ error: err });
}
return res.json(pages.map(page => page.toJSON({ virtuals: !!req.locale })));
});
return res.json({
...result,
docs: result.docs.map(doc => doc.toJSON({ virtuals: !!req.locale }))
});
})
};
export default query;

View File

@@ -1,6 +1,6 @@
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
import mongooseApiQuery from '../utils/mongooseApiQuery';
import mongooseApiQuery from '../plugins/buildQuery';
const TestUserSchema = new Schema({
name: {type: String},

View File

@@ -1,7 +1,7 @@
/* eslint-disable camelcase */
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
import {paramParser} from '../utils/paramParser';
import {paramParser} from '../plugins/paramParser';
const AuthorSchema = new Schema({
name: String,

View File

@@ -1,24 +0,0 @@
import {paramParser} from './paramParser';
export default function apiQueryPlugin(schema) {
schema.statics.apiQuery = function (rawParams, cb) {
const model = this;
const params = paramParser(this, rawParams);
// Create the Mongoose Query object.
let query = model
.find(params.searchParams)
.limit(params.per_page)
.skip((params.page - 1) * params.per_page);
if (params.sort)
query = query.sort(params.sort);
if (cb) {
query.exec(cb);
} else {
return query;
}
};
}