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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import * as cheerio from "cheerio";
|
|
2
|
+
import { IParser } from "../../../core";
|
|
3
|
+
import type { Element } from "domhandler";
|
|
4
|
+
|
|
5
|
+
export interface TableCell {
|
|
6
|
+
[key: string]: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TableCellWithRawHTML extends TableCell {
|
|
10
|
+
rawHTML: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ParserOptions {
|
|
14
|
+
headerRowIndex?: number;
|
|
15
|
+
skipRows?: number[];
|
|
16
|
+
useFirstColumnAsKey?: boolean;
|
|
17
|
+
trimWhitespace?: boolean;
|
|
18
|
+
includeRawHTML?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface CellData {
|
|
22
|
+
value: string;
|
|
23
|
+
rowspan: number;
|
|
24
|
+
colspan: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type CellMatrix = (CellData | null)[][];
|
|
28
|
+
|
|
29
|
+
export class HtmlTableParser implements IParser<
|
|
30
|
+
TableCell[] | TableCellWithRawHTML[]
|
|
31
|
+
> {
|
|
32
|
+
private options: Required<ParserOptions>;
|
|
33
|
+
private rowElements: Map<number, Element>;
|
|
34
|
+
|
|
35
|
+
constructor(options: ParserOptions = {}) {
|
|
36
|
+
this.options = {
|
|
37
|
+
headerRowIndex: options.headerRowIndex ?? 0,
|
|
38
|
+
skipRows: options.skipRows ?? [],
|
|
39
|
+
useFirstColumnAsKey: options.useFirstColumnAsKey ?? false,
|
|
40
|
+
trimWhitespace: options.trimWhitespace ?? true,
|
|
41
|
+
includeRawHTML: options.includeRawHTML ?? true,
|
|
42
|
+
};
|
|
43
|
+
this.rowElements = new Map();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public parse(html: string): TableCell[] | TableCellWithRawHTML[] {
|
|
47
|
+
const $ = cheerio.load(html);
|
|
48
|
+
const table = this.findTable($);
|
|
49
|
+
|
|
50
|
+
if (!table || table.length === 0) {
|
|
51
|
+
throw new Error("No table found in HTML");
|
|
52
|
+
}
|
|
53
|
+
const rows = table.find("tr").toArray();
|
|
54
|
+
if (rows.length === 0) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cellMatrix = this.buildCellMatrix($, rows);
|
|
59
|
+
const headers = this.extractHeaders(cellMatrix);
|
|
60
|
+
const data = this.convertToObjects($, cellMatrix, headers);
|
|
61
|
+
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private findTable($: cheerio.CheerioAPI): cheerio.Cheerio<Element> {
|
|
66
|
+
return $("table").first();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private buildCellMatrix($: cheerio.CheerioAPI, rows: Element[]): CellMatrix {
|
|
70
|
+
const matrix: CellMatrix = [];
|
|
71
|
+
|
|
72
|
+
rows.forEach((row, rowIndex) => {
|
|
73
|
+
if (this.shouldSkipRow(rowIndex)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Store row element for later HTML extraction
|
|
78
|
+
this.rowElements.set(rowIndex, row);
|
|
79
|
+
|
|
80
|
+
this.initializeMatrixRow(matrix, rowIndex);
|
|
81
|
+
|
|
82
|
+
const cells = $(row).find("td, th").toArray();
|
|
83
|
+
this.processCells($, cells, matrix, rowIndex);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return matrix;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if row should be skipped
|
|
91
|
+
*/
|
|
92
|
+
private shouldSkipRow(rowIndex: number): boolean {
|
|
93
|
+
return this.options.skipRows.includes(rowIndex);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize matrix row if not exists
|
|
98
|
+
*/
|
|
99
|
+
private initializeMatrixRow(matrix: CellMatrix, rowIndex: number): void {
|
|
100
|
+
if (!matrix[rowIndex]) {
|
|
101
|
+
matrix[rowIndex] = [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Process all cells in a row
|
|
107
|
+
*/
|
|
108
|
+
private processCells(
|
|
109
|
+
$: cheerio.CheerioAPI,
|
|
110
|
+
cells: Element[],
|
|
111
|
+
matrix: CellMatrix,
|
|
112
|
+
rowIndex: number,
|
|
113
|
+
): void {
|
|
114
|
+
let colIndex = 0;
|
|
115
|
+
|
|
116
|
+
cells.forEach((cell) => {
|
|
117
|
+
colIndex = this.findNextAvailableColumn(matrix, rowIndex, colIndex);
|
|
118
|
+
const cellData = this.extractCellData($, cell);
|
|
119
|
+
this.fillMatrixWithCell(matrix, cellData, rowIndex, colIndex);
|
|
120
|
+
colIndex += cellData.colspan;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find next available column (skip cells occupied by rowspan/colspan)
|
|
126
|
+
*/
|
|
127
|
+
private findNextAvailableColumn(
|
|
128
|
+
matrix: CellMatrix,
|
|
129
|
+
rowIndex: number,
|
|
130
|
+
startCol: number,
|
|
131
|
+
): number {
|
|
132
|
+
let colIndex = startCol;
|
|
133
|
+
while (matrix[rowIndex][colIndex] !== undefined) {
|
|
134
|
+
colIndex++;
|
|
135
|
+
}
|
|
136
|
+
return colIndex;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract cell data including rowspan and colspan
|
|
141
|
+
*/
|
|
142
|
+
private extractCellData($: cheerio.CheerioAPI, cell: Element): CellData {
|
|
143
|
+
const $cell = $(cell);
|
|
144
|
+
const rowspan = parseInt($cell.attr("rowspan") || "1", 10);
|
|
145
|
+
const colspan = parseInt($cell.attr("colspan") || "1", 10);
|
|
146
|
+
let value = $cell.text();
|
|
147
|
+
|
|
148
|
+
if (this.options.trimWhitespace) {
|
|
149
|
+
value = value.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { value, rowspan, colspan };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fill matrix cells affected by rowspan and colspan
|
|
157
|
+
*/
|
|
158
|
+
private fillMatrixWithCell(
|
|
159
|
+
matrix: CellMatrix,
|
|
160
|
+
cellData: CellData,
|
|
161
|
+
startRow: number,
|
|
162
|
+
startCol: number,
|
|
163
|
+
): void {
|
|
164
|
+
for (let r = 0; r < cellData.rowspan; r++) {
|
|
165
|
+
for (let c = 0; c < cellData.colspan; c++) {
|
|
166
|
+
const targetRow = startRow + r;
|
|
167
|
+
const targetCol = startCol + c;
|
|
168
|
+
|
|
169
|
+
this.initializeMatrixRow(matrix, targetRow);
|
|
170
|
+
matrix[targetRow][targetCol] = cellData;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Extract headers from specified row
|
|
177
|
+
*/
|
|
178
|
+
private extractHeaders(matrix: CellMatrix): string[] {
|
|
179
|
+
const headerRow = matrix[this.options.headerRowIndex] || [];
|
|
180
|
+
const headers: string[] = [];
|
|
181
|
+
|
|
182
|
+
headerRow.forEach((cell, index) => {
|
|
183
|
+
if (cell) {
|
|
184
|
+
const uniqueHeader = this.generateUniqueHeader(
|
|
185
|
+
cell.value || `column_${index}`,
|
|
186
|
+
headers,
|
|
187
|
+
);
|
|
188
|
+
headers[index] = uniqueHeader;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return headers;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate unique header name to avoid duplicates
|
|
197
|
+
*/
|
|
198
|
+
private generateUniqueHeader(
|
|
199
|
+
baseName: string,
|
|
200
|
+
existingHeaders: string[],
|
|
201
|
+
): string {
|
|
202
|
+
let uniqueName = baseName;
|
|
203
|
+
let counter = 1;
|
|
204
|
+
|
|
205
|
+
while (existingHeaders.includes(uniqueName)) {
|
|
206
|
+
uniqueName = `${baseName}_${counter}`;
|
|
207
|
+
counter++;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return uniqueName;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Convert cell matrix to array of objects
|
|
215
|
+
*/
|
|
216
|
+
private convertToObjects(
|
|
217
|
+
$: cheerio.CheerioAPI,
|
|
218
|
+
matrix: CellMatrix,
|
|
219
|
+
headers: string[],
|
|
220
|
+
): TableCell[] | TableCellWithRawHTML[] {
|
|
221
|
+
const result: (TableCell | TableCellWithRawHTML)[] = [];
|
|
222
|
+
|
|
223
|
+
for (let rowIndex = 0; rowIndex < matrix.length; rowIndex++) {
|
|
224
|
+
if (this.shouldSkipDataRow(rowIndex)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rowData = this.buildRowObject(matrix[rowIndex], headers);
|
|
229
|
+
|
|
230
|
+
if (this.isValidRow(rowData)) {
|
|
231
|
+
// Add raw HTML if option is enabled
|
|
232
|
+
if (this.options.includeRawHTML && this.rowElements.has(rowIndex)) {
|
|
233
|
+
const rowElement = this.rowElements.get(rowIndex)!;
|
|
234
|
+
const rowWithHTML: TableCellWithRawHTML = {
|
|
235
|
+
...rowData,
|
|
236
|
+
rawHTML: $.html(rowElement),
|
|
237
|
+
};
|
|
238
|
+
result.push(rowWithHTML);
|
|
239
|
+
} else {
|
|
240
|
+
result.push(rowData);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if data row should be skipped
|
|
250
|
+
*/
|
|
251
|
+
private shouldSkipDataRow(rowIndex: number): boolean {
|
|
252
|
+
return (
|
|
253
|
+
rowIndex === this.options.headerRowIndex ||
|
|
254
|
+
this.options.skipRows.includes(rowIndex)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Build single row object from cells
|
|
260
|
+
*/
|
|
261
|
+
private buildRowObject(
|
|
262
|
+
row: (CellData | null)[],
|
|
263
|
+
headers: string[],
|
|
264
|
+
): TableCell {
|
|
265
|
+
const rowData: TableCell = {};
|
|
266
|
+
|
|
267
|
+
row.forEach((cell, colIndex) => {
|
|
268
|
+
if (cell && headers[colIndex]) {
|
|
269
|
+
rowData[headers[colIndex]] = cell.value;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return rowData;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if row has valid data
|
|
278
|
+
*/
|
|
279
|
+
private isValidRow(rowData: TableCell): boolean {
|
|
280
|
+
return Object.keys(rowData).length > 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export class HtmlTableKeyValueParser implements IParser<
|
|
285
|
+
Record<string, TableCell>
|
|
286
|
+
> {
|
|
287
|
+
private baseParser: HtmlTableParser;
|
|
288
|
+
private keyColumn: string | number;
|
|
289
|
+
|
|
290
|
+
constructor(options: ParserOptions & { keyColumn?: string | number } = {}) {
|
|
291
|
+
this.baseParser = new HtmlTableParser(options);
|
|
292
|
+
this.keyColumn = options.keyColumn ?? 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public parse(html: string): Record<string, TableCell> {
|
|
296
|
+
const data = this.baseParser.parse(html);
|
|
297
|
+
return this.convertToKeyValue(data);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private convertToKeyValue(data: TableCell[]): Record<string, TableCell> {
|
|
301
|
+
const result: Record<string, TableCell> = {};
|
|
302
|
+
|
|
303
|
+
data.forEach((row) => {
|
|
304
|
+
const key = this.extractKey(row);
|
|
305
|
+
if (key) {
|
|
306
|
+
result[key] = row;
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private extractKey(row: TableCell): string | null {
|
|
314
|
+
if (typeof this.keyColumn === "number") {
|
|
315
|
+
const keys = Object.keys(row).filter((k) => k !== "rawHTML");
|
|
316
|
+
return keys[this.keyColumn] ? row[keys[this.keyColumn]] : null;
|
|
317
|
+
}
|
|
318
|
+
return row[this.keyColumn] || null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./html-table.parser";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020", // Mã JavaScript đầu ra (ES2020 hoặc cao hơn)
|
|
4
|
+
"module": "NodeNext", // Module system cho Node.js
|
|
5
|
+
"outDir": "./dist", // Thư mục chứa file đã biên dịch
|
|
6
|
+
"rootDir": "./src", // Thư mục chứa mã nguồn TypeScript
|
|
7
|
+
"strict": true, // Bật tất cả các kiểm tra nghiêm ngặt
|
|
8
|
+
"esModuleInterop": true, // Hỗ trợ import các module CommonJS
|
|
9
|
+
"forceConsistentCasingInFileNames": true, // Kiểm tra phân biệt hoa thường tên file
|
|
10
|
+
"skipLibCheck": true, // Bỏ qua kiểm tra type của các thư viện
|
|
11
|
+
"resolveJsonModule": true, // Cho phép import file JSON
|
|
12
|
+
"moduleResolution": "nodenext", // Cách TypeScript tìm module
|
|
13
|
+
"allowJs": true, // Cho phép biên dịch file .js
|
|
14
|
+
"noImplicitAny": true, // Không cho phép kiểu any ngầm định
|
|
15
|
+
"types": ["node"], // Bao gồm type của Node.js
|
|
16
|
+
"sourceMap": false, // Tạo file .map để debug,
|
|
17
|
+
"declaration": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"], // Chỉ biên dịch các file trong src/
|
|
20
|
+
"exclude": ["node_modules", "dist", "*.spec.ts"], // Loại trừ các thư mục này
|
|
21
|
+
}
|