diff --git a/src/index.js b/src/index.js index f768684..6f86ca2 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import _ from 'lodash' import queryBuilder from './query-builder' import filterBuilder from './filter-builder' import aggregationBuilder from './aggregation-builder' -import { sortMerge } from './utils' +import { sortMerge, build, buildV1 } from './utils' /** * **http://bodybuilder.js.org** @@ -140,10 +140,10 @@ export default function bodybuilder () { const aggregations = this.getAggregations() if (version === 'v1') { - return _buildV1(body, queries, filters, aggregations) + return buildV1(body, queries, filters, aggregations) } - return _build(body, queries, filters, aggregations) + return build(body, queries, filters, aggregations) } }, @@ -153,48 +153,4 @@ export default function bodybuilder () { ) } -function _buildV1(body, queries, filters, aggregations) { - let clonedBody = _.cloneDeep(body) - - if (!_.isEmpty(filters)) { - _.set(clonedBody, 'query.filtered.filter', filters) - - if (!_.isEmpty(queries)) { - _.set(clonedBody, 'query.filtered.query', queries) - } - - } else if (!_.isEmpty(queries)) { - _.set(clonedBody, 'query', queries) - } - - if (!_.isEmpty(aggregations)) { - _.set(clonedBody, 'aggregations', aggregations) - } - return clonedBody -} - -function _build(body, queries, filters, aggregations) { - let clonedBody = _.cloneDeep(body) - - if (!_.isEmpty(filters)) { - let filterBody = {} - let queryBody = {} - _.set(filterBody, 'query.bool.filter', filters) - if (!_.isEmpty(queries.bool)) { - _.set(queryBody, 'query.bool', queries.bool) - } else if (!_.isEmpty(queries)) { - _.set(queryBody, 'query.bool.must', queries) - } - _.merge(clonedBody, filterBody, queryBody) - } else if (!_.isEmpty(queries)) { - _.set(clonedBody, 'query', queries) - } - - if (!_.isEmpty(aggregations)) { - _.set(clonedBody, 'aggs', aggregations) - } - - return clonedBody -} - module.exports = bodybuilder diff --git a/src/utils.js b/src/utils.js index 71e5dce..151c546 100644 --- a/src/utils.js +++ b/src/utils.js @@ -104,24 +104,39 @@ function unwrap (arr) { return arr.length > 1 ? arr : _.last(arr) } +const nestedTypes = ['nested', 'has_parent', 'has_child'] + export function pushQuery (existing, boolKey, type, ...args) { const nested = {} if (_.isFunction(_.last(args))) { + const isNestedType = _.includes(nestedTypes, _.snakeCase(type)) const nestedCallback = args.pop() + // It is illogical to add a query nested inside a filter, because its + // scoring won't be taken into account by elasticsearch. However we do need + // to provide the `query` methods in the context of joined queries for + // backwards compatability. const nestedResult = nestedCallback( Object.assign( {}, filterBuilder({ isInFilterContext: this.isInFilterContext }), - this.isInFilterContext + (this.isInFilterContext && !isNestedType) ? {} : queryBuilder({ isInFilterContext: this.isInFilterContext }) ) ) - if (!this.isInFilterContext && nestedResult.hasQuery()) { - nested.query = nestedResult.getQuery() - } - if (nestedResult.hasFilter()) { - nested.filter = nestedResult.getFilter() + if (isNestedType) { + nested.query = build( + {}, + nestedResult.getQuery(), + nestedResult.getFilter() + ).query + } else { + if (!this.isInFilterContext && nestedResult.hasQuery()) { + nested.must = nestedResult.getQuery() + } + if (nestedResult.hasFilter()) { + nested.filter = nestedResult.getFilter() + } } } @@ -141,3 +156,47 @@ export function pushQuery (existing, boolKey, type, ...args) { ) } } + +export function buildV1(body, queries, filters, aggregations) { + let clonedBody = _.cloneDeep(body) + + if (!_.isEmpty(filters)) { + _.set(clonedBody, 'query.filtered.filter', filters) + + if (!_.isEmpty(queries)) { + _.set(clonedBody, 'query.filtered.query', queries) + } + + } else if (!_.isEmpty(queries)) { + _.set(clonedBody, 'query', queries) + } + + if (!_.isEmpty(aggregations)) { + _.set(clonedBody, 'aggregations', aggregations) + } + return clonedBody +} + +export function build(body, queries, filters, aggregations) { + let clonedBody = _.cloneDeep(body) + + if (!_.isEmpty(filters)) { + let filterBody = {} + let queryBody = {} + _.set(filterBody, 'query.bool.filter', filters) + if (!_.isEmpty(queries.bool)) { + _.set(queryBody, 'query.bool', queries.bool) + } else if (!_.isEmpty(queries)) { + _.set(queryBody, 'query.bool.must', queries) + } + _.merge(clonedBody, filterBody, queryBody) + } else if (!_.isEmpty(queries)) { + _.set(clonedBody, 'query', queries) + } + + if (!_.isEmpty(aggregations)) { + _.set(clonedBody, 'aggs', aggregations) + } + + return clonedBody +} diff --git a/test/index.js b/test/index.js index c46993f..62cd5e6 100644 --- a/test/index.js +++ b/test/index.js @@ -335,7 +335,7 @@ test('bodyBuilder should make this chained nested query', (t) => { }) test('bodyBuilder should create this big-ass query', (t) => { - t.plan(1) + t.plan(4) const result = bodyBuilder().query('constant_score', (q) => { return q @@ -352,47 +352,60 @@ test('bodyBuilder should create this big-ass query', (t) => { }) }) - t.deepEqual(result.getQuery(), { + const firstShould = { + term: { + 'created_by.user_id': 'abc' + } + } + + const secondShould = { + nested: { + path: 'doc_meta', + query: { + constant_score: { + filter: { + term: { + 'doc_meta.user_id': 'abc' + } + } + } + } + } + } + const thirdShould = { + nested: { + path: 'tests', + query: { + constant_score: { + filter: { + term: { + 'tests.created_by.user_id': 'abc' + } + } + } + } + } + } + + const resultQuery = result.getQuery() + + t.deepEqual(resultQuery, { constant_score: { filter: { bool: { should: [ - { - term: { - 'created_by.user_id': 'abc' - } - }, { - nested: { - path: 'doc_meta', - query: { - constant_score: { - filter: { - term: { - 'doc_meta.user_id': 'abc' - } - } - } - } - } - }, { - nested: { - path: 'tests', - query: { - constant_score: { - filter: { - term: { - 'tests.created_by.user_id': 'abc' - } - } - } - } - } - } + firstShould, secondShould, thirdShould ] } } } }) + + const resultShould = resultQuery.constant_score.filter.bool.should + + t.deepEqual(resultShould[0], firstShould) + t.deepEqual(resultShould[1], secondShould) + t.deepEqual(resultShould[2], thirdShould) }) test('bodyBuilder should combine queries, filters, aggregations', (t) => { @@ -548,6 +561,51 @@ test('bodybuilder | dynamic filter', t => { }) }) +test('bodybuilder | dynamic query', t => { + t.plan(3) + + const result = bodyBuilder() + .query('constant_score', f => f.query('term', 'user', 'kimchy')) + .query('term', 'message', 'this is a test') + .build() + + t.deepEqual(result, + { + query: { + bool: { + must: [ + { + constant_score: { + must: { + term: { + user: 'kimchy' + } + } + } + }, + { term: { message: 'this is a test' } } + ] + } + } + }) + + t.deepEqual(result.query.bool.must[0], + { + constant_score: { + must: { + term: { + user: 'kimchy' + } + } + } + }) + + t.deepEqual( + result.query.bool.must[1], + { term: { message: 'this is a test' } } + ) +}) + test('bodybuilder | complex dynamic filter', t => { t.plan(3) diff --git a/test/query-builder.js b/test/query-builder.js index 361bfeb..65a7852 100644 --- a/test/query-builder.js +++ b/test/query-builder.js @@ -471,6 +471,57 @@ test('queryBuilder | has_parent', (t) => { }) }) +test('queryBuilder | hasParent (valid v2 syntax)', (t) => { + t.plan(1) + + const result = queryBuilder().query('hasParent', 'parentTag', 'blog', (q) => { + return q.query('term', 'tag', 'something') + }) + + t.deepEqual(result.getQuery(), { + hasParent: { + parentTag: 'blog', + query: { + term: { tag: 'something' } + } + } + }) +}) + +test('queryBuilder | has_parent filter v1 syntax', (t) => { + t.plan(1) + + const result = queryBuilder().query('hasParent', 'parentTag', 'blog', (q) => { + return q.filter('term', 'tag', 'something') + }) + + t.deepEqual(result.getQuery('v1'), { + hasParent: { + parentTag: 'blog', + filter: { + term: { tag: 'something' } + } + } + }) +}) + +test('queryBuilder | has_parent filter v1 syntax', (t) => { + t.plan(1) + + const result = queryBuilder().query('hasParent', 'parentTag', 'blog', (q) => { + return q.filter('term', 'tag', 'something') + }) + + t.deepEqual(result.getQuery(), { + hasParent: { + parentTag: 'blog', + query: { bool: {filter: { + term: { tag: 'something' } + } } } + } + }) +}) + test('queryBuilder | geo_bounding_box', (t) => { t.plan(1)