diff --git a/src/collections/buildSchema.js b/src/collections/buildSchema.js index 49c593923e..3f71203793 100644 --- a/src/collections/buildSchema.js +++ b/src/collections/buildSchema.js @@ -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'); diff --git a/src/mongoose/buildQuery.js b/src/mongoose/buildQuery.js index f521acf5f4..2b9658ecd8 100644 --- a/src/mongoose/buildQuery.js +++ b/src/mongoose/buildQuery.js @@ -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; diff --git a/src/mongoose/requestHandlers/query.js b/src/mongoose/requestHandlers/query.js index b3ca73a36b..ffbcb60aef 100644 --- a/src/mongoose/requestHandlers/query.js +++ b/src/mongoose/requestHandlers/query.js @@ -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, diff --git a/src/responses/formatError.js b/src/responses/formatError.js index 9c6c3588f0..035b73970f 100644 --- a/src/responses/formatError.js +++ b/src/responses/formatError.js @@ -9,8 +9,6 @@ const formatErrorResponse = (incoming, source) => { return acc; }, []), }; - - case 'APIError': return { errors: [