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.
Files changed (57) hide show
  1. package/README.md +38 -0
  2. package/package.json +36 -0
  3. package/pnpm-workspace.yaml +2 -0
  4. package/src/VkuSDK.ts +184 -0
  5. package/src/core/auth/index.ts +1 -0
  6. package/src/core/auth/session-provider.interface.ts +5 -0
  7. package/src/core/cache/cache.interface.ts +6 -0
  8. package/src/core/cache/index.ts +2 -0
  9. package/src/core/cache/infra/file-cache.ts +82 -0
  10. package/src/core/cache/infra/index.ts +3 -0
  11. package/src/core/cache/infra/memory-cache.ts +32 -0
  12. package/src/core/cache/infra/storage-cache.ts +24 -0
  13. package/src/core/executor/index.ts +1 -0
  14. package/src/core/executor/request-executor.ts +20 -0
  15. package/src/core/http/adapters/axios-http-client.ts +25 -0
  16. package/src/core/http/adapters/http-account.decorator.ts +19 -0
  17. package/src/core/http/adapters/http-cache.decorator.ts +55 -0
  18. package/src/core/http/adapters/http-logger.decorator.ts +29 -0
  19. package/src/core/http/adapters/http-retry.decorator.ts +34 -0
  20. package/src/core/http/adapters/index.ts +5 -0
  21. package/src/core/http/base-request.ts +124 -0
  22. package/src/core/http/http-client.interface.ts +5 -0
  23. package/src/core/http/index.ts +3 -0
  24. package/src/core/index.ts +6 -0
  25. package/src/core/parser/index.ts +1 -0
  26. package/src/core/parser/parser.interface.ts +3 -0
  27. package/src/core/utils/html-detect.ts +13 -0
  28. package/src/core/utils/index.ts +1 -0
  29. package/src/index.ts +4 -0
  30. package/src/modules/accounts/domain/account.entity.ts +45 -0
  31. package/src/modules/accounts/domain/index.ts +1 -0
  32. package/src/modules/accounts/index.ts +2 -0
  33. package/src/modules/accounts/infra/account-session.provider.ts +17 -0
  34. package/src/modules/accounts/infra/account.factory.ts +48 -0
  35. package/src/modules/accounts/infra/chrome-cdp-login.ts +62 -0
  36. package/src/modules/accounts/infra/index.ts +3 -0
  37. package/src/modules/enrollment/domain/class-session.entity.ts +13 -0
  38. package/src/modules/enrollment/domain/course-section.entity.ts +19 -0
  39. package/src/modules/enrollment/domain/index.ts +4 -0
  40. package/src/modules/enrollment/domain/subject-action-response.entity.ts +6 -0
  41. package/src/modules/enrollment/domain/subject-class.entity.ts +66 -0
  42. package/src/modules/enrollment/index.ts +3 -0
  43. package/src/modules/enrollment/parsers/class-session.parser.ts +79 -0
  44. package/src/modules/enrollment/parsers/index.ts +2 -0
  45. package/src/modules/enrollment/parsers/subject-class.parser.ts +72 -0
  46. package/src/modules/enrollment/requests/get-registered-subject-class.request.ts +14 -0
  47. package/src/modules/enrollment/requests/get-subject-classes.request.ts +28 -0
  48. package/src/modules/enrollment/requests/index.ts +4 -0
  49. package/src/modules/enrollment/requests/register-subject-class.request.ts +21 -0
  50. package/src/modules/enrollment/requests/unregister-subject-class.request.ts +24 -0
  51. package/src/modules/index.ts +3 -0
  52. package/src/modules/shared/index.ts +2 -0
  53. package/src/modules/shared/infra/chrome-wrapper.ts +380 -0
  54. package/src/modules/shared/infra/index.ts +1 -0
  55. package/src/modules/shared/parsers/html-table.parser.ts +320 -0
  56. package/src/modules/shared/parsers/index.ts +1 -0
  57. 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
+ }