@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.
Files changed (34) hide show
  1. package/dist/repository/redis.repository.module.js +1 -1
  2. package/dist/repository/redis.repository.module.js.map +1 -1
  3. package/dist/repository/redis.repository.service.d.ts +6 -3
  4. package/dist/repository/redis.repository.service.js +83 -20
  5. package/dist/repository/redis.repository.service.js.map +1 -1
  6. package/dist/store/redis.store.module.js +1 -1
  7. package/dist/store/redis.store.module.js.map +1 -1
  8. package/dist/store/redis.store.service.js +1 -1
  9. package/dist/store/redis.store.service.js.map +1 -1
  10. package/package.json +4 -4
  11. package/src/common/definitions/common.constants.ts +10 -0
  12. package/src/common/definitions/index.ts +1 -0
  13. package/src/entityService/index.ts +2 -0
  14. package/src/entityService/redis.entity.service.definitions.ts +73 -0
  15. package/src/entityService/redis.entity.service.spec.ts +190 -0
  16. package/src/entityService/redis.entity.service.ts +291 -0
  17. package/src/index.ts +5 -0
  18. package/src/module/index.ts +2 -0
  19. package/src/module/redis.module.definitions.ts +18 -0
  20. package/src/module/redis.module.spec.ts +80 -0
  21. package/src/module/redis.module.ts +31 -0
  22. package/src/repository/index.ts +3 -0
  23. package/src/repository/redis.repository.definitions.ts +97 -0
  24. package/src/repository/redis.repository.module.spec.ts +60 -0
  25. package/src/repository/redis.repository.module.ts +34 -0
  26. package/src/repository/redis.repository.service.ts +657 -0
  27. package/src/repository/redis.repository.spec.ts +384 -0
  28. package/src/store/index.ts +3 -0
  29. package/src/store/redis.store.definitions.ts +25 -0
  30. package/src/store/redis.store.module.spec.ts +70 -0
  31. package/src/store/redis.store.module.ts +34 -0
  32. package/src/store/redis.store.service.spec.ts +392 -0
  33. package/src/store/redis.store.service.ts +391 -0
  34. 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
+ }
@@ -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
+ });