mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Saved object export: apply export hooks to referenced / nested objects (#100769)
* execute export transform for nested references * fix sort * fix duplicate references * add FTR test
This commit is contained in:
parent
d62bb452dd
commit
aa8aa7f23d
9 changed files with 1423 additions and 786 deletions
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const applyExportTransformsMock = jest.fn();
|
||||
jest.doMock('./apply_export_transforms', () => ({
|
||||
applyExportTransforms: applyExportTransformsMock,
|
||||
}));
|
|
@ -0,0 +1,528 @@
|
|||
/*
|
||||
* 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 { applyExportTransformsMock } from './collect_exported_objects.test.mocks';
|
||||
import { savedObjectsClientMock } from '../../mocks';
|
||||
import { httpServerMock } from '../../http/http_server.mocks';
|
||||
import { SavedObject, SavedObjectError } from '../../../types';
|
||||
import type { SavedObjectsExportTransform } from './types';
|
||||
import { collectExportedObjects } from './collect_exported_objects';
|
||||
|
||||
const createObject = (parts: Partial<SavedObject>): SavedObject => ({
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
references: [],
|
||||
attributes: {},
|
||||
...parts,
|
||||
});
|
||||
|
||||
const createError = (parts: Partial<SavedObjectError> = {}): SavedObjectError => ({
|
||||
error: 'error',
|
||||
message: 'message',
|
||||
statusCode: 404,
|
||||
...parts,
|
||||
});
|
||||
|
||||
const toIdTuple = (obj: SavedObject) => ({ type: obj.type, id: obj.id });
|
||||
|
||||
describe('collectExportedObjects', () => {
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
let request: ReturnType<typeof httpServerMock.createKibanaRequest>;
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
request = httpServerMock.createKibanaRequest();
|
||||
applyExportTransformsMock.mockImplementation(({ objects }) => objects);
|
||||
savedObjectsClient.bulkGet.mockResolvedValue({ saved_objects: [] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
applyExportTransformsMock.mockReset();
|
||||
savedObjectsClient.bulkGet.mockReset();
|
||||
});
|
||||
|
||||
describe('when `includeReferences` is `true`', () => {
|
||||
it('calls `applyExportTransforms` with the correct parameters', async () => {
|
||||
const obj1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
});
|
||||
const obj2 = createObject({
|
||||
type: 'foo',
|
||||
id: '2',
|
||||
});
|
||||
|
||||
const fooTransform: SavedObjectsExportTransform = jest.fn();
|
||||
|
||||
await collectExportedObjects({
|
||||
objects: [obj1, obj2],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: { foo: fooTransform },
|
||||
includeReferences: true,
|
||||
});
|
||||
|
||||
expect(applyExportTransformsMock).toHaveBeenCalledTimes(1);
|
||||
expect(applyExportTransformsMock).toHaveBeenCalledWith({
|
||||
objects: [obj1, obj2],
|
||||
transforms: { foo: fooTransform },
|
||||
request,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the collected objects', async () => {
|
||||
const foo1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
references: [
|
||||
{
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
name: 'bar-2',
|
||||
},
|
||||
],
|
||||
});
|
||||
const bar2 = createObject({
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
});
|
||||
const dolly3 = createObject({
|
||||
type: 'dolly',
|
||||
id: '3',
|
||||
});
|
||||
|
||||
applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects, dolly3]);
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [bar2],
|
||||
});
|
||||
|
||||
const { objects, missingRefs } = await collectExportedObjects({
|
||||
objects: [foo1],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: {},
|
||||
includeReferences: true,
|
||||
});
|
||||
|
||||
expect(missingRefs).toHaveLength(0);
|
||||
expect(objects.map(toIdTuple)).toEqual([foo1, dolly3, bar2].map(toIdTuple));
|
||||
});
|
||||
|
||||
it('returns the missing references', async () => {
|
||||
const foo1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
references: [
|
||||
{
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
name: 'bar-2',
|
||||
},
|
||||
{
|
||||
type: 'missing',
|
||||
id: '1',
|
||||
name: 'missing-1',
|
||||
},
|
||||
],
|
||||
});
|
||||
const bar2 = createObject({
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
references: [
|
||||
{
|
||||
type: 'missing',
|
||||
id: '2',
|
||||
name: 'missing-2',
|
||||
},
|
||||
],
|
||||
});
|
||||
const missing1 = createObject({
|
||||
type: 'missing',
|
||||
id: '1',
|
||||
error: createError(),
|
||||
});
|
||||
const missing2 = createObject({
|
||||
type: 'missing',
|
||||
id: '2',
|
||||
error: createError(),
|
||||
});
|
||||
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [bar2, missing1],
|
||||
});
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [missing2],
|
||||
});
|
||||
|
||||
const { objects, missingRefs } = await collectExportedObjects({
|
||||
objects: [foo1],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: {},
|
||||
includeReferences: true,
|
||||
});
|
||||
|
||||
expect(missingRefs).toEqual([missing1, missing2].map(toIdTuple));
|
||||
expect(objects.map(toIdTuple)).toEqual([foo1, bar2].map(toIdTuple));
|
||||
});
|
||||
|
||||
it('does not call `client.bulkGet` when no objects have references', async () => {
|
||||
const obj1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
});
|
||||
const obj2 = createObject({
|
||||
type: 'foo',
|
||||
id: '2',
|
||||
});
|
||||
|
||||
const { objects, missingRefs } = await collectExportedObjects({
|
||||
objects: [obj1, obj2],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: {},
|
||||
includeReferences: true,
|
||||
});
|
||||
|
||||
expect(missingRefs).toHaveLength(0);
|
||||
expect(objects.map(toIdTuple)).toEqual([
|
||||
{
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
type: 'foo',
|
||||
id: '2',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(savedObjectsClient.bulkGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls `applyExportTransforms` for each iteration', async () => {
|
||||
const foo1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
references: [
|
||||
{
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
name: 'bar-2',
|
||||
},
|
||||
],
|
||||
});
|
||||
const bar2 = createObject({
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
});
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [bar2],
|
||||
});
|
||||
|
||||
await collectExportedObjects({
|
||||
objects: [foo1],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: {},
|
||||
includeReferences: true,
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(
|
||||
[toIdTuple(bar2)],
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
expect(applyExportTransformsMock).toHaveBeenCalledTimes(2);
|
||||
expect(applyExportTransformsMock).toHaveBeenCalledWith({
|
||||
objects: [foo1],
|
||||
transforms: {},
|
||||
request,
|
||||
});
|
||||
expect(applyExportTransformsMock).toHaveBeenCalledWith({
|
||||
objects: [bar2],
|
||||
transforms: {},
|
||||
request,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores references that are already included in the export', async () => {
|
||||
const foo1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
references: [
|
||||
{
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
name: 'bar-2',
|
||||
},
|
||||
],
|
||||
});
|
||||
const bar2 = createObject({
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
references: [
|
||||
{
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
name: 'foo-1',
|
||||
},
|
||||
{
|
||||
type: 'dolly',
|
||||
id: '3',
|
||||
name: 'dolly-3',
|
||||
},
|
||||
],
|
||||
});
|
||||
const dolly3 = createObject({
|
||||
type: 'dolly',
|
||||
id: '3',
|
||||
references: [
|
||||
{
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
name: 'foo-1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [bar2],
|
||||
});
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [foo1, dolly3],
|
||||
});
|
||||
|
||||
const { objects } = await collectExportedObjects({
|
||||
objects: [foo1],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: {},
|
||||
includeReferences: true,
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(2);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[toIdTuple(bar2)],
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[toIdTuple(dolly3)],
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
expect(objects.map(toIdTuple)).toEqual([foo1, bar2, dolly3].map(toIdTuple));
|
||||
});
|
||||
|
||||
it('does not fetch duplicates of references', async () => {
|
||||
const foo1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
references: [
|
||||
{
|
||||
type: 'dolly',
|
||||
id: '3',
|
||||
name: 'dolly-3',
|
||||
},
|
||||
{
|
||||
type: 'baz',
|
||||
id: '4',
|
||||
name: 'baz-4',
|
||||
},
|
||||
],
|
||||
});
|
||||
const bar2 = createObject({
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
references: [
|
||||
{
|
||||
type: 'dolly',
|
||||
id: '3',
|
||||
name: 'dolly-3',
|
||||
},
|
||||
],
|
||||
});
|
||||
const dolly3 = createObject({
|
||||
type: 'dolly',
|
||||
id: '3',
|
||||
});
|
||||
const baz4 = createObject({
|
||||
type: 'baz',
|
||||
id: '4',
|
||||
});
|
||||
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [dolly3, baz4],
|
||||
});
|
||||
|
||||
await collectExportedObjects({
|
||||
objects: [foo1, bar2],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: {},
|
||||
includeReferences: true,
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(
|
||||
[dolly3, baz4].map(toIdTuple),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('fetch references for additional objects returned by the export transform', async () => {
|
||||
const foo1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
references: [
|
||||
{
|
||||
type: 'baz',
|
||||
id: '4',
|
||||
name: 'baz-4',
|
||||
},
|
||||
],
|
||||
});
|
||||
const bar2 = createObject({
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
references: [
|
||||
{
|
||||
type: 'dolly',
|
||||
id: '3',
|
||||
name: 'dolly-3',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects, bar2]);
|
||||
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
await collectExportedObjects({
|
||||
objects: [foo1],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: {},
|
||||
includeReferences: true,
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(
|
||||
[
|
||||
{ type: 'baz', id: '4' },
|
||||
{ type: 'dolly', id: '3' },
|
||||
],
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('fetch references for additional objects returned by the export transform of nested references', async () => {
|
||||
const foo1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
references: [
|
||||
{
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
name: 'bar-2',
|
||||
},
|
||||
],
|
||||
});
|
||||
const bar2 = createObject({
|
||||
type: 'bar',
|
||||
id: '2',
|
||||
references: [],
|
||||
});
|
||||
const dolly3 = createObject({
|
||||
type: 'dolly',
|
||||
id: '3',
|
||||
references: [
|
||||
{
|
||||
type: 'baz',
|
||||
id: '4',
|
||||
name: 'baz-4',
|
||||
},
|
||||
],
|
||||
});
|
||||
const baz4 = createObject({
|
||||
type: 'baz',
|
||||
id: '4',
|
||||
});
|
||||
|
||||
// first call for foo-1
|
||||
applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects]);
|
||||
// second call for bar-2
|
||||
applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects, dolly3]);
|
||||
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [bar2],
|
||||
});
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [baz4],
|
||||
});
|
||||
|
||||
await collectExportedObjects({
|
||||
objects: [foo1],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: {},
|
||||
includeReferences: true,
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(2);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[toIdTuple(bar2)],
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[toIdTuple(baz4)],
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `includeReferences` is `false`', () => {
|
||||
it('does not fetch the object references', async () => {
|
||||
const obj1 = createObject({
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
references: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'bar',
|
||||
name: 'bar-2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { objects, missingRefs } = await collectExportedObjects({
|
||||
objects: [obj1],
|
||||
savedObjectsClient,
|
||||
request,
|
||||
exportTransforms: {},
|
||||
includeReferences: false,
|
||||
});
|
||||
|
||||
expect(missingRefs).toHaveLength(0);
|
||||
expect(objects.map(toIdTuple)).toEqual([
|
||||
{
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(savedObjectsClient.bulkGet).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
128
src/core/server/saved_objects/export/collect_exported_objects.ts
Normal file
128
src/core/server/saved_objects/export/collect_exported_objects.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 type { SavedObject } from '../../../types';
|
||||
import type { KibanaRequest } from '../../http';
|
||||
import { SavedObjectsClientContract } from '../types';
|
||||
import type { SavedObjectsExportTransform } from './types';
|
||||
import { applyExportTransforms } from './apply_export_transforms';
|
||||
|
||||
interface CollectExportedObjectOptions {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
objects: SavedObject[];
|
||||
/** flag to also include all related saved objects in the export stream. */
|
||||
includeReferences?: boolean;
|
||||
/** optional namespace to override the namespace used by the savedObjectsClient. */
|
||||
namespace?: string;
|
||||
/** The http request initiating the export. */
|
||||
request: KibanaRequest;
|
||||
/** export transform per type */
|
||||
exportTransforms: Record<string, SavedObjectsExportTransform>;
|
||||
}
|
||||
|
||||
interface CollectExportedObjectResult {
|
||||
objects: SavedObject[];
|
||||
missingRefs: CollectedReference[];
|
||||
}
|
||||
|
||||
export const collectExportedObjects = async ({
|
||||
objects,
|
||||
includeReferences = true,
|
||||
namespace,
|
||||
request,
|
||||
exportTransforms,
|
||||
savedObjectsClient,
|
||||
}: CollectExportedObjectOptions): Promise<CollectExportedObjectResult> => {
|
||||
const collectedObjects: SavedObject[] = [];
|
||||
const collectedMissingRefs: CollectedReference[] = [];
|
||||
const alreadyProcessed: Set<string> = new Set();
|
||||
|
||||
let currentObjects = objects;
|
||||
do {
|
||||
const transformed = (
|
||||
await applyExportTransforms({
|
||||
request,
|
||||
objects: currentObjects,
|
||||
transforms: exportTransforms,
|
||||
})
|
||||
).filter((object) => !alreadyProcessed.has(objKey(object)));
|
||||
|
||||
transformed.forEach((obj) => alreadyProcessed.add(objKey(obj)));
|
||||
collectedObjects.push(...transformed);
|
||||
|
||||
if (includeReferences) {
|
||||
const references = collectReferences(transformed, alreadyProcessed);
|
||||
if (references.length) {
|
||||
const { objects: fetchedObjects, missingRefs } = await fetchReferences({
|
||||
references,
|
||||
namespace,
|
||||
client: savedObjectsClient,
|
||||
});
|
||||
collectedMissingRefs.push(...missingRefs);
|
||||
currentObjects = fetchedObjects;
|
||||
} else {
|
||||
currentObjects = [];
|
||||
}
|
||||
} else {
|
||||
currentObjects = [];
|
||||
}
|
||||
} while (includeReferences && currentObjects.length);
|
||||
|
||||
return {
|
||||
objects: collectedObjects,
|
||||
missingRefs: collectedMissingRefs,
|
||||
};
|
||||
};
|
||||
|
||||
const objKey = (obj: { type: string; id: string }) => `${obj.type}:${obj.id}`;
|
||||
|
||||
type ObjectKey = string;
|
||||
|
||||
interface CollectedReference {
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const collectReferences = (
|
||||
objects: SavedObject[],
|
||||
alreadyProcessed: Set<ObjectKey>
|
||||
): CollectedReference[] => {
|
||||
const references: Map<string, CollectedReference> = new Map();
|
||||
objects.forEach((obj) => {
|
||||
obj.references?.forEach((ref) => {
|
||||
const refKey = objKey(ref);
|
||||
if (!alreadyProcessed.has(refKey)) {
|
||||
references.set(refKey, { type: ref.type, id: ref.id });
|
||||
}
|
||||
});
|
||||
});
|
||||
return [...references.values()];
|
||||
};
|
||||
|
||||
interface FetchReferencesResult {
|
||||
objects: SavedObject[];
|
||||
missingRefs: CollectedReference[];
|
||||
}
|
||||
|
||||
const fetchReferences = async ({
|
||||
references,
|
||||
client,
|
||||
namespace,
|
||||
}: {
|
||||
references: CollectedReference[];
|
||||
client: SavedObjectsClientContract;
|
||||
namespace?: string;
|
||||
}): Promise<FetchReferencesResult> => {
|
||||
const { saved_objects: savedObjects } = await client.bulkGet(references, { namespace });
|
||||
return {
|
||||
objects: savedObjects.filter((obj) => !obj.error),
|
||||
missingRefs: savedObjects
|
||||
.filter((obj) => obj.error)
|
||||
.map((obj) => ({ type: obj.type, id: obj.id })),
|
||||
};
|
||||
};
|
|
@ -1,606 +0,0 @@
|
|||
/*
|
||||
* 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 { SavedObject } from '../types';
|
||||
import { savedObjectsClientMock } from '../../mocks';
|
||||
import { getObjectReferencesToFetch, fetchNestedDependencies } from './fetch_nested_dependencies';
|
||||
import { SavedObjectsErrorHelpers } from '..';
|
||||
|
||||
describe('getObjectReferencesToFetch()', () => {
|
||||
test('works with no saved objects', () => {
|
||||
const map = new Map<string, SavedObject>();
|
||||
const result = getObjectReferencesToFetch(map);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('excludes already fetched objects', () => {
|
||||
const map = new Map<string, SavedObject>();
|
||||
map.set('index-pattern:1', {
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
map.set('visualization:2', {
|
||||
id: '2',
|
||||
type: 'visualization',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = getObjectReferencesToFetch(map);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns objects that are missing', () => {
|
||||
const map = new Map<string, SavedObject>();
|
||||
map.set('visualization:2', {
|
||||
id: '2',
|
||||
type: 'visualization',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = getObjectReferencesToFetch(map);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('does not fail on circular dependencies', () => {
|
||||
const map = new Map<string, SavedObject>();
|
||||
map.set('index-pattern:1', {
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'visualization',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
});
|
||||
map.set('visualization:2', {
|
||||
id: '2',
|
||||
type: 'visualization',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = getObjectReferencesToFetch(map);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectNestedDependencies', () => {
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test(`doesn't fetch when no dependencies are missing`, async () => {
|
||||
const savedObjects = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`doesn't fetch references that are already fetched`, async () => {
|
||||
const savedObjects = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('fetches dependencies at least one level deep', async () => {
|
||||
const savedObjects = [
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"namespace": undefined,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('fetches dependencies multiple levels deep', async () => {
|
||||
const savedObjects = [
|
||||
{
|
||||
id: '5',
|
||||
type: 'dashboard',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'panel_0',
|
||||
type: 'visualization',
|
||||
id: '4',
|
||||
},
|
||||
{
|
||||
name: 'panel_1',
|
||||
type: 'visualization',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '4',
|
||||
type: 'visualization',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'search',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'visualization',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "5",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "4",
|
||||
"name": "panel_0",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "3",
|
||||
"name": "panel_1",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "dashboard",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "4",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "ref_0",
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "3",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"id": "4",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "3",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"namespace": undefined,
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"namespace": undefined,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('returns list of missing references', async () => {
|
||||
const savedObjects = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
name: 'ref_1',
|
||||
type: 'index-pattern',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '1').output
|
||||
.payload,
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"missingRefs": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "ref_1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('does not fail on circular dependencies', async () => {
|
||||
const savedObjects = [
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'search',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await fetchNestedDependencies(savedObjects, savedObjectsClient);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"missingRefs": Array [],
|
||||
"objects": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "ref_0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"id": "1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "ref_0",
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"namespace": undefined,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* 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 { SavedObject, SavedObjectsClientContract } from '../types';
|
||||
|
||||
export function getObjectReferencesToFetch(savedObjectsMap: Map<string, SavedObject>) {
|
||||
const objectsToFetch = new Map<string, { type: string; id: string }>();
|
||||
for (const savedObject of savedObjectsMap.values()) {
|
||||
for (const ref of savedObject.references || []) {
|
||||
if (!savedObjectsMap.has(objKey(ref))) {
|
||||
objectsToFetch.set(objKey(ref), { type: ref.type, id: ref.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...objectsToFetch.values()];
|
||||
}
|
||||
|
||||
export async function fetchNestedDependencies(
|
||||
savedObjects: SavedObject[],
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
namespace?: string
|
||||
) {
|
||||
const savedObjectsMap = new Map<string, SavedObject>();
|
||||
for (const savedObject of savedObjects) {
|
||||
savedObjectsMap.set(objKey(savedObject), savedObject);
|
||||
}
|
||||
let objectsToFetch = getObjectReferencesToFetch(savedObjectsMap);
|
||||
while (objectsToFetch.length > 0) {
|
||||
const bulkGetResponse = await savedObjectsClient.bulkGet(objectsToFetch, { namespace });
|
||||
// Push to array result
|
||||
for (const savedObject of bulkGetResponse.saved_objects) {
|
||||
savedObjectsMap.set(objKey(savedObject), savedObject);
|
||||
}
|
||||
objectsToFetch = getObjectReferencesToFetch(savedObjectsMap);
|
||||
}
|
||||
const allObjects = [...savedObjectsMap.values()];
|
||||
return {
|
||||
objects: allObjects.filter((obj) => !obj.error),
|
||||
missingRefs: allObjects
|
||||
.filter((obj) => !!obj.error)
|
||||
.map((obj) => ({ type: obj.type, id: obj.id })),
|
||||
};
|
||||
}
|
||||
|
||||
const objKey = (obj: { type: string; id: string }) => `${obj.type}:${obj.id}`;
|
|
@ -12,7 +12,6 @@ import { Logger } from '../../logging';
|
|||
import { SavedObject, SavedObjectsClientContract } from '../types';
|
||||
import { SavedObjectsFindResult } from '../service';
|
||||
import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
|
||||
import { fetchNestedDependencies } from './fetch_nested_dependencies';
|
||||
import { sortObjects } from './sort_objects';
|
||||
import {
|
||||
SavedObjectsExportResultDetails,
|
||||
|
@ -22,7 +21,7 @@ import {
|
|||
SavedObjectsExportTransform,
|
||||
} from './types';
|
||||
import { SavedObjectsExportError } from './errors';
|
||||
import { applyExportTransforms } from './apply_export_transforms';
|
||||
import { collectExportedObjects } from './collect_exported_objects';
|
||||
import { byIdAscComparator, getPreservedOrderComparator, SavedObjectComparator } from './utils';
|
||||
|
||||
/**
|
||||
|
@ -118,28 +117,21 @@ export class SavedObjectsExporter {
|
|||
}: SavedObjectExportBaseOptions
|
||||
) {
|
||||
this.#log.debug(`Processing [${savedObjects.length}] saved objects.`);
|
||||
let exportedObjects: Array<SavedObject<unknown>>;
|
||||
let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = [];
|
||||
|
||||
savedObjects = await applyExportTransforms({
|
||||
request,
|
||||
const {
|
||||
objects: collectedObjects,
|
||||
missingRefs: missingReferences,
|
||||
} = await collectExportedObjects({
|
||||
objects: savedObjects,
|
||||
transforms: this.#exportTransforms,
|
||||
sortFunction,
|
||||
includeReferences: includeReferencesDeep,
|
||||
namespace,
|
||||
request,
|
||||
exportTransforms: this.#exportTransforms,
|
||||
savedObjectsClient: this.#savedObjectsClient,
|
||||
});
|
||||
|
||||
if (includeReferencesDeep) {
|
||||
this.#log.debug(`Fetching saved objects references.`);
|
||||
const fetchResult = await fetchNestedDependencies(
|
||||
savedObjects,
|
||||
this.#savedObjectsClient,
|
||||
namespace
|
||||
);
|
||||
exportedObjects = sortObjects(fetchResult.objects);
|
||||
missingReferences = fetchResult.missingRefs;
|
||||
} else {
|
||||
exportedObjects = sortObjects(savedObjects);
|
||||
}
|
||||
// sort with the provided sort function then with the default export sorting
|
||||
const exportedObjects = sortObjects(collectedObjects.sort(sortFunction));
|
||||
|
||||
// redact attributes that should not be exported
|
||||
const redactedObjects = includeNamespaces
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"type": "doc",
|
||||
"id": "test-export-transform:type_1-obj_1",
|
||||
"source": {
|
||||
"test-export-transform": {
|
||||
"title": "test_1-obj_1",
|
||||
"enabled": true
|
||||
},
|
||||
"type": "test-export-transform",
|
||||
"migrationVersion": {},
|
||||
"updated_at": "2018-12-21T00:43:07.096Z",
|
||||
"references": [
|
||||
{
|
||||
"type": "test-export-transform",
|
||||
"id": "type_1-obj_2",
|
||||
"name": "ref-1"
|
||||
},
|
||||
{
|
||||
"type": "test-export-add",
|
||||
"id": "type_2-obj_1",
|
||||
"name": "ref-2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"type": "doc",
|
||||
"id": "test-export-transform:type_1-obj_2",
|
||||
"source": {
|
||||
"test-export-transform": {
|
||||
"title": "test_1-obj_2",
|
||||
"enabled": true
|
||||
},
|
||||
"type": "test-export-transform",
|
||||
"migrationVersion": {},
|
||||
"updated_at": "2018-12-21T00:43:07.096Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"type": "doc",
|
||||
"id": "test-export-add:type_2-obj_1",
|
||||
"source": {
|
||||
"test-export-add": {
|
||||
"title": "test_2-obj_1"
|
||||
},
|
||||
"type": "test-export-add",
|
||||
"migrationVersion": {},
|
||||
"updated_at": "2018-12-21T00:43:07.096Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"type": "doc",
|
||||
"id": "test-export-add-dep:type_dep-obj_1",
|
||||
"source": {
|
||||
"test-export-add-dep": {
|
||||
"title": "type_dep-obj_1"
|
||||
},
|
||||
"type": "test-export-add-dep",
|
||||
"migrationVersion": {},
|
||||
"updated_at": "2018-12-21T00:43:07.096Z",
|
||||
"references": [
|
||||
{
|
||||
"type": "test-export-add",
|
||||
"id": "type_2-obj_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,499 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"auto_expand_replicas": "0-1",
|
||||
"number_of_replicas": "0"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"test-export-transform": {
|
||||
"properties": {
|
||||
"title": { "type": "text" },
|
||||
"enabled": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"test-export-add": {
|
||||
"properties": {
|
||||
"title": { "type": "text" }
|
||||
}
|
||||
},
|
||||
"test-export-add-dep": {
|
||||
"properties": {
|
||||
"title": { "type": "text" }
|
||||
}
|
||||
},
|
||||
"test-export-transform-error": {
|
||||
"properties": {
|
||||
"title": { "type": "text" }
|
||||
}
|
||||
},
|
||||
"test-export-invalid-transform": {
|
||||
"properties": {
|
||||
"title": { "type": "text" }
|
||||
}
|
||||
},
|
||||
"apm-telemetry": {
|
||||
"properties": {
|
||||
"has_any_services": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"services_per_agent": {
|
||||
"properties": {
|
||||
"go": {
|
||||
"type": "long",
|
||||
"null_value": 0
|
||||
},
|
||||
"java": {
|
||||
"type": "long",
|
||||
"null_value": 0
|
||||
},
|
||||
"js-base": {
|
||||
"type": "long",
|
||||
"null_value": 0
|
||||
},
|
||||
"nodejs": {
|
||||
"type": "long",
|
||||
"null_value": 0
|
||||
},
|
||||
"python": {
|
||||
"type": "long",
|
||||
"null_value": 0
|
||||
},
|
||||
"ruby": {
|
||||
"type": "long",
|
||||
"null_value": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"canvas-workpad": {
|
||||
"dynamic": "false",
|
||||
"properties": {
|
||||
"@created": {
|
||||
"type": "date"
|
||||
},
|
||||
"@timestamp": {
|
||||
"type": "date"
|
||||
},
|
||||
"id": {
|
||||
"type": "text",
|
||||
"index": false
|
||||
},
|
||||
"name": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"accessibility:disableAnimations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"buildNum": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"dateFormat:tz": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultIndex": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry:optIn": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"panelsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"refreshInterval": {
|
||||
"properties": {
|
||||
"display": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pause": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"section": {
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeFrom": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timeRestore": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"timeTo": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"properties": {
|
||||
"bounds": {
|
||||
"dynamic": false,
|
||||
"properties": {}
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"layerListJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"mapStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graph-workspace": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"numLinks": {
|
||||
"type": "integer"
|
||||
},
|
||||
"numVertices": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"wsState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index-pattern": {
|
||||
"properties": {
|
||||
"fieldFormatMap": {
|
||||
"type": "text"
|
||||
},
|
||||
"fields": {
|
||||
"type": "text"
|
||||
},
|
||||
"intervalName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"notExpandable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceFilters": {
|
||||
"type": "text"
|
||||
},
|
||||
"timeFieldName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"typeMeta": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kql-telemetry": {
|
||||
"properties": {
|
||||
"optInCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"optOutCount": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"migrationVersion": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"index-pattern": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
},
|
||||
"space": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"search": {
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"space": {
|
||||
"properties": {
|
||||
"_reserved": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"color": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"disabledFeatures": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"initials": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 2048
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"spaceId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"telemetry": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion-sheet": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion_chart_height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_columns": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_other_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_sheet": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"properties": {
|
||||
"accessCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 2048
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"savedSearchId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"visState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"references": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "nested"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,122 +19,169 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('export transforms', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load(
|
||||
'../functional/fixtures/es_archiver/saved_objects_management/export_transform'
|
||||
);
|
||||
});
|
||||
describe('root objects export transforms', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load(
|
||||
'../functional/fixtures/es_archiver/saved_objects_management/export_transform'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'../functional/fixtures/es_archiver/saved_objects_management/export_transform'
|
||||
);
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'../functional/fixtures/es_archiver/saved_objects_management/export_transform'
|
||||
);
|
||||
});
|
||||
|
||||
it('allows to mutate the objects during an export', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
type: ['test-export-transform'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = parseNdJson(resp.text);
|
||||
expect(objects.map((obj) => ({ id: obj.id, enabled: obj.attributes.enabled }))).to.eql([
|
||||
{
|
||||
id: 'type_1-obj_1',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'type_1-obj_2',
|
||||
enabled: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows to add additional objects to an export', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'test-export-add',
|
||||
id: 'type_2-obj_1',
|
||||
},
|
||||
],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = parseNdJson(resp.text);
|
||||
expect(objects.map((obj) => obj.id)).to.eql(['type_2-obj_1', 'type_dep-obj_1']);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows to add additional objects to an export when exporting by type', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
type: ['test-export-add'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = parseNdJson(resp.text);
|
||||
expect(objects.map((obj) => obj.id)).to.eql([
|
||||
'type_2-obj_1',
|
||||
'type_2-obj_2',
|
||||
'type_dep-obj_1',
|
||||
'type_dep-obj_2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 400 when the type causes a transform error', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
type: ['test-export-transform-error'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
const { attributes, ...error } = resp.body;
|
||||
expect(error).to.eql({
|
||||
error: 'Bad Request',
|
||||
message: 'Error transforming objects to export',
|
||||
statusCode: 400,
|
||||
it('allows to mutate the objects during an export', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
type: ['test-export-transform'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = parseNdJson(resp.text);
|
||||
expect(objects.map((obj) => ({ id: obj.id, enabled: obj.attributes.enabled }))).to.eql([
|
||||
{
|
||||
id: 'type_1-obj_1',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'type_1-obj_2',
|
||||
enabled: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
expect(attributes.cause).to.eql('Error during transform');
|
||||
expect(attributes.objects.map((obj: any) => obj.id)).to.eql(['type_4-obj_1']);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows to add additional objects to an export', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'test-export-add',
|
||||
id: 'type_2-obj_1',
|
||||
},
|
||||
],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = parseNdJson(resp.text);
|
||||
expect(objects.map((obj) => obj.id)).to.eql(['type_2-obj_1', 'type_dep-obj_1']);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows to add additional objects to an export when exporting by type', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
type: ['test-export-add'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = parseNdJson(resp.text);
|
||||
expect(objects.map((obj) => obj.id)).to.eql([
|
||||
'type_2-obj_1',
|
||||
'type_2-obj_2',
|
||||
'type_dep-obj_1',
|
||||
'type_dep-obj_2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 400 when the type causes a transform error', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
type: ['test-export-transform-error'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
const { attributes, ...error } = resp.body;
|
||||
expect(error).to.eql({
|
||||
error: 'Bad Request',
|
||||
message: 'Error transforming objects to export',
|
||||
statusCode: 400,
|
||||
});
|
||||
expect(attributes.cause).to.eql('Error during transform');
|
||||
expect(attributes.objects.map((obj: any) => obj.id)).to.eql(['type_4-obj_1']);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 400 when the type causes an invalid transform', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
type: ['test-export-invalid-transform'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Bad Request',
|
||||
message: 'Invalid transform performed on objects to export',
|
||||
statusCode: 400,
|
||||
attributes: {
|
||||
objectKeys: ['test-export-invalid-transform|type_3-obj_1'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 400 when the type causes an invalid transform', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
type: ['test-export-invalid-transform'],
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Bad Request',
|
||||
message: 'Invalid transform performed on objects to export',
|
||||
statusCode: 400,
|
||||
attributes: {
|
||||
objectKeys: ['test-export-invalid-transform|type_3-obj_1'],
|
||||
},
|
||||
describe('FOO nested export transforms', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load(
|
||||
'../functional/fixtures/es_archiver/saved_objects_management/nested_export_transform'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'../functional/fixtures/es_archiver/saved_objects_management/nested_export_transform'
|
||||
);
|
||||
});
|
||||
|
||||
it('execute export transforms for reference objects', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'test-export-transform',
|
||||
id: 'type_1-obj_1',
|
||||
},
|
||||
],
|
||||
includeReferencesDeep: true,
|
||||
excludeExportDetails: true,
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
const objects = parseNdJson(resp.text).sort((obj1, obj2) =>
|
||||
obj1.id.localeCompare(obj2.id)
|
||||
);
|
||||
expect(objects.map((obj) => obj.id)).to.eql([
|
||||
'type_1-obj_1',
|
||||
'type_1-obj_2',
|
||||
'type_2-obj_1',
|
||||
'type_dep-obj_1',
|
||||
]);
|
||||
|
||||
expect(objects[0].attributes.enabled).to.eql(false);
|
||||
expect(objects[1].attributes.enabled).to.eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue