Skip to content

Commit

Permalink
Allow saved objects to be searched across spaces
Browse files Browse the repository at this point in the history
  • Loading branch information
legrego committed Jun 10, 2020
1 parent 3197a00 commit af184f1
Show file tree
Hide file tree
Showing 55 changed files with 918 additions and 310 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<b>Signature:</b>

```typescript
export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions
export interface SavedObjectsFindOptions extends Omit<SavedObjectsBaseOptions, 'namespace'>
```
## Properties
Expand All @@ -19,6 +19,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions
| [fields](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | <code>string[]</code> | An array of fields to include in the results |
| [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | <code>string</code> | |
| [hasReference](./kibana-plugin-core-server.savedobjectsfindoptions.hasreference.md) | <code>{</code><br/><code> type: string;</code><br/><code> id: string;</code><br/><code> }</code> | |
| [namespaces](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md) | <code>string[]</code> | |
| [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | <code>number</code> | |
| [perPage](./kibana-plugin-core-server.savedobjectsfindoptions.perpage.md) | <code>number</code> | |
| [search](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | <code>string</code> | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String <code>query</code> argument for more information |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) &gt; [namespaces](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md)

## SavedObjectsFindOptions.namespaces property

<b>Signature:</b>

```typescript
namespaces?: string[];
```
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
<b>Signature:</b>

```typescript
find<T = unknown>({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
find<T = unknown>({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, }: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| { search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, } | <code>SavedObjectsFindOptions</code> | |
| { search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, } | <code>SavedObjectsFindOptions</code> | |

<b>Returns:</b>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export declare class SavedObjectsRepository
| [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object |
| [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. |
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[<code>addToNamespaces</code>\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. |
| [find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | |
| [find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | |
| [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object |
| [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. |
| [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object |
Expand Down
6 changes: 4 additions & 2 deletions src/core/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,7 @@ export class SavedObjectsClient {
bulkUpdate<T = unknown>(objects?: SavedObjectsBulkUpdateObject[]): Promise<SavedObjectsBatchResponse<unknown>>;
create: <T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>>;
delete: (type: string, id: string) => Promise<{}>;
find: <T = unknown>(options: Pick<SavedObjectsFindOptions, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>>;
find: <T = unknown>(options: Pick<SavedObjectsFindOptions, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator" | "namespaces">) => Promise<SavedObjectsFindResponsePublic<T>>;
get: <T = unknown>(type: string, id: string) => Promise<SimpleSavedObject<T>>;
update<T = unknown>(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise<SimpleSavedObject<T>>;
}
Expand All @@ -1144,7 +1144,7 @@ export interface SavedObjectsCreateOptions {
}

// @public (undocumented)
export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
export interface SavedObjectsFindOptions extends Omit<SavedObjectsBaseOptions, 'namespace'> {
// (undocumented)
defaultSearchOperator?: 'AND' | 'OR';
fields?: string[];
Expand All @@ -1156,6 +1156,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
id: string;
};
// (undocumented)
namespaces?: string[];
// (undocumented)
page?: number;
// (undocumented)
perPage?: number;
Expand Down
1 change: 1 addition & 0 deletions src/core/public/saved_objects/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export class SavedObjectsClient {
sortField: 'sort_field',
type: 'type',
filter: 'filter',
namespaces: 'namespaces',
};

const renamedQuery = renameKeys<SavedObjectsFindOptions, any>(renameMap, options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe('getSortedObjectsForExport()', () => {
"calls": Array [
Array [
Object {
"namespace": undefined,
"namespaces": undefined,
"perPage": 500,
"search": undefined,
"type": Array [
Expand Down Expand Up @@ -251,7 +251,7 @@ describe('getSortedObjectsForExport()', () => {
"calls": Array [
Array [
Object {
"namespace": undefined,
"namespaces": undefined,
"perPage": 500,
"search": "foo",
"type": Array [
Expand Down Expand Up @@ -338,7 +338,9 @@ describe('getSortedObjectsForExport()', () => {
"calls": Array [
Array [
Object {
"namespace": "foo",
"namespaces": Array [
"foo",
],
"perPage": 500,
"search": undefined,
"type": Array [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async function fetchObjectsToExport({
type: types,
search,
perPage: exportSizeLimit,
namespace,
namespaces: namespace ? [namespace] : undefined,
});
if (findResponse.total > exportSizeLimit) {
throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`);
Expand Down
12 changes: 12 additions & 0 deletions src/core/server/saved_objects/routes/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,22 @@ export const registerFindRoute = (router: IRouter) => {
),
fields: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
filter: schema.maybe(schema.string()),
namespaces: schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
),
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
const query = req.query;

let namespaces: string[] | undefined;
if (Array.isArray(req.query.namespaces)) {
namespaces = req.query.namespaces;
} else if (typeof req.query.namespaces === 'string') {
namespaces = [req.query.namespaces];
}

const result = await context.core.savedObjects.client.find({
perPage: query.per_page,
page: query.page,
Expand All @@ -62,6 +73,7 @@ export const registerFindRoute = (router: IRouter) => {
hasReference: query.has_reference,
fields: typeof query.fields === 'string' ? [query.fields] : query.fields,
filter: query.filter,
namespaces,
});

return res.ok({ body: result });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe('GET /api/saved_objects/_find', () => {
notExpandable: true,
attributes: {},
references: [],
namespaces: ['default'],
},
{
type: 'index-pattern',
Expand All @@ -89,6 +90,7 @@ describe('GET /api/saved_objects/_find', () => {
notExpandable: true,
attributes: {},
references: [],
namespaces: ['default'],
},
],
};
Expand Down Expand Up @@ -239,4 +241,38 @@ describe('GET /api/saved_objects/_find', () => {
defaultSearchOperator: 'OR',
});
});

it('accepts the query parameter namespaces as a string', async () => {
await supertest(httpSetup.server.listener)
.get('/api/saved_objects/_find?type=index-pattern&namespaces=foo')
.expect(200);

expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);

const options = savedObjectsClient.find.mock.calls[0][0];
expect(options).toEqual({
perPage: 20,
page: 1,
type: ['index-pattern'],
namespaces: ['foo'],
defaultSearchOperator: 'OR',
});
});

it('accepts the query parameter namespaces as an array', async () => {
await supertest(httpSetup.server.listener)
.get('/api/saved_objects/_find?type=index-pattern&namespaces=default&namespaces=foo')
.expect(200);

expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);

const options = savedObjectsClient.find.mock.calls[0][0];
expect(options).toEqual({
perPage: 20,
page: 1,
type: ['index-pattern'],
namespaces: ['default', 'foo'],
defaultSearchOperator: 'OR',
});
});
});
60 changes: 40 additions & 20 deletions src/core/server/saved_objects/service/lib/repository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ describe('SavedObjectsRepository', () => {
...obj,
migrationVersion: { [obj.type]: '1.1.1' },
version: mockVersion,
namespaces: obj.namespaces ?? [obj.namespace ?? 'default'],
...mockTimestampFields,
});

Expand Down Expand Up @@ -826,9 +827,23 @@ describe('SavedObjectsRepository', () => {
// Assert that both raw docs from the ES response are deserialized
expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(1, {
...response.items[0].create,
_source: {
...response.items[0].create._source,
namespaces: response.items[0].create._source.namespaces ?? [
response.items[0].create._source.namespace ?? 'default',
],
},
_id: expect.stringMatching(/^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/),
});
expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(2, response.items[1].create);
expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(2, {
...response.items[1].create,
_source: {
...response.items[1].create._source,
namespaces: response.items[1].create._source.namespaces ?? [
response.items[1].create._source.namespace ?? 'default',
],
},
});

// Assert that ID's are deserialized to remove the type and namespace
expect(result.saved_objects[0].id).toEqual(
Expand Down Expand Up @@ -985,7 +1000,7 @@ describe('SavedObjectsRepository', () => {
const expectSuccessResult = ({ type, id }, doc) => ({
type,
id,
...(doc._source.namespaces && { namespaces: doc._source.namespaces }),
namespaces: doc._source.namespaces ?? ['default'],
...(doc._source.updated_at && { updated_at: doc._source.updated_at }),
version: encodeHitVersion(doc),
attributes: doc._source[type],
Expand Down Expand Up @@ -1032,7 +1047,7 @@ describe('SavedObjectsRepository', () => {
const result = await bulkGetSuccess([obj1, obj]);
expect(result).toEqual({
saved_objects: [
expect.not.objectContaining({ namespaces: expect.anything() }),
expect.objectContaining({ namespaces: ['default'] }),
expect.objectContaining({ namespaces: expect.any(Array) }),
],
});
Expand Down Expand Up @@ -1651,6 +1666,7 @@ describe('SavedObjectsRepository', () => {
version: mockVersion,
attributes,
references,
namespaces: [namespace ?? 'default'],
migrationVersion: { [type]: '1.1.1' },
});
});
Expand Down Expand Up @@ -1907,7 +1923,7 @@ describe('SavedObjectsRepository', () => {
await deleteByNamespaceSuccess(namespace);
const allTypes = registry.getAllTypes().map((type) => type.name);
expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, {
namespace,
namespaces: [namespace],
type: allTypes.filter((type) => !registry.isNamespaceAgnostic(type)),
});
});
Expand Down Expand Up @@ -2128,6 +2144,7 @@ describe('SavedObjectsRepository', () => {
version: mockVersion,
attributes: doc._source[doc._source.type],
references: [],
namespaces: doc._source.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : ['default'],
});
});
});
Expand All @@ -2137,7 +2154,7 @@ describe('SavedObjectsRepository', () => {
callAdminCluster.mockReturnValue(namespacedSearchResults);
const count = namespacedSearchResults.hits.hits.length;

const response = await savedObjectsRepository.find({ type, namespace });
const response = await savedObjectsRepository.find({ type, namespaces: [namespace] });

expect(response.total).toBe(count);
expect(response.saved_objects).toHaveLength(count);
Expand All @@ -2150,6 +2167,7 @@ describe('SavedObjectsRepository', () => {
version: mockVersion,
attributes: doc._source[doc._source.type],
references: [],
namespaces: doc._source.type === NAMESPACE_AGNOSTIC_TYPE ? undefined : [namespace],
});
});
});
Expand All @@ -2169,7 +2187,7 @@ describe('SavedObjectsRepository', () => {
describe('search dsl', () => {
it(`passes mappings, registry, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl`, async () => {
const relevantOpts = {
namespace,
namespaces: [namespace],
search: 'foo*',
searchFields: ['foo'],
type: [type],
Expand Down Expand Up @@ -2367,6 +2385,7 @@ describe('SavedObjectsRepository', () => {
title: 'Testing',
},
references: [],
namespaces: ['default'],
});
});

Expand All @@ -2377,10 +2396,10 @@ describe('SavedObjectsRepository', () => {
});
});

it(`doesn't include namespaces if type is not multi-namespace`, async () => {
it(`include namespaces if type is not multi-namespace`, async () => {
const result = await getSuccess(type, id);
expect(result).not.toMatchObject({
namespaces: expect.anything(),
expect(result).toMatchObject({
namespaces: ['default'],
});
});
});
Expand Down Expand Up @@ -2901,10 +2920,10 @@ describe('SavedObjectsRepository', () => {
_id: `${type}:${id}`,
...mockVersionProps,
result: 'updated',
...(registry.isMultiNamespace(type) && {
// don't need the rest of the source for test purposes, just the namespaces attribute
get: { _source: { namespaces: [options?.namespace ?? 'default'] } },
}),
// don't need the rest of the source for test purposes, just the namespace and namespaces attributes
get: {
_source: { namespaces: [options?.namespace ?? 'default'], namespace: options?.namespace },
},
}); // this._writeToCluster('update', ...)
const result = await savedObjectsRepository.update(type, id, attributes, options);
expect(callAdminCluster).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 2 : 1);
Expand Down Expand Up @@ -3004,15 +3023,15 @@ describe('SavedObjectsRepository', () => {

it(`includes _sourceIncludes when type is multi-namespace`, async () => {
await updateSuccess(MULTI_NAMESPACE_TYPE, id, attributes);
expectClusterCallArgs({ _sourceIncludes: ['namespaces'] }, 2);
expectClusterCallArgs({ _sourceIncludes: ['namespace', 'namespaces'] }, 2);
});

it(`doesn't include _sourceIncludes when type is not multi-namespace`, async () => {
it(`includes _sourceIncludes when type is not multi-namespace`, async () => {
await updateSuccess(type, id, attributes);
expect(callAdminCluster).toHaveBeenLastCalledWith(
expect.any(String),
expect.not.objectContaining({
_sourceIncludes: expect.anything(),
expect.objectContaining({
_sourceIncludes: ['namespace', 'namespaces'],
})
);
});
Expand Down Expand Up @@ -3086,6 +3105,7 @@ describe('SavedObjectsRepository', () => {
version: mockVersion,
attributes,
references,
namespaces: [namespace],
});
});

Expand All @@ -3096,10 +3116,10 @@ describe('SavedObjectsRepository', () => {
});
});

it(`doesn't include namespaces if type is not multi-namespace`, async () => {
it(`includes namespaces if type is not multi-namespace`, async () => {
const result = await updateSuccess(type, id, attributes);
expect(result).not.toMatchObject({
namespaces: expect.anything(),
expect(result).toMatchObject({
namespaces: ['default'],
});
});
});
Expand Down
Loading

0 comments on commit af184f1

Please sign in to comment.