Skip to content

Commit

Permalink
Node: Add command JSON.TOGGLE (#2491)
Browse files Browse the repository at this point in the history
* Node: Add command JSON.TOGGLE

Signed-off-by: TJ Zhang <tj.zhang@improving.com>
Co-authored-by: TJ Zhang <tj.zhang@improving.com>
  • Loading branch information
tjzhang-BQ and TJ Zhang authored Oct 22, 2024
1 parent f977b91 commit f65b0fe
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
70 changes: 65 additions & 5 deletions node/src/server-modules/GlideJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 | (T | null)[];

/**
* Represents options for formatting JSON data, to be used in the [JSON.GET](https://valkey.io/commands/json.get/) command.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 `$`):
Expand Down Expand Up @@ -164,17 +168,73 @@ export class GlideJson {
* ```
*/
static async get(
client: GlideClient | GlideClusterClient,
client: BaseClient,
key: GlideString,
options?: JsonGetOptions & DecoderOption,
): Promise<ReturnTypeJson> {
): Promise<ReturnTypeJson<GlideString>> {
const args = ["JSON.GET", key];

if (options) {
const optionArgs = _jsonGetOptionsToArgs(options);
args.push(...optionArgs);
}

return _executeCommand<ReturnTypeJson>(client, args, options);
return _executeCommand<ReturnTypeJson<GlideString>>(
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<ReturnTypeJson<boolean>> {
const args = ["JSON.TOGGLE", key];

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

return _executeCommand<ReturnTypeJson<boolean>>(client, args);
}
}
52 changes: 52 additions & 0 deletions node/tests/ServerModules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
InfoOptions,
JsonGetOptions,
ProtocolVersion,
RequestError,
} from "..";
import { ValkeyCluster } from "../../utils/TestUtils";
import {
Expand Down Expand Up @@ -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);
},
);
});

0 comments on commit f65b0fe

Please sign in to comment.