@node-c/data-redis 1.0.0-alpha64 → 1.0.0-beta1

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