mvc-common-toolkit 1.43.10 → 1.44.0

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 (210) hide show
  1. package/dist/src/constants.d.ts +59 -0
  2. package/dist/src/constants.js +69 -0
  3. package/dist/src/constants.js.map +1 -0
  4. package/dist/src/gateways/alibaba-cloud-gateway.d.ts +30 -0
  5. package/dist/src/gateways/alibaba-cloud-gateway.js +120 -0
  6. package/dist/src/gateways/alibaba-cloud-gateway.js.map +1 -0
  7. package/dist/src/gateways/http-audit-gateway.d.ts +20 -0
  8. package/dist/src/gateways/http-audit-gateway.js +76 -0
  9. package/dist/src/gateways/http-audit-gateway.js.map +1 -0
  10. package/dist/src/gateways/index.d.ts +5 -0
  11. package/dist/src/gateways/index.js +22 -0
  12. package/dist/src/gateways/index.js.map +1 -0
  13. package/dist/src/gateways/internal-auth-gateway.d.ts +8 -0
  14. package/dist/src/gateways/internal-auth-gateway.js +40 -0
  15. package/dist/src/gateways/internal-auth-gateway.js.map +1 -0
  16. package/dist/src/gateways/stdout-audit-gateway.d.ts +7 -0
  17. package/dist/src/gateways/stdout-audit-gateway.js +25 -0
  18. package/dist/src/gateways/stdout-audit-gateway.js.map +1 -0
  19. package/dist/src/gateways/webhook-audit-gateway.d.ts +14 -0
  20. package/dist/src/gateways/webhook-audit-gateway.js +27 -0
  21. package/dist/src/gateways/webhook-audit-gateway.js.map +1 -0
  22. package/dist/src/interfaces.d.ts +218 -0
  23. package/dist/src/interfaces.js +3 -0
  24. package/dist/src/interfaces.js.map +1 -0
  25. package/dist/src/models/audit-log.d.ts +77 -0
  26. package/dist/src/models/audit-log.js +92 -0
  27. package/dist/src/models/audit-log.js.map +1 -0
  28. package/dist/src/models/index.d.ts +1 -0
  29. package/dist/src/models/index.js +18 -0
  30. package/dist/src/models/index.js.map +1 -0
  31. package/dist/src/pkg/array-helper.d.ts +1 -0
  32. package/dist/src/pkg/array-helper.js +12 -0
  33. package/dist/src/pkg/array-helper.js.map +1 -0
  34. package/dist/src/pkg/bcrypt-helper.d.ts +2 -0
  35. package/dist/src/pkg/bcrypt-helper.js +36 -0
  36. package/dist/src/pkg/bcrypt-helper.js.map +1 -0
  37. package/dist/src/pkg/crypto-helper.d.ts +2 -0
  38. package/dist/src/pkg/crypto-helper.js +16 -0
  39. package/dist/src/pkg/crypto-helper.js.map +1 -0
  40. package/dist/src/pkg/encryption-helper.d.ts +18 -0
  41. package/dist/src/pkg/encryption-helper.js +89 -0
  42. package/dist/src/pkg/encryption-helper.js.map +1 -0
  43. package/dist/src/pkg/encryption-helper.spec.d.ts +1 -0
  44. package/dist/src/pkg/encryption-helper.spec.js +238 -0
  45. package/dist/src/pkg/encryption-helper.spec.js.map +1 -0
  46. package/dist/src/pkg/filter-helper.d.ts +2 -0
  47. package/dist/src/pkg/filter-helper.js +102 -0
  48. package/dist/src/pkg/filter-helper.js.map +1 -0
  49. package/dist/src/pkg/filter-helper.spec.d.ts +1 -0
  50. package/dist/src/pkg/filter-helper.spec.js +94 -0
  51. package/dist/src/pkg/filter-helper.spec.js.map +1 -0
  52. package/dist/src/pkg/geoip-helper.d.ts +2 -0
  53. package/dist/src/pkg/geoip-helper.js +32 -0
  54. package/dist/src/pkg/geoip-helper.js.map +1 -0
  55. package/dist/src/pkg/hash-helper.d.ts +1 -0
  56. package/dist/src/pkg/hash-helper.js +37 -0
  57. package/dist/src/pkg/hash-helper.js.map +1 -0
  58. package/dist/src/pkg/http-request-utils.d.ts +4 -0
  59. package/dist/src/pkg/http-request-utils.js +55 -0
  60. package/dist/src/pkg/http-request-utils.js.map +1 -0
  61. package/dist/src/pkg/index.d.ts +19 -0
  62. package/dist/src/pkg/index.js +46 -0
  63. package/dist/src/pkg/index.js.map +1 -0
  64. package/dist/src/pkg/key-helper.d.ts +2 -0
  65. package/dist/src/pkg/key-helper.js +20 -0
  66. package/dist/src/pkg/key-helper.js.map +1 -0
  67. package/dist/src/pkg/logger.d.ts +9 -0
  68. package/dist/src/pkg/logger.js +23 -0
  69. package/dist/src/pkg/logger.js.map +1 -0
  70. package/dist/src/pkg/object-helper.d.ts +2 -0
  71. package/dist/src/pkg/object-helper.js +37 -0
  72. package/dist/src/pkg/object-helper.js.map +1 -0
  73. package/dist/src/pkg/paginated-cache-registry.d.ts +8 -0
  74. package/dist/src/pkg/paginated-cache-registry.js +23 -0
  75. package/dist/src/pkg/paginated-cache-registry.js.map +1 -0
  76. package/dist/src/pkg/query-helper.d.ts +3 -0
  77. package/dist/src/pkg/query-helper.js +60 -0
  78. package/dist/src/pkg/query-helper.js.map +1 -0
  79. package/dist/src/pkg/referral-tree-utils.d.ts +33 -0
  80. package/dist/src/pkg/referral-tree-utils.js +71 -0
  81. package/dist/src/pkg/referral-tree-utils.js.map +1 -0
  82. package/dist/src/pkg/scripts/index.d.ts +1 -0
  83. package/dist/src/pkg/scripts/index.js +28 -0
  84. package/dist/src/pkg/scripts/index.js.map +1 -0
  85. package/dist/src/pkg/scripts/lua.d.ts +10 -0
  86. package/dist/src/pkg/scripts/lua.js +109 -0
  87. package/dist/src/pkg/scripts/lua.js.map +1 -0
  88. package/dist/src/pkg/sort-helper.d.ts +3 -0
  89. package/dist/src/pkg/sort-helper.js +18 -0
  90. package/dist/src/pkg/sort-helper.js.map +1 -0
  91. package/dist/src/pkg/string-utils.d.ts +10 -0
  92. package/dist/src/pkg/string-utils.js +79 -0
  93. package/dist/src/pkg/string-utils.js.map +1 -0
  94. package/dist/src/pkg/task-helper.d.ts +2 -0
  95. package/dist/src/pkg/task-helper.js +30 -0
  96. package/dist/src/pkg/task-helper.js.map +1 -0
  97. package/dist/src/pkg/trading-pair-helper.d.ts +9 -0
  98. package/dist/src/pkg/trading-pair-helper.js +44 -0
  99. package/dist/src/pkg/trading-pair-helper.js.map +1 -0
  100. package/dist/src/pkg/trading-pair-helper.spec.d.ts +1 -0
  101. package/dist/src/pkg/trading-pair-helper.spec.js +132 -0
  102. package/dist/src/pkg/trading-pair-helper.spec.js.map +1 -0
  103. package/dist/src/pkg/workflow/delayed-task-registry.d.ts +10 -0
  104. package/dist/src/pkg/workflow/delayed-task-registry.js +67 -0
  105. package/dist/src/pkg/workflow/delayed-task-registry.js.map +1 -0
  106. package/dist/src/pkg/workflow/delayed-task.d.ts +18 -0
  107. package/dist/src/pkg/workflow/delayed-task.js +95 -0
  108. package/dist/src/pkg/workflow/delayed-task.js.map +1 -0
  109. package/dist/src/pkg/workflow/index.d.ts +5 -0
  110. package/dist/src/pkg/workflow/index.js +22 -0
  111. package/dist/src/pkg/workflow/index.js.map +1 -0
  112. package/dist/src/pkg/workflow/processing-milestone.d.ts +18 -0
  113. package/dist/src/pkg/workflow/processing-milestone.js +39 -0
  114. package/dist/src/pkg/workflow/processing-milestone.js.map +1 -0
  115. package/dist/src/pkg/workflow/retry-task.d.ts +24 -0
  116. package/dist/src/pkg/workflow/retry-task.js +89 -0
  117. package/dist/src/pkg/workflow/retry-task.js.map +1 -0
  118. package/dist/src/pkg/workflow/retry-task.spec.d.ts +1 -0
  119. package/dist/src/pkg/workflow/retry-task.spec.js +145 -0
  120. package/dist/src/pkg/workflow/retry-task.spec.js.map +1 -0
  121. package/dist/src/pkg/workflow/sync-taskqueue.d.ts +32 -0
  122. package/dist/src/pkg/workflow/sync-taskqueue.js +108 -0
  123. package/dist/src/pkg/workflow/sync-taskqueue.js.map +1 -0
  124. package/dist/src/pkg/worksheet.utils.d.ts +27 -0
  125. package/dist/src/pkg/worksheet.utils.js +116 -0
  126. package/dist/src/pkg/worksheet.utils.js.map +1 -0
  127. package/dist/src/services/audit-service.d.ts +7 -0
  128. package/dist/src/services/audit-service.js +32 -0
  129. package/dist/src/services/audit-service.js.map +1 -0
  130. package/dist/src/services/excel.service.d.ts +25 -0
  131. package/dist/src/services/excel.service.js +95 -0
  132. package/dist/src/services/excel.service.js.map +1 -0
  133. package/dist/src/services/http-service.d.ts +7 -0
  134. package/dist/src/services/http-service.js +67 -0
  135. package/dist/src/services/http-service.js.map +1 -0
  136. package/dist/src/services/index.d.ts +8 -0
  137. package/dist/src/services/index.js +25 -0
  138. package/dist/src/services/index.js.map +1 -0
  139. package/dist/src/services/kafka-service.d.ts +15 -0
  140. package/dist/src/services/kafka-service.js +68 -0
  141. package/dist/src/services/kafka-service.js.map +1 -0
  142. package/dist/src/services/mailer-service.d.ts +15 -0
  143. package/dist/src/services/mailer-service.js +44 -0
  144. package/dist/src/services/mailer-service.js.map +1 -0
  145. package/dist/src/services/paginated-cache.d.ts +16 -0
  146. package/dist/src/services/paginated-cache.js +115 -0
  147. package/dist/src/services/paginated-cache.js.map +1 -0
  148. package/dist/src/services/paginated-cache.spec.d.ts +1 -0
  149. package/dist/src/services/paginated-cache.spec.js +284 -0
  150. package/dist/src/services/paginated-cache.spec.js.map +1 -0
  151. package/dist/src/services/redis-service.d.ts +33 -0
  152. package/dist/src/services/redis-service.js +230 -0
  153. package/dist/src/services/redis-service.js.map +1 -0
  154. package/dist/src/services/security-service.d.ts +11 -0
  155. package/dist/src/services/security-service.js +68 -0
  156. package/dist/src/services/security-service.js.map +1 -0
  157. package/dist/tsconfig.tsbuildinfo +1 -1
  158. package/package.json +1 -1
  159. package/src/constants.ts +66 -0
  160. package/src/gateways/alibaba-cloud-gateway.ts +127 -0
  161. package/src/gateways/http-audit-gateway.ts +104 -0
  162. package/src/gateways/index.ts +5 -0
  163. package/src/gateways/internal-auth-gateway.ts +42 -0
  164. package/src/gateways/stdout-audit-gateway.ts +23 -0
  165. package/src/gateways/webhook-audit-gateway.ts +33 -0
  166. package/src/interfaces.ts +304 -0
  167. package/src/models/audit-log.ts +126 -0
  168. package/src/models/index.ts +1 -0
  169. package/src/pkg/array-helper.ts +7 -0
  170. package/src/pkg/bcrypt-helper.ts +9 -0
  171. package/src/pkg/crypto-helper.ts +18 -0
  172. package/src/pkg/encryption-helper.spec.ts +423 -0
  173. package/src/pkg/encryption-helper.ts +155 -0
  174. package/src/pkg/filter-helper.spec.ts +105 -0
  175. package/src/pkg/filter-helper.ts +139 -0
  176. package/src/pkg/geoip-helper.ts +5 -0
  177. package/src/pkg/hash-helper.ts +12 -0
  178. package/src/pkg/http-request-utils.ts +75 -0
  179. package/src/pkg/index.ts +19 -0
  180. package/src/pkg/key-helper.ts +20 -0
  181. package/src/pkg/logger.ts +23 -0
  182. package/src/pkg/object-helper.ts +42 -0
  183. package/src/pkg/paginated-cache-registry.ts +25 -0
  184. package/src/pkg/query-helper.ts +79 -0
  185. package/src/pkg/referral-tree-utils.ts +165 -0
  186. package/src/pkg/scripts/index.ts +1 -0
  187. package/src/pkg/scripts/lua.ts +112 -0
  188. package/src/pkg/sort-helper.ts +19 -0
  189. package/src/pkg/string-utils.ts +104 -0
  190. package/src/pkg/task-helper.ts +25 -0
  191. package/src/pkg/trading-pair-helper.spec.ts +146 -0
  192. package/src/pkg/trading-pair-helper.ts +78 -0
  193. package/src/pkg/workflow/delayed-task-registry.ts +54 -0
  194. package/src/pkg/workflow/delayed-task.ts +106 -0
  195. package/src/pkg/workflow/index.ts +5 -0
  196. package/src/pkg/workflow/processing-milestone.ts +54 -0
  197. package/src/pkg/workflow/retry-task.spec.ts +194 -0
  198. package/src/pkg/workflow/retry-task.ts +119 -0
  199. package/src/pkg/workflow/sync-taskqueue.ts +118 -0
  200. package/src/pkg/worksheet.utils.ts +178 -0
  201. package/src/services/audit-service.ts +22 -0
  202. package/src/services/excel.service.ts +103 -0
  203. package/src/services/http-service.ts +71 -0
  204. package/src/services/index.ts +8 -0
  205. package/src/services/kafka-service.ts +81 -0
  206. package/src/services/mailer-service.ts +43 -0
  207. package/src/services/paginated-cache.spec.ts +519 -0
  208. package/src/services/paginated-cache.ts +122 -0
  209. package/src/services/redis-service.ts +238 -0
  210. package/src/services/security-service.ts +80 -0
@@ -0,0 +1,519 @@
1
+ import { expect } from "chai";
2
+ import { PaginatedDataCache } from "./paginated-cache";
3
+ import { generateHashFromJSON } from "../pkg/hash-helper";
4
+ import { getVersionCacheKeyForKey } from "../pkg/key-helper";
5
+
6
+ class FakeCacheService {
7
+ protected _cache: Record<string, any> = {};
8
+
9
+ public reset(): void {
10
+ this._cache = {};
11
+ }
12
+
13
+ public async get(key: string): Promise<any> {
14
+ return this._cache[key];
15
+ }
16
+
17
+ public async set(key: string, value: any, ttlInSecs?: number): Promise<void> {
18
+ this._cache[key] = value;
19
+ // Simulate TTL by not implementing it in this fake service
20
+ }
21
+
22
+ public async incrBy(key: string, increment: number): Promise<number> {
23
+ if (this._cache[key] === undefined) {
24
+ this._cache[key] = 0;
25
+ }
26
+ if (typeof this._cache[key] !== "number") {
27
+ throw new Error(`Value for key ${key} is not a number`);
28
+ }
29
+ this._cache[key] += increment;
30
+ return this._cache[key];
31
+ }
32
+
33
+ public async getNumber(key: string): Promise<number | null> {
34
+ const value = this._cache[key];
35
+ if (value === undefined) {
36
+ return null;
37
+ }
38
+
39
+ return typeof value === "number" ? value : Number(value);
40
+ }
41
+ }
42
+
43
+ var cacheService = new FakeCacheService();
44
+ const paginatedDataCache = new PaginatedDataCache("users", cacheService as any);
45
+
46
+ describe("paginated cache", () => {
47
+ beforeEach(() => {
48
+ cacheService.reset();
49
+ });
50
+
51
+ it("should generate cache key correctly", () => {
52
+ const filter = { country: "vietnam", age: 30 };
53
+ const limit = 10;
54
+ const offset = 100;
55
+
56
+ const cacheKey = paginatedDataCache.generateCacheKey(filter, limit, offset);
57
+ const filterHash = generateHashFromJSON(filter);
58
+ const expectedKey = `paginated-cache:users:version:_version:filter:${filterHash}:limit:${limit}:offset:${offset}`;
59
+
60
+ expect(cacheKey).to.equal(
61
+ expectedKey,
62
+ "Cache key should match expected format"
63
+ );
64
+ });
65
+
66
+ it("should set and get cached paginated data successfully", async () => {
67
+ const filter = { country: "vietnam", age: 30 };
68
+ const limit = 10;
69
+ const offset = 100;
70
+ const data = {
71
+ rows: [{ id: 1, name: "Andy" }],
72
+ total: 1,
73
+ limit,
74
+ offset,
75
+ };
76
+
77
+ const cacheKey = paginatedDataCache.generateCacheKey(filter, limit, offset);
78
+
79
+ await paginatedDataCache.setCachedPaginatedData(
80
+ filter,
81
+ limit,
82
+ offset,
83
+ data
84
+ );
85
+
86
+ const currentVersion = await paginatedDataCache.getCurrentVersion();
87
+ expect(currentVersion).to.equal(1, "Initial version should be 1");
88
+
89
+ const cachedData = await paginatedDataCache.getCachedPaginatedData(
90
+ filter,
91
+ limit,
92
+ offset
93
+ );
94
+
95
+ const newVersion = await paginatedDataCache.getCurrentVersion();
96
+
97
+ expect(newVersion).to.equal(1, "Version should still be 1");
98
+ expect(cachedData).to.deep.equal(data, "Cached data should match set data");
99
+ });
100
+
101
+ it("should override existing cache with new data", async () => {
102
+ const filter = { country: "vietnam", age: 30 };
103
+ const limit = 10;
104
+ const offset = 100;
105
+ const data1 = {
106
+ rows: [{ id: 1, name: "Andy" }],
107
+ total: 1,
108
+ limit,
109
+ offset,
110
+ };
111
+ const data2 = {
112
+ rows: [{ id: 2, name: "Bob" }],
113
+ total: 1,
114
+ limit,
115
+ offset,
116
+ };
117
+ const cacheKey = paginatedDataCache.generateCacheKey(filter, limit, offset);
118
+ await paginatedDataCache.setCachedPaginatedData(
119
+ filter,
120
+ limit,
121
+ offset,
122
+ data1
123
+ );
124
+ const initialVersion = await paginatedDataCache.getCurrentVersion();
125
+ expect(initialVersion).to.equal(1, "Initial version should be 1");
126
+ const cachedData1 = await paginatedDataCache.getCachedPaginatedData(
127
+ filter,
128
+ limit,
129
+ offset
130
+ );
131
+ expect(cachedData1).to.deep.equal(
132
+ data1,
133
+ "Initial cached data should match"
134
+ );
135
+ await paginatedDataCache.setCachedPaginatedData(
136
+ filter,
137
+ limit,
138
+ offset,
139
+ data2
140
+ );
141
+ const newVersion = await paginatedDataCache.getCurrentVersion();
142
+ expect(newVersion).to.equal(1, "Version should still be 1 after override");
143
+ const cachedData2 = await paginatedDataCache.getCachedPaginatedData(
144
+ filter,
145
+ limit,
146
+ offset
147
+ );
148
+ expect(cachedData2).to.deep.equal(
149
+ data2,
150
+ "Cached data should match new data after override"
151
+ );
152
+ });
153
+
154
+ it("should invalidate old cache when increment new version", async () => {
155
+ const filter = { country: "vietnam", age: 30 };
156
+ const filter2 = { country: "usa", age: 25 };
157
+ const limit = 10;
158
+ const offset = 100;
159
+ const cacheKey = paginatedDataCache.generateCacheKey(filter, limit, offset);
160
+
161
+ const data1 = {
162
+ rows: [{ id: 1, name: "Andy" }],
163
+ total: 1,
164
+ limit,
165
+ offset,
166
+ };
167
+ const data2 = {
168
+ rows: [{ id: 2, name: "Bob" }],
169
+ total: 1,
170
+ limit,
171
+ offset,
172
+ };
173
+
174
+ await paginatedDataCache.setCachedPaginatedData(
175
+ filter,
176
+ limit,
177
+ offset,
178
+ data1
179
+ );
180
+ await paginatedDataCache.setCachedPaginatedData(
181
+ filter2,
182
+ limit,
183
+ offset,
184
+ data2
185
+ );
186
+
187
+ const initialVersion = await paginatedDataCache.getCurrentVersion();
188
+ expect(initialVersion).to.equal(1, "Initial version should be 1");
189
+
190
+ const cachedData1 = await paginatedDataCache.getCachedPaginatedData(
191
+ filter,
192
+ limit,
193
+ offset
194
+ );
195
+ expect(cachedData1).to.deep.equal(
196
+ data1,
197
+ "Initial cached data should match"
198
+ );
199
+ const cachedData2 = await paginatedDataCache.getCachedPaginatedData(
200
+ filter2,
201
+ limit,
202
+ offset
203
+ );
204
+
205
+ expect(cachedData2).to.deep.equal(
206
+ data2,
207
+ "Initial cached data2 should match"
208
+ );
209
+
210
+ const newVersion = await paginatedDataCache.incrementCacheVersion();
211
+
212
+ expect(newVersion).to.equal(2, "Version should be incremented to 2");
213
+
214
+ const oldData1 = await paginatedDataCache.getCachedPaginatedData(
215
+ filter,
216
+ limit,
217
+ offset
218
+ );
219
+ const oldData2 = await paginatedDataCache.getCachedPaginatedData(
220
+ filter2,
221
+ limit,
222
+ offset
223
+ );
224
+
225
+ expect(oldData1).to.be.null;
226
+ expect(oldData2).to.be.null;
227
+ });
228
+
229
+ it("should give different result for different filters", async () => {
230
+ const filter1 = { country: "vietnam", age: 30 };
231
+ const filter2 = { country: "usa", age: 25 };
232
+ const limit = 10;
233
+ const offset = 100;
234
+
235
+ const cacheKey1 = paginatedDataCache.generateCacheKey(
236
+ filter1,
237
+ limit,
238
+ offset
239
+ );
240
+ const cacheKey2 = paginatedDataCache.generateCacheKey(
241
+ filter2,
242
+ limit,
243
+ offset
244
+ );
245
+
246
+ const data1 = {
247
+ rows: [{ id: 1, name: "Andy" }],
248
+ total: 1,
249
+ limit,
250
+ offset,
251
+ };
252
+
253
+ const data2 = {
254
+ rows: [{ id: 2, name: "Bob" }],
255
+ total: 1,
256
+ limit,
257
+ offset,
258
+ };
259
+
260
+ await paginatedDataCache.setCachedPaginatedData(
261
+ filter1,
262
+ limit,
263
+ offset,
264
+ data1
265
+ );
266
+
267
+ await paginatedDataCache.setCachedPaginatedData(
268
+ filter2,
269
+ limit,
270
+ offset,
271
+ data2
272
+ );
273
+
274
+ const currentVersion = await paginatedDataCache.getCurrentVersion();
275
+ expect(currentVersion).to.equal(1, "Filter2 version should be 1");
276
+
277
+ const cachedData1 = await paginatedDataCache.getCachedPaginatedData(
278
+ filter1,
279
+ limit,
280
+ offset
281
+ );
282
+ const cachedData2 = await paginatedDataCache.getCachedPaginatedData(
283
+ filter2,
284
+ limit,
285
+ offset
286
+ );
287
+
288
+ expect(cachedData1).to.deep.equal(
289
+ data1,
290
+ "Cached data for filter1 should match"
291
+ );
292
+ expect(cachedData2).to.deep.equal(
293
+ data2,
294
+ "Cached data for filter2 should match"
295
+ );
296
+ });
297
+
298
+ it("should paginate data correctly", async () => {
299
+ const data = [
300
+ { id: 1, name: "Andy" },
301
+ { id: 2, name: "Bob" },
302
+ { id: 3, name: "Charlie" },
303
+ { id: 4, name: "David" },
304
+ { id: 5, name: "Eve" },
305
+ ];
306
+
307
+ const filter = { country: "vietnam", age: 30 };
308
+ const limit = 2;
309
+ const offset = 0;
310
+
311
+ const data1 = {
312
+ rows: data.slice(0, limit),
313
+ total: data.length,
314
+ limit,
315
+ offset,
316
+ };
317
+
318
+ const data2 = {
319
+ rows: data.slice(limit, limit * 2),
320
+ total: data.length,
321
+ limit,
322
+ offset: limit,
323
+ };
324
+
325
+ await paginatedDataCache.setCachedPaginatedData(
326
+ filter,
327
+ limit,
328
+ offset,
329
+ data1
330
+ );
331
+
332
+ const currentVersion = await paginatedDataCache.getCurrentVersion();
333
+ expect(currentVersion).to.equal(1, "Initial version should be 1");
334
+
335
+ const cachedData = await paginatedDataCache.getCachedPaginatedData(
336
+ filter,
337
+ limit,
338
+ offset
339
+ );
340
+
341
+ expect(cachedData).to.deep.equal(
342
+ data1,
343
+ "Cached paginated data should match expected data"
344
+ );
345
+ expect(cachedData.rows.length).to.equal(
346
+ limit,
347
+ "Number of rows should match limit"
348
+ );
349
+
350
+ const nextPage = await paginatedDataCache.getCachedPaginatedData(
351
+ filter,
352
+ limit,
353
+ offset + limit
354
+ );
355
+ expect(nextPage).to.be.null;
356
+
357
+ await paginatedDataCache.setCachedPaginatedData(
358
+ filter,
359
+ limit,
360
+ offset + limit,
361
+ data2
362
+ );
363
+
364
+ const nextVersion = await paginatedDataCache.getCurrentVersion();
365
+
366
+ expect(nextVersion).to.equal(1, "Next page version should be 1");
367
+
368
+ const nextCachedData = await paginatedDataCache.getCachedPaginatedData(
369
+ filter,
370
+ limit,
371
+ offset + limit
372
+ );
373
+ expect(nextCachedData).to.deep.equal(
374
+ data2,
375
+ "Next page cached data should match expected data"
376
+ );
377
+
378
+ await paginatedDataCache.incrementCacheVersion();
379
+
380
+ const oldCachedData = await paginatedDataCache.getCachedPaginatedData(
381
+ filter,
382
+ limit,
383
+ offset
384
+ );
385
+ expect(oldCachedData).to.be.null;
386
+
387
+ const oldNextCachedData = await paginatedDataCache.getCachedPaginatedData(
388
+ filter,
389
+ limit,
390
+ offset + limit
391
+ );
392
+ expect(oldNextCachedData).to.be.null;
393
+
394
+ const newData = [
395
+ { id: 1, name: "Andy" },
396
+ { id: 2, name: "Bob" },
397
+ { id: 3, name: "Charlie" },
398
+ { id: 4, name: "David" },
399
+ { id: 5, name: "Eve" },
400
+ { id: 6, name: "Frank" },
401
+ { id: 7, name: "Grace" },
402
+ { id: 8, name: "Hank" },
403
+ { id: 9, name: "Ivy" },
404
+ { id: 10, name: "Jack" },
405
+ ];
406
+
407
+ const newData1 = {
408
+ rows: newData.slice(0, limit),
409
+ total: newData.length,
410
+ limit,
411
+ offset,
412
+ };
413
+
414
+ await paginatedDataCache.setCachedPaginatedData(
415
+ filter,
416
+ limit,
417
+ offset,
418
+ newData1
419
+ );
420
+ const newCurrentVersion = await paginatedDataCache.getCurrentVersion();
421
+ expect(newCurrentVersion).to.equal(2, "New version should be 2");
422
+
423
+ const newCachedData = await paginatedDataCache.getCachedPaginatedData(
424
+ filter,
425
+ limit,
426
+ offset
427
+ );
428
+ expect(newCachedData).to.deep.equal(
429
+ newData1,
430
+ "New cached paginated data should match expected data"
431
+ );
432
+ });
433
+
434
+ it("should gives null if next page data is not set", async () => {
435
+ const filter = { country: "vietnam", age: 30 };
436
+ const limit = 2;
437
+ const offset = 0;
438
+
439
+ const data = [
440
+ { id: 1, name: "Andy" },
441
+ { id: 2, name: "Bob" },
442
+ { id: 3, name: "Charlie" },
443
+ { id: 4, name: "David" },
444
+ { id: 5, name: "Eve" },
445
+ ];
446
+
447
+ await paginatedDataCache.setCachedPaginatedData(filter, limit, offset, {
448
+ rows: data.slice(0, limit),
449
+ total: data.length,
450
+ limit,
451
+ offset,
452
+ });
453
+ const cachedData = await paginatedDataCache.getCachedPaginatedData(
454
+ filter,
455
+ limit,
456
+ offset
457
+ );
458
+ expect(cachedData).to.not.be.null;
459
+ expect(cachedData.rows.length).to.equal(
460
+ limit,
461
+ "Should return correct number of rows"
462
+ );
463
+ expect(cachedData.rows[0].name).to.equal(
464
+ "Andy",
465
+ "First row should be Andy"
466
+ );
467
+
468
+ const nextPageData = await paginatedDataCache.getCachedPaginatedData(
469
+ filter,
470
+ limit,
471
+ offset + limit
472
+ );
473
+ expect(nextPageData).to.be.null;
474
+
475
+ await paginatedDataCache.setCachedPaginatedData(
476
+ filter,
477
+ limit,
478
+ offset + limit,
479
+ {
480
+ rows: data.slice(limit, limit * 2),
481
+ total: data.length,
482
+ limit,
483
+ offset: limit,
484
+ }
485
+ );
486
+
487
+ const nextCachedData = await paginatedDataCache.getCachedPaginatedData(
488
+ filter,
489
+ limit,
490
+ offset + limit
491
+ );
492
+ expect(nextCachedData).to.not.be.null;
493
+ expect(nextCachedData.rows.length).to.equal(
494
+ limit,
495
+ "Next page should return correct number of rows"
496
+ );
497
+ expect(nextCachedData.rows[0].name).to.equal(
498
+ "Charlie",
499
+ "First row of next page should be Charlie"
500
+ );
501
+
502
+ const version = await paginatedDataCache.getCurrentVersion();
503
+ expect(version).to.equal(1, "Next page version should be 1");
504
+
505
+ await paginatedDataCache.incrementCacheVersion();
506
+ const oldCachedData = await paginatedDataCache.getCachedPaginatedData(
507
+ filter,
508
+ limit,
509
+ offset
510
+ );
511
+ expect(oldCachedData).to.be.null;
512
+ const oldNextCachedData = await paginatedDataCache.getCachedPaginatedData(
513
+ filter,
514
+ limit,
515
+ offset + limit
516
+ );
517
+ expect(oldNextCachedData).to.be.null;
518
+ });
519
+ });
@@ -0,0 +1,122 @@
1
+ import {
2
+ getVersionCacheKeyForKey,
3
+ getVersionedCacheKey,
4
+ } from "../pkg/key-helper";
5
+ import {
6
+ CacheService,
7
+ IPaginatedDataCache,
8
+ PaginationResult,
9
+ } from "../interfaces";
10
+ import * as objectHelper from "../pkg/object-helper";
11
+ import { SET_CACHE_POLICY } from "../constants";
12
+ import { generateHashFromJSON } from "../pkg/hash-helper";
13
+
14
+ export class PaginatedDataCache<T = any> implements IPaginatedDataCache<T> {
15
+ constructor(
16
+ protected dataName: string,
17
+ protected cacheService: CacheService
18
+ ) {}
19
+
20
+ public generateCacheKey = (
21
+ filter: Record<string, any> = {},
22
+ limit = 10,
23
+ offset = 0
24
+ ): string => {
25
+ const filterString = generateHashFromJSON(filter);
26
+
27
+ return `paginated-cache:${this.dataName}:version:_version:filter:${filterString}:limit:${limit}:offset:${offset}`;
28
+ };
29
+
30
+ public async getCurrentVersion(): Promise<number> {
31
+ const result = await this.cacheService.getNumber(
32
+ getVersionCacheKeyForKey(this.generateCacheKey())
33
+ );
34
+
35
+ return result || 0;
36
+ }
37
+
38
+ public async getCachedPaginatedData(
39
+ filter: Record<string, any>,
40
+ limit: number,
41
+ offset: number
42
+ ): Promise<PaginationResult<T>> {
43
+ const cacheKey = this.generateCacheKey(filter, limit, offset);
44
+ const { data } = await this._fetchVersionedDataFromCache(cacheKey);
45
+
46
+ return data;
47
+ }
48
+
49
+ public async setCachedPaginatedData(
50
+ filter: Record<string, any>,
51
+ limit: number,
52
+ offset: number,
53
+ data: PaginationResult<T>,
54
+ ttlInSecs = 60 * 30 // Default TTL is 30 minutes
55
+ ): Promise<any> {
56
+ const cacheKey = this.generateCacheKey(filter, limit, offset);
57
+ const currentVersion = await this.getCurrentVersion();
58
+ if (currentVersion === 0) {
59
+ // If current version is 0, we need to increment it first
60
+ await this.incrementCacheVersion(ttlInSecs);
61
+ }
62
+
63
+ return this._setVersionedDataInCache(cacheKey, data, ttlInSecs);
64
+ }
65
+
66
+ public async _fetchVersionedDataFromCache(
67
+ cacheKey: string,
68
+ _overrideVersion?: number
69
+ ): Promise<{ currentVersion: number; data: PaginationResult<T> | null }> {
70
+ // Example: if cacheKey is 'user:123:profile', the version cache key will be 'user:123:profile:version'
71
+ const currentVersion = _overrideVersion ?? (await this.getCurrentVersion());
72
+ if (currentVersion === 0) {
73
+ // Data is not yet set. If data is set, version should be 1
74
+ return {
75
+ currentVersion: 0,
76
+ data: null,
77
+ };
78
+ }
79
+
80
+ // Example: if cacheKey is 'user:123:profile' and version is 1, the versioned cache key will be 'user:123:profile:version:1'
81
+ const versionedCacheKey = getVersionedCacheKey(cacheKey, currentVersion);
82
+ const cachedData = await this.cacheService.get(versionedCacheKey);
83
+ if (cachedData) {
84
+ return {
85
+ currentVersion: currentVersion,
86
+ data: objectHelper.tryParseStringIntoCorrectData(cachedData),
87
+ };
88
+ }
89
+
90
+ // No data means the cache is stale.
91
+ return {
92
+ currentVersion: currentVersion,
93
+ data: null,
94
+ };
95
+ }
96
+
97
+ protected async _setVersionedDataInCache(
98
+ cacheKey: string,
99
+ data: PaginationResult<T>,
100
+ ttl: number = 60 * 30 // Default TTL is 30 minutes
101
+ ): Promise<any> {
102
+ const currentVersion = await this.getCurrentVersion();
103
+
104
+ const versionedCacheKey = getVersionedCacheKey(cacheKey, currentVersion);
105
+ const cacheableData =
106
+ typeof data === "object" ? JSON.stringify(data) : data;
107
+
108
+ // in case two processes try to set the same versioned cache key at the same time,
109
+ // we use IF_NOT_EXISTS policy to avoid overwriting the cache
110
+ return this.cacheService.set(versionedCacheKey, cacheableData, {
111
+ policy: SET_CACHE_POLICY.IF_NOT_EXISTS,
112
+ value: ttl, // Set versioned cache key to expire in 30 minutes
113
+ });
114
+ }
115
+
116
+ public async incrementCacheVersion(ttl = 60 * 60 * 24): Promise<number> {
117
+ const cacheKey = this.generateCacheKey();
118
+ const versionCacheKey = getVersionCacheKeyForKey(cacheKey);
119
+
120
+ return this.cacheService.incrBy(versionCacheKey, 1);
121
+ }
122
+ }