Refactored code to remove deepmerge dependency and fixed the query builder
This commit is contained in:
@ -31,6 +31,9 @@ import * as t from './types'
|
|||||||
const kState = Symbol('dsl-query-state')
|
const kState = Symbol('dsl-query-state')
|
||||||
type nestedQFn = (f: FluentQ) => FluentQ
|
type nestedQFn = (f: FluentQ) => FluentQ
|
||||||
|
|
||||||
|
// TODO: the client should detect a fluent query
|
||||||
|
// and automatically call `query.build()`
|
||||||
|
|
||||||
class FluentQ {
|
class FluentQ {
|
||||||
[kState]: Record<string, any>[]
|
[kState]: Record<string, any>[]
|
||||||
constructor () {
|
constructor () {
|
||||||
@ -265,4 +268,6 @@ class FluentQ {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FluentQ
|
export default function build () {
|
||||||
|
return new FluentQ()
|
||||||
|
}
|
||||||
|
|||||||
165
dsl/src/query.ts
165
dsl/src/query.ts
@ -23,7 +23,6 @@
|
|||||||
/* eslint no-redeclare: 0 */
|
/* eslint no-redeclare: 0 */
|
||||||
/* eslint no-inner-declarations: 0 */
|
/* eslint no-inner-declarations: 0 */
|
||||||
|
|
||||||
import deepMerge from 'deepmerge'
|
|
||||||
import * as t from './types'
|
import * as t from './types'
|
||||||
|
|
||||||
function Q (...blocks: t.AnyQuery[]): Record<string, any> {
|
function Q (...blocks: t.AnyQuery[]): Record<string, any> {
|
||||||
@ -53,13 +52,14 @@ function Q (...blocks: t.AnyQuery[]): Record<string, any> {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const queries = blocks.filter(block => !topLevelKeys.includes(Object.keys(block)[0]))
|
const queries = blocks.filter(block => !topLevelKeys.includes(Object.keys(block)[0]))
|
||||||
const body: Record<string, any> = queries.length === 1 && !isClause(queries[0]) && !isBool(queries[0])
|
const body: Record<string, any> = queries.length === 1 && !isClause(queries[0])
|
||||||
? { query: queries[0] }
|
? isQuery(queries[0]) ? queries[0] : { query: queries[0] }
|
||||||
: queries.length > 0 ? Q.bool(...queries) : {}
|
: queries.length > 0 ? Q.bool(...queries) : {}
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
const key = Object.keys(block)[0]
|
const key = Object.keys(block)[0]
|
||||||
if (topLevelKeys.includes(key)) {
|
if (topLevelKeys.includes(key)) {
|
||||||
body[key] = (block as Record<string, any>)[key]
|
// @ts-expect-error
|
||||||
|
body[key] = block[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +278,7 @@ namespace Q {
|
|||||||
return { filter: queries.flatMap(mergeableFilter) }
|
return { filter: queries.flatMap(mergeableFilter) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bool (...queries: t.AnyQuery[]): t.BoolQuery {
|
export function bool (...queries: t.AnyBoolQuery[]): t.BoolQuery {
|
||||||
if (queries.length === 0) {
|
if (queries.length === 0) {
|
||||||
return { query: { bool: {} } }
|
return { query: { bool: {} } }
|
||||||
}
|
}
|
||||||
@ -297,9 +297,22 @@ namespace Q {
|
|||||||
if (q.query.bool._name) {
|
if (q.query.bool._name) {
|
||||||
return { must: [q.query] }
|
return { must: [q.query] }
|
||||||
}
|
}
|
||||||
|
if (q.query.bool.minimum_should_match) {
|
||||||
|
return { must: [q.query] }
|
||||||
|
}
|
||||||
return q.query.bool
|
return q.query.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isBoolBlock(q)) {
|
||||||
|
if (q.bool._name) {
|
||||||
|
return { must: [q] }
|
||||||
|
}
|
||||||
|
if (q.bool.minimum_should_match) {
|
||||||
|
return { must: [q] }
|
||||||
|
}
|
||||||
|
return q.bool
|
||||||
|
}
|
||||||
|
|
||||||
if (isClause(q)) {
|
if (isClause(q)) {
|
||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
@ -307,33 +320,50 @@ namespace Q {
|
|||||||
return { must: [q] }
|
return { must: [q] }
|
||||||
})
|
})
|
||||||
|
|
||||||
const clauseCount = {
|
const mustClauses: t.AnyQuery[] = []
|
||||||
must: 0,
|
const mustNotClauses: t.AnyQuery[] = []
|
||||||
should: 0,
|
const shouldClauses: t.AnyQuery[] = []
|
||||||
must_not: 0,
|
const filterClauses: t.AnyQuery[] = []
|
||||||
filter: 0
|
let minimum_should_match: number | null = null
|
||||||
}
|
let _name: string | null = null
|
||||||
for (let i = 0; i < normalizedQueries.length; i++) {
|
|
||||||
const q = normalizedQueries[i]
|
|
||||||
if (q.must !== undefined) { clauseCount.must++ }
|
|
||||||
if (q.should !== undefined) { clauseCount.should++ }
|
|
||||||
if (q.must_not !== undefined) { clauseCount.must_not++ }
|
|
||||||
if (q.filter !== undefined) { clauseCount.filter++ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is at least one should, we cannot deep merge
|
for (const query of normalizedQueries) {
|
||||||
// multiple clauses, so we check how many clauses we have per type
|
if (query.must) {
|
||||||
// and we throw an error if there is more than one per type
|
mustClauses.push(query.must)
|
||||||
if (clauseCount.should > 0) {
|
}
|
||||||
if (clauseCount.must > 1 || clauseCount.must_not > 1 || clauseCount.filter > 1) {
|
if (query.must_not) {
|
||||||
throw new Error('Cannot merge this query')
|
mustNotClauses.push(query.must_not)
|
||||||
|
}
|
||||||
|
if (query.should) {
|
||||||
|
shouldClauses.push(query.should)
|
||||||
|
}
|
||||||
|
if (query.filter) {
|
||||||
|
filterClauses.push(query.filter)
|
||||||
|
}
|
||||||
|
if (query._name) {
|
||||||
|
if (_name !== null) {
|
||||||
|
throw new Error('The query name has already been defined')
|
||||||
|
}
|
||||||
|
_name = query._name
|
||||||
|
}
|
||||||
|
if (query.minimum_should_match) {
|
||||||
|
if (minimum_should_match !== null) {
|
||||||
|
throw new Error('minimum_should_match has already been defined')
|
||||||
|
}
|
||||||
|
minimum_should_match = query.minimum_should_match
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool: t.BoolQueryOptions = deepMerge.all(normalizedQueries)
|
const bool: t.BoolQueryOptions = {
|
||||||
|
...(mustClauses.length && Q.must(...mustClauses)),
|
||||||
|
...(mustNotClauses.length && Q.mustNot(...mustNotClauses)),
|
||||||
|
...(shouldClauses.length && Q.should(...shouldClauses)),
|
||||||
|
...(filterClauses.length && Q.filter(...filterClauses))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_name) bool._name = _name
|
||||||
|
if (minimum_should_match) bool.minimum_should_match = minimum_should_match
|
||||||
|
|
||||||
// if there are not should clauses,
|
|
||||||
// we can safely deepmerge queries
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
bool: optimize(bool)
|
bool: optimize(bool)
|
||||||
@ -341,33 +371,44 @@ namespace Q {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function and (...queries: t.AnyQuery[]): t.BoolQuery {
|
export function and (...queries: t.AnyBoolQuery[]): t.BoolQuery {
|
||||||
let query = queries[0]
|
let query = queries[0]
|
||||||
for (let i = 1; i < queries.length; i++) {
|
for (let i = 1; i < queries.length; i++) {
|
||||||
query = andOp(query, queries[i])
|
query = andOp(query, queries[i])
|
||||||
}
|
}
|
||||||
return query as t.BoolQuery
|
return { query: toBoolBlock(query) }
|
||||||
|
|
||||||
function andOp (q1: t.AnyQuery, q2: t.AnyQuery): t.BoolQuery {
|
function andOp (q1: t.AnyBoolQuery, q2: t.AnyBoolQuery): t.BoolBlock {
|
||||||
const b1: t.BoolQuery = toMustQuery(q1)
|
const b1: t.BoolBlock = toBoolBlock(q1)
|
||||||
const b2: t.BoolQuery = toMustQuery(q2)
|
const b2: t.BoolBlock = toBoolBlock(q2)
|
||||||
if (!onlyShould(b1.query.bool) && !onlyShould(b2.query.bool)) {
|
if (!onlyShould(b1.bool) && !onlyShould(b2.bool)) {
|
||||||
return deepMerge(b1, b2)
|
const mustClauses: t.AnyQuery[] = (b1.bool.must || []).concat(b2.bool.must || [])
|
||||||
|
const mustNotClauses: t.AnyQuery[] = (b1.bool.must_not || []).concat(b2.bool.must_not || [])
|
||||||
|
const filterClauses: t.AnyQuery[] = (b1.bool.filter || []).concat(b2.bool.filter || [])
|
||||||
|
return {
|
||||||
|
bool: {
|
||||||
|
...(mustClauses.length && Q.must(...mustClauses)),
|
||||||
|
...(mustNotClauses.length && Q.mustNot(...mustNotClauses)),
|
||||||
|
...(filterClauses.length && Q.filter(...filterClauses))
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const { must, ...clauses } = b1.query.bool
|
const { must, ...clauses } = b1.bool
|
||||||
return Q.bool(
|
return {
|
||||||
must == null ? Q.must(b2) : Q.must(must, b2),
|
bool: {
|
||||||
clauses
|
...(must == null ? Q.must(b2) : Q.must(must, b2)),
|
||||||
)
|
...clauses
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function or (...queries: t.AnyQuery[]): t.BoolQuery {
|
export function or (...queries: t.AnyBoolQuery[]): t.BoolQuery {
|
||||||
return Q.bool(Q.should(...queries))
|
return Q.bool(Q.should(...queries))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function not (q: t.AnyQuery): t.BoolQuery {
|
export function not (q: t.AnyBoolQuery): t.BoolQuery {
|
||||||
if (!isBool(q) && !isClause(q)) {
|
if (!isBool(q) && !isClause(q)) {
|
||||||
return Q.bool(Q.mustNot(q))
|
return Q.bool(Q.mustNot(q))
|
||||||
}
|
}
|
||||||
@ -584,6 +625,10 @@ function generateValueObject (queryType: string, key: string, val: any, opts?: R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isQuery (q: any): q is t.QueryBlock {
|
||||||
|
return !!q.query
|
||||||
|
}
|
||||||
|
|
||||||
function isBool (q: any): q is t.BoolQuery {
|
function isBool (q: any): q is t.BoolQuery {
|
||||||
return q.query && q.query.bool
|
return q.query && q.query.bool
|
||||||
}
|
}
|
||||||
@ -638,20 +683,24 @@ function onlyFilter (bool: t.BoolQueryOptions): bool is t.FilterClause {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// for a given query it always return a bool query:
|
// for a given query it always return a bool block:
|
||||||
// - if is a bool query returns the query
|
// - if is a bool query returns the bool block
|
||||||
// - if is a clause, wraps the query in a bool block
|
// - if is a clause, wraps the query in a bool block
|
||||||
// - if is condition, wraps the query into a must clause and then in a bool block
|
// - if is condition, wraps the query into a must clause and then in a bool block
|
||||||
function toMustQuery (query: t.AnyQuery): t.BoolQuery {
|
function toBoolBlock (query: t.AnyBoolQuery): t.BoolBlock {
|
||||||
if (isBool(query)) {
|
if (isBool(query)) {
|
||||||
|
return query.query
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBoolBlock(query)) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isClause(query)) {
|
if (isClause(query)) {
|
||||||
return { query: { bool: query } }
|
return { bool: query }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { query: { bool: { must: [query] } } }
|
return { bool: { must: [query] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// the aim of this mergeable functions
|
// the aim of this mergeable functions
|
||||||
@ -666,6 +715,12 @@ function mergeableMust (q: t.AnyQuery): t.AnyQuery | t.AnyQuery[] {
|
|||||||
} else {
|
} else {
|
||||||
return q.query
|
return q.query
|
||||||
}
|
}
|
||||||
|
} else if (isBoolBlock(q)) {
|
||||||
|
if (onlyMust(q.bool)) {
|
||||||
|
return q.bool.must
|
||||||
|
} else {
|
||||||
|
return q
|
||||||
|
}
|
||||||
} else if (isClause(q)) {
|
} else if (isClause(q)) {
|
||||||
if (onlyMust(q)) {
|
if (onlyMust(q)) {
|
||||||
return q.must
|
return q.must
|
||||||
@ -687,6 +742,12 @@ function mergeableShould (q: t.AnyQuery): t.AnyQuery | t.AnyQuery[] {
|
|||||||
} else {
|
} else {
|
||||||
return q.query
|
return q.query
|
||||||
}
|
}
|
||||||
|
} else if (isBoolBlock(q)) {
|
||||||
|
if (onlyShould(q.bool)) {
|
||||||
|
return q.bool.should
|
||||||
|
} else {
|
||||||
|
return q
|
||||||
|
}
|
||||||
} else if (isClause(q)) {
|
} else if (isClause(q)) {
|
||||||
if (onlyShould(q)) {
|
if (onlyShould(q)) {
|
||||||
return q.should
|
return q.should
|
||||||
@ -708,6 +769,12 @@ function mergeableMustNot (q: t.AnyQuery): t.AnyQuery | t.AnyQuery[] {
|
|||||||
} else {
|
} else {
|
||||||
return q.query
|
return q.query
|
||||||
}
|
}
|
||||||
|
} else if (isBoolBlock(q)) {
|
||||||
|
if (onlyMustNot(q.bool)) {
|
||||||
|
return q.bool.must_not
|
||||||
|
} else {
|
||||||
|
return q
|
||||||
|
}
|
||||||
} else if (isClause(q)) {
|
} else if (isClause(q)) {
|
||||||
if (onlyMustNot(q)) {
|
if (onlyMustNot(q)) {
|
||||||
return q.must_not
|
return q.must_not
|
||||||
@ -729,6 +796,12 @@ function mergeableFilter (q: t.AnyQuery): t.AnyQuery | t.AnyQuery[] {
|
|||||||
} else {
|
} else {
|
||||||
return q.query
|
return q.query
|
||||||
}
|
}
|
||||||
|
} else if (isBoolBlock(q)) {
|
||||||
|
if (onlyFilter(q.bool)) {
|
||||||
|
return q.bool.filter
|
||||||
|
} else {
|
||||||
|
return q
|
||||||
|
}
|
||||||
} else if (isClause(q)) {
|
} else if (isClause(q)) {
|
||||||
if (onlyFilter(q)) {
|
if (onlyFilter(q)) {
|
||||||
return q.filter
|
return q.filter
|
||||||
|
|||||||
@ -20,14 +20,10 @@
|
|||||||
/* eslint camelcase: 0 */
|
/* eslint camelcase: 0 */
|
||||||
/* eslint no-use-before-define: 0 */
|
/* eslint no-use-before-define: 0 */
|
||||||
|
|
||||||
export interface Condition {
|
export type Condition = Record<string, any>
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryBlock {
|
export interface QueryBlock {
|
||||||
query: {
|
query: Record<string, any>
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MustClause {
|
export interface MustClause {
|
||||||
@ -66,10 +62,10 @@ export interface BoolQueryOptions {
|
|||||||
_name?: string
|
_name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnyQuery = BoolQuery | BoolQueryOptions | Condition | Condition[]
|
export type AnyQuery = QueryBlock | BoolQueryOptions | Condition | Condition[]
|
||||||
|
|
||||||
export interface Aggregation {
|
export type AnyBoolQuery = BoolQuery | BoolQueryOptions | Condition | Condition[]
|
||||||
[key: string]: any
|
|
||||||
}
|
export type Aggregation = Record<string, any>
|
||||||
|
|
||||||
export type compiledFunction<TInput> = (input: TInput) => Record<string, any>;
|
export type compiledFunction<TInput> = (input: TInput) => Record<string, any>;
|
||||||
|
|||||||
@ -83,7 +83,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"decompress-response": "^4.2.0",
|
"decompress-response": "^4.2.0",
|
||||||
"deepmerge": "^4.2.2",
|
|
||||||
"hpagent": "^0.1.1",
|
"hpagent": "^0.1.1",
|
||||||
"ms": "^2.1.1",
|
"ms": "^2.1.1",
|
||||||
"pump": "^3.0.0",
|
"pump": "^3.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user