Skip to content

Commit

Permalink
[Content Management] Cross Type Search - paginate and tags filter (#1…
Browse files Browse the repository at this point in the history
…54819)

## Summary

Follow up to #154464
Partially resolve #152224 -
implement pagination and tags filtering
  • Loading branch information
Dosant authored Apr 13, 2023
1 parent 3840136 commit 10f2015
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 5 deletions.
122 changes: 121 additions & 1 deletion src/plugins/content_management/server/core/msearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { createMockedStorage } from './mocks';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { StorageContext } from '.';

const SEARCH_LISTING_LIMIT = 100;
const SEARCH_PER_PAGE = 10;

const setup = () => {
const contentRegistry = new ContentRegistry(new EventBus());

Expand Down Expand Up @@ -50,6 +53,10 @@ const setup = () => {
const mSearchService = new MSearchService({
getSavedObjectsClient: async () => savedObjectsClient,
contentRegistry,
getConfig: {
listingLimit: async () => SEARCH_LISTING_LIMIT,
perPage: async () => SEARCH_PER_PAGE,
},
});

return { mSearchService, savedObjectsClient, contentRegistry };
Expand Down Expand Up @@ -94,7 +101,7 @@ test('should cross-content search using saved objects api', async () => {
saved_objects: [soResultFoo, soResultBar],
total: 2,
page: 1,
per_page: 10,
per_page: SEARCH_PER_PAGE,
});

const result = await mSearchService.search(
Expand All @@ -104,6 +111,10 @@ test('should cross-content search using saved objects api', async () => {
],
{
text: 'search text',
tags: {
excluded: ['excluded-tag'],
included: ['included-tag'],
},
}
);

Expand All @@ -112,6 +123,20 @@ test('should cross-content search using saved objects api', async () => {
search: 'search text',
searchFields: ['title^3', 'description', 'special-foo-field', 'special-bar-field'],
type: ['foo-type', 'bar-type'],
page: 1,
perPage: SEARCH_PER_PAGE,
hasNoReference: [
{
id: 'excluded-tag',
type: 'tag',
},
],
hasReference: [
{
id: 'included-tag',
type: 'tag',
},
],
});

expect(result).toEqual({
Expand Down Expand Up @@ -161,3 +186,98 @@ test('should error if content is registered, but no mSearch support', async () =
)
).rejects.toThrowErrorMatchingInlineSnapshot(`"Content type foo2 does not support mSearch"`);
});

test('should paginate using cursor', async () => {
const { savedObjectsClient, mSearchService } = setup();

const soResultFoo = {
id: 'fooid',
score: 0,
type: 'foo-type',
references: [],
attributes: {
title: 'foo',
},
};

savedObjectsClient.find.mockResolvedValueOnce({
saved_objects: Array(5).fill(soResultFoo),
total: 7,
page: 1,
per_page: 5,
});

const result1 = await mSearchService.search(
[
{ contentTypeId: 'foo', ctx: mockStorageContext() },
{ contentTypeId: 'bar', ctx: mockStorageContext() },
],
{
text: 'search text',
limit: 5,
}
);

expect(savedObjectsClient.find).toHaveBeenCalledWith({
defaultSearchOperator: 'AND',
search: 'search text',
searchFields: ['title^3', 'description', 'special-foo-field', 'special-bar-field'],
type: ['foo-type', 'bar-type'],
page: 1,
perPage: 5,
});

expect(result1).toEqual({
hits: Array(5).fill({ itemFoo: soResultFoo }),
pagination: {
total: 7,
cursor: '2',
},
});

savedObjectsClient.find.mockResolvedValueOnce({
saved_objects: Array(2).fill(soResultFoo),
total: 7,
page: 2,
per_page: 5,
});

const result2 = await mSearchService.search(
[
{ contentTypeId: 'foo', ctx: mockStorageContext() },
{ contentTypeId: 'bar', ctx: mockStorageContext() },
],
{
text: 'search text',
limit: 5,
cursor: result1.pagination.cursor,
}
);

expect(result2).toEqual({
hits: Array(2).fill({ itemFoo: soResultFoo }),
pagination: {
total: 7,
},
});
});

test('should error if outside of pagination limit', async () => {
const { mSearchService } = setup();
await expect(
mSearchService.search(
[
{ contentTypeId: 'foo', ctx: mockStorageContext() },
{ contentTypeId: 'bar', ctx: mockStorageContext() },
],

{
text: 'search text',
cursor: '11',
limit: 10,
}
)
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Requested page 11 with 10 items per page exceeds the maximum allowed limit of ${SEARCH_LISTING_LIMIT} items"`
);
});
42 changes: 38 additions & 4 deletions src/plugins/content_management/server/core/msearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
* Side Public License, v 1.
*/

import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import {
SavedObjectsClientContract,
SavedObjectsFindOptionsReference,
} from '@kbn/core-saved-objects-api-server';
import type { MSearchResult, SearchQuery } from '../../common';
import { ContentRegistry } from './registry';
import { StorageContext } from './types';
Expand All @@ -16,6 +19,10 @@ export class MSearchService {
private readonly deps: {
getSavedObjectsClient: () => Promise<SavedObjectsClientContract>;
contentRegistry: ContentRegistry;
getConfig: {
listingLimit: () => Promise<number>;
perPage: () => Promise<number>;
};
}
) {}

Expand Down Expand Up @@ -56,14 +63,36 @@ export class MSearchService {
});

const savedObjectsClient = await this.deps.getSavedObjectsClient();
const listingLimit = await this.deps.getConfig.listingLimit();
const defaultPerPage = await this.deps.getConfig.perPage();

const page = query.cursor ? Number(query.cursor) : 1;
const perPage = query.limit ? query.limit : defaultPerPage;

if (page * perPage > listingLimit) {
throw new Error(
`Requested page ${page} with ${perPage} items per page exceeds the maximum allowed limit of ${listingLimit} items`
);
}

const tagIdToSavedObjectReference = (tagId: string): SavedObjectsFindOptionsReference => ({
type: 'tag',
id: tagId,
});

const soResult = await savedObjectsClient.find({
type: soSearchTypes,

search: query.text,
searchFields: [`title^3`, `description`, ...additionalSearchFields],
defaultSearchOperator: 'AND',
// TODO: tags
// TODO: pagination
// TODO: sort

page,
perPage,

// tags
hasReference: query.tags?.included?.map(tagIdToSavedObjectReference),
hasNoReference: query.tags?.excluded?.map(tagIdToSavedObjectReference),
});

const contentItemHits = soResult.saved_objects.map((savedObject) => {
Expand All @@ -78,6 +107,11 @@ export class MSearchService {
hits: contentItemHits,
pagination: {
total: soResult.total,
cursor:
soResult.page * soResult.per_page < soResult.total &&
(soResult.page + 1) * soResult.per_page < listingLimit
? String(soResult.page + 1)
: undefined,
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ describe('RPC -> mSearch()', () => {
const mSearchService = new MSearchService({
getSavedObjectsClient: async () => savedObjectsClient,
contentRegistry,
getConfig: {
listingLimit: async () => 100,
perPage: async () => 10,
},
});

const mSearchSpy = jest.spyOn(mSearchService, 'search');
Expand Down
7 changes: 7 additions & 0 deletions src/plugins/content_management/server/rpc/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { schema } from '@kbn/config-schema';
import type { IRouter } from '@kbn/core/server';

import { LISTING_LIMIT_SETTING, PER_PAGE_SETTING } from '@kbn/saved-objects-finder-plugin/common';
import { ProcedureName } from '../../../common';
import type { ContentRegistry } from '../../core';
import { MSearchService } from '../../core/msearch';
Expand Down Expand Up @@ -60,6 +61,12 @@ export function initRpcRoutes(
getSavedObjectsClient: async () =>
(await requestHandlerContext.core).savedObjects.client,
contentRegistry,
getConfig: {
listingLimit: async () =>
(await requestHandlerContext.core).uiSettings.client.get(LISTING_LIMIT_SETTING),
perPage: async () =>
(await requestHandlerContext.core).uiSettings.client.get(PER_PAGE_SETTING),
},
}),
};
const { name } = request.params as { name: ProcedureName };
Expand Down
1 change: 1 addition & 0 deletions src/plugins/content_management/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@kbn/object-versioning",
"@kbn/core-saved-objects-api-server-mocks",
"@kbn/core-saved-objects-api-server",
"@kbn/saved-objects-finder-plugin",
],
"exclude": [
"target/**/*",
Expand Down

0 comments on commit 10f2015

Please sign in to comment.