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:
parent
d91c909e7d
commit
34c7e52a87
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
42
src/legacy/server/saved_objects/import/types.ts
Normal file
42
src/legacy/server/saved_objects/import/types.ts
Normal 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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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()
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in a new issue