Return import errors for saved objects referencing a missing index pattern or search (#33005)

* cherry-pick fd2bc9b

* Return errors when objects are missing references

* Fix import tslint

* Fix failing jest tests

* Fix x-pack integration tests

* Rename ensureReferencesExist to validateReferences

* Fix test naming to use validateReferences

* Update resolve_import_errors API to reflect new type attribute

* Validate references for search type as well

* Clarify comment

* Apply PR feedback

* Modify saved object bulkGet to be able to filter fields

* Apply PR feedback
This commit is contained in:
Mike Côté 2019-03-14 19:22:30 -04:00 committed by GitHub
parent d91c909e7d
commit 34c7e52a87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 854 additions and 43 deletions

View file

@ -23,6 +23,9 @@ contains the following properties:
`id` (required):: `id` (required)::
(string) ID of object to retrieve (string) ID of object to retrieve
`fields` (optional)::
(array) The fields to return in the object's response
==== Response body ==== Response body
The response body will have a top level `saved_objects` property that contains The response body will have a top level `saved_objects` property that contains

View file

@ -45,18 +45,36 @@ describe('extractErrors()', () => {
message: 'Conflict', message: 'Conflict',
}, },
}, },
{
id: '3',
type: 'dashboard',
attributes: {},
references: [],
error: {
statusCode: 400,
message: 'Bad Request',
},
},
]; ];
const result = extractErrors(savedObjects); const result = extractErrors(savedObjects);
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
Array [ Array [
Object { Object {
"error": Object { "error": Object {
"message": "Conflict", "type": "conflict",
"statusCode": 409,
}, },
"id": "2", "id": "2",
"type": "dashboard", "type": "dashboard",
}, },
Object {
"error": Object {
"message": "Bad Request",
"statusCode": 400,
"type": "unknown",
},
"id": "3",
"type": "dashboard",
},
] ]
`); `);
}); });

View file

@ -18,24 +18,29 @@
*/ */
import { SavedObject } from '../service'; import { SavedObject } from '../service';
import { ImportError } from './types';
export interface CustomError {
id: string;
type: string;
error: {
message: string;
statusCode: number;
};
}
export function extractErrors(savedObjects: SavedObject[]) { export function extractErrors(savedObjects: SavedObject[]) {
const errors: CustomError[] = []; const errors: ImportError[] = [];
for (const savedObject of savedObjects) { for (const savedObject of savedObjects) {
if (savedObject.error) { if (savedObject.error) {
if (savedObject.error.statusCode === 409) {
errors.push({
id: savedObject.id,
type: savedObject.type,
error: {
type: 'conflict',
},
});
continue;
}
errors.push({ errors.push({
id: savedObject.id, id: savedObject.id,
type: savedObject.type, type: savedObject.type,
error: savedObject.error, error: {
...savedObject.error,
type: 'unknown',
},
}); });
} }
} }

View file

@ -76,6 +76,7 @@ describe('importSavedObjects()', () => {
this.push(null); this.push(null);
}, },
}); });
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValue({ savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects, saved_objects: savedObjects,
}); });
@ -143,6 +144,7 @@ Object {
this.push(null); this.push(null);
}, },
}); });
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValue({ savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects, saved_objects: savedObjects,
}); });
@ -210,6 +212,7 @@ Object {
this.push(null); this.push(null);
}, },
}); });
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValue({ savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects.map(savedObject => ({ saved_objects: savedObjects.map(savedObject => ({
type: savedObject.type, type: savedObject.type,
@ -231,32 +234,28 @@ Object {
"errors": Array [ "errors": Array [
Object { Object {
"error": Object { "error": Object {
"message": "conflict", "type": "conflict",
"statusCode": 409,
}, },
"id": "1", "id": "1",
"type": "index-pattern", "type": "index-pattern",
}, },
Object { Object {
"error": Object { "error": Object {
"message": "conflict", "type": "conflict",
"statusCode": 409,
}, },
"id": "2", "id": "2",
"type": "search", "type": "search",
}, },
Object { Object {
"error": Object { "error": Object {
"message": "conflict", "type": "conflict",
"statusCode": 409,
}, },
"id": "3", "id": "3",
"type": "visualization", "type": "visualization",
}, },
Object { Object {
"error": Object { "error": Object {
"message": "conflict", "type": "conflict",
"statusCode": 409,
}, },
"id": "4", "id": "4",
"type": "dashboard", "type": "dashboard",

View file

@ -20,7 +20,9 @@
import { Readable } from 'stream'; import { Readable } from 'stream';
import { SavedObjectsClient } from '../service'; import { SavedObjectsClient } from '../service';
import { collectSavedObjects } from './collect_saved_objects'; import { collectSavedObjects } from './collect_saved_objects';
import { CustomError, extractErrors } from './extract_errors'; import { extractErrors } from './extract_errors';
import { ImportError } from './types';
import { validateReferences } from './validate_references';
interface ImportSavedObjectsOptions { interface ImportSavedObjectsOptions {
readStream: Readable; readStream: Readable;
@ -32,7 +34,7 @@ interface ImportSavedObjectsOptions {
interface ImportResponse { interface ImportResponse {
success: boolean; success: boolean;
successCount: number; successCount: number;
errors?: CustomError[]; errors?: ImportError[];
} }
export async function importSavedObjects({ export async function importSavedObjects({
@ -41,23 +43,29 @@ export async function importSavedObjects({
overwrite, overwrite,
savedObjectsClient, savedObjectsClient,
}: ImportSavedObjectsOptions): Promise<ImportResponse> { }: ImportSavedObjectsOptions): Promise<ImportResponse> {
const objectsToImport = await collectSavedObjects(readStream, objectLimit); const objectsFromStream = await collectSavedObjects(readStream, objectLimit);
if (objectsToImport.length === 0) { const { filteredObjects, errors: validationErrors } = await validateReferences(
objectsFromStream,
savedObjectsClient
);
if (filteredObjects.length === 0) {
return { return {
success: true, success: validationErrors.length === 0,
successCount: 0, successCount: 0,
...(validationErrors.length ? { errors: validationErrors } : {}),
}; };
} }
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToImport, { const bulkCreateResult = await savedObjectsClient.bulkCreate(filteredObjects, {
overwrite, overwrite,
}); });
const errors = extractErrors(bulkCreateResult.saved_objects); const errors = [...validationErrors, ...extractErrors(bulkCreateResult.saved_objects)];
return { return {
success: errors.length === 0, success: errors.length === 0,
successCount: objectsToImport.length - errors.length, successCount: objectsFromStream.length - errors.length,
...(errors.length ? { errors } : {}), ...(errors.length ? { errors } : {}),
}; };
} }

View file

@ -21,7 +21,8 @@ import { Readable } from 'stream';
import { SavedObjectsClient } from '../service'; 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 { CustomError, extractErrors } from './extract_errors'; import { extractErrors } from './extract_errors';
import { ImportError } from './types';
interface ResolveImportErrorsOptions { interface ResolveImportErrorsOptions {
readStream: Readable; readStream: Readable;
@ -45,7 +46,7 @@ interface ResolveImportErrorsOptions {
interface ImportResponse { interface ImportResponse {
success: boolean; success: boolean;
successCount: number; successCount: number;
errors?: CustomError[]; errors?: ImportError[];
} }
export async function resolveImportErrors({ export async function resolveImportErrors({
@ -56,7 +57,7 @@ export async function resolveImportErrors({
savedObjectsClient, savedObjectsClient,
replaceReferences, replaceReferences,
}: ResolveImportErrorsOptions): Promise<ImportResponse> { }: ResolveImportErrorsOptions): Promise<ImportResponse> {
let errors: CustomError[] = []; let errors: ImportError[] = [];
const filter = createObjectsFilter(skips, overwrites, replaceReferences); const filter = createObjectsFilter(skips, overwrites, replaceReferences);
const objectsToResolve = await collectSavedObjects(readStream, objectLimit, filter); const objectsToResolve = await collectSavedObjects(readStream, objectLimit, filter);

View file

@ -0,0 +1,42 @@
/*
* 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.
*/
interface ConflictError {
type: 'conflict';
}
interface UnknownError {
type: 'unknown';
message: string;
statusCode: number;
}
interface MissingReferencesError {
type: 'missing_references';
references: Array<{
type: string;
id: string;
}>;
}
export interface ImportError {
id: string;
type: string;
error: ConflictError | MissingReferencesError | UnknownError;
}

View file

@ -0,0 +1,586 @@
/*
* 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 { getNonExistingReferenceAsKeys, validateReferences } from './validate_references';
describe('getNonExistingReferenceAsKeys()', () => {
const savedObjectsClient = {
errors: {} as any,
bulkCreate: jest.fn(),
bulkGet: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
beforeEach(() => {
jest.resetAllMocks();
});
test('returns empty response when no objects exist', async () => {
const result = await getNonExistingReferenceAsKeys([], savedObjectsClient);
expect(result).toEqual([]);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test('removes references that exist within savedObjects', async () => {
const savedObjects = [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
],
},
];
const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
expect(result).toEqual([]);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test('removes references that exist within es', async () => {
const savedObjects = [
{
id: '2',
type: 'visualization',
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 getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
expect(result).toEqual([]);
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "1",
"type": "index-pattern",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test(`doesn't handle saved object types outside of ENFORCED_TYPES`, async () => {
const savedObjects = [
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'foo',
id: '1',
},
],
},
];
const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
expect(result).toEqual([]);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test('returns references within ENFORCED_TYPES when they are missing', async () => {
const savedObjects = [
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
{
name: 'ref_1',
type: 'search',
id: '3',
},
{
name: 'ref_2',
type: 'foo',
id: '4',
},
],
},
];
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'index-pattern',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
id: '3',
type: 'search',
error: {
statusCode: 404,
message: 'Not found',
},
},
],
});
const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
expect(result).toEqual(['index-pattern:1', 'search:3']);
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "1",
"type": "index-pattern",
},
Object {
"fields": Array [
"id",
],
"id": "3",
"type": "search",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
});
describe('validateReferences()', () => {
const savedObjectsClient = {
errors: {} as any,
bulkCreate: jest.fn(),
bulkGet: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
beforeEach(() => {
jest.resetAllMocks();
});
test('returns empty when no objects are passed in', async () => {
const result = await validateReferences([], savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [],
"filteredObjects": Array [],
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test('returns errors when references are missing', async () => {
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
{
type: 'index-pattern',
id: '3',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
type: 'index-pattern',
id: '5',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
type: 'index-pattern',
id: '6',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
type: 'search',
id: '7',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
id: '8',
type: 'search',
attributes: {},
references: [],
},
],
});
const savedObjects = [
{
id: '1',
type: 'visualization',
attributes: {},
references: [],
},
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '3',
},
],
},
{
id: '4',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '5',
},
{
name: 'ref_1',
type: 'index-pattern',
id: '6',
},
{
name: 'ref_2',
type: 'search',
id: '7',
},
{
name: 'ref_3',
type: 'search',
id: '8',
},
],
},
];
const result = await validateReferences(savedObjects, savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [
Object {
"error": Object {
"references": Array [
Object {
"id": "3",
"type": "index-pattern",
},
],
"type": "missing_references",
},
"id": "2",
"type": "visualization",
},
Object {
"error": Object {
"references": Array [
Object {
"id": "5",
"type": "index-pattern",
},
Object {
"id": "6",
"type": "index-pattern",
},
Object {
"id": "7",
"type": "search",
},
],
"type": "missing_references",
},
"id": "4",
"type": "visualization",
},
],
"filteredObjects": Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "visualization",
},
],
}
`);
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "3",
"type": "index-pattern",
},
Object {
"fields": Array [
"id",
],
"id": "5",
"type": "index-pattern",
},
Object {
"fields": Array [
"id",
],
"id": "6",
"type": "index-pattern",
},
Object {
"fields": Array [
"id",
],
"id": "7",
"type": "search",
},
Object {
"fields": Array [
"id",
],
"id": "8",
"type": "search",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test(`doesn't return errors when references exist in Elasticsearch`, async () => {
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
],
});
const savedObjects = [
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
],
},
];
const result = await validateReferences(savedObjects, savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [],
"filteredObjects": Array [
Object {
"attributes": Object {},
"id": "2",
"references": Array [
Object {
"id": "1",
"name": "ref_0",
"type": "index-pattern",
},
],
"type": "visualization",
},
],
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
});
test(`doesn't return errors when references exist within the saved objects`, async () => {
const savedObjects = [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
],
},
];
const result = await validateReferences(savedObjects, savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [],
"filteredObjects": 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": "visualization",
},
],
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test(`doesn't validate references on types not part of ENFORCED_TYPES`, async () => {
const savedObjects = [
{
id: '1',
type: 'dashboard',
attributes: {},
references: [
{
name: 'ref_0',
type: 'visualization',
id: '2',
},
{
name: 'ref_1',
type: 'other-type',
id: '3',
},
],
},
];
const result = await validateReferences(savedObjects, savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [],
"filteredObjects": Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [
Object {
"id": "2",
"name": "ref_0",
"type": "visualization",
},
Object {
"id": "3",
"name": "ref_1",
"type": "other-type",
},
],
"type": "dashboard",
},
],
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
});

View file

@ -0,0 +1,93 @@
/*
* 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, SavedObjectsClient } from '../service';
import { ImportError } from './types';
const ENFORCED_TYPES = ['index-pattern', 'search'];
export async function getNonExistingReferenceAsKeys(
savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClient
) {
const collector = new Map();
for (const savedObject of savedObjects) {
for (const { type, id } of savedObject.references || []) {
if (!ENFORCED_TYPES.includes(type)) {
continue;
}
collector.set(`${type}:${id}`, { type, id });
}
}
for (const savedObject of savedObjects) {
collector.delete(`${savedObject.type}:${savedObject.id}`);
}
if (collector.size) {
const bulkGetOpts = Array.from(collector.values()).map(obj => ({ ...obj, fields: ['id'] }));
const bulkGetResponse = await savedObjectsClient.bulkGet(bulkGetOpts);
for (const savedObject of bulkGetResponse.saved_objects) {
if (savedObject.error) {
continue;
}
collector.delete(`${savedObject.type}:${savedObject.id}`);
}
}
return [...collector.keys()];
}
export async function validateReferences(
savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClient
) {
const errors: ImportError[] = [];
const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys(
savedObjects,
savedObjectsClient
);
// Filter out objects with missing references, add to error object
const filteredObjects = savedObjects.filter(savedObject => {
const missingReferences = [];
for (const { type: refType, id: refId } of savedObject.references || []) {
if (!ENFORCED_TYPES.includes(refType)) {
continue;
}
if (nonExistingReferenceKeys.includes(`${refType}:${refId}`)) {
missingReferences.push({ type: refType, id: refId });
}
}
if (missingReferences.length) {
errors.push({
id: savedObject.id,
type: savedObject.type,
error: {
type: 'missing_references',
references: missingReferences,
},
});
}
return missingReferences.length === 0;
});
return {
errors,
filteredObjects,
};
}

View file

@ -29,6 +29,7 @@ interface BulkGetRequest extends Hapi.Request {
payload: Array<{ payload: Array<{
type: string; type: string;
id: string; id: string;
fields?: string[];
}>; }>;
} }
@ -42,6 +43,7 @@ export const createBulkGetRoute = (prereqs: Prerequisites) => ({
Joi.object({ Joi.object({
type: Joi.string().required(), type: Joi.string().required(),
id: Joi.string().required(), id: Joi.string().required(),
fields: Joi.array().items(Joi.string()),
}).required() }).required()
), ),
}, },

View file

@ -72,6 +72,7 @@ describe('POST /api/saved_objects/_import', () => {
'content-Type': 'multipart/form-data; boundary=BOUNDARY', 'content-Type': 'multipart/form-data; boundary=BOUNDARY',
}, },
}; };
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
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);
@ -100,6 +101,7 @@ describe('POST /api/saved_objects/_import', () => {
'content-Type': 'multipart/form-data; boundary=EXAMPLE', 'content-Type': 'multipart/form-data; boundary=EXAMPLE',
}, },
}; };
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValueOnce({ savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [ saved_objects: [
{ {
@ -145,6 +147,7 @@ describe('POST /api/saved_objects/_import', () => {
'content-Type': 'multipart/form-data; boundary=EXAMPLE', 'content-Type': 'multipart/form-data; boundary=EXAMPLE',
}, },
}; };
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValueOnce({ savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [ saved_objects: [
{ {
@ -178,8 +181,7 @@ describe('POST /api/saved_objects/_import', () => {
id: 'my-pattern', id: 'my-pattern',
type: 'index-pattern', type: 'index-pattern',
error: { error: {
statusCode: 409, type: 'conflict',
message: 'version conflict, document already exists',
}, },
}, },
], ],

View file

@ -414,7 +414,7 @@ export class SavedObjectsRepository {
/** /**
* Returns an array of objects by id * Returns an array of objects by id
* *
* @param {array} objects - an array ids, or an array of objects containing id and optionally type * @param {array} objects - an array of objects containing id, type and optionally fields
* @param {object} [options={}] * @param {object} [options={}]
* @property {string} [options.namespace] * @property {string} [options.namespace]
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] }
@ -436,10 +436,11 @@ export class SavedObjectsRepository {
const response = await this._callCluster('mget', { const response = await this._callCluster('mget', {
index: this._index, index: this._index,
body: { body: {
docs: objects.reduce((acc, { type, id }) => { docs: objects.reduce((acc, { type, id, fields }) => {
if (this._isTypeAllowed(type)) { if (this._isTypeAllowed(type)) {
acc.push({ acc.push({
_id: this._serializer.generateRawId(namespace, type, id), _id: this._serializer.generateRawId(namespace, type, id),
_source: includedFields(type, fields),
}); });
} else { } else {
unsupportedTypes.push({ unsupportedTypes.push({

View file

@ -68,6 +68,7 @@ export interface UpdateOptions extends BaseOptions {
export interface BulkGetObject { export interface BulkGetObject {
id: string; id: string;
type: string; type: string;
fields?: string[];
} }
export type BulkGetObjects = BulkGetObject[]; export type BulkGetObjects = BulkGetObject[];

View file

@ -57,7 +57,7 @@ export default function ({ getService }) {
}); });
}); });
it('should return 409 when conflicts exist', async () => { it('should return errors when conflicts exist', async () => {
await supertest await supertest
.post('/api/saved_objects/_import') .post('/api/saved_objects/_import')
.attach('file', join(__dirname, '../../fixtures/import.ndjson')) .attach('file', join(__dirname, '../../fixtures/import.ndjson'))
@ -71,24 +71,21 @@ export default function ({ getService }) {
id: '91200a00-9efd-11e7-acb3-3dab96693fab', id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern', type: 'index-pattern',
error: { error: {
statusCode: 409, type: 'conflict',
message: 'version conflict, document already exists',
} }
}, },
{ {
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization', type: 'visualization',
error: { error: {
statusCode: 409, type: 'conflict',
message: 'version conflict, document already exists',
} }
}, },
{ {
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard', type: 'dashboard',
error: { error: {
statusCode: 409, type: 'conflict',
message: 'version conflict, document already exists',
} }
}, },
], ],
@ -129,6 +126,57 @@ export default function ({ getService }) {
}); });
}); });
}); });
it('should return errors when index patterns or search are missing', async () => {
const objectsToImport = [
JSON.stringify({
type: 'visualization',
id: '1',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: 'non-existing',
},
{
name: 'ref_1',
type: 'search',
id: 'non-existing-search',
},
],
}),
];
await supertest
.post('/api/saved_objects/_import')
.attach('file', Buffer.from(objectsToImport.join('\n'), 'utf8'), 'export.ndjson')
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: false,
successCount: 0,
errors: [
{
type: 'visualization',
id: '1',
error: {
type: 'missing_references',
references: [
{
type: 'index-pattern',
id: 'non-existing',
},
{
type: 'search',
id: 'non-existing-search',
},
],
},
},
],
});
});
});
}); });
}); });
}); });

View file

@ -65,6 +65,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
message: `Unsupported saved object type: 'wigwags': Bad Request`, message: `Unsupported saved object type: 'wigwags': Bad Request`,
statusCode: 400, statusCode: 400,
error: 'Bad Request', error: 'Bad Request',
type: 'unknown',
}, },
}, },
], ],

View file

@ -69,6 +69,7 @@ export function resolveImportErrorsTestSuiteFactory(
message: `Unsupported saved object type: 'wigwags': Bad Request`, message: `Unsupported saved object type: 'wigwags': Bad Request`,
statusCode: 400, statusCode: 400,
error: 'Bad Request', error: 'Bad Request',
type: 'unknown',
}, },
}, },
], ],