@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,392 @@
1
+ import { AppConfig, ConfigProviderService } from '@node-c/core';
2
+ import { RedisClientType, createClient } from 'redis';
3
+ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { RedisStoreService, RedisTransaction } from './index';
6
+
7
+ interface TestAppConfig {
8
+ data: Record<string, TestAppConfigDataNoSQL>;
9
+ }
10
+ interface TestAppConfigDataNoSQL {
11
+ password?: string;
12
+ host?: string;
13
+ port?: number;
14
+ }
15
+
16
+ // Stub the createClient function from redis.
17
+ vi.mock('redis', () => {
18
+ return {
19
+ createClient: vi.fn()
20
+ };
21
+ });
22
+ vi.mock('uuid', () => {
23
+ return {
24
+ v4: vi.fn().mockReturnValue('test-transaction-id')
25
+ };
26
+ });
27
+
28
+ // TODO: unit tests with a real redis connection
29
+ describe('RedisStoreService', () => {
30
+ const moduleName = 'test';
31
+ const storeKey = 'test-store';
32
+ const configProvider = {
33
+ config: { data: { [moduleName]: { storeKey } } }
34
+ } as unknown as ConfigProviderService;
35
+
36
+ describe('constructor', () => {
37
+ it('should create an instance of the class, set its transactions property to an empty object and set its storeKey correctly when called', () => {
38
+ const service = new RedisStoreService(configProvider, {} as unknown as RedisClientType, 'test');
39
+ expect((service as unknown as { transactions: unknown }).transactions).toEqual({});
40
+ });
41
+ });
42
+
43
+ describe('createClient', () => {
44
+ // Create a dummy client that only implements the connect method
45
+ const dummyClient: RedisClientType = {
46
+ connect: vi.fn().mockResolvedValue(undefined)
47
+ } as unknown as RedisClientType;
48
+ beforeEach(() => {
49
+ vi.clearAllMocks();
50
+ (createClient as unknown as Mock).mockReturnValue(dummyClient);
51
+ });
52
+ it('should create a client with provided config values', async () => {
53
+ const config: TestAppConfig = {
54
+ data: {
55
+ testModule: {
56
+ password: 'secret',
57
+ host: 'localhost',
58
+ port: 1234
59
+ }
60
+ }
61
+ };
62
+ const options = { dataModuleName: 'testModule' };
63
+ const client = await RedisStoreService.createClient(config as unknown as AppConfig, options);
64
+ expect(createClient).toHaveBeenCalledWith({
65
+ password: 'secret',
66
+ socket: { host: 'localhost', port: 1234 },
67
+ username: 'default'
68
+ });
69
+ expect(dummyClient.connect).toHaveBeenCalled();
70
+ expect(client).toBe(dummyClient);
71
+ });
72
+ it('should create a client with default host and port when not provided', async () => {
73
+ const config: TestAppConfig = {
74
+ data: {
75
+ testModule: {
76
+ // password, host, and port are omitted (or undefined)
77
+ password: undefined,
78
+ host: undefined,
79
+ port: undefined
80
+ }
81
+ }
82
+ };
83
+ const options = { dataModuleName: 'testModule' };
84
+ const client = await RedisStoreService.createClient(config as unknown as AppConfig, options);
85
+ expect(createClient).toHaveBeenCalledWith({
86
+ password: undefined,
87
+ socket: { host: '0.0.0.0', port: 6379 },
88
+ username: 'default'
89
+ });
90
+ expect(dummyClient.connect).toHaveBeenCalled();
91
+ expect(client).toBe(dummyClient);
92
+ });
93
+ });
94
+
95
+ describe('delete', () => {
96
+ let dummyClient: RedisClientType;
97
+ let service: RedisStoreService;
98
+ beforeEach(() => {
99
+ // Create a dummy Redis client with a mocked hDel method.
100
+ dummyClient = {
101
+ hDel: vi.fn()
102
+ } as unknown as RedisClientType;
103
+ // Instantiate the service with the dummy client and a test store key.
104
+ service = new RedisStoreService(configProvider, dummyClient, moduleName);
105
+ });
106
+ it('should call client.hDel when no transactionId is provided', async () => {
107
+ // Arrange: stub hDel to resolve with a number.
108
+ const expectedResult = 5;
109
+ (dummyClient.hDel as ReturnType<typeof vi.fn>).mockResolvedValue(expectedResult);
110
+ const handle = 'testHandle';
111
+ // Act: call the delete method without transactionId.
112
+ const result = await service.delete(handle);
113
+ // Assert: verify that the client.hDel method was called correctly and the result is returned.
114
+ expect(dummyClient.hDel).toHaveBeenCalledWith(storeKey, handle);
115
+ expect(result).toBe(expectedResult);
116
+ });
117
+ it('should throw an ApplicationError when transactionId is provided but no transaction is found', async () => {
118
+ // Arrange: use a transactionId that is not in the transactions object.
119
+ const handle = 'testHandle';
120
+ const transactionId = 'nonExistentTransaction';
121
+ // Act & Assert: expect the call to throw an ApplicationError with the correct message.
122
+ await expect(service.delete(handle, { transactionId })).rejects.toThrow(
123
+ `[RedisStoreService][Error]: Transaction with id "${transactionId}" not found.`
124
+ );
125
+ });
126
+ it('should call transaction.hDel when transactionId is provided and transaction exists, then return 0', async () => {
127
+ // Arrange: create a dummy transaction object with a mocked hDel method.
128
+ const dummyTransaction = {
129
+ hDel: vi.fn().mockReturnValue(42) // The actual return value is not used by delete.
130
+ };
131
+ const handle = 'testHandle';
132
+ const transactionId = 'existingTransaction';
133
+ // Inject the dummy transaction into the service's transactions map.
134
+ service['transactions'][transactionId] = dummyTransaction as unknown as RedisTransaction;
135
+ // Act: call delete with a valid transactionId.
136
+ const result = await service.delete(handle, { transactionId });
137
+ // Assert: verify that the transaction's hDel was called with the proper arguments.
138
+ expect(dummyTransaction.hDel).toHaveBeenCalledWith(storeKey, handle);
139
+ // Also verify that the transaction in the map is replaced with the result of hDel.
140
+ expect(service['transactions'][transactionId]).toBe(42);
141
+ // And the method returns 0 as specified.
142
+ expect(result).toBe(0);
143
+ });
144
+ });
145
+
146
+ describe('createTransaction', () => {
147
+ const dummyTransaction = { transactionProp: 'dummyValue' };
148
+ let dummyClient: RedisClientType;
149
+ let service: RedisStoreService;
150
+ beforeEach(() => {
151
+ // Create a dummy client with a mocked multi method.
152
+ dummyClient = {
153
+ multi: vi.fn().mockReturnValue(dummyTransaction)
154
+ } as unknown as RedisClientType;
155
+ service = new RedisStoreService(configProvider, dummyClient, moduleName);
156
+ });
157
+ it('should create a transaction and store it in the transactions map', () => {
158
+ const transactionId = service.createTransaction();
159
+ // Verify that uuid was called and returned the constant value.
160
+ expect(transactionId).toBe('test-transaction-id');
161
+ // Verify that the client's multi method was called.
162
+ expect(dummyClient.multi).toHaveBeenCalled();
163
+ // Verify that the transaction is stored in the service's transactions map.
164
+ expect(service['transactions'][transactionId]).toBe(dummyTransaction);
165
+ });
166
+ });
167
+
168
+ describe('endTransaction', () => {
169
+ let dummyClient: RedisClientType;
170
+ let service: RedisStoreService;
171
+ beforeEach(() => {
172
+ // Create a dummy client. Its methods are not needed for endTransaction.
173
+ dummyClient = {} as RedisClientType;
174
+ service = new RedisStoreService(configProvider, dummyClient, moduleName);
175
+ });
176
+ it('should throw an ApplicationError when transaction does not exist', async () => {
177
+ const transactionId = 'nonExistentTransaction';
178
+ await expect(service.endTransaction(transactionId)).rejects.toThrow(
179
+ `[RedisStoreService][Error]: Transaction with id "${transactionId}" not found.`
180
+ );
181
+ });
182
+ it('should execute the transaction and remove it from transactions map', async () => {
183
+ const dummyTransaction = {
184
+ exec: vi.fn().mockResolvedValue(undefined)
185
+ };
186
+ const transactionId = 'existingTransaction';
187
+ // Inject the dummy transaction into the transactions map.
188
+ service['transactions'][transactionId] = dummyTransaction as unknown as RedisTransaction;
189
+ await service.endTransaction(transactionId);
190
+ expect(dummyTransaction.exec).toHaveBeenCalled();
191
+ // After execution, the transaction should be removed.
192
+ expect(service['transactions'][transactionId]).toBeUndefined();
193
+ });
194
+ it('should propagate error if exec rejects and not delete the transaction', async () => {
195
+ const error = new Error('exec error');
196
+ const dummyTransaction = {
197
+ exec: vi.fn().mockRejectedValue(error)
198
+ };
199
+ const transactionId = 'failingTransaction';
200
+ service['transactions'][transactionId] = dummyTransaction as unknown as RedisTransaction;
201
+ await expect(service.endTransaction(transactionId)).rejects.toThrow(error);
202
+ // The transaction remains since deletion happens only on successful exec.
203
+ expect(service['transactions'][transactionId]).toBe(dummyTransaction);
204
+ });
205
+ });
206
+
207
+ describe('get', () => {
208
+ let dummyClient: RedisClientType;
209
+ let service: RedisStoreService;
210
+ beforeEach(() => {
211
+ // Create a dummy Redis client with a mocked hGet method.
212
+ dummyClient = {
213
+ hGet: vi.fn()
214
+ } as unknown as RedisClientType;
215
+ service = new RedisStoreService(configProvider, dummyClient, moduleName);
216
+ });
217
+ it('should return the value as is when parseToJSON is not requested', async () => {
218
+ const expectedValue = { foo: 'bar' };
219
+ const handle = 'someKey';
220
+ (dummyClient.hGet as ReturnType<typeof vi.fn>).mockResolvedValue(expectedValue);
221
+ const result = await service.get<typeof expectedValue>(handle);
222
+ expect(dummyClient.hGet).toHaveBeenCalledWith(storeKey, handle);
223
+ expect(result).toEqual(expectedValue);
224
+ });
225
+ it('should return parsed JSON when parseToJSON is true and value is a string', async () => {
226
+ const handle = 'jsonKey';
227
+ const jsonString = '{"foo":"bar"}';
228
+ const parsedValue = { foo: 'bar' };
229
+ (dummyClient.hGet as ReturnType<typeof vi.fn>).mockResolvedValue(jsonString);
230
+ const result = await service.get<typeof parsedValue>(handle, { parseToJSON: true });
231
+ expect(dummyClient.hGet).toHaveBeenCalledWith(storeKey, handle);
232
+ expect(result).toEqual(parsedValue);
233
+ });
234
+ it('should return the original value when parseToJSON is true but value is not a string', async () => {
235
+ const expectedValue = 12345;
236
+ const handle = 'nonStringKey';
237
+ (dummyClient.hGet as ReturnType<typeof vi.fn>).mockResolvedValue(expectedValue);
238
+ const result = await service.get<number>(handle, { parseToJSON: true });
239
+ expect(dummyClient.hGet).toHaveBeenCalledWith(storeKey, handle);
240
+ expect(result).toBe(expectedValue);
241
+ });
242
+ });
243
+
244
+ describe('scan', () => {
245
+ const handle = 'pattern';
246
+ let dummyClient: RedisClientType;
247
+ let service: RedisStoreService;
248
+ beforeEach(() => {
249
+ dummyClient = {
250
+ hScan: vi.fn(),
251
+ hScanNoValues: vi.fn()
252
+ } as unknown as RedisClientType;
253
+ service = new RedisStoreService(configProvider, dummyClient, moduleName);
254
+ });
255
+ it('should throw an error with the correct message when neither the "cursor" nor the "scanAll" options are provided', async () => {
256
+ await expect(service.scan(handle, {})).rejects.toThrow(
257
+ 'The "count" options is required when the "findAll" options is not positive.'
258
+ );
259
+ });
260
+ it('should scan all using hScan when scanAll is true and return unparsed values', async () => {
261
+ // Simulate multiple iterations:
262
+ // 1st call returns cursor 1 and one tuple.
263
+ // 2nd call returns cursor 0 and another tuple.
264
+ (dummyClient.hScan as ReturnType<typeof vi.fn>)
265
+ .mockResolvedValueOnce({ cursor: 1, tuples: [{ field: 'f1', value: '10' }] })
266
+ .mockResolvedValueOnce({ cursor: 0, tuples: [{ field: 'f2', value: '20' }] });
267
+ const options = { scanAll: true, count: 1 };
268
+ const result = await service.scan(handle, options);
269
+ expect(dummyClient.hScan).toHaveBeenCalledTimes(2);
270
+ expect(dummyClient.hScan).toHaveBeenNthCalledWith(1, storeKey, 0, { count: 1, match: handle });
271
+ expect(dummyClient.hScan).toHaveBeenNthCalledWith(2, storeKey, 0, { count: 1, match: handle });
272
+ // Since parseToJSON is not requested, the values are returned as is.
273
+ expect(result).toEqual(['10', '20']);
274
+ });
275
+ it('should scan all using hScan and parse JSON values when parseToJSON is true', async () => {
276
+ // Single iteration returning one tuple with a JSON string.
277
+ (dummyClient.hScan as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
278
+ cursor: 0,
279
+ tuples: [
280
+ { field: 'f1', value: '{"a":1}' },
281
+ { field: 'f2', value: 527 }
282
+ ]
283
+ });
284
+ const options = { scanAll: true, count: 2, parseToJSON: true };
285
+ const result = await service.scan(handle, options);
286
+ expect(result).toEqual([{ a: 1 }, 527]);
287
+ });
288
+ it('should scan once using hScan when scanAll is false and return unparsed values', async () => {
289
+ // Non-scanAll branch: simulate a single call with a provided cursor and tuples.
290
+ (dummyClient.hScan as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
291
+ cursor: 0,
292
+ tuples: [{ field: 'f1', value: '100' }]
293
+ });
294
+ const options = { cursor: 5, count: 3 };
295
+ const result = await service.scan(handle, options);
296
+ expect(dummyClient.hScan).toHaveBeenCalledWith(storeKey, 5, { count: 3, match: handle });
297
+ expect(result).toEqual(['100']);
298
+ });
299
+ it('should scan once using hScan when scanAll is false and return empty result if no tuples', async () => {
300
+ // Simulate a call returning keys (and no tuples).
301
+ (dummyClient.hScan as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ cursor: 0, keys: ['key1', 'key2'] });
302
+ const options = { cursor: 0, count: 2 };
303
+ const result = await service.scan(handle, options);
304
+ // In this branch, keys are returned but values remains empty, so the result is [].
305
+ expect(result).toEqual([]);
306
+ });
307
+ it('should use hScanNoValues when withValues is false and return empty array (non-scanAll)', async () => {
308
+ // For withValues false, the hScanNoValues method is used.
309
+ (dummyClient.hScanNoValues as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ cursor: 0, keys: ['a', 'b'] });
310
+ const options = { cursor: 0, count: 2, withValues: false };
311
+ const result = await service.scan(handle, options);
312
+ expect(dummyClient.hScanNoValues).toHaveBeenCalledWith(storeKey, 0, { count: 2, match: handle });
313
+ // Since no tuples are provided, the result is an empty array.
314
+ expect(result).toEqual([]);
315
+ });
316
+ it('should use hScanNoValues when withValues is false and scanAll is true, returning empty array', async () => {
317
+ // For scanAll true with withValues false.
318
+ (dummyClient.hScanNoValues as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ cursor: 0, keys: ['a'] });
319
+ const options = { scanAll: true, count: 1, withValues: false };
320
+ const result = await service.scan(handle, options);
321
+ expect(dummyClient.hScanNoValues).toHaveBeenCalledTimes(1);
322
+ expect(result).toEqual([]);
323
+ });
324
+ });
325
+
326
+ describe('set', () => {
327
+ let dummyClient: RedisClientType;
328
+ let service: RedisStoreService;
329
+ beforeEach(() => {
330
+ dummyClient = {
331
+ hSet: vi.fn()
332
+ } as unknown as RedisClientType;
333
+ service = new RedisStoreService(configProvider, dummyClient, moduleName);
334
+ });
335
+ it('should set a non-string entry using client.hSet when no transactionId is provided and result is "OK"', async () => {
336
+ const entry = { a: 1 };
337
+ const handle = 'key1';
338
+ const stringifiedEntry = JSON.stringify(entry);
339
+ (dummyClient.hSet as ReturnType<typeof vi.fn>).mockResolvedValue('OK');
340
+ await service.set(handle, entry);
341
+ expect(dummyClient.hSet).toHaveBeenCalledWith(storeKey, handle, stringifiedEntry);
342
+ });
343
+ it('should set a string entry using client.hSet when no transactionId is provided and result is 1', async () => {
344
+ const entry = 'string value';
345
+ const handle = 'key2';
346
+ (dummyClient.hSet as ReturnType<typeof vi.fn>).mockResolvedValue(1);
347
+ await service.set(handle, entry);
348
+ expect(dummyClient.hSet).toHaveBeenCalledWith(storeKey, handle, entry);
349
+ });
350
+ it('should throw an ApplicationError when client.hSet returns an unexpected result', async () => {
351
+ const entry = 'some value';
352
+ const handle = 'key3';
353
+ (dummyClient.hSet as ReturnType<typeof vi.fn>).mockResolvedValue(0);
354
+ await expect(service.set(handle, entry)).rejects.toThrow(
355
+ `[RedisStoreService][Error]: Value not set for handle "${handle}". Result: 0`
356
+ );
357
+ });
358
+ it('should throw an ApplicationError when a transactionId is provided but no transaction exists', async () => {
359
+ const entry = { b: 2 };
360
+ const handle = 'key4';
361
+ const transactionId = 'txnNotExist';
362
+ await expect(service.set(handle, entry, { transactionId })).rejects.toThrow(
363
+ `[RedisStoreService][Error]: Transaction with id "${transactionId}" not found.`
364
+ );
365
+ });
366
+ it('should set a non-string entry in a transaction when transactionId is provided and transaction exists', async () => {
367
+ const dummyTransaction = {
368
+ hSet: vi.fn().mockReturnValue('dummyReturn')
369
+ };
370
+ const entry = { b: 2 };
371
+ const handle = 'key5';
372
+ const stringifiedEntry = JSON.stringify(entry);
373
+ const transactionId = 'txn1';
374
+ service['transactions'][transactionId] = dummyTransaction as unknown as RedisTransaction;
375
+ await service.set(handle, entry, { transactionId });
376
+ expect(dummyTransaction.hSet).toHaveBeenCalledWith(storeKey, handle, stringifiedEntry);
377
+ expect(service['transactions'][transactionId]).toBe('dummyReturn');
378
+ });
379
+ it('should set a string entry in a transaction when transactionId is provided and transaction exists', async () => {
380
+ const dummyTransaction = {
381
+ hSet: vi.fn().mockReturnValue('transactionReturn')
382
+ };
383
+ const entry = 'simple string';
384
+ const handle = 'key6';
385
+ const transactionId = 'txn2';
386
+ service['transactions'][transactionId] = dummyTransaction as unknown as RedisTransaction;
387
+ await service.set(handle, entry, { transactionId });
388
+ expect(dummyTransaction.hSet).toHaveBeenCalledWith(storeKey, handle, entry);
389
+ expect(service['transactions'][transactionId]).toBe('transactionReturn');
390
+ });
391
+ });
392
+ });