@uploadista/kv-store-redis 0.0.13-beta.4 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -7
- package/tests/redis-kv-store.test.ts +369 -0
- package/vitest.config.ts +39 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/kv-store-redis",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.13
|
|
4
|
+
"version": "0.0.13",
|
|
5
5
|
"description": "Redis KV store for Uploadista",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
@@ -14,18 +14,26 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
17
|
+
"effect": "3.19.3",
|
|
18
|
+
"@uploadista/core": "0.0.13"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@redis/client": ">=5.8.0 <6.0.0"
|
|
20
22
|
},
|
|
21
23
|
"devDependencies": {
|
|
22
|
-
"
|
|
23
|
-
"@
|
|
24
|
+
"@effect/vitest": "0.27.0",
|
|
25
|
+
"@redis/client": "5.9.0",
|
|
26
|
+
"tsdown": "0.16.3",
|
|
27
|
+
"vitest": "4.0.8",
|
|
28
|
+
"@uploadista/typescript-config": "0.0.13"
|
|
24
29
|
},
|
|
25
30
|
"scripts": {
|
|
26
31
|
"build": "tsdown",
|
|
32
|
+
"check": "biome check --write ./src",
|
|
27
33
|
"format": "biome format --write ./src",
|
|
28
34
|
"lint": "biome lint --write ./src",
|
|
29
|
-
"
|
|
35
|
+
"test": "vitest",
|
|
36
|
+
"test:run": "vitest run",
|
|
37
|
+
"test:watch": "vitest --watch"
|
|
30
38
|
}
|
|
31
39
|
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import type { RedisClientType } from "@redis/client";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { makeRedisBaseKvStore } from "../src/redis-kv-store";
|
|
5
|
+
|
|
6
|
+
describe("Redis KV Store", () => {
|
|
7
|
+
describe("Basic Operations", () => {
|
|
8
|
+
it("should store and retrieve values", async () => {
|
|
9
|
+
const mockRedis = {
|
|
10
|
+
get: vi.fn().mockResolvedValue("value1"),
|
|
11
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
12
|
+
del: vi.fn().mockResolvedValue(1),
|
|
13
|
+
scan: vi.fn(),
|
|
14
|
+
} as unknown as RedisClientType;
|
|
15
|
+
|
|
16
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
17
|
+
|
|
18
|
+
await Effect.runPromise(
|
|
19
|
+
Effect.gen(function* () {
|
|
20
|
+
yield* store.set("key1", "value1");
|
|
21
|
+
const value = yield* store.get("key1");
|
|
22
|
+
expect(value).toBe("value1");
|
|
23
|
+
expect(mockRedis.set).toHaveBeenCalledWith("key1", "value1");
|
|
24
|
+
expect(mockRedis.get).toHaveBeenCalledWith("key1");
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should return null for non-existent keys", async () => {
|
|
30
|
+
const mockRedis = {
|
|
31
|
+
get: vi.fn().mockResolvedValue(null),
|
|
32
|
+
set: vi.fn(),
|
|
33
|
+
del: vi.fn(),
|
|
34
|
+
scan: vi.fn(),
|
|
35
|
+
} as unknown as RedisClientType;
|
|
36
|
+
|
|
37
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
38
|
+
|
|
39
|
+
await Effect.runPromise(
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const value = yield* store.get("non-existent");
|
|
42
|
+
expect(value).toBeNull();
|
|
43
|
+
expect(mockRedis.get).toHaveBeenCalledWith("non-existent");
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should delete values", async () => {
|
|
49
|
+
const mockRedis = {
|
|
50
|
+
get: vi.fn().mockResolvedValue("value1"),
|
|
51
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
52
|
+
del: vi.fn().mockResolvedValue(1),
|
|
53
|
+
scan: vi.fn(),
|
|
54
|
+
} as unknown as RedisClientType;
|
|
55
|
+
|
|
56
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
57
|
+
|
|
58
|
+
await Effect.runPromise(
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
yield* store.set("key1", "value1");
|
|
61
|
+
yield* store.delete("key1");
|
|
62
|
+
expect(mockRedis.del).toHaveBeenCalledWith("key1");
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("List Operations", () => {
|
|
69
|
+
it("should list all keys with prefix", async () => {
|
|
70
|
+
const mockRedis = {
|
|
71
|
+
get: vi.fn(),
|
|
72
|
+
set: vi.fn(),
|
|
73
|
+
del: vi.fn(),
|
|
74
|
+
scan: vi
|
|
75
|
+
.fn()
|
|
76
|
+
.mockResolvedValueOnce({
|
|
77
|
+
cursor: "5",
|
|
78
|
+
keys: ["user:1", "user:2"],
|
|
79
|
+
})
|
|
80
|
+
.mockResolvedValueOnce({
|
|
81
|
+
cursor: "0",
|
|
82
|
+
keys: ["user:3"],
|
|
83
|
+
}),
|
|
84
|
+
} as unknown as RedisClientType;
|
|
85
|
+
|
|
86
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
87
|
+
|
|
88
|
+
await Effect.runPromise(
|
|
89
|
+
Effect.gen(function* () {
|
|
90
|
+
const keys = yield* store.list!("user:");
|
|
91
|
+
expect(keys).toHaveLength(3);
|
|
92
|
+
expect(keys).toContain("1");
|
|
93
|
+
expect(keys).toContain("2");
|
|
94
|
+
expect(keys).toContain("3");
|
|
95
|
+
expect(mockRedis.scan).toHaveBeenCalledWith("0", {
|
|
96
|
+
MATCH: "user:*",
|
|
97
|
+
COUNT: 20,
|
|
98
|
+
});
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should handle empty list results", async () => {
|
|
104
|
+
const mockRedis = {
|
|
105
|
+
get: vi.fn(),
|
|
106
|
+
set: vi.fn(),
|
|
107
|
+
del: vi.fn(),
|
|
108
|
+
scan: vi.fn().mockResolvedValue({
|
|
109
|
+
cursor: "0",
|
|
110
|
+
keys: [],
|
|
111
|
+
}),
|
|
112
|
+
} as unknown as RedisClientType;
|
|
113
|
+
|
|
114
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
115
|
+
|
|
116
|
+
await Effect.runPromise(
|
|
117
|
+
Effect.gen(function* () {
|
|
118
|
+
const keys = yield* store.list!("nonexistent:");
|
|
119
|
+
expect(keys).toHaveLength(0);
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should handle paginated scan results", async () => {
|
|
125
|
+
const mockRedis = {
|
|
126
|
+
get: vi.fn(),
|
|
127
|
+
set: vi.fn(),
|
|
128
|
+
del: vi.fn(),
|
|
129
|
+
scan: vi
|
|
130
|
+
.fn()
|
|
131
|
+
.mockResolvedValueOnce({
|
|
132
|
+
cursor: "10",
|
|
133
|
+
keys: ["prefix:a", "prefix:b"],
|
|
134
|
+
})
|
|
135
|
+
.mockResolvedValueOnce({
|
|
136
|
+
cursor: "20",
|
|
137
|
+
keys: ["prefix:c", "prefix:d"],
|
|
138
|
+
})
|
|
139
|
+
.mockResolvedValueOnce({
|
|
140
|
+
cursor: "0",
|
|
141
|
+
keys: ["prefix:e"],
|
|
142
|
+
}),
|
|
143
|
+
} as unknown as RedisClientType;
|
|
144
|
+
|
|
145
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
146
|
+
|
|
147
|
+
await Effect.runPromise(
|
|
148
|
+
Effect.gen(function* () {
|
|
149
|
+
const keys = yield* store.list!("prefix:");
|
|
150
|
+
expect(keys).toHaveLength(5);
|
|
151
|
+
expect(mockRedis.scan).toHaveBeenCalledTimes(3);
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("Complex Data Types", () => {
|
|
158
|
+
it("should store and retrieve JSON strings", async () => {
|
|
159
|
+
const mockRedis = {
|
|
160
|
+
get: vi.fn().mockResolvedValue('{"name":"John","age":30}'),
|
|
161
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
162
|
+
del: vi.fn(),
|
|
163
|
+
scan: vi.fn(),
|
|
164
|
+
} as unknown as RedisClientType;
|
|
165
|
+
|
|
166
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
167
|
+
|
|
168
|
+
await Effect.runPromise(
|
|
169
|
+
Effect.gen(function* () {
|
|
170
|
+
const obj = { name: "John", age: 30 };
|
|
171
|
+
yield* store.set("user1", JSON.stringify(obj));
|
|
172
|
+
const retrieved = yield* store.get("user1");
|
|
173
|
+
expect(JSON.parse(retrieved!)).toEqual(obj);
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should handle empty strings", async () => {
|
|
179
|
+
const mockRedis = {
|
|
180
|
+
get: vi.fn().mockResolvedValue(""),
|
|
181
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
182
|
+
del: vi.fn(),
|
|
183
|
+
scan: vi.fn(),
|
|
184
|
+
} as unknown as RedisClientType;
|
|
185
|
+
|
|
186
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
187
|
+
|
|
188
|
+
await Effect.runPromise(
|
|
189
|
+
Effect.gen(function* () {
|
|
190
|
+
yield* store.set("key1", "");
|
|
191
|
+
const value = yield* store.get("key1");
|
|
192
|
+
expect(value).toBe("");
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("Error Handling", () => {
|
|
199
|
+
it("should handle get errors", async () => {
|
|
200
|
+
const mockRedis = {
|
|
201
|
+
get: vi.fn().mockRejectedValue(new Error("Connection failed")),
|
|
202
|
+
set: vi.fn(),
|
|
203
|
+
del: vi.fn(),
|
|
204
|
+
scan: vi.fn(),
|
|
205
|
+
} as unknown as RedisClientType;
|
|
206
|
+
|
|
207
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
208
|
+
|
|
209
|
+
await Effect.runPromise(
|
|
210
|
+
Effect.gen(function* () {
|
|
211
|
+
const result = yield* Effect.either(store.get("key1"));
|
|
212
|
+
expect(result._tag).toBe("Left");
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should handle set errors", async () => {
|
|
218
|
+
const mockRedis = {
|
|
219
|
+
get: vi.fn(),
|
|
220
|
+
set: vi.fn().mockRejectedValue(new Error("Write failed")),
|
|
221
|
+
del: vi.fn(),
|
|
222
|
+
scan: vi.fn(),
|
|
223
|
+
} as unknown as RedisClientType;
|
|
224
|
+
|
|
225
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
226
|
+
|
|
227
|
+
await Effect.runPromise(
|
|
228
|
+
Effect.gen(function* () {
|
|
229
|
+
const result = yield* Effect.either(store.set("key1", "value1"));
|
|
230
|
+
expect(result._tag).toBe("Left");
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should handle delete errors", async () => {
|
|
236
|
+
const mockRedis = {
|
|
237
|
+
get: vi.fn(),
|
|
238
|
+
set: vi.fn(),
|
|
239
|
+
del: vi.fn().mockRejectedValue(new Error("Delete failed")),
|
|
240
|
+
scan: vi.fn(),
|
|
241
|
+
} as unknown as RedisClientType;
|
|
242
|
+
|
|
243
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
244
|
+
|
|
245
|
+
await Effect.runPromise(
|
|
246
|
+
Effect.gen(function* () {
|
|
247
|
+
const result = yield* Effect.either(store.delete("key1"));
|
|
248
|
+
expect(result._tag).toBe("Left");
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should handle scan errors", async () => {
|
|
254
|
+
const mockRedis = {
|
|
255
|
+
get: vi.fn(),
|
|
256
|
+
set: vi.fn(),
|
|
257
|
+
del: vi.fn(),
|
|
258
|
+
scan: vi.fn().mockRejectedValue(new Error("Scan failed")),
|
|
259
|
+
} as unknown as RedisClientType;
|
|
260
|
+
|
|
261
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
262
|
+
|
|
263
|
+
await Effect.runPromise(
|
|
264
|
+
Effect.gen(function* () {
|
|
265
|
+
const result = yield* Effect.either(store.list!("prefix:"));
|
|
266
|
+
expect(result._tag).toBe("Left");
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("Update Operations", () => {
|
|
273
|
+
it("should update existing values", async () => {
|
|
274
|
+
const mockRedis = {
|
|
275
|
+
get: vi.fn().mockResolvedValue("value2"),
|
|
276
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
277
|
+
del: vi.fn(),
|
|
278
|
+
scan: vi.fn(),
|
|
279
|
+
} as unknown as RedisClientType;
|
|
280
|
+
|
|
281
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
282
|
+
|
|
283
|
+
await Effect.runPromise(
|
|
284
|
+
Effect.gen(function* () {
|
|
285
|
+
yield* store.set("key1", "value1");
|
|
286
|
+
yield* store.set("key1", "value2");
|
|
287
|
+
const value = yield* store.get("key1");
|
|
288
|
+
expect(value).toBe("value2");
|
|
289
|
+
expect(mockRedis.set).toHaveBeenCalledTimes(2);
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("Isolation", () => {
|
|
296
|
+
it("should maintain separate store instances", async () => {
|
|
297
|
+
const mockRedis1 = {
|
|
298
|
+
get: vi.fn().mockResolvedValue("store1-value"),
|
|
299
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
300
|
+
del: vi.fn(),
|
|
301
|
+
scan: vi.fn(),
|
|
302
|
+
} as unknown as RedisClientType;
|
|
303
|
+
|
|
304
|
+
const mockRedis2 = {
|
|
305
|
+
get: vi.fn().mockResolvedValue("store2-value"),
|
|
306
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
307
|
+
del: vi.fn(),
|
|
308
|
+
scan: vi.fn(),
|
|
309
|
+
} as unknown as RedisClientType;
|
|
310
|
+
|
|
311
|
+
const store1 = makeRedisBaseKvStore({ redis: mockRedis1 });
|
|
312
|
+
const store2 = makeRedisBaseKvStore({ redis: mockRedis2 });
|
|
313
|
+
|
|
314
|
+
await Effect.runPromise(
|
|
315
|
+
Effect.gen(function* () {
|
|
316
|
+
yield* store1.set("key1", "store1-value");
|
|
317
|
+
yield* store2.set("key1", "store2-value");
|
|
318
|
+
|
|
319
|
+
const value1 = yield* store1.get("key1");
|
|
320
|
+
const value2 = yield* store2.get("key1");
|
|
321
|
+
|
|
322
|
+
expect(value1).toBe("store1-value");
|
|
323
|
+
expect(value2).toBe("store2-value");
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("Performance", () => {
|
|
330
|
+
it("should handle large values", async () => {
|
|
331
|
+
const largeValue = "x".repeat(10000);
|
|
332
|
+
const mockRedis = {
|
|
333
|
+
get: vi.fn().mockResolvedValue(largeValue),
|
|
334
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
335
|
+
del: vi.fn(),
|
|
336
|
+
scan: vi.fn(),
|
|
337
|
+
} as unknown as RedisClientType;
|
|
338
|
+
|
|
339
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
340
|
+
|
|
341
|
+
await Effect.runPromise(
|
|
342
|
+
Effect.gen(function* () {
|
|
343
|
+
yield* store.set("large", largeValue);
|
|
344
|
+
const value = yield* store.get("large");
|
|
345
|
+
expect(value?.length).toBe(10000);
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should handle special characters in keys", async () => {
|
|
351
|
+
const mockRedis = {
|
|
352
|
+
get: vi.fn().mockResolvedValue("value"),
|
|
353
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
354
|
+
del: vi.fn(),
|
|
355
|
+
scan: vi.fn(),
|
|
356
|
+
} as unknown as RedisClientType;
|
|
357
|
+
|
|
358
|
+
const store = makeRedisBaseKvStore({ redis: mockRedis });
|
|
359
|
+
|
|
360
|
+
await Effect.runPromise(
|
|
361
|
+
Effect.gen(function* () {
|
|
362
|
+
const specialKey = "user:123:profile:@email";
|
|
363
|
+
yield* store.set(specialKey, "value");
|
|
364
|
+
expect(mockRedis.set).toHaveBeenCalledWith(specialKey, "value");
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared vitest configuration template for uploadista-sdk packages
|
|
5
|
+
*
|
|
6
|
+
* This template should be used by all SDK packages to ensure consistent
|
|
7
|
+
* testing configuration across the monorepo.
|
|
8
|
+
*
|
|
9
|
+
* Key features:
|
|
10
|
+
* - Tests in dedicated `tests/` directories (not colocated with src)
|
|
11
|
+
* - Node environment for server-side code
|
|
12
|
+
* - V8 coverage provider
|
|
13
|
+
* - Global test functions available
|
|
14
|
+
* - Effect testing support via @effect/vitest
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* Copy this file to your package root as `vitest.config.ts` and customize
|
|
18
|
+
* if needed (though most packages should use this as-is).
|
|
19
|
+
*/
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
test: {
|
|
22
|
+
globals: true,
|
|
23
|
+
environment: "node",
|
|
24
|
+
include: ["tests/**/*.test.ts"],
|
|
25
|
+
exclude: ["node_modules", "dist"],
|
|
26
|
+
coverage: {
|
|
27
|
+
provider: "v8",
|
|
28
|
+
reporter: ["text", "json", "html"],
|
|
29
|
+
exclude: [
|
|
30
|
+
"node_modules/",
|
|
31
|
+
"dist/",
|
|
32
|
+
"**/*.d.ts",
|
|
33
|
+
"**/*.test.ts",
|
|
34
|
+
"**/*.spec.ts",
|
|
35
|
+
"tests/",
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|