From 82cf8fd1b3384c89643298781c51a6fd0a660df7 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 28 Oct 2020 13:43:07 +0300 Subject: [PATCH] Support export for SO with circular refs (#81582) * support export for SO with circular refs * add a test for export to space * update test case names * add test with complex deps tree --- .../inject_nested_depdendencies.test.ts | 4 +- .../saved_objects/export/sort_objects.test.ts | 399 ++++++++++++------ .../saved_objects/export/sort_objects.ts | 7 +- .../saved_objects/{export.js => export.ts} | 81 +++- .../saved_objects/{import.js => import.ts} | 61 ++- ...{_import_objects.js => _import_objects.ts} | 26 +- .../_import_objects_circular_refs.ndjson | 2 + .../apps/management/{index.js => index.ts} | 3 +- .../apps/spaces/copy_saved_objects.ts | 28 ++ .../spaces/copy_saved_objects/data.json | 60 +++ 10 files changed, 506 insertions(+), 165 deletions(-) rename test/api_integration/apis/saved_objects/{export.js => export.ts} (88%) rename test/api_integration/apis/saved_objects/{import.js => import.ts} (78%) rename test/functional/apps/management/{_import_objects.js => _import_objects.ts} (94%) create mode 100644 test/functional/apps/management/exports/_import_objects_circular_refs.ndjson rename test/functional/apps/management/{index.js => index.ts} (94%) diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts index 1d5ce5625bf48..862d11cfa663a 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts @@ -78,7 +78,7 @@ describe('getObjectReferencesToFetch()', () => { `); }); - test(`doesn't deal with circular dependencies`, () => { + test('does not fail on circular dependencies', () => { const map = new Map(); map.set('index-pattern:1', { id: '1', @@ -527,7 +527,7 @@ describe('injectNestedDependencies', () => { `); }); - test(`doesn't deal with circular dependencies`, async () => { + test('does not fail on circular dependencies', async () => { const savedObjects = [ { id: '2', 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 7b6698dfaf887..cd116d767b0c3 100644 --- a/src/core/server/saved_objects/export/sort_objects.test.ts +++ b/src/core/server/saved_objects/export/sort_objects.test.ts @@ -46,27 +46,27 @@ describe('sortObjects()', () => { }, ]; expect(sortObjects(docs)).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "search", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); }); test('should not mutate parameter', () => { @@ -91,49 +91,49 @@ Array [ }, ]; expect(sortObjects(docs)).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "search", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); expect(docs).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + ] + `); }); test('should sort unordered array', () => { @@ -199,71 +199,71 @@ Array [ }, ]; expect(sortObjects(docs)).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "3", - "references": Array [ - Object { - "id": "2", - "name": "ref1", - "type": "search", - }, - ], - "type": "visualization", - }, - Object { - "attributes": Object {}, - "id": "4", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "visualization", - }, - Object { - "attributes": Object {}, - "id": "5", - "references": Array [ - Object { - "id": "3", - "name": "ref1", - "type": "visualization", - }, - Object { - "id": "4", - "name": "ref2", - "type": "visualization", - }, - ], - "type": "dashboard", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, + "id": "3", + "references": Array [ + Object { + "id": "2", + "name": "ref1", + "type": "search", + }, + ], + "type": "visualization", + }, + Object { + "attributes": Object {}, + "id": "4", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "visualization", + }, + Object { + "attributes": Object {}, + "id": "5", + "references": Array [ + Object { + "id": "3", + "name": "ref1", + "type": "visualization", + }, + Object { + "id": "4", + "name": "ref2", + "type": "visualization", + }, + ], + "type": "dashboard", + }, + ] + `); }); - test('detects circular dependencies', () => { + test('should not fail on circular dependencies', () => { const docs = [ { id: '1', @@ -290,8 +290,149 @@ Array [ ], }, ]; - expect(() => sortObjects(docs)).toThrowErrorMatchingInlineSnapshot( - `"circular reference: [foo:1] ref-> [foo:2] ref-> [foo:1]"` - ); + + expect(sortObjects(docs)).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "foo", + }, + ], + "type": "foo", + }, + Object { + "attributes": Object {}, + "id": "1", + "references": Array [ + Object { + "id": "2", + "name": "ref1", + "type": "foo", + }, + ], + "type": "foo", + }, + ] + `); + }); + test('should not fail on complex circular dependencies', () => { + const docs = [ + { + id: '1', + type: 'foo', + attributes: {}, + references: [ + { + name: 'ref12', + type: 'foo', + id: '2', + }, + { + name: 'ref13', + type: 'baz', + id: '3', + }, + ], + }, + { + id: '2', + type: 'foo', + attributes: {}, + references: [ + { + name: 'ref13', + type: 'foo', + id: '3', + }, + ], + }, + { + id: '3', + type: 'baz', + attributes: {}, + references: [ + { + name: 'ref13', + type: 'xyz', + id: '4', + }, + ], + }, + { + id: '4', + type: 'xyz', + attributes: {}, + references: [ + { + name: 'ref14', + type: 'foo', + id: '1', + }, + ], + }, + ]; + + expect(sortObjects(docs)).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "3", + "name": "ref13", + "type": "foo", + }, + ], + "type": "foo", + }, + Object { + "attributes": Object {}, + "id": "4", + "references": Array [ + Object { + "id": "1", + "name": "ref14", + "type": "foo", + }, + ], + "type": "xyz", + }, + Object { + "attributes": Object {}, + "id": "3", + "references": Array [ + Object { + "id": "4", + "name": "ref13", + "type": "xyz", + }, + ], + "type": "baz", + }, + Object { + "attributes": Object {}, + "id": "1", + "references": Array [ + Object { + "id": "2", + "name": "ref12", + "type": "foo", + }, + Object { + "id": "3", + "name": "ref13", + "type": "baz", + }, + ], + "type": "foo", + }, + ] + `); }); }); diff --git a/src/core/server/saved_objects/export/sort_objects.ts b/src/core/server/saved_objects/export/sort_objects.ts index 64bab9f43bf14..ec83b687527fc 100644 --- a/src/core/server/saved_objects/export/sort_objects.ts +++ b/src/core/server/saved_objects/export/sort_objects.ts @@ -17,7 +17,6 @@ * under the License. */ -import Boom from 'boom'; import { SavedObject } from '../types'; export function sortObjects(savedObjects: SavedObject[]): SavedObject[] { @@ -30,11 +29,7 @@ export function sortObjects(savedObjects: SavedObject[]): SavedObject[] { function includeObjects(objects: SavedObject[]) { for (const object of objects) { if (path.has(object)) { - throw Boom.badRequest( - `circular reference: ${[...path, object] - .map((obj) => `[${obj.type}:${obj.id}]`) - .join(' ref-> ')}` - ); + continue; } const refdObjects = object.references diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.ts similarity index 88% rename from test/api_integration/apis/saved_objects/export.js rename to test/api_integration/apis/saved_objects/export.ts index 0c37e6b782a35..7254f3b3fcf31 100644 --- a/test/api_integration/apis/saved_objects/export.js +++ b/test/api_integration/apis/saved_objects/export.ts @@ -18,8 +18,12 @@ */ import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService }) { +function ndjsonToObject(input: string) { + return input.split('\n').map((str) => JSON.parse(str)); +} +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); @@ -38,7 +42,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = ndjsonToObject(resp.text); expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -61,7 +65,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = ndjsonToObject(resp.text); expect(objects).to.have.length(3); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -86,7 +90,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = ndjsonToObject(resp.text); expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -109,7 +113,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = resp.text.split('\n').map((str) => JSON.parse(str)); expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -133,7 +137,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = ndjsonToObject(resp.text); expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -217,6 +221,51 @@ export default function ({ getService }) { }); }); }); + + it('should export object with circular refs', async () => { + const soWithCycliRefs = [ + { + type: 'dashboard', + id: 'dashboard-a', + attributes: { + title: 'dashboard-a', + }, + references: [ + { + name: 'circular-dashboard-ref', + id: 'dashboard-b', + type: 'dashboard', + }, + ], + }, + { + type: 'dashboard', + id: 'dashboard-b', + attributes: { + title: 'dashboard-b', + }, + references: [ + { + name: 'circular-dashboard-ref', + id: 'dashboard-a', + type: 'dashboard', + }, + ], + }, + ]; + await supertest.post('/api/saved_objects/_bulk_create').send(soWithCycliRefs).expect(200); + const resp = await supertest + .post('/api/saved_objects/_export') + .send({ + includeReferencesDeep: true, + type: ['dashboard'], + }) + .expect(200); + + const objects = ndjsonToObject(resp.text); + expect(objects.find((o) => o.id === 'dashboard-a')).to.be.ok(); + expect(objects.find((o) => o.id === 'dashboard-b')).to.be.ok(); + }); }); describe('10,000 objects', () => { @@ -245,11 +294,11 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - expect(resp.headers['content-disposition']).to.eql( + expect(resp.header['content-disposition']).to.eql( 'attachment; filename="export.ndjson"' ); - expect(resp.headers['content-type']).to.eql('application/ndjson'); - const objects = resp.text.split('\n').map(JSON.parse); + expect(resp.header['content-type']).to.eql('application/ndjson'); + const objects = ndjsonToObject(resp.text); expect(objects).to.eql([ { attributes: { @@ -304,11 +353,11 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - expect(resp.headers['content-disposition']).to.eql( + expect(resp.header['content-disposition']).to.eql( 'attachment; filename="export.ndjson"' ); - expect(resp.headers['content-type']).to.eql('application/ndjson'); - const objects = resp.text.split('\n').map(JSON.parse); + expect(resp.header['content-type']).to.eql('application/ndjson'); + const objects = ndjsonToObject(resp.text); expect(objects).to.eql([ { attributes: { @@ -368,11 +417,11 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - expect(resp.headers['content-disposition']).to.eql( + expect(resp.header['content-disposition']).to.eql( 'attachment; filename="export.ndjson"' ); - expect(resp.headers['content-type']).to.eql('application/ndjson'); - const objects = resp.text.split('\n').map(JSON.parse); + expect(resp.header['content-type']).to.eql('application/ndjson'); + const objects = ndjsonToObject(resp.text); expect(objects).to.eql([ { attributes: { @@ -443,7 +492,7 @@ export default function ({ getService }) { }); describe('10,001 objects', () => { - let customVisId; + let customVisId: string; before(async () => { await esArchiver.load('saved_objects/10k'); await supertest diff --git a/test/api_integration/apis/saved_objects/import.js b/test/api_integration/apis/saved_objects/import.ts similarity index 78% rename from test/api_integration/apis/saved_objects/import.js rename to test/api_integration/apis/saved_objects/import.ts index 1666df2c83e5a..bdb695ef20dd1 100644 --- a/test/api_integration/apis/saved_objects/import.js +++ b/test/api_integration/apis/saved_objects/import.ts @@ -19,8 +19,19 @@ import expect from '@kbn/expect'; import { join } from 'path'; +import dedent from 'dedent'; +import type { SavedObjectsImportError } from 'src/core/server'; +import type { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService }) { +const createConflictError = ( + object: Omit +): SavedObjectsImportError => ({ + ...object, + title: object.meta.title, + error: { type: 'conflict' }, +}); + +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); @@ -41,11 +52,6 @@ export default function ({ getService }) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', meta: { title: 'Requests', icon: 'dashboardApp' }, }; - const createError = (object, type) => ({ - ...object, - title: object.meta.title, - error: { type }, - }); describe('with kibana index', () => { describe('with basic data existing', () => { @@ -75,9 +81,9 @@ export default function ({ getService }) { success: false, successCount: 0, errors: [ - createError(indexPattern, 'conflict'), - createError(visualization, 'conflict'), - createError(dashboard, 'conflict'), + createConflictError(indexPattern), + createConflictError(visualization), + createConflictError(dashboard), ], }); }); @@ -128,6 +134,43 @@ export default function ({ getService }) { }); }); + it('should return 200 when importing SO with circular refs', async () => { + const fileBuffer = Buffer.from( + dedent` + {"attributes":{"title":"dashboard-b"},"id":"dashboard-b","references":[{"id":"dashboard-a","name":"circular-dashboard-ref","type":"dashboard"}],"type":"dashboard"} + {"attributes":{"title":"dashboard-a"},"id":"dashboard-a","references":[{"id":"dashboard-b","name":"circular-dashboard-ref","type":"dashboard"}],"type":"dashboard"} + `, + 'utf8' + ); + const resp = await supertest + .post('/api/saved_objects/_import') + .attach('file', fileBuffer, 'export.ndjson') + .expect(200); + + expect(resp.body).to.eql({ + success: true, + successCount: 2, + successResults: [ + { + id: 'dashboard-b', + meta: { + icon: 'dashboardApp', + title: 'dashboard-b', + }, + type: 'dashboard', + }, + { + id: 'dashboard-a', + meta: { + icon: 'dashboardApp', + title: 'dashboard-a', + }, + type: 'dashboard', + }, + ], + }); + }); + it('should return 400 when trying to import more than 10,000 objects', async () => { const fileChunks = []; for (let i = 0; i < 10001; i++) { diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.ts similarity index 94% rename from test/functional/apps/management/_import_objects.js rename to test/functional/apps/management/_import_objects.ts index 3941b117e6efe..0b417d7d23e93 100644 --- a/test/functional/apps/management/_import_objects.js +++ b/test/functional/apps/management/_import_objects.ts @@ -20,10 +20,14 @@ import expect from '@kbn/expect'; import path from 'path'; import { keyBy } from 'lodash'; +import { FtrProviderContext } from '../../ftr_provider_context'; -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +function uniq(input: T[]): T[] { + return [...new Set(input)]; +} -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); @@ -67,6 +71,24 @@ export default function ({ getService, getPageObjects }) { expect(flyout['Log Agents'].relationship).to.eql('Parent'); }); + it('should import saved objects with circular refs', async function () { + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_objects_circular_refs.ndjson') + ); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + + await PageObjects.savedObjects.clickRelationshipsByTitle('dashboard-a'); + + const flyoutContent = await PageObjects.savedObjects.getRelationshipFlyout(); + + expect(uniq(flyoutContent.map(({ relationship }) => relationship).sort())).to.eql([ + 'Child', + 'Parent', + ]); + expect(uniq(flyoutContent.map(({ title }) => title))).to.eql(['dashboard-b']); + }); + it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () { await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_conflicts.ndjson') diff --git a/test/functional/apps/management/exports/_import_objects_circular_refs.ndjson b/test/functional/apps/management/exports/_import_objects_circular_refs.ndjson new file mode 100644 index 0000000000000..44297e9e8f3e0 --- /dev/null +++ b/test/functional/apps/management/exports/_import_objects_circular_refs.ndjson @@ -0,0 +1,2 @@ +{"attributes":{"title":"dashboard-b"},"id":"dashboard-b","references":[{"id":"dashboard-a","name":"circular-dashboard-ref","type":"dashboard"}],"type":"dashboard"} +{"attributes":{"title":"dashboard-a"},"id":"dashboard-a","references":[{"id":"dashboard-b","name":"circular-dashboard-ref","type":"dashboard"}],"type":"dashboard"} diff --git a/test/functional/apps/management/index.js b/test/functional/apps/management/index.ts similarity index 94% rename from test/functional/apps/management/index.js rename to test/functional/apps/management/index.ts index d5f0c286af7a5..7365f912ea4fa 100644 --- a/test/functional/apps/management/index.js +++ b/test/functional/apps/management/index.ts @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, loadTestFile }) { +export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('management', function () { diff --git a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts index 2ee6b903cc3a9..8f29ae6a27c3a 100644 --- a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts +++ b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts @@ -115,5 +115,33 @@ export default function spaceSelectorFunctonalTests({ await PageObjects.copySavedObjectsToSpace.finishCopy(); }); + + it('allows a dashboard to be copied to the marketing space, with circular references', async () => { + const destinationSpaceId = 'marketing'; + + await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('Dashboard Foo'); + + await PageObjects.copySavedObjectsToSpace.setupForm({ + overwrite: true, + destinationSpaceId, + }); + + await PageObjects.copySavedObjectsToSpace.startCopy(); + + // Wait for successful copy + await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`); + await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`); + + const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); + + expect(summaryCounts).to.eql({ + success: 2, + pending: 0, + skipped: 0, + errors: 0, + }); + + await PageObjects.copySavedObjectsToSpace.finishCopy(); + }); }); } diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json index 944b91e8be114..3434e1f80a7ce 100644 --- a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json +++ b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json @@ -109,3 +109,63 @@ } } } + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:dashboard-foo", + "source": { + "references": [{ + "id":"dashboard-bar", + "name":"dashboard-circular-ref", + "type":"dashboard" + }], + "dashboard": { + "title": "Dashboard Foo", + "hits": 0, + "description": "", + "panelsJSON": "[{}]", + "optionsJSON": "{}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:dashboard-bar", + "source": { + "references": [{ + "id":"dashboard-foo", + "name":"dashboard-circular-ref", + "type":"dashboard" + }], + "dashboard": { + "title": "Dashboard Bar", + "hits": 0, + "description": "", + "panelsJSON": "[{}]", + "optionsJSON": "{}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +}