diff --git a/redisinsight/api/src/models/redis-consumer.interface.ts b/redisinsight/api/src/models/redis-consumer.interface.ts index 8939b66d28..e42184a8d8 100644 --- a/redisinsight/api/src/models/redis-consumer.interface.ts +++ b/redisinsight/api/src/models/redis-consumer.interface.ts @@ -1,6 +1,6 @@ import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; import { ReplyError } from 'src/models/redis-client'; -import { Redis } from 'ioredis'; +import { Cluster, Redis } from 'ioredis'; export interface IRedisConsumer { execCommand( @@ -17,7 +17,7 @@ export interface IRedisConsumer { ): Promise<[ReplyError | null, any]>; execPipelineFromClient( - client: Redis, + client: Redis | Cluster, toolCommands: Array< [toolCommand: any, ...args: Array] >, diff --git a/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts b/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts index 51b5e0fbeb..720da7c50f 100644 --- a/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts @@ -27,7 +27,7 @@ import { RenameKeyDto, RenameKeyResponse, UpdateKeyTtlDto, - KeyTtlResponse, + KeyTtlResponse, GetKeysInfoDto, } from '../../dto'; @ApiTags('Keys') @@ -60,6 +60,28 @@ export class KeysController extends BaseController { ); } + @Post('get-infos') + @HttpCode(200) + @ApiOperation({ description: 'Get info for multiple keys' }) + @ApiBody({ type: GetKeysInfoDto }) + @ApiRedisParams() + @ApiOkResponse({ + description: 'Info for multiple keys', + type: GetKeysWithDetailsResponse, + }) + @ApiQueryRedisStringEncoding() + async getKeysInfo( + @Param('dbInstance') dbInstance: string, + @Body() dto: GetKeysInfoDto, + ): Promise { + return this.keysBusinessService.getKeysInfo( + { + instanceId: dbInstance, + }, + dto, + ); + } + // The key name can be very large, so it is better to send it in the request body @Post('/get-info') @HttpCode(200) diff --git a/redisinsight/api/src/modules/browser/dto/keys.dto.ts b/redisinsight/api/src/modules/browser/dto/keys.dto.ts index 4732261acd..dd4306242c 100644 --- a/redisinsight/api/src/modules/browser/dto/keys.dto.ts +++ b/redisinsight/api/src/modules/browser/dto/keys.dto.ts @@ -1,6 +1,7 @@ import { ArrayNotEmpty, IsArray, + IsBoolean, IsDefined, IsEnum, IsInt, @@ -10,7 +11,7 @@ import { Max, Min, } from 'class-validator'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { ApiProperty, ApiPropertyOptional, @@ -152,6 +153,29 @@ export class GetKeysDto { }) @IsOptional() type?: RedisDataType; + + @ApiPropertyOptional({ + description: 'Fetch keys info (type, size, ttl, length)', + type: Boolean, + default: true, + }) + @IsBoolean() + @IsOptional() + @Transform((val) => val === true || val === 'true') + keysInfo?: boolean = true; +} + +export class GetKeysInfoDto { + @ApiProperty({ + description: 'List of keys', + type: String, + isArray: true, + example: ['keys', 'key2'], + }) + @IsDefined() + @IsRedisString({ each: true }) + @RedisStringType({ each: true }) + keys: RedisString[]; } export class GetKeyInfoDto extends KeyDto {} @@ -227,7 +251,7 @@ export class GetKeyInfoResponse { @ApiProperty({ type: String, }) - type: string; + type?: string; @ApiProperty({ type: Number, @@ -235,14 +259,14 @@ export class GetKeyInfoResponse { 'The remaining time to live of a key.' + ' If the property has value of -1, then the key has no expiration time (no limit).', }) - ttl: number; + ttl?: number; @ApiProperty({ type: Number, description: 'The number of bytes that a key and its value require to be stored in RAM.', }) - size: number; + size?: number; @ApiPropertyOptional({ type: Number, diff --git a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts index 487e612502..4dc33c9e9b 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts @@ -36,6 +36,7 @@ import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-t import { BrowserToolClusterService, } from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service'; +import IORedis from 'ioredis'; import { KeysBusinessService } from './keys-business.service'; import { StringTypeInfoStrategy } from './key-info-manager/strategies/string-type-info/string-type-info.strategy'; @@ -57,6 +58,9 @@ const mockGetKeysWithDetailsResponse: GetKeysWithDetailsResponse = { keys: [getKeyInfoResponse], }; +const nodeClient = Object.create(IORedis.prototype); +nodeClient.isCluster = false; + describe('KeysBusinessService', () => { let service; let instancesBusinessService; @@ -163,6 +167,36 @@ describe('KeysBusinessService', () => { }); }); + describe('getKeysInfo', () => { + beforeEach(() => { + when(browserTool.getRedisClient) + .calledWith(mockClientOptions) + .mockResolvedValue(nodeClient); + standaloneScanner['getKeysInfo'] = jest.fn().mockResolvedValue([getKeyInfoResponse]); + }); + + it('should return keys with info', async () => { + const result = await service.getKeysInfo( + mockClientOptions, + [getKeyInfoResponse.name], + ); + + expect(result).toEqual([getKeyInfoResponse]); + }); + it("user don't have required permissions for getKeyInfo", async () => { + const replyError: ReplyError = { + ...mockRedisNoPermError, + command: 'TYPE', + }; + + standaloneScanner['getKeysInfo'] = jest.fn().mockRejectedValueOnce(replyError); + + await expect( + service.getKeysInfo(mockClientOptions, [getKeyInfoResponse.name]), + ).rejects.toThrow(ForbiddenException); + }); + }); + describe('getKeys', () => { const getKeysDto: GetKeysDto = { cursor: '0', count: 15 }; beforeEach(() => { diff --git a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts index eddbe7583e..9ca11ce58f 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts @@ -1,9 +1,5 @@ import { - BadRequestException, - Inject, - Injectable, - Logger, - NotFoundException, + BadRequestException, Inject, Injectable, Logger, NotFoundException, } from '@nestjs/common'; import { RedisErrorCodes } from 'src/constants'; import { catchAclError } from 'src/utils'; @@ -12,20 +8,19 @@ import { DeleteKeysResponse, GetKeyInfoResponse, GetKeysDto, + GetKeysInfoDto, GetKeysWithDetailsResponse, + KeyTtlResponse, + RedisDataType, RenameKeyDto, RenameKeyResponse, UpdateKeyTtlDto, - KeyTtlResponse, - RedisDataType, } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; import { InstancesBusinessService } from 'src/modules/shared/services/instances-business/instances-business.service'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { - BrowserToolClusterService, -} from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service'; +import { BrowserToolClusterService } from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service'; import { ConnectionType } from 'src/modules/core/models/database-instance.entity'; import { Scanner } from 'src/modules/browser/services/keys-business/scanner/scanner'; import { ISettingsProvider } from 'src/modules/core/models/settings-provider.interface'; @@ -34,18 +29,14 @@ import { plainToClass } from 'class-transformer'; import { StandaloneStrategy } from './scanner/strategies/standalone.strategy'; import { ClusterStrategy } from './scanner/strategies/cluster.strategy'; import { KeyInfoManager } from './key-info-manager/key-info-manager'; -import { - UnsupportedTypeInfoStrategy, -} from './key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy'; +import { UnsupportedTypeInfoStrategy } from './key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy'; import { StringTypeInfoStrategy } from './key-info-manager/strategies/string-type-info/string-type-info.strategy'; import { HashTypeInfoStrategy } from './key-info-manager/strategies/hash-type-info/hash-type-info.strategy'; import { ListTypeInfoStrategy } from './key-info-manager/strategies/list-type-info/list-type-info.strategy'; import { SetTypeInfoStrategy } from './key-info-manager/strategies/set-type-info/set-type-info.strategy'; import { ZSetTypeInfoStrategy } from './key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy'; import { StreamTypeInfoStrategy } from './key-info-manager/strategies/stream-type-info/stream-type-info.strategy'; -import { - RejsonRlTypeInfoStrategy, -} from './key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy'; +import { RejsonRlTypeInfoStrategy } from './key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy'; import { TSTypeInfoStrategy } from './key-info-manager/strategies/ts-type-info/ts-type-info.strategy'; import { GraphTypeInfoStrategy } from './key-info-manager/strategies/graph-type-info/graph-type-info.strategy'; @@ -53,7 +44,7 @@ import { GraphTypeInfoStrategy } from './key-info-manager/strategies/graph-type- export class KeysBusinessService { private logger = new Logger('KeysBusinessService'); - private scanner; + private scanner: Scanner; private keyInfoManager; @@ -148,6 +139,29 @@ export class KeysBusinessService { } } + /** + * Fetch additional keys info (type, size, ttl) + * For standalone instances will use pipeline + * For cluster instances will use single commands + * @param clientOptions + * @param dto + */ + public async getKeysInfo( + clientOptions: IFindRedisClientInstanceByOptions, + dto: GetKeysInfoDto, + ): Promise { + try { + const client = await this.browserTool.getRedisClient(clientOptions); + const scanner = this.scanner.getStrategy(client.isCluster ? ConnectionType.CLUSTER : ConnectionType.STANDALONE); + const result = await scanner.getKeysInfo(client, dto.keys); + + return plainToClass(GetKeyInfoResponse, result); + } catch (error) { + this.logger.error(`Failed to get keys info: ${error.message}.`); + throw catchAclError(error); + } + } + public async getKeyInfo( clientOptions: IFindRedisClientInstanceByOptions, key: RedisString, diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts index 840c3e1930..34fef0f5b7 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts @@ -1,6 +1,7 @@ -import { RedisDataType } from 'src/modules/browser/dto'; +import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; -import { Redis } from 'ioredis'; +import { Cluster, Redis } from 'ioredis'; +import { RedisString } from 'src/common/constants'; interface IGetKeysArgs { cursor: string; @@ -24,4 +25,10 @@ export interface IScannerStrategy { clientOptions: IFindRedisClientInstanceByOptions, args: IGetKeysArgs, ): Promise; + + getKeysInfo( + client: Redis | Cluster, + keys: RedisString[], + type?: RedisDataType, + ): Promise; } diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.spec.ts index 37b772583a..c5389bd904 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.spec.ts @@ -20,6 +20,10 @@ class TestScanStrategy implements IScannerStrategy { public async getKeys() { return []; } + + public async getKeysInfo() { + return []; + } } const strategyName = 'testStrategy'; const testStrategy = new TestScanStrategy(); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts index 9d5b4fa2cb..efc11303f7 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts @@ -22,6 +22,10 @@ const mockClientOptions: IFindRedisClientInstanceByOptions = { const nodeClient = Object.create(IORedis.prototype); +const clusterClient = Object.create(IORedis.Cluster.prototype); +clusterClient.isCluster = true; +clusterClient.sendCommand = jest.fn(); + const mockKeyInfo: GetKeyInfoResponse = { name: 'testString', type: 'string', @@ -81,6 +85,13 @@ describe('RedisScannerAbstract', () => { keys.map((key: string) => [BrowserToolKeysCommands.Type, key]), ) .mockResolvedValue([null, Array(keys.length).fill([null, 'string'])]); + when(clusterClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'type' })) + .mockResolvedValue('string') + .calledWith(jasmine.objectContaining({ name: 'ttl' })) + .mockResolvedValue(-1) + .calledWith(jasmine.objectContaining({ name: 'memory' })) + .mockResolvedValue(50); }); it('should return correct keys info', async () => { const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({ @@ -92,6 +103,16 @@ describe('RedisScannerAbstract', () => { expect(result).toEqual(mockResult); }); + it('should return correct keys info (cluster)', async () => { + const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({ + ...mockKeyInfo, + name: key, + })); + + const result = await scannerInstance.getKeysInfo(clusterClient, keys); + + expect(result).toEqual(mockResult); + }); it('should not call TYPE pipeline for keys with known type', async () => { const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({ ...mockKeyInfo, diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.ts index d6da7a0b1c..0e0cff1ef4 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.ts @@ -1,7 +1,7 @@ import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { IRedisConsumer, ReplyError } from 'src/models'; -import IORedis, { Redis, Cluster } from 'ioredis'; +import IORedis, { Redis, Cluster, Command } from 'ioredis'; import { RedisString } from 'src/common/constants'; import { IScannerStrategy } from '../scanner.interface'; @@ -54,13 +54,54 @@ export abstract class AbstractStrategy implements IScannerStrategy { } public async getKeysInfo( - client: Redis, + client: Redis | Cluster, keys: RedisString[], - type?: RedisDataType, + filterType?: RedisDataType, ): Promise { + if (client.isCluster) { + return Promise.all(keys.map(async (key) => { + let ttl; + let size; + let type; + + try { + ttl = await client.sendCommand( + new Command(BrowserToolKeysCommands.Ttl, [key], { replyEncoding: 'utf8' }), + ) as number; + } catch (e) { + // ignore error + } + + try { + size = await client.sendCommand( + new Command( + 'memory', ['usage', key, 'samples', '0'], { replyEncoding: 'utf8' }, + ), + ) as number; + } catch (e) { + // ignore error + } + + try { + type = filterType || await client.sendCommand( + new Command(BrowserToolKeysCommands.Type, [key], { replyEncoding: 'utf8' }), + ) as string; + } catch (e) { + // ignore error + } + + return { + name: key, + type, + ttl, + size, + }; + })); + } + const sizeResults = await this.getKeysSize(client, keys); - const typeResults = type - ? Array(keys.length).fill(type) + const typeResults = filterType + ? Array(keys.length).fill(filterType) : await this.getKeysType(client, keys); const ttlResults = await this.getKeysTtl(client, keys); return keys.map( @@ -74,7 +115,7 @@ export abstract class AbstractStrategy implements IScannerStrategy { } protected async getKeysTtl( - client: Redis, + client: Redis | Cluster, keys: RedisString[], ): Promise { const [ @@ -92,7 +133,7 @@ export abstract class AbstractStrategy implements IScannerStrategy { } protected async getKeysType( - client: Redis, + client: Redis | Cluster, keys: RedisString[], ): Promise { const [ @@ -110,7 +151,7 @@ export abstract class AbstractStrategy implements IScannerStrategy { } protected async getKeysSize( - client: Redis, + client: Redis | Cluster, keys: RedisString[], ): Promise { const [ diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts index f350fa17e8..a37f9867f6 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts @@ -116,7 +116,7 @@ describe('Cluster Scanner Strategy', () => { beforeEach(() => { browserTool.getNodes = jest.fn().mockResolvedValue(mockGetClusterNodes); }); - const getKeysDto: GetKeysDto = { cursor: '0', count: 15 }; + const getKeysDto: GetKeysDto = { cursor: '0', count: 15, keysInfo: true }; it('should return appropriate value with filter by type', async () => { const args = { ...getKeysDto, type: 'string', match: 'pattern*' }; when(browserTool.execCommandFromNode) @@ -778,7 +778,7 @@ describe('Cluster Scanner Strategy', () => { BrowserToolKeysCommands.InfoKeyspace, expect.anything(), expect.anything(), - ) + ) .mockRejectedValue(replyError); when(browserTool.execCommandFromNode) .calledWith( @@ -789,15 +789,15 @@ describe('Cluster Scanner Strategy', () => { null, ) .mockResolvedValue({ result: [0, [Buffer.from(getKeyInfoResponse.name)]] }); - strategy.getKeysInfo = jest - .fn() - .mockResolvedValue([getKeyInfoResponse]); - try { - await strategy.getKeys(mockClientOptions, args); - fail(); - } catch (err) { - expect(err.message).toEqual(replyError.message); - } + strategy.getKeysInfo = jest + .fn() + .mockResolvedValue([getKeyInfoResponse]); + try { + await strategy.getKeys(mockClientOptions, args); + fail(); + } catch (err) { + expect(err.message).toEqual(replyError.message); + } }); it('should throw error on scan command', async () => { const args = { ...getKeysDto }; diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts index 7e8f03fc85..6c6e8404c6 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts @@ -1,4 +1,6 @@ -import { toNumber, omit, isNull, get } from 'lodash'; +import { + toNumber, omit, isNull, get, +} from 'lodash'; import * as isGlob from 'is-glob'; import config from 'src/utils/config'; import { unescapeGlob, convertBulkStringsToObject } from 'src/utils'; @@ -85,13 +87,16 @@ export class ClusterStrategy extends AbstractStrategy { await Promise.all( nodes.map(async (node) => { - if (node.keys.length) { + if (node.keys.length && args.keysInfo) { // eslint-disable-next-line no-param-reassign node.keys = await this.getKeysInfo( node.node, node.keys, args.type, ); + } else { + // eslint-disable-next-line no-param-reassign + node.keys = node.keys.map((name) => ({ name })); } }), ); @@ -144,16 +149,16 @@ export class ClusterStrategy extends AbstractStrategy { BrowserToolKeysCommands.InfoKeyspace, [], { host: node.host, port: node.port }, - ) + ); - const info = convertBulkStringsToObject(result.result) + const info = convertBulkStringsToObject(result.result); if (!info[`db${currentDbIndex}`]) { - node.total = 0 + node.total = 0; } else { const { keys } = convertBulkStringsToObject(info[`db${currentDbIndex}`], ',', '='); node.total = parseInt(keys, 10); - } + } }), ); } diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts index 57397a6526..fc588dd9cd 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts @@ -73,7 +73,7 @@ describe('Standalone Scanner Strategy', () => { browserTool.getRedisClient.mockResolvedValue(nodeClient); }); describe('getKeys', () => { - const getKeysDto: GetKeysDto = { cursor: '0', count: 15 }; + const getKeysDto: GetKeysDto = { cursor: '0', count: 15, keysInfo: true }; it('should return appropriate value with filter by type', async () => { const args = { ...getKeysDto, type: 'string', match: 'pattern*' }; @@ -115,6 +115,42 @@ describe('Standalone Scanner Strategy', () => { null, ); }); + it('should return keys names only', async () => { + const args = { + ...getKeysDto, type: 'string', match: 'pattern*', keysInfo: false, + }; + + when(browserTool.execCommand) + .calledWith( + mockClientOptions, + BrowserToolKeysCommands.Scan, + expect.anything(), + null, + ) + .mockResolvedValue([0, [getKeyInfoResponse.name]]); + when(browserTool.execCommand) + .calledWith( + mockClientOptions, + BrowserToolKeysCommands.InfoKeyspace, + expect.anything(), + 'utf8', + ) + .mockResolvedValue(mockRedisKeyspaceInfoResponse_1); + + strategy.getKeysInfo = jest.fn(); + + const result = await strategy.getKeys(mockClientOptions, args); + + expect(strategy.getKeysInfo).not.toHaveBeenCalled(); + expect(result).toEqual([ + { + ...mockNodeEmptyResult, + total: 1, + scanned: getKeysDto.count, + keys: [{ name: getKeyInfoResponse.name }], + }, + ]); + }); it('should call scan 3 times and return appropriate value', async () => { when(browserTool.execCommand) .calledWith(mockClientOptions, BrowserToolKeysCommands.Scan, [ diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts index 8221615140..224b81b865 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts @@ -80,8 +80,10 @@ export class StandaloneStrategy extends AbstractStrategy { await this.scan(clientOptions, node, match, count, args.type); - if (node.keys.length) { + if (node.keys.length && args.keysInfo) { node.keys = await this.getKeysInfo(client, node.keys, args.type); + } else { + node.keys = node.keys.map((name) => ({ name })); } return [node]; diff --git a/redisinsight/api/test/api/keys/GET-instance-id-keys.test.ts b/redisinsight/api/test/api/keys/GET-instance-id-keys.test.ts index 4c24e4046d..1567fd6ca5 100644 --- a/redisinsight/api/test/api/keys/GET-instance-id-keys.test.ts +++ b/redisinsight/api/test/api/keys/GET-instance-id-keys.test.ts @@ -25,9 +25,9 @@ const responseSchema = Joi.array().items(Joi.object().keys({ port: Joi.number().integer(), keys: Joi.array().items(Joi.object().keys({ name: JoiRedisString.required(), - type: Joi.string().required(), - ttl: Joi.number().integer().required(), - size: Joi.number().integer(), // todo: fix size pipeline for cluster + type: Joi.string(), + ttl: Joi.number().integer(), + size: Joi.number(), // todo: fix size pipeline for cluster })).required(), }).required()).required(); @@ -315,8 +315,44 @@ describe('GET /instance/:instanceId/keys', () => { expect(result.total).to.eql(KEYS_NUMBER); expect(result.scanned).to.gte(KEYS_NUMBER); expect(result.keys.length).to.gte(10); - result.keys.map(({ name }) => { + result.keys.map(({ name, type, ttl, size }) => { expect(name.indexOf(`${constants.TEST_RUN_ID}_str_key_10`)).to.eql(0); + expect(type).to.be.a('string'); + expect(ttl).to.be.a('number'); + expect(size).to.be.a('number'); + }) + } + }, + { + name: 'Should search by with ? in the end (without keys info)', + query: { + cursor: '0', + match: `${constants.TEST_RUN_ID}_str_key_10?`, + keysInfo: 'false', + }, + responseSchema, + checkFn: ({ body }) => { + const result = { + total: 0, + scanned: 0, + keys: [], + numberOfShards: 0, + }; + + body.map(shard => { + result.total += shard.total; + result.scanned += shard.scanned; + result.keys.push(...shard.keys); + result.numberOfShards++; + }); + expect(result.total).to.eql(KEYS_NUMBER); + expect(result.scanned).to.gte(KEYS_NUMBER); + expect(result.keys.length).to.gte(10); + result.keys.map(({ name, type, ttl, size }) => { + expect(name.indexOf(`${constants.TEST_RUN_ID}_str_key_10`)).to.eql(0); + expect(type).to.eql(undefined); + expect(ttl).to.eql(undefined); + expect(size).to.eql(undefined); }) } }, @@ -865,8 +901,53 @@ describe('GET /instance/:instanceId/keys', () => { expect(result.total).to.eql(KEYS_NUMBER); expect(result.scanned).to.gte(200 * result.numberOfShards); expect(result.keys.length).to.gte(200); - result.keys.map(key => expect(key.name).to.have.string('str_key_')); - result.keys.map(key => expect(key.type).to.eql('string')); + result.keys.map(key => { + expect(key.name).to.have.string('str_key_'); + expect(key.type).to.eql('string'); + expect(key.size).to.be.a('number'); + expect(key.ttl).to.be.a('number'); + }); + } + }, + ].map(mainCheckFn); + }); + describe('Filter by type (w/o keys info)', () => { + requirements('rte.version>=6.0'); + [ + { + name: 'Should filter by type (string)', + query: { + cursor: '0', + type: 'string', + count: 200, + keysInfo: false, + }, + responseSchema, + checkFn: ({ body }) => { + const result = { + total: 0, + scanned: 0, + keys: [], + numberOfShards: 0, + }; + + body.map(shard => { + result.total += shard.total; + result.scanned += shard.scanned; + result.keys.push(...shard.keys); + result.numberOfShards++; + expect(shard.scanned).to.gte(200); + expect(shard.scanned).to.lte(KEYS_NUMBER); + }); + expect(result.total).to.eql(KEYS_NUMBER); + expect(result.scanned).to.gte(200 * result.numberOfShards); + expect(result.keys.length).to.gte(200); + result.keys.map(key => { + expect(key.name).to.have.string('str_key_'); + expect(key.ttl).to.eq(undefined); + expect(key.size).to.eq(undefined); + expect(key.type).to.eq(undefined); + }); } }, ].map(mainCheckFn); diff --git a/redisinsight/api/test/api/keys/POST-instance-id-keys-get_infos.test.ts b/redisinsight/api/test/api/keys/POST-instance-id-keys-get_infos.test.ts new file mode 100644 index 0000000000..59a9dbdb91 --- /dev/null +++ b/redisinsight/api/test/api/keys/POST-instance-id-keys-get_infos.test.ts @@ -0,0 +1,138 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall, + JoiRedisString, +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).post(`/instance/${instanceId}/keys/get-infos`); + +const responseSchema = Joi.array().items(Joi.object().keys({ + name: JoiRedisString.required(), + type: Joi.string().required(), + ttl: Joi.number().integer().required(), + size: Joi.number().integer().allow(null).required(), +})).required(); + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + }); +}; + +describe('POST /instance/:instanceId/keys/get-infos', () => { + before(async () => await rte.data.generateKeys(true)); + + describe('Modes', () => { + requirements('!rte.bigData'); + before(rte.data.generateBinKeys); + + [ + { + name: 'Should return string info in utf8 (default)', + data: { + keys: [constants.TEST_STRING_KEY_BIN_BUF_OBJ_1], + }, + responseSchema, + checkFn: ({ body }) => { + expect(body[0].name).to.eq(constants.TEST_STRING_KEY_BIN_UTF8_1); + } + }, + { + name: 'Should return string info in utf8', + query: { + encoding: 'utf8', + }, + data: { + keys: [constants.TEST_STRING_KEY_BIN_BUF_OBJ_1], + }, + responseSchema, + checkFn: ({ body }) => { + expect(body[0].name).to.eq(constants.TEST_STRING_KEY_BIN_UTF8_1); + } + }, + { + name: 'Should return string info in ASCII', + query: { + encoding: 'ascii', + }, + data: { + keys: [constants.TEST_STRING_KEY_BIN_ASCII_1], + }, + responseSchema, + checkFn: ({ body }) => { + expect(body[0].name).to.eq(constants.TEST_STRING_KEY_BIN_ASCII_1); + } + }, + { + name: 'Should return string info in Buffer', + query: { + encoding: 'buffer', + }, + data: { + keys: [constants.TEST_STRING_KEY_BIN_BUF_OBJ_1], + }, + responseSchema, + checkFn: ({ body }) => { + expect(body[0].name).to.deep.eq(constants.TEST_STRING_KEY_BIN_BUF_OBJ_1); + } + }, + { + name: 'Should return error when send unicode with unprintable chars', + query: { + encoding: 'utf8', + }, + data: { + keys: [constants.TEST_STRING_KEY_BIN_UTF8_1], + }, + responseSchema, + checkFn: ({ body }) => { + expect(body[0].name).to.deep.eq(constants.TEST_STRING_KEY_BIN_UTF8_1); + expect(body[0].ttl).to.deep.eq(-2); + expect(body[0].size).to.deep.eq(null); + expect(body[0].type).to.deep.eq('none'); + } + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should return key info', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + keys: [constants.TEST_STRING_KEY_1], + }, + }, + { + name: 'Should not throw error if no acl permissions', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + before: () => rte.data.setAclUserRules('~* +@all -ttl -memory -type'), + data: { + keys: [constants.TEST_STRING_KEY_1], + }, + }, + ].map(mainCheckFn); + }); +});