mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[saved objects] Adds bulkDelete API (#139680)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d29521e897
commit
92ca42f007
50 changed files with 2200 additions and 2 deletions
|
@ -22,4 +22,7 @@ export type {
|
|||
SavedObjectsBulkUpdateOptions,
|
||||
SavedObjectsBulkResolveResponse,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteResponseItem,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from './src/apis';
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SavedObjectError } from '@kbn/core-saved-objects-common';
|
||||
|
||||
/** @public */
|
||||
export interface SavedObjectsBulkDeleteOptions {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface SavedObjectsBulkDeleteResponseItem {
|
||||
id: string;
|
||||
type: string;
|
||||
success: boolean;
|
||||
error?: SavedObjectError;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface SavedObjectsBulkDeleteResponse {
|
||||
statuses: SavedObjectsBulkDeleteResponseItem[];
|
||||
}
|
|
@ -19,3 +19,8 @@ export type {
|
|||
} from './find';
|
||||
export type { ResolvedSimpleSavedObject } from './resolve';
|
||||
export type { SavedObjectsUpdateOptions } from './update';
|
||||
export type {
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteResponseItem,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from './bulk_delete';
|
||||
|
|
|
@ -19,7 +19,10 @@ import type {
|
|||
SavedObjectsFindOptions,
|
||||
SavedObjectsUpdateOptions,
|
||||
SavedObjectsDeleteOptions,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
} from './apis';
|
||||
|
||||
import type { SimpleSavedObject } from './simple_saved_object';
|
||||
|
||||
/**
|
||||
|
@ -52,6 +55,17 @@ export interface SavedObjectsClientContract {
|
|||
*/
|
||||
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
|
||||
|
||||
/**
|
||||
* Deletes multiple documents at once
|
||||
* @param objects - an array of objects containing id, type
|
||||
* @param options - optional force argument to force deletion of objects in a namespace other than the scoped client
|
||||
* @returns The bulk delete result for the saved objects for the given types and ids.
|
||||
*/
|
||||
bulkDelete(
|
||||
objects: SavedObjectTypeIdTuple[],
|
||||
options?: SavedObjectsBulkDeleteOptions
|
||||
): Promise<SavedObjectsBulkDeleteResponse>;
|
||||
|
||||
/**
|
||||
* Search for objects
|
||||
*
|
||||
|
|
|
@ -46,6 +46,8 @@ import type {
|
|||
SavedObjectsCollectMultiNamespaceReferencesResponse,
|
||||
SavedObjectsUpdateObjectsSpacesObject,
|
||||
SavedObjectsUpdateObjectsSpacesOptions,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import type {
|
||||
SavedObjectsType,
|
||||
|
@ -2044,6 +2046,517 @@ describe('SavedObjectsRepository', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#bulkDelete', () => {
|
||||
const obj1: SavedObjectsBulkDeleteObject = {
|
||||
type: 'config',
|
||||
id: '6.0.0-alpha1',
|
||||
};
|
||||
const obj2: SavedObjectsBulkDeleteObject = {
|
||||
type: 'index-pattern',
|
||||
id: 'logstash-*',
|
||||
};
|
||||
|
||||
const namespace = 'foo-namespace';
|
||||
|
||||
const createNamespaceAwareGetId = (type: string, id: string) =>
|
||||
`${registry.isSingleNamespace(type) && namespace ? `${namespace}:` : ''}${type}:${id}`;
|
||||
|
||||
const getMockEsBulkDeleteResponse = (
|
||||
objects: TypeIdTuple[],
|
||||
options?: SavedObjectsBulkDeleteOptions
|
||||
) =>
|
||||
({
|
||||
items: objects.map(({ type, id }) => ({
|
||||
// es response returns more fields than what we're interested in.
|
||||
delete: {
|
||||
_id: `${
|
||||
registry.isSingleNamespace(type) && options?.namespace ? `${options?.namespace}:` : ''
|
||||
}${type}:${id}`,
|
||||
...mockVersionProps,
|
||||
result: 'deleted',
|
||||
},
|
||||
})),
|
||||
} as estypes.BulkResponse);
|
||||
|
||||
const repositoryBulkDeleteSuccess = async (
|
||||
objects: SavedObjectsBulkDeleteObject[] = [],
|
||||
options?: SavedObjectsBulkDeleteOptions,
|
||||
internalOptions: {
|
||||
mockMGetResponseWithObject?: { initialNamespaces: string[]; type: string; id: string };
|
||||
} = {}
|
||||
) => {
|
||||
const multiNamespaceObjects = objects.filter(({ type }) => {
|
||||
return registry.isMultiNamespace(type);
|
||||
});
|
||||
|
||||
const { mockMGetResponseWithObject } = internalOptions;
|
||||
if (multiNamespaceObjects.length > 0) {
|
||||
const mockedMGetResponse = mockMGetResponseWithObject
|
||||
? getMockMgetResponse([mockMGetResponseWithObject], options?.namespace)
|
||||
: getMockMgetResponse(multiNamespaceObjects, options?.namespace);
|
||||
client.mget.mockResponseOnce(mockedMGetResponse);
|
||||
}
|
||||
const mockedEsBulkDeleteResponse = getMockEsBulkDeleteResponse(objects, options);
|
||||
|
||||
client.bulk.mockResponseOnce(mockedEsBulkDeleteResponse);
|
||||
const result = await savedObjectsRepository.bulkDelete(objects, options);
|
||||
|
||||
expect(client.mget).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 1 : 0);
|
||||
return result;
|
||||
};
|
||||
|
||||
// bulk delete calls only has one object for each source -- the action
|
||||
const expectClientCallBulkDeleteArgsAction = (
|
||||
objects: TypeIdTuple[],
|
||||
{
|
||||
method,
|
||||
_index = expect.any(String),
|
||||
getId = () => expect.any(String),
|
||||
overrides = {},
|
||||
}: {
|
||||
method: string;
|
||||
_index?: string;
|
||||
getId?: (type: string, id: string) => string;
|
||||
overrides?: Record<string, unknown>;
|
||||
}
|
||||
) => {
|
||||
const body = [];
|
||||
for (const { type, id } of objects) {
|
||||
body.push({
|
||||
[method]: {
|
||||
_index,
|
||||
_id: getId(type, id),
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
expect(client.bulk).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body }),
|
||||
expect.anything()
|
||||
);
|
||||
};
|
||||
|
||||
const createBulkDeleteFailStatus = ({
|
||||
type,
|
||||
id,
|
||||
error,
|
||||
}: {
|
||||
type: string;
|
||||
id: string;
|
||||
error?: ExpectedErrorResult['error'];
|
||||
}) => ({
|
||||
type,
|
||||
id,
|
||||
success: false,
|
||||
error: error ?? createBadRequestError(),
|
||||
});
|
||||
|
||||
const createBulkDeleteSuccessStatus = ({ type, id }: { type: string; id: string }) => ({
|
||||
type,
|
||||
id,
|
||||
success: true,
|
||||
});
|
||||
|
||||
// mocks a combination of success, error results for hidden and unknown object object types.
|
||||
const repositoryBulkDeleteError = async (
|
||||
obj: SavedObjectsBulkDeleteObject,
|
||||
isBulkError: boolean,
|
||||
expectedErrorResult: ExpectedErrorResult
|
||||
) => {
|
||||
const objects = [obj1, obj, obj2];
|
||||
const mockedBulkDeleteResponse = getMockEsBulkDeleteResponse(objects);
|
||||
if (isBulkError) {
|
||||
mockGetBulkOperationError.mockReturnValueOnce(undefined);
|
||||
mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error as Payload);
|
||||
}
|
||||
client.bulk.mockResponseOnce(mockedBulkDeleteResponse);
|
||||
|
||||
const result = await savedObjectsRepository.bulkDelete(objects);
|
||||
expect(client.bulk).toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
statuses: [
|
||||
createBulkDeleteSuccessStatus(obj1),
|
||||
createBulkDeleteFailStatus({ ...obj, error: expectedErrorResult.error }),
|
||||
createBulkDeleteSuccessStatus(obj2),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const expectClientCallArgsAction = (
|
||||
objects: TypeIdTuple[],
|
||||
{
|
||||
method,
|
||||
_index = expect.any(String),
|
||||
getId = () => expect.any(String),
|
||||
overrides = {},
|
||||
}: {
|
||||
method: string;
|
||||
_index?: string;
|
||||
getId?: (type: string, id: string) => string;
|
||||
overrides?: Record<string, unknown>;
|
||||
}
|
||||
) => {
|
||||
const body = [];
|
||||
for (const { type, id } of objects) {
|
||||
body.push({
|
||||
[method]: {
|
||||
_index,
|
||||
_id: getId(type, id),
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(client.bulk).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body }),
|
||||
expect.anything()
|
||||
);
|
||||
};
|
||||
|
||||
const bulkDeleteMultiNamespaceError = async (
|
||||
[obj1, _obj, obj2]: SavedObjectsBulkDeleteObject[],
|
||||
options: SavedObjectsBulkDeleteOptions | undefined,
|
||||
mgetResponse: estypes.MgetResponse,
|
||||
mgetOptions?: { statusCode?: number }
|
||||
) => {
|
||||
const getId = (type: string, id: string) => `${options?.namespace}:${type}:${id}`;
|
||||
// mock the response for the not found doc
|
||||
client.mget.mockResponseOnce(mgetResponse, { statusCode: mgetOptions?.statusCode });
|
||||
// get a mocked response for the valid docs
|
||||
const bulkResponse = getMockEsBulkDeleteResponse([obj1, obj2], { namespace });
|
||||
client.bulk.mockResponseOnce(bulkResponse);
|
||||
|
||||
const result = await savedObjectsRepository.bulkDelete([obj1, _obj, obj2], options);
|
||||
expect(client.bulk).toHaveBeenCalledTimes(1);
|
||||
expect(client.mget).toHaveBeenCalledTimes(1);
|
||||
|
||||
expectClientCallArgsAction([obj1, obj2], { method: 'delete', getId });
|
||||
expect(result).toEqual({
|
||||
statuses: [
|
||||
createBulkDeleteSuccessStatus(obj1),
|
||||
{ ...expectErrorNotFound(_obj), success: false },
|
||||
createBulkDeleteSuccessStatus(obj2),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockDeleteLegacyUrlAliases.mockClear();
|
||||
mockDeleteLegacyUrlAliases.mockResolvedValue();
|
||||
});
|
||||
|
||||
describe('client calls', () => {
|
||||
it(`should use the ES bulk action by default`, async () => {
|
||||
await repositoryBulkDeleteSuccess([obj1, obj2]);
|
||||
expect(client.bulk).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should use the ES mget action before bulk action for any types that are multi-namespace`, async () => {
|
||||
const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }];
|
||||
await repositoryBulkDeleteSuccess(objects);
|
||||
expect(client.bulk).toHaveBeenCalled();
|
||||
expect(client.mget).toHaveBeenCalled();
|
||||
|
||||
const docs = [
|
||||
expect.objectContaining({ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj2.id}` }),
|
||||
];
|
||||
expect(client.mget).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body: { docs } }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it(`should not use the ES bulk action when there are no valid documents to delete`, async () => {
|
||||
const objects = [obj1, obj2].map((x) => ({ ...x, type: 'unknownType' }));
|
||||
await savedObjectsRepository.bulkDelete(objects);
|
||||
expect(client.bulk).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it(`formats the ES request`, async () => {
|
||||
const getId = createNamespaceAwareGetId;
|
||||
await repositoryBulkDeleteSuccess([obj1, obj2], { namespace });
|
||||
expectClientCallBulkDeleteArgsAction([obj1, obj2], { method: 'delete', getId });
|
||||
});
|
||||
|
||||
it(`formats the ES request for any types that are multi-namespace`, async () => {
|
||||
const _obj2 = { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE };
|
||||
const getId = createNamespaceAwareGetId;
|
||||
await repositoryBulkDeleteSuccess([obj1, _obj2], { namespace });
|
||||
expectClientCallBulkDeleteArgsAction([obj1, _obj2], { method: 'delete', getId });
|
||||
});
|
||||
|
||||
it(`defaults to a refresh setting of wait_for`, async () => {
|
||||
await repositoryBulkDeleteSuccess([obj1, obj2]);
|
||||
expect(client.bulk).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ refresh: 'wait_for' }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it(`does not include the version of the existing document when not using a multi-namespace type`, async () => {
|
||||
const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }];
|
||||
await repositoryBulkDeleteSuccess(objects);
|
||||
expectClientCallBulkDeleteArgsAction(objects, { method: 'delete' });
|
||||
});
|
||||
|
||||
it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => {
|
||||
const getId = createNamespaceAwareGetId;
|
||||
await repositoryBulkDeleteSuccess([obj1, obj2], { namespace });
|
||||
expectClientCallBulkDeleteArgsAction([obj1, obj2], { method: 'delete', getId });
|
||||
});
|
||||
|
||||
it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => {
|
||||
const getId = (type: string, id: string) => `${type}:${id}`;
|
||||
await repositoryBulkDeleteSuccess([obj1, obj2]);
|
||||
expectClientCallBulkDeleteArgsAction([obj1, obj2], { method: 'delete', getId });
|
||||
});
|
||||
|
||||
it(`normalizes options.namespace from 'default' to undefined`, async () => {
|
||||
const getId = (type: string, id: string) => `${type}:${id}`;
|
||||
await repositoryBulkDeleteSuccess([obj1, obj2], { namespace: 'default' });
|
||||
expectClientCallBulkDeleteArgsAction([obj1, obj2], { method: 'delete', getId });
|
||||
});
|
||||
|
||||
it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => {
|
||||
const getId = (type: string, id: string) => `${type}:${id}`; // not expecting namespace prefix;
|
||||
const _obj1 = { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE };
|
||||
const _obj2 = { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE };
|
||||
|
||||
await repositoryBulkDeleteSuccess([_obj1, _obj2], { namespace });
|
||||
expectClientCallBulkDeleteArgsAction([_obj1, _obj2], { method: 'delete', getId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy URL aliases', () => {
|
||||
it(`doesn't delete legacy URL aliases for single-namespace object types`, async () => {
|
||||
await repositoryBulkDeleteSuccess([obj1, obj2]);
|
||||
expect(mockDeleteLegacyUrlAliases).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`deletes legacy URL aliases for multi-namespace object types (all spaces)`, async () => {
|
||||
const testObject = { ...obj1, type: MULTI_NAMESPACE_TYPE };
|
||||
const internalOptions = {
|
||||
mockMGetResponseWithObject: {
|
||||
...testObject,
|
||||
initialNamespaces: [ALL_NAMESPACES_STRING],
|
||||
},
|
||||
};
|
||||
await repositoryBulkDeleteSuccess(
|
||||
[testObject],
|
||||
{ namespace, force: true },
|
||||
internalOptions
|
||||
);
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MULTI_NAMESPACE_TYPE,
|
||||
id: testObject.id,
|
||||
namespaces: [],
|
||||
deleteBehavior: 'exclusive',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it(`deletes legacy URL aliases for multi-namespace object types (specific space)`, async () => {
|
||||
const testObject = { ...obj1, type: MULTI_NAMESPACE_TYPE };
|
||||
const internalOptions = {
|
||||
mockMGetResponseWithObject: {
|
||||
...testObject,
|
||||
initialNamespaces: [namespace],
|
||||
},
|
||||
};
|
||||
// specifically test against the current namespace
|
||||
await repositoryBulkDeleteSuccess([testObject], { namespace }, internalOptions);
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MULTI_NAMESPACE_TYPE,
|
||||
id: testObject.id,
|
||||
namespaces: [namespace],
|
||||
deleteBehavior: 'inclusive',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it(`deletes legacy URL aliases for multi-namespace object types shared to many specific spaces`, async () => {
|
||||
const testObject = { ...obj1, type: MULTI_NAMESPACE_TYPE };
|
||||
const initialTestObjectNamespaces = [namespace, 'bar-namespace'];
|
||||
const internalOptions = {
|
||||
mockMGetResponseWithObject: {
|
||||
...testObject,
|
||||
initialNamespaces: initialTestObjectNamespaces,
|
||||
},
|
||||
};
|
||||
// specifically test against named spaces ('*' is handled specifically, this assures we also take care of named spaces)
|
||||
await repositoryBulkDeleteSuccess(
|
||||
[testObject],
|
||||
{ namespace, force: true },
|
||||
internalOptions
|
||||
);
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MULTI_NAMESPACE_TYPE,
|
||||
id: testObject.id,
|
||||
namespaces: initialTestObjectNamespaces,
|
||||
deleteBehavior: 'inclusive',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it(`logs a message when deleteLegacyUrlAliases returns an error`, async () => {
|
||||
const testObject = { type: MULTI_NAMESPACE_ISOLATED_TYPE, id: obj1.id };
|
||||
|
||||
client.mget.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
getMockMgetResponse([testObject], namespace)
|
||||
)
|
||||
);
|
||||
const mockedBulkResponse = getMockEsBulkDeleteResponse([testObject], { namespace });
|
||||
client.bulk.mockResolvedValueOnce(mockedBulkResponse);
|
||||
|
||||
mockDeleteLegacyUrlAliases.mockRejectedValueOnce(new Error('Oh no!'));
|
||||
|
||||
await savedObjectsRepository.bulkDelete([testObject], { namespace });
|
||||
|
||||
expect(client.mget).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'Unable to delete aliases when deleting an object: Oh no!'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it(`throws an error when options.namespace is '*'`, async () => {
|
||||
await expect(
|
||||
savedObjectsRepository.bulkDelete([obj1], { namespace: ALL_NAMESPACES_STRING })
|
||||
).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"'));
|
||||
});
|
||||
|
||||
it(`throws an error when client bulk response is not defined`, async () => {
|
||||
client.mget.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
getMockMgetResponse([obj1], namespace)
|
||||
)
|
||||
);
|
||||
const mockedBulkResponse = undefined;
|
||||
// we have to cast here to test the assumption we always get a response.
|
||||
client.bulk.mockResponseOnce(mockedBulkResponse as unknown as estypes.BulkResponse);
|
||||
await expect(savedObjectsRepository.bulkDelete([obj1], { namespace })).rejects.toThrowError(
|
||||
'Unexpected error in bulkDelete saved objects: bulkDeleteResponse is undefined'
|
||||
);
|
||||
});
|
||||
|
||||
it(`returns an error for the object when the object's type is invalid`, async () => {
|
||||
const unknownObjType = { ...obj1, type: 'unknownType' };
|
||||
await repositoryBulkDeleteError(
|
||||
unknownObjType,
|
||||
false,
|
||||
expectErrorInvalidType(unknownObjType)
|
||||
);
|
||||
});
|
||||
|
||||
it(`returns an error for an object when the object's type is hidden`, async () => {
|
||||
const hiddenObject = { ...obj1, type: HIDDEN_TYPE };
|
||||
await repositoryBulkDeleteError(hiddenObject, false, expectErrorInvalidType(hiddenObject));
|
||||
});
|
||||
|
||||
it(`returns an error when ES is unable to find the document during mget`, async () => {
|
||||
const notFoundObj = { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, found: false };
|
||||
const mgetResponse = getMockMgetResponse([notFoundObj], namespace);
|
||||
await bulkDeleteMultiNamespaceError([obj1, notFoundObj, obj2], { namespace }, mgetResponse);
|
||||
});
|
||||
|
||||
it(`returns an error when ES is unable to find the index during mget`, async () => {
|
||||
const notFoundObj = { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, found: false };
|
||||
await bulkDeleteMultiNamespaceError(
|
||||
[obj1, notFoundObj, obj2],
|
||||
{ namespace },
|
||||
{} as estypes.MgetResponse,
|
||||
{
|
||||
statusCode: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it(`returns an error when the type is multi-namespace and the document exists, but not in this namespace`, async () => {
|
||||
const obj = {
|
||||
type: MULTI_NAMESPACE_ISOLATED_TYPE,
|
||||
id: 'three',
|
||||
namespace: 'bar-namespace',
|
||||
};
|
||||
const mgetResponse = getMockMgetResponse([obj], namespace);
|
||||
await bulkDeleteMultiNamespaceError([obj1, obj, obj2], { namespace }, mgetResponse);
|
||||
});
|
||||
|
||||
it(`returns an error when the type is multi-namespace and the document has multiple namespaces and the force option is not enabled`, async () => {
|
||||
const testObject = { ...obj1, type: MULTI_NAMESPACE_TYPE };
|
||||
const internalOptions = {
|
||||
mockMGetResponseWithObject: {
|
||||
...testObject,
|
||||
initialNamespaces: [namespace, 'bar-namespace'],
|
||||
},
|
||||
};
|
||||
const result = await repositoryBulkDeleteSuccess(
|
||||
[testObject],
|
||||
{ namespace },
|
||||
internalOptions
|
||||
);
|
||||
expect(result.statuses[0]).toStrictEqual(
|
||||
createBulkDeleteFailStatus({
|
||||
...testObject,
|
||||
error: createBadRequestError(
|
||||
'Unable to delete saved object that exists in multiple namespaces, use the "force" option to delete it anyway'
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it(`returns an error when the type is multi-namespace and the document has all namespaces and the force option is not enabled`, async () => {
|
||||
const testObject = { ...obj1, type: ALL_NAMESPACES_STRING };
|
||||
const internalOptions = {
|
||||
mockMGetResponseWithObject: {
|
||||
...testObject,
|
||||
initialNamespaces: [namespace, 'bar-namespace'],
|
||||
},
|
||||
};
|
||||
const result = await repositoryBulkDeleteSuccess(
|
||||
[testObject],
|
||||
{ namespace },
|
||||
internalOptions
|
||||
);
|
||||
expect(result.statuses[0]).toStrictEqual(
|
||||
createBulkDeleteFailStatus({
|
||||
...testObject,
|
||||
error: createBadRequestError("Unsupported saved object type: '*'"),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('returns', () => {
|
||||
it(`returns early for empty objects argument`, async () => {
|
||||
await savedObjectsRepository.bulkDelete([], { namespace });
|
||||
expect(client.bulk).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it(`formats the ES response`, async () => {
|
||||
const response = await repositoryBulkDeleteSuccess([obj1, obj2], { namespace });
|
||||
expect(response).toEqual({
|
||||
statuses: [obj1, obj2].map(createBulkDeleteSuccessStatus),
|
||||
});
|
||||
});
|
||||
|
||||
it(`handles a mix of successful deletes and errors`, async () => {
|
||||
const notFoundObj = { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, found: false };
|
||||
await bulkDeleteMultiNamespaceError(
|
||||
[obj1, notFoundObj, obj2],
|
||||
{ namespace },
|
||||
{} as estypes.MgetResponse,
|
||||
{ statusCode: 404 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#checkConflicts', () => {
|
||||
const obj1 = { type: 'dashboard', id: 'one' };
|
||||
const obj2 = { type: 'dashboard', id: 'two' };
|
||||
|
|
|
@ -54,6 +54,9 @@ import type {
|
|||
SavedObjectsClosePointInTimeOptions,
|
||||
SavedObjectsCreatePointInTimeFinderOptions,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import type {
|
||||
SavedObjectSanitizedDoc,
|
||||
|
@ -83,6 +86,7 @@ import {
|
|||
type IndexMapping,
|
||||
type IKibanaMigrator,
|
||||
} from '@kbn/core-saved-objects-base-server-internal';
|
||||
import pMap from 'p-map';
|
||||
import { PointInTimeFinder } from './point_in_time_finder';
|
||||
import { createRepositoryEsClient, RepositoryEsClient } from './repository_es_client';
|
||||
import { getSearchDsl } from './search_dsl';
|
||||
|
@ -109,6 +113,16 @@ import {
|
|||
PreflightCheckForCreateObject,
|
||||
} from './preflight_check_for_create';
|
||||
import { deleteLegacyUrlAliases } from './legacy_url_aliases';
|
||||
import type {
|
||||
BulkDeleteParams,
|
||||
ExpectedBulkDeleteResult,
|
||||
BulkDeleteItemErrorResult,
|
||||
NewBulkItemResponse,
|
||||
BulkDeleteExpectedBulkGetResult,
|
||||
PreflightCheckForBulkDeleteParams,
|
||||
ExpectedBulkDeleteMultiNamespaceDocsParams,
|
||||
ObjectToDeleteAliasesFor,
|
||||
} from './repository_bulk_delete_internal_types';
|
||||
|
||||
// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository
|
||||
// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient.
|
||||
|
@ -127,6 +141,7 @@ export interface SavedObjectsRepositoryOptions {
|
|||
export const DEFAULT_REFRESH_SETTING = 'wait_for';
|
||||
export const DEFAULT_RETRY_COUNT = 3;
|
||||
|
||||
const MAX_CONCURRENT_ALIAS_DELETIONS = 10;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -676,7 +691,6 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
|
|||
if (!this._allowedTypes.includes(type)) {
|
||||
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
|
||||
}
|
||||
|
||||
const { refresh = DEFAULT_REFRESH_SETTING, force } = options;
|
||||
const namespace = normalizeNamespace(options.namespace);
|
||||
|
||||
|
@ -762,6 +776,286 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs initial checks on object type validity and flags multi-namespace objects for preflight checks by adding an `esRequestIndex`
|
||||
* @param objects SavedObjectsBulkDeleteObject[]
|
||||
* @returns array BulkDeleteExpectedBulkGetResult[]
|
||||
* @internal
|
||||
*/
|
||||
private presortObjectsByNamespaceType(objects: SavedObjectsBulkDeleteObject[]) {
|
||||
let bulkGetRequestIndexCounter = 0;
|
||||
return objects.map<BulkDeleteExpectedBulkGetResult>((object) => {
|
||||
const { type, id } = object;
|
||||
if (!this._allowedTypes.includes(type)) {
|
||||
return {
|
||||
tag: 'Left',
|
||||
value: {
|
||||
id,
|
||||
type,
|
||||
error: errorContent(SavedObjectsErrorHelpers.createUnsupportedTypeError(type)),
|
||||
},
|
||||
};
|
||||
}
|
||||
const requiresNamespacesCheck = this._registry.isMultiNamespace(type);
|
||||
return {
|
||||
tag: 'Right',
|
||||
value: {
|
||||
type,
|
||||
id,
|
||||
...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch multi-namespace saved objects
|
||||
* @returns MgetResponse
|
||||
* @notes multi-namespace objects shared to more than one space require special handling. We fetch these docs to retrieve their namespaces.
|
||||
* @internal
|
||||
*/
|
||||
private async preflightCheckForBulkDelete(params: PreflightCheckForBulkDeleteParams) {
|
||||
const { expectedBulkGetResults, namespace } = params;
|
||||
const bulkGetMultiNamespaceDocs = expectedBulkGetResults
|
||||
.filter(isRight)
|
||||
.filter(({ value }) => value.esRequestIndex !== undefined)
|
||||
.map(({ value: { type, id } }) => ({
|
||||
_id: this._serializer.generateRawId(namespace, type, id),
|
||||
_index: this.getIndexForType(type),
|
||||
_source: ['type', 'namespaces'],
|
||||
}));
|
||||
|
||||
const bulkGetMultiNamespaceDocsResponse = bulkGetMultiNamespaceDocs.length
|
||||
? await this.client.mget(
|
||||
{ body: { docs: bulkGetMultiNamespaceDocs } },
|
||||
{ ignore: [404], meta: true }
|
||||
)
|
||||
: undefined;
|
||||
// fail fast if we can't verify a 404 response is from Elasticsearch
|
||||
if (
|
||||
bulkGetMultiNamespaceDocsResponse &&
|
||||
isNotFoundFromUnsupportedServer({
|
||||
statusCode: bulkGetMultiNamespaceDocsResponse.statusCode,
|
||||
headers: bulkGetMultiNamespaceDocsResponse.headers,
|
||||
})
|
||||
) {
|
||||
throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError();
|
||||
}
|
||||
return bulkGetMultiNamespaceDocsResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns array of objects sorted by expected delete success or failure result
|
||||
* @internal
|
||||
*/
|
||||
private getExpectedBulkDeleteMultiNamespaceDocsResults(
|
||||
params: ExpectedBulkDeleteMultiNamespaceDocsParams
|
||||
): ExpectedBulkDeleteResult[] {
|
||||
const { expectedBulkGetResults, multiNamespaceDocsResponse, namespace, force } = params;
|
||||
let indexCounter = 0;
|
||||
const expectedBulkDeleteMultiNamespaceDocsResults =
|
||||
expectedBulkGetResults.map<ExpectedBulkDeleteResult>((expectedBulkGetResult) => {
|
||||
if (isLeft(expectedBulkGetResult)) {
|
||||
return { ...expectedBulkGetResult };
|
||||
}
|
||||
const { esRequestIndex: esBulkGetRequestIndex, id, type } = expectedBulkGetResult.value;
|
||||
|
||||
let namespaces;
|
||||
|
||||
if (esBulkGetRequestIndex !== undefined) {
|
||||
const indexFound = multiNamespaceDocsResponse?.statusCode !== 404;
|
||||
|
||||
const actualResult = indexFound
|
||||
? multiNamespaceDocsResponse?.body.docs[esBulkGetRequestIndex]
|
||||
: undefined;
|
||||
|
||||
const docFound = indexFound && isMgetDoc(actualResult) && actualResult.found;
|
||||
|
||||
// return an error if the doc isn't found at all or the doc doesn't exist in the namespaces
|
||||
if (!docFound) {
|
||||
return {
|
||||
tag: 'Left',
|
||||
value: {
|
||||
id,
|
||||
type,
|
||||
error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)),
|
||||
},
|
||||
};
|
||||
}
|
||||
// the following check should be redundant since we're retrieving the docs from elasticsearch but we check just to make sure
|
||||
// @ts-expect-error MultiGetHit is incorrectly missing _id, _source
|
||||
if (!this.rawDocExistsInNamespace(actualResult, namespace)) {
|
||||
return {
|
||||
tag: 'Left',
|
||||
value: {
|
||||
id,
|
||||
type,
|
||||
error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)),
|
||||
},
|
||||
};
|
||||
}
|
||||
// @ts-expect-error MultiGetHit is incorrectly missing _id, _source
|
||||
namespaces = actualResult!._source.namespaces ?? [
|
||||
SavedObjectsUtils.namespaceIdToString(namespace),
|
||||
];
|
||||
const useForce = force && force === true ? true : false;
|
||||
// the document is shared to more than one space and can only be deleted by force.
|
||||
if (!useForce && (namespaces.length > 1 || namespaces.includes(ALL_NAMESPACES_STRING))) {
|
||||
return {
|
||||
tag: 'Left',
|
||||
value: {
|
||||
success: false,
|
||||
id,
|
||||
type,
|
||||
error: errorContent(
|
||||
SavedObjectsErrorHelpers.createBadRequestError(
|
||||
`Unable to delete saved object that exists in multiple namespaces, use the "force" option to delete it anyway`
|
||||
)
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
// contains all objects that passed initial preflight checks, including single namespace objects that skipped the mget call
|
||||
// single namespace objects will have namespaces:undefined
|
||||
const expectedResult = {
|
||||
type,
|
||||
id,
|
||||
namespaces,
|
||||
esRequestIndex: indexCounter++,
|
||||
};
|
||||
|
||||
return { tag: 'Right', value: expectedResult };
|
||||
});
|
||||
return expectedBulkDeleteMultiNamespaceDocsResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ISavedObjectsRepository.bulkDelete}
|
||||
*/
|
||||
async bulkDelete(
|
||||
objects: SavedObjectsBulkDeleteObject[],
|
||||
options: SavedObjectsBulkDeleteOptions = {}
|
||||
): Promise<SavedObjectsBulkDeleteResponse> {
|
||||
const { refresh = DEFAULT_REFRESH_SETTING, force } = options;
|
||||
const namespace = normalizeNamespace(options.namespace);
|
||||
const expectedBulkGetResults = this.presortObjectsByNamespaceType(objects);
|
||||
const multiNamespaceDocsResponse = await this.preflightCheckForBulkDelete({
|
||||
expectedBulkGetResults,
|
||||
namespace,
|
||||
});
|
||||
const bulkDeleteParams: BulkDeleteParams[] = [];
|
||||
|
||||
const expectedBulkDeleteMultiNamespaceDocsResults =
|
||||
this.getExpectedBulkDeleteMultiNamespaceDocsResults({
|
||||
expectedBulkGetResults,
|
||||
multiNamespaceDocsResponse,
|
||||
namespace,
|
||||
force,
|
||||
});
|
||||
// bulk up the bulkDeleteParams
|
||||
expectedBulkDeleteMultiNamespaceDocsResults.map((expectedResult) => {
|
||||
if (isRight(expectedResult)) {
|
||||
bulkDeleteParams.push({
|
||||
delete: {
|
||||
_id: this._serializer.generateRawId(
|
||||
namespace,
|
||||
expectedResult.value.type,
|
||||
expectedResult.value.id
|
||||
),
|
||||
_index: this.getIndexForType(expectedResult.value.type),
|
||||
...getExpectedVersionProperties(undefined),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const bulkDeleteResponse = bulkDeleteParams.length
|
||||
? await this.client.bulk({
|
||||
refresh,
|
||||
body: bulkDeleteParams,
|
||||
require_alias: true,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// extracted to ensure consistency in the error results returned
|
||||
let errorResult: BulkDeleteItemErrorResult;
|
||||
const objectsToDeleteAliasesFor: ObjectToDeleteAliasesFor[] = [];
|
||||
|
||||
const savedObjects = expectedBulkDeleteMultiNamespaceDocsResults.map((expectedResult) => {
|
||||
if (isLeft(expectedResult)) {
|
||||
return { ...expectedResult.value, success: false };
|
||||
}
|
||||
const {
|
||||
type,
|
||||
id,
|
||||
namespaces,
|
||||
esRequestIndex: esBulkDeleteRequestIndex,
|
||||
} = expectedResult.value;
|
||||
// we assume this wouldn't happen but is needed to ensure type consistency
|
||||
if (bulkDeleteResponse === undefined) {
|
||||
throw new Error(
|
||||
`Unexpected error in bulkDelete saved objects: bulkDeleteResponse is undefined`
|
||||
);
|
||||
}
|
||||
const rawResponse = Object.values(
|
||||
bulkDeleteResponse.items[esBulkDeleteRequestIndex]
|
||||
)[0] as NewBulkItemResponse;
|
||||
|
||||
const error = getBulkOperationError(type, id, rawResponse);
|
||||
if (error) {
|
||||
errorResult = { success: false, type, id, error };
|
||||
return errorResult;
|
||||
}
|
||||
if (rawResponse.result === 'not_found') {
|
||||
errorResult = {
|
||||
success: false,
|
||||
type,
|
||||
id,
|
||||
error: errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)),
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
if (rawResponse.result === 'deleted') {
|
||||
// `namespaces` should only exist in the expectedResult.value if the type is multi-namespace.
|
||||
if (namespaces) {
|
||||
objectsToDeleteAliasesFor.push({
|
||||
type,
|
||||
id,
|
||||
...(namespaces.includes(ALL_NAMESPACES_STRING)
|
||||
? { namespaces: [], deleteBehavior: 'exclusive' }
|
||||
: { namespaces, deleteBehavior: 'inclusive' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
const successfulResult = {
|
||||
success: true,
|
||||
id,
|
||||
type,
|
||||
};
|
||||
return successfulResult;
|
||||
});
|
||||
|
||||
// Delete aliases if necessary, ensuring we don't have too many concurrent operations running.
|
||||
const mapper = async ({ type, id, namespaces, deleteBehavior }: ObjectToDeleteAliasesFor) =>
|
||||
await deleteLegacyUrlAliases({
|
||||
mappings: this._mappings,
|
||||
registry: this._registry,
|
||||
client: this.client,
|
||||
getIndexForType: this.getIndexForType.bind(this),
|
||||
type,
|
||||
id,
|
||||
namespaces,
|
||||
deleteBehavior,
|
||||
}).catch((err) => {
|
||||
this._logger.error(`Unable to delete aliases when deleting an object: ${err.message}`);
|
||||
});
|
||||
await pMap(objectsToDeleteAliasesFor, mapper, { concurrency: MAX_CONCURRENT_ALIAS_DELETIONS });
|
||||
|
||||
return { statuses: [...savedObjects] };
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ISavedObjectsRepository.deleteByNamespace}
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import type { Payload } from '@hapi/boom';
|
||||
import {
|
||||
BulkOperationBase,
|
||||
BulkResponseItem,
|
||||
ErrorCause,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { estypes, TransportResult } from '@elastic/elasticsearch';
|
||||
import { Either } from './internal_utils';
|
||||
import { DeleteLegacyUrlAliasesParams } from './legacy_url_aliases';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface PreflightCheckForBulkDeleteParams {
|
||||
expectedBulkGetResults: BulkDeleteExpectedBulkGetResult[];
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface ExpectedBulkDeleteMultiNamespaceDocsParams {
|
||||
// contains the type and id of all objects to delete
|
||||
expectedBulkGetResults: BulkDeleteExpectedBulkGetResult[];
|
||||
// subset of multi-namespace only expectedBulkGetResults
|
||||
multiNamespaceDocsResponse: TransportResult<estypes.MgetResponse<unknown>, unknown> | undefined;
|
||||
// current namespace in which the bulkDelete call is made
|
||||
namespace: string | undefined;
|
||||
// optional parameter used to force delete multinamespace objects that exist in more than the current space
|
||||
force?: boolean;
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface BulkDeleteParams {
|
||||
delete: Omit<BulkOperationBase, 'version' | 'version_type' | 'routing'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type ExpectedBulkDeleteResult = Either<
|
||||
{ type: string; id: string; error: Payload },
|
||||
{
|
||||
type: string;
|
||||
id: string;
|
||||
namespaces: string[];
|
||||
esRequestIndex: number;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface BulkDeleteItemErrorResult {
|
||||
success: boolean;
|
||||
type: string;
|
||||
id: string;
|
||||
error: Payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type NewBulkItemResponse = BulkResponseItem & { error: ErrorCause & { index: string } };
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @note Contains all documents for bulk delete, regardless of namespace type
|
||||
*/
|
||||
export type BulkDeleteExpectedBulkGetResult = Either<
|
||||
{ type: string; id: string; error: Payload },
|
||||
{ type: string; id: string; version?: string; esRequestIndex?: number }
|
||||
>;
|
||||
|
||||
export type ObjectToDeleteAliasesFor = Pick<
|
||||
DeleteLegacyUrlAliasesParams,
|
||||
'type' | 'id' | 'namespaces' | 'deleteBehavior'
|
||||
>;
|
|
@ -16,6 +16,7 @@ const createRepositoryMock = () => {
|
|||
create: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
bulkUpdate: jest.fn(),
|
||||
bulkDelete: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
find: jest.fn(),
|
||||
|
|
|
@ -23,6 +23,8 @@ import type {
|
|||
SavedObjectsFindOptions,
|
||||
SavedObjectsUpdateObjectsSpacesObject,
|
||||
SavedObjectsUpdateObjectsSpacesOptions,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import { SavedObjectsClient } from './saved_objects_client';
|
||||
import { repositoryMock, savedObjectsPointInTimeFinderMock } from './mocks';
|
||||
|
@ -119,6 +121,22 @@ describe('SavedObjectsClient', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test(`#bulkDelete`, async () => {
|
||||
const returnValue: any = Symbol();
|
||||
mockRepository.bulkDelete.mockResolvedValueOnce(returnValue);
|
||||
const client = new SavedObjectsClient(mockRepository);
|
||||
|
||||
const objects: SavedObjectsBulkDeleteObject[] = [
|
||||
{ type: 'foo', id: '1' },
|
||||
{ type: 'bar', id: '2' },
|
||||
];
|
||||
const options: SavedObjectsBulkDeleteOptions = { namespace: 'ns-1', refresh: true };
|
||||
const result = await client.bulkDelete(objects, options);
|
||||
|
||||
expect(mockRepository.bulkDelete).toHaveBeenCalledWith(objects, options);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#delete`, async () => {
|
||||
const returnValue: any = Symbol();
|
||||
mockRepository.delete.mockResolvedValueOnce(returnValue);
|
||||
|
|
|
@ -39,6 +39,9 @@ import type {
|
|||
SavedObjectsClosePointInTimeOptions,
|
||||
SavedObjectsCreatePointInTimeFinderOptions,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-utils-server';
|
||||
|
||||
|
@ -83,6 +86,14 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
|
|||
return await this._repository.delete(type, id, options);
|
||||
}
|
||||
|
||||
/** {@inheritDoc SavedObjectsClientContract.bulkDelete} */
|
||||
async bulkDelete(
|
||||
objects: SavedObjectsBulkDeleteObject[],
|
||||
options: SavedObjectsBulkDeleteOptions = {}
|
||||
): Promise<SavedObjectsBulkDeleteResponse> {
|
||||
return await this._repository.bulkDelete(objects, options);
|
||||
}
|
||||
|
||||
/** {@inheritDoc SavedObjectsClientContract.find} */
|
||||
async find<T = unknown, A = unknown>(
|
||||
options: SavedObjectsFindOptions
|
||||
|
|
|
@ -15,6 +15,7 @@ const create = () => {
|
|||
create: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
bulkUpdate: jest.fn(),
|
||||
bulkDelete: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
find: jest.fn(),
|
||||
|
|
|
@ -18,6 +18,7 @@ const create = () => {
|
|||
checkConflicts: jest.fn(),
|
||||
bulkUpdate: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
bulkDelete: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
|
|
|
@ -52,4 +52,8 @@ export type {
|
|||
SavedObjectsCreatePointInTimeFinderOptions,
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsPointInTimeFinderClient,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteStatus,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from './src/apis';
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { SavedObjectError } from '@kbn/core-saved-objects-common';
|
||||
import type { MutatingOperationRefreshSetting, SavedObjectsBaseOptions } from './base';
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsBulkDeleteObject {
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsBulkDeleteOptions extends SavedObjectsBaseOptions {
|
||||
/** The Elasticsearch Refresh setting for this operation */
|
||||
refresh?: MutatingOperationRefreshSetting;
|
||||
/**
|
||||
* Force deletion of all objects that exists in multiple namespaces, applied to all objects.
|
||||
*/
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsBulkDeleteStatus {
|
||||
id: string;
|
||||
type: string;
|
||||
/** The status of deleting the object: true for deleted, false for error */
|
||||
success: boolean;
|
||||
/** Reason the object could not be deleted (success is false) */
|
||||
error?: SavedObjectError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsBulkDeleteResponse {
|
||||
statuses: SavedObjectsBulkDeleteStatus[];
|
||||
}
|
|
@ -72,3 +72,9 @@ export type {
|
|||
SavedObjectsUpdateObjectsSpacesOptions,
|
||||
SavedObjectsUpdateObjectsSpacesResponseObject,
|
||||
} from './update_objects_spaces';
|
||||
export type {
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteStatus,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from './bulk_delete';
|
||||
|
|
|
@ -41,6 +41,9 @@ import type {
|
|||
SavedObjectsRemoveReferencesToResponse,
|
||||
SavedObjectsCollectMultiNamespaceReferencesOptions,
|
||||
SavedObjectsBulkResponse,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from './apis';
|
||||
|
||||
/**
|
||||
|
@ -151,6 +154,16 @@ export interface SavedObjectsClientContract {
|
|||
*/
|
||||
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
|
||||
|
||||
/**
|
||||
* Deletes multiple SavedObjects batched together as a single request
|
||||
*
|
||||
* @param objects
|
||||
* @param options
|
||||
*/
|
||||
bulkDelete(
|
||||
objects: SavedObjectsBulkDeleteObject[],
|
||||
options?: SavedObjectsBulkDeleteOptions
|
||||
): Promise<SavedObjectsBulkDeleteResponse>;
|
||||
/**
|
||||
* Find all SavedObjects matching the search query
|
||||
*
|
||||
|
|
|
@ -44,6 +44,9 @@ import type {
|
|||
SavedObjectsDeleteByNamespaceOptions,
|
||||
SavedObjectsIncrementCounterField,
|
||||
SavedObjectsIncrementCounterOptions,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from './apis';
|
||||
|
||||
/**
|
||||
|
@ -105,6 +108,17 @@ export interface ISavedObjectsRepository {
|
|||
*/
|
||||
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
|
||||
|
||||
/**
|
||||
* Deletes multiple documents at once
|
||||
* @param {array} objects - an array of objects containing id and type
|
||||
* @param {object} [options={}]
|
||||
* @returns {promise} - { statuses: [{ id, type, success, error: { message } }] }
|
||||
*/
|
||||
bulkDelete(
|
||||
objects: SavedObjectsBulkDeleteObject[],
|
||||
options?: SavedObjectsBulkDeleteOptions
|
||||
): Promise<SavedObjectsBulkDeleteResponse>;
|
||||
|
||||
/**
|
||||
* Deletes all objects from the provided namespace.
|
||||
*
|
||||
|
|
|
@ -308,6 +308,45 @@ describe('SavedObjectsClient', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#bulk_delete', () => {
|
||||
const bulkDeleteDoc = {
|
||||
id: 'AVwSwFxtcMV38qjDZoQg',
|
||||
type: 'config',
|
||||
};
|
||||
beforeEach(() => {
|
||||
http.fetch.mockResolvedValue({
|
||||
statuses: [{ id: bulkDeleteDoc.id, type: bulkDeleteDoc.type, success: true }],
|
||||
});
|
||||
});
|
||||
|
||||
test('deletes with an array of id, type and success status for deleted docs', async () => {
|
||||
const response = savedObjectsClient.bulkDelete([bulkDeleteDoc]);
|
||||
await expect(response).resolves.toHaveProperty('statuses');
|
||||
|
||||
const result = await response;
|
||||
expect(result.statuses).toHaveLength(1);
|
||||
expect(result.statuses[0]).toHaveProperty('success');
|
||||
});
|
||||
|
||||
test('makes HTTP call', async () => {
|
||||
await savedObjectsClient.bulkDelete([bulkDeleteDoc]);
|
||||
expect(http.fetch.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/api/saved_objects/_bulk_delete",
|
||||
Object {
|
||||
"body": "[{\\"type\\":\\"config\\",\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\"}]",
|
||||
"method": "POST",
|
||||
"query": Object {
|
||||
"force": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
const attributes = { foo: 'Foo', bar: 'Bar' };
|
||||
const options = { version: '1' };
|
||||
|
|
|
@ -11,9 +11,11 @@ import type { HttpSetup, HttpFetchOptions } from '@kbn/core-http-browser';
|
|||
import type { SavedObject, SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common';
|
||||
import type {
|
||||
SavedObjectsBulkResolveResponse as SavedObjectsBulkResolveResponseServer,
|
||||
SavedObjectsBulkDeleteResponse as SavedObjectsBulkDeleteResponseServer,
|
||||
SavedObjectsClientContract as SavedObjectsApi,
|
||||
SavedObjectsFindResponse as SavedObjectsFindResponseServer,
|
||||
SavedObjectsResolveResponse,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
|
@ -28,6 +30,7 @@ import type {
|
|||
SavedObjectsBulkCreateOptions,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SimpleSavedObject,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from '@kbn/core-saved-objects-api-browser';
|
||||
|
||||
import { SimpleSavedObjectImpl } from './simple_saved_object';
|
||||
|
@ -255,6 +258,31 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
|
|||
return this.savedObjectsFetch(this.getPath([type, id]), { method: 'DELETE', query });
|
||||
};
|
||||
|
||||
public bulkDelete = async (
|
||||
objects: SavedObjectTypeIdTuple[],
|
||||
options?: SavedObjectsBulkDeleteOptions
|
||||
): Promise<SavedObjectsBulkDeleteResponse> => {
|
||||
const filteredObjects = objects.map(({ type, id }) => ({ type, id }));
|
||||
const queryOptions = { force: !!options?.force };
|
||||
const response = await this.performBulkDelete(filteredObjects, queryOptions);
|
||||
return {
|
||||
statuses: response.statuses,
|
||||
};
|
||||
};
|
||||
|
||||
private async performBulkDelete(
|
||||
objects: SavedObjectTypeIdTuple[],
|
||||
queryOptions: { force: boolean }
|
||||
) {
|
||||
const path = this.getPath(['_bulk_delete']);
|
||||
const request: Promise<SavedObjectsBulkDeleteResponseServer> = this.savedObjectsFetch(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(objects),
|
||||
query: queryOptions,
|
||||
});
|
||||
return request;
|
||||
}
|
||||
|
||||
public find = <T = unknown, A = unknown>(
|
||||
options: SavedObjectsFindOptions
|
||||
): Promise<SavedObjectsFindResponse<T>> => {
|
||||
|
|
|
@ -19,6 +19,7 @@ const createStartContractMock = () => {
|
|||
bulkCreate: jest.fn(),
|
||||
bulkResolve: jest.fn(),
|
||||
bulkUpdate: jest.fn(),
|
||||
bulkDelete: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
find: jest.fn(),
|
||||
|
|
|
@ -22,6 +22,7 @@ export { registerBulkCreateRoute } from './src/routes/bulk_create';
|
|||
export { registerBulkGetRoute } from './src/routes/bulk_get';
|
||||
export { registerBulkResolveRoute } from './src/routes/bulk_resolve';
|
||||
export { registerBulkUpdateRoute } from './src/routes/bulk_update';
|
||||
export { registerBulkDeleteRoute } from './src/routes/bulk_delete';
|
||||
export { registerCreateRoute } from './src/routes/create';
|
||||
export { registerDeleteRoute } from './src/routes/delete';
|
||||
export { registerExportRoute } from './src/routes/export';
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalSavedObjectRouter } from '../internal_types';
|
||||
import { catchAndReturnBoomErrors } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
}
|
||||
|
||||
export const registerBulkDeleteRoute = (
|
||||
router: InternalSavedObjectRouter,
|
||||
{ coreUsageData }: RouteDependencies
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: '/_bulk_delete',
|
||||
validate: {
|
||||
body: schema.arrayOf(
|
||||
schema.object({
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
})
|
||||
),
|
||||
query: schema.object({
|
||||
force: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
catchAndReturnBoomErrors(async (context, req, res) => {
|
||||
const { force } = req.query;
|
||||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient.incrementSavedObjectsBulkDelete({ request: req }).catch(() => {});
|
||||
|
||||
const { savedObjects } = await context.core;
|
||||
|
||||
const statuses = await savedObjects.client.bulkDelete(req.body, { force });
|
||||
return res.ok({ body: statuses });
|
||||
})
|
||||
);
|
||||
};
|
|
@ -23,6 +23,7 @@ import { registerUpdateRoute } from './update';
|
|||
import { registerBulkGetRoute } from './bulk_get';
|
||||
import { registerBulkCreateRoute } from './bulk_create';
|
||||
import { registerBulkUpdateRoute } from './bulk_update';
|
||||
import { registerBulkDeleteRoute } from './bulk_delete';
|
||||
import { registerExportRoute } from './export';
|
||||
import { registerImportRoute } from './import';
|
||||
import { registerResolveImportErrorsRoute } from './resolve_import_errors';
|
||||
|
@ -62,6 +63,7 @@ export function registerRoutes({
|
|||
registerBulkCreateRoute(router, { coreUsageData });
|
||||
registerBulkResolveRoute(router, { coreUsageData });
|
||||
registerBulkUpdateRoute(router, { coreUsageData });
|
||||
registerBulkDeleteRoute(router, { coreUsageData });
|
||||
registerExportRoute(router, { config, coreUsageData });
|
||||
registerImportRoute(router, { config, coreUsageData });
|
||||
registerResolveImportErrorsRoute(router, { config, coreUsageData });
|
||||
|
|
|
@ -43,6 +43,8 @@ export interface ICoreUsageStatsClient {
|
|||
|
||||
incrementSavedObjectsBulkUpdate(options: BaseIncrementOptions): Promise<void>;
|
||||
|
||||
incrementSavedObjectsBulkDelete(options: BaseIncrementOptions): Promise<void>;
|
||||
|
||||
incrementSavedObjectsCreate(options: BaseIncrementOptions): Promise<void>;
|
||||
|
||||
incrementSavedObjectsDelete(options: BaseIncrementOptions): Promise<void>;
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
BULK_CREATE_STATS_PREFIX,
|
||||
BULK_GET_STATS_PREFIX,
|
||||
BULK_UPDATE_STATS_PREFIX,
|
||||
BULK_DELETE_STATS_PREFIX,
|
||||
CREATE_STATS_PREFIX,
|
||||
DELETE_STATS_PREFIX,
|
||||
FIND_STATS_PREFIX,
|
||||
|
@ -452,6 +453,81 @@ describe('CoreUsageStatsClient', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#incrementSavedObjectsBulkDelete', () => {
|
||||
it('does not throw an error if repository incrementCounter operation fails', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
await expect(
|
||||
usageStatsClient.incrementSavedObjectsBulkDelete({
|
||||
request,
|
||||
} as BaseIncrementOptions)
|
||||
).resolves.toBeUndefined();
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles falsy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
await usageStatsClient.incrementSavedObjectsBulkDelete({
|
||||
request,
|
||||
} as BaseIncrementOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${BULK_DELETE_STATS_PREFIX}.total`,
|
||||
`${BULK_DELETE_STATS_PREFIX}.namespace.default.total`,
|
||||
`${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles truthy options and the default namespace string appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING);
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders });
|
||||
await usageStatsClient.incrementSavedObjectsBulkDelete({
|
||||
request,
|
||||
} as BaseIncrementOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${BULK_DELETE_STATS_PREFIX}.total`,
|
||||
`${BULK_DELETE_STATS_PREFIX}.namespace.default.total`,
|
||||
`${BULK_DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles a non-default space appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup('foo');
|
||||
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
await usageStatsClient.incrementSavedObjectsBulkDelete({
|
||||
request,
|
||||
} as BaseIncrementOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${BULK_DELETE_STATS_PREFIX}.total`,
|
||||
`${BULK_DELETE_STATS_PREFIX}.namespace.custom.total`,
|
||||
`${BULK_DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#incrementSavedObjectsDelete', () => {
|
||||
it('does not throw an error if repository incrementCounter operation fails', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
|
|
@ -25,6 +25,7 @@ export const BULK_CREATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkCreate';
|
|||
export const BULK_GET_STATS_PREFIX = 'apiCalls.savedObjectsBulkGet';
|
||||
export const BULK_RESOLVE_STATS_PREFIX = 'apiCalls.savedObjectsBulkResolve';
|
||||
export const BULK_UPDATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkUpdate';
|
||||
export const BULK_DELETE_STATS_PREFIX = 'apiCalls.savedObjectsBulkDelete';
|
||||
export const CREATE_STATS_PREFIX = 'apiCalls.savedObjectsCreate';
|
||||
export const DELETE_STATS_PREFIX = 'apiCalls.savedObjectsDelete';
|
||||
export const FIND_STATS_PREFIX = 'apiCalls.savedObjectsFind';
|
||||
|
@ -43,6 +44,7 @@ const ALL_COUNTER_FIELDS = [
|
|||
...getFieldsForCounter(BULK_GET_STATS_PREFIX),
|
||||
...getFieldsForCounter(BULK_RESOLVE_STATS_PREFIX),
|
||||
...getFieldsForCounter(BULK_UPDATE_STATS_PREFIX),
|
||||
...getFieldsForCounter(BULK_DELETE_STATS_PREFIX),
|
||||
...getFieldsForCounter(CREATE_STATS_PREFIX),
|
||||
...getFieldsForCounter(DELETE_STATS_PREFIX),
|
||||
...getFieldsForCounter(FIND_STATS_PREFIX),
|
||||
|
@ -114,6 +116,10 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient {
|
|||
await this.updateUsageStats([], BULK_UPDATE_STATS_PREFIX, options);
|
||||
}
|
||||
|
||||
public async incrementSavedObjectsBulkDelete(options: BaseIncrementOptions) {
|
||||
await this.updateUsageStats([], BULK_DELETE_STATS_PREFIX, options);
|
||||
}
|
||||
|
||||
public async incrementSavedObjectsCreate(options: BaseIncrementOptions) {
|
||||
await this.updateUsageStats([], CREATE_STATS_PREFIX, options);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ const createUsageStatsClientMock = () =>
|
|||
incrementSavedObjectsBulkGet: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsBulkResolve: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsBulkUpdate: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsBulkDelete: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsCreate: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsDelete: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsFind: jest.fn().mockResolvedValue(null),
|
||||
|
|
|
@ -42,6 +42,13 @@ export interface CoreUsageStats {
|
|||
'apiCalls.savedObjectsBulkUpdate.namespace.custom.total'?: number;
|
||||
'apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.total'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.default.total'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.default.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.default.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.custom.total'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.custom.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.custom.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsCreate.total'?: number;
|
||||
'apiCalls.savedObjectsCreate.namespace.default.total'?: number;
|
||||
'apiCalls.savedObjectsCreate.namespace.default.kibanaRequest.yes'?: number;
|
||||
|
|
|
@ -338,6 +338,9 @@ export type {
|
|||
SavedObjectsFindOptions,
|
||||
SavedObjectsFindOptionsReference,
|
||||
SavedObjectsPitParams,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
export type {
|
||||
SavedObjectsServiceSetup,
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import supertest from 'supertest';
|
||||
import { savedObjectsClientMock } from '../../../mocks';
|
||||
import type { ICoreUsageStatsClient } from '@kbn/core-usage-data-base-server-internal';
|
||||
import {
|
||||
coreUsageStatsClientMock,
|
||||
coreUsageDataServiceMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import { setupServer } from './test_utils';
|
||||
import {
|
||||
registerBulkDeleteRoute,
|
||||
type InternalSavedObjectsRequestHandlerContext,
|
||||
} from '@kbn/core-saved-objects-server-internal';
|
||||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
describe('POST /api/saved_objects/_bulk_delete', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
let handlerContext: SetupServerReturn['handlerContext'];
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
let coreUsageStatsClient: jest.Mocked<ICoreUsageStatsClient>;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, httpSetup, handlerContext } = await setupServer());
|
||||
savedObjectsClient = handlerContext.savedObjects.client;
|
||||
|
||||
savedObjectsClient.bulkDelete.mockResolvedValue({
|
||||
statuses: [],
|
||||
});
|
||||
const router =
|
||||
httpSetup.createRouter<InternalSavedObjectsRequestHandlerContext>('/api/saved_objects/');
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
coreUsageStatsClient.incrementSavedObjectsBulkDelete.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
|
||||
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
|
||||
registerBulkDeleteRoute(router, { coreUsageData });
|
||||
|
||||
await server.start();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('formats successful response and records usage stats', async () => {
|
||||
const clientResponse = {
|
||||
statuses: [
|
||||
{
|
||||
id: 'abc123',
|
||||
type: 'index-pattern',
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
savedObjectsClient.bulkDelete.mockImplementation(() => Promise.resolve(clientResponse));
|
||||
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post('/api/saved_objects/_bulk_delete')
|
||||
.send([
|
||||
{
|
||||
id: 'abc123',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
expect(result.body).toEqual(clientResponse);
|
||||
expect(coreUsageStatsClient.incrementSavedObjectsBulkDelete).toHaveBeenCalledWith({
|
||||
request: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('calls upon savedObjectClient.bulkDelete with query options', async () => {
|
||||
const docs = [
|
||||
{
|
||||
id: 'abc123',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
];
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.post('/api/saved_objects/_bulk_delete')
|
||||
.send(docs)
|
||||
.query({ force: true })
|
||||
.expect(200);
|
||||
|
||||
expect(savedObjectsClient.bulkDelete).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.bulkDelete).toHaveBeenCalledWith(docs, { force: true });
|
||||
});
|
||||
});
|
|
@ -54,6 +54,17 @@ const registerSOTypes = (setup: InternalCoreSetup) => {
|
|||
},
|
||||
namespaceType: 'single',
|
||||
});
|
||||
setup.savedObjects.registerType({
|
||||
name: 'my_bulk_delete_type',
|
||||
hidden: false,
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
title: { type: 'text' },
|
||||
},
|
||||
},
|
||||
namespaceType: 'single',
|
||||
});
|
||||
};
|
||||
|
||||
describe('404s from proxies', () => {
|
||||
|
@ -124,6 +135,7 @@ describe('404s from proxies', () => {
|
|||
let repository: ISavedObjectsRepository;
|
||||
let myOtherType: SavedObject;
|
||||
const myOtherTypeDocs: SavedObject[] = [];
|
||||
const myBulkDeleteTypeDocs: SavedObject[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
repository = start.savedObjects.createInternalRepository();
|
||||
|
@ -145,6 +157,19 @@ describe('404s from proxies', () => {
|
|||
overwrite: true,
|
||||
namespace: 'default',
|
||||
});
|
||||
|
||||
for (let i = 1; i < 11; i++) {
|
||||
myBulkDeleteTypeDocs.push({
|
||||
type: 'my_bulk_delete_type',
|
||||
id: `myOtherTypeId${i}`,
|
||||
attributes: { title: `MyOtherTypeTitle${i}` },
|
||||
references: [],
|
||||
});
|
||||
}
|
||||
await repository.bulkCreate(myBulkDeleteTypeDocs, {
|
||||
overwrite: true,
|
||||
namespace: 'default',
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -237,6 +262,18 @@ describe('404s from proxies', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('handles `bulkDelete` requests that are successful when the proxy passes through the product header', async () => {
|
||||
const docsToDelete = myBulkDeleteTypeDocs;
|
||||
const bulkDeleteDocs = docsToDelete.map((doc) => ({
|
||||
id: doc.id,
|
||||
type: 'my_bulk_delete_type',
|
||||
}));
|
||||
|
||||
const docsFound = await repository.bulkDelete(bulkDeleteDocs, { force: false });
|
||||
expect(docsFound.statuses.length).toBeGreaterThan(0);
|
||||
expect(docsFound.statuses[0].success).toBe(true);
|
||||
});
|
||||
|
||||
it('handles `bulkGet` requests that are successful when the proxy passes through the product header', async () => {
|
||||
const docsToGet = myOtherTypeDocs;
|
||||
const docsFound = await repository.bulkGet(
|
||||
|
|
|
@ -122,7 +122,8 @@ export const declarePostMgetRoute = (hapiServer: Hapi.Server, hostname: string,
|
|||
if (
|
||||
proxyInterrupt === 'bulkGetMyType' ||
|
||||
proxyInterrupt === 'checkConficts' ||
|
||||
proxyInterrupt === 'internalBulkResolve'
|
||||
proxyInterrupt === 'internalBulkResolve' ||
|
||||
proxyInterrupt === 'bulkDeleteMyDocs'
|
||||
) {
|
||||
return proxyResponseHandler(h, hostname, port);
|
||||
} else {
|
||||
|
|
|
@ -822,6 +822,46 @@ export function getCoreUsageCollector(
|
|||
'How many times this API has been called by a non-Kibana client in a custom space.',
|
||||
},
|
||||
},
|
||||
'apiCalls.savedObjectsBulkDelete.total': {
|
||||
type: 'long',
|
||||
_meta: { description: 'How many times this API has been called.' },
|
||||
},
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.default.total': {
|
||||
type: 'long',
|
||||
_meta: { description: 'How many times this API has been called in the Default space.' },
|
||||
},
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.default.kibanaRequest.yes': {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'How many times this API has been called by the Kibana client in the Default space.',
|
||||
},
|
||||
},
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.default.kibanaRequest.no': {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'How many times this API has been called by a non-Kibana client in the Default space.',
|
||||
},
|
||||
},
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.custom.total': {
|
||||
type: 'long',
|
||||
_meta: { description: 'How many times this API has been called in a custom space.' },
|
||||
},
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.custom.kibanaRequest.yes': {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'How many times this API has been called by the Kibana client in a custom space.',
|
||||
},
|
||||
},
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.custom.kibanaRequest.no': {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'How many times this API has been called by a non-Kibana client in a custom space.',
|
||||
},
|
||||
},
|
||||
// Saved Objects Management APIs
|
||||
'apiCalls.savedObjectsImport.total': {
|
||||
type: 'long',
|
||||
|
|
|
@ -38,6 +38,13 @@ export interface CoreUsageStats {
|
|||
'apiCalls.savedObjectsBulkResolve.namespace.custom.total'?: number;
|
||||
'apiCalls.savedObjectsBulkResolve.namespace.custom.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsBulkResolve.namespace.custom.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.total'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.default.total'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.default.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.default.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.custom.total'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.custom.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsBulkDelete.namespace.custom.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsBulkUpdate.total'?: number;
|
||||
'apiCalls.savedObjectsBulkUpdate.namespace.default.total'?: number;
|
||||
'apiCalls.savedObjectsBulkUpdate.namespace.default.kibanaRequest.yes'?: number;
|
||||
|
|
|
@ -7222,6 +7222,48 @@
|
|||
"description": "How many times this API has been called by a non-Kibana client in a custom space."
|
||||
}
|
||||
},
|
||||
"apiCalls.savedObjectsBulkDelete.total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "How many times this API has been called."
|
||||
}
|
||||
},
|
||||
"apiCalls.savedObjectsBulkDelete.namespace.default.total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "How many times this API has been called in the Default space."
|
||||
}
|
||||
},
|
||||
"apiCalls.savedObjectsBulkDelete.namespace.default.kibanaRequest.yes": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "How many times this API has been called by the Kibana client in the Default space."
|
||||
}
|
||||
},
|
||||
"apiCalls.savedObjectsBulkDelete.namespace.default.kibanaRequest.no": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "How many times this API has been called by a non-Kibana client in the Default space."
|
||||
}
|
||||
},
|
||||
"apiCalls.savedObjectsBulkDelete.namespace.custom.total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "How many times this API has been called in a custom space."
|
||||
}
|
||||
},
|
||||
"apiCalls.savedObjectsBulkDelete.namespace.custom.kibanaRequest.yes": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "How many times this API has been called by the Kibana client in a custom space."
|
||||
}
|
||||
},
|
||||
"apiCalls.savedObjectsBulkDelete.namespace.custom.kibanaRequest.no": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "How many times this API has been called by a non-Kibana client in a custom space."
|
||||
}
|
||||
},
|
||||
"apiCalls.savedObjectsImport.total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
|
|
114
test/api_integration/apis/saved_objects/bulk_delete.ts
Normal file
114
test/api_integration/apis/saved_objects/bulk_delete.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('bulk_delete', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.importExport.load(
|
||||
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
});
|
||||
after(async () => {
|
||||
await kibanaServer.importExport.unload(
|
||||
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 200 with individual responses when deleting many docs', async () =>
|
||||
await supertest
|
||||
.post(`/api/saved_objects/_bulk_delete`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
])
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statuses: [
|
||||
{
|
||||
success: true,
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
{
|
||||
success: true,
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
],
|
||||
});
|
||||
}));
|
||||
|
||||
it('should return generic 404 when deleting an unknown doc', async () =>
|
||||
await supertest
|
||||
.post(`/api/saved_objects/_bulk_delete`)
|
||||
.send([{ type: 'dashboard', id: 'not-a-real-id' }])
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statuses: [
|
||||
{
|
||||
error: {
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [dashboard/not-a-real-id] not found',
|
||||
statusCode: 404,
|
||||
},
|
||||
id: 'not-a-real-id',
|
||||
type: 'dashboard',
|
||||
success: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
}));
|
||||
|
||||
it('should return the result of deleting valid and invalid objects in the same request', async () =>
|
||||
await supertest
|
||||
.post(`/api/saved_objects/_bulk_delete`)
|
||||
.send([
|
||||
{ type: 'visualization', id: 'not-a-real-vis-id' },
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
])
|
||||
.expect(200)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statuses: [
|
||||
{
|
||||
error: {
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [visualization/not-a-real-vis-id] not found',
|
||||
statusCode: 404,
|
||||
},
|
||||
id: 'not-a-real-vis-id',
|
||||
type: 'visualization',
|
||||
success: false,
|
||||
},
|
||||
{
|
||||
success: true,
|
||||
type: 'index-pattern',
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
],
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
|
@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('saved_objects', () => {
|
||||
loadTestFile(require.resolve('./bulk_create'));
|
||||
loadTestFile(require.resolve('./bulk_delete'));
|
||||
loadTestFile(require.resolve('./bulk_get'));
|
||||
loadTestFile(require.resolve('./bulk_update'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
|
|
|
@ -870,6 +870,44 @@ describe('#delete', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#bulkDelete', () => {
|
||||
const obj1 = Object.freeze({ type: 'unknown-type', id: 'unknown-type-id-1' });
|
||||
const obj2 = Object.freeze({ type: 'unknown-type', id: 'unknown-type-id-2' });
|
||||
const namespace = 'some-ns';
|
||||
|
||||
it('redirects request to underlying base client if type is not registered', async () => {
|
||||
await wrapper.bulkDelete([obj1, obj2], { namespace });
|
||||
expect(mockBaseClient.bulkDelete).toHaveBeenCalledTimes(1);
|
||||
expect(mockBaseClient.bulkDelete).toHaveBeenCalledWith([obj1, obj2], { namespace });
|
||||
});
|
||||
|
||||
it('redirects request to underlying base client if type is registered', async () => {
|
||||
const knownObj1 = Object.freeze({ type: 'known-type', id: 'known-type-id-1' });
|
||||
const knownObj2 = Object.freeze({ type: 'known-type', id: 'known-type-id-2' });
|
||||
const options = { namespace: 'some-ns' };
|
||||
|
||||
await wrapper.bulkDelete([knownObj1, knownObj2], options);
|
||||
|
||||
expect(mockBaseClient.bulkDelete).toHaveBeenCalledTimes(1);
|
||||
expect(mockBaseClient.bulkDelete).toHaveBeenCalledWith([knownObj1, knownObj2], { namespace });
|
||||
});
|
||||
|
||||
it('fails if base client fails', async () => {
|
||||
const failureReason = new Error('Something bad happened...');
|
||||
mockBaseClient.bulkDelete.mockRejectedValue(failureReason);
|
||||
|
||||
await expect(wrapper.bulkDelete([{ type: 'known-type', id: 'some-id' }])).rejects.toThrowError(
|
||||
failureReason
|
||||
);
|
||||
|
||||
expect(mockBaseClient.bulkDelete).toHaveBeenCalledTimes(1);
|
||||
expect(mockBaseClient.bulkDelete).toHaveBeenCalledWith(
|
||||
[{ type: 'known-type', id: 'some-id' }],
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#find', () => {
|
||||
it('redirects request to underlying base client and does not alter response if type is not registered', async () => {
|
||||
const mockedResponse = {
|
||||
|
|
|
@ -10,6 +10,8 @@ import type {
|
|||
SavedObject,
|
||||
SavedObjectsBaseOptions,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkGetObject,
|
||||
SavedObjectsBulkResolveObject,
|
||||
SavedObjectsBulkResponse,
|
||||
|
@ -166,6 +168,13 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
|||
return await this.options.baseClient.delete(type, id, options);
|
||||
}
|
||||
|
||||
public async bulkDelete(
|
||||
objects: SavedObjectsBulkDeleteObject[],
|
||||
options?: SavedObjectsBulkDeleteOptions
|
||||
) {
|
||||
return await this.options.baseClient.bulkDelete(objects, options);
|
||||
}
|
||||
|
||||
public async find<T, A>(options: SavedObjectsFindOptions) {
|
||||
return await this.handleEncryptedAttributesInBulkResponse(
|
||||
await this.options.baseClient.find<T, A>(options),
|
||||
|
|
|
@ -24,6 +24,7 @@ const writeOperations: string[] = [
|
|||
'update',
|
||||
'bulk_update',
|
||||
'delete',
|
||||
'bulk_delete',
|
||||
'share_to_space',
|
||||
];
|
||||
const allOperations: string[] = [...readOperations, ...writeOperations];
|
||||
|
|
|
@ -109,6 +109,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('all-savedObject-all-1', 'update'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'bulk_update'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'delete'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'bulk_delete'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'share_to_space'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'bulk_get'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'get'),
|
||||
|
@ -120,6 +121,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('all-savedObject-all-2', 'update'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'bulk_update'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'delete'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'bulk_delete'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'share_to_space'),
|
||||
actions.savedObject.get('all-savedObject-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('all-savedObject-read-1', 'get'),
|
||||
|
@ -148,6 +150,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-1', 'update'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'bulk_update'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'delete'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'bulk_delete'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'share_to_space'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'get'),
|
||||
|
@ -159,6 +162,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-2', 'update'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_update'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'delete'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_delete'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'share_to_space'),
|
||||
actions.savedObject.get('read-savedObject-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-read-1', 'get'),
|
||||
|
@ -301,6 +305,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('all-savedObject-all-1', 'update'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'bulk_update'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'delete'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'bulk_delete'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'share_to_space'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'bulk_get'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'get'),
|
||||
|
@ -312,6 +317,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('all-savedObject-all-2', 'update'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'bulk_update'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'delete'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'bulk_delete'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'share_to_space'),
|
||||
actions.savedObject.get('all-savedObject-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('all-savedObject-read-1', 'get'),
|
||||
|
@ -339,6 +345,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-1', 'update'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'bulk_update'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'delete'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'bulk_delete'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'share_to_space'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'get'),
|
||||
|
@ -350,6 +357,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-2', 'update'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_update'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'delete'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_delete'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'share_to_space'),
|
||||
actions.savedObject.get('read-savedObject-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-read-1', 'get'),
|
||||
|
@ -427,6 +435,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-1', 'update'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'bulk_update'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'delete'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'bulk_delete'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'share_to_space'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'get'),
|
||||
|
@ -438,6 +447,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-2', 'update'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_update'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'delete'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_delete'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'share_to_space'),
|
||||
actions.savedObject.get('read-savedObject-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-read-1', 'get'),
|
||||
|
@ -732,6 +742,7 @@ describe('reserved', () => {
|
|||
actions.savedObject.get('savedObject-all-1', 'update'),
|
||||
actions.savedObject.get('savedObject-all-1', 'bulk_update'),
|
||||
actions.savedObject.get('savedObject-all-1', 'delete'),
|
||||
actions.savedObject.get('savedObject-all-1', 'bulk_delete'),
|
||||
actions.savedObject.get('savedObject-all-1', 'share_to_space'),
|
||||
actions.savedObject.get('savedObject-all-2', 'bulk_get'),
|
||||
actions.savedObject.get('savedObject-all-2', 'get'),
|
||||
|
@ -743,6 +754,7 @@ describe('reserved', () => {
|
|||
actions.savedObject.get('savedObject-all-2', 'update'),
|
||||
actions.savedObject.get('savedObject-all-2', 'bulk_update'),
|
||||
actions.savedObject.get('savedObject-all-2', 'delete'),
|
||||
actions.savedObject.get('savedObject-all-2', 'bulk_delete'),
|
||||
actions.savedObject.get('savedObject-all-2', 'share_to_space'),
|
||||
actions.savedObject.get('savedObject-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('savedObject-read-1', 'get'),
|
||||
|
@ -862,6 +874,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -993,6 +1006,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1015,6 +1029,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1044,6 +1059,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1081,6 +1097,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1104,6 +1121,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1127,6 +1145,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1149,6 +1168,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1227,6 +1247,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1249,6 +1270,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1278,6 +1300,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1384,6 +1407,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1406,6 +1430,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1455,6 +1480,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1484,6 +1510,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1567,6 +1594,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1589,6 +1617,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1709,6 +1738,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1738,6 +1768,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1775,6 +1806,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1798,6 +1830,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1821,6 +1854,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1843,6 +1877,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1941,6 +1976,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -1970,6 +2006,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2007,6 +2044,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2030,6 +2068,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2053,6 +2092,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2075,6 +2115,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2173,6 +2214,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
|
||||
|
@ -2184,6 +2226,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2219,6 +2262,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
|
||||
|
@ -2230,6 +2274,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2273,6 +2318,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
|
||||
|
@ -2284,6 +2330,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2313,6 +2360,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
|
||||
|
@ -2324,6 +2372,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2353,6 +2402,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
|
||||
|
@ -2364,6 +2414,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
@ -2392,6 +2443,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'get'),
|
||||
|
@ -2403,6 +2455,7 @@ describe('subFeatures', () => {
|
|||
actions.savedObject.get('all-licensed-sub-feature-type', 'update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_delete'),
|
||||
actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'bulk_get'),
|
||||
actions.savedObject.get('read-sub-feature-type', 'get'),
|
||||
|
|
|
@ -591,6 +591,67 @@ describe('#bulkUpdate', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#bulkDelete', () => {
|
||||
const obj1 = Object.freeze({ type: 'foo', id: 'foo-id' });
|
||||
const obj2 = Object.freeze({ type: 'bar', id: 'bar-id' });
|
||||
const namespace = 'some-ns';
|
||||
|
||||
test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
|
||||
const objects = [obj1];
|
||||
await expectGeneralError(client.bulkDelete, { objects });
|
||||
});
|
||||
|
||||
test(`throws decorated ForbiddenError when unauthorized`, async () => {
|
||||
const objects = [obj1, obj2];
|
||||
const options = { namespace };
|
||||
await expectForbiddenError(client.bulkDelete, { objects, options });
|
||||
});
|
||||
|
||||
test(`returns result of baseClient.bulkDelete when authorized`, async () => {
|
||||
const apiCallReturnValue = {
|
||||
statuses: [obj1, obj2].map((obj) => {
|
||||
return { ...obj, success: true };
|
||||
}),
|
||||
};
|
||||
clientOpts.baseClient.bulkDelete.mockReturnValue(apiCallReturnValue as any);
|
||||
|
||||
const objects = [obj1, obj2];
|
||||
const options = { namespace };
|
||||
const result = await expectSuccess(client.bulkDelete, { objects, options });
|
||||
expect(result).toEqual(apiCallReturnValue);
|
||||
});
|
||||
|
||||
test(`checks privileges for user, actions, and namespace`, async () => {
|
||||
const objects = [obj1, obj2];
|
||||
const options = { namespace };
|
||||
await expectPrivilegeCheck(client.bulkDelete, { objects, options }, namespace);
|
||||
});
|
||||
|
||||
test(`adds audit event when successful`, async () => {
|
||||
const apiCallReturnValue = {
|
||||
statuses: [obj1, obj2].map((obj) => {
|
||||
return { ...obj, success: true };
|
||||
}),
|
||||
};
|
||||
clientOpts.baseClient.bulkDelete.mockReturnValue(apiCallReturnValue as any);
|
||||
|
||||
const objects = [obj1, obj2];
|
||||
const options = { namespace };
|
||||
await expectSuccess(client.bulkDelete, { objects, options });
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expectAuditEvent('saved_object_delete', 'success', { type: obj1.type, id: obj1.id });
|
||||
expectAuditEvent('saved_object_delete', 'success', { type: obj2.type, id: obj2.id });
|
||||
});
|
||||
|
||||
test(`adds audit event when not successful`, async () => {
|
||||
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error());
|
||||
await expect(() => client.bulkDelete([obj1, obj2], { namespace })).rejects.toThrow();
|
||||
expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expectAuditEvent('saved_object_delete', 'failure', { type: obj1.type, id: obj1.id });
|
||||
expectAuditEvent('saved_object_delete', 'failure', { type: obj2.type, id: obj2.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#checkConflicts', () => {
|
||||
const obj1 = Object.freeze({ type: 'foo', id: 'foo-id' });
|
||||
const obj2 = Object.freeze({ type: 'bar', id: 'bar-id' });
|
||||
|
|
|
@ -9,6 +9,9 @@ import type {
|
|||
SavedObjectReferenceWithContext,
|
||||
SavedObjectsBaseOptions,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkDeleteResponse,
|
||||
SavedObjectsBulkGetObject,
|
||||
SavedObjectsBulkResolveObject,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
|
@ -224,6 +227,48 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
|
|||
return await this.baseClient.delete(type, id, options);
|
||||
}
|
||||
|
||||
public async bulkDelete(
|
||||
objects: SavedObjectsBulkDeleteObject[],
|
||||
options: SavedObjectsBulkDeleteOptions
|
||||
): Promise<SavedObjectsBulkDeleteResponse> {
|
||||
try {
|
||||
const args = { objects, options };
|
||||
await this.legacyEnsureAuthorized(
|
||||
this.getUniqueObjectTypes(objects),
|
||||
'bulk_delete',
|
||||
options?.namespace,
|
||||
{
|
||||
args,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
objects.forEach(({ type, id }) =>
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.DELETE,
|
||||
savedObject: { type, id },
|
||||
error,
|
||||
})
|
||||
)
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
const response = await this.baseClient.bulkDelete(objects, options);
|
||||
response?.statuses.forEach(({ id, type, success, error }) => {
|
||||
const auditEventOutcome = success === true ? 'success' : 'failure';
|
||||
const auditEventOutcomeError = error ? (error as unknown as Error) : undefined;
|
||||
this.auditLogger.log(
|
||||
savedObjectEvent({
|
||||
action: SavedObjectAction.DELETE,
|
||||
savedObject: { type, id },
|
||||
outcome: auditEventOutcome,
|
||||
error: auditEventOutcomeError,
|
||||
})
|
||||
);
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
public async find<T = unknown, A = unknown>(options: SavedObjectsFindOptions) {
|
||||
if (
|
||||
this.getSpacesService() == null &&
|
||||
|
|
|
@ -586,6 +586,41 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
});
|
||||
});
|
||||
|
||||
describe('#bulkDelete', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
||||
await expect(
|
||||
// @ts-expect-error
|
||||
client.bulkDelete(null, { namespace: 'bar' })
|
||||
).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED);
|
||||
});
|
||||
|
||||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = { statuses: [{ id: 'id', type: 'type', success: true }] };
|
||||
baseClient.bulkDelete.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
|
||||
const actualReturnValue = await client.bulkDelete([{ id: 'id', type: 'foo' }], {
|
||||
force: true,
|
||||
});
|
||||
|
||||
expect(actualReturnValue).toBe(expectedReturnValue);
|
||||
expect(baseClient.bulkDelete).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
id: 'id',
|
||||
type: 'foo',
|
||||
},
|
||||
],
|
||||
{
|
||||
namespace: currentSpace.expectedNamespace,
|
||||
force: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeReferencesTo', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const { client } = createSpacesSavedObjectsClient();
|
||||
|
|
|
@ -12,6 +12,8 @@ import type {
|
|||
SavedObject,
|
||||
SavedObjectsBaseOptions,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkDeleteObject,
|
||||
SavedObjectsBulkDeleteOptions,
|
||||
SavedObjectsBulkGetObject,
|
||||
SavedObjectsBulkResolveObject,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
|
@ -139,6 +141,17 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract {
|
|||
});
|
||||
}
|
||||
|
||||
async bulkDelete<T = unknown>(
|
||||
objects: SavedObjectsBulkDeleteObject[] = [],
|
||||
options: SavedObjectsBulkDeleteOptions = {}
|
||||
) {
|
||||
throwErrorIfNamespaceSpecified(options);
|
||||
return await this.client.bulkDelete(objects, {
|
||||
...options,
|
||||
namespace: spaceIdToNamespace(this.spaceId),
|
||||
});
|
||||
}
|
||||
|
||||
async find<T = unknown, A = unknown>(options: SavedObjectsFindOptions) {
|
||||
let namespaces: string[];
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SuperTest } from 'supertest';
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import expect from '@kbn/expect';
|
||||
import type { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
|
||||
import { SPACES } from '../lib/spaces';
|
||||
import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils';
|
||||
import { ExpectResponseBody, TestCase, TestDefinition, TestSuite, TestUser } from '../lib/types';
|
||||
|
||||
export interface BulkDeleteTestDefinition extends TestDefinition {
|
||||
request: { type: string; id: string; force?: boolean };
|
||||
force?: boolean;
|
||||
}
|
||||
export type BulkDeleteTestSuite = TestSuite<BulkDeleteTestDefinition>;
|
||||
|
||||
export interface BulkDeleteTestCase extends TestCase {
|
||||
force?: boolean;
|
||||
failure?: 400 | 403 | 404;
|
||||
}
|
||||
|
||||
const ALIAS_DELETE_INCLUSIVE = Object.freeze({
|
||||
type: 'resolvetype',
|
||||
id: 'alias-match-newid',
|
||||
}); // exists in three specific spaces; deleting this should also delete the aliases that target it in the default space and space_1
|
||||
const ALIAS_DELETE_EXCLUSIVE = Object.freeze({
|
||||
type: 'resolvetype',
|
||||
id: 'all_spaces',
|
||||
}); // exists in all spaces; deleting this should also delete the aliases that target it in the default space and space_1
|
||||
const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' });
|
||||
export const TEST_CASES: Record<string, BulkDeleteTestCase> = Object.freeze({
|
||||
...CASES,
|
||||
ALIAS_DELETE_INCLUSIVE,
|
||||
ALIAS_DELETE_EXCLUSIVE,
|
||||
DOES_NOT_EXIST,
|
||||
});
|
||||
|
||||
/**
|
||||
* Test cases have additional properties that we don't want to send in HTTP Requests
|
||||
*/
|
||||
const createRequest = ({ type, id, force }: BulkDeleteTestCase) => ({ type, id, force });
|
||||
|
||||
export function bulkDeleteTestSuiteFactory(es: Client, esArchiver: any, supertest: SuperTest<any>) {
|
||||
const expectSavedObjectForbidden = expectResponses.forbiddenTypes('bulk_delete');
|
||||
const expectResponseBody =
|
||||
(testCase: BulkDeleteTestCase, statusCode: 200 | 403, user?: TestUser): ExpectResponseBody =>
|
||||
async (response: Record<string, any>) => {
|
||||
if (statusCode === 403) {
|
||||
await expectSavedObjectForbidden(testCase.type)(response);
|
||||
} else {
|
||||
// permitted
|
||||
const statuses = response.body.statuses;
|
||||
expect(statuses).length([testCase].length);
|
||||
for (let i = 0; i < statuses.length; i++) {
|
||||
const object = statuses[i];
|
||||
expect(object).to.have.keys(['id', 'type', 'success']);
|
||||
if (testCase.failure) {
|
||||
const { type, id } = testCase;
|
||||
expect(object.type).to.eql(type);
|
||||
expect(object.id).to.eql(id);
|
||||
await expectResponses.permitted(object, testCase);
|
||||
} else {
|
||||
await es.indices.refresh({ index: '.kibana' }); // alias deletion uses refresh: false, so we need to manually refresh the index before searching
|
||||
const searchResponse = await es.search({
|
||||
index: '.kibana',
|
||||
body: {
|
||||
size: 0,
|
||||
query: { terms: { type: ['legacy-url-alias'] } },
|
||||
track_total_hits: true,
|
||||
},
|
||||
});
|
||||
|
||||
const expectAliasWasDeleted = !![ALIAS_DELETE_INCLUSIVE, ALIAS_DELETE_EXCLUSIVE].find(
|
||||
({ type, id }) => testCase.type === type && testCase.id === id
|
||||
);
|
||||
// Eight aliases exist and they are all deleted in the bulk operation.
|
||||
// The delete behavior for multinamespace objects shared to more than one space when using force is to delete the object from all the spaces it is shared to.
|
||||
expect((searchResponse.hits.total as SearchTotalHits).value).to.eql(
|
||||
expectAliasWasDeleted ? 6 : 8
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createTestDefinitions = (
|
||||
testCases: BulkDeleteTestCase | BulkDeleteTestCase[],
|
||||
forbidden: boolean,
|
||||
options?: {
|
||||
spaceId?: string;
|
||||
responseBodyOverride?: ExpectResponseBody;
|
||||
}
|
||||
): BulkDeleteTestDefinition[] => {
|
||||
let cases = Array.isArray(testCases) ? testCases : [testCases];
|
||||
const responseStatusCode = forbidden ? 403 : 200;
|
||||
if (forbidden) {
|
||||
// override the expected result in each test case
|
||||
cases = cases.map((x) => ({ ...x, failure: 403 }));
|
||||
}
|
||||
return cases.map((x) => ({
|
||||
title: getTestTitle(x, responseStatusCode),
|
||||
responseStatusCode,
|
||||
request: createRequest(x),
|
||||
responseBody: options?.responseBodyOverride || expectResponseBody(x, responseStatusCode),
|
||||
}));
|
||||
};
|
||||
|
||||
const makeBulkDeleteTest =
|
||||
(describeFn: Mocha.SuiteFunction) => (description: string, definition: BulkDeleteTestSuite) => {
|
||||
const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition;
|
||||
|
||||
describeFn(description, () => {
|
||||
before(() =>
|
||||
esArchiver.load(
|
||||
'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
|
||||
)
|
||||
);
|
||||
after(() =>
|
||||
esArchiver.unload(
|
||||
'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
|
||||
)
|
||||
);
|
||||
|
||||
for (const test of tests) {
|
||||
it(`should return ${test.responseStatusCode} ${test.title} `, async () => {
|
||||
const { type: testType, id: testId, force: testForce } = test.request;
|
||||
const requestBody = [{ type: testType, id: testId }];
|
||||
const query = testForce && testForce === true ? '?force=true' : '';
|
||||
await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_delete${query}`)
|
||||
.auth(user?.username, user?.password)
|
||||
.send(requestBody)
|
||||
.expect(test.responseStatusCode)
|
||||
.then(test.responseBody);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addTests = makeBulkDeleteTest(describe);
|
||||
// @ts-ignore
|
||||
addTests.only = makeBulkDeleteTest(describe.only);
|
||||
|
||||
return {
|
||||
addTests,
|
||||
createTestDefinitions,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SPACES } from '../../common/lib/spaces';
|
||||
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
|
||||
import { TestUser } from '../../common/lib/types';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
bulkDeleteTestSuiteFactory,
|
||||
TEST_CASES as CASES,
|
||||
BulkDeleteTestDefinition,
|
||||
} from '../../common/suites/bulk_delete';
|
||||
|
||||
const {
|
||||
DEFAULT: { spaceId: DEFAULT_SPACE_ID },
|
||||
SPACE_1: { spaceId: SPACE_1_ID },
|
||||
SPACE_2: { spaceId: SPACE_2_ID },
|
||||
} = SPACES;
|
||||
const { fail400, fail404 } = testCaseFailures;
|
||||
|
||||
const createTestCases = (spaceId: string) => {
|
||||
// for each permitted (non-403) outcome, if failure !== undefined then we expect
|
||||
// to receive an error; otherwise, we expect to receive a success result
|
||||
const normalTypes = [
|
||||
{ ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
|
||||
{ ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) },
|
||||
{ ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) },
|
||||
{ ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail400() },
|
||||
// try to delete this object again, this time using the `force` option
|
||||
{ ...CASES.MULTI_NAMESPACE_ALL_SPACES, force: true },
|
||||
{
|
||||
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
|
||||
...fail400(spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID),
|
||||
...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
|
||||
},
|
||||
// try to delete this object again, this time using the `force` option
|
||||
{
|
||||
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
|
||||
force: true,
|
||||
...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
|
||||
},
|
||||
{ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) },
|
||||
{ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) },
|
||||
{
|
||||
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE,
|
||||
...fail404(spaceId !== DEFAULT_SPACE_ID),
|
||||
},
|
||||
{ ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) },
|
||||
CASES.NAMESPACE_AGNOSTIC,
|
||||
{ ...CASES.ALIAS_DELETE_INCLUSIVE, force: true },
|
||||
{ ...CASES.ALIAS_DELETE_EXCLUSIVE, force: true },
|
||||
{ ...CASES.DOES_NOT_EXIST, ...fail404() },
|
||||
];
|
||||
const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; // this behavior diverges from `delete`, which throws 404
|
||||
const allTypes = normalTypes.concat(hiddenType);
|
||||
return { normalTypes, hiddenType, allTypes };
|
||||
};
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const { addTests, createTestDefinitions } = bulkDeleteTestSuiteFactory(es, esArchiver, supertest);
|
||||
const createTests = (spaceId: string) => {
|
||||
const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId);
|
||||
return {
|
||||
unauthorized: createTestDefinitions(allTypes, true, { spaceId }),
|
||||
authorized: [
|
||||
createTestDefinitions(normalTypes, false, { spaceId }),
|
||||
createTestDefinitions(hiddenType, true, { spaceId }),
|
||||
].flat(),
|
||||
superuser: createTestDefinitions(allTypes, false, { spaceId }),
|
||||
};
|
||||
};
|
||||
|
||||
describe('_bulk_delete', () => {
|
||||
getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => {
|
||||
const suffix = ` within the ${spaceId} space`;
|
||||
const { unauthorized, authorized, superuser } = createTests(spaceId);
|
||||
const _addTests = (user: TestUser, tests: BulkDeleteTestDefinition[]) => {
|
||||
addTests(`${user.description}${suffix}`, { user, spaceId, tests });
|
||||
};
|
||||
|
||||
[
|
||||
users.noAccess,
|
||||
users.legacyAll,
|
||||
users.dualRead,
|
||||
users.readGlobally,
|
||||
users.readAtSpace,
|
||||
users.allAtOtherSpace,
|
||||
].forEach((user) => {
|
||||
_addTests(user, unauthorized);
|
||||
});
|
||||
[users.dualAll, users.allGlobally, users.allAtSpace].forEach((user) => {
|
||||
_addTests(user, authorized);
|
||||
});
|
||||
_addTests(users.superuser, superuser);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -21,6 +21,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./bulk_get'));
|
||||
loadTestFile(require.resolve('./bulk_update'));
|
||||
loadTestFile(require.resolve('./bulk_resolve'));
|
||||
loadTestFile(require.resolve('./bulk_delete'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./export'));
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SPACES } from '../../common/lib/spaces';
|
||||
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { bulkDeleteTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_delete';
|
||||
|
||||
const {
|
||||
DEFAULT: { spaceId: DEFAULT_SPACE_ID },
|
||||
SPACE_1: { spaceId: SPACE_1_ID },
|
||||
SPACE_2: { spaceId: SPACE_2_ID },
|
||||
} = SPACES;
|
||||
const { fail400, fail404 } = testCaseFailures;
|
||||
|
||||
const createTestCases = (spaceId: string) => [
|
||||
{ ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
|
||||
{ ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) },
|
||||
{ ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) },
|
||||
{ ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail400() },
|
||||
// // try to delete this object again, this time using the `force` option
|
||||
{ ...CASES.MULTI_NAMESPACE_ALL_SPACES, force: true },
|
||||
{
|
||||
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
|
||||
...fail400(spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID),
|
||||
...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
|
||||
},
|
||||
// try to delete this object again, this time using the `force` option
|
||||
{
|
||||
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
|
||||
force: true,
|
||||
},
|
||||
{ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) },
|
||||
{ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) },
|
||||
{
|
||||
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE,
|
||||
...fail404(spaceId !== DEFAULT_SPACE_ID),
|
||||
},
|
||||
{ ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) },
|
||||
CASES.NAMESPACE_AGNOSTIC,
|
||||
{ ...CASES.ALIAS_DELETE_INCLUSIVE, force: true },
|
||||
{ ...CASES.ALIAS_DELETE_EXCLUSIVE, force: true },
|
||||
{ ...CASES.HIDDEN, ...fail400() },
|
||||
{ ...CASES.DOES_NOT_EXIST, ...fail404() },
|
||||
];
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const { addTests, createTestDefinitions } = bulkDeleteTestSuiteFactory(es, esArchiver, supertest);
|
||||
const createTests = (spaceId: string) => {
|
||||
const testCases = createTestCases(spaceId);
|
||||
return createTestDefinitions(testCases, false, { spaceId });
|
||||
};
|
||||
|
||||
describe('_bulk_delete', () => {
|
||||
getTestScenarios().spaces.forEach(({ spaceId }) => {
|
||||
const tests = createTests(spaceId);
|
||||
addTests(`within the ${spaceId} space`, { spaceId, tests });
|
||||
});
|
||||
});
|
||||
}
|
|
@ -15,6 +15,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./bulk_update'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./bulk_delete'));
|
||||
loadTestFile(require.resolve('./export'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue