diff --git a/src/platform/packages/shared/kbn-es-query-server/index.ts b/src/platform/packages/shared/kbn-es-query-server/index.ts index 77a0cf69c8f3d..12c2d39ec2b43 100644 --- a/src/platform/packages/shared/kbn-es-query-server/index.ts +++ b/src/platform/packages/shared/kbn-es-query-server/index.ts @@ -9,7 +9,8 @@ export { timeRangeSchema } from './src/time_range'; export { querySchema } from './src/query'; -export { filterSchema } from './src/filter'; +export { appStateSchema, globalStateSchema, filterSchema } from './src/filter/stored_filter'; +export { simpleFilterSchema } from './src/filter/simple_filter'; export type { TimeRange, @@ -19,4 +20,11 @@ export type { FilterMeta, AggregateQuery, Query, + SimpleFilter, + SimpleFilterCondition, + SimpleFilterGroup, + SimpleDSLFilter, + SimpleFilterValue, + SimpleRangeValue, + StoredFilterState, } from './src/types'; diff --git a/src/platform/packages/shared/kbn-es-query-server/src/filter/simple_filter.ts b/src/platform/packages/shared/kbn-es-query-server/src/filter/simple_filter.ts new file mode 100644 index 0000000000000..5b63f9ed3402f --- /dev/null +++ b/src/platform/packages/shared/kbn-es-query-server/src/filter/simple_filter.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/** + * Validation Schemas for Simplified Filter Interface + * + * These schemas are used for server validation of API requests and responses + * in * as Code APIs. + */ + +import { schema } from '@kbn/config-schema'; + +// ==================================================================== +// CORE FILTER OPERATOR AND VALUE SCHEMAS +// ==================================================================== + +/** + * Schema for range values used in numeric and date filters + */ +export const rangeValueSchema = schema.object({ + gte: schema.maybe( + schema.oneOf([schema.number(), schema.string()], { + meta: { description: 'Greater than or equal to' }, + }) + ), + lte: schema.maybe( + schema.oneOf([schema.number(), schema.string()], { + meta: { description: 'Less than or equal to' }, + }) + ), + gt: schema.maybe( + schema.oneOf([schema.number(), schema.string()], { + meta: { description: 'Greater than' }, + }) + ), + lt: schema.maybe( + schema.oneOf([schema.number(), schema.string()], { + meta: { description: 'Less than' }, + }) + ), +}); + +/** + * Schema for all possible filter values + */ +export const filterValueSchema = schema.oneOf( + [ + schema.string(), + schema.number(), + schema.boolean(), + schema.arrayOf(schema.string()), + schema.arrayOf(schema.number()), + schema.arrayOf(schema.boolean()), + rangeValueSchema, + ], + { meta: { description: 'Filter value - single value, array of homogeneous values, or range' } } +); + +// ==================================================================== +// BASE FILTER PROPERTIES (SHARED BY ALL SIMPLIFIED FILTERS) +// ==================================================================== + +/** + * Base properties shared by all simplified filters + */ +const baseFilterPropertiesSchema = { + pinned: schema.maybe( + schema.boolean({ + meta: { description: 'Whether the filter is pinned' }, + }) + ), + disabled: schema.maybe( + schema.boolean({ + meta: { description: 'Whether the filter is disabled' }, + }) + ), + controlledBy: schema.maybe( + schema.string({ + meta: { + description: 'Optional identifier for the component/plugin managing this filter', + }, + }) + ), + dataViewId: schema.maybe( + schema.string({ + meta: { description: 'Data view ID that this filter applies to' }, + }) + ), + negate: schema.maybe( + schema.boolean({ + meta: { description: 'Whether to negate the filter condition' }, + }) + ), + label: schema.maybe( + schema.string({ + meta: { description: 'Human-readable label for the filter' }, + }) + ), +}; + +// ==================================================================== +// SIMPLE FILTER CONDITION SCHEMAS +// ==================================================================== + +/** + * Common field property for all filter conditions + */ +const filterConditionFieldSchema = { + field: schema.string({ meta: { description: 'Field the filter applies to' } }), +}; + +/** + * Schema for 'is' and 'is_not' operators with single value + */ +const filterConditionIsSingleSchema = schema.object({ + ...filterConditionFieldSchema, + operator: schema.oneOf([schema.literal('is'), schema.literal('is_not')], { + meta: { description: 'Single value comparison operators' }, + }), + value: schema.oneOf([schema.string(), schema.number(), schema.boolean()], { + meta: { description: 'Single value for comparison' }, + }), +}); + +/** + * Schema for 'is_one_of' and 'is_not_one_of' operators with array values + */ +const filterConditionIsOneOfSchema = schema.object({ + ...filterConditionFieldSchema, + operator: schema.oneOf([schema.literal('is_one_of'), schema.literal('is_not_one_of')], { + meta: { description: 'Array value comparison operators' }, + }), + value: schema.oneOf( + [ + schema.arrayOf(schema.string()), + schema.arrayOf(schema.number()), + schema.arrayOf(schema.boolean()), + ], + { meta: { description: 'Homogeneous array of values' } } + ), +}); + +/** + * Schema for 'range' operator with range value + */ +const filterConditionRangeSchema = schema.object({ + ...filterConditionFieldSchema, + operator: schema.literal('range'), + value: rangeValueSchema, +}); + +/** + * Schema for 'exists' and 'not_exists' operators without value + */ +const filterConditionExistsSchema = schema.object({ + ...filterConditionFieldSchema, + operator: schema.oneOf([schema.literal('exists'), schema.literal('not_exists')], { + meta: { description: 'Field existence check operators' }, + }), + // value is intentionally omitted for exists/not_exists operators +}); + +/** + * Discriminated union schema for simple filter conditions with proper operator/value type combinations + */ +export const simpleFilterConditionSchema = schema.oneOf( + [ + filterConditionIsSingleSchema, + filterConditionIsOneOfSchema, + filterConditionRangeSchema, + filterConditionExistsSchema, + ], + { meta: { description: 'A filter condition with strict operator/value type matching' } } +); + +// ==================================================================== +// FILTER GROUP SCHEMA (RECURSIVE) +// ==================================================================== + +/** + * Schema for logical filter groups with recursive structure + * Uses lazy schema to handle recursive references + * Note: Groups only contain logical structure (type, conditions) - no metadata properties + */ +export const filterGroupSchema = schema.object( + { + type: schema.oneOf([schema.literal('and'), schema.literal('or')]), + conditions: schema.arrayOf( + schema.oneOf([ + simpleFilterConditionSchema, + schema.lazy('filterGroup'), // Recursive reference + ]) + ), + }, + { meta: { description: 'Grouped filters', id: 'filterGroup' } } +); + +// ==================================================================== +// RAW DSL FILTER SCHEMA +// ==================================================================== + +/** + * Schema for raw Elasticsearch Query DSL filters + */ +export const rawDSLFilterSchema = schema.object({ + query: schema.recordOf(schema.string(), schema.any(), { + meta: { description: 'Elasticsearch Query DSL object' }, + }), +}); + +// ==================================================================== +// SIMPLE FILTER DISCRIMINATED UNION SCHEMA +// ==================================================================== + +/** + * Schema for simple condition filters (Tier 1) + */ +export const simpleConditionFilterSchema = schema.object( + { + ...baseFilterPropertiesSchema, + condition: simpleFilterConditionSchema, + }, + { meta: { description: 'Simple condition filter' } } +); + +/** + * Schema for grouped condition filters (Tier 2-3) + */ +export const simpleGroupFilterSchema = schema.object( + { + ...baseFilterPropertiesSchema, + group: filterGroupSchema, + }, + { meta: { description: 'Grouped condition filter' } } +); + +/** + * Schema for raw DSL filters (Tier 4) + */ +export const simpleDSLFilterSchema = schema.object( + { + ...baseFilterPropertiesSchema, + dsl: rawDSLFilterSchema, + }, + { meta: { description: 'Raw DSL filter' } } +); + +/** + * Main discriminated union schema for SimpleFilter + * Ensures exactly one of: condition, group, or dsl is present + */ +export const simpleFilterSchema = schema.oneOf( + [simpleConditionFilterSchema, simpleGroupFilterSchema, simpleDSLFilterSchema], + { meta: { description: 'A filter which can be a condition, group, or raw DSL' } } +); diff --git a/src/platform/packages/shared/kbn-es-query-server/src/filter.ts b/src/platform/packages/shared/kbn-es-query-server/src/filter/stored_filter.ts similarity index 83% rename from src/platform/packages/shared/kbn-es-query-server/src/filter.ts rename to src/platform/packages/shared/kbn-es-query-server/src/filter/stored_filter.ts index da687d2864e47..7a4b22f1c9c7e 100644 --- a/src/platform/packages/shared/kbn-es-query-server/src/filter.ts +++ b/src/platform/packages/shared/kbn-es-query-server/src/filter/stored_filter.ts @@ -8,17 +8,16 @@ */ import { schema } from '@kbn/config-schema'; -import { FilterStateStore } from '@kbn/es-query'; -const filterStateStoreSchema = schema.oneOf( - [schema.literal(FilterStateStore.APP_STATE), schema.literal(FilterStateStore.GLOBAL_STATE)], - { - meta: { - description: - "Denote whether a filter is specific to an application's context (e.g. 'appState') or whether it should be applied globally (e.g. 'globalState').", - }, - } -); +export const appStateSchema = schema.literal('appState'); +export const globalStateSchema = schema.literal('globalState'); + +const filterStateStoreSchema = schema.oneOf([appStateSchema, globalStateSchema], { + meta: { + description: + "Denote whether a filter is specific to an application's context (e.g. 'appState') or whether it should be applied globally (e.g. 'globalState').", + }, +}); export const filterMetaSchema = schema.object( { @@ -31,6 +30,7 @@ export const filterMetaSchema = schema.object( group: schema.maybe( schema.string({ meta: { description: 'The group to which this filter belongs.' } }) ), + relation: schema.maybe(schema.string()), // field is missing from the Filter type, but is stored in SerializedSearchSourceFields // see the todo in src/platform/packages/shared/kbn-es-query/src/filters/helpers/update_filter.ts field: schema.maybe(schema.string()), diff --git a/src/platform/packages/shared/kbn-es-query-server/src/types.ts b/src/platform/packages/shared/kbn-es-query-server/src/types.ts index d2ba7e7575ab5..222f908b558d9 100644 --- a/src/platform/packages/shared/kbn-es-query-server/src/types.ts +++ b/src/platform/packages/shared/kbn-es-query-server/src/types.ts @@ -15,7 +15,20 @@ import type { relativeTimeRangeSchema, } from './time_range'; import type { aggregateQuerySchema, querySchema } from './query'; -import type { filterSchema, filterMetaSchema } from './filter'; +import type { + filterSchema, + filterMetaSchema, + globalStateSchema, + appStateSchema, +} from './filter/stored_filter'; +import type { + simpleFilterSchema, + simpleFilterConditionSchema, + filterGroupSchema, + rawDSLFilterSchema, + filterValueSchema, + rangeValueSchema, +} from './filter/simple_filter'; export type TimeRange = Writable>; export type AbsoluteTimeRange = TypeOf; @@ -26,3 +39,16 @@ export type AggregateQuery = Writable>; export type Filter = Writable>; export type FilterMeta = Writable>; + +/** + * Schema-inferred types for Simple Filter API + * + * These types are inferred from validation schemas and provide runtime validation compatibility. + */ +export type SimpleFilter = Writable>; +export type SimpleFilterCondition = Writable>; +export type SimpleFilterGroup = Writable>; +export type SimpleDSLFilter = Writable>; +export type SimpleFilterValue = Writable>; +export type SimpleRangeValue = Writable>; +export type StoredFilterState = TypeOf | TypeOf; diff --git a/src/platform/packages/shared/kbn-es-query-server/tsconfig.json b/src/platform/packages/shared/kbn-es-query-server/tsconfig.json index 74c4453447454..50aad51a203f1 100644 --- a/src/platform/packages/shared/kbn-es-query-server/tsconfig.json +++ b/src/platform/packages/shared/kbn-es-query-server/tsconfig.json @@ -9,6 +9,5 @@ "kbn_references": [ "@kbn/config-schema", "@kbn/utility-types", - "@kbn/es-query", ], }