Skip to content

Commit 77102a3

Browse files
committed
feat: add type-safe keyspaces
This feature allows you to constrain the set of valid keys in a “keyspace” (a namespace of keys). This is especially useful if the keys should have a specific format or if only a finite set of keys are valid. enum Robot { Butler, Gardener, } enum Power { On = "on", Off = "off", } const robot = new RedisEntity( new RedisKeyspace<Robot>("robot-configuration"), Type.Enum(Power), ); // You must qualify which robot you're setting the power for. // TypeScript will enforce this. SET(robot.qualify(Robot.Butler), Power.On);
1 parent 8d6d289 commit 77102a3

File tree

7 files changed

+163
-109
lines changed

7 files changed

+163
-109
lines changed

src/channel.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TSchema, TString, Type } from "@sinclair/typebox";
22
import { isString } from "radashi";
3-
import { RedisKey } from "./key";
3+
import { RedisKey, RedisKeyspace } from "./key";
44
import { MessageEvent } from "./subscriber";
55
import { RedisTransform } from "./transform";
66

@@ -9,23 +9,31 @@ import { RedisTransform } from "./transform";
99
*/
1010
export class RedisChannel<
1111
T extends TSchema = TSchema,
12+
K extends string | RedisKeyspace = string,
1213
> extends RedisTransform<T> {
1314
declare $$typeof: "RedisChannel";
1415
constructor(
15-
readonly name: string,
16+
readonly name: K,
1617
schema: T,
1718
) {
1819
super(schema);
1920
}
2021

2122
/**
22-
* Derive a subchannel by prefixing the current channel with the given
23-
* keys. When multiple keys are passed in, they will be joined with a
24-
* colon.
23+
* Creates a new channel within a namespace.
2524
*/
26-
join(...keys: (string | number)[]) {
27-
if (keys.length === 0) return this;
28-
return new RedisChannel(`${this.name}:${keys.join(":")}`, this.schema);
25+
qualify(
26+
name: K extends RedisKeyspace<infer Key> ? Key : string | number,
27+
): RedisChannel<T, string>;
28+
qualify<T extends TSchema>(
29+
name: K extends RedisKeyspace<infer Key> ? Key : string | number,
30+
schema: T,
31+
): RedisChannel<T, string>;
32+
qualify(
33+
name: K extends RedisKeyspace<infer Key> ? Key : string | number,
34+
schema: TSchema = this.schema,
35+
) {
36+
return new RedisChannel<any>(`${this.name}:${name}`, schema);
2937
}
3038

3139
/**
@@ -62,7 +70,7 @@ export class RedisChannelPattern<
6270
* [1]: https://redis.io/docs/latest/develop/use/keyspace-notifications/
6371
*/
6472
export class RedisKeyspacePattern extends RedisChannelPattern<TString> {
65-
constructor(pattern: string | RedisKey, database?: number) {
73+
constructor(pattern: string | RedisKey | RedisKeyspace, database?: number) {
6674
super(
6775
`__keyspace@${database ?? "*"}__:${isString(pattern) ? pattern : pattern.name}`,
6876
Type.String(),

src/commands/hash.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,36 @@
1-
import { Static, StaticEncode } from "@sinclair/typebox";
1+
import { StaticEncode, TSchema } from "@sinclair/typebox";
22
import { RedisCommand, RedisValue } from "../command";
3-
import { RedisField, RedisHash, TRedisHash } from "../key";
3+
import { RedisHash } from "../key";
4+
5+
type RedisField<T extends Record<string, TSchema>> = keyof T & string;
46

57
/**
68
* Get the value of a field in a hash.
79
*/
8-
export function HGET<T extends TRedisHash, TField extends RedisField<T>>(
9-
hash: RedisHash<T>,
10-
field: TField,
11-
) {
12-
return new RedisCommand<Static<T[TField]>>(
13-
["HGET", hash.name, field],
14-
(result) => hash.decodeField(field, result),
10+
export function HGET<
11+
T extends Record<string, TSchema>,
12+
TField extends RedisField<T>,
13+
>(hash: RedisHash<T>, field: TField) {
14+
return new RedisCommand(["HGET", hash.name, field as string], (result) =>
15+
hash.decodeField(field, result),
1516
);
1617
}
1718

1819
/**
1920
* Set the value of a field in a hash.
2021
*/
21-
export function HSET<T extends TRedisHash, TField extends RedisField<T>>(
22-
hash: RedisHash<T>,
23-
field: TField,
24-
): never;
22+
export function HSET<
23+
T extends Record<string, TSchema>,
24+
TField extends RedisField<T>,
25+
>(hash: RedisHash<T>, field: TField): never;
2526

2627
/**
2728
* Set the value of a field in a hash.
2829
*/
29-
export function HSET<T extends TRedisHash, TField extends RedisField<T>>(
30+
export function HSET<
31+
T extends Record<string, TSchema>,
32+
TField extends RedisField<T>,
33+
>(
3034
hash: RedisHash<T>,
3135
field: TField,
3236
value: StaticEncode<T[TField]>,
@@ -35,12 +39,12 @@ export function HSET<T extends TRedisHash, TField extends RedisField<T>>(
3539
/**
3640
* Set the values of multiple fields in a hash.
3741
*/
38-
export function HSET<T extends TRedisHash>(
42+
export function HSET<T extends Record<string, TSchema>>(
3943
hash: RedisHash<T>,
4044
values: { [K in RedisField<T>]?: StaticEncode<T[K]> },
4145
): RedisCommand<number>;
4246

43-
export function HSET<T extends TRedisHash>(
47+
export function HSET<T extends Record<string, TSchema>>(
4448
hash: RedisHash<T>,
4549
field: RedisField<T> | object,
4650
value?: unknown,
@@ -52,7 +56,7 @@ export function HSET<T extends TRedisHash>(
5256
);
5357
}
5458

55-
function encodeHashEntries<T extends TRedisHash>(
59+
function encodeHashEntries<T extends Record<string, TSchema>>(
5660
hash: RedisHash<T>,
5761
values: object,
5862
) {

src/commands/read.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import { StaticEncode, TSchema } from "@sinclair/typebox";
22
import { RedisCommand } from "../command";
3-
import { RedisKey } from "../key";
3+
import { RedisEntity } from "../key";
44
import { RedisModifier } from "../modifier";
55

66
/** Return the previous value stored at this key */
77
export type GET = RedisModifier<"GET">;
88

99
/** Get the value of a key */
1010
export function GET<T extends TSchema>(
11-
key: RedisKey<T>,
11+
key: RedisEntity<T>,
1212
): RedisCommand<StaticEncode<T> | undefined>;
1313

1414
/** Return the previous value stored at this key */
1515
export function GET(): GET;
1616

1717
export function GET<T extends TSchema>(
18-
key?: RedisKey<T>,
18+
key?: RedisEntity<T>,
1919
): GET | RedisCommand<StaticEncode<T> | undefined> {
2020
if (!key) {
2121
return new RedisModifier("GET");

src/commands/write.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { StaticEncode, TNumber, TSchema } from "@sinclair/typebox";
22
import { RedisCommand } from "../command";
3-
import { RedisKey } from "../key";
3+
import { RedisEntity, RedisKey } from "../key";
44
import { encodeModifiers, Modifiers, Require } from "../modifier";
55
import {
66
EX,
@@ -31,7 +31,7 @@ export function FLUSHALL(mode?: "sync" | "async") {
3131
* expired, the key will automatically be deleted.
3232
*/
3333
export function EXPIRE(
34-
key: RedisKey<any>,
34+
key: RedisKey,
3535
timeout: number,
3636
...modifiers: Modifiers<[XX | NX | GT | LT]>
3737
) {
@@ -46,7 +46,7 @@ export function EXPIRE(
4646
* similar to {@link GET}, but is a write command with additional options.
4747
*/
4848
export function GETEX<T extends TSchema>(
49-
key: RedisKey<T>,
49+
key: RedisEntity<T>,
5050
...modifiers: Modifiers<[EX | PX | EXAT | PXAT | PERSIST]>
5151
) {
5252
return new RedisCommand<StaticEncode<T> | undefined>(
@@ -59,22 +59,22 @@ export function GETEX<T extends TSchema>(
5959
* Set the value of a key and optionally set its expiration.
6060
*/
6161
export function SET<T extends TSchema>(
62-
key: RedisKey<T>,
62+
key: RedisEntity<T>,
6363
value: StaticEncode<T>,
6464
...modifiers: Modifiers<
6565
[NX | XX, Require<GET>, EX | PX | EXAT | PXAT | KEEPTTL]
6666
>
6767
): RedisCommand<StaticEncode<T> | undefined>;
6868

6969
export function SET<T extends TSchema>(
70-
key: RedisKey<T>,
70+
key: RedisEntity<T>,
7171
value: StaticEncode<T>,
7272
...modifiers: Modifiers<[NX | XX, EX | PX | EXAT | PXAT | KEEPTTL]>
7373
): RedisCommand<boolean>;
7474

7575
export function SET(
76-
key: RedisKey<any>,
77-
value: StaticEncode<any>,
76+
key: RedisEntity,
77+
value: unknown,
7878
...modifiers: Modifiers
7979
): RedisCommand<any> {
8080
return new RedisCommand(
@@ -95,27 +95,27 @@ export function DEL(...keys: [RedisKey, ...RedisKey[]]) {
9595
/**
9696
* Decrement the value of a key by 1.
9797
*/
98-
export function DECR(key: RedisKey<TNumber>) {
98+
export function DECR(key: RedisEntity<TNumber>) {
9999
return new RedisCommand<number>(["DECR", key.name]);
100100
}
101101

102102
/**
103103
* Decrement the value of a key by a specific amount.
104104
*/
105-
export function DECRBY(key: RedisKey<TNumber>, amount: number) {
105+
export function DECRBY(key: RedisEntity<TNumber>, amount: number) {
106106
return new RedisCommand<number>(["DECRBY", key.name, amount]);
107107
}
108108

109109
/**
110110
* Increment the value of a key by 1.
111111
*/
112-
export function INCR(key: RedisKey<TNumber>) {
112+
export function INCR(key: RedisEntity<TNumber>) {
113113
return new RedisCommand<number>(["INCR", key.name]);
114114
}
115115

116116
/**
117117
* Increment the value of a key by a specific amount.
118118
*/
119-
export function INCRBY(key: RedisKey<TNumber>, amount: number) {
119+
export function INCRBY(key: RedisEntity<TNumber>, amount: number) {
120120
return new RedisCommand<number>(["INCRBY", key.name, amount]);
121121
}

0 commit comments

Comments
 (0)