From 7eee9c9b7d882dca71ab22700ea39b67b75e15ce Mon Sep 17 00:00:00 2001 From: Luke Bayas Date: Sun, 12 Apr 2020 15:42:03 -0400 Subject: [PATCH 1/2] fix scan to return array --- lib/adapters/RedisAdapter.ts | 163 ++++++++++++++++++++++++++++------- 1 file changed, 134 insertions(+), 29 deletions(-) diff --git a/lib/adapters/RedisAdapter.ts b/lib/adapters/RedisAdapter.ts index 5321fc51..9df74cad 100644 --- a/lib/adapters/RedisAdapter.ts +++ b/lib/adapters/RedisAdapter.ts @@ -2,21 +2,75 @@ import { RedisClient, Callback } from 'redis'; import { CacheClient } from '../interfaces'; import { parseIfRequired } from '../util'; +// In order to support scalars in hmsets (likely not the intended use, but support has been requested), +// we need at least one key. We can use an empty string. +const SCALAR_KEY = ''; + +// When values are returned from redis, numbers can be converted to strings, so we need to store them +// in a way that we can differentiate them from numbers that were intentionally stored as strings +const NUMBER_IDENTIFIER = 'n'; +const BOOL_IDENTIFIER = 'b'; + export class RedisAdapter implements CacheClient { static buildSetArgumentsFromObject = (objectValue: any): string[] => - Object.keys(objectValue).reduce( - (accum: any, curr: any) => { - accum.push( - curr, - typeof objectValue[curr] === 'object' - ? JSON.stringify(objectValue[curr]) - : objectValue[curr], - ); + Object.keys(objectValue).reduce((accum: any, objectKey: any) => { + let value = objectValue[objectKey]; + + switch (typeof value) { + case 'object': { + value = JSON.stringify(value); + break; + } + case 'number': { + value = `${value}${NUMBER_IDENTIFIER}`; + break; + } + case 'boolean': { + value = `${value}${BOOL_IDENTIFIER}` + break; + } + default: + break; + } + + accum.push(objectKey, value); + + return accum; + }, [] as string[]); + + static transformRedisResponse = (response: any) => { + if (response && typeof response === 'object') { + return Object.entries(response).reduce((accum: any, curr: any[]) => { + const [key, value] = curr; + + switch (typeof value) { + case 'string': { + if ( + value.endsWith(NUMBER_IDENTIFIER) && + parseFloat(value).toString() === value.substr(0, value.length - 1) + ) { + accum[key] = parseFloat(value); + break; + } else if( + value.endsWith(BOOL_IDENTIFIER) && + (value === "false"+BOOL_IDENTIFIER || value === "true" + BOOL_IDENTIFIER) + ) { + accum[key] = value === "true" + BOOL_IDENTIFIER; + break; + } + } + default: { + accum[key] = value; + break; + } + } return accum; - }, - [] as string[], - ); + }, {}); + } + + return response; + }; static responseCallback = (resolve: Function, reject: Function): Callback => ( err: any, @@ -25,7 +79,30 @@ export class RedisAdapter implements CacheClient { if (err) { reject(err); } else { - resolve(response); + if ( + response && + typeof response === 'object' && + Object.keys(response).length === 1 && + response[SCALAR_KEY] + ) { + resolve(RedisAdapter.transformRedisResponse(response)[SCALAR_KEY]); + return; + } + + resolve(RedisAdapter.transformRedisResponse(response)); + } + }; + + static responseScanCommandCallback = (resolve: Function, reject: Function): Callback => ( + err: any, + response: any, + ) => { + if (err) { + reject(err); + } else { + // array exists at index '1' from SCAN command + resolve(response['1']); + return; } }; @@ -113,24 +190,45 @@ export class RedisAdapter implements CacheClient { if (isReady) { return new Promise((resolve, reject) => { - if (cacheKey.includes(':') && typeof value === 'object') { - const args = RedisAdapter.buildSetArgumentsFromObject(value); - - this.redisClient.hmset(cacheKey, args, (err, result) => { - if (!err) { - // hmset doesn't add expiration by default, so we have to implement that here if ttl is given - if (ttl) { - this.redisClient.expire( - cacheKey, - ttl, - RedisAdapter.responseCallback(resolve, reject), - ); - return; + if (cacheKey.includes(':')) { + if (typeof value === 'object') { + const args = RedisAdapter.buildSetArgumentsFromObject(value); + this.redisClient.hmset(cacheKey, args, (err, result) => { + if (!err) { + // hmset doesn't add expiration by default, so we have to implement that here if ttl is given + if (ttl) { + this.redisClient.expire( + cacheKey, + ttl, + RedisAdapter.responseCallback(resolve, reject), + ); + return; + } } - } - RedisAdapter.responseCallback(resolve, reject)(err, result); - }); + RedisAdapter.responseCallback(resolve, reject)(err, result); + }); + } else { + this.redisClient.hmset( + cacheKey, + RedisAdapter.buildSetArgumentsFromObject({ [SCALAR_KEY]: value }), + (err, result) => { + if (!err) { + // hset doesn't add expiration by default, so we have to implement that here if ttl is given + if (ttl) { + this.redisClient.expire( + cacheKey, + ttl, + RedisAdapter.responseCallback(resolve, reject), + ); + return; + } + } + + RedisAdapter.responseCallback(resolve, reject)(err, result); + }, + ); + } } else { const usableValue = typeof value === 'string' ? value : JSON.stringify(value); @@ -173,7 +271,14 @@ export class RedisAdapter implements CacheClient { if (isReady) { return new Promise((resolve, reject) => { - this.redisClient.scan('0', 'MATCH', `*${pattern}*`, 'COUNT', '1000', RedisAdapter.responseCallback(resolve, reject)); + this.redisClient.scan( + '0', + 'MATCH', + `*${pattern}*`, + 'COUNT', + '1000', + RedisAdapter.responseScanCommandCallback(resolve, reject), + ); }); } From 3014edfc0052dbfdce8ef29d3ee09b2ae70d2ca8 Mon Sep 17 00:00:00 2001 From: Luke Bayas Date: Sun, 12 Apr 2020 16:16:32 -0400 Subject: [PATCH 2/2] minor update --- lib/adapters/RedisAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/adapters/RedisAdapter.ts b/lib/adapters/RedisAdapter.ts index 9df74cad..fc35c285 100644 --- a/lib/adapters/RedisAdapter.ts +++ b/lib/adapters/RedisAdapter.ts @@ -284,4 +284,4 @@ export class RedisAdapter implements CacheClient { throw new Error('Redis client is not accepting connections.'); } -} +} \ No newline at end of file