diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 865b996805..65f9e7d0ed 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -53,6 +53,7 @@ "body-parser": "^1.19.0", "class-transformer": "^0.2.3", "class-validator": "^0.12.2", + "date-fns": "^2.29.3", "dotenv": "^16.0.0", "express": "^4.17.1", "fs-extra": "^10.0.0", diff --git a/redisinsight/api/src/constants/regex.ts b/redisinsight/api/src/constants/regex.ts index 8506d70ccd..08207685e3 100644 --- a/redisinsight/api/src/constants/regex.ts +++ b/redisinsight/api/src/constants/regex.ts @@ -1,6 +1,7 @@ export const ARG_IN_QUOTATION_MARKS_REGEX = /"[^"]*|'[^']*'|"+/g; export const IS_INTEGER_NUMBER_REGEX = /^\d+$/; +export const IS_NUMBER_REGEX = /^-?\d*(\.\d+)?$/; export const IS_NON_PRINTABLE_ASCII_CHARACTER = /[^ -~\u0007\b\t\n\r]/; export const IP_ADDRESS_REGEX = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; export const PRIVATE_IP_ADDRESS_REGEX = /(^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/; -export const IS_TIMESTAMP = /^\d{10,}$/; +export const IS_TIMESTAMP = /^(\d{10}|\d{13}|\d{16}|\d{19})$/; diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 760a5a25a1..0a347eea03 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -37,13 +37,39 @@ const mockRedisAclListResponse_2: string[] = [ const mockFTListResponse_1 = []; const mockFTListResponse_2 = ['idx']; +const generateRTSRecommendationTests = [ + { input: ['0', ['123', 123]], expected: null }, + { input: ['0', ['1234567891', 3]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891', 1234567891]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['123', 1234567891]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['123', 12345678911]], expected: null }, + { input: ['0', ['123', 1234567891234]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['123', 12345678912345]], expected: null }, + { input: ['0', ['123', 1234567891234567]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['12345678912345678', 1]], expected: null }, + { input: ['0', ['1234567891234567891', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1', 1234567891.2]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891.2', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891:12', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891a12', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891.2.2', 1]], expected: null }, + { input: ['0', ['1234567891asd', 1]], expected: null }, + { input: ['0', ['10-10-2020', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['', 1]], expected: null }, + { input: ['0', ['1', -12]], expected: null }, + { input: ['0', ['1', -1234567891]], expected: null }, + { input: ['0', ['1', -1234567891.123]], expected: null }, + { input: ['0', ['1', -1234567891.123]], expected: null }, + { input: ['0', ['1234567891.-123', 1]], expected: null }, +]; + const mockZScanResponse_1 = [ '0', - [123456789, 123456789, 12345678910, 12345678910], + ['1', 1, '12345678910', 12345678910], ]; const mockZScanResponse_2 = [ '0', - [12345678910, 12345678910, 1, 1], + ['12345678910', 12345678910, 1, 1], ]; const mockKeys = [ @@ -489,28 +515,14 @@ describe('RecommendationProvider', () => { }); describe('determineRTSRecommendation', () => { - it('should not return RTS recommendation', async () => { + test.each(generateRTSRecommendationTests)('%j', async ({ input, expected }) => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'zscan' })) - .mockResolvedValue(mockZScanResponse_1); + .mockResolvedValue(input); const RTSRecommendation = await service .determineRTSRecommendation(nodeClient, mockKeys); - expect(RTSRecommendation).toEqual(null); - }); - - it('should return RTS recommendation', async () => { - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'zscan' })) - .mockResolvedValueOnce(mockZScanResponse_1); - - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'zscan' })) - .mockResolvedValue(mockZScanResponse_2); - - const RTSRecommendation = await service - .determineRTSRecommendation(nodeClient, mockSortedSets); - expect(RTSRecommendation).toEqual({ name: RECOMMENDATION_NAMES.RTS }); + expect(RTSRecommendation).toEqual(expected); }); it('should not return RTS recommendation when only 101 sorted set contain timestamp', async () => { diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 291578e88b..b79f301ef5 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -1,9 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; import { Redis, Cluster, Command } from 'ioredis'; -import { get } from 'lodash'; +import { get, isNumber } from 'lodash'; +import { isValid } from 'date-fns'; import * as semverCompare from 'node-version-compare'; -import { convertRedisInfoReplyToObject, convertBulkStringsToObject, convertStringsArrayToObject } from 'src/utils'; -import { RECOMMENDATION_NAMES, IS_TIMESTAMP } from 'src/constants'; +import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils'; +import { + RECOMMENDATION_NAMES, IS_TIMESTAMP, IS_INTEGER_NUMBER_REGEX, IS_NUMBER_REGEX, +} from 'src/constants'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; @@ -311,8 +314,7 @@ export class RecommendationProvider { // get first member-score pair new Command('zscan', [keys[processedKeysNumber].name, '0', 'COUNT', 2], { replyEncoding: 'utf8' }), ) as string[]; - // check is pair member-score is timestamp - if (IS_TIMESTAMP.test(membersArray[0]) && IS_TIMESTAMP.test(membersArray[1])) { + if (this.checkTimestamp(membersArray[0]) || this.checkTimestamp(membersArray[1])) { isTimeSeries = true; } processedKeysNumber += 1; @@ -421,4 +423,25 @@ export class RecommendationProvider { } return false; } + + private checkTimestamp(value: string): boolean { + try { + if (!IS_NUMBER_REGEX.test(value) && isValid(new Date(value))) { + return true; + } + const integerPart = parseInt(value, 10); + if (!IS_TIMESTAMP.test(integerPart.toString())) { + return false; + } + if (isNumber(value) || integerPart.toString().length === value.length) { + return true; + } + // check part after separator + const subPart = value.replace(integerPart.toString(), ''); + return IS_INTEGER_NUMBER_REGEX.test(subPart.substring(1, subPart.length)); + } catch (err) { + // ignore errors + return false; + } + } } diff --git a/redisinsight/api/yarn.lock b/redisinsight/api/yarn.lock index beaebe7663..0c4a40f848 100644 --- a/redisinsight/api/yarn.lock +++ b/redisinsight/api/yarn.lock @@ -2626,6 +2626,11 @@ date-fns@^2.28.0: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931" integrity sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA== +date-fns@^2.29.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"