Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c6249c7
feat(json): add reviver support to GET, MGET, and ARRPOP
JeanSamGirard Mar 25, 2026
5e1d3d7
fix(json): ARRPOP path should be optional even if reviver is set
JeanSamGirard Mar 26, 2026
a1f862b
tests(json): add reviver tests
JeanSamGirard Mar 26, 2026
9254d8a
tests(json): typo in tests
JeanSamGirard Mar 26, 2026
ed7aa40
tests(json): fix MGET test with reviver using $ but not expecting an …
JeanSamGirard Mar 26, 2026
d26496f
fix(json): ARRPOP should ignore index if path is not defined
JeanSamGirard Mar 26, 2026
7d09619
fix(json): ARRPOP path option is optional
JeanSamGirard Mar 26, 2026
ebffff0
tests(json): added test with multi() to json.GET
JeanSamGirard Mar 31, 2026
9cc42f8
tests(json): added test with multi() to json.GET
JeanSamGirard Mar 31, 2026
5b39014
Merge branch 'feat/json-reviver-support' of https://github.com/JeanSa…
JeanSamGirard Mar 31, 2026
d8f9d88
Merge branch 'master' of https://github.com/JeanSamGirard/node-redis …
JeanSamGirard May 25, 2026
6885b6c
fix(json): revert unintended change due to merging from older source
JeanSamGirard May 25, 2026
cf2c568
docs(json): js-doc for reviver option
JeanSamGirard May 25, 2026
9b35e7f
fix(json): ARRPOP Optional path option
JeanSamGirard May 25, 2026
818a91b
Merge branch 'redis:master' into feat/json-reviver-support
JeanSamGirard Jul 7, 2026
4012f07
test(json): fix ARRPOP reviver test accessing the array directly inst…
JeanSamGirard Jul 7, 2026
af50332
fix(json): type ARRPOP reviver test reply instead of any
nkaradzhov Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/client/lib/commands/generic-transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,11 +753,13 @@ export function transformRedisJsonArgument(json: RedisJSON): string {
return JSON.stringify(json);
}

export function transformRedisJsonReply(json: BlobStringReply): RedisJSON {
const res = JSON.parse((json as unknown as UnwrapReply<typeof json>).toString());
export type JsonReviver = Parameters<typeof JSON.parse>[1];

export function transformRedisJsonReply(json: BlobStringReply, reviver?: JsonReviver): RedisJSON {
const res = JSON.parse((json as unknown as UnwrapReply<typeof json>).toString(), reviver);
return res;
}

export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON {
return isNullReply(json) ? json : transformRedisJsonReply(json);
export function transformRedisJsonNullReply(json: NullReply | BlobStringReply, reviver?: JsonReviver ): NullReply | RedisJSON {
return isNullReply(json) ? json : transformRedisJsonReply(json, reviver);
}
15 changes: 14 additions & 1 deletion packages/json/lib/commands/ARRPOP.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import ARRPOP from './ARRPOP';
import { parseArgs } from '@redis/client/lib/commands/generic-transformers';
import { parseArgs, RedisJSON } from '@redis/client/lib/commands/generic-transformers';

describe('JSON.ARRPOP', () => {
describe('transformArguments', () => {
Expand Down Expand Up @@ -63,5 +63,18 @@ describe('JSON.ARRPOP', () => {

assert.deepEqual(reply, ['value']);
}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClient('$ path with reviver', async client => {
const [, res] = await Promise.all([
client.json.set('key', '$', [{ name: 'Alice', birthday: new Date('1998-02-12') }]),
client.json.arrPop('key', {
path: '$',
reviver: (key, value) => { if (key === 'birthday') return new Date(value); else return value; }
})
]);

const [item] = (res as Array<RedisJSON>);
assert(typeof item === 'object' && item !== null && 'birthday' in item && item.birthday instanceof Date && item.birthday.getTime() === new Date('1998-02-12').getTime());
}, GLOBAL.SERVERS.OPEN);
});
});
28 changes: 17 additions & 11 deletions packages/json/lib/commands/ARRPOP.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { CommandParser } from '@redis/client/dist/lib/client/parser';
import { RedisArgument, ArrayReply, NullReply, BlobStringReply, Command, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
import { isArrayReply, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
import { isArrayReply, transformRedisJsonNullReply, JsonReviver } from '@redis/client/dist/lib/commands/generic-transformers';

export interface RedisArrPopOptions {
path: RedisArgument;
index?: number;
}
export type RedisArrPopOptions = {
reviver?: JsonReviver;
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
} & (
| { path?: never; index?: never }
| { path: RedisArgument; index?: number }
);

export default {
IS_READ_ONLY: false,
Expand All @@ -14,17 +16,21 @@ export default {
parser.pushKey(key);

if (options) {
parser.push(options.path);
if (options.path !== undefined) {
parser.push(options.path);

if (options.index !== undefined) {
parser.push(options.index.toString());
if (options.index !== undefined) {
parser.push(options.index.toString());
}
}

parser.preserve = options.reviver;
}
},
transformReply(reply: NullReply | BlobStringReply | ArrayReply<NullReply | BlobStringReply>) {
Comment thread
cursor[bot] marked this conversation as resolved.
transformReply(reply: NullReply | BlobStringReply | ArrayReply<NullReply | BlobStringReply>, reviver?: JsonReviver) {
return isArrayReply(reply) ?
(reply as unknown as UnwrapReply<typeof reply>).map(item => transformRedisJsonNullReply(item)) :
transformRedisJsonNullReply(reply);
(reply as unknown as UnwrapReply<typeof reply>).map(item => transformRedisJsonNullReply(item, reviver)) :
transformRedisJsonNullReply(reply, reviver);
}
} as const satisfies Command;

24 changes: 24 additions & 0 deletions packages/json/lib/commands/GET.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,30 @@ describe('JSON.GET', () => {

}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClient('client.json.get with reviver', async client => {
assert.equal(
await client.json.get('key',{ reviver: ()=>{ assert.fail() } }),
null
);

await client.json.set('noderedis:users:1', '$', { name: 'Alice', birthday: new Date('1998-02-12') });
const res = await client.json.get('noderedis:users:1', { reviver: (key, value) => { if (key === 'birthday') return new Date(value); else return value; } });
assert(typeof res === 'object' && res !== null && 'birthday' in res && res.birthday instanceof Date && res.birthday.getTime() === new Date('1998-02-12').getTime());

}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClient('client.multi().json.get with reviver', async client => {
assert.equal(
(await client.multi().json.get('key',{ reviver: ()=>{ assert.fail() } }).exec())[0],
null
);
Comment thread
cursor[bot] marked this conversation as resolved.

await client.json.set('noderedis:users:1', '$', { name: 'Alice', birthday: new Date('1998-02-12') });
const res = (await client.multi().json.get('noderedis:users:1', { reviver: (key, value) => { if (key === 'birthday') return new Date(value); else return value; } }).exec())[0];
assert(typeof res === 'object' && res !== null && 'birthday' in res && res.birthday instanceof Date && res.birthday.getTime() === new Date('1998-02-12').getTime());

}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClient('client.json.get with path', async client => {
await client.json.set('json:path:test', '$', {
user: { name: 'Bob', age: 25 },
Expand Down
4 changes: 3 additions & 1 deletion packages/json/lib/commands/GET.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { CommandParser } from '@redis/client/dist/lib/client/parser';
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
import { RedisVariadicArgument, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
import { RedisVariadicArgument, transformRedisJsonNullReply, JsonReviver } from '@redis/client/dist/lib/commands/generic-transformers';

export interface JsonGetOptions {
path?: RedisVariadicArgument;
reviver?: JsonReviver
}

export default {
Expand All @@ -18,6 +19,7 @@ export default {
if (options?.path !== undefined) {
parser.pushVariadic(options.path);
}
parser.preserve = options?.reviver;
},
transformReply: transformRedisJsonNullReply
} as const satisfies Command;
9 changes: 9 additions & 0 deletions packages/json/lib/commands/MGET.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,13 @@ describe('JSON.MGET', () => {
[null, null]
);
}, GLOBAL.SERVERS.OPEN);

testUtils.testWithClient('client.json.mGet with reviver', async client => {
await client.json.set('noderedis:users:1', '$', { name: 'Alice', birthday: new Date('1998-02-12') });
await client.json.set('noderedis:users:2', '$', { name: 'Bob', birthday: new Date('1996-07-23') });
const res = await client.json.mGet(['noderedis:users:1', 'noderedis:users:2'], '.', (key, value) => { if (key === 'birthday') return new Date(value); else return value; });
assert(typeof res[0] === 'object' && res[0] !== null && 'birthday' in res[0] && (res[0].birthday instanceof Date) && res[0].birthday.getTime() === new Date('1998-02-12').getTime());
assert(typeof res[1] === 'object' && res[1] !== null && 'birthday' in res[1] && res[1].birthday instanceof Date && res[1].birthday.getTime() === new Date('1996-07-23').getTime());

}, GLOBAL.SERVERS.OPEN);
});
38 changes: 27 additions & 11 deletions packages/json/lib/commands/MGET.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
import { CommandParser } from '@redis/client/dist/lib/client/parser';
import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
import { transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
import {
RedisArgument,
UnwrapReply,
ArrayReply,
NullReply,
BlobStringReply,
Command,
} from '@redis/client/dist/lib/RESP/types';
import {
transformRedisJsonNullReply,
JsonReviver,
} from '@redis/client/dist/lib/commands/generic-transformers';

export default {
IS_READ_ONLY: true,
parseCommand(parser: CommandParser, keys: Array<RedisArgument>, path: RedisArgument) {
parser.push('JSON.MGET');
parser.pushKeys(keys);
parser.push(path);
},
transformReply(reply: UnwrapReply<ArrayReply<NullReply | BlobStringReply>>) {
return reply.map(json => transformRedisJsonNullReply(json))
}
IS_READ_ONLY: true,
parseCommand(
parser: CommandParser,
keys: Array<RedisArgument>,
path: RedisArgument,
reviver?: JsonReviver,
) {
parser.push('JSON.MGET');
parser.pushKeys(keys);
parser.push(path);
parser.preserve = reviver;
},
transformReply(reply: UnwrapReply<ArrayReply<NullReply | BlobStringReply>>, reviver?: JsonReviver) {
return reply.map((json) => transformRedisJsonNullReply(json, reviver));
},
} as const satisfies Command;
6 changes: 6 additions & 0 deletions packages/json/lib/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export default {
* @param options - Optional parameters
* @param options.path - Path to the array in the JSON document
* @param options.index - Optional index to pop from. Default is -1 (last element)
* @param options.reviver - An optional reviver function to call when parsing the reply from Redis
*/
ARRPOP,
/**
Expand All @@ -130,6 +131,7 @@ export default {
* @param options - Optional parameters
* @param options.path - Path to the array in the JSON document
* @param options.index - Optional index to pop from. Default is -1 (last element)
* @param options.reviver - An optional reviver function to call when parsing the reply from Redis
*/
arrPop: ARRPOP,
/**
Expand Down Expand Up @@ -231,6 +233,7 @@ export default {
* @param key - The key containing the JSON document
* @param options - Optional parameters
* @param options.path - Path(s) to the value(s) to retrieve
* @param options.reviver - An optional reviver function to call when parsing the reply from Redis
*/
GET,
/**
Expand All @@ -240,6 +243,7 @@ export default {
* @param key - The key containing the JSON document
* @param options - Optional parameters
* @param options.path - Path(s) to the value(s) to retrieve
* @param options.reviver - An optional reviver function to call when parsing the reply from Redis
*/
get: GET,
/**
Expand All @@ -266,6 +270,7 @@ export default {
*
* @param keys - Array of keys containing JSON documents
* @param path - Path to retrieve from each document
* @param reviver - An optional reviver function to call when parsing the reply from Redis
*/
MGET,
/**
Expand All @@ -274,6 +279,7 @@ export default {
*
* @param keys - Array of keys containing JSON documents
* @param path - Path to retrieve from each document
* @param reviver - An optional reviver function to call when parsing the reply from Redis
*/
mGet: MGET,
/**
Expand Down