vku-sdk 1.0.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.
- package/README.md +38 -0
- package/package.json +36 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/VkuSDK.ts +184 -0
- package/src/core/auth/index.ts +1 -0
- package/src/core/auth/session-provider.interface.ts +5 -0
- package/src/core/cache/cache.interface.ts +6 -0
- package/src/core/cache/index.ts +2 -0
- package/src/core/cache/infra/file-cache.ts +82 -0
- package/src/core/cache/infra/index.ts +3 -0
- package/src/core/cache/infra/memory-cache.ts +32 -0
- package/src/core/cache/infra/storage-cache.ts +24 -0
- package/src/core/executor/index.ts +1 -0
- package/src/core/executor/request-executor.ts +20 -0
- package/src/core/http/adapters/axios-http-client.ts +25 -0
- package/src/core/http/adapters/http-account.decorator.ts +19 -0
- package/src/core/http/adapters/http-cache.decorator.ts +55 -0
- package/src/core/http/adapters/http-logger.decorator.ts +29 -0
- package/src/core/http/adapters/http-retry.decorator.ts +34 -0
- package/src/core/http/adapters/index.ts +5 -0
- package/src/core/http/base-request.ts +124 -0
- package/src/core/http/http-client.interface.ts +5 -0
- package/src/core/http/index.ts +3 -0
- package/src/core/index.ts +6 -0
- package/src/core/parser/index.ts +1 -0
- package/src/core/parser/parser.interface.ts +3 -0
- package/src/core/utils/html-detect.ts +13 -0
- package/src/core/utils/index.ts +1 -0
- package/src/index.ts +4 -0
- package/src/modules/accounts/domain/account.entity.ts +45 -0
- package/src/modules/accounts/domain/index.ts +1 -0
- package/src/modules/accounts/index.ts +2 -0
- package/src/modules/accounts/infra/account-session.provider.ts +17 -0
- package/src/modules/accounts/infra/account.factory.ts +48 -0
- package/src/modules/accounts/infra/chrome-cdp-login.ts +62 -0
- package/src/modules/accounts/infra/index.ts +3 -0
- package/src/modules/enrollment/domain/class-session.entity.ts +13 -0
- package/src/modules/enrollment/domain/course-section.entity.ts +19 -0
- package/src/modules/enrollment/domain/index.ts +4 -0
- package/src/modules/enrollment/domain/subject-action-response.entity.ts +6 -0
- package/src/modules/enrollment/domain/subject-class.entity.ts +66 -0
- package/src/modules/enrollment/index.ts +3 -0
- package/src/modules/enrollment/parsers/class-session.parser.ts +79 -0
- package/src/modules/enrollment/parsers/index.ts +2 -0
- package/src/modules/enrollment/parsers/subject-class.parser.ts +72 -0
- package/src/modules/enrollment/requests/get-registered-subject-class.request.ts +14 -0
- package/src/modules/enrollment/requests/get-subject-classes.request.ts +28 -0
- package/src/modules/enrollment/requests/index.ts +4 -0
- package/src/modules/enrollment/requests/register-subject-class.request.ts +21 -0
- package/src/modules/enrollment/requests/unregister-subject-class.request.ts +24 -0
- package/src/modules/index.ts +3 -0
- package/src/modules/shared/index.ts +2 -0
- package/src/modules/shared/infra/chrome-wrapper.ts +380 -0
- package/src/modules/shared/infra/index.ts +1 -0
- package/src/modules/shared/parsers/html-table.parser.ts +320 -0
- package/src/modules/shared/parsers/index.ts +1 -0
- package/tsconfig.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# VKU SDK
|
|
2
|
+
Ứng dụng này là một SDK không chính thức dành cho nền tảng VKU (Trang đào tạo trường VKU). Mục tiêu của SDK này là cung cấp các công cụ và phương thức để tương tác với nền tảng VKU một cách dễ dàng và hiệu quả.
|
|
3
|
+
|
|
4
|
+
## Tính năng chính
|
|
5
|
+
- Quản lý tín chỉ gồm: Đăng ký, xóa, lấy danh sách tín chỉ, tín chỉ đã đăng ký.
|
|
6
|
+
|
|
7
|
+
## Cài đặt
|
|
8
|
+
```bash
|
|
9
|
+
npm install vku-sdk
|
|
10
|
+
```
|
|
11
|
+
## Sử dụng ví dụ
|
|
12
|
+
```typescript
|
|
13
|
+
import { FileCache, AccountFactory, AccountHost, CourseSection, VkuSDK } from "./"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const account = AccountFactory.createAccount({
|
|
18
|
+
session: "<your_cookie_or_session_here>",
|
|
19
|
+
host: AccountHost.DAOTAO // or AccountHost.DK1, AccountHost.DK2
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const client = new VkuSDK(account, {
|
|
23
|
+
cache: new FileCache(__dirname + "/.cache") // Optional: Enable caching to improve performance
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const registeredClasses = await client.enrollment.getRegisteredClasses() // Fetch registered classes
|
|
27
|
+
const scanClasses = await client.enrollment.scanSubjectClasses(1, 1000) // 1 to 1000 will covers the entire subject of VKU
|
|
28
|
+
|
|
29
|
+
const courseToRegister = new CourseSection(1, 2, 3) // Replace with actual SubjectClass.enrollmentParams
|
|
30
|
+
await client.enrollment.registerSubjectClass(courseToRegister) // Register for a class
|
|
31
|
+
|
|
32
|
+
console.log("Registered Classes:", registeredClasses)
|
|
33
|
+
console.log("Scanned Classes:", scanClasses)
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main()
|
|
38
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vku-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
|
+
"build": "tsc && tsc-alias",
|
|
10
|
+
"rm:dist": "rimraf dist"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/chrome-remote-interface": "^0.33.0",
|
|
18
|
+
"@types/node": "^25.0.3",
|
|
19
|
+
"chrome-launcher": "^1.2.1",
|
|
20
|
+
"chrome-remote-interface": "^0.33.3",
|
|
21
|
+
"rimraf": "^6.1.2",
|
|
22
|
+
"tsc-alias": "^1.8.16",
|
|
23
|
+
"tsx": "^4.21.0",
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"axios": "^1.13.2",
|
|
28
|
+
"cheerio": "^1.1.2",
|
|
29
|
+
"domhandler": "^5.0.3",
|
|
30
|
+
"node-cache": "^5.1.2"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"chrome-launcher": "^1.2.1",
|
|
34
|
+
"chrome-remote-interface": "^0.33.3"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/VkuSDK.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AxiosHttpClient,
|
|
3
|
+
HttpAccountDecorator,
|
|
4
|
+
HttpCacheDecorator,
|
|
5
|
+
HttpLoggerDecorator,
|
|
6
|
+
HttpRetryDecorator,
|
|
7
|
+
IHttpClient,
|
|
8
|
+
ISessionProvider,
|
|
9
|
+
RequestExecutor,
|
|
10
|
+
} from "./core";
|
|
11
|
+
import { ICache } from "./core/cache";
|
|
12
|
+
import {
|
|
13
|
+
CourseSection,
|
|
14
|
+
GetRegisteredSubjectClassRequest,
|
|
15
|
+
GetSubjectClassesRequest,
|
|
16
|
+
RegisterSubjectClassRequest,
|
|
17
|
+
SubjectClass,
|
|
18
|
+
UnregisterSubjectClassRequest,
|
|
19
|
+
} from "./modules";
|
|
20
|
+
|
|
21
|
+
export interface ClientOptions {
|
|
22
|
+
httpLogging?: boolean;
|
|
23
|
+
cache?: ICache | null;
|
|
24
|
+
retry?: {
|
|
25
|
+
maxRetries: number;
|
|
26
|
+
retryDelay: number;
|
|
27
|
+
} | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default class VkuSDK {
|
|
31
|
+
private executor: RequestExecutor;
|
|
32
|
+
private client: IHttpClient;
|
|
33
|
+
private options: Required<ClientOptions> = {
|
|
34
|
+
httpLogging: false,
|
|
35
|
+
cache: null,
|
|
36
|
+
retry: null,
|
|
37
|
+
};
|
|
38
|
+
private cacheDecorator?: HttpCacheDecorator;
|
|
39
|
+
|
|
40
|
+
constructor(sessionProvider: ISessionProvider, options?: ClientOptions);
|
|
41
|
+
constructor(
|
|
42
|
+
account: ISessionProvider,
|
|
43
|
+
httpClient: IHttpClient,
|
|
44
|
+
options?: ClientOptions,
|
|
45
|
+
);
|
|
46
|
+
constructor(
|
|
47
|
+
account: ISessionProvider,
|
|
48
|
+
httpClient?: IHttpClient | ClientOptions,
|
|
49
|
+
options?: ClientOptions,
|
|
50
|
+
) {
|
|
51
|
+
this.options = {
|
|
52
|
+
...this.options,
|
|
53
|
+
...(isClientOptions(httpClient) ? httpClient : options || {}),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const client =
|
|
57
|
+
httpClient && !isClientOptions(httpClient)
|
|
58
|
+
? httpClient
|
|
59
|
+
: new AxiosHttpClient(account.getHost());
|
|
60
|
+
|
|
61
|
+
let decoratedClient: IHttpClient = new HttpAccountDecorator(
|
|
62
|
+
client,
|
|
63
|
+
account,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (this.options.cache) {
|
|
67
|
+
this.cacheDecorator = new HttpCacheDecorator(
|
|
68
|
+
decoratedClient,
|
|
69
|
+
this.options.cache,
|
|
70
|
+
);
|
|
71
|
+
decoratedClient = this.cacheDecorator;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.options.retry) {
|
|
75
|
+
decoratedClient = new HttpRetryDecorator(
|
|
76
|
+
decoratedClient,
|
|
77
|
+
this.options.retry.maxRetries,
|
|
78
|
+
this.options.retry.retryDelay,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (this.options.httpLogging) {
|
|
83
|
+
decoratedClient = new HttpLoggerDecorator(decoratedClient);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.client = decoratedClient;
|
|
87
|
+
this.executor = new RequestExecutor(this.client);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
readonly enrollment = {
|
|
91
|
+
getSubjectClasses: async (subjectId: number) => {
|
|
92
|
+
const result = await this.executor.execute(
|
|
93
|
+
new GetSubjectClassesRequest(subjectId),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return result.map((subjectClass) =>
|
|
97
|
+
subjectClass.clone({
|
|
98
|
+
id: subjectId.toString(),
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
registerSubjectClass: async (courseSection: CourseSection) => {
|
|
104
|
+
return this.executor.execute(
|
|
105
|
+
new RegisterSubjectClassRequest(courseSection),
|
|
106
|
+
true
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
unregisterSubjectClass: async (subjectRegisteredId: number) => {
|
|
111
|
+
return this.executor.execute(
|
|
112
|
+
new UnregisterSubjectClassRequest(subjectRegisteredId),
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
getRegisteredClasses: async (focusRefresh: boolean = false) => {
|
|
117
|
+
return this.executor.execute(
|
|
118
|
+
new GetRegisteredSubjectClassRequest(),
|
|
119
|
+
focusRefresh,
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
scanSubjectClasses: async (
|
|
124
|
+
minId: number = 1,
|
|
125
|
+
maxId: number = 800,
|
|
126
|
+
refreshCourses: Array<string | number> = [],
|
|
127
|
+
) => {
|
|
128
|
+
const batch = 20;
|
|
129
|
+
const foundClasses: SubjectClass[] = [];
|
|
130
|
+
|
|
131
|
+
for (let i = minId; i <= maxId; i += batch) {
|
|
132
|
+
const promises: Promise<SubjectClass[]>[] = [];
|
|
133
|
+
for (let j = i; j < i + batch && j <= maxId; j++) {
|
|
134
|
+
const needRefresh = this.neededRefreshCacheForSubjectClass(
|
|
135
|
+
j,
|
|
136
|
+
refreshCourses,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
promises.push(
|
|
140
|
+
this.executor
|
|
141
|
+
.execute(new GetSubjectClassesRequest(j), needRefresh)
|
|
142
|
+
.then((classes) =>
|
|
143
|
+
classes.map((c) =>
|
|
144
|
+
c.clone({
|
|
145
|
+
id: j.toString(),
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
.catch(() => []),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const results = await Promise.all(promises);
|
|
154
|
+
for (const classes of results) {
|
|
155
|
+
foundClasses.push(...classes);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return foundClasses;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
private neededRefreshCacheForSubjectClass(
|
|
164
|
+
subjectClassId: string | number,
|
|
165
|
+
refreshCourses: Array<string | number>,
|
|
166
|
+
): boolean {
|
|
167
|
+
if (refreshCourses.length === 1 && refreshCourses[0] === "*") return true;
|
|
168
|
+
return refreshCourses.includes(subjectClassId);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
enableCache(): void {
|
|
172
|
+
this.cacheDecorator?.enableCache();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
disableCache(): void {
|
|
176
|
+
this.cacheDecorator?.disableCache();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isClientOptions(object: unknown): object is ClientOptions {
|
|
181
|
+
if (typeof object !== "object" || object === null) return false;
|
|
182
|
+
const obj = object as ClientOptions;
|
|
183
|
+
return obj.httpLogging === undefined || typeof obj.httpLogging === "boolean";
|
|
184
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./session-provider.interface";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { ICache } from "../cache.interface";
|
|
5
|
+
|
|
6
|
+
interface CachePacket<T> {
|
|
7
|
+
data: T;
|
|
8
|
+
expiry: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class FileCache implements ICache {
|
|
12
|
+
private cacheDir: string;
|
|
13
|
+
|
|
14
|
+
constructor(baseDir: string = "./.cache_data") {
|
|
15
|
+
this.cacheDir = path.resolve(baseDir);
|
|
16
|
+
this.ensureDir();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private async ensureDir() {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(this.cacheDir);
|
|
22
|
+
} catch {
|
|
23
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private getFileName(key: string): string {
|
|
28
|
+
const hash = crypto.createHash("md5").update(key).digest("hex");
|
|
29
|
+
return path.join(this.cacheDir, `${hash}.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async get<T>(key: string): Promise<T | null> {
|
|
33
|
+
const filePath = this.getFileName(key);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
37
|
+
const packet: CachePacket<T> = JSON.parse(raw);
|
|
38
|
+
|
|
39
|
+
if (Date.now() > packet.expiry) {
|
|
40
|
+
await this.delete(key);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return packet.data;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async set<T>(key: string, value: T, ttl: number): Promise<boolean> {
|
|
51
|
+
const filePath = this.getFileName(key);
|
|
52
|
+
const packet: CachePacket<T> = {
|
|
53
|
+
data: value,
|
|
54
|
+
expiry: Date.now() + ttl * 1000,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
await fs.writeFile(filePath, JSON.stringify(packet), "utf-8");
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async delete(key: string): Promise<number> {
|
|
63
|
+
const filePath = this.getFileName(key);
|
|
64
|
+
try {
|
|
65
|
+
await fs.unlink(filePath);
|
|
66
|
+
return 1;
|
|
67
|
+
} catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async clear(): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
const files = await fs.readdir(this.cacheDir);
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
await fs.unlink(path.join(this.cacheDir, file));
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Ignore errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ICache } from "../cache.interface";
|
|
2
|
+
|
|
3
|
+
export class MemoryCache implements ICache {
|
|
4
|
+
constructor(private defaultTTL: number = 300) {}
|
|
5
|
+
private store: Map<string, { value: any; expiresAt: number | null }> =
|
|
6
|
+
new Map();
|
|
7
|
+
async get<T>(key: string): Promise<T | null> {
|
|
8
|
+
const entry = this.store.get(key);
|
|
9
|
+
if (!entry) return null;
|
|
10
|
+
|
|
11
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
12
|
+
this.store.delete(key);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return entry.value as T;
|
|
17
|
+
}
|
|
18
|
+
async set<T>(key: string, value: T, ttl?: number): Promise<boolean> {
|
|
19
|
+
const expiresAt = Date.now() + (ttl ?? this.defaultTTL) * 1000;
|
|
20
|
+
|
|
21
|
+
this.store.set(key, { value, expiresAt });
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
async delete(key: string): Promise<number> {
|
|
26
|
+
return this.store.delete(key) ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async clear(): Promise<void> {
|
|
30
|
+
this.store.clear();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ICache } from "../cache.interface";
|
|
2
|
+
import NodeCache from "node-cache";
|
|
3
|
+
|
|
4
|
+
export class StorageCache implements ICache {
|
|
5
|
+
private cache: NodeCache;
|
|
6
|
+
constructor(
|
|
7
|
+
private options: NodeCache.Options = { stdTTL: 300, checkperiod: 600 },
|
|
8
|
+
) {
|
|
9
|
+
this.cache = new NodeCache(this.options);
|
|
10
|
+
}
|
|
11
|
+
async get<T>(key: string): Promise<T | null> {
|
|
12
|
+
return this.cache.get<T>(key) ?? null;
|
|
13
|
+
}
|
|
14
|
+
async set<T>(key: string, value: T, ttl?: number | string): Promise<boolean> {
|
|
15
|
+
if (ttl) return this.cache.set(key, value, ttl);
|
|
16
|
+
return this.cache.set(key, value);
|
|
17
|
+
}
|
|
18
|
+
async delete(key: string): Promise<number> {
|
|
19
|
+
return this.cache.del(key);
|
|
20
|
+
}
|
|
21
|
+
async clear(): Promise<void> {
|
|
22
|
+
this.cache.flushAll();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./request-executor";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseRequest } from "../http/base-request";
|
|
2
|
+
import { IHttpClient } from "../http/http-client.interface";
|
|
3
|
+
import { HtmlDetect } from "../utils";
|
|
4
|
+
|
|
5
|
+
export class RequestExecutor {
|
|
6
|
+
constructor(private client: IHttpClient) {}
|
|
7
|
+
|
|
8
|
+
async execute<T>(request: BaseRequest<T>, focusRefresh: boolean = false) {
|
|
9
|
+
const httpRequest = request.buildRequest();
|
|
10
|
+
if (focusRefresh) httpRequest.disableCache();
|
|
11
|
+
|
|
12
|
+
const response = await this.client.send(httpRequest);
|
|
13
|
+
|
|
14
|
+
if (httpRequest.isAuthRequired() && HtmlDetect.isLoginPage(response))
|
|
15
|
+
throw new Error("Authentication required but session is invalid.");
|
|
16
|
+
|
|
17
|
+
const parser = request.getParser();
|
|
18
|
+
return parser.parse(response);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AxiosInstance } from "axios";
|
|
2
|
+
import { HttpRequest } from "../base-request";
|
|
3
|
+
import { IHttpClient } from "../http-client.interface";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
|
|
6
|
+
export class AxiosHttpClient implements IHttpClient {
|
|
7
|
+
private instance: AxiosInstance;
|
|
8
|
+
constructor(baseURL?: string) {
|
|
9
|
+
this.instance = axios.create({
|
|
10
|
+
baseURL: baseURL,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async send(request: HttpRequest): Promise<string> {
|
|
15
|
+
const { data } = await this.instance.request<string>({
|
|
16
|
+
url: request.getUrl(),
|
|
17
|
+
method: request.getMethod(),
|
|
18
|
+
headers: request.getHeaders(),
|
|
19
|
+
params: request.getParams(),
|
|
20
|
+
data: request.getBody(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ISessionProvider } from "../../auth/session-provider.interface";
|
|
2
|
+
import { HttpRequest } from "../base-request";
|
|
3
|
+
import { IHttpClient } from "../http-client.interface";
|
|
4
|
+
|
|
5
|
+
export class HttpAccountDecorator implements IHttpClient {
|
|
6
|
+
constructor(
|
|
7
|
+
private delegate: IHttpClient,
|
|
8
|
+
private sessionProvider: ISessionProvider,
|
|
9
|
+
) {}
|
|
10
|
+
async send(request: HttpRequest): Promise<string> {
|
|
11
|
+
if (request.isAuthRequired() && !this.sessionProvider.isAuthenticated())
|
|
12
|
+
throw new Error("Authentication required but no session available.");
|
|
13
|
+
const header = await this.sessionProvider.getAuthHeader();
|
|
14
|
+
request.mergeHeaders(header);
|
|
15
|
+
request.setHost(this.sessionProvider.getHost());
|
|
16
|
+
|
|
17
|
+
return this.delegate.send(request);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ICache } from "../../cache";
|
|
2
|
+
import { HtmlDetect } from "../../utils";
|
|
3
|
+
import { HttpMethod, HttpRequest } from "../base-request";
|
|
4
|
+
import { IHttpClient } from "../http-client.interface";
|
|
5
|
+
|
|
6
|
+
export class HttpCacheDecorator implements IHttpClient {
|
|
7
|
+
private isEnabledCache: boolean = true;
|
|
8
|
+
constructor(
|
|
9
|
+
private delegate: IHttpClient,
|
|
10
|
+
private cache: ICache,
|
|
11
|
+
private defaultTTL: number = 120
|
|
12
|
+
) {}
|
|
13
|
+
async send(request: HttpRequest): Promise<string> {
|
|
14
|
+
if (
|
|
15
|
+
request.getMethod() !== HttpMethod.GET ||
|
|
16
|
+
!this.isEnabledCache ||
|
|
17
|
+
request.isNoCache()
|
|
18
|
+
) {
|
|
19
|
+
return this.delegate.send(request);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cacheKey = this.generateCacheKey(request);
|
|
23
|
+
|
|
24
|
+
const cachedResponse = await this.cache.get<string>(cacheKey);
|
|
25
|
+
if (cachedResponse) {
|
|
26
|
+
return cachedResponse;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const response = await this.delegate.send(request);
|
|
30
|
+
|
|
31
|
+
if (!HtmlDetect.isLoginPage(response)) {
|
|
32
|
+
await this.cache.set<string>(cacheKey, response, this.defaultTTL);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private generateCacheKey(request: HttpRequest): string {
|
|
39
|
+
const url = request.getUrl();
|
|
40
|
+
const params = request.getParams();
|
|
41
|
+
const paramString = Object.keys(params)
|
|
42
|
+
.sort()
|
|
43
|
+
.map((key) => `${key}=${params[key]}`)
|
|
44
|
+
.join("&");
|
|
45
|
+
return paramString ? `${url}?${paramString}` : url;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
enableCache(): void {
|
|
49
|
+
this.isEnabledCache = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
disableCache(): void {
|
|
53
|
+
this.isEnabledCache = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HttpRequest } from "../base-request";
|
|
2
|
+
import { IHttpClient } from "../http-client.interface";
|
|
3
|
+
|
|
4
|
+
export interface IHttpLoggerDecoratorOptions {}
|
|
5
|
+
|
|
6
|
+
export class HttpLoggerDecorator implements IHttpClient {
|
|
7
|
+
constructor(
|
|
8
|
+
private delegate: IHttpClient,
|
|
9
|
+
options: IHttpLoggerDecoratorOptions = {},
|
|
10
|
+
) {}
|
|
11
|
+
async send(request: HttpRequest): Promise<string> {
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
try {
|
|
14
|
+
const response = await this.delegate.send(request);
|
|
15
|
+
const duration = Date.now() - startTime;
|
|
16
|
+
console.log(
|
|
17
|
+
`[HttpLoggerDecorator]: ${request.getMethod()} ${request.getUrl()} success - ${duration}ms\n\n`,
|
|
18
|
+
response.slice(0, 500) + "\n",
|
|
19
|
+
);
|
|
20
|
+
return response;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const duration = Date.now() - startTime;
|
|
23
|
+
console.log(
|
|
24
|
+
`[HttpLoggerDecorator]: ${request.getMethod()} ${request.getUrl()} - Failed after ${duration}ms: ${(error as any).message}`,
|
|
25
|
+
);
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { HttpRequest } from "../base-request";
|
|
2
|
+
import { IHttpClient } from "../http-client.interface";
|
|
3
|
+
|
|
4
|
+
export class HttpRetryDecorator implements IHttpClient {
|
|
5
|
+
constructor(
|
|
6
|
+
private delegate: IHttpClient,
|
|
7
|
+
private maxRetries: number = 3,
|
|
8
|
+
private retryDelay: number = 300,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
async send(request: HttpRequest): Promise<string> {
|
|
12
|
+
let attempt = 0;
|
|
13
|
+
let lastError: any;
|
|
14
|
+
|
|
15
|
+
while (attempt < this.maxRetries) {
|
|
16
|
+
try {
|
|
17
|
+
return await this.delegate.send(request);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
lastError = error;
|
|
20
|
+
attempt++;
|
|
21
|
+
console.warn(
|
|
22
|
+
`[HttpRetryDecorator]: Attempt ${attempt} failed. Retrying in ${this.retryDelay}ms...`,
|
|
23
|
+
);
|
|
24
|
+
await new Promise((resolve) => setTimeout(resolve, this.retryDelay));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.error(
|
|
29
|
+
`[HttpRetryDecorator]: All ${this.maxRetries} attempts failed.`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
throw lastError;
|
|
33
|
+
}
|
|
34
|
+
}
|