WIP: Pull in old build query plugin changes

This commit is contained in:
Elliot DeNolf
2020-02-03 00:51:57 -05:00
parent 49741f3003
commit 2ad7802ec9
4 changed files with 217 additions and 176 deletions

View File

@@ -1,7 +1,7 @@
const mongooseHidden = require('mongoose-hidden');
const paginate = require('mongoose-paginate-v2');
const autopopulate = require('mongoose-autopopulate');
const { buildQueryPlugin } = require('../mongoose/buildQuery');
const buildQueryPlugin = require('../mongoose/buildQuery');
const localizationPlugin = require('../localization/plugin');
const buildSchema = require('../mongoose/schema/buildSchema');

View File

@@ -1,11 +1,12 @@
/* eslint-disable no-underscore-dangle */
/* eslint-disable no-param-reassign */
const mongoose = require('mongoose');
/* eslint-disable no-use-before-define */
function buildQueryPlugin(schema) {
function apiQuery(rawParams, locale, cb) {
schema.statics.apiQuery = async function (rawParams, locale, cb) {
const model = this;
const params = paramParser(this, rawParams, locale);
const paramParser = new ParamParser(this, rawParams, locale);
const params = await paramParser.parse();
if (cb) {
model
@@ -14,187 +15,228 @@ function buildQueryPlugin(schema) {
}
return params.searchParams;
}
schema.statics.apiQuery = apiQuery;
};
}
function paramParser(model, rawParams, locale) {
let query = {
searchParams: {},
sort: false,
};
class ParamParser {
constructor(model, rawParams, locale) {
this.model = model;
this.rawParams = rawParams;
this.locale = locale;
this.query = {
searchParams: {},
sort: false,
};
}
// Construct searchParams
Object.keys(rawParams).forEach((key) => {
const separatedParams = rawParams[key]
.match(/{\w+}(.[^{}]*)/g);
async parse() {
// Construct searchParams
// let keys = Object.keys(this.rawParams);
Object.keys(this.rawParams).forEach(async (key) => {
const separatedParams = this.rawParams[key].match(/{\w+}(.[^{}]*)/g);
if (separatedParams === null) {
query = parseParam(key, rawParams[key], model, query, locale);
if (separatedParams === null) {
await this.parseParam(key, this.rawParams[key], this.model, this.locale);
} else {
for (let i = 0; i < separatedParams.length; ++i) {
await this.parseParam(key, separatedParams[i], this.model, this.locale);
}
}
});
return this.query;
}
async parseParam(key, val, model, locale) {
const lcKey = key;
let operator = val.match(/\{(.*)\}/);
val = val.replace(/\{(.*)\}/, '');
if (operator) operator = operator[1];
if (val === '') {
return {};
} if (lcKey === 'sort_by' || lcKey === 'order_by') {
const parts = val.split(',');
this.query.sort = {};
this.query.sort[parts[0]] = parts[1] === 'asc' || parts.length <= 1 ? 1 : parts[1];
} else if (lcKey === 'include') {
if (val.match(',')) {
const orArray = [];
val.split(',').map(id => orArray.push({ _id: id }));
this.addSearchParam('$or', orArray);
} else {
this.query.searchParams['_id'] = val;
}
} else if (lcKey === 'exclude') {
if (val.match(',')) {
const andArray = [];
val.split(',').map(id => andArray.push({ _id: { $ne: id } }));
this.addSearchParam('$and', andArray);
} else {
this.query.searchParams['_id'] = { $ne: val };
}
} else if (lcKey === 'locale') {
// Do nothing
} else {
separatedParams.forEach((param) => {
query = parseParam(key, param, model, query, locale);
await this.parseSchemaForKey(model.schema, '', lcKey, val, operator, locale);
}
return Promise.resolve(true);
}
addSearchParam(key, value) {
if (typeof this.query.searchParams[key] !== 'undefined') {
Object.keys(value).forEach((i) => {
if (value.hasOwnProperty(i)) {
this.query.searchParams[key][i] = value[i];
} else {
this.query.searchParams[key] = value;
}
});
}
});
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') {
value.forEach((i) => {
query.searchParams[key][i] = value[i];
});
} else {
query.searchParams[key] = value;
}
return query;
}
function parseParam(key, val, model, query, locale) {
const lcKey = key;
let operator = val.match(/\{(.*)\}/);
val = val.replace(/\{(.*)\}/, '');
if (operator) [, operator] = operator;
if (val === '') {
return {};
} 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(',')) {
const 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(',')) {
const andArray = [];
val.split(',').map(id => andArray.push({ _id: { $ne: id } }));
query = addSearchParam(query, '$and', andArray);
} else query.searchParams._id = { $ne: val };
} else if (lcKey === 'locale') {
// Do nothing
} else {
query = parseSchemaForKey(model.schema, query, '', lcKey, val, operator, locale);
}
return query;
}
function parseSchemaForKey(schema, query, keyPrefix, lcKey, val, operator, locale) {
let paramType;
let key = keyPrefix + lcKey;
const 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'
|| schema.paths[matches[1]].constructor.name === 'SingleNestedPath') {
// This wasn't handled in the original package but seems to work
paramType = schema.paths[matches[1]].schema.paths.name.instance;
}
} else if (schema.obj[lcKey] && typeof schema === 'object') {
if (schema.obj[lcKey].localized) {
key = `${key}.${locale}`;
}
paramType = schema.obj[lcKey].name || schema.obj[lcKey].type.name;
} 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') {
query = addSearchParam(query, key, convertToBoolean(val));
} else if (paramType === 'Number') {
if (val.match(/([0-9]+,?)/) && val.match(',')) {
if (operator === 'all') {
query = addSearchParam(query, key, { $all: val.split(',') });
} else if (operator === 'nin') {
query = addSearchParam(query, key, { $nin: val.split(',') });
} else if (operator === 'mod') {
query = addSearchParam(query, key, { $mod: [val.split(',')[0], val.split(',')[1]] });
} else {
query = addSearchParam(query, key, { $in: val.split(',') });
}
} else if (val.match(/([0-9]+)/)) {
if (operator === 'gt'
|| operator === 'gte'
|| operator === 'lt'
|| operator === 'lte'
|| operator === 'ne') {
const newParam = {};
newParam[`$${operator}`] = val;
query = addSearchParam(query, key, newParam);
} else {
query = addSearchParam(query, key, parseInt(val, 0));
}
}
} else if (paramType === 'String') {
if (val.match(',')) {
const options = val.split(',').map(str => new RegExp(str, 'i'));
async parseSchemaForKey(schema, keyPrefix, lcKey, val, operator, locale) {
let paramType;
let key = keyPrefix + lcKey;
if (operator === 'all') {
query = addSearchParam(query, key, { $all: options });
} else if (operator === 'nin') {
query = addSearchParam(query, key, { $nin: options });
} else {
query = addSearchParam(query, key, { $in: options });
const split = lcKey.split('.');
console.log('lcKey', lcKey);
if (split.length > 1) {
// Parse SubSchema
if (schema.paths[split[0]].constructor.name === 'DocumentArray'
|| schema.paths[split[0]].constructor.name === 'Mixed') {
await this.parseSchemaForKey(schema.paths[split[0]].schema, `${split[0]}.`, split[1], val, operator);
} else if (schema.paths[split[0]].constructor.name === 'SchemaType') {
// This wasn't handled in the original package but seems to work
paramType = schema.paths[split[0]].schema.paths.name.instance;
} else if (schema.paths[split[0]].constructor.name === 'SchemaArray') {
paramType = 'Array';
}
} else if (val.match(/([0-9]+)/)) {
if (operator === 'gt'
|| operator === 'gte'
|| operator === 'lt'
|| operator === 'lte') {
const newParam = {};
newParam[`$${operator}`] = val;
query = addSearchParam(query, key, newParam);
} else {
query = addSearchParam(query, key, val);
} else if (schema.obj[lcKey] && typeof schema === 'object') {
if (schema.obj[lcKey].intl) {
key = `${key}.${locale}`;
}
} else if (operator === 'ne' || operator === 'not') {
const neregex = new RegExp(val, 'i');
query = addSearchParam(query, key, { $not: neregex });
} else if (operator === 'like') {
query = addSearchParam(query, key, { $regex: val, $options: '-i' });
} else {
query = addSearchParam(query, key, val);
paramType = schema.obj[lcKey].name || schema.obj[lcKey].type.name;
} 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';
}
} else if (paramType === 'ObjectId') {
query = addSearchParam(query, key, val);
} else if (paramType === 'Array') {
query = addSearchParam(query, key, val);
if (paramType === 'Boolean') {
const convertToBoolean = (str) => {
return str.toLowerCase() === 'true'
|| str.toLowerCase() === 't'
|| str.toLowerCase() === 'yes'
|| str.toLowerCase() === 'y'
|| str === '1';
};
this.addSearchParam(key, convertToBoolean(val));
} else if (paramType === 'Number') {
if (val.match(/([0-9]+,?)/) && val.match(',')) {
if (operator === 'all') {
this.addSearchParam(key, { $all: val.split(',') });
} else if (operator === 'nin') {
this.addSearchParam(key, { $nin: val.split(',') });
} else if (operator === 'mod') {
this.addSearchParam(key, { $mod: [val.split(',')[0], val.split(',')[1]] });
} else {
this.addSearchParam(key, { $in: val.split(',') });
}
} else if (val.match(/([0-9]+)/)) {
if (operator === 'gt'
|| operator === 'gte'
|| operator === 'lt'
|| operator === 'lte'
|| operator === 'ne') {
const newParam = {};
newParam[`$${ operator}`] = val;
this.addSearchParam(key, newParam);
} else {
this.addSearchParam(key, parseInt(val));
}
}
} else if (paramType === 'String') {
if (val.match(',')) {
const options = val.split(',').map(str => new RegExp(str, 'i'));
if (operator === 'all') {
this.addSearchParam(key, { $all: options });
} else if (operator === 'nin') {
this.addSearchParam(key, { $nin: options });
} else {
this.addSearchParam(key, { $in: options });
}
} else if (val.match(/([0-9]+)/)) {
if (operator === 'gt'
|| operator === 'gte'
|| operator === 'lt'
|| operator === 'lte') {
const newParam = {};
newParam[`$${operator}`] = val;
this.addSearchParam(key, newParam);
} else {
this.addSearchParam(key, val);
}
} else if (operator === 'ne' || operator === 'not') {
const neregex = new RegExp(val, 'i');
this.addSearchParam(key, { $not: neregex });
} else if (operator === 'like') {
this.addSearchParam(key, { $regex: val, $options: '-i' });
} else {
this.addSearchParam(key, val);
}
} else if (paramType === 'ObjectId') {
this.addSearchParam(key, val);
} else if (paramType === 'Array') {
const recurseSchema = async (count, tempSchema) => {
const path = tempSchema.paths[split[count]];
const ref = path && path.options && path.options.type && path.options.type[0].ref;
console.log('ref', ref);
let refKey = split[count + 1];
if (ref && refKey) {
const refModel = mongoose.model(ref);
if (count < split.length - 1) {
count++;
await recurseSchema(count, refModel.schema);
}
if (refKey && refModel.schema.obj[refKey].intl) {
refKey = `${refKey}.${locale}`;
}
console.log('refKey', refKey, count);
// TODO: Need to recursively traverse paths longer than two segments.
// Example: this code handles "categories.title" but will not handle "categories.pages.title".
// TODO: Support operators as above
const refQuery = await refModel.find({ [refKey]: val });
if (refQuery) {
this.query.searchParams[split[count]] = { $in: refQuery.map(doc => doc.id) };
}
} else if (!ref) {
this.addSearchParam(key, val);
}
};
await recurseSchema(0, schema);
}
console.log('this.query', JSON.stringify(this.query));
}
return query;
}
module.exports = {
buildQueryPlugin,
paramParser,
};
module.exports = buildQueryPlugin;

View File

@@ -17,7 +17,8 @@ const query = (req, res) => {
req.model.paginate(req.model.apiQuery(req.query, req.locale), paginateQuery, (err, result) => {
if (err) {
return res.status(httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(err, 'mongoose'));
// return res.status(httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(err, 'mongoose'));
return res.status(httpStatus.INTERNAL_SERVER_ERROR).send();
}
res.status(httpStatus.OK).json({
...result,

View File

@@ -9,8 +9,6 @@ const formatErrorResponse = (incoming, source) => {
return acc;
}, []),
};
case 'APIError':
return {
errors: [