From 2dbc144b8f226f93b2c435b431f9b5862da2e0cb Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 29 Sep 2022 19:41:06 +0200 Subject: [PATCH] Fix SO export sorting algorithm (#142078) * Fix SO export sorting algorithm * improve var name * adapt another unit test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 4cdd74dfcd71edaeae536220400310c271ee8a41) --- .../export/saved_objects_exporter.test.ts | 6 ++- .../saved_objects/export/sort_objects.test.ts | 37 +++++++++++++++++++ .../saved_objects/export/sort_objects.ts | 21 ++++++----- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts index 5968c8dabe8a8..411731c91493c 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -146,16 +146,18 @@ describe('getSortedObjectsForExport()', () => { attributes = {}, sort = [], type = 'index-pattern', + idPrefix = '', }: { attributes?: Record; sort?: string[]; type?: string; + idPrefix?: string; } = {} ) { const hits = []; for (let i = 1; i <= hitCount; i++) { hits.push({ - id: `${i}`, + id: `${idPrefix}${i}`, type, attributes, sort, @@ -247,7 +249,7 @@ describe('getSortedObjectsForExport()', () => { describe('>1k hits', () => { const firstMockHits = generateHits(1000, { sort: ['a', 'b'] }); - const secondMockHits = generateHits(500); + const secondMockHits = generateHits(500, { idPrefix: 'second-hit-' }); test('requests multiple pages', async () => { savedObjectsClient.find.mockResolvedValueOnce({ diff --git a/src/core/server/saved_objects/export/sort_objects.test.ts b/src/core/server/saved_objects/export/sort_objects.test.ts index 1f663ea5dbc56..27fbb09a37018 100644 --- a/src/core/server/saved_objects/export/sort_objects.test.ts +++ b/src/core/server/saved_objects/export/sort_objects.test.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ +import { range } from 'lodash'; import { sortObjects } from './sort_objects'; +import type { SavedObject } from '@kbn/core-saved-objects-common'; describe('sortObjects()', () => { test('should return on empty array', () => { @@ -309,6 +311,7 @@ describe('sortObjects()', () => { ] `); }); + test('should not fail on complex circular dependencies', () => { const docs = [ { @@ -424,4 +427,38 @@ describe('sortObjects()', () => { ] `); }); + + test('should not fail on large graph of objects', () => { + // create an object that references all objects with a higher `index` up to `depth`. + const createComplexNode = (index: number, depth: number): SavedObject => { + return { + type: 'test', + id: `${index}`, + attributes: {}, + references: range(index + 1, depth).map((refIndex) => ({ + type: 'test', + id: `${refIndex}`, + name: `test-${refIndex}`, + })), + }; + }; + + const createComplexGraph = (depth: number): SavedObject[] => { + const nodes: SavedObject[] = []; + for (let i = 0; i < depth; i++) { + nodes.push(createComplexNode(i, depth)); + } + return nodes; + }; + + const depth = 100; + const graph = createComplexGraph(depth); + const sorted = sortObjects(graph); + + expect(sorted.map(({ type, id }) => `${type}:${id}`)).toEqual( + range(depth) + .reverse() + .map((index) => `test:${index}`) + ); + }); }); diff --git a/src/core/server/saved_objects/export/sort_objects.ts b/src/core/server/saved_objects/export/sort_objects.ts index 3b73f889933b9..3f9726296dd54 100644 --- a/src/core/server/saved_objects/export/sort_objects.ts +++ b/src/core/server/saved_objects/export/sort_objects.ts @@ -8,27 +8,29 @@ import { SavedObject } from '../types'; +const getId = (object: { type: string; id: string }) => `${object.type}:${object.id}`; + export function sortObjects(savedObjects: SavedObject[]): SavedObject[] { - const path = new Set(); + const traversed = new Set(); const sorted = new Set(); const objectsByTypeId = new Map( - savedObjects.map((object) => [`${object.type}:${object.id}`, object] as [string, SavedObject]) + savedObjects.map((object) => [getId(object), object] as [string, SavedObject]) ); function includeObjects(objects: SavedObject[]) { for (const object of objects) { - if (path.has(object)) { + const objectId = getId(object); + if (traversed.has(objectId)) { continue; } - const refdObjects = object.references - .map((ref) => objectsByTypeId.get(`${ref.type}:${ref.id}`)) + const objectRefs = object.references + .map((ref) => objectsByTypeId.get(getId(ref))) .filter((ref): ref is SavedObject => !!ref); - if (refdObjects.length) { - path.add(object); - includeObjects(refdObjects); - path.delete(object); + traversed.add(objectId); + if (objectRefs.length) { + includeObjects(objectRefs); } sorted.add(object); @@ -36,5 +38,6 @@ export function sortObjects(savedObjects: SavedObject[]): SavedObject[] { } includeObjects(savedObjects); + return [...sorted]; }