diff --git a/docs/api/alerts/find.asciidoc b/docs/api/alerts/find.asciidoc index 97cd9f4c19ba7..ebd1c7841206c 100644 --- a/docs/api/alerts/find.asciidoc +++ b/docs/api/alerts/find.asciidoc @@ -49,6 +49,8 @@ Retrieve a paginated set of alerts based on condition. NOTE: As alerts change in {kib}, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. +NOTE: Alert `params` are stored as {ref}/flattened.html[flattened] and analyzed as `keyword`. + [[alerts-api-find-request-codes]] ==== Response code diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 05a936db4bfee..92012bf477b3e 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -76,6 +76,9 @@ const mockMappings = { }, }, }, + params: { + type: 'flattened', + }, }, }, hiddenType: { @@ -168,6 +171,12 @@ describe('Filter Utils', () => { ).toEqual(esKuery.fromKueryExpression('alert.actions:{ actionTypeId: ".server-log" }')); }); + test('Assemble filter for flattened fields', () => { + expect( + validateConvertFilterToKueryNode(['alert'], 'alert.attributes.params.foo:bar', mockMappings) + ).toEqual(esKuery.fromKueryExpression('alert.params.foo:bar')); + }); + test('Lets make sure that we are throwing an exception if we get an error', () => { expect(() => { validateConvertFilterToKueryNode( diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index 54b0033c9fcbe..688b7ad96e8ed 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -207,23 +207,33 @@ export const hasFilterKeyError = ( export const fieldDefined = (indexMappings: IndexMapping, key: string): boolean => { const mappingKey = 'properties.' + key.split('.').join('.properties.'); - const potentialKey = get(indexMappings, mappingKey); + if (get(indexMappings, mappingKey) != null) { + return true; + } - // If the `mappingKey` does not match a valid path, before returning null, + // If the `mappingKey` does not match a valid path, before returning false, // we want to check and see if the intended path was for a multi-field // such as `x.attributes.field.text` where `field` is mapped to both text // and keyword - if (potentialKey == null) { - const propertiesAttribute = 'properties'; - const indexOfLastProperties = mappingKey.lastIndexOf(propertiesAttribute); - const fieldMapping = mappingKey.substr(0, indexOfLastProperties); - const fieldType = mappingKey.substr( - mappingKey.lastIndexOf(propertiesAttribute) + `${propertiesAttribute}.`.length - ); - const mapping = `${fieldMapping}fields.${fieldType}`; - - return get(indexMappings, mapping) != null; - } else { + const propertiesAttribute = 'properties'; + const indexOfLastProperties = mappingKey.lastIndexOf(propertiesAttribute); + const fieldMapping = mappingKey.substr(0, indexOfLastProperties); + const fieldType = mappingKey.substr( + mappingKey.lastIndexOf(propertiesAttribute) + `${propertiesAttribute}.`.length + ); + const mapping = `${fieldMapping}fields.${fieldType}`; + if (get(indexMappings, mapping) != null) { return true; } + + // If the path is for a flattned type field, we'll assume the mappings are defined. + const keys = key.split('.'); + for (let i = 0; i < keys.length; i++) { + const path = `properties.${keys.slice(0, i + 1).join('.properties.')}`; + if (get(indexMappings, path)?.type === 'flattened') { + return true; + } + } + + return false; }; diff --git a/x-pack/plugins/alerts/server/saved_objects/mappings.json b/x-pack/plugins/alerts/server/saved_objects/mappings.json index f0c5c28ecaeaf..136dc530aa119 100644 --- a/x-pack/plugins/alerts/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerts/server/saved_objects/mappings.json @@ -47,8 +47,7 @@ } }, "params": { - "enabled": false, - "type": "object" + "type": "flattened" }, "scheduledTaskId": { "type": "keyword" diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index 2b0ff37b62272..1493c99162bf5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -19,6 +19,16 @@ export default function createFindTests({ getService }: FtrProviderContext) { afterEach(() => objectRemover.removeAll()); + async function createAlert(overwrites = {}) { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData(overwrites)) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); + return createdAlert; + } + it('should handle find alert request appropriately', async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) @@ -85,5 +95,23 @@ export default function createFindTests({ getService }: FtrProviderContext) { data: [], }); }); + + it('should filter on string parameters', async () => { + await Promise.all([ + createAlert({ params: { strValue: 'my a' } }), + createAlert({ params: { strValue: 'my b' } }), + createAlert({ params: { strValue: 'my c' } }), + ]); + + const response = await supertest.get( + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerts/_find?filter=alert.attributes.params.strValue:"my b"` + ); + + expect(response.status).to.eql(200); + expect(response.body.total).to.equal(1); + expect(response.body.data[0].params.strValue).to.eql('my b'); + }); }); }