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 e9de6e77f0edfa..92eb0d041cc274 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 @@ -36,6 +36,9 @@ const mockMappings = { }, bar: { properties: { + _id: { + type: 'keyword', + }, foo: { type: 'text', }, @@ -193,6 +196,76 @@ describe('Filter Utils', () => { ).toEqual(esKuery.fromKueryExpression('alert.params.foo:bar')); }); + test('Assemble filter with just "id" and one type', () => { + expect(validateConvertFilterToKueryNode(['foo'], 'foo.id: 0123456789', mockMappings)).toEqual( + esKuery.fromKueryExpression('type: foo and _id: 0123456789') + ); + }); + + test('Assemble filter with saved object attribute "id" and one type and more', () => { + expect( + validateConvertFilterToKueryNode( + ['foo'], + 'foo.id: 0123456789 and (foo.updated_at: 5678654567 or foo.attributes.bytes > 1000)', + mockMappings + ) + ).toEqual( + esKuery.fromKueryExpression( + '(type: foo and _id: 0123456789) and ((type: foo and updated_at: 5678654567) or foo.bytes > 1000)' + ) + ); + }); + + test('Assemble filter with saved object attribute "id" and multi type and more', () => { + expect( + validateConvertFilterToKueryNode( + ['foo', 'bar'], + 'foo.id: 0123456789 and bar.id: 9876543210', + mockMappings + ) + ).toEqual( + esKuery.fromKueryExpression( + '(type: foo and _id: 0123456789) and (type: bar and _id: 9876543210)' + ) + ); + }); + + test('Allow saved object type to defined "_id" attributes and filter on it', () => { + expect( + validateConvertFilterToKueryNode( + ['foo', 'bar'], + 'foo.id: 0123456789 and bar.attributes._id: 9876543210', + mockMappings + ) + ).toEqual( + esKuery.fromKueryExpression('(type: foo and _id: 0123456789) and (bar._id: 9876543210)') + ); + }); + + test('Lets make sure that we are throwing an exception if we are using id outside of saved object attribute when it does not belong', () => { + expect(() => { + validateConvertFilterToKueryNode( + ['foo'], + 'foo.attributes.id: 0123456789 and (foo.updated_at: 5678654567 or foo.attributes.bytes > 1000)', + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + `"This key 'foo.attributes.id' does NOT exist in foo saved object index patterns: Bad Request"` + ); + }); + + test('Lets make sure that we are throwing an exception if we are using _id', () => { + expect(() => { + validateConvertFilterToKueryNode( + ['foo'], + 'foo._id: 0123456789 and (foo.updated_at: 5678654567 or foo.attributes.bytes > 1000)', + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + `"This key 'foo._id' does NOT exist in foo saved object index patterns: Bad Request"` + ); + }); + 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 6c8aee832457a8..27ff1c201cbddc 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -22,7 +22,7 @@ export const validateConvertFilterToKueryNode = ( indexMapping: IndexMapping ): KueryNode | undefined => { if (filter && indexMapping) { - const filterKueryNode = + let filterKueryNode = typeof filter === 'string' ? esKuery.fromKueryExpression(filter) : cloneDeep(filter); const validationFilterKuery = validateFilterKueryNode({ @@ -54,17 +54,20 @@ export const validateConvertFilterToKueryNode = ( const existingKueryNode: KueryNode = path.length === 0 ? filterKueryNode : get(filterKueryNode, path); if (item.isSavedObjectAttr) { - existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1]; + const keySavedObjectAttr = existingKueryNode.arguments[0].value.split('.')[1]; + existingKueryNode.arguments[0].value = + keySavedObjectAttr === 'id' ? '_id' : keySavedObjectAttr; const itemType = allowedTypes.filter((t) => t === item.type); if (itemType.length === 1) { - set( - filterKueryNode, - path, - esKuery.nodeTypes.function.buildNode('and', [ - esKuery.nodeTypes.function.buildNode('is', 'type', itemType[0]), - existingKueryNode, - ]) - ); + const kueryToAdd = esKuery.nodeTypes.function.buildNode('and', [ + esKuery.nodeTypes.function.buildNode('is', 'type', itemType[0]), + existingKueryNode, + ]); + if (path.length > 0) { + set(filterKueryNode, path, kueryToAdd); + } else { + filterKueryNode = kueryToAdd; + } } } else { existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.replace( @@ -171,6 +174,8 @@ export const isSavedObjectAttr = (key: string | null | undefined, indexMapping: const keySplit = key != null ? key.split('.') : []; if (keySplit.length === 1 && fieldDefined(indexMapping, keySplit[0])) { return true; + } else if (keySplit.length === 2 && keySplit[1] === 'id') { + return true; } else if (keySplit.length === 2 && fieldDefined(indexMapping, keySplit[1])) { return true; } else { @@ -219,6 +224,10 @@ export const fieldDefined = (indexMappings: IndexMapping, key: string): boolean return true; } + if (mappingKey === 'properties.id') { + return true; + } + // 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