Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d264710
Define schemas for simple filter interface
nickpeihl Oct 29, 2025
5989e98
Add descriptions to schemas
nickpeihl Oct 29, 2025
f900770
Remove id and rename indexPattern to dataViewId
nickpeihl Oct 30, 2025
5037bbe
Merge remote-tracking branch 'upstream/main' into simple-filters-schema
nickpeihl Oct 31, 2025
fb0f138
Changes from node scripts/lint_ts_projects --fix
kibanamachine Oct 31, 2025
b49449a
Fix filter value schema arrays to be homogeneous
nickpeihl Nov 5, 2025
ab20892
Fix casing inconsistency
nickpeihl Nov 5, 2025
48689fc
Naming consistency
nickpeihl Nov 5, 2025
f13c024
Stricter discriminated filter conditions
nickpeihl Nov 5, 2025
539a159
Document controlledBy field
nickpeihl Nov 5, 2025
565848a
Remove duplicate stored filter schema
nickpeihl Nov 5, 2025
7f85963
Changes from node scripts/lint_ts_projects --fix
kibanamachine Nov 5, 2025
51c30b1
Rename path
nickpeihl Nov 5, 2025
a2979a9
Merge remote-tracking branch 'refs/remotes/origin/simple-filters-sche…
nickpeihl Nov 5, 2025
883b3b6
Rename stored filter schema to "storedFilterSchema" and "simpleFilter…
nickpeihl Nov 6, 2025
3cbf28b
Add constants package to avoid circular references
nickpeihl Nov 7, 2025
00bd945
Add isMultiIindex, filterType, and BWC legacy properties
nickpeihl Nov 7, 2025
8aa68b4
Changes from node scripts/generate codeowners
kibanamachine Nov 7, 2025
e72bb46
Merge branch 'main' into simple-filters-schema
nickpeihl Nov 7, 2025
6b5e025
Rename AsCodeFilter types
nickpeihl Nov 7, 2025
f94f926
Merge remote-tracking branch 'origin/simple-filters-schema' into simp…
nickpeihl Nov 7, 2025
d1c1ea2
As "asCode" prefix to exported schemas
nickpeihl Nov 7, 2025
27870ca
Don't export StoredFilter types
nickpeihl Nov 7, 2025
36da619
Use schema extend instead of spread
nickpeihl Nov 7, 2025
7702432
Remove unnecessary nested query object from DSL schema
nickpeihl Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/platform/packages/shared/kbn-es-query-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { simplifiedFilterSchema } from './src/filter/simplified_filter';

export type {
TimeRange,
Expand All @@ -19,4 +20,11 @@ export type {
FilterMeta,
AggregateQuery,
Query,
SimplifiedFilter,
SimpleFilterCondition,
FilterGroup,
RawDSLFilter,
FilterValue,
RangeValue,
StoredFilterState,
} from './src/types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/*
* 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
* Supports single values, arrays, and range objects
*/
export const filterValueSchema = schema.oneOf(
[
schema.string(),
schema.number(),
schema.boolean(),
schema.arrayOf(schema.oneOf([schema.string(), schema.number(), schema.boolean()])),
rangeValueSchema,
],
{ meta: { description: 'Possible filter values that could be single values, arrays, or ranges' } }
);

// ====================================================================
// 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: 'Owner that manages 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
// ====================================================================

/**
* Base schema for simple filter conditions
*/
const baseFilterConditionSchema = {
field: schema.string({ meta: { description: 'Field the filter applies to' } }),
};

// ====================================================================
// DISCRIMINATED FILTER CONDITION SCHEMAS
// ====================================================================

/**
* Schema for filter conditions that require a value
*/
export const filterConditionWithValueSchema = schema.object({
...baseFilterConditionSchema,
operator: schema.oneOf(
[
schema.literal('is'),
schema.literal('is_not'),
schema.literal('is_one_of'),
schema.literal('is_not_one_of'),
schema.literal('range'),
],
{ meta: { description: 'Filter operators that require a value' } }
),
value: filterValueSchema,
});

/**
* Schema for filter conditions that check existence only
*/
export const filterConditionExistsSchema = schema.object({
...baseFilterConditionSchema,
operator: schema.oneOf([schema.literal('exists'), schema.literal('not_exists')], {
meta: { description: 'Filter operators that check existence' },
}),
// value is intentionally omitted for exists/not_exists operators
});

/**
* Discriminated union schema for simple filter conditions
*/
export const simpleFilterConditionSchema = schema.oneOf(
[filterConditionWithValueSchema, filterConditionExistsSchema],
{ meta: { description: 'A filter condition' } }
);

// ====================================================================
// 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' },
}),
});

// ====================================================================
// SIMPLIFIED FILTER DISCRIMINATED UNION SCHEMA
// ====================================================================

/**
* Schema for simple condition filters (Tier 1)
*/
export const simplifiedConditionFilterSchema = schema.object(
{
...baseFilterPropertiesSchema,
condition: simpleFilterConditionSchema,
},
{ meta: { description: 'Simple condition filter' } }
);

/**
* Schema for grouped condition filters (Tier 2-3)
*/
export const simplifiedGroupFilterSchema = schema.object(
{
...baseFilterPropertiesSchema,
group: filterGroupSchema,
},
{ meta: { description: 'Grouped condition filter' } }
);

/**
* Schema for raw DSL filters (Tier 4)
*/
export const simplifiedDSLFilterSchema = schema.object(
{
...baseFilterPropertiesSchema,
dsl: rawDSLFilterSchema,
},
{ meta: { description: 'Raw DSL filter' } }
);

/**
* Main discriminated union schema for SimplifiedFilter
* Ensures exactly one of: condition, group, or dsl is present
*/
export const simplifiedFilterSchema = schema.oneOf(
[simplifiedConditionFilterSchema, simplifiedGroupFilterSchema, simplifiedDSLFilterSchema],
{ meta: { description: 'A filter which can be a condition, group, or raw DSL' } }
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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".
*/

import { schema } from '@kbn/config-schema';

export const appStateSchema = schema.literal('appState');
export const globalStateSchema = schema.literal('globalState');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I might have approved prematurely. Didn't notice the type errors until now. Is it related to this change, maybe we need to stick with FilterStateStore here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the FilterStateStore enum causes a circular reference error since that would mean @kbn/es-query and @kbn/es-query-server depend on each other. Maybe we can export that enum (with future room for any others) from a new package (ex. @kbn/es-query-constants). Or is there an existing constants package we should use?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not aware of another package that would be good for this, so the new constants package sounds good 👍


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(
{
alias: schema.maybe(schema.nullable(schema.string())),
disabled: schema.maybe(schema.boolean()),
negate: schema.maybe(schema.boolean()),
controlledBy: schema.maybe(
schema.string({ meta: { description: 'Identifies the owner the filter.' } })
),
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()),
index: schema.maybe(schema.string()),
isMultiIndex: schema.maybe(schema.boolean()),
type: schema.maybe(schema.string()),
key: schema.maybe(schema.string()),
// We could consider creating FilterMetaParams as a schema to match the concrete Filter type.
// However, this is difficult because FilterMetaParams can be a `filterSchema` which is defined below.
// This would require a more complex schema definition that can handle recursive types.
// For now, we use `schema.any()` to allow flexibility in the params field.
params: schema.maybe(schema.any()),
value: schema.maybe(schema.string()),
},
{ unknowns: 'allow' }
);

export const filterSchema = schema.object(
{
meta: filterMetaSchema,
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
$state: schema.maybe(
schema.object({
store: filterStateStoreSchema,
})
),
},
{ meta: { id: 'kbn-es-query-server-filterSchema' } }
);
28 changes: 27 additions & 1 deletion src/platform/packages/shared/kbn-es-query-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
simplifiedFilterSchema,
simpleFilterConditionSchema,
filterGroupSchema,
rawDSLFilterSchema,
filterValueSchema,
rangeValueSchema,
} from './filter/simplified_filter';

export type TimeRange = Writable<TypeOf<typeof timeRangeSchema>>;
export type AbsoluteTimeRange = TypeOf<typeof absoluteTimeRangeSchema>;
Expand All @@ -26,3 +39,16 @@ export type AggregateQuery = Writable<TypeOf<typeof aggregateQuerySchema>>;

export type Filter = Writable<TypeOf<typeof filterSchema>>;
export type FilterMeta = Writable<TypeOf<typeof filterMetaSchema>>;

/**
* Schema-inferred types for Simplified Filter API
*
* These types are inferred from validation schemas and provide runtime validation compatibility.
*/
export type SimplifiedFilter = Writable<TypeOf<typeof simplifiedFilterSchema>>;
export type SimpleFilterCondition = Writable<TypeOf<typeof simpleFilterConditionSchema>>;
export type FilterGroup = Writable<TypeOf<typeof filterGroupSchema>>;
export type RawDSLFilter = Writable<TypeOf<typeof rawDSLFilterSchema>>;
export type FilterValue = Writable<TypeOf<typeof filterValueSchema>>;
export type RangeValue = Writable<TypeOf<typeof rangeValueSchema>>;
export type StoredFilterState = TypeOf<typeof appStateSchema> | TypeOf<typeof globalStateSchema>;