Fully configured response structure
This commit is contained in:
189
src/plugins/buildQuery.js
Normal file
189
src/plugins/buildQuery.js
Normal 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
155
src/plugins/paginate.js
Normal 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;
|
||||
@@ -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 = {};
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user