@jordanalec/dtk 1.0.6 → 1.1.0
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/README.md +52 -0
- package/dist/add.js +1 -0
- package/dist/cli.js +4 -1
- package/package.json +2 -1
- package/templates/init/GUIDE.md +51 -0
- package/templates/plugins/redis/env.txt +1 -0
- package/templates/plugins/redis/example.ts +37 -0
- package/templates/plugins/redis/plugin.json +36 -0
- package/templates/plugins/redis/service.test.ts +149 -0
- package/templates/plugins/redis/service.ts +66 -0
- package/templates/plugins/redis/types.ts +3 -0
package/README.md
CHANGED
|
@@ -286,6 +286,57 @@ AWS credentials are resolved from the environment via the SDK default provider c
|
|
|
286
286
|
|
|
287
287
|
---
|
|
288
288
|
|
|
289
|
+
### redis
|
|
290
|
+
|
|
291
|
+
Reads and writes data in a Redis cache.
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
dtk add redis
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Env vars appended to `.env.template`:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
REDIS_URL=redis://localhost:6379
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Usage:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
await suite()
|
|
307
|
+
.redis({ url: process.env.REDIS_URL! })
|
|
308
|
+
.step("write", async (ctx) => {
|
|
309
|
+
await ctx.services.redis.set("key", "value", 3600);
|
|
310
|
+
})
|
|
311
|
+
.step("read", async (ctx) => {
|
|
312
|
+
const value = await ctx.services.redis.get("key");
|
|
313
|
+
console.log("value:", value);
|
|
314
|
+
return value;
|
|
315
|
+
})
|
|
316
|
+
.step("disconnect", async (ctx) => {
|
|
317
|
+
await ctx.services.redis.quit();
|
|
318
|
+
})
|
|
319
|
+
.run("stopOnError");
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
> **The `disconnect` step is required.** The Redis client holds an open TCP connection. Without calling `quit()`, the Node.js process will hang indefinitely after the runbook finishes. Always use `stopOnError` (not `throwOnError`) so that the `disconnect` step is guaranteed to run even when an earlier step fails.
|
|
323
|
+
|
|
324
|
+
Available methods on `ctx.services.redis`:
|
|
325
|
+
|
|
326
|
+
| Method | Description |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `get(key)` | Returns the string value or `null` if the key does not exist |
|
|
329
|
+
| `set(key, value, ttlSeconds?)` | Sets a key, with an optional TTL in seconds |
|
|
330
|
+
| `del(key)` | Deletes a key, returns the number of keys removed |
|
|
331
|
+
| `exists(key)` | Returns `true` if the key exists |
|
|
332
|
+
| `expire(key, ttlSeconds)` | Sets a TTL on an existing key, returns `true` if applied |
|
|
333
|
+
| `hset(key, field, value)` | Sets a field on a hash, returns the number of new fields added |
|
|
334
|
+
| `hget(key, field)` | Returns a hash field value or `null` |
|
|
335
|
+
| `keys(pattern)` | Returns all keys matching a glob pattern (e.g. `user:*`) |
|
|
336
|
+
| `quit()` | Closes the connection -- must be called at the end of every runbook |
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
289
340
|
### open-ai
|
|
290
341
|
|
|
291
342
|
Lists models and sends responses via the OpenAI API.
|
|
@@ -728,4 +779,5 @@ templates/
|
|
|
728
779
|
aws-dynamo/
|
|
729
780
|
aws-s3/
|
|
730
781
|
open-ai/
|
|
782
|
+
redis/
|
|
731
783
|
```
|
package/dist/add.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import { createRequire } from 'module';
|
|
3
4
|
import { initCommand } from './init.js';
|
|
4
5
|
import { addCommand } from './add.js';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { version } = require('../package.json');
|
|
5
8
|
const program = new Command();
|
|
6
9
|
program
|
|
7
10
|
.name('dtk')
|
|
8
11
|
.description('Developer toolkit scaffolder')
|
|
9
|
-
.version(
|
|
12
|
+
.version(version);
|
|
10
13
|
program.addCommand(initCommand);
|
|
11
14
|
program.addCommand(addCommand);
|
|
12
15
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jordanalec/dtk",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI scaffolding tool for generating self-contained TypeScript runbook projects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"axios": "^1.6.0",
|
|
46
46
|
"dotenv": "^16.4.0",
|
|
47
47
|
"jest": "^30.3.0",
|
|
48
|
+
"redis": "^4.7.1",
|
|
48
49
|
"ts-jest": "^29.4.9",
|
|
49
50
|
"tsx": "^4.0.0",
|
|
50
51
|
"typescript": "^5.0.0"
|
package/templates/init/GUIDE.md
CHANGED
|
@@ -382,6 +382,57 @@ AWS credentials are picked up automatically from `AWS_ACCESS_KEY_ID` and `AWS_SE
|
|
|
382
382
|
|
|
383
383
|
---
|
|
384
384
|
|
|
385
|
+
### redis
|
|
386
|
+
|
|
387
|
+
Reads and writes data in a Redis cache.
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
dtk add redis
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Required env vars:
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
REDIS_URL=redis://localhost:6379
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Usage:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
await suite()
|
|
403
|
+
.redis({ url: process.env.REDIS_URL! })
|
|
404
|
+
.step("write", async (ctx) => {
|
|
405
|
+
await ctx.services.redis.set("key", "value", 3600);
|
|
406
|
+
})
|
|
407
|
+
.step("read", async (ctx) => {
|
|
408
|
+
const value = await ctx.services.redis.get("key");
|
|
409
|
+
console.log("value:", value);
|
|
410
|
+
return value;
|
|
411
|
+
})
|
|
412
|
+
.step("disconnect", async (ctx) => {
|
|
413
|
+
await ctx.services.redis.quit();
|
|
414
|
+
})
|
|
415
|
+
.run("stopOnError");
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
> **The `disconnect` step is required.** The Redis client holds an open TCP connection. Without calling `quit()`, the Node.js process will hang indefinitely after the runbook finishes. Always use `stopOnError` (not `throwOnError`) so that the `disconnect` step is guaranteed to run even when an earlier step fails.
|
|
419
|
+
|
|
420
|
+
Available methods on `ctx.services.redis`:
|
|
421
|
+
|
|
422
|
+
| Method | Description |
|
|
423
|
+
|---|---|
|
|
424
|
+
| `get(key)` | Returns the string value or `null` if the key does not exist |
|
|
425
|
+
| `set(key, value, ttlSeconds?)` | Sets a key, with an optional TTL in seconds |
|
|
426
|
+
| `del(key)` | Deletes a key, returns the number of keys removed |
|
|
427
|
+
| `exists(key)` | Returns `true` if the key exists |
|
|
428
|
+
| `expire(key, ttlSeconds)` | Sets a TTL on an existing key, returns `true` if applied |
|
|
429
|
+
| `hset(key, field, value)` | Sets a field on a hash, returns the number of new fields added |
|
|
430
|
+
| `hget(key, field)` | Returns a hash field value or `null` |
|
|
431
|
+
| `keys(pattern)` | Returns all keys matching a glob pattern (e.g. `user:*`) |
|
|
432
|
+
| `quit()` | Closes the connection -- must be called at the end of every runbook |
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
385
436
|
### open-ai
|
|
386
437
|
|
|
387
438
|
Lists models and sends responses via the OpenAI API.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
REDIS_URL=
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "../load-env.js";
|
|
2
|
+
import { suite } from "../suite.js";
|
|
3
|
+
|
|
4
|
+
await suite()
|
|
5
|
+
.redis({
|
|
6
|
+
url: process.env.REDIS_URL!,
|
|
7
|
+
})
|
|
8
|
+
.step("write-session", async (ctx) => {
|
|
9
|
+
await ctx.services.redis.set("session:abc123", JSON.stringify({ userId: 42, role: "admin" }), 3600);
|
|
10
|
+
console.log("session written with 1h TTL");
|
|
11
|
+
})
|
|
12
|
+
.step("read-session", async (ctx) => {
|
|
13
|
+
const raw = await ctx.services.redis.get("session:abc123");
|
|
14
|
+
const session = raw ? JSON.parse(raw) : null;
|
|
15
|
+
console.log("session:", session);
|
|
16
|
+
return session;
|
|
17
|
+
})
|
|
18
|
+
.step("check-existence", async (ctx) => {
|
|
19
|
+
const exists = await ctx.services.redis.exists("session:abc123");
|
|
20
|
+
console.log("session exists:", exists);
|
|
21
|
+
return exists;
|
|
22
|
+
})
|
|
23
|
+
.step("hash-metadata", async (ctx) => {
|
|
24
|
+
await ctx.services.redis.hset("user:42", "lastSeen", new Date().toISOString());
|
|
25
|
+
const lastSeen = await ctx.services.redis.hget("user:42", "lastSeen");
|
|
26
|
+
console.log("user:42 lastSeen:", lastSeen);
|
|
27
|
+
return lastSeen;
|
|
28
|
+
})
|
|
29
|
+
.step("list-session-keys", async (ctx) => {
|
|
30
|
+
const keys = await ctx.services.redis.keys("session:*");
|
|
31
|
+
console.log("active sessions:", keys);
|
|
32
|
+
return keys;
|
|
33
|
+
})
|
|
34
|
+
.step("disconnect", async (ctx) => {
|
|
35
|
+
await ctx.services.redis.quit();
|
|
36
|
+
})
|
|
37
|
+
.run("stopOnError");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redis",
|
|
3
|
+
"description": "Redis -- get, set, del, exists, expire, hset, hget, keys",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"redis": "^4.7.0"
|
|
6
|
+
},
|
|
7
|
+
"files": [
|
|
8
|
+
{ "src": "service.ts", "dest": "src/services/redis.ts" },
|
|
9
|
+
{ "src": "types.ts", "dest": "src/types/redis.ts" },
|
|
10
|
+
{ "src": "service.test.ts", "dest": "src/services/redis.test.ts" }
|
|
11
|
+
],
|
|
12
|
+
"env": "env.txt",
|
|
13
|
+
"example": "example.ts",
|
|
14
|
+
"transforms": {
|
|
15
|
+
"service.ts": [
|
|
16
|
+
{ "from": "./types.js", "to": "../types/redis.js" }
|
|
17
|
+
],
|
|
18
|
+
"service.test.ts": [
|
|
19
|
+
{ "from": "./service.js", "to": "./redis.js" }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"patches": {
|
|
23
|
+
"src/suite.ts": {
|
|
24
|
+
"imports": [
|
|
25
|
+
"import { createRedisService } from \"./services/redis.js\";",
|
|
26
|
+
"import type { RedisConfig } from \"./types/redis.js\";"
|
|
27
|
+
],
|
|
28
|
+
"configs": " private redisConfig?: RedisConfig;",
|
|
29
|
+
"methods": " redis(config: RedisConfig): this { this.redisConfig = config; return this; }",
|
|
30
|
+
"services": " redis: createRedisService(this.redisConfig),"
|
|
31
|
+
},
|
|
32
|
+
"src/types/suite.ts": {
|
|
33
|
+
"service-types": " redis: { get(key: string): Promise<string | null>; set(key: string, value: string, ttlSeconds?: number): Promise<void>; del(key: string): Promise<number>; exists(key: string): Promise<boolean>; expire(key: string, ttlSeconds: number): Promise<boolean>; hset(key: string, field: string, value: string): Promise<number>; hget(key: string, field: string): Promise<string | null>; keys(pattern: string): Promise<string[]>; quit(): Promise<void>; };"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createRedisService } from './service.js';
|
|
2
|
+
|
|
3
|
+
jest.mock('redis');
|
|
4
|
+
import { createClient } from 'redis';
|
|
5
|
+
|
|
6
|
+
const mockClient = {
|
|
7
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
get: jest.fn(),
|
|
9
|
+
set: jest.fn().mockResolvedValue('OK'),
|
|
10
|
+
del: jest.fn(),
|
|
11
|
+
exists: jest.fn(),
|
|
12
|
+
expire: jest.fn(),
|
|
13
|
+
hSet: jest.fn(),
|
|
14
|
+
hGet: jest.fn(),
|
|
15
|
+
keys: jest.fn(),
|
|
16
|
+
quit: jest.fn().mockResolvedValue(undefined),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
(createClient as jest.Mock).mockReturnValue(mockClient);
|
|
22
|
+
mockClient.connect.mockResolvedValue(undefined);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('createRedisService', () => {
|
|
26
|
+
const config = { url: 'redis://localhost:6379' };
|
|
27
|
+
|
|
28
|
+
it('connects to redis using the provided url', async () => {
|
|
29
|
+
mockClient.get.mockResolvedValue('value');
|
|
30
|
+
const redis = createRedisService(config);
|
|
31
|
+
await redis.get('key');
|
|
32
|
+
expect(createClient).toHaveBeenCalledWith({ url: config.url });
|
|
33
|
+
expect(mockClient.connect).toHaveBeenCalledTimes(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('reuses the same client across multiple calls', async () => {
|
|
37
|
+
mockClient.get.mockResolvedValue('value');
|
|
38
|
+
const redis = createRedisService(config);
|
|
39
|
+
await redis.get('key1');
|
|
40
|
+
await redis.get('key2');
|
|
41
|
+
expect(mockClient.connect).toHaveBeenCalledTimes(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('get returns the value for an existing key', async () => {
|
|
45
|
+
mockClient.get.mockResolvedValue('hello');
|
|
46
|
+
const redis = createRedisService(config);
|
|
47
|
+
const result = await redis.get('greeting');
|
|
48
|
+
expect(result).toBe('hello');
|
|
49
|
+
expect(mockClient.get).toHaveBeenCalledWith('greeting');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('get returns null for a missing key', async () => {
|
|
53
|
+
mockClient.get.mockResolvedValue(null);
|
|
54
|
+
const redis = createRedisService(config);
|
|
55
|
+
const result = await redis.get('missing');
|
|
56
|
+
expect(result).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('set calls client.set without EX when no ttl is given', async () => {
|
|
60
|
+
const redis = createRedisService(config);
|
|
61
|
+
await redis.set('key', 'value');
|
|
62
|
+
expect(mockClient.set).toHaveBeenCalledWith('key', 'value');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('set calls client.set with EX option when ttl is given', async () => {
|
|
66
|
+
const redis = createRedisService(config);
|
|
67
|
+
await redis.set('key', 'value', 60);
|
|
68
|
+
expect(mockClient.set).toHaveBeenCalledWith('key', 'value', { EX: 60 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('del returns the number of deleted keys', async () => {
|
|
72
|
+
mockClient.del.mockResolvedValue(1);
|
|
73
|
+
const redis = createRedisService(config);
|
|
74
|
+
const result = await redis.del('key');
|
|
75
|
+
expect(result).toBe(1);
|
|
76
|
+
expect(mockClient.del).toHaveBeenCalledWith('key');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('exists returns true when key is present', async () => {
|
|
80
|
+
mockClient.exists.mockResolvedValue(1);
|
|
81
|
+
const redis = createRedisService(config);
|
|
82
|
+
const result = await redis.exists('key');
|
|
83
|
+
expect(result).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('exists returns false when key is absent', async () => {
|
|
87
|
+
mockClient.exists.mockResolvedValue(0);
|
|
88
|
+
const redis = createRedisService(config);
|
|
89
|
+
const result = await redis.exists('missing');
|
|
90
|
+
expect(result).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('expire returns true when the timeout was set', async () => {
|
|
94
|
+
mockClient.expire.mockResolvedValue(true);
|
|
95
|
+
const redis = createRedisService(config);
|
|
96
|
+
const result = await redis.expire('key', 300);
|
|
97
|
+
expect(result).toBe(true);
|
|
98
|
+
expect(mockClient.expire).toHaveBeenCalledWith('key', 300);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('hset returns the number of new fields added', async () => {
|
|
102
|
+
mockClient.hSet.mockResolvedValue(1);
|
|
103
|
+
const redis = createRedisService(config);
|
|
104
|
+
const result = await redis.hset('hash', 'field', 'value');
|
|
105
|
+
expect(result).toBe(1);
|
|
106
|
+
expect(mockClient.hSet).toHaveBeenCalledWith('hash', 'field', 'value');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('hget returns the field value', async () => {
|
|
110
|
+
mockClient.hGet.mockResolvedValue('stored');
|
|
111
|
+
const redis = createRedisService(config);
|
|
112
|
+
const result = await redis.hget('hash', 'field');
|
|
113
|
+
expect(result).toBe('stored');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('hget returns null when field is undefined', async () => {
|
|
117
|
+
mockClient.hGet.mockResolvedValue(undefined);
|
|
118
|
+
const redis = createRedisService(config);
|
|
119
|
+
const result = await redis.hget('hash', 'missing');
|
|
120
|
+
expect(result).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('keys returns matching key names', async () => {
|
|
124
|
+
mockClient.keys.mockResolvedValue(['user:1', 'user:2']);
|
|
125
|
+
const redis = createRedisService(config);
|
|
126
|
+
const result = await redis.keys('user:*');
|
|
127
|
+
expect(result).toEqual(['user:1', 'user:2']);
|
|
128
|
+
expect(mockClient.keys).toHaveBeenCalledWith('user:*');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('quit disconnects the client and clears it', async () => {
|
|
132
|
+
mockClient.get.mockResolvedValue('value');
|
|
133
|
+
mockClient.quit = jest.fn().mockResolvedValue(undefined);
|
|
134
|
+
const redis = createRedisService(config);
|
|
135
|
+
await redis.get('key');
|
|
136
|
+
await redis.quit();
|
|
137
|
+
expect(mockClient.quit).toHaveBeenCalledTimes(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('quit is a no-op when client was never connected', async () => {
|
|
141
|
+
const redis = createRedisService(config);
|
|
142
|
+
await expect(redis.quit()).resolves.toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('throws when any method is called without config', async () => {
|
|
146
|
+
const redis = createRedisService();
|
|
147
|
+
await expect(redis.get('key')).rejects.toThrow('redis service is not configured');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createClient } from "redis";
|
|
2
|
+
import type { RedisConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function createRedisService(config?: RedisConfig) {
|
|
5
|
+
const ensureConfig = () => {
|
|
6
|
+
if (!config) throw new Error("redis service is not configured -- call .redis(config) on the suite");
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let client: ReturnType<typeof createClient> | null = null;
|
|
10
|
+
|
|
11
|
+
const getClient = async () => {
|
|
12
|
+
ensureConfig();
|
|
13
|
+
if (!client) {
|
|
14
|
+
client = createClient({ url: config!.url });
|
|
15
|
+
await client.connect();
|
|
16
|
+
}
|
|
17
|
+
return client;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
get: async (key: string): Promise<string | null> => {
|
|
22
|
+
const c = await getClient();
|
|
23
|
+
return c.get(key);
|
|
24
|
+
},
|
|
25
|
+
set: async (key: string, value: string, ttlSeconds?: number): Promise<void> => {
|
|
26
|
+
const c = await getClient();
|
|
27
|
+
if (ttlSeconds !== undefined) {
|
|
28
|
+
await c.set(key, value, { EX: ttlSeconds });
|
|
29
|
+
} else {
|
|
30
|
+
await c.set(key, value);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
del: async (key: string): Promise<number> => {
|
|
34
|
+
const c = await getClient();
|
|
35
|
+
return c.del(key);
|
|
36
|
+
},
|
|
37
|
+
exists: async (key: string): Promise<boolean> => {
|
|
38
|
+
const c = await getClient();
|
|
39
|
+
const count = await c.exists(key);
|
|
40
|
+
return count > 0;
|
|
41
|
+
},
|
|
42
|
+
expire: async (key: string, ttlSeconds: number): Promise<boolean> => {
|
|
43
|
+
const c = await getClient();
|
|
44
|
+
return c.expire(key, ttlSeconds);
|
|
45
|
+
},
|
|
46
|
+
hset: async (key: string, field: string, value: string): Promise<number> => {
|
|
47
|
+
const c = await getClient();
|
|
48
|
+
return c.hSet(key, field, value);
|
|
49
|
+
},
|
|
50
|
+
hget: async (key: string, field: string): Promise<string | null> => {
|
|
51
|
+
const c = await getClient();
|
|
52
|
+
const result = await c.hGet(key, field);
|
|
53
|
+
return result ?? null;
|
|
54
|
+
},
|
|
55
|
+
keys: async (pattern: string): Promise<string[]> => {
|
|
56
|
+
const c = await getClient();
|
|
57
|
+
return c.keys(pattern);
|
|
58
|
+
},
|
|
59
|
+
quit: async (): Promise<void> => {
|
|
60
|
+
if (client) {
|
|
61
|
+
await client.quit();
|
|
62
|
+
client = null;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|