mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Saved Objects] Add support for bulkUpdate to SavedObjectsClient (#47540)
This PR adds support for `bulkUpdate` to the Saved Objects API and exposes it on all Saved Objects clients (base client, encrypted, spaces etc.).
This commit is contained in:
parent
4956a762cd
commit
3f4024c398
76 changed files with 3245 additions and 300 deletions
|
@ -79,6 +79,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) | |
|
||||
| [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) | |
|
||||
| [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) | |
|
||||
| [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) | |
|
||||
| [SavedObjectsBulkUpdateOptions](./kibana-plugin-public.savedobjectsbulkupdateoptions.md) | |
|
||||
| [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | |
|
||||
| [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | |
|
||||
| [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects <code>find()</code> method.<!-- -->\*Note\*: this type is different between the Public and Server Saved Objects clients. |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) > [attributes](./kibana-plugin-public.savedobjectsbulkupdateobject.attributes.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject.attributes property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
attributes: T;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) > [id](./kibana-plugin-public.savedobjectsbulkupdateobject.id.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject.id property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
id: string;
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsBulkUpdateObject<T extends SavedObjectAttributes = SavedObjectAttributes>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [attributes](./kibana-plugin-public.savedobjectsbulkupdateobject.attributes.md) | <code>T</code> | |
|
||||
| [id](./kibana-plugin-public.savedobjectsbulkupdateobject.id.md) | <code>string</code> | |
|
||||
| [references](./kibana-plugin-public.savedobjectsbulkupdateobject.references.md) | <code>SavedObjectReference[]</code> | |
|
||||
| [type](./kibana-plugin-public.savedobjectsbulkupdateobject.type.md) | <code>string</code> | |
|
||||
| [version](./kibana-plugin-public.savedobjectsbulkupdateobject.version.md) | <code>string</code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) > [references](./kibana-plugin-public.savedobjectsbulkupdateobject.references.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject.references property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
references?: SavedObjectReference[];
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) > [type](./kibana-plugin-public.savedobjectsbulkupdateobject.type.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject.type property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: string;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-public.savedobjectsbulkupdateobject.md) > [version](./kibana-plugin-public.savedobjectsbulkupdateobject.version.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject.version property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
version?: string;
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkUpdateOptions](./kibana-plugin-public.savedobjectsbulkupdateoptions.md)
|
||||
|
||||
## SavedObjectsBulkUpdateOptions interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsBulkUpdateOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [namespace](./kibana-plugin-public.savedobjectsbulkupdateoptions.namespace.md) | <code>string</code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkUpdateOptions](./kibana-plugin-public.savedobjectsbulkupdateoptions.md) > [namespace](./kibana-plugin-public.savedobjectsbulkupdateoptions.namespace.md)
|
||||
|
||||
## SavedObjectsBulkUpdateOptions.namespace property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
namespace?: string;
|
||||
```
|
|
@ -0,0 +1,26 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [bulkUpdate](./kibana-plugin-public.savedobjectsclient.bulkupdate.md)
|
||||
|
||||
## SavedObjectsClient.bulkUpdate() method
|
||||
|
||||
Update multiple documents at once
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
bulkUpdate<T extends SavedObjectAttributes>(objects?: SavedObjectsBulkUpdateObject[]): Promise<SavedObjectsBatchResponse<SavedObjectAttributes>>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| objects | <code>SavedObjectsBulkUpdateObject[]</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<SavedObjectsBatchResponse<SavedObjectAttributes>>`
|
||||
|
||||
The result of the update operation containing both failed and updated saved objects.
|
||||
|
|
@ -27,6 +27,7 @@ export declare class SavedObjectsClient
|
|||
|
||||
| Method | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [bulkUpdate(objects)](./kibana-plugin-public.savedobjectsclient.bulkupdate.md) | | Update multiple documents at once |
|
||||
| [update(type, id, attributes, { version, migrationVersion, references })](./kibana-plugin-public.savedobjectsclient.update.md) | | Updates an object |
|
||||
|
||||
## Remarks
|
||||
|
|
|
@ -89,6 +89,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) | |
|
||||
| [SavedObjectsBulkGetObject](./kibana-plugin-server.savedobjectsbulkgetobject.md) | |
|
||||
| [SavedObjectsBulkResponse](./kibana-plugin-server.savedobjectsbulkresponse.md) | |
|
||||
| [SavedObjectsBulkUpdateObject](./kibana-plugin-server.savedobjectsbulkupdateobject.md) | |
|
||||
| [SavedObjectsBulkUpdateResponse](./kibana-plugin-server.savedobjectsbulkupdateresponse.md) | |
|
||||
| [SavedObjectsClientProviderOptions](./kibana-plugin-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. |
|
||||
| [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. |
|
||||
| [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-server.savedobjectsbulkupdateobject.md) > [attributes](./kibana-plugin-server.savedobjectsbulkupdateobject.attributes.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject.attributes property
|
||||
|
||||
The data for a Saved Object is stored as an object in the `attributes` property.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
attributes: Partial<T>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-server.savedobjectsbulkupdateobject.md) > [id](./kibana-plugin-server.savedobjectsbulkupdateobject.id.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject.id property
|
||||
|
||||
The ID of this Saved Object, guaranteed to be unique for all objects of the same `type`
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
id: string;
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-server.savedobjectsbulkupdateobject.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsBulkUpdateObject<T extends SavedObjectAttributes = any> extends Pick<SavedObjectsUpdateOptions, 'version' | 'references'>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [attributes](./kibana-plugin-server.savedobjectsbulkupdateobject.attributes.md) | <code>Partial<T></code> | The data for a Saved Object is stored as an object in the <code>attributes</code> property. |
|
||||
| [id](./kibana-plugin-server.savedobjectsbulkupdateobject.id.md) | <code>string</code> | The ID of this Saved Object, guaranteed to be unique for all objects of the same <code>type</code> |
|
||||
| [type](./kibana-plugin-server.savedobjectsbulkupdateobject.type.md) | <code>string</code> | The type of this Saved Object. Each plugin can define it's own custom Saved Object types. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-server.savedobjectsbulkupdateobject.md) > [type](./kibana-plugin-server.savedobjectsbulkupdateobject.type.md)
|
||||
|
||||
## SavedObjectsBulkUpdateObject.type property
|
||||
|
||||
The type of this Saved Object. Each plugin can define it's own custom Saved Object types.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
type: string;
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkUpdateResponse](./kibana-plugin-server.savedobjectsbulkupdateresponse.md)
|
||||
|
||||
## SavedObjectsBulkUpdateResponse interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsBulkUpdateResponse<T extends SavedObjectAttributes = any>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [saved\_objects](./kibana-plugin-server.savedobjectsbulkupdateresponse.saved_objects.md) | <code>Array<SavedObjectsUpdateResponse<T>></code> | |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkUpdateResponse](./kibana-plugin-server.savedobjectsbulkupdateresponse.md) > [saved\_objects](./kibana-plugin-server.savedobjectsbulkupdateresponse.saved_objects.md)
|
||||
|
||||
## SavedObjectsBulkUpdateResponse.saved\_objects property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
saved_objects: Array<SavedObjectsUpdateResponse<T>>;
|
||||
```
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [bulkUpdate](./kibana-plugin-server.savedobjectsclient.bulkupdate.md)
|
||||
|
||||
## SavedObjectsClient.bulkUpdate() method
|
||||
|
||||
Bulk Updates multiple SavedObject at once
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
bulkUpdate<T extends SavedObjectAttributes = any>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| objects | <code>Array<SavedObjectsBulkUpdateObject<T>></code> | |
|
||||
| options | <code>SavedObjectsBaseOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<SavedObjectsBulkUpdateResponse<T>>`
|
||||
|
|
@ -30,6 +30,7 @@ export declare class SavedObjectsClient
|
|||
| --- | --- | --- |
|
||||
| [bulkCreate(objects, options)](./kibana-plugin-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request |
|
||||
| [bulkGet(objects, options)](./kibana-plugin-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id |
|
||||
| [bulkUpdate(objects, options)](./kibana-plugin-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once |
|
||||
| [create(type, attributes, options)](./kibana-plugin-server.savedobjectsclient.create.md) | | Persists a SavedObject |
|
||||
| [delete(type, id, options)](./kibana-plugin-server.savedobjectsclient.delete.md) | | Deletes a SavedObject |
|
||||
| [find(options)](./kibana-plugin-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query |
|
||||
|
|
|
@ -15,6 +15,6 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [references](./kibana-plugin-server.savedobjectsupdateoptions.references.md) | <code>SavedObjectReference[]</code> | |
|
||||
| [version](./kibana-plugin-server.savedobjectsupdateoptions.version.md) | <code>string</code> | Ensures version matches that of persisted object |
|
||||
| [references](./kibana-plugin-server.savedobjectsupdateoptions.references.md) | <code>SavedObjectReference[]</code> | A reference to another saved object. |
|
||||
| [version](./kibana-plugin-server.savedobjectsupdateoptions.version.md) | <code>string</code> | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. |
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
## SavedObjectsUpdateOptions.references property
|
||||
|
||||
A reference to another saved object.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## SavedObjectsUpdateOptions.version property
|
||||
|
||||
Ensures version matches that of persisted object
|
||||
An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -79,6 +79,8 @@ export {
|
|||
SavedObjectsBatchResponse,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkCreateOptions,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
SavedObjectsBulkUpdateOptions,
|
||||
SavedObjectsCreateOptions,
|
||||
SavedObjectsFindResponsePublic,
|
||||
SavedObjectsUpdateOptions,
|
||||
|
|
|
@ -757,6 +757,26 @@ export interface SavedObjectsBulkCreateOptions {
|
|||
overwrite?: boolean;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsBulkUpdateObject<T extends SavedObjectAttributes = SavedObjectAttributes> {
|
||||
// (undocumented)
|
||||
attributes: T;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
references?: SavedObjectReference[];
|
||||
// (undocumented)
|
||||
type: string;
|
||||
// (undocumented)
|
||||
version?: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsBulkUpdateOptions {
|
||||
// (undocumented)
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class SavedObjectsClient {
|
||||
// @internal
|
||||
|
@ -766,6 +786,7 @@ export class SavedObjectsClient {
|
|||
id: string;
|
||||
type: string;
|
||||
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>>;
|
||||
bulkUpdate<T extends SavedObjectAttributes>(objects?: SavedObjectsBulkUpdateObject[]): Promise<SavedObjectsBatchResponse<SavedObjectAttributes>>;
|
||||
create: <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>>;
|
||||
delete: (type: string, id: string) => Promise<{}>;
|
||||
find: <T extends SavedObjectAttributes>(options: Pick<SavedObjectsFindOptions, "search" | "filter" | "type" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>>;
|
||||
|
|
|
@ -21,11 +21,13 @@ export {
|
|||
SavedObjectsBatchResponse,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkCreateOptions,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
SavedObjectsClient,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsCreateOptions,
|
||||
SavedObjectsFindResponsePublic,
|
||||
SavedObjectsUpdateOptions,
|
||||
SavedObjectsBulkUpdateOptions,
|
||||
} from './saved_objects_client';
|
||||
export { SimpleSavedObject } from './simple_saved_object';
|
||||
export { SavedObjectsStart } from './saved_objects_service';
|
||||
|
|
|
@ -322,6 +322,43 @@ describe('SavedObjectsClient', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#bulk_update', () => {
|
||||
const bulkUpdateDoc = {
|
||||
id: 'AVwSwFxtcMV38qjDZoQg',
|
||||
type: 'config',
|
||||
attributes: { title: 'Example title' },
|
||||
version: 'foo',
|
||||
};
|
||||
beforeEach(() => {
|
||||
http.fetch.mockResolvedValue({ saved_objects: [bulkUpdateDoc] });
|
||||
});
|
||||
|
||||
test('resolves with array of SimpleSavedObject instances', async () => {
|
||||
const response = savedObjectsClient.bulkUpdate([bulkUpdateDoc]);
|
||||
await expect(response).resolves.toHaveProperty('savedObjects');
|
||||
|
||||
const result = await response;
|
||||
expect(result.savedObjects).toHaveLength(1);
|
||||
expect(result.savedObjects[0]).toBeInstanceOf(SimpleSavedObject);
|
||||
});
|
||||
|
||||
test('makes HTTP call', async () => {
|
||||
await savedObjectsClient.bulkUpdate([bulkUpdateDoc]);
|
||||
expect(http.fetch.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/api/saved_objects/_bulk_update",
|
||||
Object {
|
||||
"body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\"}]",
|
||||
"method": "PUT",
|
||||
"query": undefined,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#find', () => {
|
||||
const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' };
|
||||
|
||||
|
@ -419,15 +456,15 @@ describe('SavedObjectsClient', () => {
|
|||
};
|
||||
http.fetch.mockRejectedValue(err);
|
||||
return expect(savedObjectsClient.get(doc.type, doc.id)).rejects.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": "response body",
|
||||
"res": Object {
|
||||
"ok": false,
|
||||
"redirected": false,
|
||||
"status": 409,
|
||||
"statusText": "Conflict",
|
||||
},
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"body": "response body",
|
||||
"res": Object {
|
||||
"ok": false,
|
||||
"redirected": false,
|
||||
"status": 409,
|
||||
"statusText": "Conflict",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -73,6 +73,22 @@ export interface SavedObjectsBulkCreateOptions {
|
|||
overwrite?: boolean;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface SavedObjectsBulkUpdateObject<
|
||||
T extends SavedObjectAttributes = SavedObjectAttributes
|
||||
> {
|
||||
type: string;
|
||||
id: string;
|
||||
attributes: T;
|
||||
version?: string;
|
||||
references?: SavedObjectReference[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface SavedObjectsBulkUpdateOptions {
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface SavedObjectsUpdateOptions {
|
||||
version?: string;
|
||||
|
@ -411,6 +427,27 @@ export class SavedObjectsClient {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update multiple documents at once
|
||||
*
|
||||
* @param {array} objects - [{ type, id, attributes, options: { version, references } }]
|
||||
* @returns The result of the update operation containing both failed and updated saved objects.
|
||||
*/
|
||||
public bulkUpdate<T extends SavedObjectAttributes>(objects: SavedObjectsBulkUpdateObject[] = []) {
|
||||
const path = this.getPath(['_bulk_update']);
|
||||
|
||||
return this.savedObjectsFetch(path, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(objects),
|
||||
}).then(resp => {
|
||||
resp.saved_objects = resp.saved_objects.map((d: SavedObject<T>) => this.createSavedObject(d));
|
||||
return renameKeys<
|
||||
PromiseType<ReturnType<SavedObjectsApi['bulkUpdate']>>,
|
||||
SavedObjectsBatchResponse
|
||||
>({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse;
|
||||
});
|
||||
}
|
||||
|
||||
private createSavedObject<T extends SavedObjectAttributes>(
|
||||
options: SavedObject<T>
|
||||
): SimpleSavedObject<T> {
|
||||
|
|
|
@ -24,6 +24,7 @@ const createStartContractMock = () => {
|
|||
client: {
|
||||
create: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
bulkUpdate: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
find: jest.fn(),
|
||||
|
|
|
@ -138,7 +138,9 @@ export {
|
|||
export {
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkGetObject,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
SavedObjectsBulkResponse,
|
||||
SavedObjectsBulkUpdateResponse,
|
||||
SavedObjectsClient,
|
||||
SavedObjectsClientProviderOptions,
|
||||
SavedObjectsClientWrapperFactory,
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObject } from '../types';
|
||||
import { SavedObjectsClientMock } from '../../mocks';
|
||||
import { getObjectReferencesToFetch, fetchNestedDependencies } from './inject_nested_depdendencies';
|
||||
|
||||
describe('getObjectReferencesToFetch()', () => {
|
||||
|
@ -107,17 +108,8 @@ describe('getObjectReferencesToFetch()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fetchNestedDependencies', () => {
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
find: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
describe('injectNestedDependencies', () => {
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -487,6 +479,8 @@ describe('fetchNestedDependencies', () => {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
|
|
|
@ -20,7 +20,14 @@
|
|||
import { Readable } from 'stream';
|
||||
import { SavedObject } from '../types';
|
||||
import { importSavedObjects } from './import_saved_objects';
|
||||
import { SavedObjectsClientMock } from '../../mocks';
|
||||
|
||||
const emptyResponse = {
|
||||
saved_objects: [],
|
||||
total: 0,
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
};
|
||||
describe('importSavedObjects()', () => {
|
||||
const savedObjects: SavedObject[] = [
|
||||
{
|
||||
|
@ -56,16 +63,7 @@ describe('importSavedObjects()', () => {
|
|||
references: [],
|
||||
},
|
||||
];
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -101,7 +99,7 @@ describe('importSavedObjects()', () => {
|
|||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.find.mockResolvedValueOnce(emptyResponse);
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
|
@ -184,7 +182,7 @@ describe('importSavedObjects()', () => {
|
|||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.find.mockResolvedValueOnce(emptyResponse);
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
|
@ -268,7 +266,7 @@ describe('importSavedObjects()', () => {
|
|||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.find.mockResolvedValueOnce(emptyResponse);
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
|
@ -351,7 +349,7 @@ describe('importSavedObjects()', () => {
|
|||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.find.mockResolvedValueOnce(emptyResponse);
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects.map(savedObject => ({
|
||||
type: savedObject.type,
|
||||
|
@ -360,6 +358,8 @@ describe('importSavedObjects()', () => {
|
|||
statusCode: 409,
|
||||
message: 'conflict',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
})),
|
||||
});
|
||||
const result = await importSavedObjects({
|
||||
|
@ -455,6 +455,8 @@ describe('importSavedObjects()', () => {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -530,7 +532,7 @@ describe('importSavedObjects()', () => {
|
|||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.find.mockResolvedValueOnce(emptyResponse);
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
import { Readable } from 'stream';
|
||||
import { SavedObject } from '../types';
|
||||
import { resolveImportErrors } from './resolve_import_errors';
|
||||
import { SavedObjectsClientMock } from '../../mocks';
|
||||
|
||||
describe('resolveImportErrors()', () => {
|
||||
const savedObjects: SavedObject[] = [
|
||||
|
@ -62,16 +63,7 @@ describe('resolveImportErrors()', () => {
|
|||
],
|
||||
},
|
||||
];
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -316,6 +308,8 @@ describe('resolveImportErrors()', () => {
|
|||
statusCode: 409,
|
||||
message: 'conflict',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
})),
|
||||
});
|
||||
const result = await resolveImportErrors({
|
||||
|
@ -416,6 +410,8 @@ describe('resolveImportErrors()', () => {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -18,18 +18,10 @@
|
|||
*/
|
||||
|
||||
import { getNonExistingReferenceAsKeys, validateReferences } from './validate_references';
|
||||
import { SavedObjectsClientMock } from '../../mocks';
|
||||
|
||||
describe('getNonExistingReferenceAsKeys()', () => {
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -176,6 +168,8 @@ describe('getNonExistingReferenceAsKeys()', () => {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
|
@ -184,6 +178,8 @@ describe('getNonExistingReferenceAsKeys()', () => {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -226,16 +222,7 @@ describe('getNonExistingReferenceAsKeys()', () => {
|
|||
});
|
||||
|
||||
describe('validateReferences()', () => {
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -262,6 +249,8 @@ Object {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
type: 'index-pattern',
|
||||
|
@ -270,6 +259,8 @@ Object {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
type: 'index-pattern',
|
||||
|
@ -278,6 +269,8 @@ Object {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
type: 'search',
|
||||
|
@ -286,6 +279,8 @@ Object {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
|
@ -611,6 +606,8 @@ Object {
|
|||
statusCode: 400,
|
||||
message: 'Error',
|
||||
},
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import { delay } from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { SavedObjectsRepository } from './repository';
|
||||
import * as getSearchDslNS from './search_dsl/search_dsl';
|
||||
|
@ -1963,6 +1964,459 @@ describe('SavedObjectsRepository', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#bulkUpdate', () => {
|
||||
const { generateSavedObject, reset } = (() => {
|
||||
let count = 0;
|
||||
return {
|
||||
generateSavedObject(overrides) {
|
||||
count++;
|
||||
return _.merge({
|
||||
type: 'index-pattern',
|
||||
id: `logstash-${count}`,
|
||||
attributes: { title: `Testing ${count}` },
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'test',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
}, overrides);
|
||||
},
|
||||
reset() {
|
||||
count = 0;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
beforeEach(() => {
|
||||
reset();
|
||||
});
|
||||
|
||||
const mockValidResponse = objects =>
|
||||
callAdminCluster.mockReturnValue({
|
||||
items: objects.map(items => ({
|
||||
update: {
|
||||
_id: `${items.type}:${items.id}`,
|
||||
_type: '_doc',
|
||||
...mockVersionProps,
|
||||
result: 'updated',
|
||||
}
|
||||
})),
|
||||
});
|
||||
|
||||
|
||||
it('waits until migrations are complete before proceeding', async () => {
|
||||
const objects = [
|
||||
generateSavedObject(),
|
||||
generateSavedObject()
|
||||
];
|
||||
|
||||
migrator.runMigrations = jest.fn(async () =>
|
||||
expect(callAdminCluster).not.toHaveBeenCalled()
|
||||
);
|
||||
|
||||
mockValidResponse(objects);
|
||||
|
||||
await expect(
|
||||
savedObjectsRepository.bulkUpdate([
|
||||
generateSavedObject(),
|
||||
])
|
||||
).resolves.toBeDefined();
|
||||
|
||||
expect(migrator.runMigrations).toHaveReturnedTimes(1);
|
||||
});
|
||||
|
||||
it('returns current ES document, _seq_no and _primary_term encoded as version', async () => {
|
||||
const objects = [
|
||||
generateSavedObject(),
|
||||
generateSavedObject()
|
||||
];
|
||||
|
||||
mockValidResponse(objects);
|
||||
|
||||
const response = await savedObjectsRepository.bulkUpdate(objects);
|
||||
|
||||
expect(response.saved_objects[0]).toMatchObject({
|
||||
..._.pick(objects[0], 'id', 'type', 'attributes'),
|
||||
version: mockVersion,
|
||||
references: objects[0].references
|
||||
});
|
||||
expect(response.saved_objects[1]).toMatchObject({
|
||||
..._.pick(objects[1], 'id', 'type', 'attributes'),
|
||||
version: mockVersion,
|
||||
references: objects[1].references
|
||||
});
|
||||
});
|
||||
|
||||
it('handles a mix of succesfull updates and errors', async () => {
|
||||
const objects = [
|
||||
generateSavedObject(),
|
||||
{
|
||||
type: 'invalid-type',
|
||||
id: 'invalid',
|
||||
attributes: { title: 'invalid' }
|
||||
},
|
||||
generateSavedObject(),
|
||||
generateSavedObject({
|
||||
id: 'version_clash'
|
||||
}),
|
||||
];
|
||||
|
||||
callAdminCluster.mockReturnValue({
|
||||
items: objects
|
||||
// remove invalid from mocks
|
||||
.filter(item => item.id !== 'invalid')
|
||||
.map(items => {
|
||||
switch(items.id) {
|
||||
case 'version_clash':
|
||||
return ({
|
||||
update: {
|
||||
_id: `${items.type}:${items.id}`,
|
||||
_type: '_doc',
|
||||
error: {
|
||||
type: 'version_conflict_engine_exception'
|
||||
}
|
||||
}
|
||||
});
|
||||
default:
|
||||
return ({
|
||||
update: {
|
||||
_id: `${items.type}:${items.id}`,
|
||||
_type: '_doc',
|
||||
...mockVersionProps,
|
||||
result: 'updated',
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
const { saved_objects: [
|
||||
firstUpdatedObject,
|
||||
invalidType,
|
||||
secondUpdatedObject,
|
||||
versionClashObject
|
||||
] } = await savedObjectsRepository.bulkUpdate(objects);
|
||||
|
||||
expect(firstUpdatedObject).toMatchObject({
|
||||
..._.pick(objects[0], 'id', 'type', 'attributes', 'references'),
|
||||
version: mockVersion
|
||||
});
|
||||
|
||||
expect(invalidType).toMatchObject({
|
||||
..._.pick(objects[1], 'id', 'type'),
|
||||
error: SavedObjectsErrorHelpers.createGenericNotFoundError('invalid-type', 'invalid').output.payload,
|
||||
});
|
||||
|
||||
expect(secondUpdatedObject).toMatchObject({
|
||||
..._.pick(objects[2], 'id', 'type', 'attributes', 'references'),
|
||||
version: mockVersion
|
||||
});
|
||||
|
||||
expect(versionClashObject).toMatchObject({
|
||||
..._.pick(objects[3], 'id', 'type'),
|
||||
error: { statusCode: 409, message: 'version conflict, document already exists' },
|
||||
});
|
||||
});
|
||||
|
||||
it('doesnt call Elasticsearch if there are no valid objects to update', async () => {
|
||||
const objects = [
|
||||
{
|
||||
type: 'invalid-type',
|
||||
id: 'invalid',
|
||||
attributes: { title: 'invalid' }
|
||||
},
|
||||
{
|
||||
type: 'invalid-type',
|
||||
id: 'invalid 2',
|
||||
attributes: { title: 'invalid' }
|
||||
},
|
||||
];
|
||||
|
||||
const { saved_objects: [
|
||||
invalidType,
|
||||
invalidType2
|
||||
] } = await savedObjectsRepository.bulkUpdate(objects);
|
||||
|
||||
expect(callAdminCluster).not.toHaveBeenCalled();
|
||||
|
||||
expect(invalidType).toMatchObject({
|
||||
..._.pick(objects[0], 'id', 'type'),
|
||||
error: SavedObjectsErrorHelpers.createGenericNotFoundError('invalid-type', 'invalid').output.payload,
|
||||
});
|
||||
|
||||
expect(invalidType2).toMatchObject({
|
||||
..._.pick(objects[1], 'id', 'type'),
|
||||
error: SavedObjectsErrorHelpers.createGenericNotFoundError('invalid-type', 'invalid 2').output.payload,
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts version', async () => {
|
||||
const objects = [
|
||||
generateSavedObject({
|
||||
version: encodeHitVersion({
|
||||
_seq_no: 100,
|
||||
_primary_term: 200,
|
||||
}),
|
||||
}),
|
||||
generateSavedObject({
|
||||
version: encodeHitVersion({
|
||||
_seq_no: 300,
|
||||
_primary_term: 400,
|
||||
}),
|
||||
})
|
||||
];
|
||||
|
||||
mockValidResponse(objects);
|
||||
|
||||
await savedObjectsRepository.bulkUpdate(objects);
|
||||
|
||||
expect(callAdminCluster).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [, { body: [{ update: firstUpdate },, { update: secondUpdate }] }] = callAdminCluster.mock.calls[0];
|
||||
|
||||
expect(firstUpdate).toMatchObject({
|
||||
if_seq_no: 100,
|
||||
if_primary_term: 200,
|
||||
});
|
||||
|
||||
expect(secondUpdate).toMatchObject({
|
||||
if_seq_no: 300,
|
||||
if_primary_term: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not pass references if omitted', async () => {
|
||||
const objects = [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: `logstash-no-ref`,
|
||||
attributes: { title: `Testing no-ref` }
|
||||
}
|
||||
];
|
||||
|
||||
mockValidResponse(objects);
|
||||
|
||||
await savedObjectsRepository.bulkUpdate(objects);
|
||||
|
||||
expect(callAdminCluster).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [, { body: [, { doc: firstDoc }] }] = callAdminCluster.mock.calls[0];
|
||||
|
||||
expect(firstDoc).not.toMatchObject({
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('passes references if they are provided', async () => {
|
||||
const objects = [
|
||||
generateSavedObject({
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'test',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
];
|
||||
|
||||
mockValidResponse(objects);
|
||||
|
||||
await savedObjectsRepository.bulkUpdate(objects);
|
||||
|
||||
expect(callAdminCluster).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [, { body: [, { doc }] } ] = callAdminCluster.mock.calls[0];
|
||||
|
||||
expect(doc).toMatchObject({
|
||||
references: [{
|
||||
name: 'ref_0',
|
||||
type: 'test',
|
||||
id: '1',
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
it('passes empty references array if empty references array is provided', async () => {
|
||||
const objects = [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: `logstash-no-ref`,
|
||||
attributes: { title: `Testing no-ref` },
|
||||
references: []
|
||||
}
|
||||
];
|
||||
|
||||
mockValidResponse(objects);
|
||||
|
||||
await savedObjectsRepository.bulkUpdate(objects);
|
||||
|
||||
expect(callAdminCluster).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [, { body: [, { doc }] } ] = callAdminCluster.mock.calls[0];
|
||||
|
||||
expect(doc).toMatchObject({
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => {
|
||||
|
||||
const objects = [
|
||||
generateSavedObject(),
|
||||
generateSavedObject()
|
||||
];
|
||||
|
||||
mockValidResponse(objects);
|
||||
|
||||
await savedObjectsRepository.bulkUpdate(objects, {
|
||||
namespace: 'foo-namespace'
|
||||
});
|
||||
|
||||
const [,
|
||||
{ body: [
|
||||
{ update: firstUpdate },
|
||||
{ doc: firstUpdateDoc },
|
||||
{ update: secondUpdate },
|
||||
{ doc: secondUpdateDoc }
|
||||
]
|
||||
}
|
||||
] = callAdminCluster.mock.calls[0];
|
||||
|
||||
expect(firstUpdate).toMatchObject({
|
||||
_id: 'foo-namespace:index-pattern:logstash-1',
|
||||
_index: '.kibana-test',
|
||||
});
|
||||
|
||||
expect(firstUpdateDoc).toMatchObject({
|
||||
updated_at: mockTimestamp,
|
||||
'index-pattern': { title: 'Testing 1' },
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'test',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(secondUpdate).toMatchObject({
|
||||
_id: 'foo-namespace:index-pattern:logstash-2',
|
||||
_index: '.kibana-test',
|
||||
});
|
||||
|
||||
expect(secondUpdateDoc).toMatchObject({
|
||||
updated_at: mockTimestamp,
|
||||
'index-pattern': { title: 'Testing 2' },
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'test',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(onBeforeWrite).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => {
|
||||
|
||||
const objects = [
|
||||
generateSavedObject(),
|
||||
generateSavedObject()
|
||||
];
|
||||
|
||||
mockValidResponse(objects);
|
||||
|
||||
await savedObjectsRepository.bulkUpdate(objects);
|
||||
|
||||
const [,
|
||||
{ body: [
|
||||
{ update: firstUpdate },
|
||||
{ doc: firstUpdateDoc },
|
||||
{ update: secondUpdate },
|
||||
{ doc: secondUpdateDoc }
|
||||
]
|
||||
}
|
||||
] = callAdminCluster.mock.calls[0];
|
||||
|
||||
expect(firstUpdate).toMatchObject({
|
||||
_id: 'index-pattern:logstash-1',
|
||||
_index: '.kibana-test',
|
||||
});
|
||||
|
||||
expect(firstUpdateDoc).toMatchObject({
|
||||
updated_at: mockTimestamp,
|
||||
'index-pattern': { title: 'Testing 1' },
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'test',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(secondUpdate).toMatchObject({
|
||||
_id: 'index-pattern:logstash-2',
|
||||
_index: '.kibana-test',
|
||||
});
|
||||
|
||||
expect(secondUpdateDoc).toMatchObject({
|
||||
updated_at: mockTimestamp,
|
||||
'index-pattern': { title: 'Testing 2' },
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'test',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(onBeforeWrite).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => {
|
||||
|
||||
const objects = [
|
||||
generateSavedObject({
|
||||
type: 'globaltype',
|
||||
id: 'foo',
|
||||
namespace: 'foo-namespace'
|
||||
})
|
||||
];
|
||||
|
||||
mockValidResponse(objects);
|
||||
|
||||
await savedObjectsRepository.bulkUpdate(objects);
|
||||
|
||||
const [,
|
||||
{ body: [{ update }, { doc }] }
|
||||
] = callAdminCluster.mock.calls[0];
|
||||
|
||||
expect(update).toMatchObject({
|
||||
_id: 'globaltype:foo',
|
||||
_index: '.kibana-test',
|
||||
});
|
||||
|
||||
expect(doc).toMatchObject({
|
||||
updated_at: mockTimestamp,
|
||||
globaltype: { title: 'Testing 1' },
|
||||
references: [
|
||||
{
|
||||
name: 'ref_0',
|
||||
type: 'test',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#incrementCounter', () => {
|
||||
beforeEach(() => {
|
||||
callAdminCluster.mockImplementation((method, params) => ({
|
||||
|
|
|
@ -34,10 +34,12 @@ import {
|
|||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkGetObject,
|
||||
SavedObjectsBulkResponse,
|
||||
SavedObjectsBulkUpdateResponse,
|
||||
SavedObjectsCreateOptions,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsUpdateOptions,
|
||||
SavedObjectsUpdateResponse,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
} from '../saved_objects_client';
|
||||
import {
|
||||
SavedObject,
|
||||
|
@ -279,22 +281,12 @@ export class SavedObjectsRepository {
|
|||
|
||||
const id = requestedId || responseId;
|
||||
if (error) {
|
||||
if (error.type === 'version_conflict_engine_exception') {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
error: { statusCode: 409, message: 'version conflict, document already exists' },
|
||||
};
|
||||
}
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
error: {
|
||||
message: error.reason || JSON.stringify(error),
|
||||
},
|
||||
error: getBulkOperationError(error, type, id),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
|
@ -673,6 +665,109 @@ export class SavedObjectsRepository {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates multiple objects in bulk
|
||||
*
|
||||
* @param {array} objects - [{ type, id, attributes, options: { version, namespace } references }]
|
||||
* @property {string} options.version - ensures version matches that of persisted object
|
||||
* @property {string} [options.namespace]
|
||||
* @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]}
|
||||
*/
|
||||
async bulkUpdate<T extends SavedObjectAttributes = any>(
|
||||
objects: Array<SavedObjectsBulkUpdateObject<T>>,
|
||||
options: SavedObjectsBaseOptions = {}
|
||||
): Promise<SavedObjectsBulkUpdateResponse<T>> {
|
||||
const time = this._getCurrentTime();
|
||||
const bulkUpdateParams: object[] = [];
|
||||
|
||||
let requestIndexCounter = 0;
|
||||
const expectedResults: Array<Either<any, any>> = objects.map(object => {
|
||||
const { type, id } = object;
|
||||
|
||||
if (!this._allowedTypes.includes(type)) {
|
||||
return {
|
||||
tag: 'Left' as 'Left',
|
||||
error: {
|
||||
id,
|
||||
type,
|
||||
error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { attributes, references, version } = object;
|
||||
const { namespace } = options;
|
||||
|
||||
const documentToSave = {
|
||||
[type]: attributes,
|
||||
updated_at: time,
|
||||
references,
|
||||
};
|
||||
|
||||
if (!Array.isArray(documentToSave.references)) {
|
||||
delete documentToSave.references;
|
||||
}
|
||||
|
||||
const expectedResult = {
|
||||
type,
|
||||
id,
|
||||
esRequestIndex: requestIndexCounter++,
|
||||
documentToSave,
|
||||
};
|
||||
|
||||
bulkUpdateParams.push(
|
||||
{
|
||||
update: {
|
||||
_id: this._serializer.generateRawId(namespace, type, id),
|
||||
_index: this.getIndexForType(type),
|
||||
...(version && decodeRequestVersion(version)),
|
||||
},
|
||||
},
|
||||
{ doc: documentToSave }
|
||||
);
|
||||
|
||||
return { tag: 'Right' as 'Right', value: expectedResult };
|
||||
});
|
||||
|
||||
const esResponse = bulkUpdateParams.length
|
||||
? await this._writeToCluster('bulk', {
|
||||
refresh: 'wait_for',
|
||||
body: bulkUpdateParams,
|
||||
})
|
||||
: {};
|
||||
|
||||
return {
|
||||
saved_objects: expectedResults.map(expectedResult => {
|
||||
if (isLeft(expectedResult)) {
|
||||
return expectedResult.error;
|
||||
}
|
||||
|
||||
const { type, id, documentToSave, esRequestIndex } = expectedResult.value;
|
||||
const response = esResponse.items[esRequestIndex];
|
||||
const { error, _seq_no: seqNo, _primary_term: primaryTerm } = Object.values(
|
||||
response
|
||||
)[0] as any;
|
||||
|
||||
const { [type]: attributes, references, updated_at } = documentToSave;
|
||||
if (error) {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
error: getBulkOperationError(error, type, id),
|
||||
};
|
||||
}
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
updated_at,
|
||||
version: encodeVersion(seqNo, primaryTerm),
|
||||
attributes,
|
||||
references,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases a counter field by one. Creates the document if one doesn't exist for the given id.
|
||||
*
|
||||
|
@ -802,3 +897,16 @@ export class SavedObjectsRepository {
|
|||
return omit(savedObject, 'namespace');
|
||||
}
|
||||
}
|
||||
|
||||
function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) {
|
||||
switch (error.type) {
|
||||
case 'version_conflict_engine_exception':
|
||||
return { statusCode: 409, message: 'version conflict, document already exists' };
|
||||
case 'document_missing_exception':
|
||||
return SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload;
|
||||
default:
|
||||
return {
|
||||
message: error.reason || JSON.stringify(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ const create = () =>
|
|||
errors: SavedObjectsErrorHelpers,
|
||||
create: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
bulkUpdate: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
find: jest.fn(),
|
||||
|
|
|
@ -127,3 +127,21 @@ test(`#update`, async () => {
|
|||
expect(mockRepository.update).toHaveBeenCalledWith(type, id, attributes, options);
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
||||
test(`#bulkUpdate`, async () => {
|
||||
const returnValue = Symbol();
|
||||
const mockRepository = {
|
||||
bulkUpdate: jest.fn().mockResolvedValue(returnValue),
|
||||
};
|
||||
const client = new SavedObjectsClient(mockRepository);
|
||||
|
||||
const type = Symbol();
|
||||
const id = Symbol();
|
||||
const attributes = Symbol();
|
||||
const version = Symbol();
|
||||
const namespace = Symbol();
|
||||
const result = await client.bulkUpdate([{ type, id, attributes, version }], { namespace });
|
||||
|
||||
expect(mockRepository.bulkUpdate).toHaveBeenCalledWith([{ type, id, attributes, version }], { namespace });
|
||||
expect(result).toBe(returnValue);
|
||||
});
|
||||
|
|
|
@ -55,6 +55,20 @@ export interface SavedObjectsBulkCreateObject<T extends SavedObjectAttributes =
|
|||
migrationVersion?: SavedObjectsMigrationVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsBulkUpdateObject<T extends SavedObjectAttributes = any>
|
||||
extends Pick<SavedObjectsUpdateOptions, 'version' | 'references'> {
|
||||
/** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */
|
||||
id: string;
|
||||
/** The type of this Saved Object. Each plugin can define it's own custom Saved Object types. */
|
||||
type: string;
|
||||
/** {@inheritdoc SavedObjectAttributes} */
|
||||
attributes: Partial<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
|
@ -83,8 +97,9 @@ export interface SavedObjectsFindResponse<T extends SavedObjectAttributes = any>
|
|||
* @public
|
||||
*/
|
||||
export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions {
|
||||
/** Ensures version matches that of persisted object */
|
||||
/** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */
|
||||
version?: string;
|
||||
/** {@inheritdoc SavedObjectReference} */
|
||||
references?: SavedObjectReference[];
|
||||
}
|
||||
|
||||
|
@ -107,6 +122,14 @@ export interface SavedObjectsBulkResponse<T extends SavedObjectAttributes = any>
|
|||
saved_objects: Array<SavedObject<T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsBulkUpdateResponse<T extends SavedObjectAttributes = any> {
|
||||
saved_objects: Array<SavedObjectsUpdateResponse<T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
|
@ -229,4 +252,16 @@ export class SavedObjectsClient {
|
|||
): Promise<SavedObjectsUpdateResponse<T>> {
|
||||
return await this._repository.update(type, id, attributes, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk Updates multiple SavedObject at once
|
||||
*
|
||||
* @param objects
|
||||
*/
|
||||
async bulkUpdate<T extends SavedObjectAttributes = any>(
|
||||
objects: Array<SavedObjectsBulkUpdateObject<T>>,
|
||||
options?: SavedObjectsBaseOptions
|
||||
): Promise<SavedObjectsBulkUpdateResponse<T>> {
|
||||
return await this._repository.bulkUpdate(objects, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1189,12 +1189,26 @@ export interface SavedObjectsBulkResponse<T extends SavedObjectAttributes = any>
|
|||
saved_objects: Array<SavedObject<T>>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsBulkUpdateObject<T extends SavedObjectAttributes = any> extends Pick<SavedObjectsUpdateOptions, 'version' | 'references'> {
|
||||
attributes: Partial<T>;
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsBulkUpdateResponse<T extends SavedObjectAttributes = any> {
|
||||
// (undocumented)
|
||||
saved_objects: Array<SavedObjectsUpdateResponse<T>>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class SavedObjectsClient {
|
||||
// Warning: (ae-forgotten-export) The symbol "SavedObjectsRepository" needs to be exported by the entry point index.d.ts
|
||||
constructor(repository: SavedObjectsRepository);
|
||||
bulkCreate<T extends SavedObjectAttributes = any>(objects: Array<SavedObjectsBulkCreateObject<T>>, options?: SavedObjectsCreateOptions): Promise<SavedObjectsBulkResponse<T>>;
|
||||
bulkGet<T extends SavedObjectAttributes = any>(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkResponse<T>>;
|
||||
bulkUpdate<T extends SavedObjectAttributes = any>(objects: Array<SavedObjectsBulkUpdateObject<T>>, options?: SavedObjectsBaseOptions): Promise<SavedObjectsBulkUpdateResponse<T>>;
|
||||
create<T extends SavedObjectAttributes = any>(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise<SavedObject<T>>;
|
||||
delete(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<{}>;
|
||||
// (undocumented)
|
||||
|
@ -1539,7 +1553,6 @@ export class SavedObjectsSerializer {
|
|||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions {
|
||||
// (undocumented)
|
||||
references?: SavedObjectReference[];
|
||||
version?: string;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface SavedObjectsClientStub {
|
|||
create: sinon.SinonStub<any[], any>;
|
||||
bulkCreate: sinon.SinonStub<any[], any>;
|
||||
bulkGet: sinon.SinonStub<any[], any>;
|
||||
bulkUpdate: sinon.SinonStub<any[], any>;
|
||||
delete: sinon.SinonStub<any[], any>;
|
||||
find: sinon.SinonStub<any[], any>;
|
||||
errors: typeof savedObjectsClientErrors;
|
||||
|
@ -41,6 +42,7 @@ export function createObjectsClientStub(esDocSource = {}): SavedObjectsClientStu
|
|||
errors: savedObjectsClientErrors,
|
||||
bulkCreate: sinon.stub(),
|
||||
bulkGet: sinon.stub(),
|
||||
bulkUpdate: sinon.stub(),
|
||||
delete: sinon.stub(),
|
||||
find: sinon.stub(),
|
||||
};
|
||||
|
|
100
src/core/utils/map_utils.test.ts
Normal file
100
src/core/utils/map_utils.test.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { mapValuesOfMap, groupIntoMap } from './map_utils';
|
||||
|
||||
describe('groupIntoMap', () => {
|
||||
it('returns an empty map when there are no items to map', () => {
|
||||
const groupBy = jest.fn();
|
||||
|
||||
expect(groupIntoMap([], groupBy)).toEqual(new Map());
|
||||
expect(groupBy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls groupBy for each item in the collection', () => {
|
||||
const groupBy = jest.fn();
|
||||
|
||||
groupIntoMap([{ id: 1 }, { id: 2 }, { id: 3 }], groupBy);
|
||||
|
||||
expect(groupBy).toHaveBeenCalledTimes(3);
|
||||
expect(groupBy).toHaveBeenCalledWith({ id: 1 });
|
||||
expect(groupBy).toHaveBeenCalledWith({ id: 2 });
|
||||
expect(groupBy).toHaveBeenCalledWith({ id: 3 });
|
||||
});
|
||||
|
||||
it('returns each item in the key returned by groupBy', () => {
|
||||
const groupBy = (item: { id: number }) => item.id;
|
||||
|
||||
expect(groupIntoMap([{ id: 1 }, { id: 2 }, { id: 3 }], groupBy)).toEqual(
|
||||
new Map([[1, [{ id: 1 }]], [2, [{ id: 2 }]], [3, [{ id: 3 }]]])
|
||||
);
|
||||
});
|
||||
|
||||
it('groups items under the same key returned by groupBy', () => {
|
||||
const groupBy = (item: { id: number }) => (item.id % 2 === 0 ? 'even' : 'odd');
|
||||
|
||||
const expectedResult = new Map();
|
||||
expectedResult.set('even', [{ id: 2 }]);
|
||||
expectedResult.set('odd', [{ id: 1 }, { id: 3 }]);
|
||||
expect(groupIntoMap([{ id: 1 }, { id: 2 }, { id: 3 }], groupBy)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('supports Symbols as keys', () => {
|
||||
const even = Symbol('even');
|
||||
const odd = Symbol('odd');
|
||||
const groupBy = (item: { id: number }) => (item.id % 2 === 0 ? even : odd);
|
||||
|
||||
const expectedResult = new Map();
|
||||
expectedResult.set(even, [{ id: 2 }]);
|
||||
expectedResult.set(odd, [{ id: 1 }, { id: 3 }]);
|
||||
expect(groupIntoMap([{ id: 1 }, { id: 2 }, { id: 3 }], groupBy)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapValuesOfMap', () => {
|
||||
it('applys the mapper to each value in a map', () => {
|
||||
const mapper = jest.fn();
|
||||
|
||||
const even = Symbol('even');
|
||||
const odd = Symbol('odd');
|
||||
|
||||
const map = new Map();
|
||||
map.set(even, 2);
|
||||
map.set(odd, 1);
|
||||
|
||||
mapValuesOfMap(map, mapper);
|
||||
expect(mapper).toHaveBeenCalledWith(1);
|
||||
expect(mapper).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('returns a new map with each value mapped to the value returned by the mapper', () => {
|
||||
const mapper = (i: number) => i * 3;
|
||||
|
||||
const even = Symbol('even');
|
||||
const odd = Symbol('odd');
|
||||
|
||||
const map = new Map();
|
||||
map.set(even, 2);
|
||||
map.set(odd, 1);
|
||||
|
||||
expect(mapValuesOfMap(map, mapper)).toEqual(new Map([[even, 6], [odd, 3]]));
|
||||
expect(map.get(odd)).toEqual(1);
|
||||
expect(map.get(even)).toEqual(2);
|
||||
});
|
||||
});
|
37
src/core/utils/map_utils.ts
Normal file
37
src/core/utils/map_utils.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export function mapValuesOfMap<T, G, H>(map: Map<T, G>, mapper: (item: G) => H): Map<T, H> {
|
||||
const result = new Map();
|
||||
for (const [key, value] of map.entries()) {
|
||||
result.set(key, mapper(value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function groupIntoMap<T, G, H>(collection: T[], groupBy: (item: T) => G): Map<G, T[]> {
|
||||
const map = new Map<G, T[]>();
|
||||
collection.forEach(item => {
|
||||
const key = groupBy(item);
|
||||
const values = map.get(key) || [];
|
||||
values.push(item);
|
||||
map.set(key, values);
|
||||
});
|
||||
return map;
|
||||
}
|
|
@ -279,6 +279,7 @@ exports[`QueryBarInput Should disable autoFocus on EuiFieldText when disableAuto
|
|||
"client": Object {
|
||||
"bulkCreate": [MockFunction],
|
||||
"bulkGet": [MockFunction],
|
||||
"bulkUpdate": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"delete": [MockFunction],
|
||||
"find": [MockFunction],
|
||||
|
@ -824,6 +825,7 @@ exports[`QueryBarInput Should disable autoFocus on EuiFieldText when disableAuto
|
|||
"client": Object {
|
||||
"bulkCreate": [MockFunction],
|
||||
"bulkGet": [MockFunction],
|
||||
"bulkUpdate": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"delete": [MockFunction],
|
||||
"find": [MockFunction],
|
||||
|
@ -1357,6 +1359,7 @@ exports[`QueryBarInput Should pass the query language to the language switcher 1
|
|||
"client": Object {
|
||||
"bulkCreate": [MockFunction],
|
||||
"bulkGet": [MockFunction],
|
||||
"bulkUpdate": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"delete": [MockFunction],
|
||||
"find": [MockFunction],
|
||||
|
@ -1899,6 +1902,7 @@ exports[`QueryBarInput Should pass the query language to the language switcher 1
|
|||
"client": Object {
|
||||
"bulkCreate": [MockFunction],
|
||||
"bulkGet": [MockFunction],
|
||||
"bulkUpdate": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"delete": [MockFunction],
|
||||
"find": [MockFunction],
|
||||
|
@ -2432,6 +2436,7 @@ exports[`QueryBarInput Should render the given query 1`] = `
|
|||
"client": Object {
|
||||
"bulkCreate": [MockFunction],
|
||||
"bulkGet": [MockFunction],
|
||||
"bulkUpdate": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"delete": [MockFunction],
|
||||
"find": [MockFunction],
|
||||
|
@ -2974,6 +2979,7 @@ exports[`QueryBarInput Should render the given query 1`] = `
|
|||
"client": Object {
|
||||
"bulkCreate": [MockFunction],
|
||||
"bulkGet": [MockFunction],
|
||||
"bulkUpdate": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"delete": [MockFunction],
|
||||
"find": [MockFunction],
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObject, SavedObjectsClient } from 'src/core/server';
|
||||
import { SavedObject, SavedObjectAttributes } from 'src/core/server';
|
||||
import { collectReferencesDeep } from './collect_references_deep';
|
||||
import { SavedObjectsClientMock } from '../../../../../../core/server/mocks';
|
||||
|
||||
const data = [
|
||||
const data: Array<SavedObject<SavedObjectAttributes>> = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
|
@ -78,6 +79,7 @@ const data = [
|
|||
attributes: {
|
||||
title: 'pattern*',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
|
@ -100,97 +102,93 @@ const data = [
|
|||
];
|
||||
|
||||
test('collects dashboard and all dependencies', async () => {
|
||||
const savedObjectClient = ({
|
||||
errors: {} as any,
|
||||
create: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
bulkGet: jest.fn(getObjects => {
|
||||
return {
|
||||
saved_objects: getObjects.map((obj: SavedObject) =>
|
||||
data.find(row => row.id === obj.id && row.type === obj.type)
|
||||
),
|
||||
};
|
||||
}),
|
||||
} as unknown) as SavedObjectsClient;
|
||||
const savedObjectClient = SavedObjectsClientMock.create();
|
||||
savedObjectClient.bulkGet.mockImplementation(objects => {
|
||||
if (!objects) {
|
||||
throw new Error('Invalid test data');
|
||||
}
|
||||
return Promise.resolve({
|
||||
saved_objects: objects.map(
|
||||
(obj: any) => data.find(row => row.id === obj.id && row.type === obj.type)!
|
||||
),
|
||||
});
|
||||
});
|
||||
const objects = await collectReferencesDeep(savedObjectClient, [{ type: 'dashboard', id: '1' }]);
|
||||
expect(objects).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"panelsJSON": "[{\\"panelRefName\\":\\"panel_0\\"},{\\"panelRefName\\":\\"panel_1\\"}]",
|
||||
},
|
||||
"id": "1",
|
||||
"references": Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"panelsJSON": "[{\\"panelRefName\\":\\"panel_0\\"},{\\"panelRefName\\":\\"panel_1\\"}]",
|
||||
},
|
||||
"id": "1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"name": "panel_0",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "3",
|
||||
"name": "panel_1",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "dashboard",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "{\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
|
||||
},
|
||||
},
|
||||
"id": "2",
|
||||
"name": "panel_0",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "4",
|
||||
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"savedSearchRefName": "search_0",
|
||||
},
|
||||
"id": "3",
|
||||
"name": "panel_1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "5",
|
||||
"name": "search_0",
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "dashboard",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "{\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
|
||||
},
|
||||
},
|
||||
"id": "2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"title": "pattern*",
|
||||
},
|
||||
"id": "4",
|
||||
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"savedSearchRefName": "search_0",
|
||||
},
|
||||
"id": "3",
|
||||
"references": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "{\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
|
||||
},
|
||||
},
|
||||
"id": "5",
|
||||
"name": "search_0",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "4",
|
||||
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"title": "pattern*",
|
||||
},
|
||||
"id": "4",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "{\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
|
||||
},
|
||||
},
|
||||
"id": "5",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "4",
|
||||
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
`);
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -20,22 +20,18 @@
|
|||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createBulkGetRoute } from './bulk_get';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('POST /api/saved_objects/_bulk_get', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient.bulkGet.mockImplementation(() => Promise.resolve(''));
|
||||
savedObjectsClient.bulkGet.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
saved_objects: [],
|
||||
})
|
||||
);
|
||||
server = createMockServer();
|
||||
const prereqs = {
|
||||
getSavedObjectsClient: {
|
||||
|
@ -73,6 +69,7 @@ describe('POST /api/saved_objects/_bulk_get', () => {
|
|||
title: 'logstash-*',
|
||||
version: 'foo',
|
||||
references: [],
|
||||
attributes: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
148
src/legacy/server/saved_objects/routes/bulk_update.test.ts
Normal file
148
src/legacy/server/saved_objects/routes/bulk_update.test.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createBulkUpdateRoute } from './bulk_update';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('PUT /api/saved_objects/_bulk_update', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
server = createMockServer();
|
||||
|
||||
const prereqs = {
|
||||
getSavedObjectsClient: {
|
||||
assign: 'savedObjectsClient',
|
||||
method() {
|
||||
return savedObjectsClient;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
server.route(createBulkUpdateRoute(prereqs));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
savedObjectsClient.bulkUpdate.mockReset();
|
||||
});
|
||||
|
||||
it('formats successful response', async () => {
|
||||
const request = {
|
||||
method: 'PUT',
|
||||
url: '/api/saved_objects/_bulk_update',
|
||||
payload: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing visualization',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing dashboard',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const time = Date.now().toLocaleString();
|
||||
const clientResponse = [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
updated_at: time,
|
||||
version: 'version',
|
||||
references: undefined,
|
||||
attributes: {
|
||||
title: 'An existing visualization',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
updated_at: time,
|
||||
version: 'version',
|
||||
references: undefined,
|
||||
attributes: {
|
||||
title: 'An existing dashboard',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
savedObjectsClient.bulkUpdate.mockImplementation(() =>
|
||||
Promise.resolve({ saved_objects: clientResponse })
|
||||
);
|
||||
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual({ saved_objects: clientResponse });
|
||||
});
|
||||
|
||||
it('calls upon savedObjectClient.bulkUpdate', async () => {
|
||||
const request = {
|
||||
method: 'PUT',
|
||||
url: '/api/saved_objects/_bulk_update',
|
||||
payload: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing visualization',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing dashboard',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
savedObjectsClient.bulkUpdate.mockImplementation(() => Promise.resolve({ saved_objects: [] }));
|
||||
|
||||
await server.inject(request);
|
||||
|
||||
expect(savedObjectsClient.bulkUpdate).toHaveBeenCalledWith([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing visualization',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing dashboard',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
61
src/legacy/server/saved_objects/routes/bulk_update.ts
Normal file
61
src/legacy/server/saved_objects/routes/bulk_update.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import Joi from 'joi';
|
||||
import { SavedObjectsClient, SavedObjectsBulkUpdateObject } from 'src/core/server';
|
||||
import { Prerequisites } from './types';
|
||||
|
||||
interface BulkUpdateRequest extends Hapi.Request {
|
||||
pre: {
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
};
|
||||
payload: SavedObjectsBulkUpdateObject[];
|
||||
}
|
||||
|
||||
export const createBulkUpdateRoute = (prereqs: Prerequisites) => {
|
||||
return {
|
||||
path: '/api/saved_objects/_bulk_update',
|
||||
method: 'PUT',
|
||||
config: {
|
||||
pre: [prereqs.getSavedObjectsClient],
|
||||
validate: {
|
||||
payload: Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
id: Joi.string().required(),
|
||||
attributes: Joi.object().required(),
|
||||
version: Joi.string(),
|
||||
references: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
name: Joi.string().required(),
|
||||
type: Joi.string().required(),
|
||||
id: Joi.string().required(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
},
|
||||
handler(request: BulkUpdateRequest) {
|
||||
const { savedObjectsClient } = request.pre;
|
||||
return savedObjectsClient.bulkUpdate(request.payload);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -20,22 +20,22 @@
|
|||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createCreateRoute } from './create';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('POST /api/saved_objects/{type}', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
const clientResponse = {
|
||||
id: 'logstash-*',
|
||||
type: 'index-pattern',
|
||||
title: 'logstash-*',
|
||||
version: 'foo',
|
||||
references: [],
|
||||
attributes: {},
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient.create.mockImplementation(() => Promise.resolve(''));
|
||||
savedObjectsClient.create.mockImplementation(() => Promise.resolve(clientResponse));
|
||||
server = createMockServer();
|
||||
|
||||
const prereqs = {
|
||||
|
@ -65,15 +65,6 @@ describe('POST /api/saved_objects/{type}', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const clientResponse = {
|
||||
type: 'index-pattern',
|
||||
id: 'logstash-*',
|
||||
title: 'Testing',
|
||||
references: [],
|
||||
};
|
||||
|
||||
savedObjectsClient.create.mockImplementation(() => Promise.resolve(clientResponse));
|
||||
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
|
||||
|
|
|
@ -20,19 +20,11 @@
|
|||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createDeleteRoute } from './delete';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('DELETE /api/saved_objects/{type}/{id}', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient.delete.mockImplementation(() => Promise.resolve('{}'));
|
||||
|
|
|
@ -28,20 +28,15 @@ import * as exportMock from '../../../../core/server/saved_objects/export';
|
|||
import { createMockServer } from './_mock_server';
|
||||
import { createExportRoute } from './export';
|
||||
import { createListStream } from '../../../utils/streams';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
const getSortedObjectsForExport = exportMock.getSortedObjectsForExport as jest.Mock;
|
||||
|
||||
describe('POST /api/saved_objects/_export', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
...SavedObjectsClientMock.create(),
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -164,6 +159,7 @@ describe('POST /api/saved_objects/_export', () => {
|
|||
"savedObjectsClient": Object {
|
||||
"bulkCreate": [MockFunction],
|
||||
"bulkGet": [MockFunction],
|
||||
"bulkUpdate": [MockFunction],
|
||||
"create": [MockFunction],
|
||||
"delete": [MockFunction],
|
||||
"errors": Object {},
|
||||
|
|
|
@ -20,22 +20,20 @@
|
|||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createFindRoute } from './find';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('GET /api/saved_objects/_find', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
const clientResponse = {
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
};
|
||||
beforeEach(() => {
|
||||
savedObjectsClient.find.mockImplementation(() => Promise.resolve(''));
|
||||
savedObjectsClient.find.mockImplementation(() => Promise.resolve(clientResponse));
|
||||
server = createMockServer();
|
||||
|
||||
const prereqs = {
|
||||
|
@ -76,15 +74,18 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
url: '/api/saved_objects/_find?type=index-pattern',
|
||||
};
|
||||
|
||||
const clientResponse = {
|
||||
const findResponse = {
|
||||
total: 2,
|
||||
data: [
|
||||
per_page: 2,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'logstash-*',
|
||||
title: 'logstash-*',
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
|
@ -93,18 +94,19 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
title: 'stocks-*',
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
savedObjectsClient.find.mockImplementation(() => Promise.resolve(clientResponse));
|
||||
savedObjectsClient.find.mockImplementation(() => Promise.resolve(findResponse));
|
||||
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual(clientResponse);
|
||||
expect(response).toEqual(findResponse);
|
||||
});
|
||||
|
||||
it('calls upon savedObjectClient.find with defaults', async () => {
|
||||
|
|
|
@ -20,22 +20,24 @@
|
|||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createGetRoute } from './get';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('GET /api/saved_objects/{type}/{id}', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient.get.mockImplementation(() => Promise.resolve(''));
|
||||
savedObjectsClient.get.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: 'logstash-*',
|
||||
title: 'logstash-*',
|
||||
type: 'logstash-type',
|
||||
attributes: {},
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
references: [],
|
||||
})
|
||||
);
|
||||
server = createMockServer();
|
||||
|
||||
const prereqs = {
|
||||
|
@ -62,6 +64,8 @@ describe('GET /api/saved_objects/{type}/{id}', () => {
|
|||
const clientResponse = {
|
||||
id: 'logstash-*',
|
||||
title: 'logstash-*',
|
||||
type: 'logstash-type',
|
||||
attributes: {},
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
references: [],
|
||||
|
|
|
@ -20,18 +20,16 @@
|
|||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createImportRoute } from './import';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('POST /api/saved_objects/_import', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
const emptyResponse = {
|
||||
saved_objects: [],
|
||||
total: 0,
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -68,7 +66,7 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
'content-Type': 'multipart/form-data; boundary=BOUNDARY',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.find.mockResolvedValueOnce(emptyResponse);
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
expect(statusCode).toBe(200);
|
||||
|
@ -95,7 +93,7 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.find.mockResolvedValueOnce(emptyResponse);
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -104,6 +102,7 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
attributes: {
|
||||
title: 'my-pattern-*',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -141,7 +140,7 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.find.mockResolvedValueOnce(emptyResponse);
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -150,6 +149,7 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
attributes: {
|
||||
title: 'my-pattern-*',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
|
@ -157,6 +157,7 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
attributes: {
|
||||
title: 'Look at my dashboard',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -187,7 +188,7 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.find.mockResolvedValueOnce(emptyResponse);
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -256,6 +257,8 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
statusCode: 404,
|
||||
message: 'Not found',
|
||||
},
|
||||
references: [],
|
||||
attributes: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -27,4 +27,5 @@ export { createImportRoute } from './import';
|
|||
export { createLogLegacyImportRoute } from './log_legacy_import';
|
||||
export { createResolveImportErrorsRoute } from './resolve_import_errors';
|
||||
export { createUpdateRoute } from './update';
|
||||
export { createBulkUpdateRoute } from './bulk_update';
|
||||
export { createExportRoute } from './export';
|
||||
|
|
|
@ -20,19 +20,11 @@
|
|||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createResolveImportErrorsRoute } from './resolve_import_errors';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('POST /api/saved_objects/_resolve_import_errors', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
server = createMockServer();
|
||||
|
@ -111,6 +103,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
|
|||
attributes: {
|
||||
title: 'Look at my dashboard',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -153,6 +146,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
|
|||
attributes: {
|
||||
title: 'Look at my dashboard',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -219,6 +213,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
|
|||
attributes: {
|
||||
title: 'Look at my dashboard',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -20,22 +20,23 @@
|
|||
import Hapi from 'hapi';
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { createUpdateRoute } from './update';
|
||||
import { SavedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('PUT /api/saved_objects/{type}/{id?}', () => {
|
||||
let server: Hapi.Server;
|
||||
const savedObjectsClient = {
|
||||
errors: {} as any,
|
||||
bulkCreate: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => {
|
||||
savedObjectsClient.update.mockImplementation(() => Promise.resolve(''));
|
||||
const clientResponse = {
|
||||
id: 'logstash-*',
|
||||
title: 'logstash-*',
|
||||
type: 'logstash-type',
|
||||
attributes: {},
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClient.update.mockImplementation(() => Promise.resolve(clientResponse));
|
||||
server = createMockServer();
|
||||
|
||||
const prereqs = {
|
||||
|
@ -69,8 +70,10 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
|
|||
const clientResponse = {
|
||||
id: 'logstash-*',
|
||||
title: 'logstash-*',
|
||||
type: 'logstash-type',
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
attributes: {},
|
||||
references: [],
|
||||
};
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
createFindRoute,
|
||||
createGetRoute,
|
||||
createUpdateRoute,
|
||||
createBulkUpdateRoute,
|
||||
createExportRoute,
|
||||
createImportRoute,
|
||||
createResolveImportErrorsRoute,
|
||||
|
@ -87,6 +88,7 @@ export async function savedObjectsMixin(kbnServer, server) {
|
|||
};
|
||||
server.route(createBulkCreateRoute(prereqs));
|
||||
server.route(createBulkGetRoute(prereqs));
|
||||
server.route(createBulkUpdateRoute(prereqs));
|
||||
server.route(createCreateRoute(prereqs));
|
||||
server.route(createDeleteRoute(prereqs));
|
||||
server.route(createFindRoute(prereqs));
|
||||
|
|
|
@ -157,9 +157,9 @@ describe('Saved Objects Mixin', () => {
|
|||
});
|
||||
|
||||
describe('Routes', () => {
|
||||
it('should create 11 routes', () => {
|
||||
it('should create 12 routes', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route).toHaveBeenCalledTimes(11);
|
||||
expect(mockServer.route).toHaveBeenCalledTimes(12);
|
||||
});
|
||||
it('should add POST /api/saved_objects/_bulk_create', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
|
|
274
test/api_integration/apis/saved_objects/bulk_update.js
Normal file
274
test/api_integration/apis/saved_objects/bulk_update.js
Normal file
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
|
||||
describe('bulkUpdate', () => {
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
it('should return 200', async () => {
|
||||
const response = await supertest
|
||||
.put(`/api/saved_objects/_bulk_update`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing visualization'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing dashboard'
|
||||
}
|
||||
},
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const { saved_objects: [ firstObject, secondObject ] } = response.body;
|
||||
|
||||
// loose ISO8601 UTC time with milliseconds validation
|
||||
expect(firstObject).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/);
|
||||
expect(_.omit(firstObject, ['updated_at'])).to.eql({
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
version: 'WzgsMV0=',
|
||||
attributes: {
|
||||
title: 'An existing visualization',
|
||||
},
|
||||
});
|
||||
|
||||
expect(secondObject).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/);
|
||||
expect(_.omit(secondObject, ['updated_at'])).to.eql({
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
type: 'dashboard',
|
||||
version: 'WzksMV0=',
|
||||
attributes: {
|
||||
title: 'An existing dashboard',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not pass references if omitted', async () => {
|
||||
const { body: { saved_objects: [ visObject, dashObject ] } } = await supertest
|
||||
.post(`/api/saved_objects/_bulk_get`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
}
|
||||
]);
|
||||
|
||||
const response = await supertest
|
||||
.put(`/api/saved_objects/_bulk_update`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'Changed title but nothing else'
|
||||
},
|
||||
version: visObject.version
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'Changed title and references'
|
||||
},
|
||||
version: dashObject.version,
|
||||
references: [{ id: 'foo', name: 'Foo', type: 'visualization' }]
|
||||
},
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const { saved_objects: [ firstUpdatedObject, secondUpdatedObject ] } = response.body;
|
||||
expect(firstUpdatedObject).to.not.have.property('error');
|
||||
expect(secondUpdatedObject).to.not.have.property('error');
|
||||
|
||||
const { body: { saved_objects: [ visObjectAfterUpdate, dashObjectAfterUpdate ] } } = await supertest
|
||||
.post(`/api/saved_objects/_bulk_get`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
}
|
||||
]);
|
||||
|
||||
expect(visObjectAfterUpdate.references).to.eql(visObject.references);
|
||||
expect(dashObjectAfterUpdate.references).to.eql([{ id: 'foo', name: 'Foo', type: 'visualization' }]);
|
||||
});
|
||||
|
||||
it('passes empty references array if empty references array is provided', async () => {
|
||||
const { body: { saved_objects: [ { version } ] } } = await supertest
|
||||
.post(`/api/saved_objects/_bulk_get`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
}
|
||||
]);
|
||||
|
||||
await supertest
|
||||
.put(`/api/saved_objects/_bulk_update`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'Changed title but nothing else'
|
||||
},
|
||||
version,
|
||||
references: []
|
||||
}
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const { body: { saved_objects: [ visObjectAfterUpdate ] } } = await supertest
|
||||
.post(`/api/saved_objects/_bulk_get`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
}
|
||||
]);
|
||||
|
||||
expect(visObjectAfterUpdate.references).to.eql([]);
|
||||
});
|
||||
|
||||
describe('unknown id', () => {
|
||||
it('should return a generic 404', async () => {
|
||||
const response = await supertest
|
||||
.put(`/api/saved_objects/_bulk_update`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'not an id',
|
||||
attributes: {
|
||||
title: 'An existing visualization'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing dashboard'
|
||||
}
|
||||
},
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const { saved_objects: [ missingObject, updatedObject ] } = response.body;
|
||||
|
||||
// loose ISO8601 UTC time with milliseconds validation
|
||||
expect(missingObject).eql({
|
||||
type: 'visualization',
|
||||
id: 'not an id',
|
||||
error: {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [visualization/not an id] not found'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updatedObject).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/);
|
||||
expect(_.omit(updatedObject, ['updated_at', 'version'])).to.eql({
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
type: 'dashboard',
|
||||
attributes: {
|
||||
title: 'An existing dashboard',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
|
||||
it('should return generic 404', async () => {
|
||||
const response = await supertest
|
||||
.put(`/api/saved_objects/_bulk_update`)
|
||||
.send([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing visualization'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
attributes: {
|
||||
title: 'An existing dashboard'
|
||||
}
|
||||
},
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const { saved_objects: [ firstObject, secondObject ] } = response.body;
|
||||
|
||||
expect(firstObject).to.eql({
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
error: {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found'
|
||||
},
|
||||
});
|
||||
|
||||
expect(secondObject).to.eql({
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
type: 'dashboard',
|
||||
error: {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab] not found'
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -29,6 +29,7 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./import'));
|
||||
loadTestFile(require.resolve('./resolve_import_errors'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./bulk_update'));
|
||||
loadTestFile(require.resolve('./migrations'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -318,6 +318,194 @@ describe('#bulkCreate', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#bulkUpdate', () => {
|
||||
it('redirects request to underlying base client if type is not registered', async () => {
|
||||
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
|
||||
const mockedResponse = {
|
||||
saved_objects: [{ id: 'some-id', type: 'unknown-type', attributes, references: [] }],
|
||||
};
|
||||
|
||||
mockBaseClient.bulkUpdate.mockResolvedValue(mockedResponse);
|
||||
|
||||
await expect(
|
||||
wrapper.bulkUpdate(
|
||||
[{ type: 'unknown-type', id: 'some-id', attributes, version: 'some-version' }],
|
||||
{}
|
||||
)
|
||||
).resolves.toEqual(mockedResponse);
|
||||
|
||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith(
|
||||
[{ type: 'unknown-type', id: 'some-id', attributes, version: 'some-version' }],
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('encrypts attributes and strips them from response', async () => {
|
||||
const docs = [
|
||||
{
|
||||
id: 'some-id',
|
||||
type: 'known-type',
|
||||
attributes: {
|
||||
attrOne: 'one',
|
||||
attrSecret: 'secret',
|
||||
attrThree: 'three',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'some-id-2',
|
||||
type: 'known-type',
|
||||
attributes: {
|
||||
attrOne: 'one 2',
|
||||
attrSecret: 'secret 2',
|
||||
attrThree: 'three 2',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockedResponse = {
|
||||
saved_objects: docs.map(doc => ({ ...doc, references: undefined })),
|
||||
};
|
||||
|
||||
mockBaseClient.bulkUpdate.mockResolvedValue(mockedResponse);
|
||||
|
||||
await expect(wrapper.bulkUpdate(docs.map(doc => ({ ...doc })), {})).resolves.toEqual({
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'some-id',
|
||||
type: 'known-type',
|
||||
attributes: {
|
||||
attrOne: 'one',
|
||||
attrThree: 'three',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'some-id-2',
|
||||
type: 'known-type',
|
||||
attributes: {
|
||||
attrOne: 'one 2',
|
||||
attrThree: 'three 2',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(2);
|
||||
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
|
||||
{ type: 'known-type', id: 'some-id' },
|
||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
||||
);
|
||||
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
|
||||
{ type: 'known-type', id: 'some-id-2' },
|
||||
{ attrOne: 'one 2', attrSecret: 'secret 2', attrThree: 'three 2' }
|
||||
);
|
||||
|
||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
id: 'some-id',
|
||||
type: 'known-type',
|
||||
attributes: {
|
||||
attrOne: 'one',
|
||||
attrSecret: '*secret*',
|
||||
attrThree: 'three',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'some-id-2',
|
||||
type: 'known-type',
|
||||
attributes: {
|
||||
attrOne: 'one 2',
|
||||
attrSecret: '*secret 2*',
|
||||
attrThree: 'three 2',
|
||||
},
|
||||
},
|
||||
],
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('uses `namespace` to encrypt attributes if it is specified', async () => {
|
||||
const docs = [
|
||||
{
|
||||
id: 'some-id',
|
||||
type: 'known-type',
|
||||
attributes: {
|
||||
attrOne: 'one',
|
||||
attrSecret: 'secret',
|
||||
attrThree: 'three',
|
||||
},
|
||||
version: 'some-version',
|
||||
},
|
||||
];
|
||||
|
||||
mockBaseClient.bulkUpdate.mockResolvedValue({
|
||||
saved_objects: docs.map(doc => ({ ...doc, references: undefined })),
|
||||
});
|
||||
|
||||
await expect(wrapper.bulkUpdate(docs, { namespace: 'some-namespace' })).resolves.toEqual({
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'some-id',
|
||||
type: 'known-type',
|
||||
attributes: {
|
||||
attrOne: 'one',
|
||||
attrThree: 'three',
|
||||
},
|
||||
version: 'some-version',
|
||||
references: undefined,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(1);
|
||||
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
|
||||
{ type: 'known-type', id: 'some-id', namespace: 'some-namespace' },
|
||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
||||
);
|
||||
|
||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
id: 'some-id',
|
||||
type: 'known-type',
|
||||
attributes: {
|
||||
attrOne: 'one',
|
||||
attrSecret: '*secret*',
|
||||
attrThree: 'three',
|
||||
},
|
||||
version: 'some-version',
|
||||
|
||||
references: undefined,
|
||||
},
|
||||
],
|
||||
{ namespace: 'some-namespace' }
|
||||
);
|
||||
});
|
||||
|
||||
it('fails if base client fails', async () => {
|
||||
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
|
||||
|
||||
const failureReason = new Error('Something bad happened...');
|
||||
mockBaseClient.bulkUpdate.mockRejectedValue(failureReason);
|
||||
|
||||
await expect(
|
||||
wrapper.bulkUpdate(
|
||||
[{ type: 'unknown-type', id: 'some-id', attributes, version: 'some-version' }],
|
||||
{}
|
||||
)
|
||||
).rejects.toThrowError(failureReason);
|
||||
|
||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith(
|
||||
[{ type: 'unknown-type', id: 'some-id', attributes, version: 'some-version' }],
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
it('redirects request to underlying base client if type is not registered', async () => {
|
||||
const options = { namespace: 'some-ns' };
|
||||
|
|
|
@ -11,7 +11,9 @@ import {
|
|||
SavedObjectsBaseOptions,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkGetObject,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
SavedObjectsBulkResponse,
|
||||
SavedObjectsBulkUpdateResponse,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsCreateOptions,
|
||||
SavedObjectsFindOptions,
|
||||
|
@ -110,6 +112,34 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
|||
);
|
||||
}
|
||||
|
||||
public async bulkUpdate(
|
||||
objects: SavedObjectsBulkUpdateObject[],
|
||||
options?: SavedObjectsBaseOptions
|
||||
) {
|
||||
// We encrypt attributes for every object in parallel and that can potentially exhaust libuv or
|
||||
// NodeJS thread pool. If it turns out to be a problem, we can consider switching to the
|
||||
// sequential processing.
|
||||
const encryptedObjects = await Promise.all(
|
||||
objects.map(async object => {
|
||||
const { type, id, attributes } = object;
|
||||
if (!this.options.service.isRegistered(type)) {
|
||||
return object;
|
||||
}
|
||||
return {
|
||||
...object,
|
||||
attributes: await this.options.service.encryptAttributes(
|
||||
{ type, id, namespace: options && options.namespace },
|
||||
attributes
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return this.stripEncryptedAttributesFromBulkResponse(
|
||||
await this.options.baseClient.bulkUpdate(encryptedObjects, options)
|
||||
);
|
||||
}
|
||||
|
||||
public async delete(type: string, id: string, options?: SavedObjectsBaseOptions) {
|
||||
return await this.options.baseClient.delete(type, id, options);
|
||||
}
|
||||
|
@ -182,7 +212,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
|||
* @param response Raw response returned by the underlying base client.
|
||||
*/
|
||||
private stripEncryptedAttributesFromBulkResponse<
|
||||
T extends SavedObjectsBulkResponse | SavedObjectsFindResponse
|
||||
T extends SavedObjectsBulkResponse | SavedObjectsFindResponse | SavedObjectsBulkUpdateResponse
|
||||
>(response: T): T {
|
||||
for (const savedObject of response.saved_objects) {
|
||||
if (this.options.service.isRegistered(savedObject.type)) {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugin
|
|||
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
|
||||
|
||||
const readOperations: string[] = ['bulk_get', 'get', 'find'];
|
||||
const writeOperations: string[] = ['create', 'bulk_create', 'update', 'delete'];
|
||||
const writeOperations: string[] = ['create', 'bulk_create', 'update', 'bulk_update', 'delete'];
|
||||
const allOperations: string[] = [...readOperations, ...writeOperations];
|
||||
|
||||
export class FeaturePrivilegeSavedObjectBuilder extends BaseFeaturePrivilegeBuilder {
|
||||
|
|
|
@ -191,6 +191,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('all-savedObject-all-1', 'create'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'bulk_create'),
|
||||
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-2', 'bulk_get'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'get'),
|
||||
|
@ -198,6 +199,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('all-savedObject-all-2', 'create'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'bulk_create'),
|
||||
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-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('all-savedObject-read-1', 'get'),
|
||||
|
@ -218,6 +220,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-1', 'create'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'bulk_create'),
|
||||
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-2', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'get'),
|
||||
|
@ -225,6 +228,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-2', 'create'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_create'),
|
||||
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-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-read-1', 'get'),
|
||||
|
@ -427,6 +431,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('bar-savedObject-all-1', 'create'),
|
||||
actions.savedObject.get('bar-savedObject-all-1', 'bulk_create'),
|
||||
actions.savedObject.get('bar-savedObject-all-1', 'update'),
|
||||
actions.savedObject.get('bar-savedObject-all-1', 'bulk_update'),
|
||||
actions.savedObject.get('bar-savedObject-all-1', 'delete'),
|
||||
actions.savedObject.get('bar-savedObject-all-2', 'bulk_get'),
|
||||
actions.savedObject.get('bar-savedObject-all-2', 'get'),
|
||||
|
@ -434,6 +439,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('bar-savedObject-all-2', 'create'),
|
||||
actions.savedObject.get('bar-savedObject-all-2', 'bulk_create'),
|
||||
actions.savedObject.get('bar-savedObject-all-2', 'update'),
|
||||
actions.savedObject.get('bar-savedObject-all-2', 'bulk_update'),
|
||||
actions.savedObject.get('bar-savedObject-all-2', 'delete'),
|
||||
actions.savedObject.get('bar-savedObject-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('bar-savedObject-read-1', 'get'),
|
||||
|
@ -453,6 +459,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('all-savedObject-all-1', 'create'),
|
||||
actions.savedObject.get('all-savedObject-all-1', 'bulk_create'),
|
||||
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-2', 'bulk_get'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'get'),
|
||||
|
@ -460,6 +467,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('all-savedObject-all-2', 'create'),
|
||||
actions.savedObject.get('all-savedObject-all-2', 'bulk_create'),
|
||||
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-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('all-savedObject-read-1', 'get'),
|
||||
|
@ -479,6 +487,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-1', 'create'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'bulk_create'),
|
||||
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-2', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'get'),
|
||||
|
@ -486,6 +495,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-2', 'create'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_create'),
|
||||
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-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-read-1', 'get'),
|
||||
|
@ -570,6 +580,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-1', 'create'),
|
||||
actions.savedObject.get('read-savedObject-all-1', 'bulk_create'),
|
||||
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-2', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'get'),
|
||||
|
@ -577,6 +588,7 @@ describe('features', () => {
|
|||
actions.savedObject.get('read-savedObject-all-2', 'create'),
|
||||
actions.savedObject.get('read-savedObject-all-2', 'bulk_create'),
|
||||
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-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('read-savedObject-read-1', 'get'),
|
||||
|
@ -920,6 +932,7 @@ describe('reserved', () => {
|
|||
actions.savedObject.get('savedObject-all-1', 'create'),
|
||||
actions.savedObject.get('savedObject-all-1', 'bulk_create'),
|
||||
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-2', 'bulk_get'),
|
||||
actions.savedObject.get('savedObject-all-2', 'get'),
|
||||
|
@ -927,6 +940,7 @@ describe('reserved', () => {
|
|||
actions.savedObject.get('savedObject-all-2', 'create'),
|
||||
actions.savedObject.get('savedObject-all-2', 'bulk_create'),
|
||||
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-read-1', 'bulk_get'),
|
||||
actions.savedObject.get('savedObject-read-1', 'get'),
|
||||
|
|
|
@ -105,6 +105,18 @@ export class SecureSavedObjectsClientWrapper {
|
|||
return await this._baseClient.update(type, id, attributes, options);
|
||||
}
|
||||
|
||||
async bulkUpdate(objects = [], options) {
|
||||
const types = uniq(objects.map(o => o.type));
|
||||
await this._ensureAuthorized(
|
||||
types,
|
||||
'bulk_update',
|
||||
options && options.namespace,
|
||||
{ objects, options },
|
||||
);
|
||||
|
||||
return await this._baseClient.bulkUpdate(objects, options);
|
||||
}
|
||||
|
||||
async _checkPrivileges(actions, namespace) {
|
||||
try {
|
||||
return await this._checkSavedObjectsPrivileges(actions, namespace);
|
||||
|
|
|
@ -995,4 +995,145 @@ describe(`spaces disabled`, () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulkUpdate', () => {
|
||||
test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
|
||||
const type = 'foo';
|
||||
const mockErrors = createMockErrors();
|
||||
const mockCheckPrivileges = jest.fn(async () => {
|
||||
throw new Error('An actual error would happen here');
|
||||
});
|
||||
const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges);
|
||||
const mockRequest = Symbol();
|
||||
const mockAuditLogger = createMockAuditLogger();
|
||||
const mockActions = createMockActions();
|
||||
const client = new SecureSavedObjectsClientWrapper({
|
||||
actions: mockActions,
|
||||
auditLogger: mockAuditLogger,
|
||||
baseClient: null,
|
||||
checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest,
|
||||
errors: mockErrors,
|
||||
request: mockRequest,
|
||||
savedObjectTypes: [],
|
||||
spaces: null,
|
||||
});
|
||||
|
||||
const objects = [{
|
||||
type
|
||||
}];
|
||||
await expect(
|
||||
client.bulkUpdate(objects)
|
||||
).rejects.toThrowError(mockErrors.generalError);
|
||||
|
||||
expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_update')], undefined);
|
||||
expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1);
|
||||
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`throws decorated ForbiddenError when unauthorized`, async () => {
|
||||
const type = 'foo';
|
||||
const username = Symbol();
|
||||
const mockActions = createMockActions();
|
||||
const mockErrors = createMockErrors();
|
||||
const mockCheckPrivileges = jest.fn(async () => ({
|
||||
hasAllRequested: false,
|
||||
username,
|
||||
privileges: {
|
||||
[mockActions.savedObject.get(type, 'bulk_update')]: false,
|
||||
}
|
||||
}));
|
||||
const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges);
|
||||
const mockRequest = Symbol();
|
||||
const mockAuditLogger = createMockAuditLogger();
|
||||
const client = new SecureSavedObjectsClientWrapper({
|
||||
actions: mockActions,
|
||||
auditLogger: mockAuditLogger,
|
||||
baseClient: null,
|
||||
checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest,
|
||||
errors: mockErrors,
|
||||
request: mockRequest,
|
||||
savedObjectTypes: [],
|
||||
spaces: null,
|
||||
});
|
||||
const id = Symbol();
|
||||
const attributes = Symbol();
|
||||
const namespace = Symbol();
|
||||
|
||||
await expect(
|
||||
client.bulkUpdate([{ type, id, attributes }], { namespace })
|
||||
).rejects.toThrowError(mockErrors.forbiddenError);
|
||||
|
||||
expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_update')], namespace);
|
||||
expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1);
|
||||
expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
|
||||
username,
|
||||
'bulk_update',
|
||||
[type],
|
||||
[mockActions.savedObject.get(type, 'bulk_update')],
|
||||
{
|
||||
objects: [
|
||||
{
|
||||
type,
|
||||
id,
|
||||
attributes,
|
||||
}
|
||||
],
|
||||
options: { namespace }
|
||||
}
|
||||
);
|
||||
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`returns result of baseClient.bulkUpdate when authorized`, async () => {
|
||||
const type = 'foo';
|
||||
const username = Symbol();
|
||||
const returnValue = Symbol();
|
||||
const mockActions = createMockActions();
|
||||
const mockBaseClient = {
|
||||
bulkUpdate: jest.fn().mockReturnValue(returnValue)
|
||||
};
|
||||
const mockCheckPrivileges = jest.fn(async () => ({
|
||||
hasAllRequested: true,
|
||||
username,
|
||||
privileges: {
|
||||
[mockActions.savedObject.get(type, 'bulkUpdate')]: true,
|
||||
}
|
||||
}));
|
||||
const mockCheckSavedObjectsPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges);
|
||||
const mockRequest = Symbol();
|
||||
const mockAuditLogger = createMockAuditLogger();
|
||||
const client = new SecureSavedObjectsClientWrapper({
|
||||
actions: mockActions,
|
||||
auditLogger: mockAuditLogger,
|
||||
baseClient: mockBaseClient,
|
||||
checkSavedObjectsPrivilegesWithRequest: mockCheckSavedObjectsPrivilegesWithRequest,
|
||||
errors: null,
|
||||
request: mockRequest,
|
||||
savedObjectTypes: [],
|
||||
spaces: null,
|
||||
});
|
||||
const id = Symbol();
|
||||
const attributes = Symbol();
|
||||
const namespace = Symbol();
|
||||
|
||||
const result = await client.bulkUpdate([{ type, id, attributes }], { namespace });
|
||||
|
||||
expect(result).toBe(returnValue);
|
||||
expect(mockCheckSavedObjectsPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_update')], namespace);
|
||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledWith([{ type, id, attributes }], { namespace });
|
||||
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
|
||||
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_update', [type], {
|
||||
objects: [{
|
||||
type,
|
||||
id,
|
||||
attributes,
|
||||
}],
|
||||
options: { namespace }
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,30 +7,28 @@
|
|||
import { DEFAULT_SPACE_ID } from '../../../common/constants';
|
||||
import { SpacesSavedObjectsClient } from './spaces_saved_objects_client';
|
||||
import { spacesServiceMock } from '../../spaces_service/spaces_service.mock';
|
||||
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
|
||||
|
||||
const types = ['foo', 'bar', 'space'];
|
||||
|
||||
const createMockRequest = () => ({});
|
||||
|
||||
const createMockClient = () => {
|
||||
const errors = Symbol() as any;
|
||||
|
||||
return {
|
||||
get: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
find: jest.fn(),
|
||||
create: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
errors,
|
||||
};
|
||||
};
|
||||
const createMockClient = () => SavedObjectsClientMock.create();
|
||||
|
||||
const createSpacesService = async (spaceId: string) => {
|
||||
return spacesServiceMock.createSetupContract(spaceId);
|
||||
};
|
||||
|
||||
const createMockResponse = () => ({
|
||||
id: 'logstash-*',
|
||||
title: 'logstash-*',
|
||||
type: 'logstash-type',
|
||||
attributes: {},
|
||||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
references: [],
|
||||
});
|
||||
|
||||
[
|
||||
{ id: DEFAULT_SPACE_ID, expectedNamespace: undefined },
|
||||
{ id: 'space_1', expectedNamespace: 'space_1' },
|
||||
|
@ -57,8 +55,8 @@ const createSpacesService = async (spaceId: string) => {
|
|||
test(`supplements options with undefined namespace`, async () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const expectedReturnValue = Symbol();
|
||||
baseClient.get.mockReturnValue(expectedReturnValue);
|
||||
const expectedReturnValue = createMockResponse();
|
||||
baseClient.get.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
|
@ -102,8 +100,10 @@ const createSpacesService = async (spaceId: string) => {
|
|||
test(`supplements options with undefined namespace`, async () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const expectedReturnValue = Symbol();
|
||||
baseClient.bulkGet.mockReturnValue(expectedReturnValue);
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
};
|
||||
baseClient.bulkGet.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
|
@ -147,8 +147,13 @@ const createSpacesService = async (spaceId: string) => {
|
|||
test(`passes options.type to baseClient if valid singular type specified`, async () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const expectedReturnValue = Symbol();
|
||||
baseClient.find.mockReturnValue(expectedReturnValue);
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
total: 1,
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
};
|
||||
baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
|
@ -171,8 +176,13 @@ const createSpacesService = async (spaceId: string) => {
|
|||
test(`supplements options with undefined namespace`, async () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const expectedReturnValue = Symbol();
|
||||
baseClient.find.mockReturnValue(expectedReturnValue);
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
total: 1,
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
};
|
||||
baseClient.find.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
|
@ -214,8 +224,8 @@ const createSpacesService = async (spaceId: string) => {
|
|||
test(`supplements options with undefined namespace`, async () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const expectedReturnValue = Symbol();
|
||||
baseClient.create.mockReturnValue(expectedReturnValue);
|
||||
const expectedReturnValue = createMockResponse();
|
||||
baseClient.create.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
|
@ -260,8 +270,10 @@ const createSpacesService = async (spaceId: string) => {
|
|||
test(`supplements options with undefined namespace`, async () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const expectedReturnValue = Symbol();
|
||||
baseClient.bulkCreate.mockReturnValue(expectedReturnValue);
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
};
|
||||
baseClient.bulkCreate.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
|
@ -306,8 +318,8 @@ const createSpacesService = async (spaceId: string) => {
|
|||
test(`supplements options with undefined namespace`, async () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const expectedReturnValue = Symbol();
|
||||
baseClient.update.mockReturnValue(expectedReturnValue);
|
||||
const expectedReturnValue = createMockResponse();
|
||||
baseClient.update.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
|
@ -332,6 +344,42 @@ const createSpacesService = async (spaceId: string) => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#bulkUpdate', () => {
|
||||
test(`supplements options with the spaces namespace`, async () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
};
|
||||
baseClient.bulkUpdate.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
request,
|
||||
baseClient,
|
||||
spacesService,
|
||||
types,
|
||||
});
|
||||
|
||||
const actualReturnValue = await client.bulkUpdate([
|
||||
{ id: 'id', type: 'foo', attributes: {}, references: [] },
|
||||
]);
|
||||
|
||||
expect(actualReturnValue).toBe(expectedReturnValue);
|
||||
expect(baseClient.bulkUpdate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
id: 'id',
|
||||
type: 'foo',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
{ namespace: currentSpace.expectedNamespace }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
test(`throws error if options.namespace is specified`, async () => {
|
||||
const request = createMockRequest();
|
||||
|
@ -354,8 +402,8 @@ const createSpacesService = async (spaceId: string) => {
|
|||
test(`supplements options with undefined namespace`, async () => {
|
||||
const request = createMockRequest();
|
||||
const baseClient = createMockClient();
|
||||
const expectedReturnValue = Symbol();
|
||||
baseClient.delete.mockReturnValue(expectedReturnValue);
|
||||
const expectedReturnValue = createMockResponse();
|
||||
baseClient.delete.mockReturnValue(Promise.resolve(expectedReturnValue));
|
||||
const spacesService = await createSpacesService(currentSpace.id);
|
||||
|
||||
const client = new SpacesSavedObjectsClient({
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
SavedObjectsBaseOptions,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsBulkGetObject,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsCreateOptions,
|
||||
SavedObjectsFindOptions,
|
||||
|
@ -211,4 +212,27 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract {
|
|||
namespace: spaceIdToNamespace(this.spaceId),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an array of objects by id
|
||||
*
|
||||
* @param {array} objects - an array ids, or an array of objects containing id, type, attributes and optionally version, references and namespace
|
||||
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }] }
|
||||
* @example
|
||||
*
|
||||
* bulkUpdate([
|
||||
* { id: 'one', type: 'config', attributes: { title: 'My new title'}, version: 'd7rhfk47d=' },
|
||||
* { id: 'foo', type: 'index-pattern', attributes: {} }
|
||||
* ])
|
||||
*/
|
||||
public async bulkUpdate(
|
||||
objects: SavedObjectsBulkUpdateObject[] = [],
|
||||
options: SavedObjectsBaseOptions = {}
|
||||
) {
|
||||
throwErrorIfNamespaceSpecified(options);
|
||||
return await this.client.bulkUpdate(objects, {
|
||||
...options,
|
||||
namespace: spaceIdToNamespace(this.spaceId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { SuperTest } from 'supertest';
|
||||
import { DEFAULT_SPACE_ID } from '../../../../legacy/plugins/spaces/common/constants';
|
||||
import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils';
|
||||
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
|
||||
|
||||
interface BulkUpdateTest {
|
||||
statusCode: number;
|
||||
response: (resp: { [key: string]: any }) => void;
|
||||
}
|
||||
|
||||
interface BulkUpdateTests {
|
||||
spaceAware: BulkUpdateTest;
|
||||
notSpaceAware: BulkUpdateTest;
|
||||
hiddenType: BulkUpdateTest;
|
||||
doesntExist: BulkUpdateTest;
|
||||
}
|
||||
|
||||
interface BulkUpdateTestDefinition {
|
||||
user?: TestDefinitionAuthentication;
|
||||
spaceId?: string;
|
||||
otherSpaceId?: string;
|
||||
tests: BulkUpdateTests;
|
||||
}
|
||||
|
||||
export function bulkUpdateTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>) {
|
||||
const createExpectNotFound = (type: string, id: string, spaceId = DEFAULT_SPACE_ID) => (resp: {
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
const [, savedObject] = resp.body.saved_objects;
|
||||
expect(savedObject.error).eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: `Saved object [${type}/${getIdPrefix(spaceId)}${id}] not found`,
|
||||
});
|
||||
};
|
||||
|
||||
const createExpectDoesntExistNotFound = (spaceId?: string) => {
|
||||
return createExpectNotFound('visualization', 'not an id', spaceId);
|
||||
};
|
||||
|
||||
const createExpectSpaceAwareNotFound = (spaceId?: string) => {
|
||||
return createExpectNotFound('visualization', 'dd7caf20-9efd-11e7-acb3-3dab96693fab', spaceId);
|
||||
};
|
||||
|
||||
const expectHiddenTypeNotFound = createExpectNotFound(
|
||||
'hiddentype',
|
||||
'hiddentype_1',
|
||||
DEFAULT_SPACE_ID
|
||||
);
|
||||
|
||||
const createExpectRbacForbidden = (types: string[]) => (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_update ${types.join()}`,
|
||||
});
|
||||
};
|
||||
|
||||
const expectDoesntExistRbacForbidden = createExpectRbacForbidden(['globaltype', 'visualization']);
|
||||
|
||||
const expectNotSpaceAwareRbacForbidden = createExpectRbacForbidden(['globaltype']);
|
||||
|
||||
const expectHiddenTypeRbacForbidden = createExpectRbacForbidden(['globaltype', 'hiddentype']);
|
||||
const expectHiddenTypeRbacForbiddenWithGlobalAllowed = createExpectRbacForbidden(['hiddentype']);
|
||||
|
||||
const expectSpaceAwareRbacForbidden = createExpectRbacForbidden(['globaltype', 'visualization']);
|
||||
|
||||
const expectNotSpaceAwareResults = (resp: { [key: string]: any }) => {
|
||||
const [, savedObject] = resp.body.saved_objects;
|
||||
// loose uuid validation
|
||||
expect(savedObject)
|
||||
.to.have.property('id')
|
||||
.match(/^[0-9a-f-]{36}$/);
|
||||
|
||||
// loose ISO8601 UTC time with milliseconds validation
|
||||
expect(savedObject)
|
||||
.to.have.property('updated_at')
|
||||
.match(/^[\d-]{10}T[\d:\.]{12}Z$/);
|
||||
|
||||
expect(savedObject).to.eql({
|
||||
id: savedObject.id,
|
||||
type: 'globaltype',
|
||||
updated_at: savedObject.updated_at,
|
||||
version: savedObject.version,
|
||||
attributes: {
|
||||
name: 'My second favorite',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const expectSpaceAwareResults = (resp: { [key: string]: any }) => {
|
||||
const [, savedObject] = resp.body.saved_objects;
|
||||
// loose uuid validation ignoring prefix
|
||||
expect(savedObject)
|
||||
.to.have.property('id')
|
||||
.match(/[0-9a-f-]{36}$/);
|
||||
|
||||
// loose ISO8601 UTC time with milliseconds validation
|
||||
expect(savedObject)
|
||||
.to.have.property('updated_at')
|
||||
.match(/^[\d-]{10}T[\d:\.]{12}Z$/);
|
||||
|
||||
expect(savedObject).to.eql({
|
||||
id: savedObject.id,
|
||||
type: 'visualization',
|
||||
updated_at: savedObject.updated_at,
|
||||
version: savedObject.version,
|
||||
attributes: {
|
||||
title: 'My second favorite vis',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const makeBulkUpdateTest = (describeFn: DescribeFn) => (
|
||||
description: string,
|
||||
definition: BulkUpdateTestDefinition
|
||||
) => {
|
||||
const { user = {}, spaceId = DEFAULT_SPACE_ID, otherSpaceId, tests } = definition;
|
||||
|
||||
// We add this type into all bulk updates
|
||||
// to ensure that having additional items in the bulk
|
||||
// update doesn't change the expected outcome overall
|
||||
let updateCount = 0;
|
||||
const generateNonSpaceAwareGlobalSavedObject = () => ({
|
||||
type: 'globaltype',
|
||||
id: `8121a00-8efd-21e7-1cb3-34ab966434445`,
|
||||
attributes: {
|
||||
name: `Update #${++updateCount}`,
|
||||
},
|
||||
});
|
||||
|
||||
describeFn(description, () => {
|
||||
before(() => esArchiver.load('saved_objects/spaces'));
|
||||
after(() => esArchiver.unload('saved_objects/spaces'));
|
||||
it(`should return ${tests.spaceAware.statusCode} for a space-aware doc`, async () => {
|
||||
await supertest
|
||||
.put(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_update`)
|
||||
.auth(user.username, user.password)
|
||||
.send([
|
||||
generateNonSpaceAwareGlobalSavedObject(),
|
||||
{
|
||||
type: 'visualization',
|
||||
id: `${getIdPrefix(otherSpaceId || spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`,
|
||||
attributes: {
|
||||
title: 'My second favorite vis',
|
||||
},
|
||||
},
|
||||
generateNonSpaceAwareGlobalSavedObject(),
|
||||
])
|
||||
.expect(tests.spaceAware.statusCode)
|
||||
.then(tests.spaceAware.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => {
|
||||
await supertest
|
||||
.put(`${getUrlPrefix(otherSpaceId || spaceId)}/api/saved_objects/_bulk_update`)
|
||||
.auth(user.username, user.password)
|
||||
.send([
|
||||
generateNonSpaceAwareGlobalSavedObject(),
|
||||
{
|
||||
type: 'globaltype',
|
||||
id: `8121a00-8efd-21e7-1cb3-34ab966434445`,
|
||||
attributes: {
|
||||
name: 'My second favorite',
|
||||
},
|
||||
},
|
||||
generateNonSpaceAwareGlobalSavedObject(),
|
||||
])
|
||||
.expect(tests.notSpaceAware.statusCode)
|
||||
.then(tests.notSpaceAware.response);
|
||||
});
|
||||
|
||||
it(`should return ${tests.hiddenType.statusCode} for hiddentype doc`, async () => {
|
||||
await supertest
|
||||
.put(`${getUrlPrefix(otherSpaceId || spaceId)}/api/saved_objects/_bulk_update`)
|
||||
.auth(user.username, user.password)
|
||||
.send([
|
||||
generateNonSpaceAwareGlobalSavedObject(),
|
||||
{
|
||||
type: 'hiddentype',
|
||||
id: 'hiddentype_1',
|
||||
attributes: {
|
||||
name: 'My favorite hidden type',
|
||||
},
|
||||
},
|
||||
generateNonSpaceAwareGlobalSavedObject(),
|
||||
])
|
||||
.expect(tests.hiddenType.statusCode)
|
||||
.then(tests.hiddenType.response);
|
||||
});
|
||||
|
||||
describe('unknown id', () => {
|
||||
it(`should return ${tests.doesntExist.statusCode}`, async () => {
|
||||
await supertest
|
||||
.put(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_update`)
|
||||
.auth(user.username, user.password)
|
||||
.send([
|
||||
generateNonSpaceAwareGlobalSavedObject(),
|
||||
{
|
||||
type: 'visualization',
|
||||
id: `${getIdPrefix(spaceId)}not an id`,
|
||||
attributes: {
|
||||
title: 'My second favorite vis',
|
||||
},
|
||||
},
|
||||
generateNonSpaceAwareGlobalSavedObject(),
|
||||
])
|
||||
.expect(tests.doesntExist.statusCode)
|
||||
.then(tests.doesntExist.response);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const bulkUpdateTest = makeBulkUpdateTest(describe);
|
||||
// @ts-ignore
|
||||
bulkUpdateTest.only = makeBulkUpdateTest(describe.only);
|
||||
|
||||
return {
|
||||
createExpectDoesntExistNotFound,
|
||||
createExpectSpaceAwareNotFound,
|
||||
expectSpaceNotFound: expectHiddenTypeNotFound,
|
||||
expectDoesntExistRbacForbidden,
|
||||
expectNotSpaceAwareRbacForbidden,
|
||||
expectNotSpaceAwareResults,
|
||||
expectSpaceAwareRbacForbidden,
|
||||
expectSpaceAwareResults,
|
||||
expectHiddenTypeRbacForbidden,
|
||||
expectHiddenTypeRbacForbiddenWithGlobalAllowed,
|
||||
bulkUpdateTest,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AUTHENTICATION } from '../../common/lib/authentication';
|
||||
import { SPACES } from '../../common/lib/spaces';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { bulkUpdateTestSuiteFactory } from '../../common/suites/bulk_update';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('bulkUpdate', () => {
|
||||
const {
|
||||
createExpectDoesntExistNotFound,
|
||||
expectDoesntExistRbacForbidden,
|
||||
expectNotSpaceAwareResults,
|
||||
expectNotSpaceAwareRbacForbidden,
|
||||
expectSpaceAwareRbacForbidden,
|
||||
expectSpaceAwareResults,
|
||||
expectSpaceNotFound,
|
||||
expectHiddenTypeRbacForbidden,
|
||||
expectHiddenTypeRbacForbiddenWithGlobalAllowed,
|
||||
bulkUpdateTest,
|
||||
} = bulkUpdateTestSuiteFactory(esArchiver, supertest);
|
||||
|
||||
[
|
||||
{
|
||||
spaceId: SPACES.DEFAULT.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
|
||||
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
},
|
||||
},
|
||||
{
|
||||
spaceId: SPACES.SPACE_1.spaceId,
|
||||
users: {
|
||||
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
superuser: AUTHENTICATION.SUPERUSER,
|
||||
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
|
||||
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
},
|
||||
},
|
||||
].forEach(scenario => {
|
||||
bulkUpdateTest(`user with no access within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.noAccess,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`superuser within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.superuser,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceAwareResults,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceNotFound,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(scenario.spaceId),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`legacy user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.legacyAll,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`dual-privileges user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.dualAll,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceAwareResults,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbiddenWithGlobalAllowed,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(scenario.spaceId),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.dualRead,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with all globally within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.allGlobally,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceAwareResults,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbiddenWithGlobalAllowed,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(scenario.spaceId),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with read globally within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.readGlobally,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with all at the space within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.allAtSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceAwareResults,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbiddenWithGlobalAllowed,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(scenario.spaceId),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with read at the space within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.readAtSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with all at other space within the ${scenario.spaceId} space`, {
|
||||
user: scenario.users.allAtOtherSpace,
|
||||
spaceId: scenario.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -28,5 +28,6 @@ export default function({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./import'));
|
||||
loadTestFile(require.resolve('./resolve_import_errors'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./bulk_update'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AUTHENTICATION } from '../../common/lib/authentication';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { bulkUpdateTestSuiteFactory } from '../../common/suites/bulk_update';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('bulkUpdate', () => {
|
||||
const {
|
||||
createExpectDoesntExistNotFound,
|
||||
expectDoesntExistRbacForbidden,
|
||||
expectNotSpaceAwareResults,
|
||||
expectNotSpaceAwareRbacForbidden,
|
||||
expectSpaceAwareRbacForbidden,
|
||||
expectSpaceAwareResults,
|
||||
expectSpaceNotFound,
|
||||
expectHiddenTypeRbacForbidden,
|
||||
expectHiddenTypeRbacForbiddenWithGlobalAllowed,
|
||||
bulkUpdateTest,
|
||||
} = bulkUpdateTestSuiteFactory(esArchiver, supertest);
|
||||
|
||||
bulkUpdateTest(`user with no access`, {
|
||||
user: AUTHENTICATION.NOT_A_KIBANA_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`superuser`, {
|
||||
user: AUTHENTICATION.SUPERUSER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceAwareResults,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceNotFound,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`legacy user`, {
|
||||
user: AUTHENTICATION.KIBANA_LEGACY_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`dual-privileges user`, {
|
||||
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceAwareResults,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbiddenWithGlobalAllowed,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`dual-privileges readonly user`, {
|
||||
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with all globally`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceAwareResults,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbiddenWithGlobalAllowed,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with read globally`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with all at default space`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with read at default space`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with all at space_1`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest(`rbac user with read at space_1`, {
|
||||
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectSpaceAwareRbacForbidden,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 403,
|
||||
response: expectNotSpaceAwareRbacForbidden,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 403,
|
||||
response: expectHiddenTypeRbacForbidden,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 403,
|
||||
response: expectDoesntExistRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
|
@ -28,5 +28,6 @@ export default function({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./import'));
|
||||
loadTestFile(require.resolve('./resolve_import_errors'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./bulk_update'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SPACES } from '../../common/lib/spaces';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { bulkUpdateTestSuiteFactory } from '../../common/suites/bulk_update';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('bulkUpdate', () => {
|
||||
const {
|
||||
createExpectSpaceAwareNotFound,
|
||||
expectSpaceAwareResults,
|
||||
createExpectDoesntExistNotFound,
|
||||
expectNotSpaceAwareResults,
|
||||
expectSpaceNotFound,
|
||||
bulkUpdateTest,
|
||||
} = bulkUpdateTestSuiteFactory(esArchiver, supertest);
|
||||
|
||||
bulkUpdateTest(`in the default space`, {
|
||||
spaceId: SPACES.DEFAULT.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceAwareResults,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceNotFound,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(SPACES.DEFAULT.spaceId),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest('in the current space (space_1)', {
|
||||
spaceId: SPACES.SPACE_1.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceAwareResults,
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceNotFound,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(SPACES.SPACE_1.spaceId),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bulkUpdateTest('objects that exist in another space (space_1)', {
|
||||
spaceId: SPACES.DEFAULT.spaceId,
|
||||
otherSpaceId: SPACES.SPACE_1.spaceId,
|
||||
tests: {
|
||||
spaceAware: {
|
||||
statusCode: 200,
|
||||
response: createExpectSpaceAwareNotFound(SPACES.SPACE_1.spaceId),
|
||||
},
|
||||
notSpaceAware: {
|
||||
statusCode: 200,
|
||||
response: expectNotSpaceAwareResults,
|
||||
},
|
||||
hiddenType: {
|
||||
statusCode: 200,
|
||||
response: expectSpaceNotFound,
|
||||
},
|
||||
doesntExist: {
|
||||
statusCode: 200,
|
||||
response: createExpectDoesntExistNotFound(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
|
@ -20,5 +20,6 @@ export default function({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./import'));
|
||||
loadTestFile(require.resolve('./resolve_import_errors'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./bulk_update'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue