Skip to content
Draft
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"typecheck": "tsc -p ."
},
"dependencies": {
"@types/ioredis": "^4.17.6",
"@types/redis": "^2.8.30"
},
"devDependencies": {
Expand All @@ -78,6 +79,7 @@
"eslint-plugin-unicorn": "33.0.1",
"expect-type": "0.11.0",
"fs-syncer": "0.3.4-next.1",
"ioredis": "4.17.3",
"jest": "27.0.5",
"lodash": "4.17.21",
"npm-run-all": "4.1.5",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export {
CreateNodeRedisClient,
} from "./node_redis";

export { createIORedisClient, WrappedIORedisClient } from "./ioredis";

// aliases for backwards-compatibility with v1.x

/** @deprecated use `createNodeRedisClient` */
Expand Down
22 changes: 22 additions & 0 deletions src/ioredis/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as IORedis from "ioredis";
import { Commands } from "../generated/interface";
import { WrappedIORedisMulti } from "./multi";

export interface WrappedIORedisClient extends Omit<Commands, "multi"> {
ioredis: IORedis.Redis;
multi(): WrappedIORedisMulti;
}

export interface CreateIORedisClient {
(): WrappedIORedisClient;
(ioredis: IORedis.Redis): WrappedIORedisClient;
(port?: number, host?: string, options?: IORedis.RedisOptions): WrappedIORedisClient;
(host?: string, options?: IORedis.RedisOptions): WrappedIORedisClient;
(options?: IORedis.RedisOptions): WrappedIORedisClient;
}

export const createIORedisClient: CreateIORedisClient = (...clientArgs: any[]): WrappedIORedisClient => {
const ioredis = clientArgs[0] instanceof IORedis ? clientArgs[0] : new IORedis(...clientArgs);

return Object.assign(ioredis, { ioredis }) as any;
};
31 changes: 31 additions & 0 deletions src/ioredis/multi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Commands } from "../generated/interface";
import { Push } from "../push";

declare module "../generated/interface" {
export interface ResultTypes<Result, Context> {
/**
* This determines the correct type for a ioredis multi result. e.g. `multi.keys('foo:*')` should be a multi instance
* which will include include a `string[]` value in the array eventually returned by `.exec()`.
*/
ioredis_multi: WrappedIORedisMulti<
Push<
// get the original results from client context
Extract<Context, { results: unknown[] }>["results"],
// Then push in the new result (e.g. for `keys`, `Result = string[]`)
MultiResult<Result>
>
>;
}
}

export type MultiResult<T> = [Error, undefined] | [null, T];

export interface WrappedIORedisMulti<Results extends unknown[] = []>
extends Omit<Commands<{ type: "ioredis_multi"; results: Results }>, "exec"> {
/** Execute all commands issued after multi */
exec(): Promise<Results>;
}

export interface IORedisMultiMixins {
multi(): WrappedIORedisMulti;
}
89 changes: 89 additions & 0 deletions test/ioredis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { createIORedisClient } from "../src";
import * as IORedis from "ioredis";
import { expectTypeOf } from "expect-type";
import { Commands } from "../src/generated/interface";

const client = createIORedisClient();

test("get and set", async () => {
expect(await client.set("foo", "bar")).toEqual("OK");
expect(await client.get("foo")).toEqual("bar");
});

test("consruct with existing ioredis instance", async () => {
const ioredis = new IORedis();

const client = createIORedisClient(ioredis);

expect(client.ioredis).toBe(ioredis);

// we don't need to worry too much about functionality - with ioredis the wrapper is purely a type helper
expect(client).toBe(ioredis);
});

test("type", async () => {
expectTypeOf(client).toMatchTypeOf<Omit<Commands, "multi">>();

expectTypeOf(client.ioredis).toEqualTypeOf<IORedis.Redis>();
});

test("multi", async () => {
await client.set("z:foo", "abc");

const multiResult = await client
.multi()
.setex("z:foo", "NOTANUMBER" as any, "xyz")
.keys("z:*")
.get("z:foo")
.exec();

expect(multiResult).toMatchInlineSnapshot(`
Array [
Array [
[ReplyError: ERR value is not an integer or out of range],
],
Array [
null,
Array [
"z:foo",
],
],
Array [
null,
"abc",
],
]
`);

expectTypeOf(multiResult).toMatchTypeOf<{ length: 3 }>();

expectTypeOf(multiResult[0]).toEqualTypeOf<[Error, undefined] | [null, "OK"]>();
expectTypeOf(multiResult[1]).toEqualTypeOf<[Error, undefined] | [null, string[]]>();
expectTypeOf(multiResult[2]).toEqualTypeOf<[Error, undefined] | [null, string | null]>();

// usage - result requires null-checking
const [err, res] = multiResult[0];

expectTypeOf(res).toEqualTypeOf<"OK" | undefined>();
expectTypeOf(err).toEqualTypeOf<Error | null>();

// @ts-expect-error
const _accessWithoutNullCheckError = () => res.slice();

const _accessWithNullCheckOk = () => res?.slice();

const firstResult = multiResult[0];
const _accessTupleWithErrorCheckOk = () => (firstResult[0] ? undefined : firstResult[1].slice());
});

test("hgetall is consistent with node_redis", async () => {
// node_redis transforms hgetall results into a dictionary. luckily ioredis does too
await client.hset("myhash", ["field1", "Hello"], ["field2", "Goodbye"]);
await client.hset("myhash", ["field3", "foo"]);

expect(await client.hgetall("myhash")).toMatchObject({
field1: "Hello",
field2: "Goodbye",
field3: "foo",
});
});
49 changes: 48 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,13 @@
dependencies:
"@types/node" "*"

"@types/ioredis@^4.17.6":
version "4.17.6"
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.17.6.tgz#b3b8d6e9fed7f494da08d991d8110daab2e273b8"
integrity sha512-Vg3vuhveOaQeiubzGpVU9Hn3UJCi7rPe2TOphc3m3Y2EniHmytHp/wj5ZtfqDGBhFJGaSwbJvGQhhqd/Nqfy4Q==
dependencies:
"@types/node" "*"

"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
Expand Down Expand Up @@ -2472,6 +2479,11 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=

cluster-key-slot@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==

cmd-shim@^3.0.0, cmd-shim@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-3.0.3.tgz#2c35238d3df37d98ecdd7d5f6b8dc6b21cadc7cb"
Expand Down Expand Up @@ -2933,7 +2945,7 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=

denque@^1.5.0:
denque@^1.1.0, denque@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
Expand Down Expand Up @@ -4414,6 +4426,21 @@ io-ts@^2.2.4:
resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.11.tgz#968a02f57b740fd422c8f9cf7ee99187854636f9"
integrity sha512-lGxyPvZNhmo1U1Hy0ovjRtXljG6F59R3bZErK9XjiIQPlt4nuzbDNSwvalXvS7Z8iSZhx2S5GpNz5tVhpnDf6w==

[email protected]:
version "4.17.3"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.17.3.tgz#9938c60e4ca685f75326337177bdc2e73ae9c9dc"
integrity sha512-iRvq4BOYzNFkDnSyhx7cmJNOi1x/HWYe+A4VXHBu4qpwJaGT1Mp+D2bVGJntH9K/Z/GeOM/Nprb8gB3bmitz1Q==
dependencies:
cluster-key-slot "^1.1.0"
debug "^4.1.1"
denque "^1.1.0"
lodash.defaults "^4.2.0"
lodash.flatten "^4.4.0"
redis-commands "1.5.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
standard-as-callback "^2.0.1"

ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
Expand Down Expand Up @@ -5641,11 +5668,21 @@ lodash.clonedeep@^4.5.0, lodash.clonedeep@~4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=

lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=

lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=

lodash.flatten@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=

lodash.ismatch@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
Expand Down Expand Up @@ -7283,6 +7320,11 @@ redeyed@~2.1.0:
dependencies:
esprima "~4.0.0"

[email protected]:
version "1.5.0"
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785"
integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==

redis-commands@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
Expand Down Expand Up @@ -7849,6 +7891,11 @@ stack-utils@^2.0.3:
dependencies:
escape-string-regexp "^2.0.0"

standard-as-callback@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==

stream-combiner2@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe"
Expand Down