From 781bd2184630b01bd246d4cef2010a6ebaa8083e Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 18 Jul 2024 17:31:52 +0300 Subject: [PATCH 1/2] feat: add json mset command --- biome.json | 3 - pkg/commands/json_mset.test.ts | 59 +++++++++++++++ pkg/commands/json_mset.ts | 25 +++++++ pkg/commands/mod.ts | 1 + pkg/pipeline.ts | 133 ++++++++++++++++++++++++--------- pkg/redis.ts | 92 ++++++++++++++++------- 6 files changed, 246 insertions(+), 67 deletions(-) create mode 100644 pkg/commands/json_mset.test.ts create mode 100644 pkg/commands/json_mset.ts diff --git a/biome.json b/biome.json index 7076dc12..504def31 100644 --- a/biome.json +++ b/biome.json @@ -10,9 +10,6 @@ "performance": { "noDelete": "off" }, - "nursery": { - "useAwait": "error" - }, "complexity": { "noBannedTypes": "off" }, diff --git a/pkg/commands/json_mset.test.ts b/pkg/commands/json_mset.test.ts new file mode 100644 index 00000000..ce5e9bc0 --- /dev/null +++ b/pkg/commands/json_mset.test.ts @@ -0,0 +1,59 @@ +import { afterAll, expect, test } from "bun:test"; +import { keygen, newHttpClient } from "../test-utils"; + +import { JsonGetCommand } from "./json_get"; +import { JsonSetCommand } from "./json_set"; +import { JsonMSetCommand } from "./json_mset"; + +const client = newHttpClient(); + +const { newKey, cleanup } = keygen(); +afterAll(cleanup); + +test("add a new value", async () => { + const key1 = newKey(); + const key2 = newKey(); + const key3 = newKey(); + const res1 = await new JsonMSetCommand([ + { key: key1, path: "$", value: { key: "value" } }, + { key: key2, path: "$", value: { key: "value" } }, + { key: key3, path: "$", value: { key: "value" } }, + ]).exec(client); + expect(res1).toEqual("OK"); +}); + +test("replace an existing value", async () => { + const key = newKey(); + const res1 = await new JsonMSetCommand([ + { key, path: "$", value: { a: 2 } }, + ]).exec(client); + expect(res1).toEqual("OK"); + const res2 = await new JsonMSetCommand([{ key, path: "$.a", value: 3 }]).exec( + client + ); + expect(res2).toEqual("OK"); + const res3 = await new JsonGetCommand([key, "$"]).exec(client); + expect(res3).toEqual([{ a: 3 }]); +}); + +test("update multi-paths", async () => { + const key = newKey(); + const data = { + f1: { a: 1 }, + f2: { a: 2 }, + }; + const res1 = await new JsonMSetCommand([ + { key, path: "$", value: data }, + ]).exec(client); + expect(res1).toEqual("OK"); + const res2 = await new JsonMSetCommand([ + { key, path: "$..a", value: 3 }, + ]).exec(client); + expect(res2).toEqual("OK"); + const res3 = await new JsonGetCommand([key, "$"]).exec(client); + + expect(res3).not.toBeNull(); + expect(res3!.length).toEqual(1); + expect(res3![0]?.f1?.a).toEqual(3); + expect(res3![0]?.f2?.a).toEqual(3); +}); diff --git a/pkg/commands/json_mset.ts b/pkg/commands/json_mset.ts new file mode 100644 index 00000000..041ba91b --- /dev/null +++ b/pkg/commands/json_mset.ts @@ -0,0 +1,25 @@ +import { Command, CommandOptions } from "./command"; + +/** + * @see https://redis.io/commands/json.mset + */ +export class JsonMSetCommand< + TData extends + | number + | string + | boolean + | Record + | (number | string | boolean | Record)[] +> extends Command<"OK" | null, "OK" | null> { + constructor( + cmd: { key: string; path: string; value: TData }[], + opts?: CommandOptions<"OK" | null, "OK" | null> + ) { + const command: unknown[] = ["JSON.MSET"]; + + for (const c of cmd) { + command.push(c.key, c.path, c.value); + } + super(command, opts); + } +} diff --git a/pkg/commands/mod.ts b/pkg/commands/mod.ts index 0ca3cd7f..eaa7f006 100644 --- a/pkg/commands/mod.ts +++ b/pkg/commands/mod.ts @@ -58,6 +58,7 @@ export * from "./json_del"; export * from "./json_forget"; export * from "./json_get"; export * from "./json_mget"; +export * from "./json_mset"; export * from "./json_numincrby"; export * from "./json_nummultby"; export * from "./json_objkeys"; diff --git a/pkg/pipeline.ts b/pkg/pipeline.ts index 003bd61d..7d174e67 100644 --- a/pkg/pipeline.ts +++ b/pkg/pipeline.ts @@ -59,6 +59,7 @@ import { JsonForgetCommand, JsonGetCommand, JsonMGetCommand, + JsonMSetCommand, JsonNumIncrByCommand, JsonNumMultByCommand, JsonObjKeysCommand, @@ -242,7 +243,7 @@ export class Pipeline[] = []> { this.exec = async < TCommandResults extends unknown[] = [] extends TCommands ? unknown[] - : InferResponseData, + : InferResponseData >(): Promise => { const start = performance.now(); const result = await originalExec(); @@ -250,8 +251,10 @@ export class Pipeline[] = []> { const loggerResult = (end - start).toFixed(2); console.log( `Latency for \x1b[38;2;19;185;39m${ - this.multiExec ? ["MULTI-EXEC"] : ["PIPELINE"].toString().toUpperCase() - }\x1b[0m: \x1b[38;2;0;255;255m${loggerResult} ms\x1b[0m`, + this.multiExec + ? ["MULTI-EXEC"] + : ["PIPELINE"].toString().toUpperCase() + }\x1b[0m: \x1b[38;2;0;255;255m${loggerResult} ms\x1b[0m` ); return result as TCommandResults; }; @@ -273,7 +276,7 @@ export class Pipeline[] = []> { exec = async < TCommandResults extends unknown[] = [] extends TCommands ? unknown[] - : InferResponseData, + : InferResponseData >(): Promise => { if (this.commands.length === 0) { throw new Error("Pipeline is empty"); @@ -287,7 +290,7 @@ export class Pipeline[] = []> { return res.map(({ error, result }, i) => { if (error) { throw new UpstashError( - `Command ${i + 1} [ ${this.commands[i].command[0]} ] failed: ${error}`, + `Command ${i + 1} [ ${this.commands[i].command[0]} ] failed: ${error}` ); } @@ -306,7 +309,9 @@ export class Pipeline[] = []> { * Pushes a command into the pipeline and returns a chainable instance of the * pipeline */ - private chain(command: Command): Pipeline<[...TCommands, Command]> { + private chain( + command: Command + ): Pipeline<[...TCommands, Command]> { this.commands.push(command); return this as any; // TS thinks we're returning Pipeline<[]> here, because we're not creating a new instance of the class, hence the cast } @@ -340,7 +345,12 @@ export class Pipeline[] = []> { * @see https://redis.io/commands/bitfield */ bitfield = (...args: CommandArgs) => - new BitFieldCommand(args, this.client, this.commandOptions, this.chain.bind(this)); + new BitFieldCommand( + args, + this.client, + this.commandOptions, + this.chain.bind(this) + ); /** * @see https://redis.io/commands/bitop @@ -352,7 +362,9 @@ export class Pipeline[] = []> { sourceKey: string, ...sourceKeys: string[] ): Pipeline<[...TCommands, BitOpCommand]>; - (op: "not", destinationKey: string, sourceKey: string): Pipeline<[...TCommands, BitOpCommand]>; + (op: "not", destinationKey: string, sourceKey: string): Pipeline< + [...TCommands, BitOpCommand] + >; } = ( op: "and" | "or" | "xor" | "not", destinationKey: string, @@ -360,7 +372,10 @@ export class Pipeline[] = []> { ...sourceKeys: string[] ) => this.chain( - new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.commandOptions), + new BitOpCommand( + [op as any, destinationKey, sourceKey, ...sourceKeys], + this.commandOptions + ) ); /** @@ -487,8 +502,9 @@ export class Pipeline[] = []> { /** * @see https://redis.io/commands/geosearchstore */ - geosearchstore = (...args: CommandArgs>) => - this.chain(new GeoSearchStoreCommand(args, this.commandOptions)); + geosearchstore = ( + ...args: CommandArgs> + ) => this.chain(new GeoSearchStoreCommand(args, this.commandOptions)); /** * @see https://redis.io/commands/get @@ -539,8 +555,9 @@ export class Pipeline[] = []> { /** * @see https://redis.io/commands/hgetall */ - hgetall = >(...args: CommandArgs) => - this.chain(new HGetAllCommand(args, this.commandOptions)); + hgetall = >( + ...args: CommandArgs + ) => this.chain(new HGetAllCommand(args, this.commandOptions)); /** * @see https://redis.io/commands/hincrby @@ -569,8 +586,9 @@ export class Pipeline[] = []> { /** * @see https://redis.io/commands/hmget */ - hmget = >(...args: CommandArgs) => - this.chain(new HMGetCommand(args, this.commandOptions)); + hmget = >( + ...args: CommandArgs + ) => this.chain(new HMGetCommand(args, this.commandOptions)); /** * @see https://redis.io/commands/hmset @@ -584,9 +602,14 @@ export class Pipeline[] = []> { hrandfield = >( key: string, count?: number, - withValues?: boolean, + withValues?: boolean ) => - this.chain(new HRandFieldCommand([key, count, withValues] as any, this.commandOptions)); + this.chain( + new HRandFieldCommand( + [key, count, withValues] as any, + this.commandOptions + ) + ); /** * @see https://redis.io/commands/hscan @@ -604,7 +627,9 @@ export class Pipeline[] = []> { * @see https://redis.io/commands/hsetnx */ hsetnx = (key: string, field: string, value: TData) => - this.chain(new HSetNXCommand([key, field, value], this.commandOptions)); + this.chain( + new HSetNXCommand([key, field, value], this.commandOptions) + ); /** * @see https://redis.io/commands/hstrlen @@ -651,8 +676,18 @@ export class Pipeline[] = []> { /** * @see https://redis.io/commands/linsert */ - linsert = (key: string, direction: "before" | "after", pivot: TData, value: TData) => - this.chain(new LInsertCommand([key, direction, pivot, value], this.commandOptions)); + linsert = ( + key: string, + direction: "before" | "after", + pivot: TData, + value: TData + ) => + this.chain( + new LInsertCommand( + [key, direction, pivot, value], + this.commandOptions + ) + ); /** * @see https://redis.io/commands/llen @@ -688,13 +723,17 @@ export class Pipeline[] = []> { * @see https://redis.io/commands/lpush */ lpush = (key: string, ...elements: TData[]) => - this.chain(new LPushCommand([key, ...elements], this.commandOptions)); + this.chain( + new LPushCommand([key, ...elements], this.commandOptions) + ); /** * @see https://redis.io/commands/lpushx */ lpushx = (key: string, ...elements: TData[]) => - this.chain(new LPushXCommand([key, ...elements], this.commandOptions)); + this.chain( + new LPushXCommand([key, ...elements], this.commandOptions) + ); /** * @see https://redis.io/commands/lrange @@ -784,7 +823,9 @@ export class Pipeline[] = []> { * @see https://redis.io/commands/psetex */ psetex = (key: string, ttl: number, value: TData) => - this.chain(new PSetEXCommand([key, ttl, value], this.commandOptions)); + this.chain( + new PSetEXCommand([key, ttl, value], this.commandOptions) + ); /** * @see https://redis.io/commands/pttl @@ -931,20 +972,28 @@ export class Pipeline[] = []> { /** * @see https://redis.io/commands/smembers */ - smembers = (...args: CommandArgs) => - this.chain(new SMembersCommand(args, this.commandOptions)); + smembers = ( + ...args: CommandArgs + ) => this.chain(new SMembersCommand(args, this.commandOptions)); /** * @see https://redis.io/commands/smismember */ smismember = (key: string, members: TMembers) => - this.chain(new SMIsMemberCommand([key, members], this.commandOptions)); + this.chain( + new SMIsMemberCommand([key, members], this.commandOptions) + ); /** * @see https://redis.io/commands/smove */ smove = (source: string, destination: string, member: TData) => - this.chain(new SMoveCommand([source, destination, member], this.commandOptions)); + this.chain( + new SMoveCommand( + [source, destination, member], + this.commandOptions + ) + ); /** * @see https://redis.io/commands/spop @@ -1022,27 +1071,31 @@ export class Pipeline[] = []> { */ zadd = ( ...args: - | [key: string, scoreMember: ScoreMember, ...scoreMemberPairs: ScoreMember[]] + | [ + key: string, + scoreMember: ScoreMember, + ...scoreMemberPairs: ScoreMember[] + ] | [ key: string, opts: ZAddCommandOptions, - ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], + ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]] ] ) => { if ("score" in args[1]) { return this.chain( new ZAddCommand( [args[0], args[1] as ScoreMember, ...(args.slice(2) as any)], - this.commandOptions, - ), + this.commandOptions + ) ); } return this.chain( new ZAddCommand( [args[0], args[1] as any, ...(args.slice(2) as any)], - this.commandOptions, - ), + this.commandOptions + ) ); }; @@ -1146,7 +1199,9 @@ export class Pipeline[] = []> { * @see https://redis.io/commands/zincrby */ zincrby = (key: string, increment: number, member: TData) => - this.chain(new ZIncrByCommand([key, increment, member], this.commandOptions)); + this.chain( + new ZIncrByCommand([key, increment, member], this.commandOptions) + ); /** * @see https://redis.io/commands/zinterstore @@ -1188,13 +1243,13 @@ export class Pipeline[] = []> { key: string, min: `(${string}` | `[${string}` | "-" | "+", max: `(${string}` | `[${string}` | "-" | "+", - opts: { byLex: true } & ZRangeCommandOptions, + opts: { byLex: true } & ZRangeCommandOptions ] | [ key: string, min: number | `(${number}` | "-inf" | "+inf", max: number | `(${number}` | "-inf" | "+inf", - opts: { byScore: true } & ZRangeCommandOptions, + opts: { byScore: true } & ZRangeCommandOptions ] ) => this.chain(new ZRangeCommand(args as any, this.commandOptions)); @@ -1329,6 +1384,12 @@ export class Pipeline[] = []> { mget: (...args: CommandArgs) => this.chain(new JsonMGetCommand(args, this.commandOptions)), + /** + * @see https://redis.io/commands/json.mset + */ + mset: (...args: CommandArgs) => + this.chain(new JsonMSetCommand(args, this.commandOptions)), + /** * @see https://redis.io/commands/json.numincrby */ diff --git a/pkg/redis.ts b/pkg/redis.ts index cb35ff44..961c7554 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -60,6 +60,7 @@ import { JsonForgetCommand, JsonGetCommand, JsonMGetCommand, + JsonMSetCommand, JsonNumIncrByCommand, JsonNumMultByCommand, JsonObjKeysCommand, @@ -278,6 +279,12 @@ export class Redis { mget: (...args: CommandArgs) => new JsonMGetCommand(args, this.opts).exec(this.client), + /** + * @see https://redis.io/commands/json.mset + */ + mset: (...args: CommandArgs) => + new JsonMSetCommand(args, this.opts).exec(this.client), + /** * @see https://redis.io/commands/json.numincrby */ @@ -345,11 +352,14 @@ export class Redis { use = ( middleware: ( r: UpstashRequest, - next: (req: UpstashRequest) => Promise>, - ) => Promise>, + next: ( + req: UpstashRequest + ) => Promise> + ) => Promise> ) => { const makeRequest = this.client.request.bind(this.client); - this.client.request = (req: UpstashRequest) => middleware(req, makeRequest) as any; + this.client.request = (req: UpstashRequest) => + middleware(req, makeRequest) as any; }; /** @@ -450,9 +460,10 @@ export class Redis { sourceKey: string, ...sourceKeys: string[] ) => - new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( - this.client, - ); + new BitOpCommand( + [op as any, destinationKey, sourceKey, ...sourceKeys], + this.opts + ).exec(this.client); /** * @see https://redis.io/commands/bitpos @@ -572,8 +583,9 @@ export class Redis { /** * @see https://redis.io/commands/geosearchstore */ - geosearchstore = (...args: CommandArgs>) => - new GeoSearchStoreCommand(args, this.opts).exec(this.client); + geosearchstore = ( + ...args: CommandArgs> + ) => new GeoSearchStoreCommand(args, this.opts).exec(this.client); /** * @see https://redis.io/commands/get @@ -625,8 +637,9 @@ export class Redis { /** * @see https://redis.io/commands/hgetall */ - hgetall = >(...args: CommandArgs) => - new HGetAllCommand(args, this.opts).exec(this.client); + hgetall = >( + ...args: CommandArgs + ) => new HGetAllCommand(args, this.opts).exec(this.client); /** * @see https://redis.io/commands/hincrby @@ -655,8 +668,9 @@ export class Redis { /** * @see https://redis.io/commands/hmget */ - hmget = >(...args: CommandArgs) => - new HMGetCommand(args, this.opts).exec(this.client); + hmget = >( + ...args: CommandArgs + ) => new HMGetCommand(args, this.opts).exec(this.client); /** * @see https://redis.io/commands/hmset @@ -673,13 +687,17 @@ export class Redis { >( key: string, count: number, - withValues: boolean, + withValues: boolean ): Promise>; } = >( key: string, count?: number, - withValues?: boolean, - ) => new HRandFieldCommand([key, count, withValues] as any, this.opts).exec(this.client); + withValues?: boolean + ) => + new HRandFieldCommand( + [key, count, withValues] as any, + this.opts + ).exec(this.client); /** * @see https://redis.io/commands/hscan @@ -744,8 +762,15 @@ export class Redis { /** * @see https://redis.io/commands/linsert */ - linsert = (key: string, direction: "before" | "after", pivot: TData, value: TData) => - new LInsertCommand([key, direction, pivot, value], this.opts).exec(this.client); + linsert = ( + key: string, + direction: "before" | "after", + pivot: TData, + value: TData + ) => + new LInsertCommand([key, direction, pivot, value], this.opts).exec( + this.client + ); /** * @see https://redis.io/commands/llen @@ -1025,19 +1050,24 @@ export class Redis { * @see https://redis.io/commands/smismember */ smismember = (key: string, members: TMembers) => - new SMIsMemberCommand([key, members], this.opts).exec(this.client); + new SMIsMemberCommand([key, members], this.opts).exec( + this.client + ); /** * @see https://redis.io/commands/smembers */ - smembers = (...args: CommandArgs) => - new SMembersCommand(args, this.opts).exec(this.client); + smembers = ( + ...args: CommandArgs + ) => new SMembersCommand(args, this.opts).exec(this.client); /** * @see https://redis.io/commands/smove */ smove = (source: string, destination: string, member: TData) => - new SMoveCommand([source, destination, member], this.opts).exec(this.client); + new SMoveCommand([source, destination, member], this.opts).exec( + this.client + ); /** * @see https://redis.io/commands/spop @@ -1199,23 +1229,27 @@ export class Redis { */ zadd = ( ...args: - | [key: string, scoreMember: ScoreMember, ...scoreMemberPairs: ScoreMember[]] + | [ + key: string, + scoreMember: ScoreMember, + ...scoreMemberPairs: ScoreMember[] + ] | [ key: string, opts: ZAddCommandOptions, - ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], + ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]] ] ) => { if ("score" in args[1]) { return new ZAddCommand( [args[0], args[1] as ScoreMember, ...(args.slice(2) as any)], - this.opts, + this.opts ).exec(this.client); } return new ZAddCommand( [args[0], args[1] as any, ...(args.slice(2) as any)], - this.opts, + this.opts ).exec(this.client); }; /** @@ -1240,7 +1274,9 @@ export class Redis { * @see https://redis.io/commands/zincrby */ zincrby = (key: string, increment: number, member: TData) => - new ZIncrByCommand([key, increment, member], this.opts).exec(this.client); + new ZIncrByCommand([key, increment, member], this.opts).exec( + this.client + ); /** * @see https://redis.io/commands/zinterstore @@ -1282,13 +1318,13 @@ export class Redis { key: string, min: `(${string}` | `[${string}` | "-" | "+", max: `(${string}` | `[${string}` | "-" | "+", - opts: { byLex: true } & ZRangeCommandOptions, + opts: { byLex: true } & ZRangeCommandOptions ] | [ key: string, min: number | `(${number}` | "-inf" | "+inf", max: number | `(${number}` | "-inf" | "+inf", - opts: { byScore: true } & ZRangeCommandOptions, + opts: { byScore: true } & ZRangeCommandOptions ] ) => new ZRangeCommand(args as any, this.opts).exec(this.client); From 12e4069dc7f50055d7622ea06449dea324009316 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Fri, 19 Jul 2024 13:09:39 +0300 Subject: [PATCH 2/2] fix: remove unused import --- pkg/commands/json_mset.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/commands/json_mset.test.ts b/pkg/commands/json_mset.test.ts index ce5e9bc0..b9c916aa 100644 --- a/pkg/commands/json_mset.test.ts +++ b/pkg/commands/json_mset.test.ts @@ -2,7 +2,6 @@ import { afterAll, expect, test } from "bun:test"; import { keygen, newHttpClient } from "../test-utils"; import { JsonGetCommand } from "./json_get"; -import { JsonSetCommand } from "./json_set"; import { JsonMSetCommand } from "./json_mset"; const client = newHttpClient();