diff --git a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts index c15a185a32..8ff0407dcf 100644 --- a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts +++ b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts @@ -1,5 +1,5 @@ import { Cluster, Command, Redis } from 'ioredis'; -import { isNull, toNumber, uniq } from 'lodash'; +import { isUndefined, toNumber, uniq } from 'lodash'; import { BadRequestException, ConflictException, @@ -26,7 +26,7 @@ import { CreateBrowserHistoryDto } from '../../dto/browser-history/create.browse @Injectable() export class RedisearchService { - private maxSearchResults: number | null = null; + private maxSearchResults: Map = new Map(); private logger = new Logger('RedisearchService'); @@ -153,24 +153,26 @@ export class RedisearchService { const client = await this.browserTool.getRedisClient(clientMetadata); - if (isNull(this.maxSearchResults)) { + if (isUndefined(this.maxSearchResults.get(clientMetadata.databaseId))) { try { + // response: [ [ 'MAXSEARCHRESULTS', '10000' ] ] const [[, maxSearchResults]] = await client.sendCommand( - // response: [ [ 'MAXSEARCHRESULTS', '10000' ] ] new Command('FT.CONFIG', ['GET', 'MAXSEARCHRESULTS'], { replyEncoding: 'utf8', }), ) as [[string, string]]; - this.maxSearchResults = toNumber(maxSearchResults); + this.maxSearchResults.set(clientMetadata.databaseId, toNumber(maxSearchResults)); } catch (error) { - this.maxSearchResults = null; + this.maxSearchResults.set(clientMetadata.databaseId, null); } } // Workaround: recalculate limit to not query more then MAXSEARCHRESULTS let safeLimit = limit; - if (this.maxSearchResults && offset + limit > this.maxSearchResults) { - safeLimit = offset <= this.maxSearchResults ? this.maxSearchResults - offset : limit; + const maxSearchResult = this.maxSearchResults.get(clientMetadata.databaseId) + + if (maxSearchResult && offset + limit > maxSearchResult) { + safeLimit = offset <= maxSearchResult ? maxSearchResult - offset : limit; } const [total, ...keyNames] = await client.sendCommand( @@ -193,7 +195,7 @@ export class RedisearchService { total, scanned: keyNames.length + offset, keys: keyNames.map((name) => ({ name })), - maxResults: this.maxSearchResults, + maxResults: maxSearchResult, }); } catch (e) { this.logger.error('Failed to search keys using redisearch index', e); diff --git a/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-search.test.ts b/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-search.test.ts index f4fa0dfa30..1dc7c2eb2c 100644 --- a/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-search.test.ts +++ b/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-search.test.ts @@ -10,7 +10,7 @@ import { validateInvalidDataTestCase, getMainCheckFn, JoiRedisString } from '../deps'; -const { server, request, constants, rte } = deps; +const { server, request, constants, rte, localDb } = deps; // endpoint to test const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => @@ -44,10 +44,13 @@ const mainCheckFn = getMainCheckFn(endpoint); describe('POST /databases/:id/redisearch/search', () => { requirements('!rte.bigData', 'rte.modules.search'); - before(async () => rte.data.generateRedisearchIndexes(true)); - beforeEach(() => rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '10000')); + before(async () => { + await rte.data.generateRedisearchIndexes(true) + await localDb.createTestDbInstance(rte, {}, { id: constants.TEST_INSTANCE_ID_2 }) + }); describe('Main', () => { + before(() => rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '10000')); describe('Validation', () => { generateInvalidDataTestCases(dataSchema, validInputData).map( validateInvalidDataTestCase(endpoint, dataSchema), @@ -84,8 +87,14 @@ describe('POST /databases/:id/redisearch/search', () => { expect(body.maxResults).to.gte(10000); }, }, + ].map(mainCheckFn); + }); + + describe('maxSearchResults', () => { + [ { name: 'Should modify limit to not exceed available search limitation', + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), data: { ...validInputData, offset: 0, @@ -98,10 +107,13 @@ describe('POST /databases/:id/redisearch/search', () => { expect(body.total).to.eq(2000); expect(body.maxResults).to.gte(1); }, - before: () => rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '1'), + before: async () => { + await rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '1') + }, }, { name: 'Should return custom error message if MAXSEARCHRESULTS less than request.limit', + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), data: { ...validInputData, offset: 10, @@ -113,16 +125,35 @@ describe('POST /databases/:id/redisearch/search', () => { error: 'Bad Request', message: `Set MAXSEARCHRESULTS to at least ${numberWithSpaces(validInputData.limit)}.`, }, - before: () => rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '1'), + before: async () => { + await rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '1') + }, }, ].map(mainCheckFn); }); describe('ACL', () => { requirements('rte.acl'); - before(async () => rte.data.setAclUserRules('~* +@all')); + before(async () => { + await rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '10000') + await rte.data.setAclUserRules('~* +@all') + }); [ + { + name: 'Should return response with maxResults = null if no permissions for "ft.config" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: validInputData, + responseSchema, + checkFn: async ({ body }) => { + expect(body.keys.length).to.eq(10); + expect(body.cursor).to.eq(10); + expect(body.scanned).to.eq(10); + expect(body.total).to.eq(2000); + expect(body.maxResults).to.eq(null); + }, + before: () => rte.data.setAclUserRules('~* +@all -ft.config') + }, { name: 'Should search', endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), @@ -143,20 +174,6 @@ describe('POST /databases/:id/redisearch/search', () => { }, before: () => rte.data.setAclUserRules('~* +@all -ft.search') }, - { - name: 'Should return response with maxResults = null if no permissions for "ft.config" command', - endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), - data: validInputData, - responseSchema, - checkFn: async ({ body }) => { - expect(body.keys.length).to.eq(10); - expect(body.cursor).to.eq(10); - expect(body.scanned).to.eq(10); - expect(body.total).to.eq(2000); - expect(body.maxResults).to.eq(null); - }, - before: () => rte.data.setAclUserRules('~* +@all -ft.config') - }, ].map(mainCheckFn); }); }); diff --git a/redisinsight/api/test/helpers/local-db.ts b/redisinsight/api/test/helpers/local-db.ts index 0c040b35ea..8a5842535e 100644 --- a/redisinsight/api/test/helpers/local-db.ts +++ b/redisinsight/api/test/helpers/local-db.ts @@ -262,7 +262,7 @@ const createClientCertificate = async (certificate) => { return rep.save(certificate); } -const createTesDbInstance = async (rte, server): Promise => { +export const createTestDbInstance = async (rte, server, data: any = {}): Promise => { const rep = await getRepository(repositories.DATABASE); const instance: any = { @@ -324,7 +324,7 @@ const createTesDbInstance = async (rte, server): Promise => { passphrase: encryptData(constants.TEST_SSH_PASSPHRASE), }; } - await rep.save(instance); + await rep.save({ ...instance, ...data}); } export const createDatabaseInstances = async () => { @@ -524,7 +524,7 @@ const truncateAll = async () => { export const initLocalDb = async (rte, server) => { await truncateAll(); - await createTesDbInstance(rte, server); + await createTestDbInstance(rte, server); await initAgreements(); if (rte.env.acl) { await createAclInstance(rte, server);