[8.8] [Security Solution][Alerts] fix missing fields merge on alerts creation (#157142) (#158540)

# Backport

This will backport the following commits from `main` to `8.8`:
- [[Security Solution][Alerts] fix missing fields merge on alerts
creation (#157142)](https://github.com/elastic/kibana/pull/157142)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Vitalii
Dmyterko","email":"92328789+vitaliidm@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-05-26T09:11:16Z","message":"[Security
Solution][Alerts] fix missing fields merge on alerts creation
(#157142)\n\n## Summary\r\n\r\n- addresses
https://github.com/elastic/kibana/issues/152446\r\n- now, instead of
string path to merged field, array path is used in\r\nmerging utilities,
that allows to work with keys, which have `dot` in\r\nnames. For exampe,
instead of `a.b.c.d` - `['a', 'b.c', 'd']`\r\n- addresses
https://github.com/elastic/kibana/issues/153607, by cloning\r\nsource
document, used in tests\r\n\r\n### Checklist\r\n\r\nDelete any items
that are not applicable to this PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Ievgen Sorokopud
<e40pud@gmail.com>\r\nCo-authored-by: Ryland Herrick
<ryalnd@gmail.com>","sha":"11fba5746fb44e829f76c1299f78f110a12cac25","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:
SecuritySolution","backport:prev-minor","v8.8.0","v8.9.0","Team:Detection
Engine"],"number":157142,"url":"https://github.com/elastic/kibana/pull/157142","mergeCommit":{"message":"[Security
Solution][Alerts] fix missing fields merge on alerts creation
(#157142)\n\n## Summary\r\n\r\n- addresses
https://github.com/elastic/kibana/issues/152446\r\n- now, instead of
string path to merged field, array path is used in\r\nmerging utilities,
that allows to work with keys, which have `dot` in\r\nnames. For exampe,
instead of `a.b.c.d` - `['a', 'b.c', 'd']`\r\n- addresses
https://github.com/elastic/kibana/issues/153607, by cloning\r\nsource
document, used in tests\r\n\r\n### Checklist\r\n\r\nDelete any items
that are not applicable to this PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Ievgen Sorokopud
<e40pud@gmail.com>\r\nCo-authored-by: Ryland Herrick
<ryalnd@gmail.com>","sha":"11fba5746fb44e829f76c1299f78f110a12cac25"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/157142","number":157142,"mergeCommit":{"message":"[Security
Solution][Alerts] fix missing fields merge on alerts creation
(#157142)\n\n## Summary\r\n\r\n- addresses
https://github.com/elastic/kibana/issues/152446\r\n- now, instead of
string path to merged field, array path is used in\r\nmerging utilities,
that allows to work with keys, which have `dot` in\r\nnames. For exampe,
instead of `a.b.c.d` - `['a', 'b.c', 'd']`\r\n- addresses
https://github.com/elastic/kibana/issues/153607, by cloning\r\nsource
document, used in tests\r\n\r\n### Checklist\r\n\r\nDelete any items
that are not applicable to this PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Ievgen Sorokopud
<e40pud@gmail.com>\r\nCo-authored-by: Ryland Herrick
<ryalnd@gmail.com>","sha":"11fba5746fb44e829f76c1299f78f110a12cac25"}}]}]
BACKPORT-->

Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2023-05-26 13:13:46 -04:00 committed by GitHub
parent d5e06a0bc2
commit 2b5725d558
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 666 additions and 240 deletions

View file

@ -17,6 +17,7 @@ import { isPrimitive } from '../utils/is_primitive';
import { isArrayOfPrimitives } from '../utils/is_array_of_primitives';
import { isTypeObject } from '../utils/is_type_object';
import { isPathValid } from '../utils/is_path_valid';
import { buildFieldsKeyAsArrayMap } from '../utils/build_fields_key_as_array_map';
/**
* Merges all of "doc._source" with its "doc.fields" on a "best effort" basis. See ../README.md for more information
@ -33,9 +34,12 @@ export const mergeAllFieldsWithSource: MergeStrategyFunction = ({ doc, ignoreFie
const fields = doc.fields ?? {};
const fieldEntries = Object.entries(fields);
const filteredEntries = filterFieldEntries(fieldEntries, ignoreFields);
const fieldsKeyMap = buildFieldsKeyAsArrayMap(source);
const transformedSource = filteredEntries.reduce(
(merged, [fieldsKey, fieldsValue]: [string, FieldsType]) => {
(merged, [fieldsKeyAsString, fieldsValue]: [string, FieldsType]) => {
const fieldsKey = fieldsKeyMap[fieldsKeyAsString] ?? fieldsKeyAsString;
if (
hasEarlyReturnConditions({
fieldsValue,
@ -101,7 +105,7 @@ const hasEarlyReturnConditions = ({
merged,
}: {
fieldsValue: FieldsType;
fieldsKey: string;
fieldsKey: string[] | string;
merged: SignalSource;
}) => {
const valueInMergedDocument = get(fieldsKey, merged);

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { cloneDeep } from 'lodash';
import { performance } from 'perf_hooks';
import { mergeMissingFieldsWithSource } from './merge_missing_fields_with_source';
import type { SignalSourceHit } from '../../../types';
@ -43,7 +44,7 @@ describe('merge_missing_fields_with_source', () => {
test('when source is "undefined", merged doc is "undefined"', () => {
const _source: SignalSourceHit['_source'] = {};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -52,7 +53,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: [],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -61,7 +62,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: 'value',
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -70,7 +71,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: ['value'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -79,7 +80,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: ['value_1', 'value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -88,7 +89,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: 'some value' },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -97,7 +98,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: [{ bar: 'some value' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -106,7 +107,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: [{ bar: 'some value' }, { foo: 'some other value' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -132,7 +133,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
'foo.bar': [],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -141,7 +142,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
'foo.bar': 'value',
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -150,7 +151,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
'foo.bar': ['value'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -159,7 +160,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
'foo.bar': ['value_1', 'value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -168,7 +169,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: 'some value' },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -177,7 +178,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: [{ bar: 'some value' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -186,7 +187,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: [{ bar: 'some value' }, { foo: 'some other value' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -216,7 +217,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: [] },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -225,7 +226,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: 'value' },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -234,7 +235,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: ['value'] },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -243,7 +244,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: ['value_1', 'value_2'] },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -252,7 +253,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: { mars: 'some value' } },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -261,7 +262,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: [{ mars: 'some value' }] },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -270,7 +271,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: [{ mars: 'some value' }, { mars: 'some other value' }] },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -308,7 +309,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
'bar.foo': [],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -317,7 +318,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
'bar.foo': 'value',
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -326,7 +327,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
'bar.foo': ['value'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -335,7 +336,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
'bar.foo': ['value_1', 'value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -344,7 +345,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: { bar: 'some value' },
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -353,7 +354,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: [{ bar: 'some value' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -362,7 +363,7 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
foo: [{ bar: 'some value' }, { foo: 'some other value' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -381,7 +382,11 @@ describe('merge_missing_fields_with_source', () => {
const _source: SignalSourceHit['_source'] = {
'bar.foo': [],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields: bigFields };
const doc: SignalSourceHit = {
...emptyEsResult(),
_source: cloneDeep(_source),
fields: bigFields,
};
const start = performance.now();
// we don't care about the response just determining performance
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
@ -408,7 +413,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>({
foo: {
@ -421,7 +426,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>({
foo: {
@ -434,7 +439,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>({});
});
@ -443,7 +448,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>({});
});
@ -468,7 +473,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -477,7 +482,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -486,7 +491,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
foo: [{ bar: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -495,7 +500,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
foo: [{ bar: 'other_value_1' }, { bar: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -519,7 +524,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -528,7 +533,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -537,7 +542,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -546,7 +551,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -572,7 +577,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -581,7 +586,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -590,7 +595,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -599,7 +604,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -623,7 +628,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -632,7 +637,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -641,7 +646,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -650,7 +655,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -674,7 +679,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -683,7 +688,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -692,7 +697,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -701,7 +706,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -725,7 +730,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -734,7 +739,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -743,7 +748,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -752,7 +757,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -778,7 +783,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -787,7 +792,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -796,7 +801,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -805,7 +810,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -829,7 +834,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -838,7 +843,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -847,7 +852,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -856,7 +861,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -882,7 +887,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -891,7 +896,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -900,7 +905,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -909,7 +914,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -933,7 +938,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -942,7 +947,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -951,7 +956,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ mars: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -960,7 +965,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -986,7 +991,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -995,7 +1000,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1004,7 +1009,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1013,7 +1018,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1037,7 +1042,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1046,7 +1051,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1055,7 +1060,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1064,7 +1069,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1088,7 +1093,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1097,7 +1102,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1106,7 +1111,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ mars: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1115,7 +1120,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1139,7 +1144,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1148,7 +1153,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1', 'other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1157,7 +1162,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ mars: 'other_value_1' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1166,7 +1171,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1192,7 +1197,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1204,7 +1209,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
'foo.bar': ['value_1', 'value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1228,7 +1233,7 @@ describe('merge_missing_fields_with_source', () => {
bar: ['bar_other_value_1'],
'bar.keyword': ['bar_other_value_keyword_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1246,7 +1251,7 @@ describe('merge_missing_fields_with_source', () => {
'host.hostname': ['hostname_other_value_1'],
'host.hostname.keyword': ['hostname_other_value_keyword_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1266,7 +1271,7 @@ describe('merge_missing_fields_with_source', () => {
'foo.host.hostname': ['hostname_other_value_1'],
'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1277,7 +1282,7 @@ describe('merge_missing_fields_with_source', () => {
foo: ['other_value_1'],
'foo.bar': ['other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>({
foo: 'other_value_1',
@ -1295,7 +1300,7 @@ describe('merge_missing_fields_with_source', () => {
'process.command_line.text': ['string longer than 10 characters'],
'@timestamp': ['2023-02-10T10:15:50.000Z'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1311,7 +1316,7 @@ describe('merge_missing_fields_with_source', () => {
'process.command_line.text': ['string longer than 10 characters'],
'@timestamp': ['2023-02-10T10:15:50.000Z'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1329,7 +1334,7 @@ describe('merge_missing_fields_with_source', () => {
'host.hostname': ['hostname_other_value_1'],
'host.hostname.keyword': ['hostname_other_value_keyword_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1345,7 +1350,7 @@ describe('merge_missing_fields_with_source', () => {
'foo.host.hostname': ['hostname_other_value_1'],
'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1357,7 +1362,7 @@ describe('merge_missing_fields_with_source', () => {
'foo.bar': ['other_value_2'],
'foo.bar.zed': ['zed_other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>({
foo: 'other_value_1',
@ -1374,7 +1379,7 @@ describe('merge_missing_fields_with_source', () => {
'process.command_line.text': ['string longer than 10 characters'],
'@timestamp': ['2023-02-10T10:15:50.000Z'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1389,7 +1394,112 @@ describe('merge_missing_fields_with_source', () => {
'process.command_line.text': ['string longer than 10 characters'],
'@timestamp': ['2023-02-10T10:15:50.000Z'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
});
describe('mixed flattened and nested keys for the _source', () => {
test('merges fields into source if it is empty', () => {
const _source: SignalSourceHit['_source'] = {
'email.headers': {},
};
const fields: SignalSourceHit['fields'] = {
'email.headers.x-test': ['from fields'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>({
email: {
headers: {
'x-test': 'from fields',
},
},
// preserves conflicting keys if values contain empty objects
'email.headers': {},
});
});
test('merges fields into source if it is "undefined"', () => {
const _source: SignalSourceHit['_source'] = {
'email.headers': { 'x-test': undefined },
};
const fields: SignalSourceHit['fields'] = {
'email.headers.x-test': ['from fields'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>({
'email.headers': {
'x-test': 'from fields',
},
});
});
test('does not merge fields into source', () => {
const _source: SignalSourceHit['_source'] = {
'email.headers': { 'x-test': 'a' },
};
const fields: SignalSourceHit['fields'] = {
'email.headers.x-test': ['from fields'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
test('does not merge fields into source if source value is array', () => {
const _source: SignalSourceHit['_source'] = {
'email.headers': { 'x-test': ['a'] },
};
const fields: SignalSourceHit['fields'] = {
'email.headers.x-test': ['from fields'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
test('does not merge fields into source if only source has nested fields', () => {
const _source: SignalSourceHit['_source'] = {
'a.b': { c: [{ d: ['1'] }, { d: ['2'] }] },
};
const fields: SignalSourceHit['fields'] = {
'a.b.c.d': ['1', '2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
test('does not merge nested fields into source if source has nested fields', () => {
const _source: SignalSourceHit['_source'] = {
'a.b': { c: [{ d: ['1'] }, { d: ['2'] }] },
};
const fields: SignalSourceHit['fields'] = {
'a.b.c': [{ d: '3 ' }, { d: '4' }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
test('does not duplicate fields in result document if field is equal to source mixed property', () => {
const _source: SignalSourceHit['_source'] = {
'email.headers': { 'x-test': 'a' },
};
const fields: SignalSourceHit['fields'] = {
'email.headers.x-test': ['a'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1414,7 +1524,7 @@ describe('merge_missing_fields_with_source', () => {
'foo.bar': ['other_value_1'],
'foo.mars': ['other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1434,7 +1544,7 @@ describe('merge_missing_fields_with_source', () => {
'foo.zed.bar': ['other_value_1'],
'foo.zed.mars': ['other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1449,7 +1559,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
foo: [{ bar: ['single_value'], zed: ['single_value'] }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>({});
});
@ -1466,7 +1576,7 @@ describe('merge_missing_fields_with_source', () => {
const fields: SignalSourceHit['fields'] = {
foo: [{ bar: ['single_value'], zed: ['single_value'] }],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source;
expect(merged).toEqual<ReturnTypeMergeFieldsWithSource>(_source);
});
@ -1483,7 +1593,7 @@ describe('merge_missing_fields_with_source', () => {
'value.should.ignore': ['other_value_2'], // string value should ignore this
'_odd.value': ['other_value_2'], // Regex should ignore this value of: /[_]+/
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({
doc,
ignoreFields: ['value.should.ignore', '/[_]+/'],
@ -1502,7 +1612,7 @@ describe('merge_missing_fields_with_source', () => {
'value.should.work': ['other_value_2'],
'_odd.value': ['other_value_2'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({
doc,
ignoreFields: ['other.string', '/[z]+/'], // Neither of these two should match anything
@ -1533,7 +1643,7 @@ describe('merge_missing_fields_with_source', () => {
'foo.bar': ['other_value_1'],
_ignored: ['other_value_1'],
};
const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields };
const doc: SignalSourceHit = { ...emptyEsResult(), _source: cloneDeep(_source), fields };
const merged = mergeMissingFieldsWithSource({
doc,
ignoreFields: [],

View file

@ -7,6 +7,7 @@
import { get } from 'lodash/fp';
import { set } from '@kbn/safer-lodash-set';
import type { SignalSource } from '../../../types';
import { filterFieldEntries } from '../utils/filter_field_entries';
import type { FieldsType, MergeStrategyFunction } from '../types';
@ -14,6 +15,7 @@ import { recursiveUnboxingFields } from '../utils/recursive_unboxing_fields';
import { isTypeObject } from '../utils/is_type_object';
import { isNestedObject } from '../utils/is_nested_object';
import { isPathValid } from '../utils/is_path_valid';
import { buildFieldsKeyAsArrayMap } from '../utils/build_fields_key_as_array_map';
/**
* Merges only missing sections of "doc._source" with its "doc.fields" on a "best effort" basis. See ../README.md for more information
@ -29,9 +31,11 @@ export const mergeMissingFieldsWithSource: MergeStrategyFunction = ({ doc, ignor
const fields = doc.fields ?? {};
const fieldEntries = Object.entries(fields);
const filteredEntries = filterFieldEntries(fieldEntries, ignoreFields);
const fieldsKeyMap = buildFieldsKeyAsArrayMap(source);
const transformedSource = filteredEntries.reduce(
(merged, [fieldsKey, fieldsValue]: [string, FieldsType]) => {
(merged, [fieldsKeyAsString, fieldsValue]: [string, FieldsType]) => {
const fieldsKey = fieldsKeyMap[fieldsKeyAsString] ?? fieldsKeyAsString;
if (
hasEarlyReturnConditions({
fieldsValue,
@ -72,7 +76,7 @@ const hasEarlyReturnConditions = ({
merged,
}: {
fieldsValue: FieldsType;
fieldsKey: string;
fieldsKey: string[] | string;
merged: SignalSource;
}) => {
const valueInMergedDocument = get(fieldsKey, merged);

View file

@ -0,0 +1,71 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { buildFieldsKeyAsArrayMap } from './build_fields_key_as_array_map';
describe('buildFieldsKeyAsArrayMap()', () => {
it('returns primitive type if it passed as source', () => {
// @ts-expect-error
expect(buildFieldsKeyAsArrayMap(1)).toEqual({});
// @ts-expect-error
expect(buildFieldsKeyAsArrayMap(Infinity)).toEqual({});
// @ts-expect-error
expect(buildFieldsKeyAsArrayMap(NaN)).toEqual({});
// @ts-expect-error
expect(buildFieldsKeyAsArrayMap(false)).toEqual({});
// @ts-expect-error
expect(buildFieldsKeyAsArrayMap(null)).toEqual({});
// @ts-expect-error
expect(buildFieldsKeyAsArrayMap(undefined)).toEqual({});
// @ts-expect-error
expect(buildFieldsKeyAsArrayMap([])).toEqual({});
});
it('builds map for nested source', () => {
expect(buildFieldsKeyAsArrayMap({ a: 'b' })).toEqual({ a: ['a'] });
expect(buildFieldsKeyAsArrayMap({ a: ['b'] })).toEqual({ a: ['a'] });
expect(buildFieldsKeyAsArrayMap({ a: { b: { c: 1 } } })).toEqual({
a: ['a'],
'a.b': ['a', 'b'],
'a.b.c': ['a', 'b', 'c'],
});
expect(buildFieldsKeyAsArrayMap({ a: { b: 'c' }, d: { e: 'f' } })).toEqual({
a: ['a'],
'a.b': ['a', 'b'],
d: ['d'],
'd.e': ['d', 'e'],
});
});
it('builds map for flattened source', () => {
expect(buildFieldsKeyAsArrayMap({ a: 'b' })).toEqual({ a: ['a'] });
expect(buildFieldsKeyAsArrayMap({ 'a.b.c': 1 })).toEqual({ 'a.b.c': ['a.b.c'] });
expect(buildFieldsKeyAsArrayMap({ 'a.b': 'c', 'd.e': 'f' })).toEqual({
'a.b': ['a.b'],
'd.e': ['d.e'],
});
});
it('builds map for arrays in a path', () => {
expect(buildFieldsKeyAsArrayMap({ a: { b: [{ c: 1 }, { c: 2 }] } })).toEqual({
a: ['a'],
'a.b': ['a', 'b'],
'a.b.c': ['a', 'b', 'c'],
});
});
it('builds map for mixed nested and flattened in a path', () => {
expect(
buildFieldsKeyAsArrayMap({
'a.b': { c: { d: 1 } },
})
).toEqual({
'a.b': ['a.b'],
'a.b.c': ['a.b', 'c'],
'a.b.c.d': ['a.b', 'c', 'd'],
});
});
});

View file

@ -0,0 +1,63 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isPlainObject, isArray } from 'lodash';
import type { SearchTypes } from '../../../../../../../common/detection_engine/types';
const isObjectTypeGuard = (value: SearchTypes): value is Record<string, SearchTypes> => {
return isPlainObject(value);
};
function traverseSource(
document: SearchTypes,
result: Record<string, string[]> = {},
prefix: string[] = []
): Record<string, string[]> {
if (prefix.length) {
result[prefix.join('.')] = prefix;
}
if (isObjectTypeGuard(document)) {
for (const [key, value] of Object.entries(document)) {
const path = [...prefix, key];
traverseSource(value, result, path);
}
} else if (isArray(document)) {
// for array of primitive values we can call traverseSource once
if (isPlainObject(document[0])) {
traverseSource(document[0], result, prefix);
} else {
document.forEach((doc) => {
traverseSource(doc, result, prefix);
});
}
}
return result;
}
/**
* takes object document and creates map of string field keys to array field keys
* source `{ 'a.b': { c: { d: 1 } } }`
* will result in map: `{
* 'a.b': ['a.b'],
* 'a.b.c': ['a.b', 'c'],
* 'a.b.c.d': ['a.b', 'c', 'd'],
* }`
* @param document - Record<string, SearchTypes>
**/
export function buildFieldsKeyAsArrayMap(
document: Record<string, SearchTypes>
): Record<string, string[]> {
if (!isPlainObject(document)) {
return {};
}
return traverseSource(document);
}

View file

@ -8,67 +8,135 @@
import { isPathValid } from './is_path_valid';
describe('isPathValid', () => {
test('not valid when empty string and empty object', () => {
expect(isPathValid('', {})).toEqual(false);
test('not valid when empty array is key', () => {
expect(isPathValid([], {})).toEqual(false);
});
test('valid when empty string is key', () => {
expect(isPathValid('', {})).toEqual(true);
expect(isPathValid([''], {})).toEqual(true);
});
test('valid when a path and empty object', () => {
expect(isPathValid(['a', 'b', 'c'], {})).toEqual(true);
expect(isPathValid('a.b.c', {})).toEqual(true);
});
test('not valid when a path and an array exists', () => {
expect(isPathValid(['a'], { a: [] })).toEqual(false);
expect(isPathValid('a', { a: [] })).toEqual(false);
});
test('not valid when a path and primitive value exists', () => {
expect(isPathValid(['a'], { a: 'test' })).toEqual(false);
expect(isPathValid(['a'], { a: 1 })).toEqual(false);
expect(isPathValid(['a'], { a: true })).toEqual(false);
expect(isPathValid('a', { a: 'test' })).toEqual(false);
expect(isPathValid('a', { a: 1 })).toEqual(false);
expect(isPathValid('a', { a: true })).toEqual(false);
});
test('valid when a path and object value exists', () => {
expect(isPathValid(['a'], { a: {} })).toEqual(true);
expect(isPathValid('a', { a: {} })).toEqual(true);
});
test('not valid when a path and an array exists within the parent path at level 1', () => {
expect(isPathValid(['a', 'b'], { a: [] })).toEqual(false);
expect(isPathValid('a.b', { a: [] })).toEqual(false);
});
test('not valid when a path and primitive value exists within the parent path at level 1', () => {
expect(isPathValid(['a', 'b'], { a: 'test' })).toEqual(false);
expect(isPathValid(['a', 'b'], { a: 1 })).toEqual(false);
expect(isPathValid(['a', 'b'], { a: true })).toEqual(false);
expect(isPathValid('a.b', { a: 'test' })).toEqual(false);
expect(isPathValid('a.b', { a: 1 })).toEqual(false);
expect(isPathValid('a.b', { a: true })).toEqual(false);
});
test('valid when a path and object value exists within the parent path at level 1', () => {
expect(isPathValid(['a', 'b'], { a: {} })).toEqual(true);
expect(isPathValid('a.b', { a: {} })).toEqual(true);
});
test('not valid when a path and an array exists within the parent path at level 2', () => {
expect(isPathValid(['a', 'b', 'c'], { a: { b: [] } })).toEqual(false);
expect(isPathValid(['a', 'b', 'c'], { 'a.b': [] })).toEqual(false);
expect(isPathValid('a.b.c', { a: { b: [] } })).toEqual(false);
expect(isPathValid('a.b.c', { 'a.b': [] })).toEqual(false);
});
test('not valid when a path and primitive value exists within the parent path at level 2', () => {
expect(isPathValid('a.b', { a: { b: 'test' } })).toEqual(false);
expect(isPathValid('a.b', { a: { b: 1 } })).toEqual(false);
expect(isPathValid('a.b', { a: { b: true } })).toEqual(false);
expect(isPathValid(['a', 'b', 'c'], { a: { b: 'test' } })).toEqual(false);
expect(isPathValid(['a', 'b', 'c'], { a: { b: 1 } })).toEqual(false);
expect(isPathValid(['a', 'b', 'c'], { a: { b: true } })).toEqual(false);
expect(isPathValid(['a', 'b', 'c'], { 'a.b': true })).toEqual(false);
expect(isPathValid('a.b.c', { a: { b: 'test' } })).toEqual(false);
expect(isPathValid('a.b.c', { a: { b: 1 } })).toEqual(false);
expect(isPathValid('a.b.c', { a: { b: true } })).toEqual(false);
expect(isPathValid('a.b.c', { 'a.b': true })).toEqual(false);
});
test('valid when a path and object value exists within the parent path at level 2', () => {
test('valid when a path and object value exists within the parent path at the last level 2', () => {
expect(isPathValid(['a', 'b'], { a: { b: {} } })).toEqual(true);
expect(isPathValid('a.b', { a: { b: {} } })).toEqual(true);
});
test('not valid when a path and an array exists within the parent path at level 3', () => {
test('not valid when a path and an array exists within the parent path at the last level 3', () => {
expect(isPathValid(['a', 'b', 'c'], { a: { b: { c: [] } } })).toEqual(false);
expect(isPathValid('a.b.c', { a: { b: { c: [] } } })).toEqual(false);
});
test('not valid when a path and primitive value exists within the parent path at level 3', () => {
test('not valid when a path and primitive value exists within the parent path at the last level 3', () => {
expect(isPathValid(['a', 'b', 'c'], { a: { b: { c: 'test' } } })).toEqual(false);
expect(isPathValid(['a', 'b', 'c'], { a: { b: { c: 1 } } })).toEqual(false);
expect(isPathValid(['a', 'b', 'c'], { a: { b: { c: true } } })).toEqual(false);
expect(isPathValid(['a', 'b', 'c'], { 'a.b.c': true })).toEqual(false);
expect(isPathValid('a.b.c', { a: { b: { c: 'test' } } })).toEqual(false);
expect(isPathValid('a.b.c', { a: { b: { c: 1 } } })).toEqual(false);
expect(isPathValid('a.b.c', { a: { b: { c: true } } })).toEqual(false);
expect(isPathValid('a.b.c', { 'a.b.c': true })).toEqual(false);
});
test('valid when a path and object value exists within the parent path at level 3', () => {
test('valid when a path and object value exists within the parent path at the last level 3', () => {
expect(isPathValid(['a', 'b', 'c'], { a: { b: { c: {} } } })).toEqual(true);
expect(isPathValid(['a', 'b', 'c'], { 'a.b.c': {} })).toEqual(true);
expect(isPathValid('a.b.c', { a: { b: { c: {} } } })).toEqual(true);
expect(isPathValid('a.b.c', { 'a.b.c': {} })).toEqual(true);
});
test('valid when any key has dot notation', () => {
expect(isPathValid(['a', 'b.c'], { a: { 'b.c': {} } })).toEqual(true);
expect(isPathValid(['a.b', 'c'], { 'a.b': { c: {} } })).toEqual(true);
expect(isPathValid(['a', 'b.c', 'd'], { a: { 'b.c': { d: {} } } })).toEqual(true);
});
test('not valid when any key has dot notation and array is present in source on the last level', () => {
expect(isPathValid(['a', 'b.c'], { a: { 'b.c': [] } })).toEqual(false);
expect(isPathValid(['a.b', 'c'], { 'a.b': { c: [] } })).toEqual(false);
expect(isPathValid(['a', 'b.c', 'd'], { a: { 'b.c': { d: [] } } })).toEqual(false);
});
test('not valid when any key has dot notation and primitive value is present in source on the last level', () => {
expect(isPathValid(['a', 'b.c'], { a: { 'b.c': 1 } })).toEqual(false);
expect(isPathValid(['a.b', 'c'], { 'a.b': { c: 1 } })).toEqual(false);
expect(isPathValid(['a', 'b.c', 'd'], { a: { 'b.c': { d: 1 } } })).toEqual(false);
});
test('not valid when any key has dot notation and array is present in source on level 2', () => {
expect(isPathValid(['a', 'b.c', 'd'], { a: { 'b.c': [] } })).toEqual(false);
expect(isPathValid(['a.b', 'c', 'd'], { 'a.b': { c: [] } })).toEqual(false);
});
});

View file

@ -16,15 +16,21 @@ import type { SignalSource } from '../../../types';
* @param source The source document
* @returns boolean
*/
export const isPathValid = (path: string, source: SignalSource): boolean => {
if (!path) {
export const isPathValid = (path: string[] | string, source: SignalSource): boolean => {
if (path == null) {
return false;
}
const splitPath = path.split('.');
const pathAsArray = typeof path === 'string' ? path.split('.') : path;
return splitPath.every((_, index, array) => {
const newPath = [...array].splice(0, index + 1).join('.');
const valueToCheck = get(newPath, source);
if (pathAsArray.length === 0) {
return false;
}
return pathAsArray.every((_, index, array) => {
const newPath = [...array].splice(0, index + 1);
// _.get won't retrieve value of flattened key 'a.b' when receives path ['a', 'b'].
// so we would try to call _.get with dot-notation path if array path results in undefined
const valueToCheck = get(newPath, source) ?? get(newPath.join('.'), source);
return valueToCheck === undefined || isPlainObject(valueToCheck);
});
};