From 42f582231bc3040a6a40faea12fdb9bf861ba467 Mon Sep 17 00:00:00 2001 From: nodkz Date: Wed, 15 Mar 2017 22:15:46 +0600 Subject: [PATCH] feat(Aggs): Start describing body.aggs input for `searchTyped` --- src/ElasticDSL/Aggs/AggBlock.js | 33 +++++++++++ src/ElasticDSL/Aggs/AggRules.js | 38 +++++++++++++ src/ElasticDSL/Aggs/Metrics/Avg.js | 29 ++++++++++ src/ElasticDSL/Aggs/Metrics/Cardinality.js | 39 +++++++++++++ src/ElasticDSL/Aggs/Metrics/ExtendedStats.js | 31 +++++++++++ .../Aggs/__tests__/converter-test.js | 52 ++++++++++++++++++ src/ElasticDSL/Aggs/converter.js | 55 +++++++++++++++++++ src/ElasticDSL/SearchBody.js | 17 +++++- 8 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 src/ElasticDSL/Aggs/AggBlock.js create mode 100644 src/ElasticDSL/Aggs/AggRules.js create mode 100644 src/ElasticDSL/Aggs/Metrics/Avg.js create mode 100644 src/ElasticDSL/Aggs/Metrics/Cardinality.js create mode 100644 src/ElasticDSL/Aggs/Metrics/ExtendedStats.js create mode 100644 src/ElasticDSL/Aggs/__tests__/converter-test.js create mode 100644 src/ElasticDSL/Aggs/converter.js diff --git a/src/ElasticDSL/Aggs/AggBlock.js b/src/ElasticDSL/Aggs/AggBlock.js new file mode 100644 index 0000000..ad6eb83 --- /dev/null +++ b/src/ElasticDSL/Aggs/AggBlock.js @@ -0,0 +1,33 @@ +/* @flow */ + +import { InputTypeComposer } from 'graphql-compose'; +import { getTypeName, getOrSetType, desc } from '../../utils'; +import { getAggRulesITC } from './AggRules'; + +export function getAggBlockITC(opts: mixed = {}): InputTypeComposer { + const name = getTypeName('AggBlock', opts); + const description = desc(` + The aggregations framework helps provide aggregated data based on + a search query. It is based on simple building blocks called aggregations, + that can be composed in order to build complex summaries of the data. + [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) + `); + + return getOrSetType(name, () => + // $FlowFixMe + InputTypeComposer.create({ + name, + description, + fields: { + key: { + type: 'String', + description: 'FieldName in response for aggregation result', + }, + value: { + type: () => getAggRulesITC(opts), + description: 'Aggregation rules', + }, + }, + }) + ); +} diff --git a/src/ElasticDSL/Aggs/AggRules.js b/src/ElasticDSL/Aggs/AggRules.js new file mode 100644 index 0000000..f4a1a58 --- /dev/null +++ b/src/ElasticDSL/Aggs/AggRules.js @@ -0,0 +1,38 @@ +/* @flow */ + +import { InputTypeComposer } from 'graphql-compose'; +import { getTypeName, getOrSetType, desc } from '../../utils'; +import { getAggBlockITC } from './AggBlock'; + +import { getAvgITC } from './Metrics/Avg'; +import { getCardinalityITC } from './Metrics/Cardinality'; +import { getExtendedStatsITC } from './Metrics/ExtendedStats'; + +export function getAggRulesITC(opts: mixed = {}): InputTypeComposer { + const name = getTypeName('AggRules', opts); + const description = desc( + ` + The aggregations framework helps provide aggregated data based on + a search query. It is based on simple building blocks called aggregations, + that can be composed in order to build complex summaries of the data. + [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) + ` + ); + + return getOrSetType(name, () => + // $FlowFixMe + InputTypeComposer.create({ + name, + description, + fields: { + avg: () => getAvgITC(opts), + cardinality: () => getCardinalityITC(opts), + extended_stats: () => getExtendedStatsITC(opts), + + aggs: { + type: () => [getAggBlockITC(opts)], + description: 'Aggregation block', + }, + }, + })); +} diff --git a/src/ElasticDSL/Aggs/Metrics/Avg.js b/src/ElasticDSL/Aggs/Metrics/Avg.js new file mode 100644 index 0000000..06fd055 --- /dev/null +++ b/src/ElasticDSL/Aggs/Metrics/Avg.js @@ -0,0 +1,29 @@ +/* @flow */ + +import { InputTypeComposer } from 'graphql-compose'; +import { getTypeName, getOrSetType, desc } from '../../../utils'; +import { getCommonsScriptITC } from '../../Commons/Script'; + +export function getAvgITC(opts: mixed = {}): InputTypeComposer { + const name = getTypeName('AggsAvg', opts); + const description = desc(` + A single-value metrics aggregation that computes the average + of numeric values that are extracted from the aggregated documents. + These values can be extracted either from specific numeric fields + in the documents, or be generated by a provided script. + [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html) + `); + + return getOrSetType(name, () => + // $FlowFixMe + InputTypeComposer.create({ + name, + description, + fields: { + field: 'String', + missing: 'Float', + script: () => getCommonsScriptITC(), + }, + }) + ); +} diff --git a/src/ElasticDSL/Aggs/Metrics/Cardinality.js b/src/ElasticDSL/Aggs/Metrics/Cardinality.js new file mode 100644 index 0000000..38f2642 --- /dev/null +++ b/src/ElasticDSL/Aggs/Metrics/Cardinality.js @@ -0,0 +1,39 @@ +/* @flow */ + +import { InputTypeComposer } from 'graphql-compose'; +import { getTypeName, getOrSetType, desc } from '../../../utils'; +import { getCommonsScriptITC } from '../../Commons/Script'; + +export function getCardinalityITC(opts: mixed = {}): InputTypeComposer { + const name = getTypeName('AggsCardinality', opts); + const description = desc( + ` + A single-value metrics aggregation that calculates an approximate count + of distinct values. Values can be extracted either from specific fields + in the document or generated by a script. + [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html) + ` + ); + + return getOrSetType(name, () => + // $FlowFixMe + InputTypeComposer.create({ + name, + description, + fields: { + field: 'String', + precision_threshold: { + type: 'Int', + defaultValue: 3000, + description: desc( + ` + Allows to trade memory for accuracy, and defines a unique count + below which counts are expected to be close to accurate. + ` + ), + }, + missing: 'String', + script: () => getCommonsScriptITC(), + }, + })); +} diff --git a/src/ElasticDSL/Aggs/Metrics/ExtendedStats.js b/src/ElasticDSL/Aggs/Metrics/ExtendedStats.js new file mode 100644 index 0000000..617ad0f --- /dev/null +++ b/src/ElasticDSL/Aggs/Metrics/ExtendedStats.js @@ -0,0 +1,31 @@ +/* @flow */ + +import { InputTypeComposer } from 'graphql-compose'; +import { getTypeName, getOrSetType, desc } from '../../../utils'; +import { getCommonsScriptITC } from '../../Commons/Script'; + +export function getExtendedStatsITC(opts: mixed = {}): InputTypeComposer { + const name = getTypeName('AggsExtendedStats', opts); + const description = desc( + ` + A multi-value metrics aggregation that computes stats over numeric values + extracted from the aggregated documents. These values can be extracted + either from specific numeric fields in the documents, or be generated + by a provided script. + [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html) + ` + ); + + return getOrSetType(name, () => + // $FlowFixMe + InputTypeComposer.create({ + name, + description, + fields: { + field: 'String', + sigma: 'Float', + missing: 'Float', + script: () => getCommonsScriptITC(), + }, + })); +} diff --git a/src/ElasticDSL/Aggs/__tests__/converter-test.js b/src/ElasticDSL/Aggs/__tests__/converter-test.js new file mode 100644 index 0000000..49e30bb --- /dev/null +++ b/src/ElasticDSL/Aggs/__tests__/converter-test.js @@ -0,0 +1,52 @@ +import argsBlockConverter, { + convertAggsBlocks, + convertAggsRules, +} from '../converter'; + +describe('AGGS args converter', () => { + it('convertAggsRules()', () => { + expect(convertAggsRules({ some: { field: 1 } })).toEqual({ + some: { field: 1 }, + }); + }); + + it('convertAggsBlocks()', () => { + expect( + convertAggsBlocks([ + { key: 'field1', value: {} }, + { key: 'field2', value: {} }, + ]) + ).toEqual({ + field1: {}, + field2: {}, + }); + }); + + it('should convert recursively aggs', () => { + expect( + convertAggsBlocks([ + { key: 'field1', value: { aggs: [{ key: 'field2', value: {} }] } }, + ]) + ).toEqual({ field1: { aggs: { field2: {} } } }); + }); + + it('argsBlockConverter()', () => { + expect.assertions(4); + const mockResolve = (source, args, context, info) => { + expect(args.body.aggs).toEqual({ + field1: {}, + field2: {}, + }); + expect(source).toEqual('source'); + expect(context).toEqual('context'); + expect(info).toEqual('info'); + }; + + const args = { + body: { + aggs: [{ key: 'field1', value: {} }, { key: 'field2', value: {} }], + }, + }; + argsBlockConverter(mockResolve)('source', args, 'context', 'info'); + }); +}); diff --git a/src/ElasticDSL/Aggs/converter.js b/src/ElasticDSL/Aggs/converter.js new file mode 100644 index 0000000..204cba3 --- /dev/null +++ b/src/ElasticDSL/Aggs/converter.js @@ -0,0 +1,55 @@ +import type { GraphQLFieldResolver } from 'graphql/type/definition'; + +export type ElasticAggsT = { + [outputFieldName: string]: ElasticAggsRulesT, +}; + +export type ElasticAggsRulesT = { + [aggOperationName: string]: mixed, + aggs: ElasticAggsT, +}; + +export type GqlAggBlock = { + key: string, + value: GqlAggRules, +}; + +export type GqlAggRules = { + [aggOperationName: string]: mixed, + aggs: GqlAggBlock, +}; + +export default function argsBlockConverter( + resolve: GraphQLFieldResolver<*, *> +): GraphQLFieldResolver<*, *> { + return (source, args, context, info) => { + if (args.body && Array.isArray(args.body.aggs)) { + const aggs: GqlAggBlock[] = args.body.aggs; + args.body.aggs = convertAggsBlocks(aggs); // eslint-disable-line + } + + return resolve(source, args, context, info); + }; +} + +export function convertAggsBlocks(blockList: GqlAggBlock[]): ElasticAggsT { + const result = {}; + blockList.forEach(block => { + if (block.key && block.value) { + result[block.key] = convertAggsRules(block.value); + } + }); + return result; +} + +export function convertAggsRules(rules: GqlAggRules): ElasticAggsRulesT { + const result = {}; + Object.keys(rules).forEach(key => { + if (key === 'aggs' && rules.aggs) { + result.aggs = convertAggsBlocks(rules.aggs); + } else { + result[key] = rules[key]; + } + }); + return result; +} diff --git a/src/ElasticDSL/SearchBody.js b/src/ElasticDSL/SearchBody.js index df5bc7a..9352d45 100644 --- a/src/ElasticDSL/SearchBody.js +++ b/src/ElasticDSL/SearchBody.js @@ -1,16 +1,21 @@ /* @flow */ import { InputTypeComposer } from 'graphql-compose'; +import type { GraphQLFieldResolver } from 'graphql/type/definition'; import { getQueryITC } from './Query/Query'; +import { getAggBlockITC } from './Aggs/AggBlock'; +import prepareArgAggs from './Aggs/converter'; import { getTypeName, getOrSetType, desc } from '../utils'; export function getSearchBodyITC(opts: mixed = {}): InputTypeComposer { const name = getTypeName('SearchBody', opts); - const description = desc(` + const description = desc( + ` The search request can be executed with a search DSL, which includes the [Query DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html), within its body. - `); + ` + ); return getOrSetType(name, () => // $FlowFixMe @@ -19,6 +24,14 @@ export function getSearchBodyITC(opts: mixed = {}): InputTypeComposer { description, fields: { query: () => getQueryITC(opts), + aggs: () => [getAggBlockITC(opts)], + size: 'Int', }, })); } + +export function prepareSearchArgs( + resolve: GraphQLFieldResolver<*, *> +): GraphQLFieldResolver<*, *> { + return prepareArgAggs(resolve); +}