2019-03-13 19:27:18 +01:00
const _ = require ( 'lodash' ) ;
const utils = require ( './utils' ) ( ) ;
2019-03-22 12:16:09 +01:00
/ * *
* Build a mongo query
* @ param { Object } options - Query options
* @ param { Object } options . model - The model you are querying
* @ param { Object } options . filers - An object with the possible filters ( start , limit , sort , where )
* @ param { Object } options . populate - An array of paths to populate
2019-03-26 14:57:02 +01:00
* @ param { boolean } options . aggregate - Force aggregate function to use group by feature
2019-03-22 12:16:09 +01:00
* /
2019-05-29 16:09:19 +02:00
const buildQuery = ( {
model ,
filters = { } ,
populate = [ ] ,
aggregate = false ,
} = { } ) => {
const deepFilters = ( filters . where || [ ] ) . filter (
( { field } ) => field . split ( '.' ) . length > 1
) ;
2019-03-26 14:57:02 +01:00
if ( deepFilters . length === 0 && aggregate === false ) {
return buildSimpleQuery ( { model , filters , populate } ) ;
}
return buildDeepQuery ( { model , filters , populate } ) ;
} ;
/ * *
* Builds a simple find query when there are no deep filters
* @ param { Object } options - Query options
* @ param { Object } options . model - The model you are querying
* @ param { Object } options . filers - An object with the possible filters ( start , limit , sort , where )
* @ param { Object } options . populate - An array of paths to populate
* /
const buildSimpleQuery = ( { model , filters , populate } ) => {
const { where = [ ] } = filters ;
2019-04-25 18:18:54 +02:00
const wheres = where . map ( buildWhereClause ) ;
const findCriteria = wheres . length > 0 ? { $and : wheres } : { } ;
2019-03-26 14:57:02 +01:00
2019-04-25 18:18:54 +02:00
let query = model . find ( findCriteria ) . populate ( populate ) ;
2019-03-26 14:57:02 +01:00
query = applyQueryParams ( { query , filters } ) ;
return Object . assign ( query , {
2019-03-29 16:53:25 +01:00
// Override count to use countDocuments on simple find query
2019-03-26 14:57:02 +01:00
count ( ... args ) {
return query . countDocuments ( ... args ) ;
} ,
} ) ;
} ;
/ * *
* Builds a deep aggregate query when there are deep filters
* @ param { Object } options - Query options
* @ param { Object } options . model - The model you are querying
* @ param { Object } options . filers - An object with the possible filters ( start , limit , sort , where )
* @ param { Object } options . populate - An array of paths to populate
* /
const buildDeepQuery = ( { model , filters , populate } ) => {
2019-03-29 16:53:25 +01:00
// Build a tree of paths to populate based on the filtering and the populate option
2019-03-22 12:16:09 +01:00
const { populatePaths , wherePaths } = computePopulatedPaths ( {
model ,
populate ,
where : filters . where ,
} ) ;
2019-03-13 19:27:18 +01:00
2019-03-29 16:53:25 +01:00
// Init the query
2019-03-22 12:16:09 +01:00
let query = model
2019-05-29 16:09:19 +02:00
. aggregate (
buildQueryAggregate ( model , {
paths : _ . merge ( { } , populatePaths , wherePaths ) ,
} )
)
2019-03-22 12:16:09 +01:00
. append ( buildQueryMatches ( model , filters ) ) ;
2019-03-13 19:27:18 +01:00
return {
2019-03-22 12:16:09 +01:00
/ * *
* Overrides the promise to rehydrate mongoose docs after the aggregation query
* /
2019-03-26 14:57:02 +01:00
then ( ... args ) {
2019-03-22 12:16:09 +01:00
return query
2019-08-20 12:07:45 +02:00
. append ( {
$project : { _id : true } ,
} )
. then ( results => results . map ( el => el . _id ) )
. then ( ids => {
if ( ids . length === 0 ) return [ ] ;
const query = model
. find ( {
_id : {
$in : ids ,
} ,
} )
. populate ( populate ) ;
return applyQueryParams ( { query , filters } ) ;
2019-03-22 12:16:09 +01:00
} )
2019-03-26 14:57:02 +01:00
. then ( ... args ) ;
2019-03-22 12:16:09 +01:00
} ,
catch ( ... args ) {
2019-03-26 14:57:02 +01:00
return this . then ( r => r ) . catch ( ... args ) ;
2019-03-22 12:16:09 +01:00
} ,
/ * *
* Maps to query . count
* /
2019-03-26 14:57:02 +01:00
count ( ) {
2019-05-29 16:09:19 +02:00
return query
. count ( 'count' )
. then ( results => _ . get ( results , [ '0' , 'count' ] , 0 ) ) ;
2019-03-13 19:27:18 +01:00
} ,
2019-03-26 14:57:02 +01:00
2019-03-29 16:53:25 +01:00
/ * *
* Maps to query group
* /
2019-03-13 19:27:18 +01:00
group ( ... args ) {
return query . group ( ... args ) ;
} ,
2019-03-22 12:16:09 +01:00
/ * *
* Returns an array of plain JS object instead of mongoose documents
* /
2019-03-13 19:27:18 +01:00
lean ( ) {
2019-03-29 16:53:25 +01:00
// Returns plain js objects without the transformations we normally do on find
2019-03-13 19:27:18 +01:00
return this . then ( results => {
2019-03-22 12:16:09 +01:00
return results . map ( r => r . toObject ( { transform : false } ) ) ;
2019-03-13 19:27:18 +01:00
} ) ;
} ,
} ;
} ;
2019-03-26 14:57:02 +01:00
/ * *
2019-03-29 16:53:25 +01:00
* Apply sort limit and start params
2019-03-26 14:57:02 +01:00
* @ param { Object } options - Options
* @ param { Object } options . query - Mongoose query
* @ param { Object } options . filters - Filters object
* /
const applyQueryParams = ( { query , filters } ) => {
2019-03-29 16:53:25 +01:00
// Apply sort param
2019-03-26 14:57:02 +01:00
if ( _ . has ( filters , 'sort' ) ) {
const sortFilter = filters . sort . reduce ( ( acc , sort ) => {
const { field , order } = sort ;
acc [ field ] = order === 'asc' ? 1 : - 1 ;
return acc ;
} , { } ) ;
query = query . sort ( sortFilter ) ;
}
2019-03-29 16:53:25 +01:00
// Apply start param
2019-03-26 14:57:02 +01:00
if ( _ . has ( filters , 'start' ) ) {
query = query . skip ( filters . start ) ;
}
2019-03-29 16:53:25 +01:00
// Apply limit param
2019-03-26 14:57:02 +01:00
if ( _ . has ( filters , 'limit' ) && filters . limit >= 0 ) {
query = query . limit ( filters . limit ) ;
}
return query ;
} ;
2019-03-22 12:16:09 +01:00
/ * *
* Returns a tree of the paths to populate both for population and deep filtering purposes
* @ param { Object } options - Options
* @ param { Object } options . model - Model from which to populate
* @ param { Object } options . populate - Paths to populate
* @ param { Object } options . where - Where clauses we need to populate to filters
* /
const computePopulatedPaths = ( { model , populate = [ ] , where = [ ] } ) => {
const castedPopulatePaths = populate
. map ( el => ( Array . isArray ( el ) ? el . join ( '.' ) : el ) )
. map ( path => findModelPath ( { rootModel : model , path } ) )
. map ( path => {
const assocModel = findModelByPath ( { rootModel : model , path } ) ;
// autoload morph relations
let extraPaths = [ ] ;
if ( assocModel ) {
extraPaths = assocModel . associations
. filter ( assoc => assoc . nature . toLowerCase ( ) . indexOf ( 'morph' ) !== - 1 )
. map ( assoc => ` ${ path } . ${ assoc . alias } ` ) ;
}
2019-03-13 19:27:18 +01:00
2019-03-22 12:16:09 +01:00
return [ path , ... extraPaths ] ;
} )
. reduce ( ( acc , paths ) => acc . concat ( paths ) , [ ] ) ;
2019-03-21 12:11:55 +01:00
2019-03-22 12:16:09 +01:00
const castedWherePaths = where
. map ( ( { field } ) => findModelPath ( { rootModel : model , path : field } ) )
. filter ( path => ! ! path ) ;
2019-03-13 19:27:18 +01:00
2019-03-22 12:16:09 +01:00
return {
populatePaths : pathsToTree ( castedPopulatePaths ) ,
wherePaths : pathsToTree ( castedWherePaths ) ,
} ;
} ;
2019-03-13 19:27:18 +01:00
2019-03-22 12:16:09 +01:00
/ * *
* Builds an object based on paths :
* [
* 'articles' ,
* 'articles.tags.cateogry' ,
* 'articles.tags.label' ,
* ] => {
* articles : {
* tags : {
* category : { } ,
* label : { }
* }
* }
* }
* @ param { Array < string > } paths - A list of paths to transform
* /
2019-05-29 16:09:19 +02:00
const pathsToTree = paths =>
paths . reduce ( ( acc , path ) => _ . merge ( acc , _ . set ( { } , path , { } ) ) , { } ) ;
2019-03-22 12:16:09 +01:00
/ * *
* Builds the aggregations pipeling of the query
* @ param { Object } model - Queried model
* @ param { Object } options - Options
2019-03-29 16:53:25 +01:00
* @ param { Object } options . paths - A tree of paths to aggregate e . g { article : { tags : { label : { } } } }
2019-03-22 12:16:09 +01:00
* /
const buildQueryAggregate = ( model , { paths } = { } ) => {
return Object . keys ( paths ) . reduce ( ( acc , key ) => {
return acc . concat ( buildLookup ( { model , key , paths : paths [ key ] } ) ) ;
2019-03-13 19:27:18 +01:00
} , [ ] ) ;
2019-03-22 12:16:09 +01:00
} ;
2019-03-13 19:27:18 +01:00
2019-03-22 12:16:09 +01:00
/ * *
2019-03-29 16:53:25 +01:00
* Builds a lookup aggregation for a specific key
* @ param { Object } options - Options
2019-03-22 12:16:09 +01:00
* @ param { Object } options . model - Queried model
2019-03-29 16:53:25 +01:00
* @ param { string } options . key - The attribute name to lookup on the model
* @ param { Object } options . paths - A tree of paths to aggregate inside the current lookup e . g { { tags : { label : { } } }
2019-03-22 12:16:09 +01:00
* /
const buildLookup = ( { model , key , paths } ) => {
const assoc = model . associations . find ( a => a . alias === key ) ;
const assocModel = findModelByAssoc ( { assoc } ) ;
2019-03-13 19:27:18 +01:00
2019-03-22 12:16:09 +01:00
if ( ! assocModel ) return [ ] ;
return [
{
$lookup : {
from : assocModel . collectionName ,
as : assoc . alias ,
let : {
localId : '$_id' ,
localAlias : ` $ ${ assoc . alias } ` ,
} ,
pipeline : [ ]
. concat ( buildLookupMatch ( { assoc } ) )
. concat ( buildQueryAggregate ( assocModel , { paths } ) ) ,
} ,
} ,
2019-08-20 12:07:45 +02:00
] ;
2019-03-22 12:16:09 +01:00
} ;
/ * *
* Build a lookup match expression ( equivalent to a SQL join condition )
2019-03-29 16:53:25 +01:00
* @ param { Object } options - Options
2019-03-22 12:16:09 +01:00
* @ param { Object } options . assoc - The association on which is based the ematching xpression
* /
const buildLookupMatch = ( { assoc } ) => {
switch ( assoc . nature ) {
case 'oneToOne' : {
return [
{
$match : {
$expr : {
$eq : [ ` $ ${ assoc . via } ` , '$$localId' ] ,
} ,
} ,
} ,
] ;
}
case 'oneToMany' : {
return {
$match : {
$expr : {
$eq : [ ` $ ${ assoc . via } ` , '$$localId' ] ,
} ,
} ,
} ;
}
case 'oneWay' :
case 'manyToOne' : {
return {
$match : {
$expr : {
$eq : [ '$$localAlias' , '$_id' ] ,
} ,
} ,
} ;
}
case 'manyToMany' : {
if ( assoc . dominant === true ) {
return {
$match : {
$expr : {
$in : [ '$_id' , '$$localAlias' ] ,
} ,
} ,
} ;
}
return {
$match : {
$expr : {
$in : [ '$$localId' , ` $ ${ assoc . via } ` ] ,
} ,
} ,
} ;
}
case 'manyToManyMorph' :
case 'oneToManyMorph' : {
return [
2019-05-29 16:09:19 +02:00
{
$unwind : { path : ` $ ${ assoc . via } ` , preserveNullAndEmptyArrays : true } ,
} ,
2019-03-22 12:16:09 +01:00
{
$match : {
$expr : {
$and : [
{ $eq : [ ` $ ${ assoc . via } .ref ` , '$$localId' ] } ,
{ $eq : [ ` $ ${ assoc . via } . ${ assoc . filter } ` , assoc . alias ] } ,
] ,
} ,
} ,
} ,
] ;
}
default :
return [ ] ;
}
} ;
2019-03-26 14:57:02 +01:00
/ * *
* Match query for lookups
* @ param { Object } model - Mongoose model
* @ param { Object } filters - Filters object
* /
2019-03-22 12:16:09 +01:00
const buildQueryMatches = ( model , filters ) => {
if ( _ . has ( filters , 'where' ) && Array . isArray ( filters . where ) ) {
return filters . where . map ( whereClause => {
return {
$match : buildWhereClause ( formatWhereClause ( model , whereClause ) ) ,
} ;
} ) ;
}
return [ ] ;
2019-03-13 19:27:18 +01:00
} ;
2019-03-26 14:57:02 +01:00
/ * *
* Cast values
* @ param { * } value - Value to cast
* /
2019-03-28 17:12:43 +01:00
const formatValue = value => utils . valueToId ( value ) ;
2019-03-22 17:58:36 +01:00
2019-03-26 14:57:02 +01:00
/ * *
* Builds a where clause
* @ param { Object } options - Options
* @ param { string } options . field - Where clause field
* @ param { string } options . operator - Where clause operator
* @ param { * } options . value - Where clause alue
* /
2019-03-13 19:27:18 +01:00
const buildWhereClause = ( { field , operator , value } ) => {
2019-03-28 17:12:43 +01:00
if ( Array . isArray ( value ) && ! [ 'in' , 'nin' ] . includes ( operator ) ) {
2019-03-22 17:58:36 +01:00
return {
2019-03-28 17:12:43 +01:00
$or : value . map ( val => buildWhereClause ( { field , operator , value : val } ) ) ,
2019-03-22 17:58:36 +01:00
} ;
}
2019-03-28 17:12:43 +01:00
const val = formatValue ( value ) ;
2019-03-13 19:27:18 +01:00
switch ( operator ) {
case 'eq' :
2019-03-22 17:58:36 +01:00
return { [ field ] : val } ;
2019-03-13 19:27:18 +01:00
case 'ne' :
2019-03-22 17:58:36 +01:00
return { [ field ] : { $ne : val } } ;
2019-03-13 19:27:18 +01:00
case 'lt' :
2019-03-22 17:58:36 +01:00
return { [ field ] : { $lt : val } } ;
2019-03-13 19:27:18 +01:00
case 'lte' :
2019-03-22 17:58:36 +01:00
return { [ field ] : { $lte : val } } ;
2019-03-13 19:27:18 +01:00
case 'gt' :
2019-03-22 17:58:36 +01:00
return { [ field ] : { $gt : val } } ;
2019-03-13 19:27:18 +01:00
case 'gte' :
2019-03-22 17:58:36 +01:00
return { [ field ] : { $gte : val } } ;
2019-03-13 19:27:18 +01:00
case 'in' :
return {
[ field ] : {
2019-03-22 17:58:36 +01:00
$in : Array . isArray ( val ) ? val : [ val ] ,
2019-03-13 19:27:18 +01:00
} ,
} ;
case 'nin' :
return {
[ field ] : {
2019-03-22 17:58:36 +01:00
$nin : Array . isArray ( val ) ? val : [ val ] ,
2019-03-13 19:27:18 +01:00
} ,
} ;
case 'contains' : {
return {
[ field ] : {
2019-03-22 17:58:36 +01:00
$regex : ` ${ val } ` ,
2019-03-13 19:27:18 +01:00
$options : 'i' ,
} ,
} ;
}
case 'ncontains' :
return {
[ field ] : {
2019-03-22 17:58:36 +01:00
$not : new RegExp ( val , 'i' ) ,
2019-03-13 19:27:18 +01:00
} ,
} ;
case 'containss' :
return {
[ field ] : {
2019-03-22 17:58:36 +01:00
$regex : ` ${ val } ` ,
2019-03-13 19:27:18 +01:00
} ,
} ;
case 'ncontainss' :
return {
[ field ] : {
2019-03-22 17:58:36 +01:00
$not : new RegExp ( val ) ,
2019-03-13 19:27:18 +01:00
} ,
} ;
2019-07-04 19:10:17 -03:00
case 'null' : {
return value ? { [ field ] : { $eq : null } } : { [ field ] : { $ne : null } } ;
}
2019-03-13 19:27:18 +01:00
default :
2019-05-29 16:09:19 +02:00
throw new Error ( ` Unhandled whereClause : ${ field } ${ operator } ${ value } ` ) ;
2019-03-13 19:27:18 +01:00
}
} ;
2019-03-26 14:57:02 +01:00
/ * *
* Add primaryKey on relation where clause for lookups match
* @ param { Object } model - Mongoose model
* @ param { Object } whereClause - Where clause
* @ param { string } whereClause . field - Where clause field
* @ param { string } whereClause . operator - Where clause operator
* @ param { * } whereClause . value - Where clause alue
* /
2019-03-22 12:16:09 +01:00
const formatWhereClause = ( model , { field , operator , value } ) => {
const { assoc , model : assocModel } = getAssociationFromFieldKey ( model , field ) ;
const shouldFieldBeSuffixed =
assoc &&
! _ . endsWith ( field , assocModel . primaryKey ) &&
( [ 'in' , 'nin' ] . includes ( operator ) || // When using in or nin operators we want to apply the filter on the relation's primary key and not the relation itself
( [ 'eq' , 'ne' ] . includes ( operator ) && utils . isMongoId ( value ) ) ) ; // Only suffix the field if the operators are eq or ne and the value is a valid mongo id
return {
field : shouldFieldBeSuffixed ? ` ${ field } . ${ assocModel . primaryKey } ` : field ,
operator ,
value ,
} ;
} ;
2019-03-26 14:57:02 +01:00
/ * *
* Returns an association from a path starting from model
* @ param { Object } model - Mongoose model
* @ param { string } fieldKey - Relation path
* /
const getAssociationFromFieldKey = ( model , fieldKey ) => {
let tmpModel = model ;
2019-03-13 19:27:18 +01:00
let assoc ;
const parts = fieldKey . split ( '.' ) ;
for ( let key of parts ) {
2019-03-26 14:57:02 +01:00
assoc = tmpModel . associations . find ( ast => ast . alias === key ) ;
2019-03-13 19:27:18 +01:00
if ( assoc ) {
2019-03-26 14:57:02 +01:00
tmpModel = findModelByAssoc ( { assoc } ) ;
2019-03-13 19:27:18 +01:00
}
}
return {
assoc ,
2019-03-26 14:57:02 +01:00
model : tmpModel ,
2019-03-13 19:27:18 +01:00
} ;
} ;
2019-03-26 14:57:02 +01:00
/ * *
2019-07-05 03:05:36 +02:00
* Returns a model from a relation path and a root model
2019-03-26 14:57:02 +01:00
* @ param { Object } options - Options
* @ param { Object } options . rootModel - Mongoose model
* @ param { string } options . path - Relation path
* /
2019-03-22 12:16:09 +01:00
const findModelByPath = ( { rootModel , path } ) => {
const parts = path . split ( '.' ) ;
2019-03-13 19:27:18 +01:00
2019-03-22 12:16:09 +01:00
let tmpModel = rootModel ;
for ( let part of parts ) {
const assoc = tmpModel . associations . find ( ast => ast . alias === part ) ;
if ( assoc ) {
tmpModel = findModelByAssoc ( { assoc } ) ;
2019-03-13 19:27:18 +01:00
}
}
2019-03-22 12:16:09 +01:00
return tmpModel ;
2019-03-13 19:27:18 +01:00
} ;
2019-03-26 14:57:02 +01:00
/ * *
* Returns a model path from an attribute path and a root model
* @ param { Object } options - Options
* @ param { Object } options . rootModel - Mongoose model
* @ param { string } options . path - Attribute path
* /
2019-03-22 12:16:09 +01:00
const findModelPath = ( { rootModel , path } ) => {
const parts = path . split ( '.' ) ;
2019-03-13 19:27:18 +01:00
2019-03-22 12:16:09 +01:00
let tmpModel = rootModel ;
let tmpPath = [ ] ;
for ( let part of parts ) {
const assoc = tmpModel . associations . find ( ast => ast . alias === part ) ;
2019-03-21 12:11:55 +01:00
2019-03-22 12:16:09 +01:00
if ( assoc ) {
tmpModel = findModelByAssoc ( { assoc } ) ;
tmpPath . push ( part ) ;
}
}
2019-03-13 19:27:18 +01:00
2019-03-22 12:16:09 +01:00
return tmpPath . length > 0 ? tmpPath . join ( '.' ) : null ;
2019-03-13 19:27:18 +01:00
} ;
2019-03-22 12:16:09 +01:00
const findModelByAssoc = ( { assoc } ) => {
const { models } = strapi . plugins [ assoc . plugin ] || strapi ;
return models [ assoc . model || assoc . collection ] ;
} ;
2019-03-13 19:27:18 +01:00
module . exports = buildQuery ;