@lobehub/lobehub 2.0.0-next.127 → 2.0.0-next.128

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.
Files changed (46) hide show
  1. package/.env.example +23 -3
  2. package/.env.example.development +5 -0
  3. package/CHANGELOG.md +25 -0
  4. package/changelog/v1.json +9 -0
  5. package/docker-compose/local/docker-compose.yml +24 -1
  6. package/docker-compose/local/logto/docker-compose.yml +25 -2
  7. package/docker-compose.development.yml +6 -0
  8. package/locales/ar/auth.json +114 -1
  9. package/locales/bg-BG/auth.json +114 -1
  10. package/locales/de-DE/auth.json +114 -1
  11. package/locales/en-US/auth.json +42 -22
  12. package/locales/es-ES/auth.json +114 -1
  13. package/locales/fa-IR/auth.json +114 -1
  14. package/locales/fr-FR/auth.json +114 -1
  15. package/locales/it-IT/auth.json +114 -1
  16. package/locales/ja-JP/auth.json +114 -1
  17. package/locales/ko-KR/auth.json +114 -1
  18. package/locales/nl-NL/auth.json +114 -1
  19. package/locales/pl-PL/auth.json +114 -1
  20. package/locales/pt-BR/auth.json +114 -1
  21. package/locales/ru-RU/auth.json +114 -1
  22. package/locales/tr-TR/auth.json +114 -1
  23. package/locales/vi-VN/auth.json +114 -1
  24. package/locales/zh-CN/auth.json +36 -29
  25. package/locales/zh-TW/auth.json +114 -1
  26. package/package.json +4 -1
  27. package/packages/database/src/client/db.ts +21 -21
  28. package/packages/database/src/repositories/dataImporter/deprecated/index.ts +5 -5
  29. package/packages/database/src/repositories/dataImporter/index.ts +59 -59
  30. package/packages/database/src/schemas/generation.ts +16 -16
  31. package/packages/database/src/schemas/oidc.ts +36 -36
  32. package/packages/model-runtime/src/providers/newapi/index.ts +61 -18
  33. package/packages/model-runtime/src/runtimeMap.ts +1 -0
  34. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/UpdateProviderInfo/SettingModal.tsx +10 -6
  35. package/src/envs/redis.ts +106 -0
  36. package/src/libs/redis/index.ts +5 -0
  37. package/src/libs/redis/manager.test.ts +107 -0
  38. package/src/libs/redis/manager.ts +56 -0
  39. package/src/libs/redis/redis.test.ts +158 -0
  40. package/src/libs/redis/redis.ts +117 -0
  41. package/src/libs/redis/types.ts +71 -0
  42. package/src/libs/redis/upstash.test.ts +154 -0
  43. package/src/libs/redis/upstash.ts +109 -0
  44. package/src/libs/redis/utils.test.ts +46 -0
  45. package/src/libs/redis/utils.ts +53 -0
  46. package/.github/workflows/check-console-log.yml +0 -117
@@ -0,0 +1,158 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { IoRedisConfig } from './types';
4
+
5
+ const buildRedisConfig = (): IoRedisConfig | null => {
6
+ const url = process.env.REDIS_URL;
7
+
8
+ if (!url) return null;
9
+
10
+ const database = Number.parseInt(process.env.REDIS_DATABASE ?? '', 10);
11
+
12
+ return {
13
+ database: Number.isNaN(database) ? undefined : database,
14
+ enabled: true,
15
+ password: process.env.REDIS_PASSWORD,
16
+ prefix: process.env.REDIS_PREFIX ?? 'lobe-chat-test',
17
+ provider: 'redis',
18
+ tls: process.env.REDIS_TLS === 'true',
19
+ url,
20
+ username: process.env.REDIS_USERNAME,
21
+ };
22
+ };
23
+
24
+ const loadRedisProvider = async () => (await import('./redis')).IoRedisRedisProvider;
25
+
26
+ const createMockedProvider = async () => {
27
+ const mocks = {
28
+ connect: vi.fn().mockResolvedValue(undefined),
29
+ ping: vi.fn().mockResolvedValue('PONG'),
30
+ quit: vi.fn().mockResolvedValue(undefined),
31
+ get: vi.fn().mockResolvedValue('mock-value'),
32
+ set: vi.fn().mockResolvedValue('OK'),
33
+ setex: vi.fn().mockResolvedValue('OK'),
34
+ del: vi.fn().mockResolvedValue(1),
35
+ exists: vi.fn().mockResolvedValue(1),
36
+ expire: vi.fn().mockResolvedValue(1),
37
+ ttl: vi.fn().mockResolvedValue(50),
38
+ incr: vi.fn().mockResolvedValue(2),
39
+ decr: vi.fn().mockResolvedValue(0),
40
+ mget: vi.fn().mockResolvedValue(['a', 'b']),
41
+ mset: vi.fn().mockResolvedValue('OK'),
42
+ hget: vi.fn().mockResolvedValue('field'),
43
+ hset: vi.fn().mockResolvedValue(1),
44
+ hdel: vi.fn().mockResolvedValue(1),
45
+ hgetall: vi.fn().mockResolvedValue({ a: '1' }),
46
+ };
47
+
48
+ vi.resetModules();
49
+ vi.doMock('ioredis', () => {
50
+ class FakeRedis {
51
+ constructor(
52
+ public url: string,
53
+ public options: any,
54
+ ) {}
55
+ connect = mocks.connect;
56
+ ping = mocks.ping;
57
+ quit = mocks.quit;
58
+ get = mocks.get;
59
+ set = mocks.set;
60
+ setex = mocks.setex;
61
+ del = mocks.del;
62
+ exists = mocks.exists;
63
+ expire = mocks.expire;
64
+ ttl = mocks.ttl;
65
+ incr = mocks.incr;
66
+ decr = mocks.decr;
67
+ mget = mocks.mget;
68
+ mset = mocks.mset;
69
+ hget = mocks.hget;
70
+ hset = mocks.hset;
71
+ hdel = mocks.hdel;
72
+ hgetall = mocks.hgetall;
73
+ }
74
+
75
+ return { default: FakeRedis };
76
+ });
77
+
78
+ const IoRedisRedisProvider = await loadRedisProvider();
79
+ const provider = new IoRedisRedisProvider({
80
+ enabled: true,
81
+ prefix: 'mock',
82
+ provider: 'redis',
83
+ tls: false,
84
+ url: 'redis://localhost:6379',
85
+ });
86
+
87
+ await provider.initialize();
88
+
89
+ return { mocks, provider };
90
+ };
91
+
92
+ const shouldSkipIntegration = (error: unknown) =>
93
+ error instanceof Error &&
94
+ ['ENOTFOUND', 'ECONNREFUSED', 'EAI_AGAIN', 'Connection is closed'].some((msg) =>
95
+ error.message.includes(msg),
96
+ );
97
+
98
+ afterEach(() => {
99
+ vi.clearAllMocks();
100
+ vi.resetModules();
101
+ vi.unmock('ioredis');
102
+ });
103
+
104
+ describe('integrated', (test) => {
105
+ const config = buildRedisConfig();
106
+ if (!config) {
107
+ test.skip('REDIS_URL not provided; skip integrated ioredis tests');
108
+ return;
109
+ }
110
+
111
+ it('set -> get -> del roundtrip', async () => {
112
+ vi.unmock('ioredis');
113
+ vi.resetModules();
114
+
115
+ const IoRedisRedisProvider = await loadRedisProvider();
116
+ const provider = new IoRedisRedisProvider(config);
117
+ try {
118
+ await provider.initialize();
119
+
120
+ const key = `redis:test:${Date.now()}`;
121
+ await provider.set(key, 'value', { ex: 60 });
122
+ expect(await provider.get(key)).toBe('value');
123
+ expect(await provider.del(key)).toBe(1);
124
+ } catch (error) {
125
+ if (shouldSkipIntegration(error)) {
126
+ // Remote Redis unavailable in current environment; treat as skipped.
127
+ return;
128
+ }
129
+
130
+ throw error;
131
+ } finally {
132
+ await provider.disconnect();
133
+ }
134
+ });
135
+ });
136
+
137
+ describe('mocked', () => {
138
+ it('normalizes set options into ioredis arguments', async () => {
139
+ const { mocks, provider } = await createMockedProvider();
140
+ await provider.set('key', 'value', { ex: 10, nx: true, get: true });
141
+
142
+ expect(mocks.set).toHaveBeenCalledWith('key', 'value', 'EX', 10, 'NX', 'GET');
143
+ await provider.disconnect();
144
+ });
145
+
146
+ it('supports buffer keys for hashes and strings', async () => {
147
+ const { mocks, provider } = await createMockedProvider();
148
+
149
+ const bufKey = Buffer.from('buffer-key');
150
+ await provider.hset(bufKey, 'field', 'value');
151
+ await provider.get(bufKey);
152
+
153
+ expect(mocks.hset).toHaveBeenCalledWith(bufKey, 'field', 'value');
154
+ expect(mocks.get).toHaveBeenCalledWith(bufKey);
155
+
156
+ await provider.disconnect();
157
+ });
158
+ });
@@ -0,0 +1,117 @@
1
+ import debug from 'debug';
2
+ import type { Redis } from 'ioredis';
3
+
4
+ import {
5
+ BaseRedisProvider,
6
+ IoRedisConfig,
7
+ RedisKey,
8
+ RedisMSetArgument,
9
+ RedisProviderName,
10
+ RedisSetResult,
11
+ RedisValue,
12
+ SetOptions,
13
+ } from './types';
14
+ import { buildIORedisSetArgs, normalizeMsetValues } from './utils';
15
+
16
+ const log = debug('lobe:redis');
17
+
18
+ export class IoRedisRedisProvider implements BaseRedisProvider {
19
+ provider: RedisProviderName = 'redis';
20
+ private client: Redis | null = null;
21
+
22
+ constructor(private config: IoRedisConfig) {}
23
+
24
+ async initialize() {
25
+ const IORedis = await import('ioredis');
26
+
27
+ this.client = new IORedis.default(this.config.url, {
28
+ db: this.config.database,
29
+ keyPrefix: this.config.prefix ? `${this.config.prefix}:` : undefined,
30
+ lazyConnect: true,
31
+ maxRetriesPerRequest: 2,
32
+ password: this.config.password,
33
+ tls: this.config.tls ? {} : undefined,
34
+ username: this.config.username,
35
+ });
36
+
37
+ await this.client.connect();
38
+ await this.client.ping();
39
+
40
+ log('Connected to Redis provider with prefix "%s"', this.config.prefix);
41
+ }
42
+
43
+ async disconnect() {
44
+ await this.client?.quit();
45
+ }
46
+
47
+ private ensureClient(): Redis {
48
+ if (!this.client) {
49
+ throw new Error('Redis client is not initialized');
50
+ }
51
+
52
+ return this.client;
53
+ }
54
+
55
+ async get(key: RedisKey): Promise<string | null> {
56
+ return this.ensureClient().get(key);
57
+ }
58
+
59
+ async set(key: RedisKey, value: RedisValue, options?: SetOptions): Promise<RedisSetResult> {
60
+ const args = buildIORedisSetArgs(options);
61
+
62
+ // ioredis has many overloads for SET; use a cast to keep async-only usage ergonomic
63
+ return (this.ensureClient().set as any)(key, value, ...args);
64
+ }
65
+
66
+ async setex(key: RedisKey, seconds: number, value: RedisValue): Promise<'OK'> {
67
+ return this.ensureClient().setex(key, seconds, value);
68
+ }
69
+
70
+ async del(...keys: RedisKey[]): Promise<number> {
71
+ return this.ensureClient().del(...keys);
72
+ }
73
+
74
+ async exists(...keys: RedisKey[]): Promise<number> {
75
+ return this.ensureClient().exists(...keys);
76
+ }
77
+
78
+ async expire(key: RedisKey, seconds: number): Promise<number> {
79
+ return this.ensureClient().expire(key, seconds);
80
+ }
81
+
82
+ async ttl(key: RedisKey): Promise<number> {
83
+ return this.ensureClient().ttl(key);
84
+ }
85
+
86
+ async incr(key: RedisKey): Promise<number> {
87
+ return this.ensureClient().incr(key);
88
+ }
89
+
90
+ async decr(key: RedisKey): Promise<number> {
91
+ return this.ensureClient().decr(key);
92
+ }
93
+
94
+ async mget(...keys: RedisKey[]): Promise<(string | null)[]> {
95
+ return this.ensureClient().mget(...keys);
96
+ }
97
+
98
+ async mset(values: RedisMSetArgument): Promise<'OK'> {
99
+ return this.ensureClient().mset(normalizeMsetValues(values));
100
+ }
101
+
102
+ async hget(key: RedisKey, field: RedisKey): Promise<string | null> {
103
+ return this.ensureClient().hget(key, field);
104
+ }
105
+
106
+ async hset(key: RedisKey, field: RedisKey, value: RedisValue): Promise<number> {
107
+ return this.ensureClient().hset(key, field, value);
108
+ }
109
+
110
+ async hdel(key: RedisKey, ...fields: RedisKey[]): Promise<number> {
111
+ return this.ensureClient().hdel(key, ...fields);
112
+ }
113
+
114
+ async hgetall(key: RedisKey): Promise<Record<string, string>> {
115
+ return this.ensureClient().hgetall(key);
116
+ }
117
+ }
@@ -0,0 +1,71 @@
1
+ export type RedisKey = string | Buffer;
2
+ export type RedisValue = string | Buffer | number;
3
+ export type RedisProvider = false | 'redis' | 'upstash';
4
+ export type RedisProviderName = Exclude<RedisProvider, false>;
5
+
6
+ export type IoRedisConfig = {
7
+ database?: number;
8
+ enabled: boolean;
9
+ password?: string;
10
+ prefix: string;
11
+ provider: 'redis';
12
+ tls: boolean;
13
+ url: string;
14
+ username?: string;
15
+ };
16
+
17
+ export type UpstashConfig = {
18
+ enabled: boolean;
19
+ prefix: string;
20
+ provider: 'upstash';
21
+ token: string;
22
+ url: string;
23
+ };
24
+
25
+ export type DisabledRedisConfig = {
26
+ enabled: false;
27
+ prefix: string;
28
+ provider: false;
29
+ };
30
+
31
+ export type RedisConfig = IoRedisConfig | UpstashConfig | DisabledRedisConfig;
32
+
33
+ export interface SetOptions {
34
+ ex?: number;
35
+ exat?: number;
36
+ get?: boolean;
37
+ keepTtl?: boolean;
38
+ nx?: boolean;
39
+ px?: number;
40
+ pxat?: number;
41
+ xx?: boolean;
42
+ }
43
+
44
+ // NOTICE: number comes from upstash
45
+ export type RedisSetResult = 'OK' | null | string | number;
46
+ export type RedisMSetArgument = Record<string, RedisValue> | Map<RedisKey, RedisValue>;
47
+
48
+ export interface RedisClient {
49
+ decr(key: RedisKey): Promise<number>;
50
+ del(...keys: RedisKey[]): Promise<number>;
51
+ exists(...keys: RedisKey[]): Promise<number>;
52
+ expire(key: RedisKey, seconds: number): Promise<number>;
53
+ get(key: RedisKey): Promise<string | null>;
54
+ hdel(key: RedisKey, ...fields: RedisKey[]): Promise<number>;
55
+ hget(key: RedisKey, field: RedisKey): Promise<string | null>;
56
+ hgetall(key: RedisKey): Promise<Record<string, string>>;
57
+ hset(key: RedisKey, field: RedisKey, value: RedisValue): Promise<number>;
58
+ incr(key: RedisKey): Promise<number>;
59
+ mget(...keys: RedisKey[]): Promise<(string | null)[]>;
60
+ mset(values: RedisMSetArgument): Promise<'OK'>;
61
+ set(key: RedisKey, value: RedisValue, options?: SetOptions): Promise<RedisSetResult>;
62
+ setex(key: RedisKey, seconds: number, value: RedisValue): Promise<'OK'>;
63
+ ttl(key: RedisKey): Promise<number>;
64
+ }
65
+
66
+ export interface BaseRedisProvider extends RedisClient {
67
+ disconnect(): Promise<void>;
68
+
69
+ initialize(): Promise<void>;
70
+ provider: RedisProviderName;
71
+ }
@@ -0,0 +1,154 @@
1
+ // @vitest-environment node
2
+ // NOTICE: here due to the reason we are using [`happy-dom`](https://github.com/lobehub/lobe-chat/blob/13753145557a9dede98b1f5489f93ac570ef2956/vitest.config.mts#L45)
3
+ // for Vitest environment, and in fact that this is a known bug for happy-dom not including
4
+ // Authorization header in fetch requests.
5
+ //
6
+ // Read more here: https://github.com/capricorn86/happy-dom/issues/1042#issuecomment-3585851354
7
+ import { Buffer } from 'node:buffer';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+
10
+ import { UpstashConfig } from './types';
11
+
12
+ const buildUpstashConfig = (): UpstashConfig | null => {
13
+ const url = process.env.UPSTASH_REDIS_REST_URL;
14
+ const token = process.env.UPSTASH_REDIS_REST_TOKEN;
15
+
16
+ if (!url || !token) return null;
17
+
18
+ return {
19
+ enabled: true,
20
+ prefix: process.env.REDIS_PREFIX ?? 'lobe-chat-test',
21
+ provider: 'upstash',
22
+ token,
23
+ url,
24
+ };
25
+ };
26
+
27
+ const loadUpstashProvider = async () => (await import('./upstash')).UpstashRedisProvider;
28
+
29
+ const createMockedProvider = async () => {
30
+ const mocks = {
31
+ mockSet: vi.fn().mockResolvedValue('OK'),
32
+ mockGet: vi.fn().mockResolvedValue('mock-value'),
33
+ mockDel: vi.fn().mockResolvedValue(1),
34
+ mockSetex: vi.fn().mockResolvedValue('OK'),
35
+ mockMset: vi.fn().mockResolvedValue('OK'),
36
+ mockHset: vi.fn().mockResolvedValue(1),
37
+ mockHdel: vi.fn().mockResolvedValue(1),
38
+ mockHgetall: vi.fn().mockResolvedValue({ a: '1' }),
39
+ mockPing: vi.fn().mockResolvedValue('PONG'),
40
+ mockExists: vi.fn().mockResolvedValue(1),
41
+ mockExpire: vi.fn().mockResolvedValue(1),
42
+ mockTtl: vi.fn().mockResolvedValue(50),
43
+ mockIncr: vi.fn().mockResolvedValue(2),
44
+ mockDecr: vi.fn().mockResolvedValue(0),
45
+ mockMget: vi.fn().mockResolvedValue(['a', 'b']),
46
+ mockHget: vi.fn().mockResolvedValue('field'),
47
+ };
48
+
49
+ vi.resetModules();
50
+ vi.doMock('@upstash/redis', () => {
51
+ class FakeRedis {
52
+ constructor(public config: any) {}
53
+ ping = mocks.mockPing;
54
+ set = mocks.mockSet;
55
+ get = mocks.mockGet;
56
+ del = mocks.mockDel;
57
+ setex = mocks.mockSetex;
58
+ exists = mocks.mockExists;
59
+ expire = mocks.mockExpire;
60
+ ttl = mocks.mockTtl;
61
+ incr = mocks.mockIncr;
62
+ decr = mocks.mockDecr;
63
+ mget = mocks.mockMget;
64
+ mset = mocks.mockMset;
65
+ hget = mocks.mockHget;
66
+ hset = mocks.mockHset;
67
+ hdel = mocks.mockHdel;
68
+ hgetall = mocks.mockHgetall;
69
+ }
70
+
71
+ return { Redis: FakeRedis };
72
+ });
73
+
74
+ const UpstashRedisProvider = await loadUpstashProvider();
75
+ const provider = new UpstashRedisProvider({
76
+ enabled: true,
77
+ prefix: 'mock',
78
+ provider: 'upstash',
79
+ token: 'token',
80
+ url: 'https://example.upstash.io',
81
+ });
82
+
83
+ await provider.initialize();
84
+
85
+ return { mocks, provider };
86
+ };
87
+
88
+ const shouldSkipIntegration = (error: unknown) =>
89
+ error instanceof Error &&
90
+ ['ENOTFOUND', 'ECONNREFUSED', 'EAI_AGAIN', 'Connection is closed'].some((msg) =>
91
+ error.message.includes(msg),
92
+ );
93
+
94
+ afterEach(() => {
95
+ vi.clearAllMocks();
96
+ vi.resetModules();
97
+ vi.unmock('@upstash/redis');
98
+ });
99
+
100
+ describe('integrated', (test) => {
101
+ const config = buildUpstashConfig();
102
+ if (!config) {
103
+ test.skip('UPSTASH_REDIS_REST_URL/TOKEN not provided; skip integrated upstash tests');
104
+ return;
105
+ }
106
+
107
+ it('set -> get -> del roundtrip', async () => {
108
+ vi.unmock('@upstash/redis');
109
+ vi.resetModules();
110
+
111
+ const UpstashRedisProvider = await loadUpstashProvider();
112
+ const provider = new UpstashRedisProvider(config);
113
+ try {
114
+ await provider.initialize();
115
+
116
+ const key = `upstash:test:${Date.now()}`;
117
+ await provider.set(key, 'value', { ex: 60 });
118
+ expect(await provider.get(key)).toBe('value');
119
+ expect(await provider.del(key)).toBe(1);
120
+ } catch (error) {
121
+ if (shouldSkipIntegration(error)) {
122
+ // Remote Upstash Redis unavailable in current environment; treat as skipped.
123
+ return;
124
+ }
125
+
126
+ throw error;
127
+ } finally {
128
+ await provider.disconnect();
129
+ }
130
+ });
131
+ });
132
+
133
+ describe('mocked', () => {
134
+ it('normalizes buffer keys to strings', async () => {
135
+ const { mocks, provider } = await createMockedProvider();
136
+
137
+ const bufKey = Buffer.from('buffer-key');
138
+ await provider.set(bufKey, 'value');
139
+ await provider.hset(bufKey, 'field', 'value');
140
+ await provider.del(bufKey);
141
+
142
+ expect(mocks.mockSet).toHaveBeenCalledWith('buffer-key', 'value', undefined);
143
+ expect(mocks.mockHset).toHaveBeenCalledWith('buffer-key', { field: 'value' });
144
+ expect(mocks.mockDel).toHaveBeenCalledWith('buffer-key');
145
+ });
146
+
147
+ it('passes set options through to upstash client', async () => {
148
+ const { mocks, provider } = await createMockedProvider();
149
+
150
+ await provider.set('key', 'value', { ex: 10, nx: true, get: true });
151
+
152
+ expect(mocks.mockSet).toHaveBeenCalledWith('key', 'value', { ex: 10, nx: true, get: true });
153
+ });
154
+ });
@@ -0,0 +1,109 @@
1
+ import { Redis, RedisConfigNodejs } from '@upstash/redis';
2
+ import { Buffer } from 'node:buffer';
3
+
4
+ import {
5
+ BaseRedisProvider,
6
+ RedisKey,
7
+ RedisMSetArgument,
8
+ RedisSetResult,
9
+ RedisValue,
10
+ SetOptions,
11
+ UpstashConfig,
12
+ } from './types';
13
+ import {
14
+ buildUpstashSetOptions,
15
+ normalizeMsetValues,
16
+ normalizeRedisKey,
17
+ normalizeRedisKeys,
18
+ } from './utils';
19
+
20
+ export class UpstashRedisProvider implements BaseRedisProvider {
21
+ provider: 'upstash' = 'upstash';
22
+ private client: Redis;
23
+
24
+ constructor(options: UpstashConfig | RedisConfigNodejs) {
25
+ this.client = new Redis(options as RedisConfigNodejs);
26
+ }
27
+
28
+ async initialize(): Promise<void> {
29
+ await this.client.ping();
30
+ }
31
+
32
+ async disconnect() {
33
+ // upstash client is stateless http, nothing to disconnect
34
+ }
35
+
36
+ async get(key: RedisKey): Promise<string | null> {
37
+ return this.client.get(normalizeRedisKey(key));
38
+ }
39
+
40
+ async set(key: RedisKey, value: RedisValue, options?: SetOptions): Promise<RedisSetResult> {
41
+ const res = await this.client.set(
42
+ normalizeRedisKey(key),
43
+ value,
44
+ buildUpstashSetOptions(options),
45
+ );
46
+ if (Buffer.isBuffer(res)) {
47
+ return res.toString();
48
+ }
49
+
50
+ return res;
51
+ }
52
+
53
+ async setex(key: RedisKey, seconds: number, value: RedisValue): Promise<'OK'> {
54
+ return this.client.setex(normalizeRedisKey(key), seconds, value);
55
+ }
56
+
57
+ async del(...keys: RedisKey[]): Promise<number> {
58
+ return this.client.del(...normalizeRedisKeys(keys));
59
+ }
60
+
61
+ async exists(...keys: RedisKey[]): Promise<number> {
62
+ return this.client.exists(...normalizeRedisKeys(keys));
63
+ }
64
+
65
+ async expire(key: RedisKey, seconds: number): Promise<number> {
66
+ return this.client.expire(normalizeRedisKey(key), seconds);
67
+ }
68
+
69
+ async ttl(key: RedisKey): Promise<number> {
70
+ return this.client.ttl(normalizeRedisKey(key));
71
+ }
72
+
73
+ async incr(key: RedisKey): Promise<number> {
74
+ return this.client.incr(normalizeRedisKey(key));
75
+ }
76
+
77
+ async decr(key: RedisKey): Promise<number> {
78
+ return this.client.decr(normalizeRedisKey(key));
79
+ }
80
+
81
+ async mget(...keys: RedisKey[]): Promise<(string | null)[]> {
82
+ return this.client.mget(...normalizeRedisKeys(keys));
83
+ }
84
+
85
+ async mset(values: RedisMSetArgument): Promise<'OK'> {
86
+ return this.client.mset(normalizeMsetValues(values));
87
+ }
88
+
89
+ async hget(key: RedisKey, field: RedisKey): Promise<string | null> {
90
+ return this.client.hget(normalizeRedisKey(key), normalizeRedisKey(field));
91
+ }
92
+
93
+ async hset(key: RedisKey, field: RedisKey, value: RedisValue): Promise<number> {
94
+ return this.client.hset(normalizeRedisKey(key), { [normalizeRedisKey(field)]: value });
95
+ }
96
+
97
+ async hdel(key: RedisKey, ...fields: RedisKey[]): Promise<number> {
98
+ return this.client.hdel(normalizeRedisKey(key), ...normalizeRedisKeys(fields));
99
+ }
100
+
101
+ async hgetall(key: RedisKey): Promise<Record<string, string>> {
102
+ const res = await this.client.hgetall(normalizeRedisKey(key));
103
+ if (!res) {
104
+ return {};
105
+ }
106
+
107
+ return res as Record<string, string>;
108
+ }
109
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ buildIORedisSetArgs,
5
+ buildUpstashSetOptions,
6
+ normalizeMsetValues,
7
+ normalizeRedisKey,
8
+ normalizeRedisKeys,
9
+ } from './utils';
10
+
11
+ describe('redis utils', () => {
12
+ it('normalizes single redis key to string', () => {
13
+ expect(normalizeRedisKey('foo')).toBe('foo');
14
+ expect(normalizeRedisKey(Buffer.from('bar'))).toBe('bar');
15
+ });
16
+
17
+ it('normalizes an array of redis keys', () => {
18
+ expect(normalizeRedisKeys(['foo', Buffer.from('bar')])).toEqual(['foo', 'bar']);
19
+ });
20
+
21
+ it('normalizes mset values from Map', () => {
22
+ const payload = normalizeMsetValues(
23
+ new Map<Buffer | string, number>([
24
+ [Buffer.from('a'), 1],
25
+ ['b', 2],
26
+ ]),
27
+ );
28
+
29
+ expect(payload).toEqual({ a: 1, b: 2 });
30
+ });
31
+
32
+ it('builds ioredis set arguments', () => {
33
+ const args = buildIORedisSetArgs({ ex: 1, nx: true, get: true });
34
+
35
+ expect(args).toEqual(['EX', 1, 'NX', 'GET']);
36
+ });
37
+
38
+ it('builds upstash set options', () => {
39
+ expect(buildUpstashSetOptions()).toBeUndefined();
40
+ expect(buildUpstashSetOptions({ ex: 10, nx: true, get: true })).toEqual({
41
+ ex: 10,
42
+ nx: true,
43
+ get: true,
44
+ });
45
+ });
46
+ });