Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature generic query builder #71

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b530258
Install deps for tape test runner
danpaz Oct 25, 2016
7bf8fac
Introduce QueryBuilder as generic api for building query clauses
danpaz Oct 25, 2016
01b6ca8
WIP on query builder
msanguineti Oct 25, 2016
d6f174d
Combine aggregation and filter builders into QueryBuilder so we can n…
danpaz Oct 25, 2016
ded95da
Merge branch 'feature-generic-query-builder' of https://github.com/da…
msanguineti Oct 27, 2016
24e7ec8
fixed recurring nested queries issue
msanguineti Oct 27, 2016
1112908
ops... forgot a 'console.log()' statement... my bad
msanguineti Oct 27, 2016
c1ff27e
Merge pull request #72 from msanguineti/feature-generic-query-builder
danpaz Oct 27, 2016
aea60de
Improve object merge with Object.assign and dont overwrite field
danpaz Oct 27, 2016
56d6afb
Remove commented lines
danpaz Oct 27, 2016
79a88d8
Use _.find to get clause
danpaz Oct 27, 2016
27fa0bd
split into individual builders
johannes-scharlach Nov 6, 2016
48b7ee6
fix wrongly named variables
johannes-scharlach Nov 6, 2016
fca9377
add has convenience methods
johannes-scharlach Nov 7, 2016
6d64f96
add script for new testing strategy
johannes-scharlach Nov 7, 2016
85e5703
Merge pull request #77 from ShareIQ/feature-generic-builders
danpaz Nov 11, 2016
6c70156
Split builders into separate files
danpaz Nov 11, 2016
f980dc9
Add build method
danpaz Nov 11, 2016
695a1e9
Add build v2
danpaz Nov 11, 2016
a51fc62
Add size, from, sort, rawOption apis
danpaz Nov 11, 2016
916ab47
Remove node 0.10 and 0.12 from travis
danpaz Nov 11, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ node_js:
- '5.11'
- '4.4'
- '4.2'
- '0.12'
- '0.10'
branches:
only:
- master
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"lint": "eslint src test",
"style": "npm run lint",
"test": "mocha --require babel-core/register --recursive test",
"test-bb": "babel-tape-runner test/body-builder.js | tap-spec",
"watch:test": "mocha --watch --require babel-core/register --recursive test",
"check": "npm run lint && npm test",
"preversion": "npm run check && npm run build"
Expand All @@ -34,10 +35,14 @@
"babel-core": "6.5.1",
"babel-plugin-lodash": "2.0.1",
"babel-preset-es2015": "6.5.0",
"babel-register": "6.18.0",
"babel-tape-runner": "2.0.1",
"chai": "3.2.0",
"documentation": "4.0.0-beta10",
"eslint": "1.10.2",
"mocha": "2.4.5",
"tap-spec": "4.1.1",
"tape": "4.6.2",
"webpack": "1.12.13"
},
"dependencies": {
Expand Down
52 changes: 52 additions & 0 deletions src/aggregation-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import _ from 'lodash'
import { buildAggregation } from './utils'
import filterBuilder from './filter-builder'

export default function aggregationBuilder () {
let aggregations = {}

function makeAggregation (type, field, ...args) {
const aggName = _.find(args, _.isString) || `agg_${type}_${field}`
const opts = _.find(args, _.isPlainObject) || {}
const nested = _.find(args, _.isFunction)
const nestedClause = {}

if (_.isFunction(nested)) {
const nestedResult = nested(Object.assign(
{},
aggregationBuilder(),
filterBuilder()
))
if (nestedResult.hasFilter()) {
nestedClause.filter = nestedResult.getFilter()
}
if (nestedResult.hasAggregations()) {
nestedClause.aggs = nestedResult.getAggregations()
}
}

Object.assign(
aggregations,
{[aggName]: Object.assign(
buildAggregation(type, field, opts),
nestedClause
)}
)
}

return {
aggregation (...args) {
makeAggregation(...args)
return this
},
agg (...args) {
return this.aggregation(...args)
},
getAggregations () {
return aggregations
},
hasAggregations () {
return !!_.size(aggregations)
}
}
}
137 changes: 137 additions & 0 deletions src/body-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import _ from 'lodash'
import queryBuilder from './query-builder'
import filterBuilder from './filter-builder'
import aggregationBuilder from './aggregation-builder'
import { sortMerge } from './utils'

export default function bodyBuilder () {
let body = {}

return Object.assign(
{
/**
* Set a sort direction on a given field.
*
* @param {String} field Field name.
* @param {String} [direction='asc'] A valid direction: 'asc' or 'desc'.
* @returns {Bodybuilder} Builder class.
*/
sort(field, direction = 'asc') {
body.sort = body.sort || []

if (_.isArray(field)) {

if(_.isPlainObject(body.sort)) {
body.sort = [body.sort]
}

if(_.isArray(body.sort)) {
_.each(field, (sorts) => {
_.each(sorts, (value, key) => {
sortMerge(body.sort, key, value)
})
})
}
} else {
sortMerge(body.sort, field, direction)
}
return this
},

/**
* Set a *from* offset value, for paginating a query.
*
* @param {Number} quantity The offset from the first result you want to
* fetch.
* @returns {Bodybuilder} Builder class.
*/
from(quantity) {
body.from = quantity
return this
},

/**
* Set a *size* value for maximum results to return.
*
* @param {Number} quantity Maximum number of results to return.
* @returns {Bodybuilder} Builder class.
*/
size(quantity) {
body.size = quantity
return this
},

/**
* Set any key-value on the elasticsearch body.
*
* @param {String} k Key.
* @param {String} v Value.
* @returns {Bodybuilder} Builder class.
*/
rawOption(k, v) {
body[k] = v
return this
},

build(version) {
const queries = this.getQuery()
const filters = this.getFilter()
const aggregations = this.getAggregations()

if (version === 'v2') {
return _buildV2(body, queries, filters, aggregations)
}

return _buildV1(body, queries, filters, aggregations)
}

},
queryBuilder(),
filterBuilder(),
aggregationBuilder()
)
}

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 _buildV2(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
}
65 changes: 65 additions & 0 deletions src/filter-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import _ from 'lodash'
import { boolMerge, buildClause } from './utils'
import queryBuilder from './query-builder'
import aggregationBuilder from './aggregation-builder'

export default function filterBuilder () {
let filter = {}

function makeFilter (boolType, filterType, ...args) {
const nested = {}
if (_.isFunction(_.last(args))) {
const nestedCallback = args.pop()
const nestedResult = nestedCallback(
Object.assign(
{},
queryBuilder(),
filterBuilder(),
aggregationBuilder()
)
)
if (nestedResult.hasQuery()) {
nested.query = nestedResult.getQuery()
}
if (nestedResult.hasFilter()) {
nested.filter = nestedResult.getFilter()
}
if (nestedResult.hasAggregations()) {
nested.aggs = nestedResult.getAggregations()
}
}

filter = boolMerge(
{[filterType]: Object.assign(buildClause(...args), nested)},
filter,
boolType
)
}

return {
filter (...args) {
makeFilter('and', ...args)
return this
},
andFilter (...args) {
return this.filter(...args)
},
addFilter (...args) {
return this.filter(...args)
},
orFilter (...args) {
makeFilter('or', ...args)
return this
},
notFilter (...args) {
makeFilter('not', ...args)
return this
},
getFilter () {
return filter
},
hasFilter () {
return !!_.size(filter)
}
}
}
60 changes: 60 additions & 0 deletions src/query-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import _ from 'lodash'
import { boolMerge, buildClause } from './utils'
import filterBuilder from './filter-builder'

export default function queryBuilder () {
let query = {}

function makeQuery (boolType, queryType, ...args) {
const nested = {}
if (_.isFunction(_.last(args))) {
const nestedCallback = args.pop()
const nestedResult = nestedCallback(
Object.assign(
{},
queryBuilder(),
filterBuilder()
)
)
if (nestedResult.hasQuery()) {
nested.query = nestedResult.getQuery()
}
if (nestedResult.hasFilter()) {
nested.filter = nestedResult.getFilter()
}
}

query = boolMerge(
{[queryType]: Object.assign(buildClause(...args), nested)},
query,
boolType
)
}

return {
query (...args) {
makeQuery('and', ...args)
return this
},
andQuery (...args) {
return this.query(...args)
},
addQuery (...args) {
return this.query(...args)
},
orQuery (...args) {
makeQuery('or', ...args)
return this
},
notQuery (...args) {
makeQuery('not', ...args)
return this
},
getQuery () {
return query
},
hasQuery () {
return !!_.size(query)
}
}
}
38 changes: 33 additions & 5 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import queries from './queries'
* @param {Object} target Target.
* @returns {Object} Merged object.
*/
export function mergeConcat(target) {
let args = Array.prototype.slice.call(arguments, 1)

args.unshift(target)
export function mergeConcat() {
let args = Array.prototype.slice.call(arguments, 0)
args.push(function customizer(a, b) {
if (_.isPlainObject(a)) {
return _.assignWith(a, b, customizer)
Expand Down Expand Up @@ -80,4 +78,34 @@ export function sortMerge(current, field, direction) {
}

return current
}
}

/**
* Generic builder for filter and query clauses
*
* @private
*
* @param {string} field Field name.
* @param {any} value Field value.
* @param {Object} opts Additional key-value pairs.
* @return {Object} query clause component
*/
export function buildClause (field, value, opts) {
const clause = {}

if (field && value && opts) {
Object.assign(clause, opts, {[field]: value})
} else if (field && value) {
clause[field] = value
} else if (field) {
clause.field = field
}

return clause
}

export function buildAggregation (type, field, opts) {
return {
[type]: Object.assign({field}, opts)
}
}
Loading