diff --git a/CHANGELOG.md b/CHANGELOG.md index 9297ff4644..974fcec9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * Core: Update routing for commands from server modules ([#2461](https://github.com/valkey-io/valkey-glide/pull/2461)) * Node: Added `JSON.SET` and `JSON.GET` ([#2427](https://github.com/valkey-io/valkey-glide/pull/2427)) * Java: Added `JSON.ARRAPPEND` ([#2489](https://github.com/valkey-io/valkey-glide/pull/2489)) +* Node: Added `JSON.TOGGLE` ([#2491](https://github.com/valkey-io/valkey-glide/pull/2491)) #### Breaking Changes diff --git a/node/src/server-modules/GlideJson.ts b/node/src/server-modules/GlideJson.ts index 6dd57b16d3..9cab5092e4 100644 --- a/node/src/server-modules/GlideJson.ts +++ b/node/src/server-modules/GlideJson.ts @@ -7,7 +7,7 @@ import { ConditionalChange } from "../Commands"; import { GlideClient } from "../GlideClient"; import { GlideClusterClient, RouteOption } from "../GlideClusterClient"; -export type ReturnTypeJson = GlideString | (GlideString | null)[]; +export type ReturnTypeJson = T | (T | null)[]; /** * Represents options for formatting JSON data, to be used in the [JSON.GET](https://valkey.io/commands/json.get/) command. @@ -80,6 +80,7 @@ export class GlideJson { /** * Sets the JSON value at the specified `path` stored at `key`. * + * @param client The client to execute the command. * @param key - The key of the JSON document. * @param path - Represents the path within the JSON document where the value will be set. * The key will be modified only if `value` is added as the last child in the specified `path`, or if the specified `path` acts as the parent of a new child being added. @@ -123,8 +124,11 @@ export class GlideJson { /** * Retrieves the JSON value at the specified `paths` stored at `key`. * + * @param client The client to execute the command. * @param key - The key of the JSON document. - * @param options - Options for formatting the byte representation of the JSON data. See {@link JsonGetOptions}. + * @param options - (Optional) Additional parameters: + * - (Optional) Options for formatting the byte representation of the JSON data. See {@link JsonGetOptions}. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns ReturnTypeJson: * - If one path is given: * - For JSONPath (path starts with `$`): @@ -164,10 +168,10 @@ export class GlideJson { * ``` */ static async get( - client: GlideClient | GlideClusterClient, + client: BaseClient, key: GlideString, options?: JsonGetOptions & DecoderOption, - ): Promise { + ): Promise> { const args = ["JSON.GET", key]; if (options) { @@ -175,6 +179,62 @@ export class GlideJson { args.push(...optionArgs); } - return _executeCommand(client, args, options); + return _executeCommand>( + client, + args, + options, + ); + } + + /** + * Toggles a Boolean value stored at the specified `path` within the JSON document stored at `key`. + * + * @param client - The client to execute the command. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) The JSONPath to specify. Defaults to the root if not specified. + * @returns - For JSONPath (`path` starts with `$`), returns a list of boolean replies for every possible path, with the toggled boolean value, + * or null for JSON values matching the path that are not boolean. + * - For legacy path (`path` doesn't starts with `$`), returns the value of the toggled boolean in `path`. + * - Note that when sending legacy path syntax, If `path` doesn't exist or the value at `path` isn't a boolean, an error is raised. + * + * @example + * ```typescript + * const value = {bool: true, nested: {bool: false, nested: {bool: 10}}}; + * const jsonStr = JSON.stringify(value); + * const resultSet = await GlideJson.set("doc", "$", jsonStr); + * // Output: 'OK' + * + * const resultToggle = await.GlideJson.toggle(client, "doc", "$.bool") + * // Output: [false, true, null] - Indicates successful toggling of the Boolean values at path '$.bool' in the key stored at `doc`. + * + * const resultToggle = await.GlideJson.toggle(client, "doc", "bool") + * // Output: true - Indicates successful toggling of the Boolean value at path 'bool' in the key stored at `doc`. + * + * const resultToggle = await.GlideJson.toggle(client, "doc", "bool") + * // Output: true - Indicates successful toggling of the Boolean value at path 'bool' in the key stored at `doc`. + * + * const jsonGetStr = await GlideJson.get(client, "doc", "$"); + * console.log(JSON.stringify(jsonGetStr)); + * // Output: [{bool: true, nested: {bool: true, nested: {bool: 10}}}] - The updated JSON value in the key stored at `doc`. + * + * // Without specifying a path, the path defaults to root. + * console.log(await GlideJson.set(client, "doc2", ".", true)); // Output: "OK" + * console.log(await GlideJson.toggle(client,"doc2")); // Output: "false" + * console.log(await GlideJson.toggle(client, "doc2")); // Output: "true" + * ``` + */ + static async toggle( + client: BaseClient, + key: GlideString, + options?: { path: GlideString }, + ): Promise> { + const args = ["JSON.TOGGLE", key]; + + if (options !== undefined) { + args.push(options.path); + } + + return _executeCommand>(client, args); } } diff --git a/node/tests/ServerModules.test.ts b/node/tests/ServerModules.test.ts index 855dfd4305..fad81f2bbd 100644 --- a/node/tests/ServerModules.test.ts +++ b/node/tests/ServerModules.test.ts @@ -17,6 +17,7 @@ import { InfoOptions, JsonGetOptions, ProtocolVersion, + RequestError, } from ".."; import { ValkeyCluster } from "../../utils/TestUtils"; import { @@ -227,4 +228,55 @@ describe("GlideJson", () => { expect(result).toEqual(expectedResult2); }, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "json.toggle tests", + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const key = uuidv4(); + const key2 = uuidv4(); + const jsonValue = { + bool: true, + nested: { bool: false, nested: { bool: 10 } }, + }; + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); + expect( + await GlideJson.toggle(client, key, { path: "$..bool" }), + ).toEqual([false, true, null]); + expect(await GlideJson.toggle(client, key, { path: "bool" })).toBe( + true, + ); + expect( + await GlideJson.toggle(client, key, { path: "$.non_existing" }), + ).toEqual([]); + expect( + await GlideJson.toggle(client, key, { path: "$.nested" }), + ).toEqual([null]); + + // testing behavior with default pathing + expect(await GlideJson.set(client, key2, ".", "true")).toBe("OK"); + expect(await GlideJson.toggle(client, key2)).toBe(false); + expect(await GlideJson.toggle(client, key2)).toBe(true); + + // expect request errors + await expect( + GlideJson.toggle(client, key, { path: "nested" }), + ).rejects.toThrow(RequestError); + await expect( + GlideJson.toggle(client, key, { path: ".non_existing" }), + ).rejects.toThrow(RequestError); + await expect( + GlideJson.toggle(client, "non_existing_key", { path: "$" }), + ).rejects.toThrow(RequestError); + }, + ); });