diff --git a/.vscode/settings.json b/.vscode/settings.json index 08d3434b..42a6a0bb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,13 @@ // Place your settings in this file to overwrite default and user settings. { "eslint.enable": true, - "eslint.autoFixOnSave": true, "files.eol": "\n", "vsicons.presets.angular": false, "editor.detectIndentation": true, "[json]": { "editor.tabSize": 2 + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" } } diff --git a/package.json b/package.json index c990dc16..3df0fdd8 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "author": "Suhas Karanth ", "contributors": [ "austin ce ", + "Julien Maitrehenry ", "ochan12 ", "kennylindahl ", "foxstarius ", diff --git a/src/core/index.js b/src/core/index.js index dcf0305c..80013bec 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -31,3 +31,5 @@ exports.SearchTemplate = require('./search-template'); exports.consts = require('./consts'); exports.util = require('./util'); + +exports.RuntimeField = require('./runtime-field'); diff --git a/src/core/request-body-search.js b/src/core/request-body-search.js index c1a90d11..e69a3c2d 100644 --- a/src/core/request-body-search.js +++ b/src/core/request-body-search.js @@ -13,6 +13,7 @@ const Query = require('./query'), InnerHits = require('./inner-hits'); const { checkType, setDefault, recursiveToJSON } = require('./util'); +const RuntimeField = require('./runtime-field'); /** * Helper function to call `recursiveToJSON` on elements of array and assign to object. @@ -406,6 +407,87 @@ class RequestBodySearch { return this; } + /** + * Computes a document property dynamically based on the supplied `runtimeField`. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-search-request.html) + * + * Added in Elasticsearch v7.11.0 + * [Release note](https://www.elastic.co/guide/en/elasticsearch/reference/7.11/release-notes-7.11.0.html) + * + * @example + * const reqBody = esb.requestBodySearch() + * .query(esb.matchAllQuery()) + * .runtimeMapping( + * 'sessionId-name', + * esb.runtimeField( + * 'keyword', + * `emit(doc['session_id'].value + '::' + doc['name'].value)` + * ) + * ) + * + * @example + * // runtime fields can also be used in query aggregation + * const reqBody = esb.requestBodySearch() + * .query(esb.matchAllQuery()) + * .runtimeMapping( + * 'sessionId-eventName', + * esb.runtimeField( + * 'keyword', + * `emit(doc['session_id'].value + '::' + doc['eventName'].value)`, + * ) + * ) + * .agg(esb.cardinalityAggregation('uniqueCount', `sessionId-eventName`)),; + * + * @param {string} runtimeFieldName Name for the computed runtime mapping field. + * @param {RuntimeField} runtimeField Instance of RuntimeField + * + * @returns {RequestBodySearch} returns `this` so that calls can be chained + * + */ + runtimeMapping(runtimeFieldName, runtimeField) { + checkType(runtimeField, RuntimeField); + + setDefault(this._body, 'runtime_mappings', {}); + this._body.runtime_mappings[runtimeFieldName] = runtimeField; + return this; + } + + /** + * Computes one or more document properties dynamically based on supplied `RuntimeField`s. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-search-request.html) + * + * Added in Elasticsearch v7.11.0 + * [Release note](https://www.elastic.co/guide/en/elasticsearch/reference/7.11/release-notes-7.11.0.html) + * + * @example + * const fieldA = esb.runtimeField( + * 'keyword', + * `emit(doc['session_id'].value + '::' + doc['name'].value)` + * ); + * const reqBody = esb.requestBodySearch() + * .query(esb.matchAllQuery()) + * .runtimeMappings({ + * 'sessionId-name': fieldA, + * }) + * + * @param {Object} runtimeMappings Object with `runtimeFieldName` as key and instance of `RuntimeField` as the value. + * @returns {RequestBodySearch} returns `this` so that calls can be chained + */ + runtimeMappings(runtimeMappings) { + checkType(runtimeMappings, Object); + + Object.keys(runtimeMappings).forEach(runtimeFieldName => + this.runtimeMapping( + runtimeFieldName, + runtimeMappings[runtimeFieldName] + ) + ); + + return this; + } + /** * Computes a document property dynamically based on the supplied `Script`. * diff --git a/src/core/runtime-field.js b/src/core/runtime-field.js new file mode 100644 index 00000000..5820d0ba --- /dev/null +++ b/src/core/runtime-field.js @@ -0,0 +1,91 @@ +'use strict'; + +const isNil = require('lodash.isnil'); +const validType = [ + 'boolean', + 'composite', + 'date', + 'double', + 'geo_point', + 'ip', + 'keyword', + 'long', + 'lookup' +]; + +/** + * Class supporting the Elasticsearch runtime field. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime.html) + * + * Added in Elasticsearch v7.11.0 + * [Release note](https://www.elastic.co/guide/en/elasticsearch/reference/7.11/release-notes-7.11.0.html) + * + * @param {string=} type One of `boolean`, `composite`, `date`, `double`, `geo_point`, `ip`, `keyword`, `long`, `lookup`. + * @param {string=} script Source of the script. + * + * @example + * const field = esb.runtimeField('keyword', `emit(doc['sessionId'].value + '::' + doc['name'].value)`); + */ +class RuntimeField { + // eslint-disable-next-line require-jsdoc + constructor(type, script) { + this._body = {}; + this._isTypeSet = false; + this._isScriptSet = false; + + if (!isNil(type)) { + this.type(type); + } + + if (!isNil(script)) { + this.script(script); + } + } + + /** + * Sets the source of the script. + * @param {string} script + * @returns {void} + */ + script(script) { + this._body.script = { + source: script + }; + this._isScriptSet = true; + } + + /** + * Sets the type of the runtime field. + * @param {string} type One of `boolean`, `composite`, `date`, `double`, `geo_point`, `ip`, `keyword`, `long`, `lookup`. + * @returns {void} + */ + type(type) { + const typeLower = type.toLowerCase(); + if (!validType.includes(typeLower)) { + throw new Error(`\`type\` must be one of ${validType.join(', ')}`); + } + this._body.type = typeLower; + this._isTypeSet = true; + } + + /** + * Override default `toJSON` to return DSL representation for the `script`. + * + * @override + * @returns {Object} returns an Object which maps to the elasticsearch query DSL + */ + toJSON() { + if (!this._isTypeSet) { + throw new Error('`type` should be set'); + } + + if (!this._isScriptSet) { + throw new Error('`script` should be set'); + } + + return this._body; + } +} + +module.exports = RuntimeField; diff --git a/src/index.d.ts b/src/index.d.ts index 606d1d49..48f7bb02 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -167,6 +167,73 @@ declare namespace esb { */ storedFields(fields: object | string): this; + + + /** + * Computes a document property dynamically based on the supplied `runtimeField`. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-search-request.html) + * + * Added in Elasticsearch v7.11.0 + * [Release note](https://www.elastic.co/guide/en/elasticsearch/reference/7.11/release-notes-7.11.0.html) + * + * @example + * const reqBody = esb.requestBodySearch() + * .query(esb.matchAllQuery()) + * .runtimeMapping( + * 'sessionId-name', + * esb.runtimeField( + * 'keyword', + * `emit(doc['session_id'].value + '::' + doc['name'].value)` + * ) + * ) + * + * @example + * // runtime fields can also be used in query aggregation + * const reqBody = esb.requestBodySearch() + * .query(esb.matchAllQuery()) + * .runtimeMapping( + * 'sessionId-eventName', + * esb.runtimeField( + * 'keyword', + * `emit(doc['session_id'].value + '::' + doc['eventName'].value)`, + * ) + * ) + * .agg(esb.cardinalityAggregation('uniqueCount', `sessionId-eventName`)),; + * + * @param {string} runtimeFieldName Name for the computed runtime mapping field. + * @param {RuntimeField} runtimeField Instance of RuntimeField + * + * @returns {RequestBodySearch} returns `this` so that calls can be chained + * + */ + runtimeMapping(runtimeFieldName: string, runtimeField: RuntimeField): this; + + + /** + * Computes one or more document properties dynamically based on supplied `RuntimeField`s. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-search-request.html) + * + * Added in Elasticsearch v7.11.0 + * [Release note](https://www.elastic.co/guide/en/elasticsearch/reference/7.11/release-notes-7.11.0.html) + * + * @example + * const fieldA = esb.runtimeField( + * 'keyword', + * `emit(doc['session_id'].value + '::' + doc['name'].value)` + * ); + * const reqBody = esb.requestBodySearch() + * .query(esb.matchAllQuery()) + * .runtimeMappings({ + * 'sessionId-name': fieldA, + * }) + * + * @param {Object} runtimeMappings Object with `runtimeFieldName` as key and instance of `RuntimeField` as the value. + * @returns {RequestBodySearch} returns `this` so that calls can be chained + */ + runtimeMappings(runtimeMappings: object): this; + /** * Computes a document property dynamically based on the supplied `Script`. * @@ -8836,6 +8903,63 @@ declare namespace esb { */ export function highlight(fields?: string | string[]): Highlight; + /** + * Class supporting the Elasticsearch runtime field. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime.html) + * + * Added in Elasticsearch v7.11.0 + * [Release note](https://www.elastic.co/guide/en/elasticsearch/reference/7.11/release-notes-7.11.0.html) + * + * @param {string=} type One of `boolean`, `composite`, `date`, `double`, `geo_point`, `ip`, `keyword`, `long`, `lookup`. + * @param {string=} script Source of the script. + * + * @example + * const field = esb.runtimeField('keyword', `emit(doc['sessionId'].value + '::' + doc['name'].value)`); + */ + export class RuntimeField { + constructor(type?: string, script?: string); + + /** + * Sets the type of the runtime field. + * + * @param {string} type One of `boolean`, `composite`, `date`, `double`, `geo_point`, `ip`, `keyword`, `long`, `lookup`. + * @returns {void} + */ + type(type: 'boolean' | 'composite' | 'date' | 'double' | 'geo_point' | 'ip' | 'keyword' | 'long' | 'lookup'); + + /** + * Sets the source of the script. + * + * @param {string} script + * @returns {void} + */ + script(script: string); + + /** + * Override default `toJSON` to return DSL representation for the `script`. + * + * @override + */ + toJSON(): object; + } + + /** + * Class supporting the Elasticsearch runtime field. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime.html) + * + * Added in Elasticsearch v7.11.0 + * [Release note](https://www.elastic.co/guide/en/elasticsearch/reference/7.11/release-notes-7.11.0.html) + * + * @param {string=} type One of `boolean`, `composite`, `date`, `double`, `geo_point`, `ip`, `keyword`, `long`, `lookup`. + * @param {string=} script Source of the script. + * + * @example + * const field = esb.runtimeField('keyword', `emit(doc['sessionId'].value + '::' + doc['name'].value)`); + */ + export function runtimeField(type?: 'boolean' | 'composite' | 'date' | 'double' | 'geo_point' | 'ip' | 'keyword' | 'long' | 'lookup', script?: string): RuntimeField; + /** * Class supporting the Elasticsearch scripting API. * diff --git a/src/index.js b/src/index.js index 6a45a593..77c49df4 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ const { Sort, Rescore, InnerHits, + RuntimeField, SearchTemplate, Query, util: { constructorWrapper } @@ -650,6 +651,9 @@ exports.innerHits = constructorWrapper(InnerHits); exports.SearchTemplate = SearchTemplate; exports.searchTemplate = constructorWrapper(SearchTemplate); +exports.RuntimeField = RuntimeField; +exports.runtimeField = constructorWrapper(RuntimeField); + exports.prettyPrint = function prettyPrint(obj) { console.log(JSON.stringify(obj, null, 2)); }; diff --git a/test/core-test/request-body-search.test.js b/test/core-test/request-body-search.test.js index a6567f04..47b11910 100644 --- a/test/core-test/request-body-search.test.js +++ b/test/core-test/request-body-search.test.js @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import test from 'ava'; import { RequestBodySearch, @@ -14,7 +15,8 @@ import { Script, Highlight, Rescore, - InnerHits + InnerHits, + RuntimeField } from '../../src'; import { illegalParamType, makeSetsOptionMacro } from '../_macros'; @@ -37,6 +39,12 @@ const suggest = new TermSuggester( const sortChannel = new Sort('channel', 'desc'); const sortCategories = new Sort('categories', 'desc'); +const runtimeFieldA = new RuntimeField('keyword', "emit(doc['name'].value)"); +const runtimeFieldB = new RuntimeField( + 'boolean', + "emit(doc['qty'].value > 10)" +); + const scriptA = new Script('inline', "doc['my_field_name'].value * 2").lang( 'painless' ); @@ -131,6 +139,38 @@ test('sets stored_fields(arr) option', setsOption, 'storedFields', { param: ['user', 'postDate'], spread: false }); + +test(setsOption, 'runtimeMapping', { + param: ['test1', runtimeFieldA], + propValue: { + test1: { + type: 'keyword', + script: { + source: "emit(doc['name'].value)" + } + } + }, + keyName: 'runtime_mappings' +}); +test(setsOption, 'runtimeMappings', { + param: { test1: runtimeFieldA, test2: runtimeFieldB }, + propValue: { + test1: { + type: 'keyword', + script: { + source: "emit(doc['name'].value)" + } + }, + test2: { + type: 'boolean', + script: { + source: "emit(doc['qty'].value > 10)" + } + } + }, + keyName: 'runtime_mappings' +}); + test(setsOption, 'scriptField', { param: ['test1', scriptA], propValue: { test1: { script: scriptA } }, diff --git a/test/core-test/runtime-field.test.js b/test/core-test/runtime-field.test.js new file mode 100644 index 00000000..4d487bb5 --- /dev/null +++ b/test/core-test/runtime-field.test.js @@ -0,0 +1,53 @@ +import test from 'ava'; +import RuntimeField from '../../src/core/runtime-field'; + +test('constructor set arguments', t => { + const valueA = new RuntimeField( + 'keyword', + "emit(doc['name'].value)" + ).toJSON(); + + const expected = { + type: 'keyword', + script: { + source: "emit(doc['name'].value)" + } + }; + t.deepEqual(valueA, expected); + + let err = t.throws(() => new RuntimeField().toJSON(), Error); + t.is(err.message, '`type` should be set'); + + err = t.throws(() => new RuntimeField('keyword').toJSON(), Error); + t.is(err.message, '`script` should be set'); +}); + +test('type validate and set argument', t => { + const fieldA = new RuntimeField('keyword', "emit(doc['name'].value)"); + fieldA.type('boolean'); + const expected = { + type: 'boolean', + script: { + source: "emit(doc['name'].value)" + } + }; + t.deepEqual(fieldA.toJSON(), expected); + + const err = t.throws(() => fieldA.type('invalid'), Error); + t.is( + err.message, + '`type` must be one of boolean, composite, date, double, geo_point, ip, keyword, long, lookup' + ); +}); + +test('script method sets script source', t => { + const fieldA = new RuntimeField('keyword'); + fieldA.script("emit(doc['name'].value)"); + const expected = { + type: 'keyword', + script: { + source: "emit(doc['name'].value)" + } + }; + t.deepEqual(fieldA.toJSON(), expected); +});