diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d1dabacd4..82abcf003a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ - Added `notification` and `notificationColor` props to `EuiHeaderSectionItemButton` ([#2914](https://github.com/elastic/eui/pull/2914)) - Added `folderCheck`, `folderExclamation`, `push`, `quote`, `reporter` and `users` icons ([#2935](https://github.com/elastic/eui/pull/2935)) - Updated `folderClosed` and `folderOpen` to match new additions and sit better on the pixel grid ([#2935](https://github.com/elastic/eui/pull/2935)) +- Converted `EuiSearchBar` to Typescript ([#2909](https://github.com/elastic/eui/pull/2909)) **Bug fixes** diff --git a/src-docs/src/views/search_bar/controlled_search_bar.js b/src-docs/src/views/search_bar/controlled_search_bar.js index 5cb461c49dc..03ffd47ae3a 100644 --- a/src-docs/src/views/search_bar/controlled_search_bar.js +++ b/src-docs/src/views/search_bar/controlled_search_bar.js @@ -154,6 +154,9 @@ export class ControlledSearchBar extends Component { const schema = { strict: true, fields: { + type: { + type: 'string', + }, active: { type: 'boolean', }, diff --git a/src-docs/src/views/search_bar/search_bar.js b/src-docs/src/views/search_bar/search_bar.js index 2f9dd3334b3..28c6ecfac9b 100644 --- a/src-docs/src/views/search_bar/search_bar.js +++ b/src-docs/src/views/search_bar/search_bar.js @@ -136,6 +136,9 @@ export class SearchBar extends Component { const schema = { strict: true, fields: { + type: { + type: 'string', + }, active: { type: 'boolean', }, diff --git a/src-docs/src/views/search_bar/search_bar_example.js b/src-docs/src/views/search_bar/search_bar_example.js index 31b59653e3e..27f701293d0 100644 --- a/src-docs/src/views/search_bar/search_bar_example.js +++ b/src-docs/src/views/search_bar/search_bar_example.js @@ -37,9 +37,9 @@ export const SearchBarExample = { text: (

- A EuiSearchBar is a toolbar that enables the user - to create/define a search query. This can be done either by entering - the query syntax in a search box or by clicking any of the + An EuiSearchBar is a toolbar that enables the + user to create/define a search query. This can be done either by + entering the query syntax in a search box or by clicking any of the configured filters. The query language is not meant to be full blown search language for arbitrary data (e.g. as required in the Discover App in Kibana), yet it does provide some useful features: @@ -231,8 +231,8 @@ export const SearchBarExample = { text: (

- A EuiSearchBar can have its query controlled by a - parent component by passing the query prop. + An EuiSearchBar can have its query controlled by + a parent component by passing the query prop. Changes to the query will be passed back up through the{' '} onChange callback where the new query must be stored in state and passed back into the search bar. @@ -256,7 +256,7 @@ export const SearchBarExample = { text: (

- A EuiSearchBar can have custom filter dropdowns + An EuiSearchBar can have custom filter dropdowns that control how a user can search.

diff --git a/src-docs/src/views/search_bar/search_bar_filters.js b/src-docs/src/views/search_bar/search_bar_filters.js index 5a5efe6eac1..1b74a967672 100644 --- a/src-docs/src/views/search_bar/search_bar_filters.js +++ b/src-docs/src/views/search_bar/search_bar_filters.js @@ -122,6 +122,9 @@ export class SearchBarFilters extends Component { const schema = { strict: true, fields: { + type: { + type: 'string', + }, active: { type: 'boolean', }, diff --git a/src-docs/src/views/tables/in_memory/in_memory_selection.js b/src-docs/src/views/tables/in_memory/in_memory_selection.js index 44c6721872e..630f07f03f4 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_selection.js +++ b/src-docs/src/views/tables/in_memory/in_memory_selection.js @@ -97,7 +97,7 @@ export class Table extends Component { renderToolsLeft() { const selection = this.state.control_columns; - if (selection.length === 0) { + if (!selection || selection.length === 0) { return; } diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index 798d977024a..415d1498e59 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -2,13 +2,10 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { requiredProps } from '../../test'; -import { - EuiInMemoryTable, - EuiInMemoryTableProps, - FilterConfig, -} from './in_memory_table'; +import { EuiInMemoryTable, EuiInMemoryTableProps } from './in_memory_table'; import { ENTER } from '../../services/key_codes'; import { SortDirection } from '../../services'; +import { FilterConfig } from '../search_bar/filters'; interface BasicItem { id: number | string; @@ -652,6 +649,7 @@ describe('EuiInMemoryTable', () => { pagination: true, sorting: true, search: { + onChange: () => {}, defaultQuery: 'name:name1', box: { incremental: true, @@ -702,7 +700,9 @@ describe('EuiInMemoryTable', () => { name: 'Name', }, ], - search: {}, + search: { + onChange: () => true, + }, className: 'testTable', }; @@ -768,7 +768,10 @@ describe('EuiInMemoryTable', () => { name: 'Name', }, ], - search: { defaultQuery: 'No' }, + search: { + onChange: () => {}, + defaultQuery: 'No', + }, className: 'testTable', message: No items found!, }; @@ -810,6 +813,7 @@ describe('EuiInMemoryTable', () => { }, ], search: { + onChange: () => {}, defaultQuery: 'No', }, className: 'testTable', diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index e4d9b803344..37ce69bd15d 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -18,134 +18,16 @@ import { } from './pagination_bar'; import { isString } from '../../services/predicate'; import { Comparators, Direction } from '../../services/sort'; -// @ts-ignore -import { EuiSearchBar } from '../search_bar'; +import { EuiSearchBar, Query } from '../search_bar'; import { EuiSpacer } from '../spacer'; import { CommonProps } from '../common'; - -// Search bar types. Should be moved when it is typescriptified. -interface SearchBoxConfig { - placeholder?: string; - incremental?: boolean; - schema?: SchemaType; -} - -interface SchemaType { - strict?: boolean; - fields?: object; - flags?: string[]; -} - -interface IsFilterConfigType { - type: 'is'; - field: string; - name: string; - negatedName?: string; - available?: () => boolean; -} - -interface FieldValueOptionType { - field?: string; - value: any; - name?: string; - view?: ReactNode; -} - -interface FieldValueSelectionFilterConfigType { - type: 'field_value_selection'; - field?: string; - autoClose?: boolean; - name: string; - options: - | FieldValueOptionType[] - | ((query: Query) => Promise); - filterWith?: - | ((name: string, query: string, options: object) => boolean) - | 'prefix' - | 'includes'; - cache?: number; - multiSelect?: boolean | 'and' | 'or'; - loadingMessage?: string; - noOptionsMessage?: string; - searchThreshold?: number; - available?: () => boolean; -} - -interface FieldValueToggleFilterConfigType { - type: 'field_value_toggle'; - field: string; - value: string | number | boolean; - name: string; - negatedName?: string; - available?: () => boolean; - operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; -} - -interface FieldValueToggleGroupFilterItem { - value: string | number | boolean; - name: string; - negatedName?: string; - operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; -} - -interface FieldValueToggleGroupFilterConfigType { - type: 'field_value_toggle_group'; - field: string; - items: FieldValueToggleGroupFilterItem[]; - available?: () => boolean; -} - -export type FilterConfig = - | IsFilterConfigType - | FieldValueSelectionFilterConfigType - | FieldValueToggleFilterConfigType - | FieldValueToggleGroupFilterConfigType; - -type SearchBox = Omit & { - schema?: boolean | SchemaType; -}; - -/* Should point at search_bar/query type when it is converted to typescript */ -type Query = any; +import { EuiSearchBarProps } from '../search_bar/search_bar'; +import { SchemaType } from '../search_bar/search_box'; interface onChangeArgument { - query: Query; + query: Query | null; queryText: string; - error: string; -} - -interface EuiSearchBarProps { - /** - The initial query the bar will hold when first mounted - */ - defaultQuery?: Query; - /** - If you wish to use the search bar as a controlled component, continuously pass the query - via this prop - */ - query?: Query; - /** - Configures the search box. Set `placeholder` to change the placeholder text in the box and - `incremental` to support incremental (as you type) search. - */ - box?: SearchBox; - /** - An array of search filters. - */ - filters?: FilterConfig[]; - /** - * Tools which go to the left of the search bar. - */ - toolsLeft?: React.ReactNode; - /** - * Tools which go to the right of the search bar. - */ - toolsRight?: React.ReactNode; - /** - * Date formatter to use when parsing date values - */ - dateFormat?: object; - onChange?: (values: onChangeArgument) => boolean | void; + error: Error | null; } function isEuiSearchBarProps( @@ -208,7 +90,7 @@ interface State { sortName: ReactNode; sortDirection?: Direction; }; - query: Query; + query: Query | null; pageIndex: number; pageSize?: number; pageSizeOptions?: number[]; @@ -219,11 +101,13 @@ interface State { } const getInitialQuery = (search: Search | undefined) => { + let query: Query | string; if (!search) { - return; + query = ''; + } else { + query = (search as EuiSearchBarProps).defaultQuery || ''; } - const query = (search as EuiSearchBarProps).defaultQuery || ''; return isString(query) ? EuiSearchBar.Query.parse(query) : query; }; @@ -382,7 +266,7 @@ export class EuiInMemoryTable extends Component< pageSizeOptions, sortName, sortDirection, - allowNeutralSort: allowNeutralSort === false ? false : true, + allowNeutralSort: allowNeutralSort !== false, hidePerPageOptions, }; } @@ -444,14 +328,21 @@ export class EuiInMemoryTable extends Component< }; onQueryChange = ({ query, queryText, error }: onChangeArgument) => { - if (isEuiSearchBarProps(this.props.search)) { - const search = this.props.search; + const { search } = this.props; + if (isEuiSearchBarProps(search)) { if (search.onChange) { - const shouldQueryInMemory = search.onChange({ - query, - queryText, - error, - }); + const shouldQueryInMemory = + error == null + ? search.onChange({ + query: query!, + queryText, + error: null, + }) + : search.onChange({ + query: null, + queryText, + error, + }); if (!shouldQueryInMemory) { return; } @@ -468,14 +359,17 @@ export class EuiInMemoryTable extends Component< renderSearchBar() { const { search } = this.props; if (search) { - let searchBarProps: EuiSearchBarProps = {}; + let searchBarProps: Omit = {}; if (isEuiSearchBarProps(search)) { const { onChange, ..._searchBarProps } = search; searchBarProps = _searchBarProps; if (searchBarProps.box && searchBarProps.box.schema === true) { - searchBarProps.box.schema = this.resolveSearchSchema(); + searchBarProps.box = { + ...searchBarProps.box, + schema: this.resolveSearchSchema(), + }; } } @@ -483,7 +377,7 @@ export class EuiInMemoryTable extends Component< } } - resolveSearchSchema() { + resolveSearchSchema(): SchemaType { const { columns } = this.props; return columns.reduce<{ strict: boolean; @@ -501,7 +395,7 @@ export class EuiInMemoryTable extends Component< ); } - getItemSorter() { + getItemSorter(): (a: T, b: T) => number { const { sortName, sortDirection } = this.state; const { columns } = this.props; @@ -512,7 +406,7 @@ export class EuiInMemoryTable extends Component< if (sortColumn == null) { // can't return a non-function so return a function that says everything is the same - return () => () => 0; + return () => 0; } const sortable = sortColumn.sortable; diff --git a/src/components/search_bar/__snapshots__/search_bar.test.js.snap b/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap similarity index 100% rename from src/components/search_bar/__snapshots__/search_bar.test.js.snap rename to src/components/search_bar/__snapshots__/search_bar.test.tsx.snap diff --git a/src/components/search_bar/__snapshots__/search_box.test.js.snap b/src/components/search_bar/__snapshots__/search_box.test.tsx.snap similarity index 100% rename from src/components/search_bar/__snapshots__/search_box.test.js.snap rename to src/components/search_bar/__snapshots__/search_box.test.tsx.snap diff --git a/src/components/search_bar/__snapshots__/search_filters.test.js.snap b/src/components/search_bar/__snapshots__/search_filters.test.tsx.snap similarity index 100% rename from src/components/search_bar/__snapshots__/search_filters.test.js.snap rename to src/components/search_bar/__snapshots__/search_filters.test.tsx.snap diff --git a/src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.js.snap b/src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.tsx.snap similarity index 99% rename from src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.js.snap rename to src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.tsx.snap index 5cf798a14d8..980a2b87d0f 100644 --- a/src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.js.snap +++ b/src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.tsx.snap @@ -252,7 +252,7 @@ exports[`FieldValueSelectionFilter render - all configurations 1`] = ` ownFocus={true} panelClassName="euiFilterGroup__popoverPanel" panelPaddingSize="none" - withTitle={null} + withTitle={false} >
{ test('render - options as a function', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -15,7 +18,7 @@ describe('FieldValueSelectionFilter', () => { type: 'field_value_selection', field: 'tag', name: 'Tag', - options: () => {}, + options: () => Promise.resolve([]), }, }; @@ -25,7 +28,7 @@ describe('FieldValueSelectionFilter', () => { }); test('render - options as an array', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -57,7 +60,7 @@ describe('FieldValueSelectionFilter', () => { }); test('render - fields in options', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -91,7 +94,7 @@ describe('FieldValueSelectionFilter', () => { }); test('render - all configurations', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -101,11 +104,11 @@ describe('FieldValueSelectionFilter', () => { field: 'tag', name: 'Tag', multiSelect: true, - available: () => {}, + available: () => false, loadingMessage: 'loading...', noOptionsMessage: 'oops...', searchThreshold: 5, - options: () => {}, + options: () => Promise.resolve([]), }, }; @@ -115,7 +118,7 @@ describe('FieldValueSelectionFilter', () => { }); test('render - multi-select OR', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -125,11 +128,11 @@ describe('FieldValueSelectionFilter', () => { field: 'tag', name: 'Tag', multiSelect: 'or', - available: () => {}, + available: () => false, loadingMessage: 'loading...', noOptionsMessage: 'oops...', searchThreshold: 5, - options: () => {}, + options: () => Promise.resolve([]), }, }; @@ -139,7 +142,7 @@ describe('FieldValueSelectionFilter', () => { }); test('inactive - field is global', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -171,7 +174,7 @@ describe('FieldValueSelectionFilter', () => { }); test('active - field is global', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -203,7 +206,7 @@ describe('FieldValueSelectionFilter', () => { }); test('inactive - fields in options', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -237,7 +240,7 @@ describe('FieldValueSelectionFilter', () => { }); test('active - fields in options', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, diff --git a/src/components/search_bar/filters/field_value_selection_filter.js b/src/components/search_bar/filters/field_value_selection_filter.tsx similarity index 63% rename from src/components/search_bar/filters/field_value_selection_filter.js rename to src/components/search_bar/filters/field_value_selection_filter.tsx index fe9f6792cd0..13373a72466 100644 --- a/src/components/search_bar/filters/field_value_selection_filter.js +++ b/src/components/search_bar/filters/field_value_selection_filter.tsx @@ -1,56 +1,53 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, ReactElement, ReactNode } from 'react'; import { isArray, isNil } from '../../../services/predicate'; import { keyCodes } from '../../../services'; -import { EuiPropTypes } from '../../../utils/prop_types'; -import { EuiPopover } from '../../popover/popover'; -import { EuiPopoverTitle } from '../../popover/popover_title'; -import { EuiFieldSearch } from '../../form'; -import { EuiFilterSelectItem, EuiFilterButton } from '../../filter_group'; -import { EuiLoadingChart } from '../../loading/loading_chart'; -import { EuiSpacer } from '../../spacer/spacer'; -import { EuiIcon } from '../../icon/icon'; +import { EuiPopover, EuiPopoverTitle } from '../../popover'; +import { EuiFieldSearch } from '../../form/field_search'; +import { EuiFilterButton, EuiFilterSelectItem } from '../../filter_group'; +import { EuiLoadingChart } from '../../loading'; +import { EuiSpacer } from '../../spacer'; +import { EuiIcon } from '../../icon'; import { Query } from '../query'; +import { Clause, Value } from '../query/ast'; -const FieldValueOptionType = PropTypes.shape({ - field: PropTypes.string, - value: PropTypes.any.isRequired, - name: PropTypes.string, - view: PropTypes.node, -}); - -const FieldValueOptionsType = PropTypes.oneOfType([ - PropTypes.func, // (query) => Promise - PropTypes.arrayOf(FieldValueOptionType), -]); - -export const FieldValueSelectionFilterConfigType = PropTypes.shape({ - type: EuiPropTypes.is('field_value_selection').isRequired, - field: PropTypes.string, - autoClose: PropTypes.boolean, - name: PropTypes.string.isRequired, - options: FieldValueOptionsType.isRequired, - filterWith: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.oneOf(['prefix', 'includes']), - ]), - cache: PropTypes.number, - multiSelect: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.oneOf(['and', 'or']), - ]), - loadingMessage: PropTypes.string, - noOptionsMessage: PropTypes.string, - searchThreshold: PropTypes.number, - available: PropTypes.func, // () => boolean -}); - -const FieldValueSelectionFilterPropTypes = { - index: PropTypes.number.isRequired, - config: FieldValueSelectionFilterConfigType.isRequired, - query: PropTypes.any.isRequired, - onChange: PropTypes.func.isRequired, // (value) => void -}; +interface FieldValueOptionType { + field?: string; + value: Value; + name?: string; + view?: ReactNode; +} + +type OptionsLoader = () => Promise; + +type OptionsFilter = ( + name: string, + query: string, + options?: FieldValueOptionType[] +) => boolean; + +type MultiSelect = boolean | 'and' | 'or'; + +export interface FieldValueSelectionFilterConfigType { + type: 'field_value_selection'; + field?: string; + name: string; + options: FieldValueOptionType[] | OptionsLoader; + filterWith?: 'prefix' | 'includes' | OptionsFilter; + cache?: number; + multiSelect?: MultiSelect; + loadingMessage?: string; + noOptionsMessage?: string; + searchThreshold?: number; + available?: () => boolean; +} + +export interface FieldValueSelectionFilterProps { + index: number; + config: FieldValueSelectionFilterConfigType; + query: Query; + onChange: (query: Query) => void; + autoClose?: boolean; +} const defaults = { config: { @@ -62,14 +59,30 @@ const defaults = { }, }; -export class FieldValueSelectionFilter extends Component { - static propTypes = FieldValueSelectionFilterPropTypes; +interface State { + popoverOpen: boolean; + error: string | null; + options: { + all: FieldValueOptionType[]; + shown: FieldValueOptionType[]; + } | null; + cachedOptions?: FieldValueOptionType[] | null; +} + +type DefaultProps = Pick; - static defaultProps = { +export class FieldValueSelectionFilter extends Component< + FieldValueSelectionFilterProps, + State +> { + static defaultProps: DefaultProps = { autoClose: true, }; - constructor(props) { + private readonly selectItems: EuiFilterSelectItem[]; + private searchInput: HTMLInputElement | null = null; + + constructor(props: FieldValueSelectionFilterProps) { super(props); const { options } = props.config; @@ -136,6 +149,7 @@ export class FieldValueSelectionFilter extends Component { const predicate = this.getOptionFilter(); return { + ...prevState, options: { ...prevState.options, shown: prevState.options.all.filter((option, i, options) => { @@ -148,7 +162,7 @@ export class FieldValueSelectionFilter extends Component { }); } - getOptionFilter() { + getOptionFilter(): OptionsFilter { const filterWith = this.props.config.filterWith || defaults.config.filterWith; @@ -163,42 +177,42 @@ export class FieldValueSelectionFilter extends Component { return (name, query) => name.startsWith(query); } - resolveOptionsLoader() { + resolveOptionsLoader: () => OptionsLoader = () => { const options = this.props.config.options; if (isArray(options)) { return () => Promise.resolve(options); } - if (isNil(this.props.config.cache) || this.props.config.cache <= 0) { - return options; - } + return () => { const cachedOptions = this.state.cachedOptions; if (cachedOptions) { return Promise.resolve(cachedOptions); } - if (this.props.config.cache > 0) { - return new Promise((resolve, reject) => { - return options() - .then(opts => { - this.setState({ cachedOptions: opts }); - this.timeoutId = setTimeout(() => { - this.setState({ cachedOptions: null }); - }, this.props.config.cache); - resolve(opts); - }) - .catch(error => { - reject(error); - }); - }); - } + + return (options as OptionsLoader)().then(opts => { + // If a cache time is set, populate the cache and also schedule a + // cache reset. + if (this.props.config.cache != null && this.props.config.cache > 0) { + this.setState({ cachedOptions: opts }); + setTimeout(() => { + this.setState({ cachedOptions: null }); + }, this.props.config.cache); + } + + return opts; + }); }; - } + }; - resolveOptionName(option) { + resolveOptionName(option: FieldValueOptionType) { return option.name || option.value.toString(); } - onOptionClick(field, value, checked) { + onOptionClick( + field: string, + value: Value, + checked: 'on' | 'off' | undefined + ) { const multiSelect = this.resolveMultiSelect(); const { autoClose } = this.props; @@ -231,7 +245,12 @@ export class FieldValueSelectionFilter extends Component { } } - onKeyDown(index, event) { + onKeyDown( + index: number, + event: + | React.KeyboardEvent + | React.KeyboardEvent + ) { switch (event.keyCode) { case keyCodes.DOWN: if (index < this.selectItems.length - 1) { @@ -254,7 +273,7 @@ export class FieldValueSelectionFilter extends Component { } } - resolveMultiSelect() { + resolveMultiSelect(): MultiSelect { const { config } = this.props; return !isNil(config.multiSelect) ? config.multiSelect @@ -293,7 +312,7 @@ export class FieldValueSelectionFilter extends Component { const threshold = this.props.config.searchThreshold || defaults.config.searchThreshold; const withTitle = - this.state.options && this.state.options.all.length >= threshold; + this.state.options != null && this.state.options.all.length >= threshold; return ( = threshold) { - const disabled = this.state.error; + const disabled = this.state.error != null; return ( { - const optionField = option.field || field; - - const clause = - multiSelect === 'or' - ? query.getOrFieldClause(optionField, option.value) - : query.getSimpleFieldClause(optionField, option.value); - - const checked = this.resolveChecked(clause); - const onClick = () => { - // clicking a checked item will uncheck it and effective remove the filter (value = undefined) - this.onOptionClick(optionField, option.value, checked); - }; - - const item = ( - (this.selectItems[index] = ref)} - onKeyDown={this.onKeyDown.bind(this, index)}> - {option.view ? option.view : this.resolveOptionName(option)} - + + if (this.state.options == null) { + return; + } + + const items: { + on: ReactElement[]; + off: ReactElement[]; + rest: ReactElement[]; + } = { + on: [], + off: [], + rest: [], + }; + + this.state.options.shown.forEach((option, index) => { + const optionField = option.field || field; + + if (optionField == null) { + throw new Error( + 'option.field or field should be provided in ' ); - if (!checked) { - items.rest.push(item); - } else if (checked === 'on') { - items.on.push(item); - } else { - items.off.push(item); - } - return items; - }, - { on: [], off: [], rest: [] } - ); + } + + const clause = + multiSelect === 'or' + ? query.getOrFieldClause(optionField, option.value) + : query.getSimpleFieldClause(optionField, option.value); + + const checked = this.resolveChecked(clause); + const onClick = () => { + // clicking a checked item will uncheck it and effective remove the filter (value = undefined) + this.onOptionClick(optionField, option.value, checked); + }; + + const item = ( + (this.selectItems[index] = ref!)} + onKeyDown={this.onKeyDown.bind(this, index)}> + {option.view ? option.view : this.resolveOptionName(option)} + + ); + + if (!checked) { + items.rest.push(item); + } else if (checked === 'on') { + items.on.push(item); + } else { + items.off.push(item); + } + return items; + }); + return (
{[...items.on, ...items.off, ...items.rest]} @@ -384,7 +428,7 @@ export class FieldValueSelectionFilter extends Component { ); } - resolveChecked(clause) { + resolveChecked(clause: Clause | undefined): 'on' | 'off' | undefined { if (clause) { return Query.isMust(clause) ? 'on' : 'off'; } @@ -404,7 +448,7 @@ export class FieldValueSelectionFilter extends Component { ); } - renderError(message) { + renderError(message: string) { return (
@@ -430,7 +474,7 @@ export class FieldValueSelectionFilter extends Component { ); } - isActiveField(field) { + isActiveField(field: string | undefined): boolean { if (typeof field !== 'string') { return false; } diff --git a/src/components/search_bar/filters/field_value_toggle_filter.test.js b/src/components/search_bar/filters/field_value_toggle_filter.test.tsx similarity index 86% rename from src/components/search_bar/filters/field_value_toggle_filter.test.js rename to src/components/search_bar/filters/field_value_toggle_filter.test.tsx index 04dbf48c568..df045412915 100644 --- a/src/components/search_bar/filters/field_value_toggle_filter.test.js +++ b/src/components/search_bar/filters/field_value_toggle_filter.test.tsx @@ -2,11 +2,14 @@ import React from 'react'; import { requiredProps } from '../../../test'; import { shallow } from 'enzyme'; import { Query } from '../query'; -import { FieldValueToggleFilter } from './field_value_toggle_filter'; +import { + FieldValueToggleFilter, + FieldValueToggleFilterProps, +} from './field_value_toggle_filter'; describe('FieldValueToggleFilter', () => { test('render', () => { - const props = { + const props: FieldValueToggleFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -24,7 +27,7 @@ describe('FieldValueToggleFilter', () => { }); test('render - active', () => { - const props = { + const props: FieldValueToggleFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -42,7 +45,7 @@ describe('FieldValueToggleFilter', () => { }); test('render - active negated', () => { - const props = { + const props: FieldValueToggleFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -60,7 +63,7 @@ describe('FieldValueToggleFilter', () => { }); test('render - active negated - custom negated name', () => { - const props = { + const props: FieldValueToggleFilterProps = { ...requiredProps, index: 0, onChange: () => {}, diff --git a/src/components/search_bar/filters/field_value_toggle_filter.js b/src/components/search_bar/filters/field_value_toggle_filter.tsx similarity index 55% rename from src/components/search_bar/filters/field_value_toggle_filter.js rename to src/components/search_bar/filters/field_value_toggle_filter.tsx index 49836c687f2..772c1a32361 100644 --- a/src/components/search_bar/filters/field_value_toggle_filter.js +++ b/src/components/search_bar/filters/field_value_toggle_filter.tsx @@ -1,39 +1,30 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiFilterButton } from '../../filter_group'; import { isNil } from '../../../services/predicate'; -import { EuiPropTypes } from '../../../utils/prop_types'; import { Query } from '../query'; +import { Clause, Value } from '../query/ast'; -export const FieldValueToggleFilterConfigType = PropTypes.shape({ - type: EuiPropTypes.is('field_value_toggle').isRequired, - field: PropTypes.string.isRequired, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.bool, - ]).isRequired, - name: PropTypes.string.isRequired, - negatedName: PropTypes.string, - available: PropTypes.func, // () => boolean - operator: PropTypes.oneOf(['eq', 'exact', 'gt', 'gte', 'lt', 'lte']), -}); - -const FieldValueToggleFilterPropTypes = { - index: PropTypes.number.isRequired, - config: FieldValueToggleFilterConfigType.isRequired, - query: PropTypes.any.isRequired, - onChange: PropTypes.func.isRequired, // (value: boolean) => void -}; - -export class FieldValueToggleFilter extends Component { - static propTypes = FieldValueToggleFilterPropTypes; +export interface FieldValueToggleFilterConfigType { + type: 'field_value_toggle'; + field: string; + value: Value; + name: string; + negatedName?: string; + available?: () => boolean; + operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; +} - constructor(props) { - super(props); - } +export interface FieldValueToggleFilterProps { + index: number; + config: FieldValueToggleFilterConfigType; + query: Query; + onChange: (value: Query) => void; +} - resolveDisplay(clause) { +export class FieldValueToggleFilter extends Component< + FieldValueToggleFilterProps +> { + resolveDisplay(clause: Clause | undefined) { const { name, negatedName } = this.props.config; if (isNil(clause)) { return { hasActiveFilters: false, name }; @@ -46,7 +37,7 @@ export class FieldValueToggleFilter extends Component { }; } - valueChanged(checked) { + valueChanged(checked: boolean) { const { field, value, operator } = this.props.config; const query = checked ? this.props.query.removeSimpleFieldValue(field, value) diff --git a/src/components/search_bar/filters/field_value_toggle_group_filter.test.js b/src/components/search_bar/filters/field_value_toggle_group_filter.test.tsx similarity index 88% rename from src/components/search_bar/filters/field_value_toggle_group_filter.test.js rename to src/components/search_bar/filters/field_value_toggle_group_filter.test.tsx index 542f3b12c49..7a859f362ae 100644 --- a/src/components/search_bar/filters/field_value_toggle_group_filter.test.js +++ b/src/components/search_bar/filters/field_value_toggle_group_filter.test.tsx @@ -2,11 +2,14 @@ import React from 'react'; import { requiredProps } from '../../../test'; import { shallow } from 'enzyme'; import { Query } from '../query'; -import { FieldValueToggleGroupFilter } from './field_value_toggle_group_filter'; +import { + FieldValueToggleGroupFilter, + FieldValueToggleGroupFilterProps, +} from './field_value_toggle_group_filter'; describe('TermToggleGroupFilter', () => { test('render', () => { - const props = { + const props: FieldValueToggleGroupFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -32,7 +35,7 @@ describe('TermToggleGroupFilter', () => { }); test('render - active', () => { - const props = { + const props: FieldValueToggleGroupFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -58,7 +61,7 @@ describe('TermToggleGroupFilter', () => { }); test('render - active negated', () => { - const props = { + const props: FieldValueToggleGroupFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -84,7 +87,7 @@ describe('TermToggleGroupFilter', () => { }); test('render - active negated - custom negated name', () => { - const props = { + const props: FieldValueToggleGroupFilterProps = { ...requiredProps, index: 0, onChange: () => {}, diff --git a/src/components/search_bar/filters/field_value_toggle_group_filter.js b/src/components/search_bar/filters/field_value_toggle_group_filter.tsx similarity index 55% rename from src/components/search_bar/filters/field_value_toggle_group_filter.js rename to src/components/search_bar/filters/field_value_toggle_group_filter.tsx index 6a08fcc1c17..da54e6c7470 100644 --- a/src/components/search_bar/filters/field_value_toggle_group_filter.js +++ b/src/components/search_bar/filters/field_value_toggle_group_filter.tsx @@ -1,42 +1,36 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiFilterButton } from '../../filter_group'; -import { EuiPropTypes } from '../../../utils/prop_types'; import { Query } from '../query'; -export const FieldValueToggleGroupFilterItemType = PropTypes.shape({ - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.bool, - ]).isRequired, - name: PropTypes.string.isRequired, - negatedName: PropTypes.string, - operator: PropTypes.oneOf(['eq', 'exact', 'gt', 'gte', 'lt', 'lte']), -}); - -export const FieldValueToggleGroupFilterConfigType = PropTypes.shape({ - type: EuiPropTypes.is('field_value_toggle_group').isRequired, - field: PropTypes.string.isRequired, - items: PropTypes.arrayOf(FieldValueToggleGroupFilterItemType).isRequired, - available: PropTypes.func, // () => boolean -}); - -const FieldValueToggleGroupFilterPropTypes = { - index: PropTypes.number.isRequired, - config: FieldValueToggleGroupFilterConfigType.isRequired, - query: PropTypes.any.isRequired, - onChange: PropTypes.func.isRequired, // (value: boolean) => void -}; +export interface FieldValueToggleGroupFilterItemType { + value: string | number | boolean; + name: string; + negatedName?: string; + operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; +} -export class FieldValueToggleGroupFilter extends Component { - static propTypes = FieldValueToggleGroupFilterPropTypes; +export interface FieldValueToggleGroupFilterConfigType { + type: 'field_value_toggle_group'; + field: string; + items: FieldValueToggleGroupFilterItemType[]; + available?: () => boolean; +} - constructor(props) { - super(props); - } +export interface FieldValueToggleGroupFilterProps { + index: number; + config: FieldValueToggleGroupFilterConfigType; + query: Query; + onChange: (value: Query) => void; +} - resolveDisplay(config, query, item) { +export class FieldValueToggleGroupFilter extends Component< + FieldValueToggleGroupFilterProps +> { + resolveDisplay( + config: FieldValueToggleGroupFilterConfigType, + query: Query, + item: FieldValueToggleGroupFilterItemType + ) { const clause = query.getSimpleFieldClause(config.field, item.value); if (clause) { if (Query.isMust(clause)) { @@ -50,7 +44,7 @@ export class FieldValueToggleGroupFilter extends Component { return { active: false, name: item.name }; } - valueChanged(item, active) { + valueChanged(item: FieldValueToggleGroupFilterItemType, active: boolean) { const { field } = this.props.config; const { value, operator } = item; const query = active diff --git a/src/components/search_bar/filters/filters.js b/src/components/search_bar/filters/filters.js deleted file mode 100644 index 87c09aa7dce..00000000000 --- a/src/components/search_bar/filters/filters.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { IsFilter, IsFilterConfigType } from './is_filter'; -import { - FieldValueSelectionFilter, - FieldValueSelectionFilterConfigType, -} from './field_value_selection_filter'; -import { - FieldValueToggleFilter, - FieldValueToggleFilterConfigType, -} from './field_value_toggle_filter'; -import { - FieldValueToggleGroupFilter, - FieldValueToggleGroupFilterConfigType, -} from './field_value_toggle_group_filter'; -import PropTypes from 'prop-types'; - -export const createFilter = (index, config, query, onChange) => { - const props = { index, config, query, onChange }; - switch (config.type) { - case 'is': - return ; - case 'field_value_selection': - return ; - case 'field_value_toggle': - return ; - case 'field_value_toggle_group': - return ; - default: - throw new Error(`Unknown search filter type [${config.type}]`); - } -}; - -export const FilterConfigType = PropTypes.oneOfType([ - IsFilterConfigType, - FieldValueSelectionFilterConfigType, - FieldValueToggleFilterConfigType, - FieldValueToggleGroupFilterConfigType, -]); diff --git a/src/components/search_bar/filters/filters.tsx b/src/components/search_bar/filters/filters.tsx new file mode 100644 index 00000000000..c3e67f3b18d --- /dev/null +++ b/src/components/search_bar/filters/filters.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { IsFilter, IsFilterConfigType } from './is_filter'; +import { + FieldValueSelectionFilter, + FieldValueSelectionFilterConfigType, +} from './field_value_selection_filter'; +import { + FieldValueToggleFilter, + FieldValueToggleFilterConfigType, +} from './field_value_toggle_filter'; +import { + FieldValueToggleGroupFilter, + FieldValueToggleGroupFilterConfigType, +} from './field_value_toggle_group_filter'; +import { Query } from '../query'; + +export const createFilter = ( + index: number, + config: FilterConfig, + query: Query, + onChange: (query: Query) => void +) => { + const props = { index, query, onChange }; + + // We don't put `config` into `props` above because until we check + // `config.type`, TS only knows that it's a `FilterConfig`, and that type + // is used to define `props` as well. Once we've checked `config.type` + // below, its type is narrowed correctly, hence we pass down `config` + // separately. + switch (config.type) { + case 'is': + return ; + + case 'field_value_selection': + return ; + + case 'field_value_toggle': + return ; + + case 'field_value_toggle_group': + return ; + + default: + // @ts-ignore TS knows that we've checked `config.type` exhaustively + throw new Error(`Unknown search filter type [${config.type}]`); + } +}; + +export type FilterConfig = + | IsFilterConfigType + | FieldValueSelectionFilterConfigType + | FieldValueToggleFilterConfigType + | FieldValueToggleGroupFilterConfigType; diff --git a/src/components/search_bar/filters/index.js b/src/components/search_bar/filters/index.js deleted file mode 100644 index f284d707e29..00000000000 --- a/src/components/search_bar/filters/index.js +++ /dev/null @@ -1 +0,0 @@ -export { createFilter, FilterConfigType } from './filters'; diff --git a/src/components/search_bar/filters/index.ts b/src/components/search_bar/filters/index.ts new file mode 100644 index 00000000000..6dd76a72649 --- /dev/null +++ b/src/components/search_bar/filters/index.ts @@ -0,0 +1 @@ +export { createFilter, FilterConfig } from './filters'; diff --git a/src/components/search_bar/filters/is_filter.test.js b/src/components/search_bar/filters/is_filter.test.tsx similarity index 84% rename from src/components/search_bar/filters/is_filter.test.js rename to src/components/search_bar/filters/is_filter.test.tsx index 0566b6be15f..f560ef6a389 100644 --- a/src/components/search_bar/filters/is_filter.test.js +++ b/src/components/search_bar/filters/is_filter.test.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { requiredProps } from '../../../test'; import { shallow } from 'enzyme'; -import { IsFilter } from './is_filter'; +import { IsFilter, IsFilterProps } from './is_filter'; import { Query } from '../query'; describe('IsFilter', () => { test('render', () => { - const props = { + const props: IsFilterProps = { ...requiredProps, index: 0, onChange: () => {}, diff --git a/src/components/search_bar/filters/is_filter.js b/src/components/search_bar/filters/is_filter.tsx similarity index 60% rename from src/components/search_bar/filters/is_filter.js rename to src/components/search_bar/filters/is_filter.tsx index 304d6f1e2d3..2d51139f4ef 100644 --- a/src/components/search_bar/filters/is_filter.js +++ b/src/components/search_bar/filters/is_filter.tsx @@ -1,33 +1,26 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiFilterButton } from '../../filter_group'; import { isNil } from '../../../services/predicate'; -import { EuiPropTypes } from '../../../utils/prop_types'; import { Query } from '../query'; +import { Clause } from '../query/ast'; -export const IsFilterConfigType = PropTypes.shape({ - type: EuiPropTypes.is('is').isRequired, - field: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - negatedName: PropTypes.string, - available: PropTypes.func, // () => boolean -}); - -const IsFilterPropTypes = { - index: PropTypes.number.isRequired, - config: IsFilterConfigType.isRequired, - query: PropTypes.any.isRequired, - onChange: PropTypes.func.isRequired, // (value: boolean) => void -}; - -export class IsFilter extends Component { - static propTypes = IsFilterPropTypes; +export interface IsFilterConfigType { + type: 'is'; + field: string; + name: string; + negatedName?: string; + available?: () => boolean; +} - constructor(props) { - super(props); - } +export interface IsFilterProps { + index: number; + config: IsFilterConfigType; + query: Query; + onChange: (value: Query) => void; +} - resolveDisplay(clause) { +export class IsFilter extends Component { + resolveDisplay(clause: Clause) { const { name, negatedName } = this.props.config; if (isNil(clause)) { return { hasActiveFilters: false, name }; @@ -40,7 +33,7 @@ export class IsFilter extends Component { }; } - valueChanged(field, checked) { + valueChanged(field: string, checked: boolean) { const query = checked ? this.props.query.removeIsClause(field) : this.props.query.addMustIsClause(field); diff --git a/src/components/search_bar/index.js b/src/components/search_bar/index.js deleted file mode 100644 index a758d48a4d3..00000000000 --- a/src/components/search_bar/index.js +++ /dev/null @@ -1,6 +0,0 @@ -export { EuiSearchBar, QueryType, Query, Ast } from './search_bar'; -export { SearchBoxConfigPropTypes } from './search_box'; -export { SearchFiltersFiltersType } from './search_filters'; - -// TODO: Some related types are defined in basic_table/in_memory_table. -// Use and remove them when TypeScriptification is done. diff --git a/src/components/search_bar/index.ts b/src/components/search_bar/index.ts new file mode 100644 index 00000000000..394c5df78b2 --- /dev/null +++ b/src/components/search_bar/index.ts @@ -0,0 +1,9 @@ +export { + EuiSearchBar, + EuiSearchBarProps, + QueryType, + Query, + Ast, +} from './search_bar'; +export { SearchBoxConfigProps } from './search_box'; +export { SearchFiltersFiltersType } from './search_filters'; diff --git a/src/components/search_bar/query/__snapshots__/ast_to_es_query_dsl.test.js.snap b/src/components/search_bar/query/__snapshots__/ast_to_es_query_dsl.test.ts.snap similarity index 100% rename from src/components/search_bar/query/__snapshots__/ast_to_es_query_dsl.test.js.snap rename to src/components/search_bar/query/__snapshots__/ast_to_es_query_dsl.test.ts.snap diff --git a/src/components/search_bar/query/__snapshots__/ast_to_es_query_string.test.js.snap b/src/components/search_bar/query/__snapshots__/ast_to_es_query_string.test.ts.snap similarity index 100% rename from src/components/search_bar/query/__snapshots__/ast_to_es_query_string.test.js.snap rename to src/components/search_bar/query/__snapshots__/ast_to_es_query_string.test.ts.snap diff --git a/src/components/search_bar/query/ast.js b/src/components/search_bar/query/ast.ts similarity index 53% rename from src/components/search_bar/query/ast.js rename to src/components/search_bar/query/ast.ts index 7f8106fd8f2..09af3e94d73 100644 --- a/src/components/search_bar/query/ast.js +++ b/src/components/search_bar/query/ast.ts @@ -1,37 +1,71 @@ import { isArray, isNil } from '../../../services/predicate'; -import { isDateValue, dateValuesEqual } from './date_value'; +import { DateValue, dateValuesEqual, isDateValue } from './date_value'; + +export type MatchType = 'must' | 'must_not'; + +export type Value = string | number | boolean | DateValue; + +export interface IsClause { + type: 'is'; + match?: MatchType; + flag: string; +} + +export interface FieldClause { + type: 'field'; + match?: MatchType; + operator: OperatorType; + field: string; + value: Value | Value[]; +} + +export interface TermClause { + type: 'term'; + match?: MatchType; + value: Value; +} + +export interface GroupClause { + type: 'group'; + match: MatchType; + value: Clause[]; +} + +export type Clause = IsClause | FieldClause | TermClause | GroupClause; export const Match = Object.freeze({ - MUST: 'must', - MUST_NOT: 'must_not', - isMust(match) { + MUST: 'must' as const, + MUST_NOT: 'must_not' as const, + isMust(match: MatchType | undefined) { return match === Match.MUST; }, - isMustClause(clause) { + isMustClause(clause: Clause) { return Match.isMust(clause.match); }, }); +export type OperatorType = 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; + export const Operator = Object.freeze({ - EQ: 'eq', - EXACT: 'exact', - GT: 'gt', - GTE: 'gte', - LT: 'lt', - LTE: 'lte', - isEQ(match) { + EQ: 'eq' as const, + EXACT: 'exact' as const, + GT: 'gt' as const, + GTE: 'gte' as const, + LT: 'lt' as const, + LTE: 'lte' as const, + isEQ(match: OperatorType | undefined) { return match === Operator.EQ; }, - isEQClause(clause) { - return Operator.isEQ(clause.operator); + isEQClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isEQ(clause.operator); }, - isEXACT(match) { + isEXACT(match: OperatorType | undefined) { return match === Operator.EXACT; }, - isEXACTClause(clause) { - return Operator.isEXACT(clause.operator); + isEXACTClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isEXACT(clause.operator); }, - isRange(match) { + isRange(match: OperatorType | undefined) { return ( Operator.isGT(match) || Operator.isGTE(match) || @@ -39,103 +73,103 @@ export const Operator = Object.freeze({ Operator.isLTE(match) ); }, - isRangeClause(clause) { - return Operator.isRange(clause.operator); + isRangeClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isRange(clause.operator); }, - isGT(match) { + isGT(match: OperatorType | undefined) { return match === Operator.GT; }, - isGTClause(clause) { - return Operator.isGT(clause.operator); + isGTClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isGT(clause.operator); }, - isGTE(match) { + isGTE(match: OperatorType | undefined) { return match === Operator.GTE; }, - isGTEClause(clause) { - return Operator.isGTE(clause.operator); + isGTEClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isGTE(clause.operator); }, - isLT(match) { + isLT(match: OperatorType | undefined) { return match === Operator.LT; }, - isLTClause(clause) { - return Operator.isLT(clause.operator); + isLTClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isLT(clause.operator); }, - isLTE(match) { + isLTE(match: OperatorType | undefined) { return match === Operator.LTE; }, - isLTEClause(clause) { - return Operator.isLTE(clause.operator); + isLTEClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isLTE(clause.operator); }, }); const Term = Object.freeze({ - TYPE: 'term', - isInstance: clause => { + TYPE: 'term' as const, + isInstance: (clause: Clause): clause is TermClause => { return clause.type === Term.TYPE; }, - must: value => { + must: (value: Value) => { return { type: Term.TYPE, value, match: Match.MUST }; }, - mustNot: value => { + mustNot: (value: Value) => { return { type: Term.TYPE, value, match: Match.MUST_NOT }; }, }); const Group = Object.freeze({ - TYPE: 'group', - isInstance: clause => { + TYPE: 'group' as const, + isInstance: (clause: Clause): clause is GroupClause => { return clause.type === Group.TYPE; }, - must: value => { + must: (value: Clause[]) => { return { type: Group.TYPE, value, match: Match.MUST }; }, - mustNot: value => { + mustNot: (value: Clause[]) => { return { type: Group.TYPE, value, match: Match.MUST_NOT }; }, }); const Field = Object.freeze({ - TYPE: 'field', - isInstance: clause => { + TYPE: 'field' as const, + isInstance: (clause: Clause): clause is FieldClause => { return clause.type === Field.TYPE; }, must: { - eq: (field, value) => ({ + eq: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.EQ, }), - exact: (field, value) => ({ + exact: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.EXACT, }), - gt: (field, value) => ({ + gt: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.GT, }), - gte: (field, value) => ({ + gte: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.GTE, }), - lt: (field, value) => ({ + lt: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.LT, }), - lte: (field, value) => ({ + lte: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, @@ -144,42 +178,42 @@ const Field = Object.freeze({ }), }, mustNot: { - eq: (field, value) => ({ + eq: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.EQ, }), - exact: (field, value) => ({ + exact: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.EXACT, }), - gt: (field, value) => ({ + gt: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.GT, }), - gte: (field, value) => ({ + gte: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.GTE, }), - lt: (field, value) => ({ + lt: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.LT, }), - lte: (field, value) => ({ + lte: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, @@ -190,26 +224,26 @@ const Field = Object.freeze({ }); const Is = Object.freeze({ - TYPE: 'is', - isInstance: clause => { + TYPE: 'is' as const, + isInstance: (clause: Clause): clause is IsClause => { return clause.type === Is.TYPE; }, - must: flag => { + must: (flag: string) => { return { type: Is.TYPE, flag, match: Match.MUST }; }, - mustNot: flag => { + mustNot: (flag: string) => { return { type: Is.TYPE, flag, match: Match.MUST_NOT }; }, }); -const valuesEqual = (v1, v2) => { +const valuesEqual = (v1: any, v2: any) => { if (isDateValue(v1)) { return dateValuesEqual(v1, v2); } return v1 === v2; }; -const arrayIncludesValue = (array, value) => { +const arrayIncludesValue = (array: any[], value: any) => { return array.some(item => valuesEqual(item, value)); }; @@ -232,36 +266,52 @@ const arrayIncludesValue = (array, value) => { * This AST is immutable - every "mutating" operation returns a newly mutated AST. */ export class _AST { - static create(clauses) { + private readonly _clauses: Clause[]; + private readonly _indexedClauses: { + field: { + [field: string]: FieldClause[]; + }; + is: { + [flag: string]: IsClause; + }; + term: TermClause[]; + group: GroupClause[]; + }; + + static create(clauses: Clause[]) { return new _AST(clauses); } - constructor(clauses = []) { + constructor(clauses: Clause[] = []) { this._clauses = clauses; - this._indexedClauses = clauses.reduce( - (map, clause) => { - switch (clause.type) { - case Field.TYPE: - if (!map.field[clause.field]) { - map.field[clause.field] = []; - } - map.field[clause.field].push(clause); - return map; - case Is.TYPE: - map.is[clause.flag] = clause; - return map; - case Term.TYPE: - map.term.push(clause); - return map; - case Group.TYPE: - map.group.push(clause); - return map; - default: - throw new Error(`Unknown query clause type [${clause.type}]`); - } - }, - { field: {}, is: {}, term: [], group: [] } - ); + this._indexedClauses = { field: {}, is: {}, term: [], group: [] }; + + clauses.forEach(clause => { + switch (clause.type) { + case Field.TYPE: + if (!this._indexedClauses.field[clause.field]) { + this._indexedClauses.field[clause.field] = []; + } + this._indexedClauses.field[clause.field].push(clause); + break; + + case Is.TYPE: + this._indexedClauses.is[clause.flag] = clause; + break; + + case Term.TYPE: + this._indexedClauses.term.push(clause); + break; + + case Group.TYPE: + this._indexedClauses.group.push(clause); + break; + + default: + // @ts-ignore TS knows we have exhausted the match + throw new Error(`Unknown query clause type [${clause.type}]`); + } + }); } get clauses() { @@ -272,7 +322,7 @@ export class _AST { return this._indexedClauses.term; } - getTermClause(value) { + getTermClause(value: Value) { const clauses = this.getTermClauses(); return clauses.find(clause => valuesEqual(clause.value, value)); } @@ -281,31 +331,33 @@ export class _AST { return Object.keys(this._indexedClauses.field); } - getFieldClauses(field = undefined) { + getFieldClauses(field?: string): FieldClause[] { return field ? this._indexedClauses.field[field] : this._clauses.filter(Field.isInstance); } - getFieldClause(field, predicate) { + getFieldClause( + field: string, + predicate: (c: FieldClause) => boolean + ): FieldClause | undefined { const clauses = this.getFieldClauses(field); if (clauses) { return clauses.find(predicate); } } - hasOrFieldClause(field, value = undefined) { - const clauses = this.getFieldClause(field, clause => isArray(clause.value)); - if (!clauses) { + hasOrFieldClause(field: string, value?: Value) { + const clause = this.getFieldClause(field, clause => isArray(clause.value)); + if (!clause) { return false; } - return ( - isNil(value) || - clauses.some(clause => arrayIncludesValue(clause.value, value)) - ); + + // We can apply this type cast due to the `isArray` filter above + return isNil(value) || arrayIncludesValue(clause.value as Value[], value); } - getOrFieldClause(field, value = undefined) { + getOrFieldClause(field: string, value?: Value) { return this.getFieldClause( field, clause => @@ -314,7 +366,12 @@ export class _AST { ); } - addOrFieldValue(field, value, must = true, operator = Operator.EQ) { + addOrFieldValue( + field: string, + value: Value, + must = true, + operator: OperatorType = Operator.EQ + ) { const existingClause = this.getOrFieldClause(field); if (!existingClause) { const newClause = must @@ -322,38 +379,45 @@ export class _AST { : Field.mustNot[operator](field, [value]); return new _AST([...this._clauses, newClause]); } + const clauses = this._clauses.map(clause => { if (clause === existingClause) { - clause.value.push(value); + (clause.value as Value[]).push(value); } return clause; }); return new _AST(clauses); } - removeOrFieldValue(field, value) { + removeOrFieldValue(field: string, value: Value) { const existingClause = this.getOrFieldClause(field, value); if (!existingClause) { return new _AST([...this._clauses]); } - const clauses = this._clauses.reduce((clauses, clause) => { - if (clause !== existingClause) { - clauses.push(clause); - return clauses; - } - const filteredValue = clause.value.filter( - val => !valuesEqual(val, value) - ); - if (filteredValue.length === 0) { + const clauses = this._clauses.reduce( + (clauses, clause) => { + if (clause !== existingClause) { + clauses.push(clause); + return clauses; + } + const filteredValue = (clause.value as Value[]).filter( + val => !valuesEqual(val, value) + ); + if (filteredValue.length === 0) { + return clauses; + } + clauses.push({ + ...clause, + value: filteredValue, + }); return clauses; - } - clauses.push({ ...clause, value: filteredValue }); - return clauses; - }, []); + }, + [] as Clause[] + ); return new _AST(clauses); } - removeOrFieldClauses(field) { + removeOrFieldClauses(field: string) { const clauses = this._clauses.filter(clause => { return ( !Field.isInstance(clause) || @@ -364,20 +428,15 @@ export class _AST { return new _AST(clauses); } - hasSimpleFieldClause(field, value = undefined) { - const clauses = this.getFieldClause( - field, - clause => !isArray(clause.value) - ); - if (!clauses) { + hasSimpleFieldClause(field: string, value?: Value) { + const clause = this.getFieldClause(field, clause => !isArray(clause.value)); + if (!clause) { return false; } - return ( - isNil(value) || clauses.some(clause => valuesEqual(clause.value, value)) - ); + return isNil(value) || valuesEqual(clause.value, value); } - getSimpleFieldClause(field, value = undefined) { + getSimpleFieldClause(field: string, value?: Value) { return this.getFieldClause( field, clause => @@ -386,14 +445,19 @@ export class _AST { ); } - addSimpleFieldValue(field, value, must = true, operator = Operator.EQ) { + addSimpleFieldValue( + field: string, + value: Value, + must = true, + operator: OperatorType = Operator.EQ + ) { const clause = must ? Field.must[operator](field, value) : Field.mustNot[operator](field, value); return this.addClause(clause); } - removeSimpleFieldValue(field, value) { + removeSimpleFieldValue(field: string, value: Value) { const existingClause = this.getSimpleFieldClause(field, value); if (!existingClause) { return new _AST([...this._clauses]); @@ -402,7 +466,7 @@ export class _AST { return new _AST(clauses); } - removeSimpleFieldClauses(field) { + removeSimpleFieldClauses(field: string) { const clauses = this._clauses.filter(clause => { return ( !Field.isInstance(clause) || @@ -417,11 +481,11 @@ export class _AST { return Object.values(this._indexedClauses.is); } - getIsClause(flag) { + getIsClause(flag: string) { return this._indexedClauses.is[flag]; } - removeIsClause(flag) { + removeIsClause(flag: string) { return new _AST( this._clauses.filter( clause => !Is.isInstance(clause) || clause.flag !== flag @@ -452,42 +516,50 @@ export class _AST { * note: in-place replacement means the given clause will be placed in the same position as the one it * replaced */ - addClause(newClause) { + addClause(newClause: Clause) { let added = false; - const newClauses = this._clauses.reduce((clauses, clause) => { - if (newClause.type !== clause.type) { - clauses.push(clause); + const newClauses = this._clauses.reduce( + (clauses, clause) => { + if (newClause.type !== clause.type) { + clauses.push(clause); + return clauses; + } + + switch (newClause.type) { + case Term.TYPE: + if (newClause.value !== (clause as TermClause).value) { + clauses.push(clause); + return clauses; + } + break; + + case Field.TYPE: + if ( + newClause.field !== (clause as FieldClause).field || + newClause.value !== (clause as FieldClause).value + ) { + clauses.push(clause); + return clauses; + } + break; + + case Is.TYPE: + if (newClause.flag !== (clause as IsClause).flag) { + clauses.push(clause); + return clauses; + } + break; + + default: + throw new Error(`unknown clause type [${newClause.type}]`); + } + added = true; + clauses.push(newClause); return clauses; - } - switch (newClause.type) { - case Term.TYPE: - if (newClause.value !== clause.value) { - clauses.push(clause); - return clauses; - } - break; - case Field.TYPE: - if ( - newClause.field !== clause.field || - newClause.value !== clause.value - ) { - clauses.push(clause); - return clauses; - } - break; - case Is.TYPE: - if (newClause.flag !== clause.flag) { - clauses.push(clause); - return clauses; - } - break; - default: - throw new Error(`unknown clause type [${newClause.type}]`); - } - added = true; - clauses.push(newClause); - return clauses; - }, []); + }, + [] as Clause[] + ); + if (!added) { newClauses.push(newClause); } @@ -502,5 +574,5 @@ export const AST = Object.freeze({ Group, Field, Is, - create: clauses => new _AST(clauses), + create: (clauses: Clause[]) => new _AST(clauses), }); diff --git a/src/components/search_bar/query/ast_to_es_query_dsl.test.js b/src/components/search_bar/query/ast_to_es_query_dsl.test.ts similarity index 93% rename from src/components/search_bar/query/ast_to_es_query_dsl.test.js rename to src/components/search_bar/query/ast_to_es_query_dsl.test.ts index dd03a5e6159..a2a5cd9441d 100644 --- a/src/components/search_bar/query/ast_to_es_query_dsl.test.js +++ b/src/components/search_bar/query/ast_to_es_query_dsl.test.ts @@ -99,7 +99,7 @@ describe('astToEsQueryDsl', () => { AST.create([ AST.Field.must.gte( 'date', - dateValue(moment.utc('2004-03-22'), Granularity.DAY) + dateValue(moment.utc('2004-03-22'), Granularity.DAY)! ), ]) ); @@ -111,11 +111,11 @@ describe('astToEsQueryDsl', () => { AST.create([ AST.Field.must.eq( 'date', - dateValue(moment.utc('2004-03'), Granularity.MONTH) + dateValue(moment.utc('2004-03'), Granularity.MONTH)! ), AST.Field.mustNot.lt( 'date', - dateValue(moment.utc('2004-03-10'), Granularity.DAY) + dateValue(moment.utc('2004-03-10'), Granularity.DAY)! ), ]) ); @@ -127,11 +127,11 @@ describe('astToEsQueryDsl', () => { AST.create([ AST.Field.must.gt( 'date', - dateValue(moment.utc('2004-02'), Granularity.MONTH) + dateValue(moment.utc('2004-02'), Granularity.MONTH)! ), AST.Field.mustNot.gte( 'date', - dateValue(moment.utc('2004-03-10'), Granularity.DAY) + dateValue(moment.utc('2004-03-10'), Granularity.DAY)! ), ]) ); diff --git a/src/components/search_bar/query/ast_to_es_query_dsl.js b/src/components/search_bar/query/ast_to_es_query_dsl.ts similarity index 60% rename from src/components/search_bar/query/ast_to_es_query_dsl.js rename to src/components/search_bar/query/ast_to_es_query_dsl.ts index acb69606108..d53e6af627a 100644 --- a/src/components/search_bar/query/ast_to_es_query_dsl.js +++ b/src/components/search_bar/query/ast_to_es_query_dsl.ts @@ -1,9 +1,67 @@ import { printIso8601 } from './date_format'; -import { isDateValue, dateValue } from './date_value'; -import { AST } from './ast'; +import { isDateValue, dateValue, DateValue } from './date_value'; +import { + _AST, + AST, + FieldClause, + IsClause, + OperatorType, + TermClause, + Value, +} from './ast'; import { isArray, isDateLike, isString } from '../../../services/predicate'; +import { keysOf } from '../../common'; -const processDateOperation = (value, operator) => { +export interface QueryContainer { + bool?: BoolQuery; + match_all?: {}; + match?: object; + match_phrase?: object; + range?: object; + term?: object; + simple_query_string?: object; +} + +interface TermsQuery { + must: Value[]; + mustNot: Value[]; +} + +interface BoolQuery { + must?: QueryContainer[]; + must_not?: QueryContainer[]; + should?: QueryContainer[]; +} + +interface FieldsQuery { + must: { + and: { + [field: string]: any; + }; + or: { + [field: string]: any; + }; + }; + mustNot: { + and: { + [field: string]: any; + }; + or: { + [field: string]: any; + }; + }; +} + +type Options = Partial<{ + defaultFields: string[]; + extraMustQueries: QueryContainer[]; + extraMustNotQueries: QueryContainer[]; + termValuesToQuery: (terms: Value[], options: {}) => QueryContainer; + fieldValuesToQuery: (terms: string, options: {}) => QueryContainer; + isFlagToQuery: (flag: string, on: boolean) => QueryContainer; +}>; + +const processDateOperation = (value: DateValue, operator?: OperatorType) => { const { granularity, resolve } = value; let expression = printIso8601(resolve()); if (!granularity) { @@ -13,23 +71,27 @@ const processDateOperation = (value, operator) => { case AST.Operator.GT: expression = `${expression}||+1${granularity.es}/${granularity.es}`; return { operator: AST.Operator.GTE, expression }; + case AST.Operator.GTE: expression = `${expression}||/${granularity.es}`; return { operator, expression }; + case AST.Operator.LT: expression = `${expression}||/${granularity.es}`; return { operator, expression }; + case AST.Operator.LTE: expression = `${expression}||+1${granularity.es}/${granularity.es}`; return { operator: AST.Operator.LT, expression }; + default: expression = `${expression}||/${granularity.es}`; return { expression }; } }; -export const _termValuesToQuery = (values, options) => { - const body = { +export const _termValuesToQuery = (values: Value[], options: Options) => { + const body: { query: string; fields?: string[] } = { query: values.join(' '), }; if (body.query === '') { @@ -43,28 +105,32 @@ export const _termValuesToQuery = (values, options) => { }; }; -export const _fieldValuesToQuery = (field, operations, andOr) => { - const queries = []; +export const _fieldValuesToQuery = ( + field: string, + operations: { [x in OperatorType]: Value[] }, + andOr: 'and' | 'or' +) => { + const queries: QueryContainer[] = []; - Object.keys(operations).forEach(operator => { + keysOf(operations).forEach(operator => { const values = operations[operator]; switch (operator) { case AST.Operator.EQ: - const { terms, phrases, dates } = values.reduce( - (tokenTypes, value) => { - if (isDateValue(value)) { - tokenTypes.dates.push(value); - } else if (isDateLike(value)) { - tokenTypes.dates.push(dateValue(value)); - } else if (isString(value) && value.match(/\s/)) { - tokenTypes.phrases.push(value); - } else { - tokenTypes.terms.push(value); - } - return tokenTypes; - }, - { terms: [], phrases: [], dates: [] } - ); + const terms: Value[] = []; + const phrases: string[] = []; + const dates: DateValue[] = []; + + values.forEach((value: Value) => { + if (isDateValue(value)) { + dates.push(value); + } else if (isDateLike(value)) { + dates.push(dateValue(value)!); + } else if (isString(value) && value.match(/\s/)) { + phrases.push(value); + } else { + terms.push(value); + } + }); if (terms.length > 0) { queries.push({ @@ -100,13 +166,13 @@ export const _fieldValuesToQuery = (field, operations, andOr) => { break; default: - values.forEach(value => { + values.forEach((value: Value) => { if (isDateValue(value)) { const operation = processDateOperation(value, operator); queries.push({ range: { [field]: { - [operation.operator]: operation.expression, + [operation.operator!]: operation.expression, }, }, }); @@ -135,28 +201,28 @@ export const _fieldValuesToQuery = (field, operations, andOr) => { }; }; -export const _isFlagToQuery = (flag, on) => { +export const _isFlagToQuery = (flag: string, on: boolean) => { return { term: { [flag]: on }, }; }; -const collectTerms = clauses => { - return clauses.reduce( - (values, clause) => { - if (AST.Match.isMustClause(clause)) { - values.must.push(clause.value); - } else { - values.mustNot.push(clause.value); - } - return values; - }, - { must: [], mustNot: [] } - ); +const collectTerms = (clauses: TermClause[]) => { + const values: TermsQuery = { must: [], mustNot: [] }; + + for (const clause of clauses) { + if (AST.Match.isMustClause(clause)) { + values.must.push(clause.value); + } else { + values.mustNot.push(clause.value); + } + } + + return values; }; -const collectFields = clauses => { - const fieldArray = (obj, field, operator) => { +const collectFields = (clauses: FieldClause[]) => { + const fieldArray = (obj: any, field: string, operator: OperatorType) => { if (!obj[field]) { obj[field] = {}; } @@ -198,15 +264,23 @@ const collectFields = clauses => { ); }; -const clausesToEsQueryDsl = ({ fields, terms, is }, options = {}) => { +const clausesToEsQueryDsl = ( + { + fields, + terms, + is, + }: { fields: FieldsQuery; terms: TermsQuery; is: IsClause[] }, + options: Options = {} +) => { const extraMustQueries = options.extraMustQueries || []; const extraMustNotQueries = options.extraMustNotQueries || []; const termValuesToQuery = options.termValuesToQuery || _termValuesToQuery; const fieldValuesToQuery = options.fieldValuesToQuery || _fieldValuesToQuery; const isFlagToQuery = options.isFlagToQuery || _isFlagToQuery; - const must = []; + const must: QueryContainer[] = []; must.push(...extraMustQueries); + const termMustQuery = termValuesToQuery(terms.must, options); if (termMustQuery) { must.push(termMustQuery); @@ -221,7 +295,7 @@ const clausesToEsQueryDsl = ({ fields, terms, is }, options = {}) => { must.push(isFlagToQuery(clause.flag, AST.Match.isMustClause(clause))); }); - const mustNot = []; + const mustNot: QueryContainer[] = []; mustNot.push(...extraMustNotQueries); const termMustNotQuery = termValuesToQuery(terms.mustNot, options); if (termMustNotQuery) { @@ -234,7 +308,7 @@ const clausesToEsQueryDsl = ({ fields, terms, is }, options = {}) => { mustNot.push(fieldValuesToQuery(field, fields.mustNot.or[field], 'or')); }); - const bool = {}; + const bool: BoolQuery = {}; if (must.length !== 0) { bool.must = must; } @@ -245,13 +319,13 @@ const clausesToEsQueryDsl = ({ fields, terms, is }, options = {}) => { return bool; }; -const EMPTY_TERMS = { must: [], mustNot: [] }; -const EMPTY_FIELDS = { +const EMPTY_TERMS: TermsQuery = { must: [], mustNot: [] }; +const EMPTY_FIELDS: FieldsQuery = { must: { and: {}, or: {} }, mustNot: { and: {}, or: {} }, }; -export const astToEsQueryDsl = (ast, options) => { +export const astToEsQueryDsl = (ast: _AST, options = {}): QueryContainer => { if (ast.clauses.length === 0) { return { match_all: {} }; } @@ -271,34 +345,37 @@ export const astToEsQueryDsl = (ast, options) => { // there is at least one GroupClause, wrap the above clauses in another layer and append the ORs const must = groupClauses.reduce( (must, groupClause) => { - const clauses = groupClause.value.reduce((clauses, clause) => { - if (AST.Term.isInstance(clause)) { - clauses.push( - clausesToEsQueryDsl({ - terms: collectTerms([clause]), - fields: EMPTY_FIELDS, - is: [], - }) - ); - } else if (AST.Field.isInstance(clause)) { - clauses.push( - clausesToEsQueryDsl({ - terms: EMPTY_TERMS, - fields: collectFields([clause]), - is: [], - }) - ); - } else if (AST.Is.isInstance(clause)) { - clauses.push( - clausesToEsQueryDsl({ - terms: EMPTY_TERMS, - fields: EMPTY_FIELDS, - is: [clause], - }) - ); - } - return clauses; - }, []); + const clauses = groupClause.value.reduce( + (clauses, clause) => { + if (AST.Term.isInstance(clause)) { + clauses.push( + clausesToEsQueryDsl({ + terms: collectTerms([clause]), + fields: EMPTY_FIELDS, + is: [], + }) + ); + } else if (AST.Field.isInstance(clause)) { + clauses.push( + clausesToEsQueryDsl({ + terms: EMPTY_TERMS, + fields: collectFields([clause]), + is: [], + }) + ); + } else if (AST.Is.isInstance(clause)) { + clauses.push( + clausesToEsQueryDsl({ + terms: EMPTY_TERMS, + fields: EMPTY_FIELDS, + is: [clause], + }) + ); + } + return clauses; + }, + [] as BoolQuery[] + ); must.push({ bool: { @@ -308,8 +385,8 @@ export const astToEsQueryDsl = (ast, options) => { return must; }, hasTopMatches // only include the first match group if there are any conditions - ? [{ bool: matchesBool }] - : [] + ? ([{ bool: matchesBool }] as QueryContainer[]) + : ([] as QueryContainer[]) ); return { diff --git a/src/components/search_bar/query/ast_to_es_query_string.test.js b/src/components/search_bar/query/ast_to_es_query_string.test.ts similarity index 93% rename from src/components/search_bar/query/ast_to_es_query_string.test.js rename to src/components/search_bar/query/ast_to_es_query_string.test.ts index 787661b57ef..b95799b1310 100644 --- a/src/components/search_bar/query/ast_to_es_query_string.test.js +++ b/src/components/search_bar/query/ast_to_es_query_string.test.ts @@ -101,7 +101,7 @@ describe('astToEsQueryString', () => { AST.create([ AST.Field.must.gte( 'date', - dateValue(moment.utc('2004-03-22'), Granularity.DAY) + dateValue(moment.utc('2004-03-22'), Granularity.DAY)! ), ]) ); @@ -113,11 +113,11 @@ describe('astToEsQueryString', () => { AST.create([ AST.Field.must.eq( 'date', - dateValue(moment.utc('2004-03'), Granularity.MONTH) + dateValue(moment.utc('2004-03'), Granularity.MONTH)! ), AST.Field.mustNot.lt( 'date', - dateValue(moment.utc('2004-03-10'), Granularity.DAY) + dateValue(moment.utc('2004-03-10'), Granularity.DAY)! ), ]) ); @@ -129,11 +129,11 @@ describe('astToEsQueryString', () => { AST.create([ AST.Field.must.gt( 'date', - dateValue(moment.utc('2004-02'), Granularity.MONTH) + dateValue(moment.utc('2004-02'), Granularity.MONTH)! ), AST.Field.mustNot.gte( 'date', - dateValue(moment.utc('2004-03-10'), Granularity.DAY) + dateValue(moment.utc('2004-03-10'), Granularity.DAY)! ), ]) ); diff --git a/src/components/search_bar/query/ast_to_es_query_string.js b/src/components/search_bar/query/ast_to_es_query_string.ts similarity index 73% rename from src/components/search_bar/query/ast_to_es_query_string.js rename to src/components/search_bar/query/ast_to_es_query_string.ts index a5ba7b2c5d1..36bed7cec03 100644 --- a/src/components/search_bar/query/ast_to_es_query_string.js +++ b/src/components/search_bar/query/ast_to_es_query_string.ts @@ -1,6 +1,19 @@ +import moment from 'moment'; import { printIso8601 } from './date_format'; -import { isDateValue } from './date_value'; -import { AST, Operator } from './ast'; +import { DateValue, isDateValue } from './date_value'; +import { + _AST, + AST, + Clause, + FieldClause, + GroupClause, + IsClause, + MatchType, + Operator, + OperatorType, + TermClause, + Value, +} from './ast'; import { isArray, isDateLike, @@ -9,14 +22,19 @@ import { isNumber, } from '../../../services/predicate'; -const emitMatch = match => { +const emitMatch = (match: MatchType | undefined) => { if (!match) { return ''; } return AST.Match.isMust(match) ? '+' : '-'; }; -const emitFieldDateLikeClause = (field, value, operator, match) => { +const emitFieldDateLikeClause = ( + field: string, + value: moment.Moment | Date, + operator: OperatorType, + match?: MatchType +) => { const matchOp = emitMatch(match); switch (operator) { case Operator.EQ: @@ -34,7 +52,12 @@ const emitFieldDateLikeClause = (field, value, operator, match) => { } }; -const emitFieldDateValueClause = (field, value, operator, match) => { +const emitFieldDateValueClause = ( + field: string, + value: DateValue, + operator: OperatorType, + match?: MatchType +) => { const matchOp = emitMatch(match); const { granularity, resolve } = value; const date = resolve(); @@ -67,7 +90,12 @@ const emitFieldDateValueClause = (field, value, operator, match) => { return emitFieldDateLikeClause(field, date, operator, match); }; -const emitFieldNumericClause = (field, value, operator, match) => { +const emitFieldNumericClause = ( + field: string, + value: number, + operator: OperatorType, + match?: MatchType +) => { const matchOp = emitMatch(match); switch (operator) { case Operator.EQ: @@ -85,7 +113,11 @@ const emitFieldNumericClause = (field, value, operator, match) => { } }; -const emitFieldStringClause = (field, value, match) => { +const emitFieldStringClause = ( + field: string, + value: string, + match?: MatchType +) => { const matchOp = emitMatch(match); if (value.match(/\s/)) { return `${matchOp}${field}:"${value}"`; @@ -93,12 +125,21 @@ const emitFieldStringClause = (field, value, match) => { return `${matchOp}${field}:${value}`; }; -const emitFieldBooleanClause = (field, value, match) => { +const emitFieldBooleanClause = ( + field: string, + value: Value, + match?: MatchType +) => { const matchOp = emitMatch(match); return `${matchOp}${field}:${value}`; }; -const emitFieldSingleValueClause = (field, value, operator, match) => { +const emitFieldSingleValueClause = ( + field: string, + value: Value, + operator: OperatorType, + match?: MatchType +) => { if (isDateValue(value)) { return emitFieldDateValueClause(field, value, operator, match); } @@ -117,10 +158,15 @@ const emitFieldSingleValueClause = (field, value, operator, match) => { throw new Error(`unknown type of field value [${value}]`); }; -const emitFieldClause = (clause, isGroupMember) => { +const emitFieldClause = ( + clause: FieldClause, + isGroupMember: boolean +): string => { const { field, value, operator } = clause; let { match } = clause; - if (isGroupMember && AST.Match.isMust(match)) match = null; + if (isGroupMember && AST.Match.isMust(match)) { + match = undefined; + } if (!isArray(value)) { return emitFieldSingleValueClause(field, value, operator, match); @@ -132,23 +178,25 @@ const emitFieldClause = (clause, isGroupMember) => { return `${matchOp}(${clauses})`; }; -const emitTermClause = (clause, isGroupMember) => { +const emitTermClause = (clause: TermClause, isGroupMember: boolean): string => { const { value } = clause; let { match } = clause; - if (isGroupMember && AST.Match.isMust(match)) match = null; + if (isGroupMember && AST.Match.isMust(match)) { + match = undefined; + } const matchOp = emitMatch(match); return `${matchOp}${value}`; }; -const emitIsClause = (clause, isGroupMember) => { +const emitIsClause = (clause: IsClause, isGroupMember: boolean): string => { const { flag, match } = clause; const matchOp = isGroupMember ? '' : '+'; const flagValue = AST.Match.isMust(match); return `${matchOp}${flag}:${flagValue}`; }; -const emitGroupClause = clause => { +const emitGroupClause = (clause: GroupClause): string => { const { value } = clause; const formattedValues = value.map(clause => { return emitClause(clause, true); @@ -156,7 +204,7 @@ const emitGroupClause = clause => { return `+(${formattedValues.join(' ')})`; }; -function emitClause(clause, isGroupMember = false) { +function emitClause(clause: Clause, isGroupMember = false) { if (AST.Field.isInstance(clause)) { return emitFieldClause(clause, isGroupMember); } @@ -167,12 +215,12 @@ function emitClause(clause, isGroupMember = false) { return emitIsClause(clause, isGroupMember); } if (AST.Group.isInstance(clause)) { - return emitGroupClause(clause, isGroupMember); + return emitGroupClause(clause); } throw new Error(`unknown clause type [${JSON.stringify(clause)}]`); } -export const astToEsQueryString = ast => { +export const astToEsQueryString = (ast: _AST) => { if (ast.clauses.length === 0) { return '*'; } diff --git a/src/components/search_bar/query/date_format.test.js b/src/components/search_bar/query/date_format.test.ts similarity index 99% rename from src/components/search_bar/query/date_format.test.js rename to src/components/search_bar/query/date_format.test.ts index 410c66d2164..6e7d64f5bcf 100644 --- a/src/components/search_bar/query/date_format.test.js +++ b/src/components/search_bar/query/date_format.test.ts @@ -1,5 +1,5 @@ import { dateFormat, dateGranularity, Granularity } from './date_format'; -import { Random } from '../../../services/random'; +import { Random } from '../../../services'; import moment from 'moment'; const random = new Random(); diff --git a/src/components/search_bar/query/date_format.js b/src/components/search_bar/query/date_format.ts similarity index 77% rename from src/components/search_bar/query/date_format.js rename to src/components/search_bar/query/date_format.ts index 097ade9a825..23435fe0b86 100644 --- a/src/components/search_bar/query/date_format.js +++ b/src/components/search_bar/query/date_format.ts @@ -1,12 +1,35 @@ import { dateFormatAliases } from '../../../services/format'; -import moment from 'moment'; +// ESLint doesn't realise that we can import Moment directly. +// eslint-disable-next-line import/named +import moment, { Moment, MomentInput } from 'moment'; const utc = moment.utc; const GRANULARITY_KEY = '__eui_granularity'; const FORMAT_KEY = '__eui_format'; -export const Granularity = Object.freeze({ +export interface EuiMoment extends Moment { + __eui_granularity?: GranularityType; + __eui_format?: string; +} + +export interface GranularityType { + es: 'd' | 'w' | 'M' | 'y'; + js: 'day' | 'week' | 'month' | 'year'; + isSame: (d1: Moment, d2: Moment) => boolean; + start: (date: Moment) => Moment; + startOfNext: (date: Moment) => Moment; + iso8601: (date: Moment) => string; +} + +interface GranularitiesType { + DAY: GranularityType; + WEEK: GranularityType; + MONTH: GranularityType; + YEAR: GranularityType; +} + +export const Granularity: GranularitiesType = Object.freeze({ DAY: { es: 'd', js: 'day', @@ -41,26 +64,28 @@ export const Granularity = Object.freeze({ }, }); -const parseTime = value => { - const parsed = utc( +const parseTime = (value: string) => { + const parsed: EuiMoment = utc( value, ['HH:mm', 'H:mm', 'H:mm', 'h:mm a', 'h:mm A', 'hh:mm a', 'hh:mm A'], true ); if (parsed.isValid()) { - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } }; -const parseDay = value => { - let parsed = null; +const parseDay = (value: string) => { + let parsed: EuiMoment; + switch (value.toLowerCase()) { case 'today': parsed = utc().startOf('day'); parsed[GRANULARITY_KEY] = Granularity.DAY; parsed[FORMAT_KEY] = value; return parsed; + case 'yesterday': parsed = utc() .subtract(1, 'days') @@ -68,6 +93,7 @@ const parseDay = value => { parsed[GRANULARITY_KEY] = Granularity.DAY; parsed[FORMAT_KEY] = value; return parsed; + case 'tomorrow': parsed = utc() .add(1, 'days') @@ -75,6 +101,7 @@ const parseDay = value => { parsed[GRANULARITY_KEY] = Granularity.DAY; parsed[FORMAT_KEY] = value; return parsed; + default: parsed = utc( value, @@ -100,7 +127,7 @@ const parseDay = value => { if (parsed.isValid()) { try { parsed[GRANULARITY_KEY] = Granularity.DAY; - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } catch (e) { console.error(e); @@ -109,8 +136,8 @@ const parseDay = value => { } }; -const parseWeek = value => { - let parsed = null; +const parseWeek = (value: string) => { + let parsed: EuiMoment; switch (value.toLowerCase()) { case 'this week': parsed = utc(); @@ -126,18 +153,20 @@ const parseWeek = value => { if (match) { const weekNr = Number(match[1]); parsed = utc().weeks(weekNr); + } else { + return; } } - if (parsed && parsed.isValid()) { + if (parsed != null && parsed.isValid()) { parsed = parsed.startOf('week'); parsed[GRANULARITY_KEY] = Granularity.WEEK; - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } }; -const parseMonth = value => { - let parsed = null; +const parseMonth = (value: string) => { + let parsed: EuiMoment; switch (value.toLowerCase()) { case 'this month': parsed = utc(); @@ -156,7 +185,7 @@ const parseMonth = value => { parsed = utc(value, ['MMM', 'MMMM'], true); if (parsed.isValid()) { const now = utc(); - parsed.year(now.year); + parsed.year(now.year()); } else { parsed = utc( value, @@ -176,13 +205,13 @@ const parseMonth = value => { if (parsed.isValid()) { parsed.startOf('month'); parsed[GRANULARITY_KEY] = Granularity.MONTH; - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } }; -const parseYear = value => { - let parsed = null; +const parseYear = (value: string) => { + let parsed: EuiMoment; switch (value.toLowerCase()) { case 'this year': parsed = utc().startOf('year'); @@ -209,14 +238,14 @@ const parseYear = value => { parsed = utc(value, ['YY', 'YYYY'], true); if (parsed.isValid()) { parsed[GRANULARITY_KEY] = Granularity.YEAR; - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } } }; -const parseDefault = value => { - let parsed = utc( +const parseDefault = (value: string) => { + let parsed: EuiMoment = utc( value, [ moment.ISO_8601, @@ -237,12 +266,12 @@ const parseDefault = value => { parsed.add(offset, 'minutes'); } if (parsed.isValid()) { - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; } return parsed; }; -const printDay = (now, date, format) => { +const printDay = (now: Moment, date: Moment, format: string) => { if (format.match(/yesterday|tomorrow|today/i)) { if (now.isSame(date, 'day')) { return 'today'; @@ -260,7 +289,7 @@ const printDay = (now, date, format) => { return date.format(format); }; -const printWeek = (now, date, format) => { +const printWeek = (now: Moment, date: Moment, format: string) => { if (format.match(/(?:this|next|last) week/i)) { if (now.isSame(date, 'week')) { return 'This Week'; @@ -285,7 +314,7 @@ const printWeek = (now, date, format) => { return date.format(format); }; -const printMonth = (now, date, format) => { +const printMonth = (now: Moment, date: Moment, format: string) => { if (format.match(/(?:this|next|last) month/i)) { if (now.isSame(date, 'month')) { return 'This Month'; @@ -310,7 +339,7 @@ const printMonth = (now, date, format) => { return date.format(format); }; -const printYear = (now, date, format) => { +const printYear = (now: Moment, date: Moment, format: string) => { if (format.match(/(?:this|next|last) year/i)) { if (now.isSame(date, 'year')) { return 'This Year'; @@ -335,16 +364,16 @@ const printYear = (now, date, format) => { return date.format(format); }; -export const printIso8601 = value => { +export const printIso8601 = (value: MomentInput) => { return utc(value).format(moment.defaultFormatUtc); }; -export const dateGranularity = parsedDate => { - return parsedDate[GRANULARITY_KEY]; +export const dateGranularity = (parsedDate: EuiMoment) => { + return parsedDate[GRANULARITY_KEY]!; }; export const dateFormat = Object.freeze({ - parse(value) { + parse(value: string) { const parsed = parseDay(value) || parseMonth(value) || @@ -358,14 +387,15 @@ export const dateFormat = Object.freeze({ return parsed; }, - print(date, defaultGranularity = undefined) { + print(date: EuiMoment | MomentInput, defaultGranularity = undefined) { date = moment.isMoment(date) ? date : utc(date); + const euiDate: EuiMoment = date as EuiMoment; const now = utc(); - const format = date[FORMAT_KEY]; + const format = euiDate[FORMAT_KEY]; if (!format) { return date.format(dateFormatAliases.iso8601); } - const granularity = date[GRANULARITY_KEY] || defaultGranularity; + const granularity = euiDate[GRANULARITY_KEY] || defaultGranularity; switch (granularity) { case Granularity.DAY: return printDay(now, date, format); diff --git a/src/components/search_bar/query/date_value.test.js b/src/components/search_bar/query/date_value.test.ts similarity index 83% rename from src/components/search_bar/query/date_value.test.js rename to src/components/search_bar/query/date_value.test.ts index 28951e0721e..3ec4be0ad2b 100644 --- a/src/components/search_bar/query/date_value.test.js +++ b/src/components/search_bar/query/date_value.test.ts @@ -1,5 +1,5 @@ import { dateValueParser, isDateValue } from './date_value'; -import { Random } from '../../../services/random'; +import { Random } from '../../../services'; const random = new Random(); @@ -11,9 +11,10 @@ describe('date value', () => { const format = { parse, print: jest.fn() }; const parser = dateValueParser(format); const value = parser('dateString'); + expect(parse.mock.calls.length).toBe(1); expect(parse.mock.calls[0][0]).toBe('dateString'); expect(isDateValue(value)).toBe(true); - expect(value.resolve().isSame(date)).toBe(true); + expect(value!.resolve().isSame(date)).toBe(true); }); }); diff --git a/src/components/search_bar/query/date_value.js b/src/components/search_bar/query/date_value.ts similarity index 60% rename from src/components/search_bar/query/date_value.js rename to src/components/search_bar/query/date_value.ts index caa714f5a2e..95c063d6a1e 100644 --- a/src/components/search_bar/query/date_value.js +++ b/src/components/search_bar/query/date_value.ts @@ -2,12 +2,23 @@ import { isDateLike, isNumber } from '../../../services/predicate'; import { dateFormat as defaultDateFormat, dateGranularity, + GranularityType, } from './date_format'; -import moment from 'moment'; +// ESLint doesn't realise that we can import Moment directly. +// eslint-disable-next-line import/named +import moment, { MomentInput } from 'moment'; export const DATE_TYPE = 'date'; -export const dateValuesEqual = (v1, v2) => { +export interface DateValue { + type: 'date'; + raw: MomentInput; + granularity: GranularityType | undefined; + text: string; + resolve: () => moment.Moment; +} + +export const dateValuesEqual = (v1: DateValue, v2: DateValue) => { return ( v1.raw === v2.raw && v1.granularity === v2.granularity && @@ -15,7 +26,7 @@ export const dateValuesEqual = (v1, v2) => { ); }; -export const isDateValue = value => { +export const isDateValue = (value: any): value is DateValue => { return ( !!value && value.type === DATE_TYPE && @@ -25,18 +36,28 @@ export const isDateValue = value => { ); }; -export const dateValue = (raw, granularity, dateFormat = defaultDateFormat) => { +export const dateValue: ( + raw: MomentInput, + granularity?: GranularityType, + dateFormat?: any +) => DateValue | undefined = ( + raw, + granularity, + dateFormat = defaultDateFormat +) => { if (!raw) { return undefined; } + if (isDateLike(raw)) { - return { + const dateValue: DateValue = { type: DATE_TYPE, raw, granularity, text: dateFormat.print(raw), resolve: () => moment(raw), }; + return dateValue; } if (isNumber(raw)) { return { @@ -58,7 +79,7 @@ export const dateValue = (raw, granularity, dateFormat = defaultDateFormat) => { }; export const dateValueParser = (format = defaultDateFormat) => { - return text => { + return (text: string) => { const parsed = format.parse(text); return dateValue(text, dateGranularity(parsed), format); }; diff --git a/src/components/search_bar/query/default_syntax.test.js b/src/components/search_bar/query/default_syntax.test.ts similarity index 85% rename from src/components/search_bar/query/default_syntax.test.js rename to src/components/search_bar/query/default_syntax.test.ts index ff5949befda..5ec75d0b0e7 100644 --- a/src/components/search_bar/query/default_syntax.test.js +++ b/src/components/search_bar/query/default_syntax.test.ts @@ -1,8 +1,8 @@ import { defaultSyntax } from './default_syntax'; -import { AST } from './ast'; +import { AST, Clause, FieldClause } from './ast'; import { Granularity } from './date_format'; -import { isDateValue } from './date_value'; -import { Random } from '../../../services/random'; +import { DateValue, isDateValue } from './date_value'; +import { Random } from '../../../services'; const random = new Random(); @@ -28,39 +28,39 @@ describe('defaultSyntax', () => { expect(ast.clauses).toBeDefined(); expect(ast.clauses).toHaveLength(6); - let clause = ast.getTermClause('term-1'); + let clause: Clause = ast.getTermClause('term-1')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('term-1'); - clause = ast.getTermClause('term-2'); + clause = ast.getTermClause('term-2')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe('term-2'); - clause = ast.getTermClause('-term-3'); + clause = ast.getTermClause('-term-3')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('-term-3'); - clause = ast.getSimpleFieldClause('name-1', 'dash-1'); + clause = ast.getSimpleFieldClause('name-1', 'dash-1')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name-1'); expect(clause.value).toBe('dash-1'); - clause = ast.getSimpleFieldClause('name-2', 'dash-2'); + clause = ast.getSimpleFieldClause('name-2', 'dash-2')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.field).toBe('name-2'); expect(clause.value).toBe('dash-2'); - clause = ast.getSimpleFieldClause('-name-3', 'dash-3'); + clause = ast.getSimpleFieldClause('-name-3', 'dash-3')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -76,14 +76,14 @@ describe('defaultSyntax', () => { expect(ast.clauses).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getSimpleFieldClause('name', '👸Queen_Elizabeth'); + let clause: Clause = ast.getSimpleFieldClause('name', '👸Queen_Elizabeth')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('👸Queen_Elizabeth'); - clause = ast.getTermClause('🤴King_Henry'); + clause = ast.getTermClause('🤴King_Henry')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -98,25 +98,25 @@ describe('defaultSyntax', () => { expect(ast.clauses).toBeDefined(); expect(ast.clauses).toHaveLength(4); - let clause = ast.getTermClause(':'); + let clause: Clause = ast.getTermClause(':')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe(':'); - clause = ast.getTermClause('\\'); + clause = ast.getTermClause('\\')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('\\'); - clause = ast.getTermClause('('); + clause = ast.getTermClause('(')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('('); - clause = ast.getTermClause(')'); + clause = ast.getTermClause(')')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -133,7 +133,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name', 'john'); + const clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -151,7 +151,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('n:ame', 'jo:h:n'); + const clause: Clause = ast.getSimpleFieldClause('n:ame', 'jo:h:n')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -169,7 +169,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('na_me', 'john'); + const clause: Clause = ast.getSimpleFieldClause('na_me', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -189,7 +189,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name', 'jo-h:n'); + const clause: Clause = ast.getSimpleFieldClause('name', 'jo-h:n')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -207,14 +207,14 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getSimpleFieldClause('name', 'john'); + let clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getSimpleFieldClause('age', 6); + clause = ast.getSimpleFieldClause('age', 6)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -232,21 +232,21 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(3); - let clause = ast.getSimpleFieldClause('name', 'john'); + let clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getSimpleFieldClause('age', 6); + clause = ast.getSimpleFieldClause('age', 6)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(6); - clause = ast.getSimpleFieldClause('age', 5); + clause = ast.getSimpleFieldClause('age', 5)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -264,21 +264,21 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(3); - let clause = ast.getSimpleFieldClause('name', 'john'); + let clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getSimpleFieldClause('age', 6); + clause = ast.getSimpleFieldClause('age', 6)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(6); - clause = ast.getSimpleFieldClause('age', 5); + clause = ast.getSimpleFieldClause('age', 5)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -296,13 +296,13 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getTermClause('foo'); + let clause: Clause = ast.getTermClause('foo')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getTermClause('bar'); + clause = ast.getTermClause('bar')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -319,13 +319,13 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getTermClause('foo'); + let clause: Clause = ast.getTermClause('foo')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getTermClause('bar'); + clause = ast.getTermClause('bar')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -342,33 +342,33 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(5); - let clause = ast.getTermClause('foo'); + let clause: Clause = ast.getTermClause('foo')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getSimpleFieldClause('name', 'john'); + clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getTermClause('bar'); + clause = ast.getTermClause('bar')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe('bar'); - clause = ast.getSimpleFieldClause('age', 5); + clause = ast.getSimpleFieldClause('age', 5)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(5); - clause = ast.getSimpleFieldClause('name', 'joe'); + clause = ast.getSimpleFieldClause('name', 'joe')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -386,46 +386,46 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(7); - let clause = ast.getTermClause('foo'); + let clause: Clause = ast.getTermClause('foo')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getSimpleFieldClause('name', 'john'); + clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getTermClause('bar'); + clause = ast.getTermClause('bar')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe('bar'); - clause = ast.getSimpleFieldClause('age', 5); + clause = ast.getSimpleFieldClause('age', 5)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(5); - clause = ast.getSimpleFieldClause('name', 'joe'); + clause = ast.getSimpleFieldClause('name', 'joe')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('joe'); - clause = ast.getIsClause('open'); + clause = ast.getIsClause('open')!; expect(clause).toBeDefined(); expect(AST.Is.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.flag).toBe('open'); - clause = ast.getIsClause('liberal'); + clause = ast.getIsClause('liberal')!; expect(clause).toBeDefined(); expect(AST.Is.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -442,7 +442,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getTermClause('foo (bar)'); + const clause: Clause = ast.getTermClause('foo (bar)')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -459,7 +459,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('field', 'foo bar'); + const clause: Clause = ast.getSimpleFieldClause('field', 'foo bar')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -478,7 +478,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getTermClause(''); + const clause: Clause = ast.getTermClause('')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -495,7 +495,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('field', ''); + const clause: Clause = ast.getSimpleFieldClause('field', '')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -514,7 +514,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getOrFieldClause('field'); + const clause: Clause = ast.getOrFieldClause('field')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -534,7 +534,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getOrFieldClause('field1'); + let clause: Clause = ast.getOrFieldClause('field1')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -543,7 +543,7 @@ describe('defaultSyntax', () => { expect(clause.value).toContain('foo'); expect(clause.value).toContain('bar baz'); - clause = ast.getSimpleFieldClause('field2'); + clause = ast.getSimpleFieldClause('field2')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -561,7 +561,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('f'); + const clause: Clause = ast.getSimpleFieldClause('f')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -579,7 +579,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getOrFieldClause('f'); + const clause: Clause = ast.getOrFieldClause('f')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -598,17 +598,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('created'); + const clause: Clause = ast.getSimpleFieldClause('created')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isEQClause(clause)).toBe(true); expect(clause.field).toBe('created'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('12 Jan 2010'); - expect(clause.value.text).toBe('12 Jan 2010'); - expect(clause.value.granularity).toBe(Granularity.DAY); + expect((clause.value as DateValue).raw).toBe('12 Jan 2010'); + expect((clause.value as DateValue).text).toBe('12 Jan 2010'); + expect((clause.value as DateValue).granularity).toBe(Granularity.DAY); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -621,17 +622,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('expires'); + const clause: Clause = ast.getSimpleFieldClause('expires')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isGTClause(clause)).toBe(true); expect(clause.field).toBe('expires'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('last week'); - expect(clause.value.text).toBe('last week'); - expect(clause.value.granularity).toBe(Granularity.WEEK); + expect((clause.value as DateValue).raw).toBe('last week'); + expect((clause.value as DateValue).text).toBe('last week'); + expect((clause.value as DateValue).granularity).toBe(Granularity.WEEK); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -644,17 +646,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('expires'); + const clause: Clause = ast.getSimpleFieldClause('expires')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isGTEClause(clause)).toBe(true); expect(clause.field).toBe('expires'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('next year'); - expect(clause.value.text).toBe('next year'); - expect(clause.value.granularity).toBe(Granularity.YEAR); + expect((clause.value as DateValue).raw).toBe('next year'); + expect((clause.value as DateValue).text).toBe('next year'); + expect((clause.value as DateValue).granularity).toBe(Granularity.YEAR); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -667,17 +670,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('created'); + const clause: Clause = ast.getSimpleFieldClause('created')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isLTClause(clause)).toBe(true); expect(clause.field).toBe('created'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('last month'); - expect(clause.value.text).toBe('last month'); - expect(clause.value.granularity).toBe(Granularity.MONTH); + expect((clause.value as DateValue).raw).toBe('last month'); + expect((clause.value as DateValue).text).toBe('last month'); + expect((clause.value as DateValue).granularity).toBe(Granularity.MONTH); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -690,17 +694,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('created'); + const clause: Clause = ast.getSimpleFieldClause('created')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isLTEClause(clause)).toBe(true); expect(clause.field).toBe('created'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('Sunday'); - expect(clause.value.text).toBe('Sunday'); - expect(clause.value.granularity).toBe(Granularity.DAY); + expect((clause.value as DateValue).raw).toBe('Sunday'); + expect((clause.value as DateValue).text).toBe('Sunday'); + expect((clause.value as DateValue).granularity).toBe(Granularity.DAY); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -713,7 +718,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getSimpleFieldClause('active'); + let clause: Clause = ast.getSimpleFieldClause('active')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -721,10 +726,10 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('active'); expect(clause.value).toBe(true); - clause = ast.getSimpleFieldClause('closed'); + clause = ast.getSimpleFieldClause('closed')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); - expect(AST.Match.isMust(clause)).toBe(false); + expect(AST.Match.isMustClause(clause)).toBe(false); expect(AST.Operator.isEQClause(clause)).toBe(true); expect(clause.field).toBe('closed'); expect(clause.value).toBe(false); @@ -740,7 +745,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('active'); + const clause: Clause = ast.getSimpleFieldClause('active')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -794,7 +799,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name', 'john'); + const clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -813,7 +818,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(4); - let clause = ast.getSimpleFieldClause('num1'); + let clause: Clause = ast.getSimpleFieldClause('num1')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -821,7 +826,7 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('num1'); expect(clause.value).toBe(6); - clause = ast.getSimpleFieldClause('num2'); + clause = ast.getSimpleFieldClause('num2')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -829,7 +834,7 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('num2'); expect(clause.value).toBe(8); - clause = ast.getSimpleFieldClause('num3'); + clause = ast.getSimpleFieldClause('num3')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -837,7 +842,7 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('num3'); expect(clause.value).toBe(4); - clause = ast.getSimpleFieldClause('num4'); + clause = ast.getSimpleFieldClause('num4')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -856,7 +861,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('count'); + const clause: Clause = ast.getSimpleFieldClause('count')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -871,7 +876,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('count'); + const clause: Clause = ast.getSimpleFieldClause('count')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -890,7 +895,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getIsClause('active'); + const clause: Clause = ast.getIsClause('active')!; expect(clause).toBeDefined(); expect(AST.Is.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -912,7 +917,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getIsClause('active'); + const clause: Clause = ast.getIsClause('active')!; expect(clause).toBeDefined(); expect(AST.Is.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -925,7 +930,7 @@ describe('defaultSyntax', () => { strict: true, fields: { active: { - type: random.oneOf('number', 'string', 'date'), + type: random.oneOf(['number', 'string', 'date']), }, }, }; @@ -959,7 +964,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name'); + const clause: Clause = ast.getSimpleFieldClause('name')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -1041,7 +1046,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name'); + const clause: Clause = ast.getSimpleFieldClause('name')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -1067,7 +1072,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - const ageClause = ast.getSimpleFieldClause('age'); + const ageClause: Clause = ast.getSimpleFieldClause('age')!; expect(ageClause).toBeDefined(); expect(AST.Field.isInstance(ageClause)).toBe(true); expect(AST.Match.isMustClause(ageClause)).toBe(true); @@ -1086,13 +1091,13 @@ describe('defaultSyntax', () => { expect(AST.Field.isInstance(nameClauseA)).toBe(true); expect(AST.Match.isMustClause(nameClauseA)).toBe(true); - expect(nameClauseA.field).toBe('name'); - expect(nameClauseA.value).toBe('john'); + expect((nameClauseA as FieldClause).field).toBe('name'); + expect((nameClauseA as FieldClause).value).toBe('john'); expect(AST.Field.isInstance(nameClauseB)).toBe(true); expect(AST.Match.isMustClause(nameClauseB)).toBe(true); - expect(nameClauseB.field).toBe('name'); - expect(nameClauseB.value).toBe('susan'); + expect((nameClauseB as FieldClause).field).toBe('name'); + expect((nameClauseB as FieldClause).value).toBe('susan'); }); test('negated OR clause', () => { @@ -1122,13 +1127,13 @@ describe('defaultSyntax', () => { expect(AST.Field.isInstance(nameClauseA)).toBe(true); expect(AST.Match.isMustClause(nameClauseA)).toBe(true); - expect(nameClauseA.field).toBe('name'); - expect(nameClauseA.value).toBe('john'); + expect((nameClauseA as FieldClause).field).toBe('name'); + expect((nameClauseA as FieldClause).value).toBe('john'); expect(AST.Field.isInstance(nameClauseB)).toBe(true); expect(AST.Match.isMustClause(nameClauseB)).toBe(true); - expect(nameClauseB.field).toBe('name'); - expect(nameClauseB.value).toBe('susan'); + expect((nameClauseB as FieldClause).field).toBe('name'); + expect((nameClauseB as FieldClause).value).toBe('susan'); }); test('or term parsing and printing', () => { @@ -1138,7 +1143,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getTermClause('or'); + const clause: Clause = ast.getTermClause('or')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); diff --git a/src/components/search_bar/query/default_syntax.js b/src/components/search_bar/query/default_syntax.ts similarity index 78% rename from src/components/search_bar/query/default_syntax.js rename to src/components/search_bar/query/default_syntax.ts index 114e174320f..605721ce7dc 100644 --- a/src/components/search_bar/query/default_syntax.js +++ b/src/components/search_bar/query/default_syntax.ts @@ -1,7 +1,8 @@ -import { AST } from './ast'; +import { _AST, AST, Clause, OperatorType, Value } from './ast'; import { isArray, isString, isDateLike } from '../../../services/predicate'; import { dateFormat as defaultDateFormat } from './date_format'; -import { dateValueParser, isDateValue } from './date_value'; +import { DateValue, dateValueParser, isDateValue } from './date_value'; +// @ts-ignore This is a Babel plugin that parses inline PEG grammars. import peg from 'pegjs-inline-precompile'; // eslint-disable-line import/no-unresolved const parser = peg` @@ -109,7 +110,7 @@ identifierChar = alnum / [-_] / escapedChar - + fieldRangeValue = rangeValue @@ -124,7 +125,7 @@ containsOrValues = "(" space? head:containsValue tail:( space orWord space value:containsValue { return value; } )* space? ")" { return [ head, ...tail ]; } - + rangeValue = numberWord / date @@ -144,7 +145,7 @@ phrase phraseWord = orWord / word - / [()] // adding parens directly to "wordChar" makes it too aggresive as it consumes the closing paren + / [()] // adding parens directly to "wordChar" makes it too aggresive as it consumes the closing paren word = wordChar+ { @@ -162,7 +163,7 @@ wordChar / [-_*:/] / escapedChar / extendedGlyph - + // This isn't _strictly_ correct: // for our purposes, a non-ascii word character is considered to // be anything above \`Latin-1 Punctuation & Symbols\`, which ends at U+00BF @@ -197,7 +198,7 @@ boolean number = [\\-]?[0-9]+("."[0-9]+)* { return Exp.number(text(), location()); } -// only match numbers followed by whitespace or end of input +// only match numbers followed by whitespace or end of input numberWord = num:number &space { return num; } / num:number !. { return num; } @@ -214,30 +215,78 @@ space "whitespace" = [ \\t\\n\\r]+ `; -const unescapeValue = value => { +type DataType = 'date' | 'number' | 'string' | 'boolean'; + +interface SchemaField { + type: DataType; + valueDescription: string; + validate?: (value: string | boolean | number | DateValue) => void; +} + +interface Context { + schema: { + fields: { + [name: string]: SchemaField; + }; + flags: string[]; + strict: boolean; + }; + strict: boolean; + error: (message: string, location?: Location) => never; + parseDate: (text: string) => DateValue; +} + +type Expression = any; +type Location = any; + +interface ValueExpression { + type: DataType; + expression: string; + location: Location; +} + +export interface ParseOptions { + dateFormat?: any; + schema?: any; + escapeValue?: (value: any) => string; +} + +const unescapeValue = (value: string) => { return value.replace(/\\([:\-\\()])/g, '$1'); }; -const escapeValue = value => { +const escapeValue = (value: string) => { return value.replace(/([:\-\\()])/g, '\\$1'); }; -const escapeFieldValue = value => { +const escapeFieldValue = (value: string) => { return value.replace(/(\\)/g, '\\$1'); }; const Exp = { - date: (expression, location) => ({ type: 'date', expression, location }), - number: (expression, location) => ({ type: 'number', expression, location }), - string: (expression, location) => ({ type: 'string', expression, location }), - boolean: (expression, location) => ({ + date: (expression: Expression, location: Location) => ({ + type: 'date', + expression, + location, + }), + number: (expression: Expression, location: Location) => ({ + type: 'number', + expression, + location, + }), + string: (expression: Expression, location: Location) => ({ + type: 'string', + expression, + location, + }), + boolean: (expression: Expression, location: Location) => ({ type: 'boolean', expression, location, }), }; -const validateFlag = (flag, location, ctx) => { +const validateFlag = (flag: string, location: Location, ctx: Context) => { if (ctx.schema && ctx.schema.strict) { if (ctx.schema.flags && ctx.schema.flags.includes(flag)) { return; @@ -254,12 +303,12 @@ const validateFlag = (flag, location, ctx) => { }; const validateFieldValue = ( - field, - schemaField, - expression, - value, - location, - error + field: string, + schemaField: SchemaField, + expression: Expression, + value: Value, + location: Location, + error: Context['error'] ) => { if (schemaField && schemaField.validate) { try { @@ -275,10 +324,18 @@ const validateFieldValue = ( } }; -const resolveFieldValue = (field, valueExpression, ctx) => { +const resolveFieldValue = ( + field: string, + valueExpression: ValueExpression | ValueExpression[], + ctx: Context +): Value | Value[] => { const { schema, error, parseDate } = ctx; if (isArray(valueExpression)) { - return valueExpression.map(exp => resolveFieldValue(field, exp, ctx)); + // I don't know if this cast is valid. This function is called recursively and + // doesn't apply any kind of flat-map. + return valueExpression.map( + exp => resolveFieldValue(field, exp, ctx) as Value + ); } const { location } = valueExpression; let { type, expression } = valueExpression; @@ -301,7 +358,7 @@ const resolveFieldValue = (field, valueExpression, ctx) => { } switch (type) { case 'date': - let date = null; + let date: DateValue | null = null; try { date = parseDate(expression); } catch (e) { @@ -310,8 +367,16 @@ const resolveFieldValue = (field, valueExpression, ctx) => { location ); } - validateFieldValue(field, schemaField, expression, date, location, error); - return date; + // error() throws an exception if called, so now `date` is not null. + validateFieldValue( + field, + schemaField, + expression, + date!, + location, + error + ); + return date!; case 'number': const number = Number(expression); @@ -332,6 +397,8 @@ const resolveFieldValue = (field, valueExpression, ctx) => { return number; case 'boolean': + // FIXME This would also match 'lion'. It should really anchor the match + // and the start and end of the input. const boolean = !!expression.match(/true|yes|on/i); validateFieldValue( field, @@ -356,7 +423,7 @@ const resolveFieldValue = (field, valueExpression, ctx) => { } }; -const printValue = (value, options) => { +const printValue = (value: Value, options: ParseOptions) => { if (isDateValue(value)) { return `'${value.text}'`; } @@ -375,7 +442,7 @@ const printValue = (value, options) => { return escapeFn(value); }; -const resolveOperator = operator => { +const resolveOperator = (operator: OperatorType) => { switch (operator) { case AST.Operator.EQ: return ':'; @@ -394,8 +461,14 @@ const resolveOperator = operator => { } }; -export const defaultSyntax = Object.freeze({ - parse: (query, options = {}) => { +export type Syntax = Readonly<{ + printClause: (clause: Clause, text: string, options: any) => string; + print: (ast: _AST, options?: {}) => string; + parse: (query: string, options?: ParseOptions) => _AST; +}>; + +export const defaultSyntax: Syntax = Object.freeze({ + parse: (query: string, options: ParseOptions = {}) => { const dateFormat = options.dateFormat || defaultDateFormat; const parseDate = dateValueParser(dateFormat); const schema = options.schema || {}; @@ -411,7 +484,7 @@ export const defaultSyntax = Object.freeze({ return AST.create(clauses); }, - printClause: (clause, text, options) => { + printClause: (clause: Clause, text: string, options: any): string => { const prefix = AST.Match.isMustClause(clause) ? '' : '-'; switch (clause.type) { case AST.Field.TYPE: @@ -446,7 +519,7 @@ export const defaultSyntax = Object.freeze({ } }, - print: (ast, options = {}) => { + print: (ast: _AST, options = {}) => { return ast.clauses .reduce((text, clause) => { return defaultSyntax.printClause(clause, text, options); diff --git a/src/components/search_bar/query/execute_ast.test.js b/src/components/search_bar/query/execute_ast.test.ts similarity index 99% rename from src/components/search_bar/query/execute_ast.test.js rename to src/components/search_bar/query/execute_ast.test.ts index 5be44c2d11d..11cce9a5c50 100644 --- a/src/components/search_bar/query/execute_ast.test.js +++ b/src/components/search_bar/query/execute_ast.test.ts @@ -1,6 +1,6 @@ import { AST } from './ast'; import { executeAst } from './execute_ast'; -import { Random } from '../../../services/random'; +import { Random } from '../../../services'; const random = new Random(); diff --git a/src/components/search_bar/query/execute_ast.js b/src/components/search_bar/query/execute_ast.ts similarity index 70% rename from src/components/search_bar/query/execute_ast.js rename to src/components/search_bar/query/execute_ast.ts index cb47597ab05..e6cb1490008 100644 --- a/src/components/search_bar/query/execute_ast.js +++ b/src/components/search_bar/query/execute_ast.ts @@ -1,7 +1,25 @@ import { get } from '../../../services/objects'; import { isString, isArray } from '../../../services/predicate'; -import { eq, exact, gt, gte, lt, lte } from './operators'; -import { AST } from './ast'; +import { + ClauseValue, + eq, + exact, + FieldValue, + gt, + gte, + lt, + lte, +} from './operators'; +import { + _AST, + AST, + Clause, + FieldClause, + IsClause, + MatchType, + TermClause, + Value, +} from './ast'; const EXPLAIN_FIELD = '__explain'; @@ -14,7 +32,21 @@ const nameToOperatorMap = { [AST.Operator.LTE]: lte, }; -const defaultIsClauseMatcher = (item, clause, explain) => { +interface Explain { + hit: boolean; + type: Clause['type']; + field?: string; + value?: Value | Value[]; + flag?: string; + match?: MatchType; + operator?: any; // It's not really worth specifying this at the moment +} + +const defaultIsClauseMatcher = ( + item: T, + clause: IsClause, + explain?: Explain[] +) => { const { type, flag, match } = clause; const value = get(item, clause.flag); const must = AST.Match.isMustClause(clause); @@ -25,7 +57,12 @@ const defaultIsClauseMatcher = (item, clause, explain) => { return hit; }; -const fieldClauseMatcher = (item, field, clauses = [], explain) => { +const fieldClauseMatcher = ( + item: T, + field: string, + clauses: FieldClause[] = [], + explain?: Explain[] +) => { return clauses.every(clause => { const { type, value, match } = clause; let operator = nameToOperatorMap[clause.operator]; @@ -34,7 +71,7 @@ const fieldClauseMatcher = (item, field, clauses = [], explain) => { return true; } if (!AST.Match.isMust(match)) { - operator = (value, token) => + operator = (value: FieldValue, token: ClauseValue) => !nameToOperatorMap[clause.operator](value, token); } const itemValue = get(item, field); @@ -48,16 +85,26 @@ const fieldClauseMatcher = (item, field, clauses = [], explain) => { }); }; -const extractStringFieldsFromItem = item => { - return Object.keys(item).reduce((fields, key) => { - if (isString(item[key])) { - fields.push(key); - } - return fields; - }, []); +// You might think that we could specify `item: T` here and do something +// with `keyof`, but that wouldn't work with `nested.field.name` +const extractStringFieldsFromItem = (item: any) => { + return Object.keys(item).reduce( + (fields, key) => { + if (isString(item[key])) { + fields.push(key); + } + return fields; + }, + [] as string[] + ); }; -const termClauseMatcher = (item, fields, clauses = [], explain) => { +const termClauseMatcher = ( + item: T, + fields: string[] | undefined, + clauses: TermClause[] = [], + explain?: Explain[] +) => { const searchableFields = fields || extractStringFieldsFromItem(item); return clauses.every(clause => { const { type, value, match } = clause; @@ -91,18 +138,20 @@ const termClauseMatcher = (item, fields, clauses = [], explain) => { }); }; -export const createFilter = ( - ast, - defaultFields, +export const createFilter = ( + ast: _AST, + defaultFields: string[] | undefined, isClauseMatcher = defaultIsClauseMatcher, explain = false ) => { // Return items which pass ALL conditions: matches the terms entered, the specified field values, // and the specified "is" clauses. - return item => { - const explainLines = explain ? [] : undefined; + return (item: T) => { + const explainLines = explain ? ([] as Explain[]) : undefined; if (explainLines) { + // @ts-ignore technically, we could require T to extend `{ __explain?: Explain[] }` but that seems + // like a ridiculous requirement on the caller. item[EXPLAIN_FIELD] = explainLines; } @@ -155,8 +204,18 @@ export const createFilter = ( }; }; -export const executeAst = (ast, items, options = {}) => { +interface Options { + isClauseMatcher?: typeof defaultIsClauseMatcher; + defaultFields?: string[]; + explain?: boolean; +} + +export function executeAst( + ast: _AST, + items: T[], + options: Options = {} +): T[] { const { isClauseMatcher, defaultFields, explain } = options; const filter = createFilter(ast, defaultFields, isClauseMatcher, explain); return items.filter(filter); -}; +} diff --git a/src/components/search_bar/query/index.js b/src/components/search_bar/query/index.js deleted file mode 100644 index fe0f6519665..00000000000 --- a/src/components/search_bar/query/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { Query } from './query'; -export { AST } from './ast'; -export { dateValueParser as parseDateValue } from './date_value'; diff --git a/src/components/search_bar/query/index.ts b/src/components/search_bar/query/index.ts new file mode 100644 index 00000000000..60e75e781d8 --- /dev/null +++ b/src/components/search_bar/query/index.ts @@ -0,0 +1,2 @@ +export { Query } from './query'; +export { AST } from './ast'; diff --git a/src/components/search_bar/query/operators.test.js b/src/components/search_bar/query/operators.test.ts similarity index 98% rename from src/components/search_bar/query/operators.test.js rename to src/components/search_bar/query/operators.test.ts index 497d2d58057..a86a9ac586d 100644 --- a/src/components/search_bar/query/operators.test.js +++ b/src/components/search_bar/query/operators.test.ts @@ -1,18 +1,28 @@ import moment from 'moment'; import { eq, gt, gte, lt, lte } from './operators'; import { dateValue } from './date_value'; -import { Random } from '../../../services/random'; +import { Random } from '../../../services'; import { Granularity } from './date_format'; const random = new Random(); -const laterMoment = (date, count, units) => { +type TimeUnits = 'hours' | 'days' | 'weeks' | 'months' | 'years'; + +const laterMoment = ( + date: moment.MomentInput, + count: number, + units: TimeUnits +) => { const later = moment(date); later.add(count, units); return later; }; -const earlierMoment = (date, count, units) => { +const earlierMoment = ( + date: moment.MomentInput, + count: number, + units: TimeUnits +) => { const later = moment(date); later.subtract(count, units); return later; diff --git a/src/components/search_bar/query/operators.js b/src/components/search_bar/query/operators.ts similarity index 71% rename from src/components/search_bar/query/operators.js rename to src/components/search_bar/query/operators.ts index d7ae3df4ab7..f9e2d192692 100644 --- a/src/components/search_bar/query/operators.js +++ b/src/components/search_bar/query/operators.ts @@ -9,29 +9,64 @@ import { isNil, } from '../../../services/predicate'; import moment from 'moment'; +import { Value } from './ast'; + +export type FieldValue = + | string + | number + | boolean + | any[] + | Date + | moment.Moment + | null + | undefined; + +export type ClauseValue = Value | Date | moment.Moment | null | undefined; + const utc = moment.utc; -const resolveValueAsDate = value => { +const resolveValueAsDate = (value: FieldValue) => { if (moment.isMoment(value)) { return value; } if (moment.isDate(value) || isNumber(value)) { return moment(value); } - return dateFormat.parse(value.toString()); + return dateFormat.parse(String(value)); }; -const defaultEqOptions = { +type Options = Partial<{ + exactMatch: boolean; + ignoreCase: boolean; +}>; + +const defaultEqOptions: Options = { ignoreCase: true, }; -export const eq = (fieldValue, clauseValue, options = {}) => { +export const eq = ( + fieldValue: FieldValue, + clauseValue: ClauseValue, + options: Options = {} +): boolean => { options = { ...defaultEqOptions, ...options }; if (isNil(fieldValue) || isNil(clauseValue)) { return fieldValue === clauseValue; } + if (isBoolean(fieldValue)) { + return clauseValue === fieldValue; + } + + if (isArray(fieldValue)) { + if (fieldValue.length > 0) { + return fieldValue.some(item => eq(item, clauseValue, options)); + } else { + return eq('', clauseValue, options); + } + } + if (isDateValue(clauseValue)) { const dateFieldValue = resolveValueAsDate(fieldValue); if (clauseValue.granularity) { @@ -62,10 +97,6 @@ export const eq = (fieldValue, clauseValue, options = {}) => { return fieldValue === clauseValue; } - if (isBoolean(fieldValue)) { - return clauseValue === fieldValue; - } - if (isDateLike(fieldValue)) { const date = resolveValueAsDate(clauseValue); if (!date.isValid()) { @@ -75,44 +106,48 @@ export const eq = (fieldValue, clauseValue, options = {}) => { if (!granularity) { return utc(fieldValue).isSame(date); } - return granularity.isSame(fieldValue, date); - } - - if (isArray(fieldValue)) { - if (fieldValue.length > 0) { - return fieldValue.some(item => eq(item, clauseValue, options)); - } else { - return eq('', clauseValue, options); - } + return granularity.isSame(fieldValue as moment.Moment, date); } return false; // unknown value type }; -export const exact = (fieldValue, clauseValue, options = {}) => { +export const exact = ( + fieldValue: FieldValue, + clauseValue: ClauseValue, + options = {} +) => { return eq(fieldValue, clauseValue, { ...options, exactMatch: true }); }; -const greaterThen = (fieldValue, clauseValue, inclusive = false) => { +const greaterThen = ( + fieldValue: FieldValue, + clauseValue: ClauseValue, + inclusive = false +): boolean => { if (isDateValue(clauseValue)) { const clauseDateValue = clauseValue.resolve(); + + const fieldValueAsMomentInput = fieldValue as moment.MomentInput; + if (!clauseValue.granularity) { return inclusive - ? utc(fieldValue).isSameOrAfter(clauseDateValue) - : utc(fieldValue).isAfter(clauseDateValue); + ? utc(fieldValueAsMomentInput).isSameOrAfter(clauseDateValue) + : utc(fieldValueAsMomentInput).isAfter(clauseDateValue); } + if (inclusive) { - return utc(fieldValue).isSameOrAfter( + return utc(fieldValueAsMomentInput).isSameOrAfter( clauseValue.granularity.start(clauseDateValue) ); } - return utc(fieldValue).isSameOrAfter( + return utc(fieldValueAsMomentInput).isSameOrAfter( clauseValue.granularity.startOfNext(clauseDateValue) ); } if (isString(fieldValue)) { - const str = clauseValue.toString(); + const str = String(clauseValue); return inclusive ? fieldValue >= str : fieldValue > str; } @@ -136,34 +171,34 @@ const greaterThen = (fieldValue, clauseValue, inclusive = false) => { } if (isArray(fieldValue)) { - return fieldValue.all(item => greaterThen(item, clauseValue, inclusive)); + return fieldValue.every(item => greaterThen(item, clauseValue, inclusive)); } return false; // unsupported value type }; -export const gt = (fieldValue, clauseValue) => { +export const gt = (fieldValue: FieldValue, clauseValue: ClauseValue) => { if (isNil(fieldValue) || isNil(clauseValue)) { return false; } return greaterThen(fieldValue, clauseValue); }; -export const gte = (fieldValue, clauseValue) => { +export const gte = (fieldValue: FieldValue, clauseValue: ClauseValue) => { if (isNil(fieldValue) || isNil(clauseValue)) { return fieldValue === clauseValue; } return greaterThen(fieldValue, clauseValue, true); }; -export const lt = (fieldValue, clauseValue) => { +export const lt = (fieldValue: FieldValue, clauseValue: ClauseValue) => { if (isNil(fieldValue) || isNil(clauseValue)) { return false; } return !greaterThen(fieldValue, clauseValue, true); }; -export const lte = (fieldValue, clauseValue) => { +export const lte = (fieldValue: FieldValue, clauseValue: ClauseValue) => { if (isNil(fieldValue) || isNil(clauseValue)) { return fieldValue === clauseValue; } diff --git a/src/components/search_bar/query/query.js b/src/components/search_bar/query/query.ts similarity index 75% rename from src/components/search_bar/query/query.js rename to src/components/search_bar/query/query.ts index b9f09c225ff..89ab6bcfc6e 100644 --- a/src/components/search_bar/query/query.js +++ b/src/components/search_bar/query/query.ts @@ -1,10 +1,9 @@ -import { defaultSyntax } from './default_syntax'; +import { defaultSyntax, ParseOptions, Syntax } from './default_syntax'; import { executeAst } from './execute_ast'; import { isNil, isString } from '../../../services/predicate'; import { astToEsQueryDsl } from './ast_to_es_query_dsl'; import { astToEsQueryString } from './ast_to_es_query_string'; -import { dateValueParser } from './date_value'; -import { AST, Operator } from './ast'; +import { _AST, AST, Clause, Operator, OperatorType, Value } from './ast'; /** * This is the consumer interface for the query - it's effectively a wrapper construct around @@ -12,103 +11,113 @@ import { AST, Operator } from './ast'; * It is immutable - all mutating operations return a new (mutated) query instance. */ export class Query { - static parse(text, options, syntax = defaultSyntax) { + static parse( + text: string, + options?: ParseOptions, + syntax: Syntax = defaultSyntax + ) { return new Query(syntax.parse(text, options), syntax, text); } - static parseDateValue(value, format = undefined) { - return dateValueParser(format)(value); - } - - static isMust(clause) { + static isMust(clause: Clause) { return AST.Match.isMustClause(clause); } static MATCH_ALL = Query.parse(''); - static isTerm(clause) { + static isTerm(clause: Clause) { return AST.Term.isInstance(clause); } - static isIs(clause) { + static isIs(clause: Clause) { return AST.Is.isInstance(clause); } - static isField(clause) { + static isField(clause: Clause) { return AST.Field.isInstance(clause); } - constructor(ast, syntax = defaultSyntax, text = undefined) { + // This ought to be `private`, but Kibana has some customizations that rely on access to this field + public ast: _AST; + public text: string; + private syntax: Syntax; + + constructor(ast: _AST, syntax: Syntax = defaultSyntax, text?: string) { this.ast = ast; this.text = text || syntax.print(ast); this.syntax = syntax; } - hasSimpleFieldClause(field, value = undefined) { + hasSimpleFieldClause(field: string, value?: string) { return this.ast.hasSimpleFieldClause(field, value); } - getSimpleFieldClause(field, value) { + getSimpleFieldClause(field: string, value?: Value) { return this.ast.getSimpleFieldClause(field, value); } - removeSimpleFieldClauses(field) { + removeSimpleFieldClauses(field: string) { const ast = this.ast.removeSimpleFieldClauses(field); return new Query(ast, this.syntax); } - addSimpleFieldValue(field, value, must = true, operator = Operator.EQ) { + addSimpleFieldValue( + field: string, + value: Value, + must = true, + operator: OperatorType = Operator.EQ + ) { const ast = this.ast.addSimpleFieldValue(field, value, must, operator); return new Query(ast, this.syntax); } - removeSimpleFieldValue(field, value) { + removeSimpleFieldValue(field: string, value: Value) { const ast = this.ast.removeSimpleFieldValue(field, value); return new Query(ast, this.syntax); } - hasOrFieldClause(field, value = undefined) { + hasOrFieldClause(field: string, value?: Value) { return this.ast.hasOrFieldClause(field, value); } - getOrFieldClause(field, value) { + getOrFieldClause(field: string, value?: Value) { return this.ast.getOrFieldClause(field, value); } - addOrFieldValue(field, value, must = true) { + addOrFieldValue(field: string, value: Value, must = true) { const ast = this.ast.addOrFieldValue(field, value, must); return new Query(ast, this.syntax); } - removeOrFieldValue(field, value) { + removeOrFieldValue(field: string, value: Value) { const ast = this.ast.removeOrFieldValue(field, value); return new Query(ast, this.syntax); } - removeOrFieldClauses(field) { + removeOrFieldClauses(field: string) { const ast = this.ast.removeOrFieldClauses(field); return new Query(ast, this.syntax); } - hasIsClause(flag) { + hasIsClause(flag: string) { return !isNil(this.ast.getIsClause(flag)); } - getIsClause(flag) { + getIsClause(flag: string) { return this.ast.getIsClause(flag); } - addMustIsClause(flag) { + addMustIsClause(flag: string) { const ast = this.ast.addClause(AST.Is.must(flag)); return new Query(ast, this.syntax); } - addMustNotIsClause(flag) { + addMustNotIsClause(flag: string) { const ast = this.ast.addClause(AST.Is.mustNot(flag)); return new Query(ast, this.syntax); } - removeIsClause(flag) { + removeIsClause(flag: string) { const ast = this.ast.removeIsClause(flag); return new Query(ast, this.syntax); } @@ -135,7 +144,7 @@ export class Query { * information about why the objects matched the query (default to `false`, mainly/only useful for * debugging) */ - static execute(query, items, options = {}) { + static execute(query: string | Query, items: T[], options = {}): T[] { const q = isString(query) ? Query.parse(query) : query; return executeAst(q.ast, items, options); } @@ -166,13 +175,13 @@ export class Query { * terms in the query(the operator is AND). This function lets you change this default translation * and provide your own custom one. */ - static toESQuery(query, options = {}) { + static toESQuery(query: string | Query, options = {}) { const q = isString(query) ? Query.parse(query) : query; return astToEsQueryDsl(q.ast, options); } - static toESQueryString(query, options = {}) { + static toESQueryString(query: string | Query) { const q = isString(query) ? Query.parse(query) : query; - return astToEsQueryString(q.ast, options); + return astToEsQueryString(q.ast); } } diff --git a/src/components/search_bar/search_bar.test.js b/src/components/search_bar/search_bar.test.tsx similarity index 85% rename from src/components/search_bar/search_bar.test.js rename to src/components/search_bar/search_bar.test.tsx index 4581fb60eaa..f783e07c4a1 100644 --- a/src/components/search_bar/search_bar.test.js +++ b/src/components/search_bar/search_bar.test.tsx @@ -5,6 +5,7 @@ import { mount, shallow } from 'enzyme'; import { EuiSearchBar } from './search_bar'; import { Query } from './query'; import { ENTER } from '../../services/key_codes'; +import { SearchFiltersFiltersType } from './search_filters'; describe('SearchBar', () => { test('render - no config, no query', () => { @@ -47,21 +48,23 @@ describe('SearchBar', () => { }); test('render - provided query, filters', () => { + const filters: SearchFiltersFiltersType = [ + { + type: 'is', + field: 'open', + name: 'Open', + }, + { + type: 'field_value_selection', + field: 'tag', + name: 'Tag', + options: () => Promise.resolve([]), + }, + ]; + const props = { ...requiredProps, - filters: [ - { - type: 'is', - field: 'open', - name: 'Open', - }, - { - type: 'field_value_selection', - field: 'tag', - name: 'Tag', - options: () => {}, - }, - ], + filters, query: 'this is a query', onChange: () => {}, }; diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.tsx similarity index 60% rename from src/components/search_bar/search_bar.js rename to src/components/search_bar/search_bar.tsx index cd214f118b6..f00ff1aeb35 100644 --- a/src/components/search_bar/search_bar.js +++ b/src/components/search_bar/search_bar.tsx @@ -1,65 +1,78 @@ -import React, { Component } from 'react'; +import React, { Component, ReactElement } from 'react'; import { isString } from '../../services/predicate'; -import { EuiFlexGroup } from '../flex/flex_group'; -import { EuiSearchBox, SearchBoxConfigPropTypes } from './search_box'; +import { EuiFlexGroup, EuiFlexItem } from '../flex'; +import { EuiSearchBox, SchemaType, SearchBoxConfigProps } from './search_box'; import { EuiSearchFilters, SearchFiltersFiltersType } from './search_filters'; -import PropTypes from 'prop-types'; import { Query } from './query'; -import { EuiFlexItem } from '../flex/flex_item'; +import { CommonProps } from '../common'; export { Query, AST as Ast } from './query'; -export const QueryType = PropTypes.oneOfType([ - PropTypes.instanceOf(Query), - PropTypes.string, -]); +export type QueryType = Query | string; -export const SearchBarPropTypes = { - /** - (query?: Query, queryText: string, error?: string) => void - */ - onChange: PropTypes.func.isRequired, +type Tools = ReactElement | ReactElement[]; + +interface ArgsWithQuery { + query: Query; + queryText: string; + error: null; +} + +interface ArgsWithError { + query: null; + queryText: string; + error: Error; +} + +export interface EuiSearchBarProps extends CommonProps { + onChange?: (args: ArgsWithQuery | ArgsWithError) => void | boolean; /** The initial query the bar will hold when first mounted */ - defaultQuery: QueryType, + defaultQuery?: QueryType; /** If you wish to use the search bar as a controlled component, continuously pass the query via this prop */ - query: QueryType, + query?: QueryType; /** Configures the search box. Set `placeholder` to change the placeholder text in the box and `incremental` to support incremental (as you type) search. */ - box: PropTypes.shape(SearchBoxConfigPropTypes), + box?: SearchBoxConfigProps; /** An array of search filters. */ - filters: SearchFiltersFiltersType, + filters?: SearchFiltersFiltersType; /** * Tools which go to the left of the search bar. */ - toolsLeft: PropTypes.node, + toolsLeft?: Tools; /** * Tools which go to the right of the search bar. */ - toolsRight: PropTypes.node, + toolsRight?: Tools; /** * Date formatter to use when parsing date values */ - dateFormat: PropTypes.object, -}; + dateFormat?: object; +} -const parseQuery = (query, props) => { - const schema = props.box ? props.box.schema : undefined; +const parseQuery = ( + query: QueryType | undefined, + props: EuiSearchBarProps +): Query => { + let schema: SchemaType | undefined = undefined; + if (props.box && props.box.schema && typeof props.box.schema === 'object') { + schema = props.box.schema; + } const dateFormat = props.dateFormat; const parseOptions = { schema, dateFormat }; if (!query) { @@ -68,12 +81,20 @@ const parseQuery = (query, props) => { return isString(query) ? Query.parse(query, parseOptions) : query; }; -export class EuiSearchBar extends Component { - static propTypes = SearchBarPropTypes; +interface State { + query: Query; + queryText: string; + error: null | Error; +} + +// `state.query` is never null, but can be passed as `null` to `notifyControllingParent` +// when `error` is not null. +type StateWithOptionalQuery = Omit & { query: Query | null }; +export class EuiSearchBar extends Component { static Query = Query; - constructor(props) { + constructor(props: EuiSearchBarProps) { super(props); const query = parseQuery(props.defaultQuery || props.query, props); this.state = { @@ -83,10 +104,17 @@ export class EuiSearchBar extends Component { }; } - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps( + nextProps: EuiSearchBarProps, + prevState: State + ): State | null { if ( nextProps.query && - (!prevState.query || nextProps.query.text !== prevState.query.text) + (!prevState.query || + (typeof nextProps.query !== 'string' && + nextProps.query.text !== prevState.query.text) || + (typeof nextProps.query === 'string' && + nextProps.query !== prevState.query.text)) ) { const query = parseQuery(nextProps.query, nextProps); return { @@ -98,7 +126,11 @@ export class EuiSearchBar extends Component { return null; } - notifyControllingParent(newState) { + notifyControllingParent(newState: StateWithOptionalQuery) { + const { onChange } = this.props; + if (!onChange) { + return; + } const oldState = this.state; const { query, queryText, error } = newState; @@ -109,23 +141,27 @@ export class EuiSearchBar extends Component { const isErrorDifferent = oldError !== newError; if (isQueryDifferent || isErrorDifferent) { - this.props.onChange({ query, queryText, error }); + if (error == null) { + onChange({ query: query!, queryText, error }); + } else { + onChange({ query: null, queryText, error }); + } } } - onSearch = queryText => { + onSearch = (queryText: string) => { try { const query = parseQuery(queryText, this.props); this.notifyControllingParent({ query, queryText, error: null }); this.setState({ query, queryText, error: null }); } catch (e) { - const error = { message: e.message }; + const error: Error = { name: e.name, message: e.message }; this.notifyControllingParent({ query: null, queryText, error }); this.setState({ queryText, error }); } }; - onFiltersChange = query => { + onFiltersChange = (query: Query) => { this.notifyControllingParent({ query, queryText: query.text, error: null }); this.setState({ query, @@ -134,14 +170,14 @@ export class EuiSearchBar extends Component { }); }; - renderTools(tools) { + renderTools(tools?: Tools) { if (!tools) { return undefined; } if (Array.isArray(tools)) { return tools.map(tool => ( - + {tool} )); @@ -178,7 +214,7 @@ export class EuiSearchBar extends Component { {...box} query={queryText} onSearch={this.onSearch} - isInvalid={!!error} + isInvalid={error != null} title={error ? error.message : undefined} /> diff --git a/src/components/search_bar/search_box.test.js b/src/components/search_bar/search_box.test.tsx similarity index 100% rename from src/components/search_bar/search_box.test.js rename to src/components/search_bar/search_box.test.tsx diff --git a/src/components/search_bar/search_box.js b/src/components/search_bar/search_box.tsx similarity index 50% rename from src/components/search_bar/search_box.js rename to src/components/search_bar/search_box.tsx index c13dc590fad..6e989dcdd16 100644 --- a/src/components/search_bar/search_box.js +++ b/src/components/search_bar/search_box.tsx @@ -1,39 +1,41 @@ import React, { Component } from 'react'; import { EuiFieldSearch } from '../form'; -import PropTypes from 'prop-types'; +import { CommonProps } from '../common'; -export const SchemaType = PropTypes.shape({ - strict: PropTypes.bool, - fields: PropTypes.object, - flags: PropTypes.arrayOf(PropTypes.string), -}); +export interface SchemaType { + strict?: boolean; + fields?: any; + flags?: string[]; +} -export const SearchBoxConfigPropTypes = { - placeholder: PropTypes.string, - incremental: PropTypes.bool, - schema: SchemaType, -}; +export interface SearchBoxConfigProps extends CommonProps { + placeholder?: string; + incremental?: boolean; + // Boolean values are not meaningful to this component, but are allowed so that other + // components can use e.g. a true value to mean "auto-derive a schema". See EuiInMemoryTable. + // Admittedly, this is a bit of a hack. + schema?: SchemaType | boolean; +} -export class EuiSearchBox extends Component { - static propTypes = { - query: PropTypes.string.isRequired, - onSearch: PropTypes.func.isRequired, // (queryText) => void - isInvalid: PropTypes.bool, - title: PropTypes.string, - ...SearchBoxConfigPropTypes, - }; +export interface EuiSearchBoxProps extends SearchBoxConfigProps { + query: string; + onSearch: (queryText: string) => void; + isInvalid?: boolean; + title?: string; +} + +type DefaultProps = Pick; - static defaultProps = { +export class EuiSearchBox extends Component { + static defaultProps: DefaultProps = { placeholder: 'Search...', incremental: false, }; - constructor(props) { - super(props); - } + private inputElement: HTMLInputElement | null = null; - componentDidUpdate(oldProps) { - if (oldProps.query !== this.props.query) { + componentDidUpdate(oldProps: EuiSearchBoxProps) { + if (oldProps.query !== this.props.query && this.inputElement != null) { this.inputElement.value = this.props.query; } } diff --git a/src/components/search_bar/search_filters.js b/src/components/search_bar/search_filters.js deleted file mode 100644 index 33a0dd250dc..00000000000 --- a/src/components/search_bar/search_filters.js +++ /dev/null @@ -1,37 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { createFilter, FilterConfigType } from './filters'; -import { Query } from './query'; -import { EuiFilterGroup } from '../../components/filter_group'; - -export const SearchFiltersFiltersType = PropTypes.arrayOf(FilterConfigType); - -export class EuiSearchFilters extends Component { - static propTypes = { - query: PropTypes.instanceOf(Query).isRequired, - onChange: PropTypes.func.isRequired, - filters: SearchFiltersFiltersType, - }; - - static defaultProps = { - filters: [], - }; - - constructor(props) { - super(props); - } - - render() { - const { filters = [], query, onChange } = this.props; - const items = filters.reduce((controls, filterConfig, index) => { - if (filterConfig.available && !filterConfig.available()) { - return controls; - } - const key = `filter_${index}`; - const control = createFilter(index, filterConfig, query, onChange); - controls.push({control}); - return controls; - }, []); - return {items}; - } -} diff --git a/src/components/search_bar/search_filters.test.js b/src/components/search_bar/search_filters.test.tsx similarity index 65% rename from src/components/search_bar/search_filters.test.js rename to src/components/search_bar/search_filters.test.tsx index 9abb494787b..6d8306d6b67 100644 --- a/src/components/search_bar/search_filters.test.js +++ b/src/components/search_bar/search_filters.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { requiredProps } from '../../test'; import { shallow } from 'enzyme'; -import { EuiSearchFilters } from './search_filters'; +import { EuiSearchFilters, SearchFiltersFiltersType } from './search_filters'; import { Query } from './query'; describe('EuiSearchFilters', () => { @@ -19,23 +19,25 @@ describe('EuiSearchFilters', () => { }); test('render - with filters', () => { + const filters: SearchFiltersFiltersType = [ + { + type: 'is', + field: 'open', + name: 'Open', + }, + { + type: 'field_value_selection', + field: 'tag', + name: 'Tag', + options: () => Promise.resolve([]), + }, + ]; + const props = { ...requiredProps, onChange: () => {}, query: Query.parse(''), - filters: [ - { - type: 'is', - field: 'open', - name: 'Open', - }, - { - type: 'field_value_selection', - field: 'tag', - name: 'Tag', - options: () => {}, - }, - ], + filters, }; const component = shallow(); diff --git a/src/components/search_bar/search_filters.tsx b/src/components/search_bar/search_filters.tsx new file mode 100644 index 00000000000..88576b631ee --- /dev/null +++ b/src/components/search_bar/search_filters.tsx @@ -0,0 +1,37 @@ +import React, { Component, Fragment, ReactElement } from 'react'; +import { createFilter, FilterConfig } from './filters'; +import { Query } from './query'; +import { EuiFilterGroup } from '../filter_group'; + +export type SearchFiltersFiltersType = FilterConfig[]; + +interface EuiSearchFiltersProps { + query: Query; + onChange: (query: Query) => void; + filters: SearchFiltersFiltersType; +} + +type DefaultProps = Pick; + +export class EuiSearchFilters extends Component { + static defaultProps: DefaultProps = { + filters: [], + }; + + render() { + const { filters = [], query, onChange } = this.props; + + const items: ReactElement[] = []; + + filters.forEach((filterConfig, index) => { + if (filterConfig.available && !filterConfig.available()) { + return; + } + const key = `filter_${index}`; + const control = createFilter(index, filterConfig, query, onChange); + items.push({control}); + }); + + return {items}; + } +} diff --git a/src/services/predicate/common_predicates.ts b/src/services/predicate/common_predicates.ts index 129e14348db..7142b1ef60a 100644 --- a/src/services/predicate/common_predicates.ts +++ b/src/services/predicate/common_predicates.ts @@ -24,6 +24,6 @@ export const isDate = (value: any): value is Date => { return moment.isDate(value); }; -export const isDateLike = (value: any) => { +export const isDateLike = (value: any): value is moment.Moment | Date => { return isMoment(value) || isDate(value); };