From d6f581e104ea14493c750ca4af0b9fe804c526bd Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 14 Sep 2020 17:01:46 -0700 Subject: [PATCH 1/5] [Search] [WIP] Add shard delay aggregation --- .../common/search/aggs/buckets/shard_delay.ts | 43 +++++++++++++++++++ src/plugins/data/config.ts | 7 +++ src/plugins/data/public/plugin.ts | 2 +- .../data/public/search/search_service.ts | 23 +++++++--- src/plugins/data/server/index.ts | 1 + .../data/server/search/search_service.ts | 22 +++++++++- 6 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/buckets/shard_delay.ts diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay.ts new file mode 100644 index 0000000000000..96faf4eba8e9c --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BucketAggType } from './bucket_agg_type'; + +export const SHARD_DELAY_AGG_NAME = 'shard_delay'; + +export const getShardDelayBucketAgg = () => + new BucketAggType({ + name: SHARD_DELAY_AGG_NAME, + title: 'Shard Delay', + createFilter: () => ({ match_all: {} }), + customLabels: false, + params: [ + { + name: 'delay', + type: 'string', + default: '5s', + write(aggConfig, output) { + output.params = { + ...output.params, + value: aggConfig.params.delay, + }; + }, + }, + ], + }); diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts index 09cb2cb2afeca..55d34aa0b2616 100644 --- a/src/plugins/data/config.ts +++ b/src/plugins/data/config.ts @@ -28,6 +28,13 @@ export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), }), }), + search: schema.object({ + aggs: schema.object({ + shardDelay: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), + }), + }), }); export type ConfigSchema = TypeOf; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 3b18e0fbed537..5abf4d3648af7 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -96,7 +96,7 @@ export class DataPublicPlugin private readonly storage: IStorageWrapper; constructor(initializerContext: PluginInitializerContext) { - this.searchService = new SearchService(); + this.searchService = new SearchService(initializerContext); this.queryService = new QueryService(); this.fieldFormatsService = new FieldFormatsService(); this.autocomplete = new AutocompleteService(initializerContext); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 6b73761c5a437..9ec1209cc9008 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; +import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; import { BehaviorSubject } from 'rxjs'; import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; @@ -30,6 +30,11 @@ import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; import { esdsl, esRawResponse } from './expressions'; import { ExpressionsSetup } from '../../../expressions/public'; +import { ConfigSchema } from '../../config'; +import { + SHARD_DELAY_AGG_NAME, + getShardDelayBucketAgg, +} from '../../common/search/aggs/buckets/shard_delay'; /** @internal */ export interface SearchServiceSetupDependencies { @@ -48,6 +53,8 @@ export class SearchService implements Plugin { private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; + constructor(private initializerContext: PluginInitializerContext) {} + public setup( { http, getStartServices, injectedMetadata, notifications, uiSettings }: CoreSetup, { expressions, usageCollection }: SearchServiceSetupDependencies @@ -69,11 +76,17 @@ export class SearchService implements Plugin { expressions.registerFunction(esdsl); expressions.registerType(esRawResponse); + const aggs = this.aggsService.setup({ + registerFunction: expressions.registerFunction, + uiSettings, + }); + + if (this.initializerContext.config.get().search.aggs.shardDelay.enabled) { + aggs.types.registerBucket(SHARD_DELAY_AGG_NAME, getShardDelayBucketAgg); + } + return { - aggs: this.aggsService.setup({ - registerFunction: expressions.registerFunction, - uiSettings, - }), + aggs, usageCollector: this.usageCollector!, __enhance: (enhancements: SearchEnhancements) => { this.searchInterceptor = enhancements.searchInterceptor; diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 03baff4910309..305ef9c85889c 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -286,6 +286,7 @@ export { export const config: PluginConfigDescriptor = { exposeToBrowser: { autocomplete: true, + search: true, }, schema: configSchema, }; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index e19d3dd8a5451..e721459fd4761 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -28,6 +28,7 @@ import { SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; +import { first } from 'rxjs/operators'; import { ISearchSetup, ISearchStart, ISearchStrategy, SearchEnhancements } from './types'; import { AggsService, AggsSetupDependencies } from './aggs'; @@ -41,6 +42,11 @@ import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; import { searchTelemetry } from '../saved_objects'; import { IEsSearchRequest, IEsSearchResponse, ISearchOptions } from '../../common'; +import { + getShardDelayBucketAgg, + SHARD_DELAY_AGG_NAME, +} from '../../common/search/aggs/buckets/shard_delay'; +import { ConfigSchema } from '../../config'; type StrategyMap< SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, @@ -70,7 +76,7 @@ export class SearchService implements Plugin { private searchStrategies: StrategyMap = {}; constructor( - private initializerContext: PluginInitializerContext, + private initializerContext: PluginInitializerContext, private readonly logger: Logger ) {} @@ -102,13 +108,25 @@ export class SearchService implements Plugin { registerUsageCollector(usageCollection, this.initializerContext); } + const aggs = this.aggsService.setup({ registerFunction }); + + this.initializerContext.config + .create() + .pipe(first()) + .toPromise() + .then((value) => { + if (value.search.aggs.shardDelay.enabled) { + aggs.types.registerBucket(SHARD_DELAY_AGG_NAME, getShardDelayBucketAgg); + } + }); + return { __enhance: (enhancements: SearchEnhancements) => { if (this.searchStrategies.hasOwnProperty(enhancements.defaultStrategy)) { this.defaultSearchStrategyName = enhancements.defaultStrategy; } }, - aggs: this.aggsService.setup({ registerFunction }), + aggs, registerSearchStrategy: this.registerSearchStrategy, usage, }; From 4d89044819b7e40a14781ce663a2ca319cd8c5f8 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 22 Sep 2020 15:27:54 -0700 Subject: [PATCH 2/5] Add expression functions --- .../search/aggs/buckets/shard_delay.test.ts | 76 +++++++++++++ .../common/search/aggs/buckets/shard_delay.ts | 5 + .../aggs/buckets/shard_delay_fn.test.ts | 65 ++++++++++++ .../search/aggs/buckets/shard_delay_fn.ts | 100 ++++++++++++++++++ 4 files changed, 246 insertions(+) create mode 100644 src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts create mode 100644 src/plugins/data/common/search/aggs/buckets/shard_delay_fn.test.ts create mode 100644 src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts new file mode 100644 index 0000000000000..15399ffc43791 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AggConfigs } from '../agg_configs'; +import { FieldFormatsGetConfigFn, NumberFormat } from '../../../../common/field_formats'; +import { getShardDelayBucketAgg, SHARD_DELAY_AGG_NAME } from './shard_delay'; + +describe('Shard Delay Agg', () => { + const getConfig = (() => {}) as FieldFormatsGetConfigFn; + const getAggConfigs = () => { + const field = { name: 'bytes' }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + getFormatterForField: () => + new NumberFormat( + { + pattern: '0,0.[000] b', + }, + getConfig + ), + } as any; + + return new AggConfigs( + indexPattern, + [ + { + type: SHARD_DELAY_AGG_NAME, + params: { + duration: 1000, + }, + }, + ], + { + typesRegistry: { + get: getShardDelayBucketAgg, + } as any, + } + ); + }; + + describe('write', () => { + test('writes the delay as the value parameter', () => { + const aggConfigs = getAggConfigs(); + const agg = aggConfigs.aggs[0]; + expect(agg.write(aggConfigs)).toMatchInlineSnapshot(` + Object { + "params": Object { + "value": "5s", + }, + } + `); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay.ts index 96faf4eba8e9c..d87c4085202e6 100644 --- a/src/plugins/data/common/search/aggs/buckets/shard_delay.ts +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay.ts @@ -18,9 +18,14 @@ */ import { BucketAggType } from './bucket_agg_type'; +import { BaseAggParams } from '../types'; export const SHARD_DELAY_AGG_NAME = 'shard_delay'; +export interface AggParamsShardDelay extends BaseAggParams { + delay: number; +} + export const getShardDelayBucketAgg = () => new BucketAggType({ name: SHARD_DELAY_AGG_NAME, diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.test.ts new file mode 100644 index 0000000000000..b0ebfb005c218 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.test.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggShardDelay } from './shard_delay_fn'; + +describe('agg_expression_functions', () => { + describe('aggShardDelay', () => { + const fn = functionWrapper(aggShardDelay()); + + test('correctly serializes', () => { + const actual = fn({ + delay: 1000, + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + "delay": 1000, + "json": undefined, + }, + "schema": undefined, + "type": "shard_delay", + }, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + delay: 1000, + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + + expect(() => { + fn({ + delay: 1000, + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts new file mode 100644 index 0000000000000..755800697cdbb --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionType, AggConfigSerialized } from '../'; +import { getParsedValue } from '../utils/get_parsed_value'; +import { AggParamsShardDelay, SHARD_DELAY_AGG_NAME } from './shard_delay'; + +const fnName = 'aggShardDelay'; + +type Input = any; +type AggArgs = AggParamsShardDelay & Pick; + +type Arguments = Assign; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggShardDelay = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.shardDelay.help', { + defaultMessage: 'Generates a serialized agg config for a Shard Delay agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.shardDelay.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.shardDelay.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.shardDelay.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + delay: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.shardDelay.delay.help', { + defaultMessage: 'Delay in ms between shards to process.', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.shardDelay.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.shardDelay.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: SHARD_DELAY_AGG_NAME, + params: { + ...rest, + json: getParsedValue(args, 'json'), + delay: getParsedValue(args, 'delay'), + }, + }, + }; + }, +}); From 2e59614855343330535cf8f401bf7a32d8a97e59 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 22 Sep 2020 20:17:28 -0700 Subject: [PATCH 3/5] Register function --- .../data/common/search/aggs/buckets/shard_delay.ts | 4 +++- .../data/common/search/aggs/buckets/shard_delay_fn.ts | 11 ++++++++--- src/plugins/data/public/search/search_service.ts | 2 ++ src/plugins/data/server/search/search_service.ts | 2 ++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay.ts index d87c4085202e6..2decf5a74bc85 100644 --- a/src/plugins/data/common/search/aggs/buckets/shard_delay.ts +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay.ts @@ -19,17 +19,19 @@ import { BucketAggType } from './bucket_agg_type'; import { BaseAggParams } from '../types'; +import { aggShardDelayFnName } from './shard_delay_fn'; export const SHARD_DELAY_AGG_NAME = 'shard_delay'; export interface AggParamsShardDelay extends BaseAggParams { - delay: number; + delay?: number; } export const getShardDelayBucketAgg = () => new BucketAggType({ name: SHARD_DELAY_AGG_NAME, title: 'Shard Delay', + expressionName: aggShardDelayFnName, createFilter: () => ({ match_all: {} }), customLabels: false, params: [ diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts index 755800697cdbb..86de428fa03d7 100644 --- a/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts @@ -24,7 +24,7 @@ import { AggExpressionType, AggConfigSerialized } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; import { AggParamsShardDelay, SHARD_DELAY_AGG_NAME } from './shard_delay'; -const fnName = 'aggShardDelay'; +export const aggShardDelayFnName = 'aggShardDelay'; type Input = any; type AggArgs = AggParamsShardDelay & Pick; @@ -32,10 +32,15 @@ type AggArgs = AggParamsShardDelay & Pick; type Output = AggExpressionType; -type FunctionDefinition = ExpressionFunctionDefinition; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggShardDelayFnName, + Input, + Arguments, + Output +>; export const aggShardDelay = (): FunctionDefinition => ({ - name: fnName, + name: aggShardDelayFnName, help: i18n.translate('data.search.aggs.function.buckets.shardDelay.help', { defaultMessage: 'Generates a serialized agg config for a Shard Delay agg', }), diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 60edbc2539ace..ca857b9d36c6b 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -41,6 +41,7 @@ import { SHARD_DELAY_AGG_NAME, getShardDelayBucketAgg, } from '../../common/search/aggs/buckets/shard_delay'; +import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; /** @internal */ export interface SearchServiceSetupDependencies { @@ -89,6 +90,7 @@ export class SearchService implements Plugin { if (this.initializerContext.config.get().search.aggs.shardDelay.enabled) { aggs.types.registerBucket(SHARD_DELAY_AGG_NAME, getShardDelayBucketAgg); + expressions.registerFunction(aggShardDelay); } return { diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index e721459fd4761..96dcd09e38a5f 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -47,6 +47,7 @@ import { SHARD_DELAY_AGG_NAME, } from '../../common/search/aggs/buckets/shard_delay'; import { ConfigSchema } from '../../config'; +import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; type StrategyMap< SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, @@ -117,6 +118,7 @@ export class SearchService implements Plugin { .then((value) => { if (value.search.aggs.shardDelay.enabled) { aggs.types.registerBucket(SHARD_DELAY_AGG_NAME, getShardDelayBucketAgg); + registerFunction(aggShardDelay); } }); From 78341c0d2e6d638f420c4fc59f3192d1cb393694 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 24 Sep 2020 12:55:34 -0700 Subject: [PATCH 4/5] Fix test --- src/plugins/data/public/search/search_service.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 738f1e8ffbca0..15356fb1a31b7 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -26,11 +26,15 @@ describe('Search service', () => { let searchService: SearchService; let mockCoreSetup: MockedKeys; let mockCoreStart: MockedKeys; + const initializerContext = coreMock.createPluginInitializerContext(); + initializerContext.config.get = jest.fn().mockReturnValue({ + search: { aggs: { shardDelay: { enabled: false } } }, + }); beforeEach(() => { - searchService = new SearchService(); mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); + searchService = new SearchService(initializerContext); }); describe('setup()', () => { From 5f136427fcb344b97bda5359cdf18753d2e0b629 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 28 Sep 2020 14:08:44 -0700 Subject: [PATCH 5/5] Add comment --- src/plugins/data/config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts index 55d34aa0b2616..83a384a049a75 100644 --- a/src/plugins/data/config.ts +++ b/src/plugins/data/config.ts @@ -31,6 +31,9 @@ export const configSchema = schema.object({ search: schema.object({ aggs: schema.object({ shardDelay: schema.object({ + // Whether or not to register the shard_delay (which is only available in snapshot versions + // of Elasticsearch) agg type/expression function to make it available in the UI for either + // functional or manual testing enabled: schema.boolean({ defaultValue: false }), }), }),