[7.x] [Index patterns] Guarantee order of fields in flattenHits (#93344) (#93483)

* [Index patterns] Guarantee order of fields in flattenHits (#93344)

* [Discover] Guarantee order of fields in table preview

* Remove comments

* Fix test that relied on discover ordering

* Fix ordering of test
# Conflicts:
#	test/functional/apps/discover/_large_string.ts
#	x-pack/test/functional/apps/security/doc_level_security_roles.js
#	x-pack/test/functional/apps/security/field_level_security.js

* Update tests

* Fix whitespace
This commit is contained in:
Wylie Conlon 2021-03-03 17:17:11 -05:00 committed by GitHub
parent 767859cb0c
commit ab13e4c9fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 130 additions and 15 deletions

View file

@ -0,0 +1,69 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IndexPattern } from './index_pattern';
// @ts-expect-error
import mockLogStashFields from './fixtures/logstash_fields';
import { stubbedSavedObjectIndexPattern } from './fixtures/stubbed_saved_object_index_pattern';
import { fieldFormatsMock } from '../../field_formats/mocks';
import { flattenHitWrapper } from './flatten_hit';
class MockFieldFormatter {}
fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any;
// helper function to create index patterns
function create(id: string) {
const {
type,
version,
attributes: { timeFieldName, fields, title },
} = stubbedSavedObjectIndexPattern(id);
return new IndexPattern({
spec: {
id,
type,
version,
timeFieldName,
fields,
title,
runtimeFieldMap: {},
},
fieldFormats: fieldFormatsMock,
shortDotsEnable: false,
metaFields: [],
});
}
describe('flattenHit', () => {
let indexPattern: IndexPattern;
// create an indexPattern instance for each test
beforeEach(() => {
indexPattern = create('test-pattern');
});
it('returns sorted object keys that combine _source, fields and metaFields in a defined order', () => {
const response = flattenHitWrapper(indexPattern, ['_id', '_type', '_score', '_routing'])({
_id: 'a',
_source: {
name: 'first',
},
fields: {
date: ['1'],
zzz: ['z'],
},
});
const expectedOrder = ['date', 'name', 'zzz', '_id', '_routing', '_score', '_type'];
expect(Object.keys(response)).toEqual(expectedOrder);
expect(Object.entries(response).map(([key]) => key)).toEqual(expectedOrder);
});
});

View file

@ -75,7 +75,26 @@ function decorateFlattenedWrapper(hit: Record<string, any>, metaFields: Record<s
}
});
return flattened;
// Force all usage of Object.keys to use a predefined sort order,
// instead of using insertion order
return new Proxy(flattened, {
ownKeys: (target) => {
return Reflect.ownKeys(target).sort((a, b) => {
const aIsMeta = _.includes(metaFields, a);
const bIsMeta = _.includes(metaFields, b);
if (aIsMeta && bIsMeta) {
return String(a).localeCompare(String(b));
}
if (aIsMeta) {
return 1;
}
if (bIsMeta) {
return -1;
}
return String(a).localeCompare(String(b));
});
},
});
};
}

View file

@ -13,10 +13,15 @@ import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks
describe('Row formatter', () => {
const hit = {
foo: 'bar',
number: 42,
hello: '<h1>World</h1>',
also: 'with "quotes" or \'single quotes\'',
_id: 'a',
_type: 'doc',
_score: 1,
_source: {
foo: 'bar',
number: 42,
hello: '<h1>World</h1>',
also: 'with "quotes" or \'single quotes\'',
},
};
const createIndexPattern = () => {
@ -37,12 +42,17 @@ describe('Row formatter', () => {
const indexPattern = createIndexPattern();
// Realistic response with alphabetical insertion order
const formatHitReturnValue = {
also: 'with \\&quot;quotes\\&quot; or &#39;single qoutes&#39;',
number: '42',
foo: 'bar',
number: '42',
hello: '&lt;h1&gt;World&lt;/h1&gt;',
_id: 'a',
_type: 'doc',
_score: 1,
};
const formatHitMock = jest.fn().mockReturnValue(formatHitReturnValue);
beforeEach(() => {
@ -52,7 +62,7 @@ describe('Row formatter', () => {
it('formats document properly', () => {
expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot(
`"<dl class=\\"source truncate-by-height\\"><dt>also:</dt><dd>with \\\\&quot;quotes\\\\&quot; or &#39;single qoutes&#39;</dd> <dt>number:</dt><dd>42</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd>&lt;h1&gt;World&lt;/h1&gt;</dd> </dl>"`
`"<dl class=\\"source truncate-by-height\\"><dt>also:</dt><dd>with \\\\&quot;quotes\\\\&quot; or &#39;single qoutes&#39;</dd> <dt>foo:</dt><dd>bar</dd> <dt>number:</dt><dd>42</dd> <dt>hello:</dt><dd>&lt;h1&gt;World&lt;/h1&gt;</dd> <dt>_id:</dt><dd>a</dd> <dt>_type:</dt><dd>doc</dd> <dt>_score:</dt><dd>1</dd> </dl>"`
);
});
@ -60,7 +70,7 @@ describe('Row formatter', () => {
expect(
formatRow({ ...hit, highlight: { number: '42' } }, indexPattern).trim()
).toMatchInlineSnapshot(
`"<dl class=\\"source truncate-by-height\\"><dt>number:</dt><dd>42</dd> <dt>also:</dt><dd>with \\\\&quot;quotes\\\\&quot; or &#39;single qoutes&#39;</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd>&lt;h1&gt;World&lt;/h1&gt;</dd> </dl>"`
`"<dl class=\\"source truncate-by-height\\"><dt>number:</dt><dd>42</dd> <dt>also:</dt><dd>with \\\\&quot;quotes\\\\&quot; or &#39;single qoutes&#39;</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd>&lt;h1&gt;World&lt;/h1&gt;</dd> <dt>_id:</dt><dd>a</dd> <dt>_type:</dt><dd>doc</dd> <dt>_score:</dt><dd>1</dd> </dl>"`
);
});
@ -88,6 +98,21 @@ describe('Row formatter', () => {
);
});
it('formats top level objects in alphabetical order', () => {
indexPattern.getFieldByName = jest.fn().mockReturnValue({
name: 'subfield',
});
indexPattern.getFormatterForField = jest.fn().mockReturnValue({
convert: () => 'formatted',
});
const formatted = formatTopLevelObject(
{ fields: { 'a.zzz': [100], 'a.ccc': [50] } },
{ 'a.zzz': [100], 'a.ccc': [50] },
indexPattern
).trim();
expect(formatted.indexOf('<dt>a.ccc:</dt>')).toBeLessThan(formatted.indexOf('<dt>a.zzz:</dt>'));
});
it('formats top level objects with subfields and highlights', () => {
indexPattern.getFieldByName = jest.fn().mockReturnValue({
name: 'subfield',

View file

@ -26,6 +26,7 @@ export const doTemplate = template(noWhiteSpace(templateHtml));
export const formatRow = (hit: Record<string, any>, indexPattern: IndexPattern) => {
const highlights = hit?.highlight ?? {};
// Keys are sorted in the hits object
const formatted = indexPattern.formatHit(hit);
const highlightPairs: Array<[string, unknown]> = [];
const sourcePairs: Array<[string, unknown]> = [];
@ -44,7 +45,8 @@ export const formatTopLevelObject = (
const highlights = row.highlight ?? {};
const highlightPairs: Array<[string, unknown]> = [];
const sourcePairs: Array<[string, unknown]> = [];
Object.entries(fields).forEach(([key, values]) => {
const sorted = Object.entries(fields).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
sorted.forEach(([key, values]) => {
const field = indexPattern.getFieldByName(key);
const formatter = field
? indexPattern.getFormatterForField(field)

View file

@ -32,8 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('verify the large string book present', async function () {
const ExpectedDoc =
'_id:1 _type:_doc _index:testlargestring _score:0' +
' mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' +
'mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' +
' This eBook is for the use of anyone anywhere in the United States' +
' and most other parts of the world at no cost and with almost no restrictions whatsoever.' +
' You may copy it, give it away or re-use it under the terms of the' +
@ -42,7 +41,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
' youll have to check the laws of the country where you are' +
' located before using this ebook.' +
' Title: Hamlet Author: William Shakespeare Release Date: November 1998 [EBook #1524]' +
' Last Updated: December 30, 2017 Language: English Character set encoding:';
' Last Updated: December 30, 2017 Language: English Character set encoding:' +
' _id:1 _type:_doc _index:testlargestring _score:0';
let rowData;
await PageObjects.common.navigateToApp('discover');

View file

@ -81,7 +81,7 @@ export default function ({ getService, getPageObjects }) {
});
const rowData = await PageObjects.discover.getDocTableIndex(1);
expect(rowData).to.be(
'_id:doc1 _type:_doc _index:dlstest _score:0 region.keyword:EAST name:ABC Company name.keyword:ABC Company region:EAST'
'name:ABC Company name.keyword:ABC Company region:EAST region.keyword:EAST _id:doc1 _index:dlstest _score:0 _type:_doc'
);
});
after('logout', async () => {

View file

@ -113,7 +113,7 @@ export default function ({ getService, getPageObjects }) {
});
const rowData = await PageObjects.discover.getDocTableIndex(1);
expect(rowData).to.be(
'_id:2 _type:_doc _index:flstest _score:0 customer_name.keyword:ABC Company customer_ssn:444.555.6666 customer_region.keyword:WEST runtime_customer_ssn:444.555.6666 calculated at runtime customer_region:WEST customer_name:ABC Company customer_ssn.keyword:444.555.6666'
'customer_name:ABC Company customer_name.keyword:ABC Company customer_region:WEST customer_region.keyword:WEST customer_ssn:444.555.6666 customer_ssn.keyword:444.555.6666 runtime_customer_ssn:444.555.6666 calculated at runtime _id:2 _index:flstest _score:0 _type:_doc'
);
});
@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }) {
});
const rowData = await PageObjects.discover.getDocTableIndex(1);
expect(rowData).to.be(
'_id:2 _type:_doc _index:flstest _score:0 customer_name.keyword:ABC Company customer_region.keyword:WEST customer_region:WEST customer_name:ABC Company'
'customer_name:ABC Company customer_name.keyword:ABC Company customer_region:WEST customer_region.keyword:WEST _id:2 _index:flstest _score:0 _type:_doc'
);
});