diff --git a/CHANGELOG.md b/CHANGELOG.md index cb98c5549b..a554b9652b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ * Node: Added `JSON.TYPE` ([#2510](https://github.com/valkey-io/valkey-glide/pull/2510)) * Java: Added `JSON.RESP` ([#2513](https://github.com/valkey-io/valkey-glide/pull/2513)) * Node: Added `FT.DROPINDEX` ([#2516](https://github.com/valkey-io/valkey-glide/pull/2516)) +* Node: Added `JSON.RESP` ([#2517](https://github.com/valkey-io/valkey-glide/pull/2517)) * Python: Add `JSON.STRAPPEND` , `JSON.STRLEN` commands ([#2372](https://github.com/valkey-io/valkey-glide/pull/2372)) * Python: Add `JSON.OBJKEYS` command ([#2395](https://github.com/valkey-io/valkey-glide/pull/2395)) * Python: Add `JSON.ARRINSERT` command ([#2464](https://github.com/valkey-io/valkey-glide/pull/2464)) diff --git a/node/src/server-modules/GlideJson.ts b/node/src/server-modules/GlideJson.ts index db40a31efc..92d3ae35a8 100644 --- a/node/src/server-modules/GlideJson.ts +++ b/node/src/server-modules/GlideJson.ts @@ -352,4 +352,52 @@ export class GlideJson { return _executeCommand>(client, args); } + + /** + * Retrieve the JSON value at the specified `path` within the JSON document stored at `key`. + * The returning result is in the Valkey or Redis OSS Serialization Protocol (RESP). + * JSON null is mapped to the RESP Null Bulk String. + * JSON Booleans are mapped to RESP Simple string. + * JSON integers are mapped to RESP Integers. + * JSON doubles are mapped to RESP Bulk Strings. + * JSON strings are mapped to RESP Bulk Strings. + * JSON arrays are represented as RESP arrays, where the first element is the simple string [, followed by the array's elements. + * JSON objects are represented as RESP object, where the first element is the simple string {, followed by key-value pairs, each of which is a RESP bulk string. + * + * @param client - The client to execute the command. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) path - The path within the JSON document, Defaults to root if not provided. + * @returns ReturnTypeJson: + * - For JSONPath (path starts with `$`): + * - Returns an array of replies for every possible path, indicating the RESP form of the JSON value. + * If `path` doesn't exist, returns an empty array. + * - For legacy path (path doesn't start with `$`): + * - Returns a single reply for the JSON value at the specified `path`, in its RESP form. + * If multiple paths match, the value of the first JSON value match is returned. If `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, `null` is returned. + * + * @example + * ```typescript + * console.log(await GlideJson.set(client, "doc", ".", "{a: [1, 2, 3], b: {a: [1, 2], c: {a: 42}}}")); + * // Output: 'OK' - Indicates successful setting of the value at path '.' in the key stored at `doc`. + * const result = await GlideJson.resp(client, "doc", "$..a"); + * console.log(result); + * // Output: [ ["[", 1L, 2L, 3L], ["[", 1L, 2L], [42L]]; + * console.log(await GlideJson.type(client, "doc", "..a")); // Output: ["[", 1L, 2L, 3L] + * ``` + */ + static async resp( + client: BaseClient, + key: GlideString, + options?: { path: GlideString }, + ): Promise> { + const args = ["JSON.RESP", key]; + + if (options) { + args.push(options.path); + } + + return _executeCommand>(client, args); + } } diff --git a/node/tests/ServerModules.test.ts b/node/tests/ServerModules.test.ts index 899bf88644..4ec5757fd7 100644 --- a/node/tests/ServerModules.test.ts +++ b/node/tests/ServerModules.test.ts @@ -590,6 +590,135 @@ describe("Server Module Tests", () => { ).toBeNull(); }, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "json.resp tests", + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const key = uuidv4(); + const jsonValue = { + obj: { a: 1, b: 2 }, + arr: [1, 2, 3], + str: "foo", + bool: true, + int: 42, + float: 3.14, + nullVal: null, + }; + // setup + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); + expect( + await GlideJson.resp(client, key, { path: "$.*" }), + ).toEqual([ + ["{", ["a", 1], ["b", 2]], + ["[", 1, 2, 3], + "foo", + "true", + 42, + "3.14", + null, + ]); // leading "{" - JSON objects, leading "[" - JSON arrays + + // multiple path match, the first will be returned + expect( + await GlideJson.resp(client, key, { path: "*" }), + ).toEqual(["{", ["a", 1], ["b", 2]]); + + // testing $ path + expect( + await GlideJson.resp(client, key, { path: "$" }), + ).toEqual([ + [ + "{", + ["obj", ["{", ["a", 1], ["b", 2]]], + ["arr", ["[", 1, 2, 3]], + ["str", "foo"], + ["bool", "true"], + ["int", 42], + ["float", "3.14"], + ["nullVal", null], + ], + ]); + + // testing . path + expect( + await GlideJson.resp(client, key, { path: "." }), + ).toEqual([ + "{", + ["obj", ["{", ["a", 1], ["b", 2]]], + ["arr", ["[", 1, 2, 3]], + ["str", "foo"], + ["bool", "true"], + ["int", 42], + ["float", "3.14"], + ["nullVal", null], + ]); + + // $.str and .str + expect( + await GlideJson.resp(client, key, { path: "$.str" }), + ).toEqual(["foo"]); + expect( + await GlideJson.resp(client, key, { path: ".str" }), + ).toEqual("foo"); + + // setup new json value + const jsonValue2 = { + a: [1, 2, 3], + b: { a: [1, 2], c: { a: 42 } }, + }; + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue2), + ), + ).toBe("OK"); + + expect( + await GlideJson.resp(client, key, { path: "..a" }), + ).toEqual(["[", 1, 2, 3]); + + expect( + await GlideJson.resp(client, key, { + path: "$.nonexistent", + }), + ).toEqual([]); + + // error case + await expect( + GlideJson.resp(client, key, { path: "nonexistent" }), + ).rejects.toThrow(RequestError); + + // non-existent key + expect( + await GlideJson.resp(client, "nonexistent_key", { + path: "$", + }), + ).toBeNull(); + expect( + await GlideJson.resp(client, "nonexistent_key", { + path: ".", + }), + ).toBeNull(); + expect( + await GlideJson.resp(client, "nonexistent_key"), + ).toBeNull(); + }, + ); }); describe("GlideFt", () => {