Modify saved object import APIs to handle special use cases from the previous import process (#34161)

* Modify import APIs to handle special use cases from the previous import process

* Cleanup

* Add more examples to the docs

* Make title come from data inside file

* Fix some broken tests

* Fix docs

* Fix docs wording

* Apply PR feedback pt1

* Apply PR feedback pt2
This commit is contained in:
Mike Côté 2019-04-04 09:10:54 -04:00 committed by GitHub
parent 5c2267f2ca
commit 51e6a009ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1288 additions and 514 deletions

View file

@ -86,11 +86,58 @@ containing a JSON structure similar to the following example:
{ {
"id": "my-pattern", "id": "my-pattern",
"type": "index-pattern", "type": "index-pattern",
"title": "my-pattern-*",
"error": { "error": {
"statusCode": 409, "type": "conflict"
"message": "version conflict, document already exists",
}, },
}, },
], ],
} }
-------------------------------------------------- --------------------------------------------------
The following example imports a visualization and dashboard but the index pattern for the visualization reference doesn't exist.
[source,js]
--------------------------------------------------
POST api/saved_objects/_import
Content-Type: multipart/form-data; boundary=EXAMPLE
--EXAMPLE
Content-Disposition: form-data; name="file"; filename="export.ndjson"
Content-Type: application/ndjson
{"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern-*"}]}
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"ref_0","type":"visualization","id":"my-vis"}]}
--EXAMPLE--
--------------------------------------------------
// KIBANA
The call returns a response code of `200` and a response body
containing a JSON structure similar to the following example:
[source,js]
--------------------------------------------------
"success": false,
"successCount": 0,
"errors": [
{
"id": "my-vis",
"type": "visualization",
"title": "my-vis",
"error": {
"type": "missing_references",
"references": [
{
"type": "index-pattern",
"id": "my-pattern-*"
}
],
"blocking": [
{
"type": "dashboard",
"id": "my-dashboard"
}
]
}
}
]
--------------------------------------------------

View file

@ -3,7 +3,7 @@
experimental[This functionality is *experimental* and may be changed or removed completely in a future release.] experimental[This functionality is *experimental* and may be changed or removed completely in a future release.]
The resolve import errors API enables you to resolve errors given by the import API by either overwriting specific saved objects or changing references to a newly created object. The resolve import errors API enables you to resolve errors given by the import API by either retrying certain saved objects, overwriting specific saved objects or changing references to different saved objects.
Note: You cannot access this endpoint via the Console in Kibana. Note: You cannot access this endpoint via the Console in Kibana.
@ -16,27 +16,20 @@ Note: You cannot access this endpoint via the Console in Kibana.
The request body must be of type multipart/form-data. The request body must be of type multipart/form-data.
`file`:: `file`::
(ndjson) The same new line delimited JSON objects given to the import API. The same file given to the import API.
`overwrites` (optional):: `retries`::
(array) A list of `type` and `id` objects allowed to be overwritten on import. (array) A list of `type`, `id`, `replaceReferences` and `overwrite` objects to retry importing. The property `replaceReferences` is a list of `type`, `from` and `to` used to change the object's references.
`replaceReferences` (optional)::
(array) A list of `type`, `from` and `to` used to change imported saved object references to.
`skips` (optional)::
(array) A list of `type` and `id` objects to skip importing.
==== Response body ==== Response body
The response body will have a top level `success` property that indicates The response body will have a top level `success` property that indicates
if the import was successful or not as well as a `successCount` indicating how many records are successfully resolved. if resolving errors was successful or not as well as a `successCount` indicating how many records are successfully resolved.
In the scenario the import wasn't successful a top level `errors` array will contain the objects that failed to import. In the scenario resolving errors wasn't successful, a top level `errors` array will contain the objects that failed to be resolved.
==== Examples ==== Examples
The following example resolves errors for an index pattern and dashboard but indicates to skip the index pattern. The following example retries importing a dashboard.
This will cause the index pattern to not be in the system and the dashboard to overwrite the existing saved object.
[source,js] [source,js]
-------------------------------------------------- --------------------------------------------------
@ -46,14 +39,9 @@ Content-Type: multipart/form-data; boundary=EXAMPLE
Content-Disposition: form-data; name="file"; filename="export.ndjson" Content-Disposition: form-data; name="file"; filename="export.ndjson"
Content-Type: application/ndjson Content-Type: application/ndjson
{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}} {"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}
--EXAMPLE --EXAMPLE
Content-Disposition: form-data; name="skips" Content-Disposition: form-data; name="retries"
[{"type":"index-pattern","id":"my-pattern"}]
--EXAMPLE
Content-Disposition: form-data; name="overwrites"
[{"type":"dashboard","id":"my-dashboard"}] [{"type":"dashboard","id":"my-dashboard"}]
--EXAMPLE-- --EXAMPLE--
@ -71,8 +59,7 @@ containing a JSON structure similar to the following example:
} }
-------------------------------------------------- --------------------------------------------------
The following example resolves errors for a visualization and dashboard but indicates The following example resolves errors for a dashboard. This will cause the dashboard to overwrite the existing saved object.
to replace the dashboard references to another visualization.
[source,js] [source,js]
-------------------------------------------------- --------------------------------------------------
@ -82,12 +69,42 @@ Content-Type: multipart/form-data; boundary=EXAMPLE
Content-Disposition: form-data; name="file"; filename="export.ndjson" Content-Disposition: form-data; name="file"; filename="export.ndjson"
Content-Type: application/ndjson Content-Type: application/ndjson
{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"}} {"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"panel_0","type":"visualization","id":"my-vis"}]} {"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}
--EXAMPLE --EXAMPLE
Content-Disposition: form-data; name="replaceReferences" Content-Disposition: form-data; name="retries"
[{"type":"visualization","from":"my-vis","to":"my-vis-2"}] [{"type":"dashboard","id":"my-dashboard","overwrite":true}]
--EXAMPLE--
--------------------------------------------------
// KIBANA
A successful call returns a response code of `200` and a response body
containing a JSON structure similar to the following example:
[source,js]
--------------------------------------------------
{
"success": true,
"successCount": 1
}
--------------------------------------------------
The following example resolves errors for a visualization by replacing the index pattern to another.
[source,js]
--------------------------------------------------
POST api/saved_objects/_resolve_import_errors
Content-Type: multipart/form-data; boundary=EXAMPLE
--EXAMPLE
Content-Disposition: form-data; name="file"; filename="export.ndjson"
Content-Type: application/ndjson
{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"},"references":[{"name":"ref_0","type":"index-pattern","id":"missing"}]}
--EXAMPLE
Content-Disposition: form-data; name="retries"
[{"type":"visualization","id":"my-vis","replaceReferences":[{"type":"index-pattern","from":"missing","to":"existing"}]}]
--EXAMPLE-- --EXAMPLE--
-------------------------------------------------- --------------------------------------------------
// KIBANA // KIBANA

View file

@ -20,34 +20,45 @@
import { createObjectsFilter } from './create_objects_filter'; import { createObjectsFilter } from './create_objects_filter';
describe('createObjectsFilter()', () => { describe('createObjectsFilter()', () => {
test('filters should return false when contains empty parameters', () => { test('filter should return false when contains empty parameters', () => {
const fn = createObjectsFilter([], [], []); const fn = createObjectsFilter([]);
expect(fn({ type: 'a', id: '1', attributes: {}, references: [] })).toEqual(false); expect(fn({ type: 'a', id: '1', attributes: {}, references: [] })).toEqual(false);
}); });
test('filters should exclude skips', () => { test('filter should return true for objects that are being retried', () => {
const fn = createObjectsFilter( const fn = createObjectsFilter([
[ {
{ type: 'a',
type: 'a', id: '1',
id: '1', overwrite: false,
}, replaceReferences: [],
], },
[], ]);
[
{
type: 'b',
from: '1',
to: '2',
},
]
);
expect( expect(
fn({ fn({
type: 'a', type: 'a',
id: '1', id: '1',
attributes: {}, attributes: {},
references: [{ name: 'ref_0', type: 'b', id: '1' }], references: [],
})
).toEqual(true);
});
test(`filter should return false for objects that aren't being retried`, () => {
const fn = createObjectsFilter([
{
type: 'a',
id: '1',
overwrite: false,
replaceReferences: [],
},
]);
expect(
fn({
type: 'b',
id: '1',
attributes: {},
references: [],
}) })
).toEqual(false); ).toEqual(false);
expect( expect(
@ -55,131 +66,8 @@ describe('createObjectsFilter()', () => {
type: 'a', type: 'a',
id: '2', id: '2',
attributes: {}, attributes: {},
references: [{ name: 'ref_0', type: 'b', id: '1' }], references: [],
})
).toEqual(true);
});
test('filter should include references to replace', () => {
const fn = createObjectsFilter(
[],
[],
[
{
type: 'b',
from: '1',
to: '2',
},
]
);
expect(
fn({
type: 'a',
id: '1',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '1',
},
],
})
).toEqual(true);
expect(
fn({
type: 'a',
id: '1',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '2',
},
],
}) })
).toEqual(false); ).toEqual(false);
}); });
test('filter should include objects to overwrite', () => {
const fn = createObjectsFilter(
[],
[
{
type: 'a',
id: '1',
},
],
[]
);
expect(fn({ type: 'a', id: '1', attributes: {}, references: [] })).toEqual(true);
expect(fn({ type: 'a', id: '2', attributes: {}, references: [] })).toEqual(false);
});
test('filter should work with skips, overwrites and replaceReferences', () => {
const fn = createObjectsFilter(
[
{
type: 'a',
id: '1',
},
],
[
{
type: 'a',
id: '2',
},
],
[
{
type: 'b',
from: '1',
to: '2',
},
]
);
expect(
fn({
type: 'a',
id: '1',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '1',
},
],
})
).toEqual(false);
expect(
fn({
type: 'a',
id: '2',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '2',
},
],
})
).toEqual(true);
expect(
fn({
type: 'a',
id: '3',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '1',
},
],
})
).toEqual(true);
});
}); });

View file

@ -18,37 +18,11 @@
*/ */
import { SavedObject } from '../service'; import { SavedObject } from '../service';
import { Retry } from './types';
export function createObjectsFilter( export function createObjectsFilter(retries: Retry[]) {
skips: Array<{ const retryKeys = new Set<string>(retries.map(retry => `${retry.type}:${retry.id}`));
type: string;
id: string;
}>,
overwrites: Array<{
type: string;
id: string;
}>,
replaceReferences: Array<{
type: string;
from: string;
to: string;
}>
) {
const refReplacements = replaceReferences.map(ref => `${ref.type}:${ref.from}`);
return (obj: SavedObject) => { return (obj: SavedObject) => {
if (skips.some(skipObj => skipObj.type === obj.type && skipObj.id === obj.id)) { return retryKeys.has(`${obj.type}:${obj.id}`);
return false;
}
if (
overwrites.some(overwriteObj => overwriteObj.type === obj.type && overwriteObj.id === obj.id)
) {
return true;
}
for (const reference of obj.references || []) {
if (refReplacements.includes(`${reference.type}:${reference.id}`)) {
return true;
}
}
return false;
}; };
} }

View file

@ -23,7 +23,7 @@ import { extractErrors } from './extract_errors';
describe('extractErrors()', () => { describe('extractErrors()', () => {
test('returns empty array when no errors exist', () => { test('returns empty array when no errors exist', () => {
const savedObjects: SavedObject[] = []; const savedObjects: SavedObject[] = [];
const result = extractErrors(savedObjects); const result = extractErrors(savedObjects, savedObjects);
expect(result).toMatchInlineSnapshot(`Array []`); expect(result).toMatchInlineSnapshot(`Array []`);
}); });
@ -32,13 +32,17 @@ describe('extractErrors()', () => {
{ {
id: '1', id: '1',
type: 'dashboard', type: 'dashboard',
attributes: {}, attributes: {
title: 'My Dashboard 1',
},
references: [], references: [],
}, },
{ {
id: '2', id: '2',
type: 'dashboard', type: 'dashboard',
attributes: {}, attributes: {
title: 'My Dashboard 2',
},
references: [], references: [],
error: { error: {
statusCode: 409, statusCode: 409,
@ -48,7 +52,9 @@ describe('extractErrors()', () => {
{ {
id: '3', id: '3',
type: 'dashboard', type: 'dashboard',
attributes: {}, attributes: {
title: 'My Dashboard 3',
},
references: [], references: [],
error: { error: {
statusCode: 400, statusCode: 400,
@ -56,7 +62,7 @@ describe('extractErrors()', () => {
}, },
}, },
]; ];
const result = extractErrors(savedObjects); const result = extractErrors(savedObjects, savedObjects);
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
Array [ Array [
Object { Object {
@ -64,6 +70,7 @@ Array [
"type": "conflict", "type": "conflict",
}, },
"id": "2", "id": "2",
"title": "My Dashboard 2",
"type": "dashboard", "type": "dashboard",
}, },
Object { Object {
@ -73,6 +80,7 @@ Array [
"type": "unknown", "type": "unknown",
}, },
"id": "3", "id": "3",
"title": "My Dashboard 3",
"type": "dashboard", "type": "dashboard",
}, },
] ]

View file

@ -20,14 +20,29 @@
import { SavedObject } from '../service'; import { SavedObject } from '../service';
import { ImportError } from './types'; import { ImportError } from './types';
export function extractErrors(savedObjects: SavedObject[]) { export function extractErrors(
savedObjectResults: SavedObject[],
savedObjectsToImport: SavedObject[]
) {
const errors: ImportError[] = []; const errors: ImportError[] = [];
for (const savedObject of savedObjects) { const originalSavedObjectsMap = new Map<string, SavedObject>();
for (const savedObject of savedObjectsToImport) {
originalSavedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject);
}
for (const savedObject of savedObjectResults) {
if (savedObject.error) { if (savedObject.error) {
const originalSavedObject = originalSavedObjectsMap.get(
`${savedObject.type}:${savedObject.id}`
);
const title =
originalSavedObject &&
originalSavedObject.attributes &&
originalSavedObject.attributes.title;
if (savedObject.error.statusCode === 409) { if (savedObject.error.statusCode === 409) {
errors.push({ errors.push({
id: savedObject.id, id: savedObject.id,
type: savedObject.type, type: savedObject.type,
title,
error: { error: {
type: 'conflict', type: 'conflict',
}, },
@ -37,6 +52,7 @@ export function extractErrors(savedObjects: SavedObject[]) {
errors.push({ errors.push({
id: savedObject.id, id: savedObject.id,
type: savedObject.type, type: savedObject.type,
title,
error: { error: {
...savedObject.error, ...savedObject.error,
type: 'unknown', type: 'unknown',

View file

@ -26,25 +26,33 @@ describe('importSavedObjects()', () => {
{ {
id: '1', id: '1',
type: 'index-pattern', type: 'index-pattern',
attributes: {}, attributes: {
title: 'My Index Pattern',
},
references: [], references: [],
}, },
{ {
id: '2', id: '2',
type: 'search', type: 'search',
attributes: {}, attributes: {
title: 'My Search',
},
references: [], references: [],
}, },
{ {
id: '3', id: '3',
type: 'visualization', type: 'visualization',
attributes: {}, attributes: {
title: 'My Visualization',
},
references: [], references: [],
}, },
{ {
id: '4', id: '4',
type: 'dashboard', type: 'dashboard',
attributes: {}, attributes: {
title: 'My Dashboard',
},
references: [], references: [],
}, },
]; ];
@ -60,13 +68,27 @@ describe('importSavedObjects()', () => {
}; };
beforeEach(() => { beforeEach(() => {
savedObjectsClient.bulkCreate.mockReset(); jest.resetAllMocks();
savedObjectsClient.bulkGet.mockReset(); });
savedObjectsClient.create.mockReset();
savedObjectsClient.delete.mockReset(); test('returns early when no objects exist', async () => {
savedObjectsClient.find.mockReset(); const readStream = new Readable({
savedObjectsClient.get.mockReset(); read() {
savedObjectsClient.update.mockReset(); this.push(null);
},
});
const result = await importSavedObjects({
readStream,
objectLimit: 1,
overwrite: false,
savedObjectsClient,
});
expect(result).toMatchInlineSnapshot(`
Object {
"success": true,
"successCount": 0,
}
`);
}); });
test('calls bulkCreate without overwrite', async () => { test('calls bulkCreate without overwrite', async () => {
@ -98,25 +120,33 @@ Object {
Array [ Array [
Array [ Array [
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Index Pattern",
},
"id": "1", "id": "1",
"references": Array [], "references": Array [],
"type": "index-pattern", "type": "index-pattern",
}, },
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Search",
},
"id": "2", "id": "2",
"references": Array [], "references": Array [],
"type": "search", "type": "search",
}, },
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Visualization",
},
"id": "3", "id": "3",
"references": Array [], "references": Array [],
"type": "visualization", "type": "visualization",
}, },
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Dashboard",
},
"id": "4", "id": "4",
"references": Array [], "references": Array [],
"type": "dashboard", "type": "dashboard",
@ -166,25 +196,33 @@ Object {
Array [ Array [
Array [ Array [
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Index Pattern",
},
"id": "1", "id": "1",
"references": Array [], "references": Array [],
"type": "index-pattern", "type": "index-pattern",
}, },
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Search",
},
"id": "2", "id": "2",
"references": Array [], "references": Array [],
"type": "search", "type": "search",
}, },
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Visualization",
},
"id": "3", "id": "3",
"references": Array [], "references": Array [],
"type": "visualization", "type": "visualization",
}, },
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Dashboard",
},
"id": "4", "id": "4",
"references": Array [], "references": Array [],
"type": "dashboard", "type": "dashboard",
@ -205,7 +243,7 @@ Object {
`); `);
}); });
test('extracts errors', async () => { test('extracts errors for conflicts', async () => {
const readStream = new Readable({ const readStream = new Readable({
read() { read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n')); savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
@ -237,6 +275,7 @@ Object {
"type": "conflict", "type": "conflict",
}, },
"id": "1", "id": "1",
"title": "My Index Pattern",
"type": "index-pattern", "type": "index-pattern",
}, },
Object { Object {
@ -244,6 +283,7 @@ Object {
"type": "conflict", "type": "conflict",
}, },
"id": "2", "id": "2",
"title": "My Search",
"type": "search", "type": "search",
}, },
Object { Object {
@ -251,6 +291,7 @@ Object {
"type": "conflict", "type": "conflict",
}, },
"id": "3", "id": "3",
"title": "My Visualization",
"type": "visualization", "type": "visualization",
}, },
Object { Object {
@ -258,12 +299,122 @@ Object {
"type": "conflict", "type": "conflict",
}, },
"id": "4", "id": "4",
"title": "My Dashboard",
"type": "dashboard", "type": "dashboard",
}, },
], ],
"success": false, "success": false,
"successCount": 0, "successCount": 0,
} }
`);
});
test('validates references', async () => {
const readStream = new Readable({
read() {
this.push(
JSON.stringify({
id: '1',
type: 'search',
attributes: {
title: 'My Search',
},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '2',
},
],
}) + '\n'
);
this.push(
JSON.stringify({
id: '3',
type: 'visualization',
attributes: {
title: 'My Visualization',
},
references: [
{
name: 'ref_0',
type: 'search',
id: '1',
},
],
}) + '\n'
);
this.push(null);
},
});
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
type: 'index-pattern',
id: '2',
error: {
statusCode: 404,
message: 'Not found',
},
},
],
});
const result = await importSavedObjects({
readStream,
objectLimit: 4,
overwrite: false,
savedObjectsClient,
});
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [
Object {
"error": Object {
"blocking": Array [
Object {
"id": "3",
"type": "visualization",
},
],
"references": Array [
Object {
"id": "2",
"type": "index-pattern",
},
],
"type": "missing_references",
},
"id": "1",
"title": "My Search",
"type": "search",
},
],
"success": false,
"successCount": 0,
}
`);
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "2",
"type": "index-pattern",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`); `);
}); });
}); });

View file

@ -43,13 +43,14 @@ export async function importSavedObjects({
overwrite, overwrite,
savedObjectsClient, savedObjectsClient,
}: ImportSavedObjectsOptions): Promise<ImportResponse> { }: ImportSavedObjectsOptions): Promise<ImportResponse> {
// Get the objects to import
const objectsFromStream = await collectSavedObjects(readStream, objectLimit); const objectsFromStream = await collectSavedObjects(readStream, objectLimit);
// Validate references
const { filteredObjects, errors: validationErrors } = await validateReferences( const { filteredObjects, errors: validationErrors } = await validateReferences(
objectsFromStream, objectsFromStream,
savedObjectsClient savedObjectsClient
); );
// Exit early if no objects to import
if (filteredObjects.length === 0) { if (filteredObjects.length === 0) {
return { return {
success: validationErrors.length === 0, success: validationErrors.length === 0,
@ -57,15 +58,18 @@ export async function importSavedObjects({
...(validationErrors.length ? { errors: validationErrors } : {}), ...(validationErrors.length ? { errors: validationErrors } : {}),
}; };
} }
// Create objects in bulk
const bulkCreateResult = await savedObjectsClient.bulkCreate(filteredObjects, { const bulkCreateResult = await savedObjectsClient.bulkCreate(filteredObjects, {
overwrite, overwrite,
}); });
const errors = [...validationErrors, ...extractErrors(bulkCreateResult.saved_objects)]; const errors = [
...validationErrors,
...extractErrors(bulkCreateResult.saved_objects, filteredObjects),
];
return { return {
success: errors.length === 0, success: errors.length === 0,
successCount: objectsFromStream.length - errors.length, successCount: bulkCreateResult.saved_objects.filter(obj => !obj.error).length,
...(errors.length ? { errors } : {}), ...(errors.length ? { errors } : {}),
}; };
} }

View file

@ -26,25 +26,33 @@ describe('resolveImportErrors()', () => {
{ {
id: '1', id: '1',
type: 'index-pattern', type: 'index-pattern',
attributes: {}, attributes: {
title: 'My Index Pattern',
},
references: [], references: [],
}, },
{ {
id: '2', id: '2',
type: 'search', type: 'search',
attributes: {}, attributes: {
title: 'My Search',
},
references: [], references: [],
}, },
{ {
id: '3', id: '3',
type: 'visualization', type: 'visualization',
attributes: {}, attributes: {
title: 'My Visualization',
},
references: [], references: [],
}, },
{ {
id: '4', id: '4',
type: 'dashboard', type: 'dashboard',
attributes: {}, attributes: {
title: 'My Dashboard',
},
references: [ references: [
{ {
name: 'panel_0', name: 'panel_0',
@ -66,13 +74,7 @@ describe('resolveImportErrors()', () => {
}; };
beforeEach(() => { beforeEach(() => {
savedObjectsClient.bulkCreate.mockReset(); jest.resetAllMocks();
savedObjectsClient.bulkGet.mockReset();
savedObjectsClient.create.mockReset();
savedObjectsClient.delete.mockReset();
savedObjectsClient.find.mockReset();
savedObjectsClient.get.mockReset();
savedObjectsClient.update.mockReset();
}); });
test('works with empty parameters', async () => { test('works with empty parameters', async () => {
@ -83,15 +85,13 @@ describe('resolveImportErrors()', () => {
}, },
}); });
savedObjectsClient.bulkCreate.mockResolvedValue({ savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects, saved_objects: [],
}); });
const result = await resolveImportErrors({ const result = await resolveImportErrors({
readStream, readStream,
objectLimit: 4, objectLimit: 4,
skips: [], retries: [],
overwrites: [],
savedObjectsClient, savedObjectsClient,
replaceReferences: [],
}); });
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
Object { Object {
@ -102,66 +102,28 @@ Object {
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`);
}); });
test('works with skips', async () => { test('works with retries', async () => {
const readStream = new Readable({ const readStream = new Readable({
read() { read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n')); savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null); this.push(null);
}, },
}); });
savedObjectsClient.bulkCreate.mockResolvedValue({ savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: savedObjects, saved_objects: savedObjects.filter(obj => obj.type === 'visualization' && obj.id === '3'),
}); });
const result = await resolveImportErrors({ const result = await resolveImportErrors({
readStream, readStream,
objectLimit: 4, objectLimit: 4,
skips: [ retries: [
{
type: 'dashboard',
id: '4',
},
],
overwrites: [],
savedObjectsClient,
replaceReferences: [
{ {
type: 'visualization', type: 'visualization',
from: '3', id: '3',
to: '30', replaceReferences: [],
}, overwrite: false,
],
});
expect(result).toMatchInlineSnapshot(`
Object {
"success": true,
"successCount": 0,
}
`);
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`);
});
test('works with overwrites', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
},
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects,
});
const result = await resolveImportErrors({
readStream,
objectLimit: 4,
skips: [],
overwrites: [
{
type: 'index-pattern',
id: '1',
}, },
], ],
savedObjectsClient, savedObjectsClient,
replaceReferences: [],
}); });
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
Object { Object {
@ -175,7 +137,64 @@ Object {
Array [ Array [
Array [ Array [
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Visualization",
},
"id": "3",
"references": Array [],
"type": "visualization",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('works with overwrites', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
},
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects.filter(obj => obj.type === 'index-pattern' && obj.id === '1'),
});
const result = await resolveImportErrors({
readStream,
objectLimit: 4,
retries: [
{
type: 'index-pattern',
id: '1',
overwrite: true,
replaceReferences: [],
},
],
savedObjectsClient,
});
expect(result).toMatchInlineSnapshot(`
Object {
"success": true,
"successCount": 1,
}
`);
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"attributes": Object {
"title": "My Index Pattern",
},
"id": "1", "id": "1",
"references": Array [], "references": Array [],
"type": "index-pattern", "type": "index-pattern",
@ -204,21 +223,26 @@ Object {
}, },
}); });
savedObjectsClient.bulkCreate.mockResolvedValue({ savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects, saved_objects: savedObjects.filter(obj => obj.type === 'dashboard' && obj.id === '4'),
}); });
const result = await resolveImportErrors({ const result = await resolveImportErrors({
readStream, readStream,
objectLimit: 4, objectLimit: 4,
skips: [], retries: [
overwrites: [],
savedObjectsClient,
replaceReferences: [
{ {
type: 'visualization', type: 'dashboard',
from: '3', id: '4',
to: '13', overwrite: false,
replaceReferences: [
{
type: 'visualization',
from: '3',
to: '13',
},
],
}, },
], ],
savedObjectsClient,
}); });
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
Object { Object {
@ -232,7 +256,9 @@ Object {
Array [ Array [
Array [ Array [
Object { Object {
"attributes": Object {}, "attributes": Object {
"title": "My Dashboard",
},
"id": "4", "id": "4",
"references": Array [ "references": Array [
Object { Object {
@ -244,9 +270,198 @@ Object {
"type": "dashboard", "type": "dashboard",
}, },
], ],
Object { ],
"overwrite": true, ],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('extracts errors for conflicts', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
}, },
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects.map(savedObject => ({
type: savedObject.type,
id: savedObject.id,
error: {
statusCode: 409,
message: 'conflict',
},
})),
});
const result = await resolveImportErrors({
readStream,
objectLimit: 4,
retries: savedObjects.map(obj => ({
type: obj.type,
id: obj.id,
overwrite: false,
replaceReferences: [],
})),
savedObjectsClient,
});
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [
Object {
"error": Object {
"type": "conflict",
},
"id": "1",
"title": "My Index Pattern",
"type": "index-pattern",
},
Object {
"error": Object {
"type": "conflict",
},
"id": "2",
"title": "My Search",
"type": "search",
},
Object {
"error": Object {
"type": "conflict",
},
"id": "3",
"title": "My Visualization",
"type": "visualization",
},
Object {
"error": Object {
"type": "conflict",
},
"id": "4",
"title": "My Dashboard",
"type": "dashboard",
},
],
"success": false,
"successCount": 0,
}
`);
});
test('validates references', async () => {
const readStream = new Readable({
read() {
this.push(
JSON.stringify({
id: '1',
type: 'search',
attributes: {
title: 'My Search',
},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '2',
},
],
}) + '\n'
);
this.push(
JSON.stringify({
id: '3',
type: 'visualization',
attributes: {
title: 'My Visualization',
},
references: [
{
name: 'ref_0',
type: 'search',
id: '1',
},
],
}) + '\n'
);
this.push(null);
},
});
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
type: 'index-pattern',
id: '2',
error: {
statusCode: 404,
message: 'Not found',
},
},
],
});
const result = await resolveImportErrors({
readStream,
objectLimit: 2,
retries: [
{
type: 'search',
id: '1',
overwrite: false,
replaceReferences: [],
},
{
type: 'visualization',
id: '3',
overwrite: false,
replaceReferences: [],
},
],
savedObjectsClient,
});
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [
Object {
"error": Object {
"blocking": Array [
Object {
"id": "3",
"type": "visualization",
},
],
"references": Array [
Object {
"id": "2",
"type": "index-pattern",
},
],
"type": "missing_references",
},
"id": "1",
"title": "My Search",
"type": "search",
},
],
"success": false,
"successCount": 0,
}
`);
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "2",
"type": "index-pattern",
},
],
], ],
], ],
"results": Array [ "results": Array [

View file

@ -22,25 +22,15 @@ import { SavedObjectsClient } from '../service';
import { collectSavedObjects } from './collect_saved_objects'; import { collectSavedObjects } from './collect_saved_objects';
import { createObjectsFilter } from './create_objects_filter'; import { createObjectsFilter } from './create_objects_filter';
import { extractErrors } from './extract_errors'; import { extractErrors } from './extract_errors';
import { ImportError } from './types'; import { splitOverwrites } from './split_overwrites';
import { ImportError, Retry } from './types';
import { validateReferences } from './validate_references';
interface ResolveImportErrorsOptions { interface ResolveImportErrorsOptions {
readStream: Readable; readStream: Readable;
objectLimit: number; objectLimit: number;
savedObjectsClient: SavedObjectsClient; savedObjectsClient: SavedObjectsClient;
overwrites: Array<{ retries: Retry[];
type: string;
id: string;
}>;
replaceReferences: Array<{
type: string;
from: string;
to: string;
}>;
skips: Array<{
type: string;
id: string;
}>;
} }
interface ImportResponse { interface ImportResponse {
@ -52,38 +42,65 @@ interface ImportResponse {
export async function resolveImportErrors({ export async function resolveImportErrors({
readStream, readStream,
objectLimit, objectLimit,
skips, retries,
overwrites,
savedObjectsClient, savedObjectsClient,
replaceReferences,
}: ResolveImportErrorsOptions): Promise<ImportResponse> { }: ResolveImportErrorsOptions): Promise<ImportResponse> {
let successCount = 0;
let errors: ImportError[] = []; let errors: ImportError[] = [];
const filter = createObjectsFilter(skips, overwrites, replaceReferences); const filter = createObjectsFilter(retries);
// Get the objects to resolve errors
const objectsToResolve = await collectSavedObjects(readStream, objectLimit, filter); const objectsToResolve = await collectSavedObjects(readStream, objectLimit, filter);
// Replace references // Create a map of references to replace for each object to avoid iterating through
const refReplacementsMap: Record<string, string> = {}; // retries for every object to resolve
for (const { type, to, from } of replaceReferences) { const retriesReferencesMap = new Map<string, { [key: string]: string }>();
refReplacementsMap[`${type}:${from}`] = to; for (const retry of retries) {
const map: { [key: string]: string } = {};
for (const { type, from, to } of retry.replaceReferences) {
map[`${type}:${from}`] = to;
}
retriesReferencesMap.set(`${retry.type}:${retry.id}`, map);
} }
// Replace references
for (const savedObject of objectsToResolve) { for (const savedObject of objectsToResolve) {
const refMap = retriesReferencesMap.get(`${savedObject.type}:${savedObject.id}`);
if (!refMap) {
continue;
}
for (const reference of savedObject.references || []) { for (const reference of savedObject.references || []) {
if (refReplacementsMap[`${reference.type}:${reference.id}`]) { if (refMap[`${reference.type}:${reference.id}`]) {
reference.id = refReplacementsMap[`${reference.type}:${reference.id}`]; reference.id = refMap[`${reference.type}:${reference.id}`];
} }
} }
} }
if (objectsToResolve.length) { // Validate references
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToResolve, { const { filteredObjects, errors: validationErrors } = await validateReferences(
objectsToResolve,
savedObjectsClient
);
errors = errors.concat(validationErrors);
// Bulk create in two batches, overwrites and non-overwrites
const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(filteredObjects, retries);
if (objectsToOverwrite.length) {
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToOverwrite, {
overwrite: true, overwrite: true,
}); });
errors = extractErrors(bulkCreateResult.saved_objects); errors = errors.concat(extractErrors(bulkCreateResult.saved_objects, objectsToOverwrite));
successCount += bulkCreateResult.saved_objects.filter(obj => !obj.error).length;
}
if (objectsToNotOverwrite.length) {
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToNotOverwrite);
errors = errors.concat(extractErrors(bulkCreateResult.saved_objects, objectsToNotOverwrite));
successCount += bulkCreateResult.saved_objects.filter(obj => !obj.error).length;
} }
return { return {
successCount,
success: errors.length === 0, success: errors.length === 0,
successCount: objectsToResolve.length - errors.length,
...(errors.length ? { errors } : {}), ...(errors.length ? { errors } : {}),
}; };
} }

View file

@ -0,0 +1,92 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { splitOverwrites } from './split_overwrites';
describe('splitOverwrites()', () => {
test('should split array accordingly', () => {
const retries = [
{
type: 'a',
id: '1',
overwrite: true,
replaceReferences: [],
},
{
id: '2',
type: 'b',
overwrite: false,
replaceReferences: [],
},
{
type: 'c',
id: '3',
overwrite: true,
replaceReferences: [],
},
];
const savedObjects = [
{
id: '1',
type: 'a',
attributes: {},
references: [],
},
{
id: '2',
type: 'b',
attributes: {},
references: [],
},
{
id: '3',
type: 'c',
attributes: {},
references: [],
},
];
const result = splitOverwrites(savedObjects, retries);
expect(result).toMatchInlineSnapshot(`
Object {
"objectsToNotOverwrite": Array [
Object {
"attributes": Object {},
"id": "2",
"references": Array [],
"type": "b",
},
],
"objectsToOverwrite": Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "a",
},
Object {
"attributes": Object {},
"id": "3",
"references": Array [],
"type": "c",
},
],
}
`);
});
});

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SavedObject } from '../service';
import { Retry } from './types';
export function splitOverwrites(savedObjects: SavedObject[], retries: Retry[]) {
const objectsToOverwrite: SavedObject[] = [];
const objectsToNotOverwrite: SavedObject[] = [];
const overwrites = retries
.filter(retry => retry.overwrite)
.map(retry => `${retry.type}:${retry.id}`);
for (const savedObject of savedObjects) {
if (overwrites.includes(`${savedObject.type}:${savedObject.id}`)) {
objectsToOverwrite.push(savedObject);
} else {
objectsToNotOverwrite.push(savedObject);
}
}
return { objectsToOverwrite, objectsToNotOverwrite };
}

View file

@ -17,26 +17,42 @@
* under the License. * under the License.
*/ */
interface ConflictError { export interface Retry {
type: string;
id: string;
overwrite: boolean;
replaceReferences: Array<{
type: string;
from: string;
to: string;
}>;
}
export interface ConflictError {
type: 'conflict'; type: 'conflict';
} }
interface UnknownError { export interface UnknownError {
type: 'unknown'; type: 'unknown';
message: string; message: string;
statusCode: number; statusCode: number;
} }
interface MissingReferencesError { export interface MissingReferencesError {
type: 'missing_references'; type: 'missing_references';
references: Array<{ references: Array<{
type: string; type: string;
id: string; id: string;
}>; }>;
blocking: Array<{
type: string;
id: string;
}>;
} }
export interface ImportError { export interface ImportError {
id: string; id: string;
type: string; type: string;
title?: string;
error: ConflictError | MissingReferencesError | UnknownError; error: ConflictError | MissingReferencesError | UnknownError;
} }

View file

@ -299,7 +299,9 @@ Object {
{ {
id: '2', id: '2',
type: 'visualization', type: 'visualization',
attributes: {}, attributes: {
title: 'My Visualization 2',
},
references: [ references: [
{ {
name: 'ref_0', name: 'ref_0',
@ -311,7 +313,9 @@ Object {
{ {
id: '4', id: '4',
type: 'visualization', type: 'visualization',
attributes: {}, attributes: {
title: 'My Visualization 4',
},
references: [ references: [
{ {
name: 'ref_0', name: 'ref_0',
@ -342,6 +346,7 @@ Object {
"errors": Array [ "errors": Array [
Object { Object {
"error": Object { "error": Object {
"blocking": Array [],
"references": Array [ "references": Array [
Object { Object {
"id": "3", "id": "3",
@ -351,10 +356,12 @@ Object {
"type": "missing_references", "type": "missing_references",
}, },
"id": "2", "id": "2",
"title": "My Visualization 2",
"type": "visualization", "type": "visualization",
}, },
Object { Object {
"error": Object { "error": Object {
"blocking": Array [],
"references": Array [ "references": Array [
Object { Object {
"id": "5", "id": "5",
@ -372,6 +379,7 @@ Object {
"type": "missing_references", "type": "missing_references",
}, },
"id": "4", "id": "4",
"title": "My Visualization 4",
"type": "visualization", "type": "visualization",
}, },
], ],
@ -583,4 +591,36 @@ Object {
`); `);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
}); });
test('throws when bulkGet fails', async () => {
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
{
id: '1',
type: 'index-pattern',
error: {
statusCode: 400,
message: 'Error',
},
},
],
});
const savedObjects = [
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
],
},
];
await expect(
validateReferences(savedObjects, savedObjectsClient)
).rejects.toThrowErrorMatchingInlineSnapshot(`"Bad Request"`);
});
}); });

View file

@ -17,37 +17,61 @@
* under the License. * under the License.
*/ */
import Boom from 'boom';
import { SavedObject, SavedObjectsClient } from '../service'; import { SavedObject, SavedObjectsClient } from '../service';
import { ImportError } from './types'; import { ImportError } from './types';
const ENFORCED_TYPES = ['index-pattern', 'search']; const REF_TYPES_TO_VLIDATE = ['index-pattern', 'search'];
function filterReferencesToValidate({ type }: { type: string }) {
return REF_TYPES_TO_VLIDATE.includes(type);
}
export async function getNonExistingReferenceAsKeys( export async function getNonExistingReferenceAsKeys(
savedObjects: SavedObject[], savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClient savedObjectsClient: SavedObjectsClient
) { ) {
const collector = new Map(); const collector = new Map();
// Collect all references within objects
for (const savedObject of savedObjects) { for (const savedObject of savedObjects) {
for (const { type, id } of savedObject.references || []) { const filteredReferences = (savedObject.references || []).filter(filterReferencesToValidate);
if (!ENFORCED_TYPES.includes(type)) { for (const { type, id } of filteredReferences) {
continue;
}
collector.set(`${type}:${id}`, { type, id }); collector.set(`${type}:${id}`, { type, id });
} }
} }
// Remove objects that could be references
for (const savedObject of savedObjects) { for (const savedObject of savedObjects) {
collector.delete(`${savedObject.type}:${savedObject.id}`); collector.delete(`${savedObject.type}:${savedObject.id}`);
} }
if (collector.size) { if (collector.size === 0) {
const bulkGetOpts = Array.from(collector.values()).map(obj => ({ ...obj, fields: ['id'] })); return [];
const bulkGetResponse = await savedObjectsClient.bulkGet(bulkGetOpts);
for (const savedObject of bulkGetResponse.saved_objects) {
if (savedObject.error) {
continue;
}
collector.delete(`${savedObject.type}:${savedObject.id}`);
}
} }
// Fetch references to see if they exist
const bulkGetOpts = Array.from(collector.values()).map(obj => ({ ...obj, fields: ['id'] }));
const bulkGetResponse = await savedObjectsClient.bulkGet(bulkGetOpts);
// Error handling
const erroredObjects = bulkGetResponse.saved_objects.filter(
obj => obj.error && obj.error.statusCode !== 404
);
if (erroredObjects.length) {
const err = Boom.badRequest();
err.output.payload.attributes = {
objects: erroredObjects,
};
throw err;
}
// Cleanup collector
for (const savedObject of bulkGetResponse.saved_objects) {
if (savedObject.error) {
continue;
}
collector.delete(`${savedObject.type}:${savedObject.id}`);
}
return [...collector.keys()]; return [...collector.keys()];
} }
@ -55,39 +79,59 @@ export async function validateReferences(
savedObjects: SavedObject[], savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClient savedObjectsClient: SavedObjectsClient
) { ) {
const errors: ImportError[] = []; const errorMap: { [key: string]: ImportError } = {};
const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys( const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys(
savedObjects, savedObjects,
savedObjectsClient savedObjectsClient
); );
// Filter out objects with missing references, add to error object // Filter out objects with missing references, add to error object
const filteredObjects = savedObjects.filter(savedObject => { let filteredObjects = savedObjects.filter(savedObject => {
const missingReferences = []; const missingReferences = [];
for (const { type: refType, id: refId } of savedObject.references || []) { const enforcedTypeReferences = (savedObject.references || []).filter(
if (!ENFORCED_TYPES.includes(refType)) { filterReferencesToValidate
continue; );
} for (const { type: refType, id: refId } of enforcedTypeReferences) {
if (nonExistingReferenceKeys.includes(`${refType}:${refId}`)) { if (nonExistingReferenceKeys.includes(`${refType}:${refId}`)) {
missingReferences.push({ type: refType, id: refId }); missingReferences.push({ type: refType, id: refId });
} }
} }
if (missingReferences.length) { if (missingReferences.length === 0) {
errors.push({ return true;
id: savedObject.id,
type: savedObject.type,
error: {
type: 'missing_references',
references: missingReferences,
},
});
} }
return missingReferences.length === 0; errorMap[`${savedObject.type}:${savedObject.id}`] = {
id: savedObject.id,
type: savedObject.type,
title: savedObject.attributes && savedObject.attributes.title,
error: {
type: 'missing_references',
references: missingReferences,
blocking: [],
},
};
return false;
});
// Filter out objects that reference objects within the import but are missing_references
// For example: visualization referencing a search that is missing an index pattern needs to be filtered out
filteredObjects = filteredObjects.filter(savedObject => {
let isBlocked = false;
for (const reference of savedObject.references || []) {
const referencedObjectError = errorMap[`${reference.type}:${reference.id}`];
if (!referencedObjectError || referencedObjectError.error.type !== 'missing_references') {
continue;
}
referencedObjectError.error.blocking.push({
type: savedObject.type,
id: savedObject.id,
});
isBlocked = true;
}
return !isBlocked;
}); });
return { return {
errors, errors: Object.values(errorMap),
filteredObjects, filteredObjects,
}; };
} }

View file

@ -36,13 +36,7 @@ describe('POST /api/saved_objects/_import', () => {
beforeEach(() => { beforeEach(() => {
server = createMockServer(); server = createMockServer();
savedObjectsClient.bulkCreate.mockReset(); jest.resetAllMocks();
savedObjectsClient.bulkGet.mockReset();
savedObjectsClient.create.mockReset();
savedObjectsClient.delete.mockReset();
savedObjectsClient.find.mockReset();
savedObjectsClient.get.mockReset();
savedObjectsClient.update.mockReset();
const prereqs = { const prereqs = {
getSavedObjectsClient: { getSavedObjectsClient: {
@ -180,6 +174,7 @@ describe('POST /api/saved_objects/_import', () => {
{ {
id: 'my-pattern', id: 'my-pattern',
type: 'index-pattern', type: 'index-pattern',
title: 'my-pattern-*',
error: { error: {
type: 'conflict', type: 'conflict',
}, },
@ -187,4 +182,88 @@ describe('POST /api/saved_objects/_import', () => {
], ],
}); });
}); });
test('imports a visualization with missing references', async () => {
// NOTE: changes to this scenario should be reflected in the docs
const request = {
method: 'POST',
url: '/api/saved_objects/_import',
payload: [
'--EXAMPLE',
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson',
'',
'{"type":"visualization","id":"my-vis","attributes":{"title":"my-vis"},"references":[{"name":"ref_0","type":"index-pattern","id":"my-pattern-*"}]}',
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"ref_0","type":"visualization","id":"my-vis"}]}',
'--EXAMPLE--',
].join('\r\n'),
headers: {
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
},
};
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: 'my-pattern-*',
type: 'index-pattern',
error: {
statusCode: 404,
message: 'Not found',
},
},
],
});
const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toEqual({
success: false,
successCount: 0,
errors: [
{
id: 'my-vis',
type: 'visualization',
title: 'my-vis',
error: {
type: 'missing_references',
references: [
{
type: 'index-pattern',
id: 'my-pattern-*',
},
],
blocking: [
{
type: 'dashboard',
id: 'my-dashboard',
},
],
},
},
],
});
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "my-pattern-*",
"type": "index-pattern",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
}); });

View file

@ -36,13 +36,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
beforeEach(() => { beforeEach(() => {
server = createMockServer(); server = createMockServer();
savedObjectsClient.bulkCreate.mockReset(); jest.resetAllMocks();
savedObjectsClient.bulkGet.mockReset();
savedObjectsClient.create.mockReset();
savedObjectsClient.delete.mockReset();
savedObjectsClient.find.mockReset();
savedObjectsClient.get.mockReset();
savedObjectsClient.update.mockReset();
const prereqs = { const prereqs = {
getSavedObjectsClient: { getSavedObjectsClient: {
@ -66,6 +60,10 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
'Content-Type: application/ndjson', 'Content-Type: application/ndjson',
'', '',
'', '',
'--BOUNDARY',
'Content-Disposition: form-data; name="retries"',
'',
'[]',
'--BOUNDARY--', '--BOUNDARY--',
].join('\r\n'), ].join('\r\n'),
headers: { headers: {
@ -79,7 +77,68 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
}); });
test('resolves conflicts for an index pattern and dashboard but skips the index pattern', async () => { test('retries importin a dashboard', async () => {
// NOTE: changes to this scenario should be reflected in the docs
const request = {
method: 'POST',
url: '/api/saved_objects/_resolve_import_errors',
payload: [
'--EXAMPLE',
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson',
'',
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
'--EXAMPLE',
'Content-Disposition: form-data; name="retries"',
'',
'[{"type":"dashboard","id":"my-dashboard"}]',
'--EXAMPLE--',
].join('\r\n'),
headers: {
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
},
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
type: 'dashboard',
id: 'my-dashboard',
attributes: {
title: 'Look at my dashboard',
},
},
],
});
const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toEqual({ success: true, successCount: 1 });
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"attributes": Object {
"title": "Look at my dashboard",
},
"id": "my-dashboard",
"type": "dashboard",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('resolves conflicts for dashboard', async () => {
// NOTE: changes to this scenario should be reflected in the docs // NOTE: changes to this scenario should be reflected in the docs
const request = { const request = {
method: 'POST', method: 'POST',
@ -92,13 +151,9 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
'{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}', '{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}', '{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
'--EXAMPLE', '--EXAMPLE',
'Content-Disposition: form-data; name="skips"', 'Content-Disposition: form-data; name="retries"',
'', '',
'[{"type":"index-pattern","id":"my-pattern"}]', '[{"type":"dashboard","id":"my-dashboard","overwrite":true}]',
'--EXAMPLE',
'Content-Disposition: form-data; name="overwrites"',
'',
'[{"type":"dashboard","id":"my-dashboard"}]',
'--EXAMPLE--', '--EXAMPLE--',
].join('\r\n'), ].join('\r\n'),
headers: { headers: {
@ -158,12 +213,11 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
'Content-Disposition: form-data; name="file"; filename="export.ndjson"', 'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson', 'Content-Type: application/ndjson',
'', '',
'{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"}}', '{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"},"references":[{"name":"ref_0","type":"index-pattern","id":"missing"}]}',
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"panel_0","type":"visualization","id":"my-vis"}]}',
'--EXAMPLE', '--EXAMPLE',
'Content-Disposition: form-data; name="replaceReferences"', 'Content-Disposition: form-data; name="retries"',
'', '',
'[{"type":"visualization","from":"my-vis","to":"my-vis-2"}]', '[{"type":"visualization","id":"my-vis","replaceReferences":[{"type":"index-pattern","from":"missing","to":"existing"}]}]',
'--EXAMPLE--', '--EXAMPLE--',
].join('\r\n'), ].join('\r\n'),
headers: { headers: {
@ -173,21 +227,31 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
savedObjectsClient.bulkCreate.mockResolvedValueOnce({ savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [ saved_objects: [
{ {
type: 'dashboard', type: 'visualization',
id: 'my-dashboard', id: 'my-vis',
attributes: { attributes: {
title: 'Look at my dashboard', title: 'Look at my visualization',
}, },
references: [ references: [
{ {
name: 'panel_0', name: 'ref_0',
type: 'visualization', type: 'index-pattern',
id: 'my-vis-2', id: 'existing',
}, },
], ],
}, },
], ],
}); });
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: 'existing',
type: 'index-pattern',
attributes: {},
references: [],
},
],
});
const { payload, statusCode } = await server.inject(request); const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload); const response = JSON.parse(payload);
expect(statusCode).toBe(200); expect(statusCode).toBe(200);
@ -199,22 +263,42 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
Array [ Array [
Object { Object {
"attributes": Object { "attributes": Object {
"title": "Look at my dashboard", "title": "Look at my visualization",
}, },
"id": "my-dashboard", "id": "my-vis",
"references": Array [ "references": Array [
Object { Object {
"id": "my-vis-2", "id": "existing",
"name": "panel_0", "name": "ref_0",
"type": "visualization", "type": "index-pattern",
}, },
], ],
"type": "dashboard", "type": "visualization",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "existing",
"type": "index-pattern",
}, },
], ],
Object {
"overwrite": true,
},
], ],
], ],
"results": Array [ "results": Array [

View file

@ -38,18 +38,15 @@ interface ImportRequest extends Hapi.Request {
}; };
payload: { payload: {
file: HapiReadableStream; file: HapiReadableStream;
overwrites: Array<{ retries: Array<{
type: string;
id: string;
}>;
replaceReferences: Array<{
type: string;
from: string;
to: string;
}>;
skips: Array<{
type: string; type: string;
id: string; id: string;
overwrite: boolean;
replaceReferences: Array<{
type: string;
from: string;
to: string;
}>;
}>; }>;
}; };
} }
@ -67,31 +64,24 @@ export const createResolveImportErrorsRoute = (prereqs: Prerequisites, server: H
validate: { validate: {
payload: Joi.object({ payload: Joi.object({
file: Joi.object().required(), file: Joi.object().required(),
overwrites: Joi.array() retries: Joi.array()
.items( .items(
Joi.object({ Joi.object({
type: Joi.string().required(), type: Joi.string().required(),
id: Joi.string().required(), id: Joi.string().required(),
overwrite: Joi.boolean().default(false),
replaceReferences: Joi.array()
.items(
Joi.object({
type: Joi.string().required(),
from: Joi.string().required(),
to: Joi.string().required(),
})
)
.default([]),
}) })
) )
.default([]), .required(),
replaceReferences: Joi.array()
.items(
Joi.object({
type: Joi.string().required(),
from: Joi.string().required(),
to: Joi.string().required(),
})
)
.default([]),
skips: Joi.array()
.items(
Joi.object({
type: Joi.string().required(),
id: Joi.string().required(),
})
)
.default([]),
}).default(), }).default(),
}, },
}, },
@ -99,16 +89,16 @@ export const createResolveImportErrorsRoute = (prereqs: Prerequisites, server: H
const { savedObjectsClient } = request.pre; const { savedObjectsClient } = request.pre;
const { filename } = request.payload.file.hapi; const { filename } = request.payload.file.hapi;
const fileExtension = extname(filename).toLowerCase(); const fileExtension = extname(filename).toLowerCase();
if (fileExtension !== '.ndjson') { if (fileExtension !== '.ndjson') {
return Boom.badRequest(`Invalid file extension ${fileExtension}`); return Boom.badRequest(`Invalid file extension ${fileExtension}`);
} }
return await resolveImportErrors({ return await resolveImportErrors({
savedObjectsClient, savedObjectsClient,
readStream: request.payload.file, readStream: request.payload.file,
retries: request.payload.retries,
objectLimit: request.server.config().get('savedObjects.maxImportExportSize'), objectLimit: request.server.config().get('savedObjects.maxImportExportSize'),
skips: request.payload.skips,
overwrites: request.payload.overwrites,
replaceReferences: request.payload.replaceReferences,
}); });
}, },
}); });

View file

@ -70,6 +70,7 @@ export default function ({ getService }) {
{ {
id: '91200a00-9efd-11e7-acb3-3dab96693fab', id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern', type: 'index-pattern',
title: 'logstash-*',
error: { error: {
type: 'conflict', type: 'conflict',
} }
@ -77,6 +78,7 @@ export default function ({ getService }) {
{ {
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization', type: 'visualization',
title: 'Count of requests',
error: { error: {
type: 'conflict', type: 'conflict',
} }
@ -84,6 +86,7 @@ export default function ({ getService }) {
{ {
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard', type: 'dashboard',
title: 'Requests',
error: { error: {
type: 'conflict', type: 'conflict',
} }
@ -161,6 +164,7 @@ export default function ({ getService }) {
id: '1', id: '1',
error: { error: {
type: 'missing_references', type: 'missing_references',
blocking: [],
references: [ references: [
{ {
type: 'index-pattern', type: 'index-pattern',

View file

@ -32,6 +32,7 @@ export default function ({ getService }) {
it('should return 200 and import nothing when empty parameters are passed in', async () => { it('should return 200 and import nothing when empty parameters are passed in', async () => {
await supertest await supertest
.post('/api/saved_objects/_resolve_import_errors') .post('/api/saved_objects/_resolve_import_errors')
.field('retries', '[]')
.attach('file', join(__dirname, '../../fixtures/import.ndjson')) .attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200) .expect(200)
.then((resp) => { .then((resp) => {
@ -45,18 +46,21 @@ export default function ({ getService }) {
it('should return 200 and import everything when overwrite parameters contains all objects', async () => { it('should return 200 and import everything when overwrite parameters contains all objects', async () => {
await supertest await supertest
.post('/api/saved_objects/_resolve_import_errors') .post('/api/saved_objects/_resolve_import_errors')
.field('overwrites', JSON.stringify([ .field('retries', JSON.stringify([
{ {
type: 'index-pattern', type: 'index-pattern',
id: '91200a00-9efd-11e7-acb3-3dab96693fab', id: '91200a00-9efd-11e7-acb3-3dab96693fab',
overwrite: true,
}, },
{ {
type: 'visualization', type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
overwrite: true,
}, },
{ {
type: 'dashboard', type: 'dashboard',
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
overwrite: true,
}, },
])) ]))
.attach('file', join(__dirname, '../../fixtures/import.ndjson')) .attach('file', join(__dirname, '../../fixtures/import.ndjson'))
@ -72,7 +76,7 @@ export default function ({ getService }) {
it('should return 400 when no file passed in', async () => { it('should return 400 when no file passed in', async () => {
await supertest await supertest
.post('/api/saved_objects/_resolve_import_errors') .post('/api/saved_objects/_resolve_import_errors')
.field('skips', '[]') .field('retries', '[]')
.expect(400) .expect(400)
.then((resp) => { .then((resp) => {
expect(resp.body).to.eql({ expect(resp.body).to.eql({
@ -84,7 +88,26 @@ export default function ({ getService }) {
}); });
}); });
it('should return 200 when replacing references', async () => { it('should return 400 when resolving conflicts with a file containing more than 10,000 objects', async () => {
const fileChunks = [];
for (let i = 0; i < 10001; i++) {
fileChunks.push(`{"type":"visualization","id":"${i}","attributes":{},"references":[]}`);
}
await supertest
.post('/api/saved_objects/_resolve_import_errors')
.field('retries', '[]')
.attach('file', Buffer.from(fileChunks.join('\n'), 'utf8'), 'export.ndjson')
.expect(400)
.then((resp) => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: 'Can\'t import more than 10000 objects',
});
});
});
it('should return 200 with errors when missing references', async () => {
const objToInsert = { const objToInsert = {
id: '1', id: '1',
type: 'visualization', type: 'visualization',
@ -94,58 +117,44 @@ export default function ({ getService }) {
references: [ references: [
{ {
name: 'ref_0', name: 'ref_0',
type: 'search', type: 'index-pattern',
id: '1', id: '2',
}, },
] ],
}; };
await supertest await supertest
.post('/api/saved_objects/_resolve_import_errors') .post('/api/saved_objects/_resolve_import_errors')
.field('replaceReferences', JSON.stringify( .field('retries', JSON.stringify(
[ [
{ {
type: 'search', type: 'visualization',
from: '1', id: '1',
to: '2', },
}
] ]
)) ))
.attach('file', Buffer.from(JSON.stringify(objToInsert), 'utf8'), 'export.ndjson') .attach('file', Buffer.from(JSON.stringify(objToInsert), 'utf8'), 'export.ndjson')
.expect(200) .expect(200)
.then((resp) => { .then((resp) => {
expect(resp.body).to.eql({ expect(resp.body).to.eql({
success: true, success: false,
successCount: 1, successCount: 0,
}); errors: [
}); {
await supertest id: '1',
.get('/api/saved_objects/visualization/1') type: 'visualization',
.expect(200) title: 'My favorite vis',
.then((resp) => { error: {
expect(resp.body.references).to.eql([ type: 'missing_references',
{ blocking: [],
name: 'ref_0', references: [
type: 'search', {
id: '2', type: 'index-pattern',
}, id: '2',
]); },
}); ],
}); },
},
it('should return 400 when resolving conflicts with a file containing more than 10,000 objects', async () => { ],
const fileChunks = [];
for (let i = 0; i < 10001; i++) {
fileChunks.push(`{"type":"visualization","id":"${i}","attributes":{},"references":[]}`);
}
await supertest
.post('/api/saved_objects/_resolve_import_errors')
.attach('file', Buffer.from(fileChunks.join('\n'), 'utf8'), 'export.ndjson')
.expect(400)
.then((resp) => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: 'Can\'t import more than 10000 objects',
}); });
}); });
}); });
@ -159,22 +168,7 @@ export default function ({ getService }) {
it('should return 200 when skipping all the records', async () => { it('should return 200 when skipping all the records', async () => {
await supertest await supertest
.post('/api/saved_objects/_resolve_import_errors') .post('/api/saved_objects/_resolve_import_errors')
.field('skips', JSON.stringify( .field('retries', '[]')
[
{
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern',
},
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
},
{
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard',
},
]
))
.attach('file', join(__dirname, '../../fixtures/import.ndjson')) .attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200) .expect(200)
.then((resp) => { .then((resp) => {
@ -185,19 +179,22 @@ export default function ({ getService }) {
it('should return 200 when manually overwriting each object', async () => { it('should return 200 when manually overwriting each object', async () => {
await supertest await supertest
.post('/api/saved_objects/_resolve_import_errors') .post('/api/saved_objects/_resolve_import_errors')
.field('overwrites', JSON.stringify( .field('retries', JSON.stringify(
[ [
{ {
id: '91200a00-9efd-11e7-acb3-3dab96693fab', id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern', type: 'index-pattern',
overwrite: true,
}, },
{ {
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization', type: 'visualization',
overwrite: true,
}, },
{ {
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard', type: 'dashboard',
overwrite: true,
}, },
] ]
)) ))
@ -211,19 +208,12 @@ export default function ({ getService }) {
it('should return 200 with only one record when overwriting 1 and skipping 1', async () => { it('should return 200 with only one record when overwriting 1 and skipping 1', async () => {
await supertest await supertest
.post('/api/saved_objects/_resolve_import_errors') .post('/api/saved_objects/_resolve_import_errors')
.field('overwrites', JSON.stringify( .field('retries', JSON.stringify(
[ [
{ {
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization', type: 'visualization',
}, overwrite: true,
]
))
.field('skips', JSON.stringify(
[
{
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern',
}, },
] ]
)) ))
@ -233,6 +223,60 @@ export default function ({ getService }) {
expect(resp.body).to.eql({ success: true, successCount: 1 }); expect(resp.body).to.eql({ success: true, successCount: 1 });
}); });
}); });
it('should return 200 when replacing references', async () => {
const objToInsert = {
id: '1',
type: 'visualization',
attributes: {
title: 'My favorite vis',
},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '2',
},
]
};
await supertest
.post('/api/saved_objects/_resolve_import_errors')
.field('retries', JSON.stringify(
[
{
type: 'visualization',
id: '1',
replaceReferences: [
{
type: 'index-pattern',
from: '2',
to: '91200a00-9efd-11e7-acb3-3dab96693fab',
},
],
},
]
))
.attach('file', Buffer.from(JSON.stringify(objToInsert), 'utf8'), 'export.ndjson')
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 1,
});
});
await supertest
.get('/api/saved_objects/visualization/1')
.expect(200)
.then((resp) => {
expect(resp.body.references).to.eql([
{
name: 'ref_0',
type: 'index-pattern',
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
},
]);
});
});
}); });
}); });
}); });

View file

@ -61,6 +61,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
{ {
id: '1', id: '1',
type: 'wigwags', type: 'wigwags',
title: 'Wigwags title',
error: { error: {
message: `Unsupported saved object type: 'wigwags': Bad Request`, message: `Unsupported saved object type: 'wigwags': Bad Request`,
statusCode: 400, statusCode: 400,

View file

@ -65,6 +65,7 @@ export function resolveImportErrorsTestSuiteFactory(
{ {
id: '1', id: '1',
type: 'wigwags', type: 'wigwags',
title: 'Wigwags title',
error: { error: {
message: `Unsupported saved object type: 'wigwags': Bad Request`, message: `Unsupported saved object type: 'wigwags': Bad Request`,
statusCode: 400, statusCode: 400,
@ -116,11 +117,12 @@ export function resolveImportErrorsTestSuiteFactory(
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`)
.auth(user.username, user.password) .auth(user.username, user.password)
.field( .field(
'overwrites', 'retries',
JSON.stringify([ JSON.stringify([
{ {
type: 'dashboard', type: 'dashboard',
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
overwrite: true,
}, },
]) ])
) )
@ -147,15 +149,17 @@ export function resolveImportErrorsTestSuiteFactory(
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`) .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`)
.auth(user.username, user.password) .auth(user.username, user.password)
.field( .field(
'overwrites', 'retries',
JSON.stringify([ JSON.stringify([
{ {
type: 'wigwags', type: 'wigwags',
id: '1', id: '1',
overwrite: true,
}, },
{ {
type: 'dashboard', type: 'dashboard',
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`, id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
overwrite: true,
}, },
]) ])
) )