mvc-common-toolkit 1.43.10 → 1.43.11
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/src/constants.d.ts +42 -0
- package/dist/src/constants.js +50 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/gateways/alibaba-cloud-gateway.d.ts +30 -0
- package/dist/src/gateways/alibaba-cloud-gateway.js +120 -0
- package/dist/src/gateways/alibaba-cloud-gateway.js.map +1 -0
- package/dist/src/gateways/http-audit-gateway.d.ts +20 -0
- package/dist/src/gateways/http-audit-gateway.js +76 -0
- package/dist/src/gateways/http-audit-gateway.js.map +1 -0
- package/dist/src/gateways/index.d.ts +5 -0
- package/dist/src/gateways/index.js +22 -0
- package/dist/src/gateways/index.js.map +1 -0
- package/dist/src/gateways/internal-auth-gateway.d.ts +8 -0
- package/dist/src/gateways/internal-auth-gateway.js +40 -0
- package/dist/src/gateways/internal-auth-gateway.js.map +1 -0
- package/dist/src/gateways/stdout-audit-gateway.d.ts +7 -0
- package/dist/src/gateways/stdout-audit-gateway.js +25 -0
- package/dist/src/gateways/stdout-audit-gateway.js.map +1 -0
- package/dist/src/gateways/webhook-audit-gateway.d.ts +14 -0
- package/dist/src/gateways/webhook-audit-gateway.js +27 -0
- package/dist/src/gateways/webhook-audit-gateway.js.map +1 -0
- package/dist/src/interfaces.d.ts +218 -0
- package/dist/src/interfaces.js +3 -0
- package/dist/src/interfaces.js.map +1 -0
- package/dist/src/models/audit-log.d.ts +77 -0
- package/dist/src/models/audit-log.js +92 -0
- package/dist/src/models/audit-log.js.map +1 -0
- package/dist/src/models/index.d.ts +1 -0
- package/dist/src/models/index.js +18 -0
- package/dist/src/models/index.js.map +1 -0
- package/dist/src/pkg/array-helper.d.ts +1 -0
- package/dist/src/pkg/array-helper.js +12 -0
- package/dist/src/pkg/array-helper.js.map +1 -0
- package/dist/src/pkg/bcrypt-helper.d.ts +2 -0
- package/dist/src/pkg/bcrypt-helper.js +36 -0
- package/dist/src/pkg/bcrypt-helper.js.map +1 -0
- package/dist/src/pkg/crypto-helper.d.ts +2 -0
- package/dist/src/pkg/crypto-helper.js +16 -0
- package/dist/src/pkg/crypto-helper.js.map +1 -0
- package/dist/src/pkg/encryption-helper.d.ts +18 -0
- package/dist/src/pkg/encryption-helper.js +89 -0
- package/dist/src/pkg/encryption-helper.js.map +1 -0
- package/dist/src/pkg/encryption-helper.spec.d.ts +1 -0
- package/dist/src/pkg/encryption-helper.spec.js +238 -0
- package/dist/src/pkg/encryption-helper.spec.js.map +1 -0
- package/dist/src/pkg/filter-helper.d.ts +2 -0
- package/dist/src/pkg/filter-helper.js +102 -0
- package/dist/src/pkg/filter-helper.js.map +1 -0
- package/dist/src/pkg/filter-helper.spec.d.ts +1 -0
- package/dist/src/pkg/filter-helper.spec.js +94 -0
- package/dist/src/pkg/filter-helper.spec.js.map +1 -0
- package/dist/src/pkg/geoip-helper.d.ts +2 -0
- package/dist/src/pkg/geoip-helper.js +32 -0
- package/dist/src/pkg/geoip-helper.js.map +1 -0
- package/dist/src/pkg/hash-helper.d.ts +1 -0
- package/dist/src/pkg/hash-helper.js +37 -0
- package/dist/src/pkg/hash-helper.js.map +1 -0
- package/dist/src/pkg/http-request-utils.d.ts +4 -0
- package/dist/src/pkg/http-request-utils.js +55 -0
- package/dist/src/pkg/http-request-utils.js.map +1 -0
- package/dist/src/pkg/index.d.ts +18 -0
- package/dist/src/pkg/index.js +45 -0
- package/dist/src/pkg/index.js.map +1 -0
- package/dist/src/pkg/key-helper.d.ts +2 -0
- package/dist/src/pkg/key-helper.js +20 -0
- package/dist/src/pkg/key-helper.js.map +1 -0
- package/dist/src/pkg/logger.d.ts +9 -0
- package/dist/src/pkg/logger.js +23 -0
- package/dist/src/pkg/logger.js.map +1 -0
- package/dist/src/pkg/object-helper.d.ts +2 -0
- package/dist/src/pkg/object-helper.js +37 -0
- package/dist/src/pkg/object-helper.js.map +1 -0
- package/dist/src/pkg/paginated-cache-registry.d.ts +8 -0
- package/dist/src/pkg/paginated-cache-registry.js +23 -0
- package/dist/src/pkg/paginated-cache-registry.js.map +1 -0
- package/dist/src/pkg/query-helper.d.ts +3 -0
- package/dist/src/pkg/query-helper.js +60 -0
- package/dist/src/pkg/query-helper.js.map +1 -0
- package/dist/src/pkg/referral-tree-utils.d.ts +33 -0
- package/dist/src/pkg/referral-tree-utils.js +71 -0
- package/dist/src/pkg/referral-tree-utils.js.map +1 -0
- package/dist/src/pkg/scripts/index.d.ts +1 -0
- package/dist/src/pkg/scripts/index.js +28 -0
- package/dist/src/pkg/scripts/index.js.map +1 -0
- package/dist/src/pkg/scripts/lua.d.ts +10 -0
- package/dist/src/pkg/scripts/lua.js +109 -0
- package/dist/src/pkg/scripts/lua.js.map +1 -0
- package/dist/src/pkg/sort-helper.d.ts +3 -0
- package/dist/src/pkg/sort-helper.js +18 -0
- package/dist/src/pkg/sort-helper.js.map +1 -0
- package/dist/src/pkg/string-utils.d.ts +10 -0
- package/dist/src/pkg/string-utils.js +79 -0
- package/dist/src/pkg/string-utils.js.map +1 -0
- package/dist/src/pkg/task-helper.d.ts +2 -0
- package/dist/src/pkg/task-helper.js +30 -0
- package/dist/src/pkg/task-helper.js.map +1 -0
- package/dist/src/pkg/workflow/delayed-task-registry.d.ts +10 -0
- package/dist/src/pkg/workflow/delayed-task-registry.js +67 -0
- package/dist/src/pkg/workflow/delayed-task-registry.js.map +1 -0
- package/dist/src/pkg/workflow/delayed-task.d.ts +18 -0
- package/dist/src/pkg/workflow/delayed-task.js +95 -0
- package/dist/src/pkg/workflow/delayed-task.js.map +1 -0
- package/dist/src/pkg/workflow/index.d.ts +5 -0
- package/dist/src/pkg/workflow/index.js +22 -0
- package/dist/src/pkg/workflow/index.js.map +1 -0
- package/dist/src/pkg/workflow/processing-milestone.d.ts +18 -0
- package/dist/src/pkg/workflow/processing-milestone.js +39 -0
- package/dist/src/pkg/workflow/processing-milestone.js.map +1 -0
- package/dist/src/pkg/workflow/retry-task.d.ts +24 -0
- package/dist/src/pkg/workflow/retry-task.js +89 -0
- package/dist/src/pkg/workflow/retry-task.js.map +1 -0
- package/dist/src/pkg/workflow/retry-task.spec.d.ts +1 -0
- package/dist/src/pkg/workflow/retry-task.spec.js +145 -0
- package/dist/src/pkg/workflow/retry-task.spec.js.map +1 -0
- package/dist/src/pkg/workflow/sync-taskqueue.d.ts +32 -0
- package/dist/src/pkg/workflow/sync-taskqueue.js +108 -0
- package/dist/src/pkg/workflow/sync-taskqueue.js.map +1 -0
- package/dist/src/pkg/worksheet.utils.d.ts +27 -0
- package/dist/src/pkg/worksheet.utils.js +116 -0
- package/dist/src/pkg/worksheet.utils.js.map +1 -0
- package/dist/src/services/audit-service.d.ts +7 -0
- package/dist/src/services/audit-service.js +32 -0
- package/dist/src/services/audit-service.js.map +1 -0
- package/dist/src/services/excel.service.d.ts +25 -0
- package/dist/src/services/excel.service.js +95 -0
- package/dist/src/services/excel.service.js.map +1 -0
- package/dist/src/services/http-service.d.ts +7 -0
- package/dist/src/services/http-service.js +67 -0
- package/dist/src/services/http-service.js.map +1 -0
- package/dist/src/services/index.d.ts +8 -0
- package/dist/src/services/index.js +25 -0
- package/dist/src/services/index.js.map +1 -0
- package/dist/src/services/kafka-service.d.ts +15 -0
- package/dist/src/services/kafka-service.js +68 -0
- package/dist/src/services/kafka-service.js.map +1 -0
- package/dist/src/services/mailer-service.d.ts +15 -0
- package/dist/src/services/mailer-service.js +44 -0
- package/dist/src/services/mailer-service.js.map +1 -0
- package/dist/src/services/paginated-cache.d.ts +16 -0
- package/dist/src/services/paginated-cache.js +115 -0
- package/dist/src/services/paginated-cache.js.map +1 -0
- package/dist/src/services/paginated-cache.spec.d.ts +1 -0
- package/dist/src/services/paginated-cache.spec.js +284 -0
- package/dist/src/services/paginated-cache.spec.js.map +1 -0
- package/dist/src/services/redis-service.d.ts +33 -0
- package/dist/src/services/redis-service.js +230 -0
- package/dist/src/services/redis-service.js.map +1 -0
- package/dist/src/services/security-service.d.ts +11 -0
- package/dist/src/services/security-service.js +68 -0
- package/dist/src/services/security-service.js.map +1 -0
- package/package.json +1 -1
- package/src/constants.ts +47 -0
- package/src/gateways/alibaba-cloud-gateway.ts +127 -0
- package/src/gateways/http-audit-gateway.ts +104 -0
- package/src/gateways/index.ts +5 -0
- package/src/gateways/internal-auth-gateway.ts +42 -0
- package/src/gateways/stdout-audit-gateway.ts +23 -0
- package/src/gateways/webhook-audit-gateway.ts +33 -0
- package/src/interfaces.ts +304 -0
- package/src/models/audit-log.ts +126 -0
- package/src/models/index.ts +1 -0
- package/src/pkg/array-helper.ts +7 -0
- package/src/pkg/bcrypt-helper.ts +9 -0
- package/src/pkg/crypto-helper.ts +18 -0
- package/src/pkg/encryption-helper.spec.ts +423 -0
- package/src/pkg/encryption-helper.ts +155 -0
- package/src/pkg/filter-helper.spec.ts +105 -0
- package/src/pkg/filter-helper.ts +139 -0
- package/src/pkg/geoip-helper.ts +5 -0
- package/src/pkg/hash-helper.ts +12 -0
- package/src/pkg/http-request-utils.ts +75 -0
- package/src/pkg/index.ts +18 -0
- package/src/pkg/key-helper.ts +20 -0
- package/src/pkg/logger.ts +23 -0
- package/src/pkg/object-helper.ts +42 -0
- package/src/pkg/paginated-cache-registry.ts +25 -0
- package/src/pkg/query-helper.ts +79 -0
- package/src/pkg/referral-tree-utils.ts +165 -0
- package/src/pkg/scripts/index.ts +1 -0
- package/src/pkg/scripts/lua.ts +112 -0
- package/src/pkg/sort-helper.ts +19 -0
- package/src/pkg/string-utils.ts +104 -0
- package/src/pkg/task-helper.ts +25 -0
- package/src/pkg/workflow/delayed-task-registry.ts +54 -0
- package/src/pkg/workflow/delayed-task.ts +106 -0
- package/src/pkg/workflow/index.ts +5 -0
- package/src/pkg/workflow/processing-milestone.ts +54 -0
- package/src/pkg/workflow/retry-task.spec.ts +194 -0
- package/src/pkg/workflow/retry-task.ts +119 -0
- package/src/pkg/workflow/sync-taskqueue.ts +118 -0
- package/src/pkg/worksheet.utils.ts +178 -0
- package/src/services/audit-service.ts +22 -0
- package/src/services/excel.service.ts +103 -0
- package/src/services/http-service.ts +71 -0
- package/src/services/index.ts +8 -0
- package/src/services/kafka-service.ts +81 -0
- package/src/services/mailer-service.ts +43 -0
- package/src/services/paginated-cache.spec.ts +519 -0
- package/src/services/paginated-cache.ts +122 -0
- package/src/services/redis-service.ts +238 -0
- package/src/services/security-service.ts +80 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EmailSender, SendMailOptions } from "../interfaces";
|
|
2
|
+
|
|
3
|
+
export interface MailServiceConfig {
|
|
4
|
+
adminEmails?: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class MailService {
|
|
8
|
+
protected adminEmails: string[];
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
protected transporter: EmailSender,
|
|
12
|
+
protected config: MailServiceConfig
|
|
13
|
+
) {
|
|
14
|
+
this.adminEmails = config?.adminEmails || [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public async sendToAdmin(mailOptions: SendMailOptions) {
|
|
18
|
+
if (this.adminEmails.length === 0) {
|
|
19
|
+
throw new Error("admin email list is empty");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
mailOptions.to = this.adminEmails;
|
|
23
|
+
|
|
24
|
+
return this.send(mailOptions);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async send(
|
|
28
|
+
mailOptions: SendMailOptions,
|
|
29
|
+
ccOptions = { ccToAdmin: false, bccToAdmin: false }
|
|
30
|
+
) {
|
|
31
|
+
const message = { from: process.env.SMTP_MAIL_FROM, ...mailOptions };
|
|
32
|
+
const { ccToAdmin, bccToAdmin } = ccOptions;
|
|
33
|
+
|
|
34
|
+
if (ccToAdmin && this.adminEmails.length > 0) message.cc = this.adminEmails;
|
|
35
|
+
if (bccToAdmin && this.adminEmails.length > 0)
|
|
36
|
+
message.bcc = this.adminEmails;
|
|
37
|
+
|
|
38
|
+
return this.transporter.send(message).catch((e) => {
|
|
39
|
+
console.error("Send mail error", e);
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -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
|
+
}
|