Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node: Add command JSON.GET and JSON.SET #2406

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
164 changes: 164 additions & 0 deletions node/src/server-modules/GlideJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

import { GlideString } from "../BaseClient";
import { ConditionalChange } from "../Commands";
import { GlideClient } from "../GlideClient";
import { GlideClusterClient } from "../GlideClusterClient";

export type ReturnTypeJson = GlideString | GlideString[];
/**
* Represents options for formatting JSON data, to be used in the [JSON.GET](https://valkey.io/commands/json.get/) command.
*/
export interface JsonGetOptions {
/** The path or list of paths within the JSON document. Default is root `$`. */
paths?: GlideString[];
/** Sets an indentation string for nested levels. Defaults to None. */
indent?: GlideString;
/** Sets a string that's printed at the end of each line. Defaults to None. */
newline?: GlideString;
/** Sets a string that's put between a key and a value. Defaults to None. */
space?: GlideString;
}

function jsonGetOptionsToArgs(options: JsonGetOptions): GlideString[] {
const result: GlideString[] = [];

if (options.paths !== undefined) {
result.push(...options.paths);
}

if (options.indent !== undefined) {
result.push("INDENT", options.indent);
}

if (options.newline !== undefined) {
result.push("NEWLINE", options.newline);
}

if (options.space !== undefined) {
result.push("SPACE", options.space);
}

return result;
}

/**
* Sets the JSON value at the specified `path` stored at `key`.

* See https://valkey.io/commands/json.set/ for more details.
*
* @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.
* @param value - The value to set at the specific path, in JSON formatted bytes or str.
* @param conditionalChange - Set the value only if the given condition is met (within the key or path).
* Equivalent to [`XX` | `NX`] in the Redis API. Defaults to null.
*
* @returns If the value is successfully set, returns OK.
* If `value` isn't set because of `conditionalChange`, returns null.
*
* @example
* ```typescript
* const value = {a: 1.0, b:2};
* const jsonStr = JSON.stringify(value);
* const result = await client.jsonSet("doc", "$", jsonStr);
* console.log(result); // 'OK' - Indicates successful setting of the value at path '$' in the key stored at `doc`.
*
* const jsonGetStr = await redisJson.get(client, "doc", "$"); // Returns the value at path '$' in the JSON document stored at `doc` as JSON string.
* console.log(jsonGetStr); // b"[{\"a\":1.0,\"b\":2}]"
* console.log(JSON.stringify(jsonGetStr)); // [{"a": 1.0, "b" :2}] # JSON object retrieved from the key `doc`
* ```
*/
export async function set(
client: GlideClient | GlideClusterClient,
key: GlideString,
path: GlideString,
value: GlideString,
conditionalChange?: ConditionalChange,
): Promise<"OK" | null> {
const args: GlideString[] = ["JSON.SET", key, path, value];

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

if (client instanceof GlideClient) {
return (client as GlideClient).customCommand(args) as Promise<
"OK" | null
>;
} else {
return (client as GlideClusterClient).customCommand(
args,
) as Promise<"OK" | null>;
}
}

/**
* Retrieves the JSON value at the specified `paths` stored at `key`.

* See https://valkey.io/commands/json.get/ for more details.

* @param key - The key of the JSON document.
* @param options - Options for formatting the byte representation of the JSON data. See {@link JsonGetOptions}.
* @returns ReturnTypeJson:
* If one path is given:
* For JSONPath (path starts with `$`):
* Returns a stringified JSON list of bytes replies for every possible path,
* or a byte string representation of an empty array, if path doesn't exists.
* If `key` doesn't exist, returns None.
* For legacy path (path doesn't start with `$`):
* Returns a byte string representation of the value in `path`.
* If `path` doesn't exist, an error is raised.
* If `key` doesn't exist, returns None.
* If multiple paths are given:
* Returns a stringified JSON object in bytes, in which each path is a key, and it's corresponding value, is the value as if the path was executed in the command as a single path.
* In case of multiple paths, and `paths` are a mix of both JSONPath and legacy path, the command behaves as if all are JSONPath paths.
*
* @example
* ```typescript
* const jsonStr = await client.jsonGet('doc', '$');
* console.log(JSON.parse(jsonStr as string));
* // Output: [{"a": 1.0, "b" :2}] - JSON object retrieved from the key `doc`
*
* const jsonData = await client.jsonGet('doc', '$');
* console.log(jsonData);
* // Output: "[{\"a\":1.0,\"b\":2}]" - Returns the value at path '$' in the JSON document stored at `doc`.
*
* const formattedJson = await client.jsonGet('doc', {
* ['$.a', '$.b']
* indent: " ",
* newline: "\n",
* space: " "
* });
* console.log(formattedJson);
* // Output: "{\n \"$.a\": [\n 1.0\n ],\n \"$.b\": [\n 2\n ]\n}" - Returns values at paths '$.a' and '$.b' with custom formatt
*
* const nonExistingPath = await client.jsonGet('doc', '$.non_existing_path');
* console.log(nonExistingPath);
* // Output: "[]" - Empty array since the path does not exist in the JSON document.
* ```
*/
export async function jsonGet(
client: GlideClient | GlideClusterClient,
key: GlideString,
options?: JsonGetOptions,
): Promise<ReturnTypeJson> {
const args = ["JSON.GET", key];

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

if (client instanceof GlideClient) {
return (client as GlideClient).customCommand(
args,
) as Promise<ReturnTypeJson>;
} else {
return (client as GlideClusterClient).customCommand(
args,
) as Promise<ReturnTypeJson>;
}
}
170 changes: 170 additions & 0 deletions node/tests/TestJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/


export function runJsonTests(config: {
init: () => Promise<{ client: Client }>;
close: (testSucceeded: boolean) => void;
timeout?: number;
}) {
const runTest = async (test: (client: Client) => Promise<void>) => {
const { client } = await config.init();
let testSucceeded = false;

try {
await test(client);
testSucceeded = true;
} finally {
config.close(testSucceeded);
}
};

it(
"test1",
async () => {
await runTest((client: Client) => (client));
},
config.timeout,
);
}


// @pytest.mark.asyncio
// class TestJson:
// @pytest.mark.parametrize("cluster_mode", [True, False])
// @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
// async def test_json_module_is_loaded(self, redis_client: TRedisClient):
// res = parse_info_response(await redis_client.info([InfoSection.MODULES]))
// assert "ReJSON" in res["module"]

// @pytest.mark.parametrize("cluster_mode", [True, False])
// @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
// async def test_json_set_get(self, redis_client: TRedisClient):
// key = get_random_string(5)

// json_value = {"a": 1.0, "b": 2}
// assert await json.set(redis_client, key, "$", OuterJson.dumps(json_value)) == OK

// result = await json.get(redis_client, key, ".")
// assert isinstance(result, str)
// assert OuterJson.loads(result) == json_value

// result = await json.get(redis_client, key, ["$.a", "$.b"])
// assert isinstance(result, str)
// assert OuterJson.loads(result) == {"$.a": [1.0], "$.b": [2]}

// assert await json.get(redis_client, "non_existing_key", "$") is None
// assert await json.get(redis_client, key, "$.d") == "[]"

// @pytest.mark.parametrize("cluster_mode", [True, False])
// @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
// async def test_json_set_get_multiple_values(self, redis_client: TRedisClient):
// key = get_random_string(5)

// assert (
// await json.set(
// redis_client,
// key,
// "$",
// OuterJson.dumps({"a": {"c": 1, "d": 4}, "b": {"c": 2}, "c": True}),
// )
// == OK
// )

// result = await json.get(redis_client, key, "$..c")
// assert isinstance(result, str)
// assert OuterJson.loads(result) == [True, 1, 2]

// result = await json.get(redis_client, key, ["$..c", "$.c"])
// assert isinstance(result, str)
// assert OuterJson.loads(result) == {"$..c": [True, 1, 2], "$.c": [True]}

// assert await json.set(redis_client, key, "$..c", '"new_value"') == OK
// result = await json.get(redis_client, key, "$..c")
// assert isinstance(result, str)
// assert OuterJson.loads(result) == ["new_value"] * 3

// @pytest.mark.parametrize("cluster_mode", [True, False])
// @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
// async def test_json_set_conditional_set(self, redis_client: TRedisClient):
// key = get_random_string(5)
// value = OuterJson.dumps({"a": 1.0, "b": 2})
// assert (
// await json.set(
// redis_client,
// key,
// "$",
// value,
// ConditionalChange.ONLY_IF_EXISTS,
// )
// is None
// )
// assert (
// await json.set(
// redis_client,
// key,
// "$",
// value,
// ConditionalChange.ONLY_IF_DOES_NOT_EXIST,
// )
// == OK
// )

// assert (
// await json.set(
// redis_client,
// key,
// "$.a",
// "4.5",
// ConditionalChange.ONLY_IF_DOES_NOT_EXIST,
// )
// is None
// )

// assert await json.get(redis_client, key, ".a") == "1.0"

// assert (
// await json.set(
// redis_client,
// key,
// "$.a",
// "4.5",
// ConditionalChange.ONLY_IF_EXISTS,
// )
// == OK
// )

// assert await json.get(redis_client, key, ".a") == "4.5"

// @pytest.mark.parametrize("cluster_mode", [True, False])
// @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
// async def test_json_get_formatting(self, redis_client: TRedisClient):
// key = get_random_string(5)
// assert (
// await json.set(
// redis_client,
// key,
// "$",
// OuterJson.dumps({"a": 1.0, "b": 2, "c": {"d": 3, "e": 4}}),
// )
// == OK
// )

// result = await json.get(
// redis_client, key, "$", JsonGetOptions(indent=" ", newline="\n", space=" ")
// )

// expected_result = '[\n {\n "a": 1.0,\n "b": 2,\n "c": {\n "d": 3,\n "e": 4\n }\n }\n]'
// assert result == expected_result

// result = await json.get(
// redis_client, key, "$", JsonGetOptions(indent="~", newline="\n", space="*")
// )

// expected_result = (
// '[\n~{\n~~"a":*1.0,\n~~"b":*2,\n~~"c":*{\n~~~"d":*3,\n~~~"e":*4\n~~}\n~}\n]'
// )
// assert result == expected_result

// TODO: convert to ts tests
Loading