From 476da6d7ae774a7594849daf7f5b5bccc0d0990d Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 11 Mar 2025 19:11:56 +0200 Subject: [PATCH 1/6] [CAE-686] Added hash field expiration commands --- packages/client/lib/commands/HGETDEL.spec.ts | 49 +++++++ packages/client/lib/commands/HGETDEL.ts | 13 ++ packages/client/lib/commands/HGETEX.spec.ts | 79 +++++++++++ packages/client/lib/commands/HGETEX.ts | 42 ++++++ packages/client/lib/commands/HSETEX.spec.ts | 99 ++++++++++++++ packages/client/lib/commands/HSETEX.ts | 124 ++++++++++++++++++ .../lib/commands/generic-transformers.ts | 2 +- packages/client/lib/commands/index.ts | 9 ++ packages/client/lib/test-utils.ts | 2 + 9 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 packages/client/lib/commands/HGETDEL.spec.ts create mode 100644 packages/client/lib/commands/HGETDEL.ts create mode 100644 packages/client/lib/commands/HGETEX.spec.ts create mode 100644 packages/client/lib/commands/HGETEX.ts create mode 100644 packages/client/lib/commands/HSETEX.spec.ts create mode 100644 packages/client/lib/commands/HSETEX.ts diff --git a/packages/client/lib/commands/HGETDEL.spec.ts b/packages/client/lib/commands/HGETDEL.spec.ts new file mode 100644 index 00000000000..ab446c9599b --- /dev/null +++ b/packages/client/lib/commands/HGETDEL.spec.ts @@ -0,0 +1,49 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { BasicCommandParser } from '../client/parser'; +import HGETDEL from './HGETDEL'; + +describe('HGETDEL parseCommand', () => { + it('hGetDel parseCommand base', () => { + const parser = new BasicCommandParser; + HGETDEL.parseCommand(parser, 'key', 'field'); + assert.deepEqual(parser.redisArgs, ['HGETDEL', 'key', 'FIELDS', '1', 'field']); + }); + + it('hGetDel parseCommand variadic', () => { + const parser = new BasicCommandParser; + HGETDEL.parseCommand(parser, 'key', ['field1', 'field2']); + assert.deepEqual(parser.redisArgs, ['HGETDEL', 'key', 'FIELDS', '2', 'field1', 'field2']); + }); +}); + + +// TODO: enable when new test container is released +describe.skip('HGETDEL call', () => { + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel empty single field', async client => { + assert.deepEqual( + await client.hGetDel('key', 'filed1'), + [null] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel empty multiple fields', async client => { + assert.deepEqual( + await client.hGetDel('key', ['filed1', 'field2']), + [null, null] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel partially populated multiple fields', async client => { + await client.hSet('key', 'field1', 'value1') + assert.deepEqual( + await client.hGetDel('key', ['filed1', 'field2']), + ['value1', null] + ); + + assert.deepEqual( + await client.hGetDel('key', 'field1'), + [null] + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/HGETDEL.ts b/packages/client/lib/commands/HGETDEL.ts new file mode 100644 index 00000000000..a0326c425ea --- /dev/null +++ b/packages/client/lib/commands/HGETDEL.ts @@ -0,0 +1,13 @@ +import { CommandParser } from '../client/parser'; +import { RedisVariadicArgument } from './generic-transformers'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + parseCommand(parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument) { + parser.push('HGETDEL'); + parser.pushKey(key); + parser.push('FIELDS') + parser.pushVariadicWithLength(fields); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HGETEX.spec.ts b/packages/client/lib/commands/HGETEX.spec.ts new file mode 100644 index 00000000000..03beb3e7f85 --- /dev/null +++ b/packages/client/lib/commands/HGETEX.spec.ts @@ -0,0 +1,79 @@ +import { strict as assert } from 'node:assert'; +import testUtils,{ GLOBAL, sleep } from '../test-utils'; +import { BasicCommandParser } from '../client/parser'; +import HGETEX from './HGETEX'; + +describe('HGETEX parseCommand', () => { + it('hGetEx parseCommand base', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', 'field'); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'FIELDS', '1', 'field']); + }); + + it('hGetEx parseCommand expiration PERSIST string', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', 'field', {expiration: 'PERSIST'}); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'PERSIST', 'FIELDS', '1', 'field']); + }); + + it('hGetEx parseCommand expiration PERSIST obj', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', 'field', {expiration: {type: 'PERSIST'}}); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'PERSIST', 'FIELDS', '1', 'field']); + }); + + it('hGetEx parseCommand expiration EX obj', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', 'field', {expiration: {type: 'EX', value: 1000}}); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'EX', '1000', 'FIELDS', '1', 'field']); + }); + + it('hGetEx parseCommand expiration EXAT obj variadic', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', ['field1', 'field2'], {expiration: {type: 'EXAT', value: 1000}}); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'EXAT', '1000', 'FIELDS', '2', 'field1', 'field2']); + }); +}); + + +// TODO: enable when new test container is released +describe.skip('HGETEX call', () => { + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx empty single field', async client => { + assert.deepEqual( + await client.hGetEx('key', 'filed1', {expiration: 'PERSIST'}), + [null] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx empty multiple fields', async client => { + assert.deepEqual( + await client.hGetEx('key', ['filed1', 'field2'], {expiration: 'PERSIST'}), + [null, null] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx set expiry', async client => { + await client.hSet('key', 'field', 'value') + assert.deepEqual( + await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 500}}), + ['value'] + ); + + await sleep(600) + assert.deepEqual( + await client.hGet('key', 'field'), + null + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'gGetEx set expiry PERSIST', async client => { + await client.hSet('key', 'field', 'value') + await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 500}}) + await client.hGetEx('key', 'field', {expiration: 'PERSIST'}) + await sleep(600) + assert.deepEqual( + await client.hGet('key', 'field'), + 'value' + ) + }, GLOBAL.SERVERS.OPEN); +}); \ No newline at end of file diff --git a/packages/client/lib/commands/HGETEX.ts b/packages/client/lib/commands/HGETEX.ts new file mode 100644 index 00000000000..ce265e15bd6 --- /dev/null +++ b/packages/client/lib/commands/HGETEX.ts @@ -0,0 +1,42 @@ +import { CommandParser } from '../client/parser'; +import { RedisVariadicArgument } from './generic-transformers'; +import { ArrayReply, Command, BlobStringReply, NullReply, RedisArgument } from '../RESP/types'; + +export interface HGetExOptions { + expiration?: { + type: 'EX' | 'PX' | 'EXAT' | 'PXAT'; + value: number; + } | { + type: 'PERSIST'; + } | 'PERSIST'; +} + +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument, + options?: HGetExOptions + ) { + parser.push('HGETEX'); + parser.pushKey(key); + + if (options?.expiration) { + if (typeof options.expiration === 'string') { + parser.push(options.expiration); + } else if (options.expiration.type === 'PERSIST') { + parser.push('PERSIST'); + } else { + parser.push( + options.expiration.type, + options.expiration.value.toString() + ); + } + } + + parser.push('FIELDS') + + parser.pushVariadicWithLength(fields); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HSETEX.spec.ts b/packages/client/lib/commands/HSETEX.spec.ts new file mode 100644 index 00000000000..4dc80129c13 --- /dev/null +++ b/packages/client/lib/commands/HSETEX.spec.ts @@ -0,0 +1,99 @@ +import { strict as assert } from 'node:assert'; +import testUtils,{ GLOBAL, sleep } from '../test-utils'; +import { BasicCommandParser } from '../client/parser'; +import HSETEX from './HSETEX'; + +describe('HSETEX parseCommand', () => { + it('hSetEx parseCommand base', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', {}, 'field', 'value'); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '1', 'field', 'value']); + }); + + it('hSetEx parseCommand base empty obj', () => { + const parser = new BasicCommandParser; + assert.throws(() => {HSETEX.parseCommand(parser, 'key', {}, {})}); + }); + + it('hSetEx parseCommand base one key obj', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', {}, {'k': 'v'}); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '1', 'k', 'v']); + }); + + it('hSetEx parseCommand array', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', {}, ['field1', 'value1', 'field2', 'value2']); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); + }); + + it('hSetEx parseCommand array invalid args, thows error', () => { + const parser = new BasicCommandParser; + assert.throws(() => {HSETEX.parseCommand(parser, 'key', {}, ['field1', 'value1', 'field2'])}); + }); + + it('hSetEx parseCommand array in array', () => { + const parser1 = new BasicCommandParser; + HSETEX.parseCommand(parser1, 'key', {}, [['field1', 'value1'], ['field2', 'value2']]); + assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); + + const parser2 = new BasicCommandParser; + HSETEX.parseCommand(parser2, 'key', {}, [['field1', 'value1'], ['field2', 'value2'], ['field3', 'value3']]); + assert.deepEqual(parser2.redisArgs, ['HSETEX', 'key', 'FIELDS', '3', 'field1', 'value1', 'field2', 'value2', 'field3', 'value3']); + }); + + it('hSetEx parseCommand map', () => { + const parser1 = new BasicCommandParser; + HSETEX.parseCommand(parser1, 'key', {}, new Map([['field1', 'value1'], ['field2', 'value2']])); + assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); + }); + + it('hSetEx parseCommand obj', () => { + const parser1 = new BasicCommandParser; + HSETEX.parseCommand(parser1, 'key', {}, {field1: "value1", field2: "value2"}); + assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); + }); + + it('hSetEx parseCommand options FNX KEEPTTL', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', {mode: 'FNX', expiration: 'KEEPTTL'}, 'field', 'value'); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FNX', 'KEEPTTL', 'FIELDS', '1', 'field', 'value']); + }); + + it('hSetEx parseCommand options FXX EX 500', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', {mode: 'FXX', expiration: {type: 'EX', value: 500}}, 'field', 'value'); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FXX', 'EX', '500', 'FIELDS', '1', 'field', 'value']); + }); +}); + + +// TODO: enable when new test container is released +describe.skip('HSETEX call', () => { + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hSetEx empty single field', async client => { + assert.deepEqual( + await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FNX"}, 'filed1', 'value1'), + 1 + ); + + assert.deepEqual( + await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FXX"}, ['filed1', 'value1', 'field2', 'value2']), + 0 + ); + + assert.deepEqual( + await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FNX"}, ['filed1', 'value1', 'field2', 'value2']), + 1 + ); + + assert.deepEqual( + await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FNX"}, 'filed2', 'value2'), + 1 + ); + + assert.deepEqual( + await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FXX"}, ['filed1', 'value1', 'field2', 'value2']), + 1 + ); + }, GLOBAL.SERVERS.OPEN); +}); \ No newline at end of file diff --git a/packages/client/lib/commands/HSETEX.ts b/packages/client/lib/commands/HSETEX.ts new file mode 100644 index 00000000000..6ace1e98f4c --- /dev/null +++ b/packages/client/lib/commands/HSETEX.ts @@ -0,0 +1,124 @@ +import { BasicCommandParser, CommandParser } from '../client/parser'; +import { Command, NumberReply, RedisArgument } from '../RESP/types'; + +export interface HSetExOptions { + expiration?: { + type: 'EX' | 'PX' | 'EXAT' | 'PXAT'; + value: number; + } | { + type: 'KEEPTTL'; + } | 'KEEPTTL'; + mode?: 'FNX' | 'FXX' + } + +export type HashTypes = RedisArgument | number; + +type HSETEXObject = Record; + +type HSETEXMap = Map; + +type HSETEXTuples = Array<[HashTypes, HashTypes]> | Array; + +type SingleFieldArguments = [field: HashTypes, value: HashTypes]; + +type MultipleFieldsArguments = [value: HSETEXObject | HSETEXMap | HSETEXTuples]; + +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + options?: HSetExOptions, + ...[value, fieldValue]: SingleFieldArguments | MultipleFieldsArguments + ) { + parser.push('HSETEX'); + parser.pushKey(key); + + if (options?.mode) { + parser.push(options.mode) + } + if (options?.expiration) { + if (typeof options.expiration === 'string') { + parser.push(options.expiration); + } else if (options.expiration.type === 'KEEPTTL') { + parser.push('KEEPTTL'); + } else { + parser.push( + options.expiration.type, + options.expiration.value.toString() + ); + } + } + + parser.push('FIELDS') + if (typeof value === 'string' || typeof value === 'number' || value instanceof Buffer) { + parser.push( + '1', + convertValue(value), + convertValue(fieldValue!) + ); + } else if (typeof value === 'undefined' || typeof value === null) { + throw Error('passed undefined or null object') + } else if (value instanceof Map) { + pushMap(parser, value); + } else if (Array.isArray(value)) { + pushTuples(parser, value); + } else { + pushObject(parser, value); + } + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; + + +function pushMap(parser: CommandParser, map: HSETEXMap): void { + parser.push(map.size.toString()) + for (const [key, value] of map.entries()) { + parser.push( + convertValue(key), + convertValue(value) + ); + } +} + +function pushTuples(parser: CommandParser, tuples: HSETEXTuples): void { + const tmpParser = new BasicCommandParser + _pushTuples(tmpParser, tuples) + + if (tmpParser.redisArgs.length%2 != 0) { + throw Error('invalid number of arguments, expected key value ....[key value] pairs, got key without value') + } + + parser.push((tmpParser.redisArgs.length/2).toString()) + tmpParser.redisArgs.forEach(arg => { + parser.push(arg) + }); +} + +function _pushTuples(parser: CommandParser, tuples: HSETEXTuples): void { + for (const tuple of tuples) { + if (Array.isArray(tuple)) { + _pushTuples(parser, tuple); + continue; + } + parser.push(convertValue(tuple)); + } +} + +function pushObject(parser: CommandParser, object: HSETEXObject): void { + const len = Object.keys(object).length + if (len == 0) { + throw Error('object without keys') + } + + parser.push(len.toString()) + for (const key of Object.keys(object)) { + parser.push( + convertValue(key), + convertValue(object[key]) + ); + } +} + +function convertValue(value: HashTypes): RedisArgument { + return typeof value === 'number' ? value.toString() : value; +} \ No newline at end of file diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index fc139a948e0..91eab7107a1 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -269,7 +269,7 @@ export function pushVariadicArgument( return args; } -export function parseOptionalVariadicArgument( +export function parseOptionalVariadicArgument( parser: CommandParser, name: RedisArgument, value?: RedisVariadicArgument diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 024ee2191b8..5cd81331a4e 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -138,6 +138,8 @@ import HEXPIREAT from './HEXPIREAT'; import HEXPIRETIME from './HEXPIRETIME'; import HGET from './HGET'; import HGETALL from './HGETALL'; +import HGETDEL from './HGETDEL'; +import HGETEX from './HGETEX'; import HINCRBY from './HINCRBY'; import HINCRBYFLOAT from './HINCRBYFLOAT'; import HKEYS from './HKEYS'; @@ -154,6 +156,7 @@ import HRANDFIELD from './HRANDFIELD'; import HSCAN from './HSCAN'; import HSCAN_NOVALUES from './HSCAN_NOVALUES'; import HSET from './HSET'; +import HSETEX from './HSETEX'; import HSETNX from './HSETNX'; import HSTRLEN from './HSTRLEN'; import HTTL from './HTTL'; @@ -621,6 +624,10 @@ export default { hGet: HGET, HGETALL, hGetAll: HGETALL, + HGETDEL, + hGetDel: HGETDEL, + HGETEX, + hGetEx: HGETEX, HINCRBY, hIncrBy: HINCRBY, HINCRBYFLOAT, @@ -653,6 +660,8 @@ export default { hScanNoValues: HSCAN_NOVALUES, HSET, hSet: HSET, + HSETEX, + hSetEx: HSETEX, HSETNX, hSetNX: HSETNX, HSTRLEN, diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index f7862a9d685..93b50a43935 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -13,6 +13,8 @@ const utils = TestUtils.createFromConfig({ export default utils; +export const sleep = (delay) => new Promise((resolve: any) => setTimeout((resolve), delay)) + const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? ['--enable-debug-command', 'yes'] : []; From cd060597de99bcb07356a715836012c41d195187 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Wed, 12 Mar 2025 11:03:34 +0200 Subject: [PATCH 2/6] [CAE-686] Improve HSETEX return type --- packages/client/lib/commands/HSETEX.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/lib/commands/HSETEX.ts b/packages/client/lib/commands/HSETEX.ts index 6ace1e98f4c..3ca99ec4a84 100644 --- a/packages/client/lib/commands/HSETEX.ts +++ b/packages/client/lib/commands/HSETEX.ts @@ -66,7 +66,7 @@ export default { pushObject(parser, value); } }, - transformReply: undefined as unknown as () => NumberReply + transformReply: undefined as unknown as () => NumberReply<0 | 1> } as const satisfies Command; From 38da7a4b9d389c974b80174fc98ddba4f01f0bca Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Wed, 12 Mar 2025 11:06:10 +0200 Subject: [PATCH 3/6] [CAE-686] Minor pushTuples change, renamed HSETEX test --- packages/client/lib/commands/HSETEX.spec.ts | 2 +- packages/client/lib/commands/HSETEX.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/client/lib/commands/HSETEX.spec.ts b/packages/client/lib/commands/HSETEX.spec.ts index 4dc80129c13..d7af3560ad5 100644 --- a/packages/client/lib/commands/HSETEX.spec.ts +++ b/packages/client/lib/commands/HSETEX.spec.ts @@ -70,7 +70,7 @@ describe('HSETEX parseCommand', () => { // TODO: enable when new test container is released describe.skip('HSETEX call', () => { - testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hSetEx empty single field', async client => { + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hSetEx calls', async client => { assert.deepEqual( await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FNX"}, 'filed1', 'value1'), 1 diff --git a/packages/client/lib/commands/HSETEX.ts b/packages/client/lib/commands/HSETEX.ts index 3ca99ec4a84..faa111e0efb 100644 --- a/packages/client/lib/commands/HSETEX.ts +++ b/packages/client/lib/commands/HSETEX.ts @@ -89,9 +89,7 @@ function pushTuples(parser: CommandParser, tuples: HSETEXTuples): void { } parser.push((tmpParser.redisArgs.length/2).toString()) - tmpParser.redisArgs.forEach(arg => { - parser.push(arg) - }); + parser.push(...tmpParser.redisArgs) } function _pushTuples(parser: CommandParser, tuples: HSETEXTuples): void { From b77db59170adcc769a4a1ff3c660d27e4e283470 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Wed, 12 Mar 2025 13:52:12 +0200 Subject: [PATCH 4/6] [CAE-686] Changed hsetex function signature for better consistency with other commands --- packages/client/lib/commands/HSETEX.spec.ts | 34 ++++++++++----------- packages/client/lib/commands/HSETEX.ts | 30 ++++++------------ 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/packages/client/lib/commands/HSETEX.spec.ts b/packages/client/lib/commands/HSETEX.spec.ts index d7af3560ad5..fc486296607 100644 --- a/packages/client/lib/commands/HSETEX.spec.ts +++ b/packages/client/lib/commands/HSETEX.spec.ts @@ -6,63 +6,63 @@ import HSETEX from './HSETEX'; describe('HSETEX parseCommand', () => { it('hSetEx parseCommand base', () => { const parser = new BasicCommandParser; - HSETEX.parseCommand(parser, 'key', {}, 'field', 'value'); + HSETEX.parseCommand(parser, 'key', ['field', 'value']); assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '1', 'field', 'value']); }); it('hSetEx parseCommand base empty obj', () => { const parser = new BasicCommandParser; - assert.throws(() => {HSETEX.parseCommand(parser, 'key', {}, {})}); + assert.throws(() => {HSETEX.parseCommand(parser, 'key', {})}); }); it('hSetEx parseCommand base one key obj', () => { const parser = new BasicCommandParser; - HSETEX.parseCommand(parser, 'key', {}, {'k': 'v'}); + HSETEX.parseCommand(parser, 'key', {'k': 'v'}); assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '1', 'k', 'v']); }); it('hSetEx parseCommand array', () => { const parser = new BasicCommandParser; - HSETEX.parseCommand(parser, 'key', {}, ['field1', 'value1', 'field2', 'value2']); + HSETEX.parseCommand(parser, 'key', ['field1', 'value1', 'field2', 'value2']); assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); }); - it('hSetEx parseCommand array invalid args, thows error', () => { + it('hSetEx parseCommand array invalid args, throws an error', () => { const parser = new BasicCommandParser; - assert.throws(() => {HSETEX.parseCommand(parser, 'key', {}, ['field1', 'value1', 'field2'])}); + assert.throws(() => {HSETEX.parseCommand(parser, 'key', ['field1', 'value1', 'field2'])}); }); it('hSetEx parseCommand array in array', () => { const parser1 = new BasicCommandParser; - HSETEX.parseCommand(parser1, 'key', {}, [['field1', 'value1'], ['field2', 'value2']]); + HSETEX.parseCommand(parser1, 'key', [['field1', 'value1'], ['field2', 'value2']]); assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); const parser2 = new BasicCommandParser; - HSETEX.parseCommand(parser2, 'key', {}, [['field1', 'value1'], ['field2', 'value2'], ['field3', 'value3']]); + HSETEX.parseCommand(parser2, 'key', [['field1', 'value1'], ['field2', 'value2'], ['field3', 'value3']]); assert.deepEqual(parser2.redisArgs, ['HSETEX', 'key', 'FIELDS', '3', 'field1', 'value1', 'field2', 'value2', 'field3', 'value3']); }); it('hSetEx parseCommand map', () => { const parser1 = new BasicCommandParser; - HSETEX.parseCommand(parser1, 'key', {}, new Map([['field1', 'value1'], ['field2', 'value2']])); + HSETEX.parseCommand(parser1, 'key', new Map([['field1', 'value1'], ['field2', 'value2']])); assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); }); it('hSetEx parseCommand obj', () => { const parser1 = new BasicCommandParser; - HSETEX.parseCommand(parser1, 'key', {}, {field1: "value1", field2: "value2"}); + HSETEX.parseCommand(parser1, 'key', {field1: "value1", field2: "value2"}); assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); }); it('hSetEx parseCommand options FNX KEEPTTL', () => { const parser = new BasicCommandParser; - HSETEX.parseCommand(parser, 'key', {mode: 'FNX', expiration: 'KEEPTTL'}, 'field', 'value'); + HSETEX.parseCommand(parser, 'key', ['field', 'value'], {mode: 'FNX', expiration: 'KEEPTTL'}); assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FNX', 'KEEPTTL', 'FIELDS', '1', 'field', 'value']); }); it('hSetEx parseCommand options FXX EX 500', () => { const parser = new BasicCommandParser; - HSETEX.parseCommand(parser, 'key', {mode: 'FXX', expiration: {type: 'EX', value: 500}}, 'field', 'value'); + HSETEX.parseCommand(parser, 'key', ['field', 'value'], {mode: 'FXX', expiration: {type: 'EX', value: 500}}); assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FXX', 'EX', '500', 'FIELDS', '1', 'field', 'value']); }); }); @@ -72,27 +72,27 @@ describe('HSETEX parseCommand', () => { describe.skip('HSETEX call', () => { testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hSetEx calls', async client => { assert.deepEqual( - await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FNX"}, 'filed1', 'value1'), + await client.hSetEx('key', ['f1', 'f2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), 1 ); assert.deepEqual( - await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FXX"}, ['filed1', 'value1', 'field2', 'value2']), + await client.hSetEx('key', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}), 0 ); assert.deepEqual( - await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FNX"}, ['filed1', 'value1', 'field2', 'value2']), + await client.hSetEx('key', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), 1 ); assert.deepEqual( - await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FNX"}, 'filed2', 'value2'), + await client.hSetEx('key', ['field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), 1 ); assert.deepEqual( - await client.hSetEx('key', {expiration: {type: "EX", value: 500}, mode: "FXX"}, ['filed1', 'value1', 'field2', 'value2']), + await client.hSetEx('key', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}), 1 ); }, GLOBAL.SERVERS.OPEN); diff --git a/packages/client/lib/commands/HSETEX.ts b/packages/client/lib/commands/HSETEX.ts index faa111e0efb..3827538934c 100644 --- a/packages/client/lib/commands/HSETEX.ts +++ b/packages/client/lib/commands/HSETEX.ts @@ -19,16 +19,12 @@ type HSETEXMap = Map; type HSETEXTuples = Array<[HashTypes, HashTypes]> | Array; -type SingleFieldArguments = [field: HashTypes, value: HashTypes]; - -type MultipleFieldsArguments = [value: HSETEXObject | HSETEXMap | HSETEXTuples]; - export default { parseCommand( parser: CommandParser, key: RedisArgument, - options?: HSetExOptions, - ...[value, fieldValue]: SingleFieldArguments | MultipleFieldsArguments + fields: HSETEXObject | HSETEXMap | HSETEXTuples, + options?: HSetExOptions ) { parser.push('HSETEX'); parser.pushKey(key); @@ -50,21 +46,13 @@ export default { } parser.push('FIELDS') - if (typeof value === 'string' || typeof value === 'number' || value instanceof Buffer) { - parser.push( - '1', - convertValue(value), - convertValue(fieldValue!) - ); - } else if (typeof value === 'undefined' || typeof value === null) { - throw Error('passed undefined or null object') - } else if (value instanceof Map) { - pushMap(parser, value); - } else if (Array.isArray(value)) { - pushTuples(parser, value); - } else { - pushObject(parser, value); - } + if (fields instanceof Map) { + pushMap(parser, fields); + } else if (Array.isArray(fields)) { + pushTuples(parser, fields); + } else { + pushObject(parser, fields); + } }, transformReply: undefined as unknown as () => NumberReply<0 | 1> } as const satisfies Command; From f5fbb9217ffaca4f82ab1a9ee5ad3fe601b51e00 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Wed, 12 Mar 2025 17:39:26 +0200 Subject: [PATCH 5/6] [CAE-686] Fixed hsetex test --- packages/client/lib/commands/HSETEX.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/client/lib/commands/HSETEX.spec.ts b/packages/client/lib/commands/HSETEX.spec.ts index fc486296607..2252e8b0c09 100644 --- a/packages/client/lib/commands/HSETEX.spec.ts +++ b/packages/client/lib/commands/HSETEX.spec.ts @@ -72,27 +72,27 @@ describe('HSETEX parseCommand', () => { describe.skip('HSETEX call', () => { testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hSetEx calls', async client => { assert.deepEqual( - await client.hSetEx('key', ['f1', 'f2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), + await client.hSetEx('key_hsetex_call', ['field1', 'value1'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), 1 ); assert.deepEqual( - await client.hSetEx('key', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}), + await client.hSetEx('key_hsetex_call', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}), 0 ); assert.deepEqual( - await client.hSetEx('key', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), - 1 + await client.hSetEx('key_hsetex_call', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), + 0 ); assert.deepEqual( - await client.hSetEx('key', ['field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), + await client.hSetEx('key_hsetex_call', ['field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), 1 ); assert.deepEqual( - await client.hSetEx('key', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}), + await client.hSetEx('key_hsetex_call', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}), 1 ); }, GLOBAL.SERVERS.OPEN); From f03c758dfcb4e4d3e59aca9053b37aac9ebd2661 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Mon, 17 Mar 2025 16:41:52 +0200 Subject: [PATCH 6/6] [CAE-686] Bumped docker version to 8.0-M05-pre, enabled and fixed tests --- packages/client/lib/commands/HGETDEL.spec.ts | 5 ++--- packages/client/lib/commands/HGETEX.spec.ts | 19 +++++++++---------- packages/client/lib/commands/HSETEX.spec.ts | 5 ++--- packages/client/lib/test-utils.ts | 2 -- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/client/lib/commands/HGETDEL.spec.ts b/packages/client/lib/commands/HGETDEL.spec.ts index ab446c9599b..b2e19967f1d 100644 --- a/packages/client/lib/commands/HGETDEL.spec.ts +++ b/packages/client/lib/commands/HGETDEL.spec.ts @@ -18,8 +18,7 @@ describe('HGETDEL parseCommand', () => { }); -// TODO: enable when new test container is released -describe.skip('HGETDEL call', () => { +describe('HGETDEL call', () => { testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel empty single field', async client => { assert.deepEqual( await client.hGetDel('key', 'filed1'), @@ -37,7 +36,7 @@ describe.skip('HGETDEL call', () => { testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel partially populated multiple fields', async client => { await client.hSet('key', 'field1', 'value1') assert.deepEqual( - await client.hGetDel('key', ['filed1', 'field2']), + await client.hGetDel('key', ['field1', 'field2']), ['value1', null] ); diff --git a/packages/client/lib/commands/HGETEX.spec.ts b/packages/client/lib/commands/HGETEX.spec.ts index 03beb3e7f85..2625a0ac023 100644 --- a/packages/client/lib/commands/HGETEX.spec.ts +++ b/packages/client/lib/commands/HGETEX.spec.ts @@ -1,7 +1,8 @@ import { strict as assert } from 'node:assert'; -import testUtils,{ GLOBAL, sleep } from '../test-utils'; +import testUtils,{ GLOBAL } from '../test-utils'; import { BasicCommandParser } from '../client/parser'; import HGETEX from './HGETEX'; +import { setTimeout } from 'timers/promises'; describe('HGETEX parseCommand', () => { it('hGetEx parseCommand base', () => { @@ -36,18 +37,17 @@ describe('HGETEX parseCommand', () => { }); -// TODO: enable when new test container is released -describe.skip('HGETEX call', () => { +describe('HGETEX call', () => { testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx empty single field', async client => { assert.deepEqual( - await client.hGetEx('key', 'filed1', {expiration: 'PERSIST'}), + await client.hGetEx('key', 'field1', {expiration: 'PERSIST'}), [null] ); }, GLOBAL.SERVERS.OPEN); testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx empty multiple fields', async client => { assert.deepEqual( - await client.hGetEx('key', ['filed1', 'field2'], {expiration: 'PERSIST'}), + await client.hGetEx('key', ['field1', 'field2'], {expiration: 'PERSIST'}), [null, null] ); }, GLOBAL.SERVERS.OPEN); @@ -55,11 +55,10 @@ describe.skip('HGETEX call', () => { testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx set expiry', async client => { await client.hSet('key', 'field', 'value') assert.deepEqual( - await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 500}}), + await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 50}}), ['value'] ); - - await sleep(600) + await setTimeout(100) assert.deepEqual( await client.hGet('key', 'field'), null @@ -68,9 +67,9 @@ describe.skip('HGETEX call', () => { testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'gGetEx set expiry PERSIST', async client => { await client.hSet('key', 'field', 'value') - await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 500}}) + await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 50}}) await client.hGetEx('key', 'field', {expiration: 'PERSIST'}) - await sleep(600) + await setTimeout(100) assert.deepEqual( await client.hGet('key', 'field'), 'value' diff --git a/packages/client/lib/commands/HSETEX.spec.ts b/packages/client/lib/commands/HSETEX.spec.ts index 2252e8b0c09..fc38e0f0f45 100644 --- a/packages/client/lib/commands/HSETEX.spec.ts +++ b/packages/client/lib/commands/HSETEX.spec.ts @@ -1,5 +1,5 @@ import { strict as assert } from 'node:assert'; -import testUtils,{ GLOBAL, sleep } from '../test-utils'; +import testUtils,{ GLOBAL } from '../test-utils'; import { BasicCommandParser } from '../client/parser'; import HSETEX from './HSETEX'; @@ -68,8 +68,7 @@ describe('HSETEX parseCommand', () => { }); -// TODO: enable when new test container is released -describe.skip('HSETEX call', () => { +describe('HSETEX call', () => { testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hSetEx calls', async client => { assert.deepEqual( await client.hSetEx('key_hsetex_call', ['field1', 'value1'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 93b50a43935..f7862a9d685 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -13,8 +13,6 @@ const utils = TestUtils.createFromConfig({ export default utils; -export const sleep = (delay) => new Promise((resolve: any) => setTimeout((resolve), delay)) - const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? ['--enable-debug-command', 'yes'] : [];