@node-c/data-redis 1.0.0-alpha64 → 1.0.0-beta0
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/dist/repository/redis.repository.module.js +1 -1
- package/dist/repository/redis.repository.module.js.map +1 -1
- package/dist/repository/redis.repository.service.d.ts +6 -3
- package/dist/repository/redis.repository.service.js +83 -20
- package/dist/repository/redis.repository.service.js.map +1 -1
- package/dist/store/redis.store.module.js +1 -1
- package/dist/store/redis.store.module.js.map +1 -1
- package/dist/store/redis.store.service.js +1 -1
- package/dist/store/redis.store.service.js.map +1 -1
- package/package.json +4 -4
- package/src/common/definitions/common.constants.ts +10 -0
- package/src/common/definitions/index.ts +1 -0
- package/src/entityService/index.ts +2 -0
- package/src/entityService/redis.entity.service.definitions.ts +73 -0
- package/src/entityService/redis.entity.service.spec.ts +190 -0
- package/src/entityService/redis.entity.service.ts +291 -0
- package/src/index.ts +5 -0
- package/src/module/index.ts +2 -0
- package/src/module/redis.module.definitions.ts +18 -0
- package/src/module/redis.module.spec.ts +80 -0
- package/src/module/redis.module.ts +31 -0
- package/src/repository/index.ts +3 -0
- package/src/repository/redis.repository.definitions.ts +97 -0
- package/src/repository/redis.repository.module.spec.ts +60 -0
- package/src/repository/redis.repository.module.ts +34 -0
- package/src/repository/redis.repository.service.ts +657 -0
- package/src/repository/redis.repository.spec.ts +384 -0
- package/src/store/index.ts +3 -0
- package/src/store/redis.store.definitions.ts +25 -0
- package/src/store/redis.store.module.spec.ts +70 -0
- package/src/store/redis.store.module.ts +34 -0
- package/src/store/redis.store.service.spec.ts +392 -0
- package/src/store/redis.store.service.ts +391 -0
- package/src/vitest.config.ts +9 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AppConfig,
|
|
5
|
+
AppConfigDataNoSQL,
|
|
6
|
+
ApplicationError,
|
|
7
|
+
ConfigProviderService,
|
|
8
|
+
Constants as CoreConstants,
|
|
9
|
+
GenericObject,
|
|
10
|
+
NoSQLType
|
|
11
|
+
} from '@node-c/core';
|
|
12
|
+
|
|
13
|
+
import Redis, { ChainableCommander, Cluster, ClusterOptions, RedisOptions } from 'ioredis';
|
|
14
|
+
import Valkey from 'iovalkey';
|
|
15
|
+
import { v4 as uuid } from 'uuid';
|
|
16
|
+
|
|
17
|
+
import { GetOptions, ScanOptions, SetOptions, StoreDeleteOptions } from './redis.store.definitions';
|
|
18
|
+
|
|
19
|
+
import { Constants } from '../common/definitions';
|
|
20
|
+
|
|
21
|
+
// TODO: support switching between hashmap and non-hashmap methods (e.g. hget/get) on the method basis, rather than
|
|
22
|
+
// for the whole store
|
|
23
|
+
// TODO: support sets
|
|
24
|
+
@Injectable()
|
|
25
|
+
export class RedisStoreService {
|
|
26
|
+
protected defaultTTL?: number;
|
|
27
|
+
protected storeDelimiter: string;
|
|
28
|
+
protected storeKey: string;
|
|
29
|
+
protected transactions: GenericObject<ChainableCommander>;
|
|
30
|
+
protected useHashmap: boolean;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
protected configProvider: ConfigProviderService,
|
|
34
|
+
@Inject(Constants.REDIS_CLIENT)
|
|
35
|
+
// eslint-disable-next-line no-unused-vars
|
|
36
|
+
protected client: Redis | Cluster,
|
|
37
|
+
@Inject(CoreConstants.DATA_MODULE_NAME)
|
|
38
|
+
protected dataModuleName: string
|
|
39
|
+
) {
|
|
40
|
+
const { defaultTTL, storeDelimiter, storeKey, useHashmap } = configProvider.config.data[
|
|
41
|
+
dataModuleName
|
|
42
|
+
] as AppConfigDataNoSQL;
|
|
43
|
+
this.defaultTTL = defaultTTL;
|
|
44
|
+
this.storeDelimiter = storeDelimiter || Constants.DEFAULT_STORE_DELIMITER;
|
|
45
|
+
this.storeKey = storeKey;
|
|
46
|
+
this.transactions = {};
|
|
47
|
+
this.useHashmap = typeof useHashmap !== 'undefined' ? useHashmap : true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static async createClient(config: AppConfig, options: { dataModuleName: string }): Promise<Redis | Cluster> {
|
|
51
|
+
const { dataModuleName } = options;
|
|
52
|
+
const {
|
|
53
|
+
clusterMode,
|
|
54
|
+
failOnConnectionError = true,
|
|
55
|
+
password,
|
|
56
|
+
host,
|
|
57
|
+
port,
|
|
58
|
+
sentinelMasterName,
|
|
59
|
+
sentinelMode,
|
|
60
|
+
sentinelPassword,
|
|
61
|
+
sentinelRole,
|
|
62
|
+
type,
|
|
63
|
+
usePasswordForSentinelPassword,
|
|
64
|
+
user
|
|
65
|
+
} = config.data[dataModuleName] as AppConfigDataNoSQL;
|
|
66
|
+
const actualHost = host || '0.0.0.0';
|
|
67
|
+
const actualPassword = password?.length ? password : undefined;
|
|
68
|
+
const actualPort = port || 6379;
|
|
69
|
+
const actualUser = user?.length ? user : undefined;
|
|
70
|
+
const clientOptions: {
|
|
71
|
+
clusterRetryStrategy?: ClusterOptions['clusterRetryStrategy'];
|
|
72
|
+
maxRetriesPerRequest?: RedisOptions['maxRetriesPerRequest'];
|
|
73
|
+
retryStrategy?: RedisOptions['retryStrategy'];
|
|
74
|
+
sentinelRetryStrategy?: RedisOptions['sentinelRetryStrategy'];
|
|
75
|
+
} = {};
|
|
76
|
+
let lastRetryAt = new Date().valueOf();
|
|
77
|
+
const retryMethod = () => {
|
|
78
|
+
const now = new Date().valueOf();
|
|
79
|
+
// 1 minute retry interval
|
|
80
|
+
if (Math.abs(lastRetryAt - now) > 60000) {
|
|
81
|
+
lastRetryAt = now;
|
|
82
|
+
return 500;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
if (clusterMode) {
|
|
87
|
+
if (!failOnConnectionError) {
|
|
88
|
+
clientOptions.clusterRetryStrategy = retryMethod;
|
|
89
|
+
}
|
|
90
|
+
const ClusterConstructor = type === NoSQLType.Valkey ? Valkey.Cluster : Cluster;
|
|
91
|
+
const client = new ClusterConstructor(RedisStoreService.getNodeList(actualHost, actualPort), {
|
|
92
|
+
...clientOptions,
|
|
93
|
+
lazyConnect: true,
|
|
94
|
+
redisOptions: { password: actualPassword, username: actualUser }
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
await client.connect();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(`[RedisStore][${dataModuleName}]: Error connecting to Redis:`, err);
|
|
100
|
+
if (failOnConnectionError) {
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
client.disconnect();
|
|
104
|
+
}
|
|
105
|
+
return client as Cluster;
|
|
106
|
+
}
|
|
107
|
+
if (sentinelMode) {
|
|
108
|
+
if (!failOnConnectionError) {
|
|
109
|
+
clientOptions.maxRetriesPerRequest = 0;
|
|
110
|
+
clientOptions.sentinelRetryStrategy = retryMethod;
|
|
111
|
+
}
|
|
112
|
+
const SentinelConstructor = type === NoSQLType.Valkey ? Valkey : Redis;
|
|
113
|
+
const client = new SentinelConstructor({
|
|
114
|
+
...clientOptions,
|
|
115
|
+
lazyConnect: true,
|
|
116
|
+
name: sentinelMasterName || 'mymaster',
|
|
117
|
+
password: actualPassword,
|
|
118
|
+
role: sentinelRole || 'master',
|
|
119
|
+
sentinels: RedisStoreService.getNodeList(actualHost, actualPort),
|
|
120
|
+
sentinelPassword: sentinelPassword?.length
|
|
121
|
+
? sentinelPassword
|
|
122
|
+
: usePasswordForSentinelPassword
|
|
123
|
+
? actualPassword
|
|
124
|
+
: undefined,
|
|
125
|
+
username: actualUser
|
|
126
|
+
});
|
|
127
|
+
client.on('error', (error: unknown) => {
|
|
128
|
+
console.error(`[RedisStore][${dataModuleName}]: Error:`, error);
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
await client.connect();
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(`[RedisStore][${dataModuleName}]: Error connecting to Redis:`, err);
|
|
134
|
+
if (failOnConnectionError) {
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
client.disconnect();
|
|
138
|
+
}
|
|
139
|
+
return client as Redis;
|
|
140
|
+
}
|
|
141
|
+
if (!failOnConnectionError) {
|
|
142
|
+
clientOptions.maxRetriesPerRequest = 0;
|
|
143
|
+
clientOptions.retryStrategy = retryMethod;
|
|
144
|
+
}
|
|
145
|
+
const ClientConstructor = type === NoSQLType.Valkey ? Valkey : Redis;
|
|
146
|
+
const client = new ClientConstructor({
|
|
147
|
+
...clientOptions,
|
|
148
|
+
host: actualHost,
|
|
149
|
+
lazyConnect: true,
|
|
150
|
+
password: actualPassword,
|
|
151
|
+
port: actualPort,
|
|
152
|
+
username: actualUser
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
await client.connect();
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error(`[RedisStore][${dataModuleName}]: Error connecting to Redis:`, err);
|
|
158
|
+
if (failOnConnectionError) {
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
client.disconnect();
|
|
162
|
+
}
|
|
163
|
+
return client as Redis;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
createTransaction(): string {
|
|
167
|
+
const transactionId = uuid();
|
|
168
|
+
this.transactions[transactionId] = this.client.multi();
|
|
169
|
+
return transactionId;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async delete(handle: string | string[], options?: StoreDeleteOptions): Promise<number> {
|
|
173
|
+
const { client, storeDelimiter, storeKey, transactions, useHashmap } = this;
|
|
174
|
+
const { transactionId } = options || ({} as StoreDeleteOptions);
|
|
175
|
+
const handles = handle instanceof Array ? handle : [handle];
|
|
176
|
+
if (transactionId) {
|
|
177
|
+
const transaction = transactions[transactionId];
|
|
178
|
+
if (!transaction) {
|
|
179
|
+
throw new ApplicationError(`[RedisStoreService][Error]: Transaction with id "${transactionId}" not found.`);
|
|
180
|
+
}
|
|
181
|
+
transactions[transactionId] = useHashmap
|
|
182
|
+
? transaction.hdel(storeKey, ...handles)
|
|
183
|
+
: transaction.del(handles.map(handleItem => `${storeKey}${storeDelimiter}${handleItem}`));
|
|
184
|
+
// TODO: return the actual amount
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
return useHashmap
|
|
188
|
+
? await client.hdel(storeKey, ...handles)
|
|
189
|
+
: await client.del(handles.map(handleItem => `${storeKey}${storeDelimiter}${handleItem}`));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async endTransaction(transactionId: string): Promise<void> {
|
|
193
|
+
const { transactions } = this;
|
|
194
|
+
const transaction = transactions[transactionId];
|
|
195
|
+
if (!transaction) {
|
|
196
|
+
throw new ApplicationError(`[RedisStoreService][Error]: Transaction with id "${transactionId}" not found.`);
|
|
197
|
+
}
|
|
198
|
+
// TODO: how will we know whether it's successful or not?
|
|
199
|
+
await transaction.exec();
|
|
200
|
+
delete transactions[transactionId];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// TODO: support get from transaction data
|
|
204
|
+
async get<Value = unknown>(handle: string, options?: GetOptions): Promise<Value> {
|
|
205
|
+
const { client, storeDelimiter, storeKey, useHashmap } = this;
|
|
206
|
+
const { parseToJSON, withValues } = options || ({} as GetOptions);
|
|
207
|
+
if (withValues || typeof withValues === 'undefined') {
|
|
208
|
+
const value = useHashmap
|
|
209
|
+
? await client.hget(storeKey, handle)
|
|
210
|
+
: await client.get(`${storeKey}${storeDelimiter}${handle}`);
|
|
211
|
+
return parseToJSON && typeof value === 'string' ? JSON.parse(value) : (value as Value);
|
|
212
|
+
}
|
|
213
|
+
return useHashmap
|
|
214
|
+
? (!!(await client.hexists(storeKey, handle)) as Value)
|
|
215
|
+
: (!!(await client.exists(`${storeKey}${storeDelimiter}${handle}`)) as Value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static getNodeList(host: string, port: number): { host: string; port: number }[] {
|
|
219
|
+
const hostList = host.split(',');
|
|
220
|
+
const portList = `${port}`.split(',');
|
|
221
|
+
return hostList.map((hostAddress, hostIndex) => {
|
|
222
|
+
return { host: hostAddress, port: parseInt(portList[hostIndex] || portList[0], 10) };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// TODO: support scan from transaction data
|
|
227
|
+
// TODO: optimize this method to reduce branches, ugly conidtional statements and repeatability
|
|
228
|
+
async scan<Values = unknown[]>(handle: string, options: ScanOptions): Promise<{ cursor: number; values: Values }> {
|
|
229
|
+
const { client, storeDelimiter, storeKey, useHashmap } = this;
|
|
230
|
+
const { count, cursor: optCursor, parseToJSON, scanAll, withValues } = options;
|
|
231
|
+
const getValues = typeof withValues === 'undefined' || withValues === true;
|
|
232
|
+
const values: { field: string; value: string }[] = [];
|
|
233
|
+
let cursor = 0;
|
|
234
|
+
let keys: string[] = [];
|
|
235
|
+
let parsedValues: unknown[] = [];
|
|
236
|
+
if (scanAll) {
|
|
237
|
+
if (useHashmap) {
|
|
238
|
+
// TODO: remove repeating code
|
|
239
|
+
while (true) {
|
|
240
|
+
const [newCursor, newKeys] = await client.hscan(
|
|
241
|
+
storeKey,
|
|
242
|
+
cursor,
|
|
243
|
+
'MATCH',
|
|
244
|
+
handle,
|
|
245
|
+
...((typeof count !== 'undefined' ? ['COUNT', count] : []) as ['COUNT', number])
|
|
246
|
+
);
|
|
247
|
+
cursor = parseInt(newCursor, 10);
|
|
248
|
+
if (getValues) {
|
|
249
|
+
// TODO: remove repeating code
|
|
250
|
+
for (const i in newKeys) {
|
|
251
|
+
const key = newKeys[i];
|
|
252
|
+
const value = await client.hget(storeKey, key);
|
|
253
|
+
if (value === null) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
values.push({ field: key, value });
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
keys = keys.concat(newKeys!);
|
|
260
|
+
}
|
|
261
|
+
if (cursor === 0) {
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
// TODO: remove repeating code
|
|
267
|
+
while (true) {
|
|
268
|
+
const [newCursor, newKeys] = await client.scan(cursor, 'MATCH', `${storeKey}${storeDelimiter}${handle}`);
|
|
269
|
+
cursor = parseInt(newCursor, 10);
|
|
270
|
+
if (getValues) {
|
|
271
|
+
for (const i in newKeys) {
|
|
272
|
+
const key = newKeys[i];
|
|
273
|
+
const value = await client.get(key);
|
|
274
|
+
if (value === null) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
values.push({ field: key, value });
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
keys = keys.concat(newKeys!);
|
|
281
|
+
}
|
|
282
|
+
if (cursor === 0) {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
if (typeof count === 'undefined') {
|
|
289
|
+
throw new ApplicationError('The "count" options is required when the "findAll" options is not positive.');
|
|
290
|
+
}
|
|
291
|
+
// TODO: remove repeating code
|
|
292
|
+
if (useHashmap) {
|
|
293
|
+
const [newCursor, newKeys] = await client.hscan(storeKey, optCursor || 0, 'MATCH', handle, 'COUNT', count);
|
|
294
|
+
cursor = parseInt(newCursor, 10);
|
|
295
|
+
// TODO: remove repeating code
|
|
296
|
+
if (getValues) {
|
|
297
|
+
for (const i in newKeys) {
|
|
298
|
+
const key = newKeys[i];
|
|
299
|
+
const value = await client.get(key);
|
|
300
|
+
if (value === null) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
values.push({ field: key, value });
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
keys = keys.concat(newKeys!);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
const [newCursor, newKeys] = await client.scan(
|
|
310
|
+
optCursor || 0,
|
|
311
|
+
'MATCH',
|
|
312
|
+
`${storeKey}${storeDelimiter}${handle}`,
|
|
313
|
+
'COUNT',
|
|
314
|
+
count
|
|
315
|
+
);
|
|
316
|
+
cursor = parseInt(newCursor, 10);
|
|
317
|
+
// TODO: remove repeating code
|
|
318
|
+
if (getValues) {
|
|
319
|
+
for (const i in newKeys) {
|
|
320
|
+
const key = newKeys[i];
|
|
321
|
+
const value = await client.get(key);
|
|
322
|
+
if (value === null) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
values.push({ field: key, value });
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
keys = keys.concat(newKeys!);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (parseToJSON) {
|
|
333
|
+
for (const i in values) {
|
|
334
|
+
const { value } = values[i];
|
|
335
|
+
if (typeof value === 'string') {
|
|
336
|
+
parsedValues.push(JSON.parse(value));
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
parsedValues.push(value);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
parsedValues = values.map(({ value }) => value);
|
|
343
|
+
}
|
|
344
|
+
return { cursor, values: parsedValues as Values };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// TODO: fix hExpire
|
|
348
|
+
// TODO: optimize this method to reduce branches, ugly conidtional statements and repeatability
|
|
349
|
+
async set<Entry = unknown>(handle: string, entry: Entry, options?: SetOptions): Promise<void> {
|
|
350
|
+
const { client, defaultTTL, storeDelimiter, storeKey, transactions, useHashmap } = this;
|
|
351
|
+
const { transactionId, ttl } = options || ({} as SetOptions);
|
|
352
|
+
const actualTTL = ttl || defaultTTL;
|
|
353
|
+
const valueToSet = typeof entry !== 'string' ? JSON.stringify(entry) : entry;
|
|
354
|
+
if (transactionId) {
|
|
355
|
+
const transaction = transactions[transactionId];
|
|
356
|
+
if (!transaction) {
|
|
357
|
+
throw new ApplicationError(`[RedisStoreService][Error]: Transaction with id "${transactionId}" not found.`);
|
|
358
|
+
}
|
|
359
|
+
if (useHashmap) {
|
|
360
|
+
transactions[transactionId] = transaction.hset(this.storeKey, handle, valueToSet);
|
|
361
|
+
// if (actualTTL) {
|
|
362
|
+
// transactions[transactionId] = transactions[transactionId].hExpire(this.storeKey, handle, actualTTL, 'NX');
|
|
363
|
+
// }
|
|
364
|
+
} else {
|
|
365
|
+
const fullKey = `${storeKey}${storeDelimiter}${handle}`;
|
|
366
|
+
transactions[transactionId] = transaction.set(fullKey, valueToSet);
|
|
367
|
+
if (actualTTL) {
|
|
368
|
+
transactions[transactionId] = transactions[transactionId].expire(fullKey, actualTTL, 'NX');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
let result: unknown;
|
|
374
|
+
if (useHashmap) {
|
|
375
|
+
result = await client.hset(storeKey, handle, valueToSet);
|
|
376
|
+
// if (actualTTL) {
|
|
377
|
+
// await client.hexpire(storeKey, handle, actualTTL, 'NX');
|
|
378
|
+
// // await client.expire(storeKey, actualTTL, 'NX');
|
|
379
|
+
// }
|
|
380
|
+
} else {
|
|
381
|
+
const fullKey = `${storeKey}${storeDelimiter}${handle}`;
|
|
382
|
+
result = await client.set(fullKey, valueToSet);
|
|
383
|
+
if (actualTTL) {
|
|
384
|
+
await client.expire(fullKey, actualTTL, 'NX');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (result !== 'OK' && result !== 1) {
|
|
388
|
+
throw new ApplicationError(`[RedisStoreService][Error]: Value not set for handle "${handle}". Result: ${result}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|