diff --git a/README.md b/README.md index c81a4ab0..986f3748 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ or [`bodybuilder`](https://github.com/danpaz/bodybuilder) Although there were [breaking changes](https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_60_search_changes.html), all deprecated queries, features in 5.0 were avoided or not implemented. +What's Included: + * [Request body search](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html) + * [Queries](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html) + * [Aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) + * [Search Template](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html) + ## Install ``` npm install elastic-builder --save diff --git a/docs/documentation.yml b/docs/documentation.yml index e961c47c..222da0df 100644 --- a/docs/documentation.yml +++ b/docs/documentation.yml @@ -92,6 +92,7 @@ toc: - ValueCountAggregation - name: Bucket Aggregations - BucketAggregationBase + - AdjacencyMatrixAggregation - ChildrenAggregation - HistogramAggregationBase - DateHistogramAggregation @@ -146,3 +147,4 @@ toc: - Sort - Rescore - InnerHits + - SearchTemplate diff --git a/index.d.ts b/index.d.ts index 5a9fff88..e6d23a0a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7034,6 +7034,87 @@ export class InnerHits { */ export function innerHits(name?: string): InnerHits; +/** + * Class supporting the Elasticsearch search template API. + * + * The `/_search/template` endpoint allows to use the mustache language to + * pre render search requests, before they are executed and fill existing + * templates with template parameters. + * + * @param {string=} type One of `inline`, `id`, `file`. `id` is also + * aliased as `indexed` + * @param {string|Object=} source Source of the search template. + * This needs to be specified if optional argument `type` is passed. + */ +export class SearchTemplate { + constructor(type?: string, source?: string | object); + + /** + * Sets the type of search template to be `inline` and specifies the query. + * + * @param {string|Query} query Either a `Query` object or a string. + */ + inline(query: string | object): this; + + /** + * Specify the indexed search template by `templateName` which will be + * retrieved from cluster state. + * + * @param {string} templId The unique identifier for the indexed template + */ + id(templId: string): this; + + /** + * Specify the indexed search template by `templateName` which will be + * retrieved from cluster state. + * + * Alias for `SearchTemplate.id` + * + * @param {string} templId The unique identifier for the indexed template + */ + indexed(templId: string): this; + + /** + * Specify the search template by filename stored in the scripts folder, + * with `mustache` extension. + * + * @param {string} fileName The name of the search template stored as a file + * in the scripts folder. + * For file `config/scripts/storedTemplate.mustache`, + * `fileName` should be `storedTemplate` + */ + file(fileName: string): this; + + /** + * Specifies any named parameters that are used to render the search template. + * + * @param {Object} params Named parameters to be used for rendering. + */ + params(params: object): this; + + /** + * Override default `toJSON` to return DSL representation for the Search Template. + * + * @override + * @returns {Object} returns an Object which maps to the elasticsearch query DSL + */ + toJSON(): object; +} + +/** + * Class supporting the Elasticsearch search template API. + * + * The `/_search/template` endpoint allows to use the mustache language to + * pre render search requests, before they are executed and fill existing + * templates with template parameters. + * + * @param {string=} type One of `inline`, `id`, `file`. `id` is also + * aliased as `indexed` + * @param {string|Object=} source Source of the search template. + * This needs to be specified if optional argument `type` is passed. + */ +export function searchTemplate(type?: string, source?: string | object): SearchTemplate; + export namespace recipes { /** * Recipe for the now removed `missing` query. diff --git a/src/core/index.js b/src/core/index.js index e16e3cc3..c4b4d398 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -24,6 +24,8 @@ exports.Rescore = require('./rescore'); exports.InnerHits = require('./inner-hits'); +exports.SearchTemplate = require('./search-template'); + exports.consts = require('./consts'); exports.util = require('./util'); diff --git a/src/core/search-template.js b/src/core/search-template.js new file mode 100644 index 00000000..6cad072a --- /dev/null +++ b/src/core/search-template.js @@ -0,0 +1,195 @@ +'use strict'; + +const isNil = require('lodash.isnil'); + +const { recursiveToJSON } = require('./util'); + +/** + * Class supporting the Elasticsearch search template API. + * + * The `/_search/template` endpoint allows to use the mustache language to + * pre render search requests, before they are executed and fill existing + * templates with template parameters. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html) + * + * @param {string=} type One of `inline`, `id`, `file`. `id` is also + * aliased as `indexed` + * @param {string|Object=} source Source of the search template. + * This needs to be specified if optional argument `type` is passed. + * + * @example + * const templ = bob.searchTemplate('inline', { + * query: bob.matchQuery('{{my_field}}', '{{my_value}}'), + * size: '{{my_size}}' + * }).params({ + * my_field: 'message', + * my_value: 'some message', + * my_size: 5 + * }); + * + * @example + * const templ = new bob.SearchTemplate( + * 'inline', + * '{ "query": { "terms": {{#toJson}}statuses{{/toJson}} }}' + * ).params({ + * statuses: { + * status: ['pending', 'published'] + * } + * }); + * + * @example + * const templ = new bob.SearchTemplate( + * 'inline', + * '{ "query": { "bool": { "must": {{#toJson}}clauses{{/toJson}} } } }' + * ).params({ + * clauses: [ + * bob.termQuery('user', 'boo'), + * bob.termQuery('user', 'bar'), + * bob.termQuery('user', 'baz') + * ] + * }); + */ +class SearchTemplate { + // eslint-disable-next-line require-jsdoc + constructor(type, source) { + this._isTypeSet = false; + this._body = {}; + + if (!isNil(type) && !isNil(source)) { + const typeLower = type.toLowerCase(); + + if ( + typeLower !== 'inline' && + typeLower !== 'id' && + typeLower !== 'indexed' && // alias for id + typeLower !== 'file' + ) { + throw new Error('`type` must be one of `inline`, `id`, `indexed`, `file`'); + } + + this[typeLower](source); + } + } + + /** + * Print warning message to console namespaced by class name. + * + * @param {string} msg + * @private + */ + _warn(msg) { + console.warn(`[SearchTemplate] ${msg}`); + } + + /** + * Print warning messages to not mix `SearchTemplate` source + * + * @private + */ + _checkMixedRepr() { + if (this._isTypeSet) { + this._warn('Search template source(`inline`/`id`/`file`) was already specified!'); + this._warn('Overwriting.'); + + delete this._body.file; + delete this._body.id; + delete this._body.file; + } + } + + /** + * Helper method to set the type and source + * + * @param {string} type + * @param {*} source + * @returns {SearchTemplate} returns `this` so that calls can be chained. + * @private + */ + _setSource(type, source) { + this._checkMixedRepr(); + + this._body[type] = source; + this._isTypeSet = true; + return this; + } + + /** + * Sets the type of search template to be `inline` and specifies the + * template with `query` and other optional fields such as `size`. + * + * @param {string|Object} templ Either an object or a string. + * @returns {SearchTemplate} returns `this` so that calls can be chained. + */ + inline(templ) { + return this._setSource('inline', templ); + } + + /** + * Specify the indexed search template by `templateName` which will be + * retrieved from cluster state. + * + * @param {string} templId The unique identifier for the indexed template. + * @returns {SearchTemplate} returns `this` so that calls can be chained. + */ + id(templId) { + return this._setSource('id', templId); + } + + /** + * Specify the indexed search template by `templateName` which will be + * retrieved from cluster state. + * + * Alias for `SearchTemplate.id` + * + * @param {string} templId The unique identifier for the indexed template. + * @returns {SearchTemplate} returns `this` so that calls can be chained. + */ + indexed(templId) { + return this.id(templId); + } + + /** + * Specify the search template by filename stored in the scripts folder, + * with `mustache` extension. + * + * @example + * // `templId` - Name of the query template in config/scripts/, i.e., + * // storedTemplate.mustache. + * const templ = new bob.SearchTemplate('file', 'storedTemplate').params({ + * query_string: 'search for these words' + * }); + * + * @param {string} fileName The name of the search template stored as a file + * in the scripts folder. + * For file `config/scripts/storedTemplate.mustache`, + * `fileName` should be `storedTemplate` + * @returns {SearchTemplate} returns `this` so that calls can be chained. + */ + file(fileName) { + return this._setSource('file', fileName); + } + + /** + * Specifies any named parameters that are used to render the search template. + * + * @param {Object} params Named parameters to be used for rendering. + * @returns {SearchTemplate} returns `this` so that calls can be chained. + */ + params(params) { + this._body.params = params; + return this; + } + + /** + * Override default `toJSON` to return DSL representation for the Search Template. + * + * @override + * @returns {Object} returns an Object which maps to the elasticsearch query DSL + */ + toJSON() { + return recursiveToJSON(this._body); + } +} + +module.exports = SearchTemplate; diff --git a/src/index.js b/src/index.js index 912d401d..8a870d6c 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ const { Sort, Rescore, InnerHits, + SearchTemplate, util: { constructorWrapper } } = require('./core'); @@ -513,6 +514,9 @@ exports.rescore = constructorWrapper(Rescore); exports.InnerHits = InnerHits; exports.innerHits = constructorWrapper(InnerHits); +exports.SearchTemplate = SearchTemplate; +exports.searchTemplate = constructorWrapper(SearchTemplate); + exports.prettyPrint = function prettyPrint(obj) { console.log(JSON.stringify(obj, null, 2)); }; diff --git a/test/core-test/search-template.test.js b/test/core-test/search-template.test.js new file mode 100644 index 00000000..78f83ceb --- /dev/null +++ b/test/core-test/search-template.test.js @@ -0,0 +1,97 @@ +import test from 'ava'; +import sinon from 'sinon'; +import { SearchTemplate, searchTemplate, matchQuery, termQuery } from '../../src'; +import { makeSetsOptionMacro } from '../_macros'; + +const setsOption = makeSetsOptionMacro(searchTemplate); + +test(setsOption, 'inline', { + param: { + query: matchQuery('{{my_field}}', '{{my_value}}'), + size: '{{my_size}}' + } +}); +test(setsOption, 'file', { param: 'storedTemplate' }); +test(setsOption, 'id', { param: 'indexedTemplate' }); +test(setsOption, 'indexed', { param: 'indexedTemplate', keyName: 'id' }); +test(setsOption, 'params', { + param: { + my_field: 'message', + my_value: 'some message', + my_size: 5 + } +}); + +test('constructor sets arguments', t => { + let valueA = new SearchTemplate( + 'inline', + '{ "query": { "terms": {{#toJson}}statuses{{/toJson}} }}' + ).toJSON(); + let valueB = new SearchTemplate() + .inline('{ "query": { "terms": {{#toJson}}statuses{{/toJson}} }}') + .toJSON(); + t.deepEqual(valueA, valueB); + + let expected = { + inline: '{ "query": { "terms": {{#toJson}}statuses{{/toJson}} }}' + }; + t.deepEqual(valueA, expected); + + valueA = new SearchTemplate('file', 'storedTemplate').toJSON(); + valueB = new SearchTemplate().file('storedTemplate').toJSON(); + t.deepEqual(valueA, valueB); + + expected = { + file: 'storedTemplate' + }; + t.deepEqual(valueA, expected); + + valueA = new SearchTemplate('id', 'indexedTemplate').toJSON(); + valueB = new SearchTemplate().id('indexedTemplate').toJSON(); + t.deepEqual(valueA, valueB); + + expected = { + id: 'indexedTemplate' + }; + t.deepEqual(valueA, expected); + + const err = t.throws(() => new SearchTemplate('invalid_script_type', 'src'), Error); + t.is(err.message, '`type` must be one of `inline`, `id`, `indexed`, `file`'); +}); + +test.serial('mixed representaion', t => { + const spy = sinon.spy(console, 'warn'); + + const value = new SearchTemplate().file('storedTemplate').id('indexedTemplate').toJSON(); + const expected = { + id: 'indexedTemplate' + }; + t.deepEqual(value, expected); + + t.true(spy.calledTwice); + t.true( + spy.firstCall.calledWith( + '[SearchTemplate] Search template source(`inline`/`id`/`file`) was already specified!' + ) + ); + t.true(spy.secondCall.calledWith('[SearchTemplate] Overwriting.')); + console.warn.restore(); +}); + +test('toJSON can handle elastic-builder objs', t => { + const value = new SearchTemplate( + 'inline', + '{ "query": { "bool": { "must": {{#toJson}}clauses{{/toJson}} } } }' + ) + .params({ + clauses: [termQuery('user', 'foo'), termQuery('user', 'bar')] + }) + .toJSON(); + const expected = { + inline: '{ "query": { "bool": { "must": {{#toJson}}clauses{{/toJson}} } } }', + params: { + clauses: [{ term: { user: 'foo' } }, { term: { user: 'bar' } }] + } + }; + t.deepEqual(value, expected); +}); diff --git a/test/index.test.js b/test/index.test.js index c7532303..f19668dc 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -375,6 +375,9 @@ test('misc are exported', t => { t.truthy(bob.InnerHits); t.truthy(bob.innerHits); + t.truthy(bob.SearchTemplate); + t.truthy(bob.searchTemplate); + t.truthy(bob.prettyPrint); });