@fastify-core/base 1.0.7 → 1.0.8
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 +466 -136
- package/dist/index.d.mts +1846 -272
- package/dist/index.mjs +1 -1
- package/package.json +71 -57
package/dist/index.d.mts
CHANGED
|
@@ -1,18 +1,173 @@
|
|
|
1
1
|
import * as Decimal from "decimal.js";
|
|
2
|
-
import { Pool, PoolConnection } from "mysql2/promise";
|
|
3
|
-
import { FastifyRequest } from "fastify";
|
|
2
|
+
import { Pool, PoolConnection, RowDataPacket } from "mysql2/promise";
|
|
3
|
+
import { FastifyRequest, Session } from "fastify";
|
|
4
4
|
import { SessionStore } from "@fastify/session";
|
|
5
|
+
//#region src/Sql.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Các kiểu và hàm dùng để xây dựng SQL từ định danh.
|
|
8
|
+
*
|
|
9
|
+
* Identifier động luôn được escape bằng backtick. Riêng SELECT và điều kiện
|
|
10
|
+
* JOIN được giới hạn ở column reference đơn giản, không nhận SQL thô.
|
|
11
|
+
*/
|
|
12
|
+
type SqlSelectField = string | {
|
|
13
|
+
column: string;
|
|
14
|
+
alias?: string;
|
|
15
|
+
};
|
|
16
|
+
type SqlSelectInput = string | readonly SqlSelectField[];
|
|
17
|
+
type SqlJoinOperator = "=" | "<>" | "!=" | "<=" | ">=" | "<" | ">";
|
|
18
|
+
type SqlJoinCondition = string | {
|
|
19
|
+
left: string;
|
|
20
|
+
right: string;
|
|
21
|
+
operator?: SqlJoinOperator;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Escape identifier đơn theo chuẩn MySQL bằng cách nhân đôi backtick bên trong.
|
|
25
|
+
* Từ chối tên rỗng, chứa NUL hoặc vượt giới hạn của MySQL.
|
|
26
|
+
*/
|
|
27
|
+
declare function escapeIdentifier(name: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Escape từng phần của một đường dẫn, ví dụ `users.id` thành
|
|
30
|
+
* `` `users`.`id` ``. Giá trị chỉ được dùng làm identifier, không phải SQL.
|
|
31
|
+
*/
|
|
32
|
+
declare function escapeQualifiedName(path: string): string;
|
|
33
|
+
/** Escape một column reference hợp lệ dùng cho SELECT hoặc JOIN. */
|
|
34
|
+
declare function escapeColumnReference(reference: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Chuẩn hóa và kiểm tra tên JOIN table để giữ nguyên alias được truyền vào.
|
|
37
|
+
*
|
|
38
|
+
* - Tên đơn được escape thành backtick.
|
|
39
|
+
* - Tên có alias được ghi thành `` `table` `alias` ``; alias vẫn được escape.
|
|
40
|
+
*
|
|
41
|
+
* @param tableAndAlias Chuỗi `table` hoặc `table AS alias`
|
|
42
|
+
* @throws {Error} `invalid_join_table` khi thiếu/sai định dạng hoặc alias
|
|
43
|
+
*/
|
|
44
|
+
declare function buildJoinTable(tableAndAlias: string): string;
|
|
45
|
+
/** Chuẩn hóa và kiểm tra chiều ORDER BY. */
|
|
46
|
+
declare function assertOrderDirection(direction: string): "ASC" | "DESC";
|
|
47
|
+
/** Chuẩn hóa loại JOIN; giá trị thiếu mặc định thành LEFT JOIN. */
|
|
48
|
+
declare function assertJoinType(type?: string | null): string;
|
|
49
|
+
/**
|
|
50
|
+
* Xây dựng danh sách cột của mệnh đề SELECT.
|
|
51
|
+
*
|
|
52
|
+
* Chấp nhận:
|
|
53
|
+
* - Chuỗi: `"id"`, `"users.id, roles.name"`.
|
|
54
|
+
* - Mảng chuỗi: `["id", { column: "users.name", alias: "userName" }]`.
|
|
55
|
+
* - `{ column, alias? }` để đặt tên cột trả về.
|
|
56
|
+
*
|
|
57
|
+
* Chỉ nhận tên cột hoặc column reference; aggregate, hàm, subquery và SQL
|
|
58
|
+
* thô đều bị từ chối bằng `invalid_column_reference`.
|
|
59
|
+
*
|
|
60
|
+
* @throws {Error} `invalid_select` khi danh sách rỗng
|
|
61
|
+
* @throws {Error} `invalid_column_reference` khi có tên cột không hợp lệ
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* buildSelectList(["id", { column: "roles.name", alias: "roleName" }]);
|
|
66
|
+
* // `id`, `roles`.`name` AS `roleName`
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function buildSelectList(select: SqlSelectInput): string;
|
|
70
|
+
/**
|
|
71
|
+
* Xây dựng điều kiện JOIN mà không nối SQL thô.
|
|
72
|
+
*
|
|
73
|
+
* Hỗ trợ `"users.id = roles.userId"` và `{ left, right, operator? }` với
|
|
74
|
+
* operator thuộc `=`, `<>`, `!=`, `<=`, `>=`, `<`, `>`. Subquery, function,
|
|
75
|
+
* comment, dấu `;` và biểu thức nhiều phép so sánh đều bị từ chối.
|
|
76
|
+
*
|
|
77
|
+
* @throws {Error} `invalid_join_condition` khi cú pháp không hợp lệ
|
|
78
|
+
*/
|
|
79
|
+
declare function buildJoinCondition(condition: SqlJoinCondition): string;
|
|
80
|
+
//#endregion
|
|
5
81
|
//#region src/BaseModel.d.ts
|
|
6
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Ràng buộc tối thiểu cho entity của model: bắt buộc có khoá chính `id`.
|
|
84
|
+
*
|
|
85
|
+
* Cố ý KHÔNG dùng `Record<string, unknown>` ở đây. Một `interface` khai báo
|
|
86
|
+
* kiểu thường (`interface UserItem { id: number; ... }`) không có implicit
|
|
87
|
+
* index signature nên sẽ không thoả `Record<string, unknown>`, dẫn đến lỗi
|
|
88
|
+
* `TS2344` cho đúng cách khai báo entity mà README hướng dẫn.
|
|
89
|
+
*/
|
|
90
|
+
type ModelEntity = {
|
|
91
|
+
id: number;
|
|
92
|
+
};
|
|
93
|
+
/** Dòng dữ liệu dạng generic cho các model chưa có kiểu entity cụ thể. */
|
|
94
|
+
type ModelRow = Record<string, unknown> & ModelEntity;
|
|
95
|
+
/** Giá trị được phép truyền trực tiếp cho placeholder (`?`) của mysql2. */
|
|
96
|
+
type SqlParam = string | number | boolean | null | Date | Buffer | SqlParam[];
|
|
97
|
+
/** Giá trị "rộng" mà bộ lọc động và các hàm CRUD chấp nhận. */
|
|
98
|
+
type LooseValue = string | number | boolean | null | Date | Buffer | LooseValue[] | {
|
|
99
|
+
[key: string]: LooseValue;
|
|
100
|
+
};
|
|
101
|
+
type LooseRecord = Record<string, LooseValue>;
|
|
102
|
+
/**
|
|
103
|
+
* Thu hẹp biến `unknown` trong `catch` về kiểu `Error` để đọc được `.message`.
|
|
104
|
+
*
|
|
105
|
+
* @param error Giá trị bắt được từ `catch` (có thể là bất kỳ kiểu nào)
|
|
106
|
+
* @returns Chính `error` nếu đã là `Error`, ngược lại một `Error` mới chứa `String(error)`
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* try {
|
|
111
|
+
* JSON.parse(raw);
|
|
112
|
+
* } catch (error) {
|
|
113
|
+
* // Không cần tự kiểm tra `error instanceof Error` thủ công
|
|
114
|
+
* console.error(toError(error).message);
|
|
115
|
+
* // => "Unexpected token o in JSON at position 1"
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
declare function toError(error: unknown): Error;
|
|
120
|
+
type FilterValue = unknown;
|
|
121
|
+
/**
|
|
122
|
+
* Điều kiện lọc của `BaseModel`.
|
|
123
|
+
*
|
|
124
|
+
* `T` chỉ dùng để **gợi ý tên field hợp lệ** khi gõ; `Record<string, FilterValue>`
|
|
125
|
+
* vẫn cho phép mọi khoá vì filter là ngôn ngữ động (toán tử `$in`, `$or`, tên
|
|
126
|
+
* cột không nằm trong entity...). Vì vậy TypeScript **không** bắt được lỗi gõ
|
|
127
|
+
* typo trong tên field — hãy dùng `model.isFields([...])` để kiểm tra tên cột
|
|
128
|
+
* lấy từ người dùng.
|
|
129
|
+
*
|
|
130
|
+
* @template T Kiểu entity của model, dùng để gợi ý
|
|
131
|
+
*/
|
|
132
|
+
type Filter<T extends ModelEntity = ModelEntity> = Partial<Record<keyof T, FilterValue>> & Record<string, FilterValue>;
|
|
133
|
+
/**
|
|
134
|
+
* Filter dạng "rộng" cho code nội bộ của thư viện.
|
|
135
|
+
*
|
|
136
|
+
* `keyof T` ở {@link Filter} chưa cụ thể hoá khi T là type parameter, nên không
|
|
137
|
+
* dùng được cho các thao tác nội bộ như gán `isDeleted` hay dựng `{ id }`.
|
|
138
|
+
*/
|
|
139
|
+
type AnyFilter = Record<string, FilterValue>;
|
|
140
|
+
/** Kết quả của {@link BaseModel.saveResult}. */
|
|
141
|
+
type SaveResult<T> = {
|
|
142
|
+
ok: true;
|
|
143
|
+
item: T;
|
|
144
|
+
} | {
|
|
145
|
+
ok: false;
|
|
146
|
+
errors: string[];
|
|
147
|
+
};
|
|
148
|
+
type QueryRows = RowDataPacket[];
|
|
149
|
+
/** Một tập hàng kết quả truy vấn. */
|
|
150
|
+
type QueryResult = [QueryRows, unknown];
|
|
151
|
+
/**
|
|
152
|
+
* Giá trị trả về thật của {@link BaseModel.query}, giống hệt `mysql2`: một tuple
|
|
153
|
+
* `[result, fields]`, nơi `result` là `OkPacket`, `ResultSetHeader` hoặc mảng hàng
|
|
154
|
+
* tuỳ loại câu lệnh.
|
|
155
|
+
*
|
|
156
|
+
* Phải dùng kiểu này cho chữ ký `query()`. Nếu khai báo `Promise<QueryResult>` thì
|
|
157
|
+
* `BaseModel` **không** gán được cho `IBaseModel`, khiến khai báo `modifieds` báo
|
|
158
|
+
* lỗi type ở phía người dùng.
|
|
159
|
+
*/
|
|
160
|
+
type RawQueryResult = [unknown, unknown];
|
|
161
|
+
interface IBaseModel<T extends ModelEntity = ModelRow> {
|
|
7
162
|
table: string;
|
|
8
163
|
isDeleted: boolean;
|
|
9
164
|
errors: string[];
|
|
10
165
|
fieldName(key: string): string;
|
|
11
166
|
save(item: Partial<T>, conn?: PoolConnection): Promise<T | false>;
|
|
12
|
-
findOne(filter:
|
|
13
|
-
find(filter:
|
|
14
|
-
query(sql: string, params?:
|
|
15
|
-
update(data: Record<string,
|
|
167
|
+
findOne(filter: Filter<T>): Promise<T | null>;
|
|
168
|
+
find(filter: Filter<T>): Promise<T[]>;
|
|
169
|
+
query(sql: string, params?: SqlParam[], conn?: PoolConnection): Promise<RawQueryResult>;
|
|
170
|
+
update(data: Partial<T> | Record<string, SqlParam | SqlParam[]>, filter: Filter<T>, conn?: PoolConnection): Promise<number>;
|
|
16
171
|
}
|
|
17
172
|
interface PaginationResult<T> {
|
|
18
173
|
page: number;
|
|
@@ -22,191 +177,729 @@ interface PaginationResult<T> {
|
|
|
22
177
|
items: T[];
|
|
23
178
|
}
|
|
24
179
|
interface JoinType {
|
|
180
|
+
/** Tên bảng liên kết — được escape tự động */
|
|
25
181
|
table: string;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Điều kiện nối bảng: `"users.id = profiles.user_id"` hoặc
|
|
184
|
+
* `{ left: "users.id", right: "profiles.user_id", operator: "=" }`.
|
|
185
|
+
* Chỉ hỗ trợ phép so sánh đơn giản giữa các column reference.
|
|
186
|
+
*/
|
|
187
|
+
on: SqlJoinCondition;
|
|
188
|
+
/** Kiểu JOIN — phải thuộc whitelist (`LEFT JOIN`, `INNER JOIN`...), sai sẽ ném `invalid_join_type` */
|
|
189
|
+
type?: string | null;
|
|
190
|
+
/** Danh sách cột cần lấy — chỉ nhận tên cột, không nhận SQL thô */
|
|
191
|
+
fields?: readonly string[];
|
|
29
192
|
}
|
|
30
193
|
interface ModifiedType {
|
|
31
194
|
[field: string]: {
|
|
32
|
-
|
|
195
|
+
/** Model liên kết; dùng `AnyIBaseModel` để nhận được model khai báo entity bằng `interface`. */
|
|
196
|
+
smodel: AnyIBaseModel;
|
|
33
197
|
tkey: string;
|
|
34
198
|
skey: string;
|
|
35
199
|
fmap: string;
|
|
36
200
|
};
|
|
37
201
|
}
|
|
38
202
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
203
|
+
* Lớp model cơ sở (abstract).
|
|
204
|
+
*
|
|
205
|
+
* Cung cấp các thao tác database dùng chung như:
|
|
206
|
+
* CRUD, dựng câu truy vấn, phân trang và kiểm tra dữ liệu (validation).
|
|
42
207
|
*
|
|
43
|
-
* @template T
|
|
208
|
+
* @template T Kiểu entity, bắt buộc phải có trường `id`
|
|
44
209
|
*/
|
|
45
|
-
declare abstract class BaseModel<T extends
|
|
46
|
-
id: number;
|
|
47
|
-
}> {
|
|
210
|
+
declare abstract class BaseModel<T extends ModelEntity = ModelRow> {
|
|
48
211
|
abstract table: string;
|
|
49
212
|
protected pool: Pool;
|
|
50
213
|
isDeleted: boolean;
|
|
51
214
|
errors: string[];
|
|
52
|
-
|
|
215
|
+
validate: Record<string, string> | undefined;
|
|
53
216
|
modifieds: ModifiedType | undefined;
|
|
54
217
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
218
|
+
* Cache danh sách cột theo bảng, phục vụ `isField`/`isFields`.
|
|
219
|
+
* Xoá bằng `clearSchemaCache()`.
|
|
220
|
+
*/
|
|
221
|
+
private readonly schemaCache;
|
|
222
|
+
/**
|
|
223
|
+
* Khởi tạo instance của model.
|
|
224
|
+
*
|
|
225
|
+
* @param pool Connection pool MySQL dùng để truy vấn
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* import mysql from 'mysql2/promise';
|
|
230
|
+
* import { UserModel } from './models/UserModel.js';
|
|
231
|
+
*
|
|
232
|
+
* const pool = mysql.createPool({ host: 'localhost', user: 'root', database: 'app_db' });
|
|
233
|
+
* const userModel = new UserModel(pool);
|
|
234
|
+
* ```
|
|
57
235
|
*/
|
|
58
236
|
constructor(pool: Pool);
|
|
237
|
+
/**
|
|
238
|
+
* Lấy tên hiển thị của trường.
|
|
239
|
+
*
|
|
240
|
+
* Có thể ghi đè trong class con để ứng dụng tự dùng khi dựng thông báo lỗi.
|
|
241
|
+
* Lưu ý: `Validation` không còn gọi tới hàm này vì validator không phụ thuộc model.
|
|
242
|
+
*
|
|
243
|
+
* @param key Tên cột trong bảng
|
|
244
|
+
* @returns Tên hiển thị tương ứng
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* // Lớp con ghi đè để trả về nhãn tiếng Việt dùng cho thông báo lỗi
|
|
249
|
+
* override fieldName(key: string) {
|
|
250
|
+
* return { fullname: 'Họ và tên', email: 'Địa chỉ Email' }[key] ?? key;
|
|
251
|
+
* }
|
|
252
|
+
*
|
|
253
|
+
* userModel.fieldName('fullname'); // => "Họ và tên"
|
|
254
|
+
* userModel.fieldName('status'); // => "status" (không có trong map thì trả về chính key)
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
59
257
|
fieldName(key: string): string;
|
|
258
|
+
/**
|
|
259
|
+
* Lấy danh sách các thông báo lỗi phát sinh trong quá trình thực thi của model.
|
|
260
|
+
* @returns Mảng các chuỗi thông báo lỗi
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* const ok = await userModel.save({ email: 'khong-hop-le' }); // false do DB báo lỗi
|
|
265
|
+
* userModel.getErrors();
|
|
266
|
+
* // => ["ER_BAD_NULL_ERROR: Column 'fullname' cannot be null"]
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
60
269
|
getErrors(): string[];
|
|
270
|
+
/**
|
|
271
|
+
* Lấy một kết nối đơn lẻ từ connection pool để chạy transaction.
|
|
272
|
+
* @returns Promise giải quyết kết nối PoolConnection
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* const conn = await userModel.getConnection();
|
|
277
|
+
* try {
|
|
278
|
+
* await conn.beginTransaction();
|
|
279
|
+
* await userModel.update({ status: 1 }, { id: 7 }, conn);
|
|
280
|
+
* await conn.commit();
|
|
281
|
+
* } catch (err) {
|
|
282
|
+
* await conn.rollback();
|
|
283
|
+
* throw err;
|
|
284
|
+
* } finally {
|
|
285
|
+
* // Bắt buộc trả kết nối về pool
|
|
286
|
+
* conn.release();
|
|
287
|
+
* }
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
61
290
|
getConnection(): Promise<PoolConnection>;
|
|
62
291
|
/**
|
|
63
|
-
*
|
|
292
|
+
* Kiểm tra dữ liệu theo bộ quy tắc của lớp `Validation`.
|
|
64
293
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
294
|
+
* `Validation` hoàn toàn thuần (không truy vấn database), nên việc kiểm tra
|
|
295
|
+
* trùng lặp phải tự thực hiện ở tầng nghiệp vụ trước khi gọi hàm này.
|
|
296
|
+
*
|
|
297
|
+
* @param item Dữ liệu cần kiểm tra
|
|
298
|
+
* @param validate Bộ quy tắc kiểm tra dạng `field -> rule1|rule2`
|
|
299
|
+
* @param lang Mã ngôn ngữ cho thông báo lỗi (mặc định `'vi'`)
|
|
300
|
+
* @throws {Error} Khi có ít nhất một rule không thỏa hoặc rule không tồn tại
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* await userModel.runValidate(
|
|
305
|
+
* { fullname: 'Nguyen Van A', email: 'a@example.com', age: 25 },
|
|
306
|
+
* { email: 'required|email', age: 'required|integer|rangeNum(18,60)' }
|
|
307
|
+
* );
|
|
308
|
+
* // Không ném lỗi => dữ liệu hợp lệ
|
|
309
|
+
*
|
|
310
|
+
* // Trả thông báo lỗi bằng tiếng Anh
|
|
311
|
+
* await userModel.runValidate({ email: 'sai' }, { email: 'required|email' }, 'en');
|
|
312
|
+
* // throw: "Email is not in the correct format"
|
|
313
|
+
* ```
|
|
67
314
|
*/
|
|
68
|
-
|
|
315
|
+
runValidate(item: Partial<T>, validate?: Record<string, string>, lang?: string): Promise<void>;
|
|
69
316
|
/**
|
|
70
|
-
*
|
|
317
|
+
* Thực thi câu SQL thông qua connection pool hoặc connection được truyền vào.
|
|
318
|
+
*
|
|
319
|
+
* @param sql Câu SQL, mọi giá trị động đều phải dùng placeholder `?`
|
|
320
|
+
* @param params Danh sách tham số bind cho các placeholder
|
|
321
|
+
* @param conn Connection cụ thể (thường dùng khi chạy transaction)
|
|
322
|
+
* @returns Kết quả trả về nguyên bản của mysql2
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```ts
|
|
326
|
+
* const [rows] = await userModel.query(
|
|
327
|
+
* 'SELECT id, email FROM users WHERE roleId = ? AND status = ?',
|
|
328
|
+
* [2, 1]
|
|
329
|
+
* );
|
|
330
|
+
* ```
|
|
71
331
|
*/
|
|
72
|
-
query(sql: string, params?:
|
|
332
|
+
query(sql: string, params?: SqlParam[], conn?: PoolConnection): Promise<[import("mysql2").QueryResult, import("mysql2").FieldPacket[]]>;
|
|
73
333
|
/**
|
|
74
|
-
*
|
|
334
|
+
* Đồng bộ các trường được ánh xạ (mapped field) từ model liên kết.
|
|
335
|
+
*
|
|
336
|
+
* Dùng để tự động điền trường hiển thị từ khóa ngoại.
|
|
75
337
|
*
|
|
76
|
-
*
|
|
338
|
+
* @param data Dữ liệu đích (được sửa tại chỗ)
|
|
339
|
+
* @returns Chính đối tượng `data` sau khi đã bổ sung các trường ánh xạ
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```ts
|
|
343
|
+
* // Khai báo ánh xạ: lấy `roleName` từ bảng `roles` theo `roleId` của bản ghi hiện tại
|
|
344
|
+
* userModel.modifieds = {
|
|
345
|
+
* roleName: { smodel: roleModel, tkey: 'roleId', skey: 'id', fmap: 'name' }
|
|
346
|
+
* };
|
|
77
347
|
*
|
|
78
|
-
*
|
|
348
|
+
* const data = await userModel.modifiedSync({ id: 1, fullname: 'Nguyen Van A', roleId: 2 });
|
|
349
|
+
* // data.roleName => "Admin" (tự động truy vấn roles WHERE id = 2)
|
|
350
|
+
* ```
|
|
79
351
|
*/
|
|
80
|
-
modifiedSync<T extends Record<string, any>>(data: any): Promise<T>;
|
|
81
352
|
/**
|
|
82
|
-
*
|
|
353
|
+
* Đồng bộ các trường hiển thị lấy từ model liên kết.
|
|
354
|
+
*
|
|
355
|
+
* Dùng để điền tên gọi từ khoá ngoại (ví dụ `roleId` → `roleName`) lấy từ bảng
|
|
356
|
+
* khác.
|
|
357
|
+
*
|
|
358
|
+
* Truy vấn được **gom nhóm**: các field dùng chung một `(model, khoá)` chỉ gây
|
|
359
|
+
* ra đúng 1 lệnh `SELECT ... WHERE khoá IN (...)` thay vì 1 lệnh cho mỗi
|
|
360
|
+
* field — trước đây đây là mẫu N+1 và các lệnh được `await` tuần tự.
|
|
83
361
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
362
|
+
* Chỉ gán giá trị khi tìm thấy bản ghi nguồn; nếu không thì field đích được
|
|
363
|
+
* để nguyên (giống hành vi trước đây).
|
|
86
364
|
*
|
|
87
|
-
* @param
|
|
88
|
-
* @
|
|
365
|
+
* @param data Bản ghi cần điền trường hiển thị (được sửa tại chỗ)
|
|
366
|
+
* @returns Chính `data`
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```ts
|
|
370
|
+
* // Khai báo ánh xạ: lấy `roleName` từ bảng `roles` theo `roleId` của bản ghi
|
|
371
|
+
* userModel.modifieds = {
|
|
372
|
+
* roleName: { smodel: roleModel, tkey: 'roleId', skey: 'id', fmap: 'name' }
|
|
373
|
+
* };
|
|
374
|
+
*
|
|
375
|
+
* const data = await userModel.modifiedSync({ id: 1, fullname: 'Nguyen Van A', roleId: 2 });
|
|
376
|
+
* // data.roleName => "Admin"
|
|
377
|
+
* ```
|
|
89
378
|
*/
|
|
90
|
-
|
|
379
|
+
modifiedSync<R extends Record<string, unknown>>(data: Record<string, unknown>): Promise<R>;
|
|
380
|
+
/**
|
|
381
|
+
* Dựng mệnh đề SQL `WHERE` từ đối tượng filter.
|
|
382
|
+
*
|
|
383
|
+
* Hỗ trợ các toán tử nâng cao:
|
|
384
|
+
* `$in`, `$nin`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$like`, `$likeIn`
|
|
385
|
+
* và nhóm điều kiện `$or`, `$and`.
|
|
386
|
+
*
|
|
387
|
+
* Khi `isDeleted = true`, mọi filter sẽ được tự động gắn thêm điều kiện `isDeleted = 0`
|
|
388
|
+
* (soft delete), trừ khi filter đã chỉ định `isDeleted` rõ ràng.
|
|
389
|
+
*
|
|
390
|
+
* @param filter Đối tượng điều kiện lọc
|
|
391
|
+
* @param als Tên alias của bảng (tùy chọn), dùng khi truy vấn có `join`
|
|
392
|
+
* @returns Object gồm `sql` (mệnh đề WHERE) và `params` (tham số theo đúng thứ tự placeholder)
|
|
393
|
+
* @throws {Error} `$between requires [min, max]` khi mảng khoảng không đúng 2 phần tử
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```ts
|
|
397
|
+
* const { sql, params } = userModel.buildWhere({
|
|
398
|
+
* status: 1,
|
|
399
|
+
* roleId: { $in: [1, 2] },
|
|
400
|
+
* fullname: { $like: 'Nguyen' },
|
|
401
|
+
* $or: [{ email: 'a@example.com' }, { email: 'b@example.com' }]
|
|
402
|
+
* });
|
|
403
|
+
* // sql => "`status` = ? AND (`roleId` IN (?,?)) AND (`fullname` LIKE ?) AND ((`email` = ?) OR (`email` = ?)) AND `isDeleted` = ?"
|
|
404
|
+
* // params => [1, 1, 2, '%Nguyen%', 'a@example.com', 'b@example.com', 0]
|
|
405
|
+
*
|
|
406
|
+
* // Filter rỗng khi bật soft delete vẫn lọc theo isDeleted = 0
|
|
407
|
+
* userModel.buildWhere({}); // { sql: "`isDeleted` = ?", params: [0] }
|
|
408
|
+
*
|
|
409
|
+
* // Truyền alias khi cần tự viết câu SQL có join:
|
|
410
|
+
* const where = userModel.buildWhere({ roleId: 2 }, 'u');
|
|
411
|
+
* // sql => "`u`.`roleId` = ? AND `u`.`isDeleted` = ?"
|
|
412
|
+
* ```
|
|
413
|
+
*/
|
|
414
|
+
buildWhere(filter: AnyFilter, als?: string): {
|
|
91
415
|
sql: string;
|
|
92
|
-
params:
|
|
416
|
+
params: SqlParam[];
|
|
93
417
|
};
|
|
94
418
|
/**
|
|
95
|
-
*
|
|
419
|
+
* Đếm số bản ghi khớp với điều kiện lọc.
|
|
96
420
|
*
|
|
97
|
-
* @param filter
|
|
421
|
+
* @param filter Điều kiện lọc (mặc định không lọc)
|
|
422
|
+
* @returns Tổng số bản ghi, trả về `0` nếu không có kết quả
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```ts
|
|
426
|
+
* await userModel.count(); // => tổng số user
|
|
427
|
+
* await userModel.count({ status: 1, roleId: { $gte: 2 } }); // => số user còn hoạt động thuộc role từ 2 trở lên
|
|
428
|
+
* ```
|
|
98
429
|
*/
|
|
99
|
-
count(filter?:
|
|
430
|
+
count(filter?: Filter<T>): Promise<number>;
|
|
100
431
|
/**
|
|
101
|
-
*
|
|
432
|
+
* Thêm mới một bản ghi.
|
|
433
|
+
*
|
|
434
|
+
* @param data Dữ liệu cần thêm (tên cột sẽ được escape tự động)
|
|
435
|
+
* @param conn Kết nối DB tùy chọn (dùng trong transaction)
|
|
436
|
+
* @returns `insertId` của bản ghi vừa thêm
|
|
102
437
|
*
|
|
103
|
-
* @
|
|
104
|
-
*
|
|
438
|
+
* @example
|
|
439
|
+
* ```ts
|
|
440
|
+
* const newId = await userModel.insert({
|
|
441
|
+
* fullname: 'Nguyen Van A',
|
|
442
|
+
* email: 'a@example.com',
|
|
443
|
+
* roleId: 2,
|
|
444
|
+
* status: 1
|
|
445
|
+
* });
|
|
446
|
+
* // newId => 42
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
insert(data: Record<string, SqlParam>, conn?: PoolConnection): Promise<number>;
|
|
450
|
+
/**
|
|
451
|
+
* Cập nhật bản ghi theo điều kiện (so sánh bằng dấu `=`).
|
|
452
|
+
*
|
|
453
|
+
* Khi `isDeleted = true`, mệnh đề `WHERE` cũng được gắn thêm `isDeleted = 0`
|
|
454
|
+
* để không cập nhật nhầm lên bản ghi đã xoá mềm (trừ khi filter đã chỉ định
|
|
455
|
+
* rõ `isDeleted`).
|
|
456
|
+
*
|
|
457
|
+
* @param data Dữ liệu cần cập nhật
|
|
458
|
+
* @param filter Điều kiện lọc WHERE
|
|
459
|
+
* @param conn Kết nối DB tùy chọn
|
|
460
|
+
* @returns Số dòng bị ảnh hưởng
|
|
461
|
+
* @throws {Error} Khi `filter` rỗng, tránh cập nhật toàn bảng
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```ts
|
|
465
|
+
* const affected = await userModel.update(
|
|
466
|
+
* { status: 0, fullname: 'Nguyen Van B' },
|
|
467
|
+
* { id: 7 }
|
|
468
|
+
* );
|
|
469
|
+
* // affected => 1
|
|
470
|
+
*
|
|
471
|
+
* // Nhiều điều kiện được nối bằng AND
|
|
472
|
+
* await userModel.update({ status: 1 }, { roleId: 2, status: 0 });
|
|
473
|
+
* ```
|
|
105
474
|
*/
|
|
106
|
-
|
|
475
|
+
update(data: Record<string, SqlParam>, filter: Filter<T>, conn?: PoolConnection): Promise<number>;
|
|
107
476
|
/**
|
|
108
|
-
*
|
|
477
|
+
* Cập nhật nâng cao với điều kiện `WHERE` động (hỗ trợ toán tử nâng cao).
|
|
478
|
+
*
|
|
479
|
+
* @param data Danh sách giá trị cần cập nhật; bỏ trống thì hàm thoát ra ngay
|
|
480
|
+
* @param filter Điều kiện lọc, dựng bằng `buildWhere()`
|
|
481
|
+
* @param conn Kết nối DB tùy chọn
|
|
482
|
+
* @returns Số dòng bị ảnh hưởng, `undefined` nếu `data` rỗng
|
|
483
|
+
* @throws {Error} `invalid_update_is_deleted` khi có lỗi phát sinh trong quá trình cập nhật
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```ts
|
|
487
|
+
* // Khác `update()`: điều kiện lọc dùng được toán tử nâng cao
|
|
488
|
+
* const affected = await userModel.updateAdv(
|
|
489
|
+
* { status: 0 },
|
|
490
|
+
* { roleId: { $in: [1, 2] }, createdAt: { $between: ['2024-01-01', '2024-12-31'] } }
|
|
491
|
+
* );
|
|
492
|
+
* // affected => số user thuộc role 1 hoặc 2 được tạo trong năm 2024
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
updateAdv(data?: Record<string, SqlParam>, filter?: Filter<T>, conn?: PoolConnection): Promise<number | undefined>;
|
|
496
|
+
/**
|
|
497
|
+
* Thêm mới hoặc cập nhật bản ghi theo khóa chính.
|
|
498
|
+
*
|
|
499
|
+
* - Nếu có `item.id` → thực hiện `UPDATE`
|
|
500
|
+
* - Ngược lại → thực hiện `INSERT`
|
|
501
|
+
*
|
|
502
|
+
* @param item Dữ liệu entity
|
|
503
|
+
* @param conn Kết nối DB tùy chọn
|
|
504
|
+
* @returns Đối tượng đã lưu (với `id` đã được gán khi thêm mới), hoặc `false` nếu có lỗi
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```ts
|
|
508
|
+
* // Không có id => INSERT, item.id được gán lại bằng insertId
|
|
509
|
+
* const created = await userModel.save({ fullname: 'Nguyen Van A', email: 'a@example.com' });
|
|
510
|
+
* // created.id => 42
|
|
511
|
+
*
|
|
512
|
+
* // Có id => UPDATE
|
|
513
|
+
* await userModel.save({ id: 42, status: 0 });
|
|
514
|
+
* ```
|
|
109
515
|
*/
|
|
110
|
-
update(data: Record<string, any>, filter: Record<string, any>, conn?: PoolConnection): Promise<number>;
|
|
111
516
|
/**
|
|
112
|
-
*
|
|
517
|
+
* Lưu bản ghi và trả về kết quả có kiểu, **không** dùng state lỗi dùng chung.
|
|
518
|
+
*
|
|
519
|
+
* Model thường được đăng ký là singleton (`initModels`) nên dùng chung cho mọi
|
|
520
|
+
* request. Biến `errors` của instance vì thế có thể bị lẫn giữa các request
|
|
521
|
+
* đang chạy song song. Hàm này trả lỗi trực tiếp trong giá trị trả về nên
|
|
522
|
+
* không cần đọc `getErrors()` — đây là API nên dùng.
|
|
113
523
|
*
|
|
114
|
-
* @param
|
|
115
|
-
* @param
|
|
116
|
-
* @
|
|
524
|
+
* @param item Dữ liệu cần lưu
|
|
525
|
+
* @param conn Kết nối DB tùy chọn
|
|
526
|
+
* @returns `{ ok: true, item }` khi thành công, `{ ok: false, errors }` khi lỗi
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```ts
|
|
530
|
+
* const result = await userModel.saveResult({ fullname: 'A' });
|
|
531
|
+
* if (!result.ok) throw new ClientError(result.errors.join(', '), 400);
|
|
532
|
+
* console.log(result.item.id);
|
|
533
|
+
* ```
|
|
117
534
|
*/
|
|
118
|
-
|
|
535
|
+
saveResult(item: Partial<T>, conn?: PoolConnection): Promise<SaveResult<T>>;
|
|
119
536
|
/**
|
|
120
|
-
*
|
|
537
|
+
* Lưu bản ghi, trả về `false` nếu thất bại.
|
|
121
538
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
539
|
+
* > **Cảnh báo về trạng thái dùng chung**: hàm này ghi lỗi vào `this.errors`
|
|
540
|
+
* > (dùng chung cho mọi request khi model là singleton). Với code chạy đồng
|
|
541
|
+
* > thời, hãy dùng {@link BaseModel.saveResult} để nhận lỗi trực tiếp.
|
|
124
542
|
*
|
|
125
|
-
* @param item
|
|
126
|
-
* @param conn
|
|
543
|
+
* @param item Dữ liệu cần lưu
|
|
544
|
+
* @param conn Kết nối DB tùy chọn
|
|
545
|
+
* @returns Bản ghi đã lưu, hoặc `false` khi thất bại
|
|
127
546
|
*/
|
|
128
547
|
save(item: Partial<T>, conn?: PoolConnection): Promise<T | false>;
|
|
129
548
|
/**
|
|
130
|
-
*
|
|
549
|
+
* Kiểm tra dữ liệu rồi lưu bản ghi.
|
|
550
|
+
*
|
|
551
|
+
* Trình tự thực hiện: `modifiedSync()` → `validate()` → `save()`.
|
|
131
552
|
*
|
|
132
|
-
* @
|
|
553
|
+
* @param item Dữ liệu cần lưu
|
|
554
|
+
* @param validate Bộ quy tắc kiểm tra; truyền `false` để bỏ qua bước validate
|
|
555
|
+
* @param conn Kết nối DB tùy chọn
|
|
556
|
+
* @param lang Mã ngôn ngữ cho thông báo lỗi (mặc định `'vi'`)
|
|
557
|
+
* @returns Bản ghi đã lưu, hoặc `null` nếu bỏ qua validate và lưu thất bại
|
|
558
|
+
* @throws {Error} Khi kiểm tra hoặc lưu thất bại; message là các lỗi nối bằng dấu `, `
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```ts
|
|
562
|
+
* const rules = { email: 'required|email' };
|
|
563
|
+
*
|
|
564
|
+
* try {
|
|
565
|
+
* const user = await userModel.vdSave(
|
|
566
|
+
* { fullname: 'Nguyen Van A', email: 'a@example.com' },
|
|
567
|
+
* rules
|
|
568
|
+
* );
|
|
569
|
+
* // user.id => 42
|
|
570
|
+
* } catch (error) {
|
|
571
|
+
* // Ví dụ: "Email không đúng định dạng"
|
|
572
|
+
* console.error(toError(error).message);
|
|
573
|
+
* }
|
|
574
|
+
*
|
|
575
|
+
* // Thông báo lỗi bằng tiếng Anh (lang là tham số cuối)
|
|
576
|
+
* await userModel.vdSave({ email: 'sai' }, rules, undefined, 'en');
|
|
577
|
+
* // throw: "Email is not in the correct format"
|
|
578
|
+
* ```
|
|
133
579
|
*/
|
|
134
|
-
vdSave(item:
|
|
580
|
+
vdSave(item: Partial<T>, validate?: Record<string, string> | false, conn?: PoolConnection, lang?: string): Promise<T | null>;
|
|
135
581
|
/**
|
|
136
|
-
*
|
|
582
|
+
* Cập nhật hàng loạt nhiều bản ghi bằng kỹ thuật `CASE WHEN`.
|
|
583
|
+
*
|
|
584
|
+
* Mỗi cột được dựng thành `CASE ... WHEN id THEN ? ... ELSE <cột hiện tại> END`,
|
|
585
|
+
* nên câu lệnh chỉ chạy một lần thay vì N lần.
|
|
586
|
+
*
|
|
587
|
+
* @param rows Danh sách dữ liệu, các phần tử phải có cùng tập cột
|
|
588
|
+
* @param key Tên trường khóa chính dùng để định danh từng dòng
|
|
589
|
+
* @param conn Kết nối DB tùy chọn
|
|
590
|
+
* @throws {Error} `invalid_update_many` khi có lỗi phát sinh trong quá trình cập nhật
|
|
137
591
|
*
|
|
138
|
-
* @
|
|
139
|
-
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```ts
|
|
594
|
+
* await userModel.updateMany([
|
|
595
|
+
* { id: 1, status: 0 },
|
|
596
|
+
* { id: 2, status: 1 }
|
|
597
|
+
* ], 'id');
|
|
598
|
+
* // => UPDATE `users` SET `status` = CASE `id` WHEN ? THEN ? WHEN ? THEN ? ELSE `status` END WHERE `id` IN (?,?)
|
|
599
|
+
* ```
|
|
140
600
|
*/
|
|
141
|
-
updateMany(rows:
|
|
601
|
+
updateMany(rows: Array<Record<string, SqlParam>>, key: string, conn?: PoolConnection): Promise<void>;
|
|
142
602
|
/**
|
|
143
|
-
*
|
|
603
|
+
* Thêm hàng loạt bản ghi, tự cập nhật nếu bản ghi đã tồn tại (UPSERT).
|
|
604
|
+
*
|
|
605
|
+
* Dùng `INSERT ... ON DUPLICATE KEY UPDATE` với mệnh đề `col = VALUES(col)`
|
|
606
|
+
* cho mọi cột trừ khóa chính.
|
|
607
|
+
*
|
|
608
|
+
* @param rows Danh sách dữ liệu, các phần tử phải có cùng tập cột
|
|
609
|
+
* @param key Tên trường khóa chính (không được đưa vào phần `UPDATE`)
|
|
610
|
+
* @param conn Kết nối DB tùy chọn
|
|
611
|
+
* @throws {Error} `invalid_update_and_create` khi có lỗi phát sinh trong quá trình ghi
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```ts
|
|
615
|
+
* await userModel.updateAndCreateMany([
|
|
616
|
+
* { id: 1, fullname: 'Nguyen Van A', status: 1 },
|
|
617
|
+
* { id: 2, fullname: 'Tran Thi B', status: 1 }
|
|
618
|
+
* ], 'id');
|
|
619
|
+
* // id chưa có => INSERT; id đã có => UPDATE các cột còn lại
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
updateAndCreateMany(rows: Array<Record<string, SqlParam>>, key: string, conn?: PoolConnection): Promise<void>;
|
|
623
|
+
/**
|
|
624
|
+
* Tìm danh sách bản ghi với các tùy chọn nâng cao (lọc, sắp xếp, join, chọn cột, giới hạn).
|
|
625
|
+
*
|
|
626
|
+
* @param filter Điều kiện lọc WHERE
|
|
627
|
+
* @param order Tùy chọn sắp xếp (ORDER BY) — key là tên cột (đã escape), value chỉ nhận `'ASC' | 'DESC'`
|
|
628
|
+
* @param join Cấu hình JOIN bảng; `on` chỉ nhận phép so sánh đơn giản giữa các cột
|
|
629
|
+
* @param select Danh sách cột cần lấy; không nhận SQL thô
|
|
630
|
+
* @param limit Số bản ghi tối đa cần lấy, `0` nghĩa là không giới hạn
|
|
631
|
+
* @returns Mảng các bản ghi tìm thấy (mảng rỗng nếu không có kết quả)
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* ```ts
|
|
635
|
+
* // Lọc + sắp xếp + giới hạn
|
|
636
|
+
* const users = await userModel.find(
|
|
637
|
+
* { status: 1, roleId: { $in: [1, 2] }, fullname: { $like: 'Nguyen' } },
|
|
638
|
+
* { id: 'DESC' },
|
|
639
|
+
* false,
|
|
640
|
+
* ['id', 'fullname', 'email'],
|
|
641
|
+
* 10
|
|
642
|
+
* );
|
|
144
643
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
644
|
+
* // Kết hợp join để lấy thêm cột của bảng khác
|
|
645
|
+
* const usersWithRoles = await userModel.find(
|
|
646
|
+
* { status: 1 },
|
|
647
|
+
* { id: 'DESC' },
|
|
648
|
+
* { table: 'roles', on: { left: 'users.roleId', right: 'roles.id', operator: '=' }, fields: ['name'] },
|
|
649
|
+
* [{ column: 'users.id' }, { column: 'roles.name', alias: 'roleName' }]
|
|
650
|
+
* );
|
|
651
|
+
* ```
|
|
147
652
|
*/
|
|
148
|
-
|
|
653
|
+
find(filter?: Filter<T>, order?: Record<string, string> | false, join?: JoinType | JoinType[] | false, select?: SqlSelectInput | false, limit?: number): Promise<T[]>;
|
|
149
654
|
/**
|
|
150
|
-
*
|
|
655
|
+
* Tìm 1 bản ghi đầu tiên thỏa mãn điều kiện lọc
|
|
656
|
+
*
|
|
657
|
+
* @param filter Điều kiện lọc WHERE
|
|
658
|
+
* @param order Tùy chọn sắp xếp (ORDER BY); key là tên cột, value là `ASC` / `DESC`
|
|
659
|
+
* @param join Cấu hình JOIN bảng; `on` chỉ nhận phép so sánh đơn giản giữa các cột
|
|
660
|
+
* @param select Danh sách cột cần lấy; không nhận SQL thô
|
|
661
|
+
* @returns Bản ghi tìm thấy hoặc null nếu không tồn tại
|
|
151
662
|
*
|
|
152
|
-
* @
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
663
|
+
* @example
|
|
664
|
+
* ```ts
|
|
665
|
+
* const user = await userModel.findOne({ email: 'a@example.com' });
|
|
666
|
+
* // user?.id => 42, hoặc null nếu email không tồn tại
|
|
667
|
+
*
|
|
668
|
+
* // Kèm điều kiện sắp xếp để chọn đúng 1 bản ghi khi có nhiều kết quả
|
|
669
|
+
* await userModel.findOne({ roleId: 2 }, { id: 'DESC' });
|
|
670
|
+
* ```
|
|
671
|
+
*/
|
|
672
|
+
findOne(filter?: Filter<T>, order?: Record<string, string> | false, join?: JoinType | JoinType[] | false, select?: SqlSelectInput | false): Promise<T | null>;
|
|
673
|
+
/**
|
|
674
|
+
* Tìm bản ghi theo ID khóa chính
|
|
675
|
+
*
|
|
676
|
+
* @param id Khóa chính ID của bản ghi
|
|
677
|
+
* @param filter Điều kiện lọc bổ sung
|
|
678
|
+
* @returns Bản ghi tìm thấy hoặc null
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* ```ts
|
|
682
|
+
* const user = await userModel.findById(42);
|
|
683
|
+
* // user?.email => "a@example.com", hoặc null nếu id không tồn tại
|
|
684
|
+
*
|
|
685
|
+
* // Có thể kèm điều kiện lọc bổ sung (được nối với điều kiện id bằng AND)
|
|
686
|
+
* await userModel.findById(42, { status: 1 });
|
|
687
|
+
* ```
|
|
688
|
+
*/
|
|
689
|
+
findById(id: number, filter?: Filter<T>): Promise<T | null>;
|
|
690
|
+
/**
|
|
691
|
+
* Xóa 1 bản ghi thỏa mãn điều kiện lọc (LIMIT 1)
|
|
692
|
+
*
|
|
693
|
+
* @param filter Điều kiện lọc WHERE
|
|
694
|
+
* @param conn Kết nối DB tùy chọn
|
|
695
|
+
* @returns `true` nếu xóa thành công ít nhất 1 dòng, ngược lại `false`
|
|
696
|
+
*
|
|
697
|
+
* @example
|
|
698
|
+
* ```ts
|
|
699
|
+
* // Xóa tối đa 1 bản ghi đầu tiên khớp điều kiện
|
|
700
|
+
* const ok = await userModel.deleteOne({ email: 'a@example.com' });
|
|
701
|
+
* // ok => true nếu có dòng bị xóa, ngược lại false
|
|
702
|
+
* ```
|
|
703
|
+
*/
|
|
704
|
+
deleteOne(filter: Filter<T>, conn?: PoolConnection): Promise<boolean>;
|
|
705
|
+
/**
|
|
706
|
+
* Xóa bản ghi theo ID khóa chính
|
|
707
|
+
*
|
|
708
|
+
* @param id Khóa chính ID cần xóa
|
|
709
|
+
* @param conn Kết nối DB tùy chọn
|
|
710
|
+
* @returns `true` nếu xóa thành công, ngược lại `false`
|
|
711
|
+
*
|
|
712
|
+
* @example
|
|
713
|
+
* ```ts
|
|
714
|
+
* const ok = await userModel.deleteById(42);
|
|
715
|
+
* // ok => true nếu bản ghi id = 42 bị xóa, ngược lại false
|
|
716
|
+
* ```
|
|
157
717
|
*/
|
|
158
|
-
find(filter?: Record<string, any>, order?: Record<string, string> | false, join?: JoinType | JoinType[] | false, select?: string | string[] | false, limit?: number): Promise<T[]>;
|
|
159
|
-
findOne(filter?: Record<string, any>, order?: Record<string, string> | false, join?: JoinType | JoinType[] | false, select?: string | string[] | false): Promise<T | null>;
|
|
160
|
-
findById(id: number, filter?: Record<string, any>): Promise<T | null>;
|
|
161
|
-
deleteOne(filter: Record<string, any>, conn?: PoolConnection): Promise<boolean>;
|
|
162
718
|
deleteById(id: number, conn?: PoolConnection): Promise<boolean>;
|
|
163
|
-
|
|
719
|
+
/**
|
|
720
|
+
* Xóa nhiều bản ghi thỏa mãn điều kiện lọc
|
|
721
|
+
*
|
|
722
|
+
* @param filter Điều kiện lọc WHERE (bắt buộc)
|
|
723
|
+
* @param limit Giới hạn số dòng cần xóa (0 = không giới hạn)
|
|
724
|
+
* @param conn Kết nối DB tùy chọn
|
|
725
|
+
* @returns `true` nếu có dòng bị xóa, ngược lại `false`
|
|
726
|
+
* @throws {Error} `deleteMany requires a filter condition` khi `filter` rỗng.
|
|
727
|
+
* Bắt buộc chặn trường hợp này vì `buildWhere()` luôn trả về `1=1` khi
|
|
728
|
+
* không có điều kiện, và `DELETE FROM t WHERE 1=1` sẽ xóa toàn bộ bảng.
|
|
729
|
+
*
|
|
730
|
+
* @example
|
|
731
|
+
* ```ts
|
|
732
|
+
* // Xóa toàn bộ bản ghi khớp điều kiện
|
|
733
|
+
* const ok = await userModel.deleteMany({ roleId: 2 });
|
|
734
|
+
* // ok => true nếu có dòng bị xóa
|
|
735
|
+
*
|
|
736
|
+
* // Giới hạn số dòng xóa tối đa
|
|
737
|
+
* await userModel.deleteMany({ status: 0 }, 100);
|
|
738
|
+
*
|
|
739
|
+
* // Filter rỗng bị từ chối để tránh xóa nhầm toàn bộ bảng
|
|
740
|
+
* await userModel.deleteMany({}); // throw Error: deleteMany requires a filter condition
|
|
741
|
+
* ```
|
|
742
|
+
*/
|
|
743
|
+
deleteMany(filter: Filter<T>, limit?: number, conn?: PoolConnection): Promise<boolean>;
|
|
744
|
+
/**
|
|
745
|
+
* Kiểm tra một cột cụ thể có tồn tại trong bảng hay không
|
|
746
|
+
*
|
|
747
|
+
* @param field Tên cột cần kiểm tra
|
|
748
|
+
* @param table Tên bảng (mặc định: bảng hiện tại của model)
|
|
749
|
+
* @returns `true` nếu cột tồn tại, ngược lại `false`
|
|
750
|
+
*
|
|
751
|
+
* @example
|
|
752
|
+
* ```ts
|
|
753
|
+
* await userModel.isField('email'); // => true (cột tồn tại trong bảng `users`)
|
|
754
|
+
* await userModel.isField('not_exist'); // => false
|
|
755
|
+
* await userModel.isField('name', 'roles'); // kiểm tra trên bảng khác
|
|
756
|
+
*
|
|
757
|
+
* // Thường dùng để kiểm tra cột do người dùng gửi lên trước khi đưa vào truy vấn
|
|
758
|
+
* if (await userModel.isField(inputField)) {
|
|
759
|
+
* await userModel.find({ [inputField]: inputValue });
|
|
760
|
+
* }
|
|
761
|
+
* ```
|
|
762
|
+
*/
|
|
164
763
|
isField(field: string, table?: string): Promise<boolean>;
|
|
764
|
+
/**
|
|
765
|
+
* Xác thực danh sách các cột có thuộc schema của bảng hay không.
|
|
766
|
+
* Thường dùng để phòng chống SQL injection qua custom fields.
|
|
767
|
+
*
|
|
768
|
+
* @param fields Danh sách tên cột cần kiểm tra
|
|
769
|
+
* @returns Danh sách các cột hợp lệ
|
|
770
|
+
* @throws {Error} `invalid_fields` nếu có cột không tồn tại trong bảng
|
|
771
|
+
*
|
|
772
|
+
* @example
|
|
773
|
+
* ```ts
|
|
774
|
+
* // Mảng rỗng => trả về mảng rỗng, không truy vấn DB
|
|
775
|
+
* await userModel.isFields([]); // => []
|
|
776
|
+
*
|
|
777
|
+
* await userModel.isFields(['id', 'fullname', 'email']); // => ['id', 'fullname', 'email']
|
|
778
|
+
* await userModel.isFields(['id', 'hack']); // throw Error: invalid_fields
|
|
779
|
+
*
|
|
780
|
+
* // Dùng để chống SQL injection khi người dùng tự chọn cột cần sắp xếp
|
|
781
|
+
* const orderBy = await userModel.isFields([req.query.sortBy as string]);
|
|
782
|
+
* await userModel.find({}, { [orderBy[0]]: 'DESC' });
|
|
783
|
+
* ```
|
|
784
|
+
*/
|
|
165
785
|
isFields(fields?: string[]): Promise<string[]>;
|
|
166
786
|
/**
|
|
167
|
-
*
|
|
787
|
+
* Đọc (và ghi nhớ) danh sách cột của một bảng.
|
|
168
788
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
789
|
+
* Kết quả được cache theo tên bảng để tránh gọi `SHOW COLUMNS` lặp lại cho
|
|
790
|
+
* mỗi request — trước đây `isField`/`isFields` đều chạy một round-trip mỗi lần
|
|
791
|
+
* gọi (mẫu N+1).
|
|
171
792
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* - `fieldVal` is different from the previous value (`itemOld`)
|
|
793
|
+
* Cache là thuộc tính của **instance**. Nếu schema bảng thay đổi trong lúc
|
|
794
|
+
* ứng dụng đang chạy, dùng `clearSchemaCache()` để nạp lại.
|
|
175
795
|
*
|
|
176
|
-
* @param
|
|
177
|
-
* @
|
|
178
|
-
* @
|
|
179
|
-
|
|
180
|
-
|
|
796
|
+
* @param table Tên bảng cần đọc schema
|
|
797
|
+
* @returns Danh sách tên cột
|
|
798
|
+
* @private
|
|
799
|
+
*/
|
|
800
|
+
private getSchemaFields;
|
|
801
|
+
/**
|
|
802
|
+
* Xoá cache schema của model.
|
|
181
803
|
*
|
|
182
|
-
*
|
|
183
|
-
* or empty string if not found or unchanged
|
|
804
|
+
* Cần gọi khi bảng được migrate/ALTER trong lúc ứng dụng đang chạy.
|
|
184
805
|
*
|
|
185
806
|
* @example
|
|
186
807
|
* ```ts
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
*
|
|
808
|
+
* await migrate();
|
|
809
|
+
* userModel.clearSchemaCache();
|
|
810
|
+
* ```
|
|
811
|
+
*/
|
|
812
|
+
clearSchemaCache(): void;
|
|
813
|
+
/**
|
|
814
|
+
* Lấy giá trị hiển thị được ánh xạ từ model liên kết.
|
|
815
|
+
*
|
|
816
|
+
* Thường dùng để chuyển một giá trị khóa ngoại thành trường dễ đọc
|
|
817
|
+
* (ví dụ `name`, `title`) lấy từ bảng khác.
|
|
818
|
+
*
|
|
819
|
+
* Chỉ truy vấn model liên kết khi:
|
|
820
|
+
* - `fieldVal` có giá trị
|
|
821
|
+
* - `fieldVal` khác giá trị cũ (`itemOld`)
|
|
822
|
+
*
|
|
823
|
+
* @param field Tên trường đích trong model hiện tại
|
|
824
|
+
* @param fieldVal Giá trị mới của trường (thường là ID khóa ngoại)
|
|
825
|
+
* @param mdClass Instance model liên kết dùng để lấy dữ liệu
|
|
826
|
+
* @param itemOld Dữ liệu bản ghi cũ (dùng để phát hiện giá trị có thay đổi)
|
|
827
|
+
* @param fieldMap Tên trường cần lấy trong model liên kết (mặc định: `"name"`)
|
|
828
|
+
*
|
|
829
|
+
* @returns Giá trị hiển thị lấy từ model liên kết,
|
|
830
|
+
* hoặc chuỗi rỗng nếu không tìm thấy hoặc giá trị không đổi
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* ```ts
|
|
834
|
+
* // Chuyển roleId => roleName
|
|
835
|
+
* const roleName = await userModel.getMapName(
|
|
836
|
+
* 'roleId',
|
|
837
|
+
* user.roleId,
|
|
838
|
+
* roleModel,
|
|
839
|
+
* oldUser,
|
|
840
|
+
* 'name'
|
|
194
841
|
* );
|
|
842
|
+
* // roleName => "Admin"
|
|
843
|
+
* // Nếu roleId không đổi so với oldUser hoặc không tìm thấy bản ghi => ""
|
|
195
844
|
* ```
|
|
196
845
|
*/
|
|
197
|
-
getMapName(field: string, fieldVal:
|
|
846
|
+
getMapName(field: string, fieldVal: SqlParam, mdClass: IBaseModel | null | undefined, itemOld?: Partial<T> | null, fieldMap?: string): Promise<{}>;
|
|
198
847
|
/**
|
|
199
|
-
*
|
|
848
|
+
* Tìm bản ghi kèm phân trang.
|
|
849
|
+
*
|
|
850
|
+
* Chạy 2 câu truy vấn: một câu `COUNT(*)` để tính tổng số bản ghi
|
|
851
|
+
* và một câu `SELECT ... LIMIT ? OFFSET ?` để lấy dữ liệu trang hiện tại.
|
|
852
|
+
*
|
|
853
|
+
* @param filter Điều kiện lọc WHERE
|
|
854
|
+
* @param order Tùy chọn sắp xếp (ORDER BY) — key là tên cột (đã escape), value chỉ nhận `'ASC' | 'DESC'`
|
|
855
|
+
* @param join Cấu hình JOIN bảng; `on` chỉ nhận phép so sánh đơn giản giữa các cột
|
|
856
|
+
* @param select Danh sách cột cần lấy; không nhận SQL thô
|
|
857
|
+
* @param page Số trang, bắt đầu từ 1 (giá trị không hợp lệ sẽ được chuẩn hóa về 1)
|
|
858
|
+
* @param limit Số bản ghi mỗi trang (tối thiểu 1)
|
|
859
|
+
* @returns Object gồm `page`, `length`, `pageTotal`, `recordTotal` và `items`
|
|
860
|
+
*
|
|
861
|
+
* @example
|
|
862
|
+
* ```ts
|
|
863
|
+
* const result = await userModel.findWithPagination(
|
|
864
|
+
* { status: 1 },
|
|
865
|
+
* { id: 'DESC' },
|
|
866
|
+
* false, // join
|
|
867
|
+
* false, // select
|
|
868
|
+
* 2, // page: trang thứ 2
|
|
869
|
+
* 20 // limit: 20 bản ghi mỗi trang
|
|
870
|
+
* );
|
|
871
|
+
* // result.recordTotal => 45 (tổng số bản ghi)
|
|
872
|
+
* // result.pageTotal => 3 (Math.ceil(45 / 20))
|
|
873
|
+
* // result.items => mảng 20 bản ghi của trang 2
|
|
200
874
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
875
|
+
* // Kết hợp join để lấy tên role kèm theo
|
|
876
|
+
* await userModel.findWithPagination(
|
|
877
|
+
* { status: 1 },
|
|
878
|
+
* { id: 'DESC' },
|
|
879
|
+
* { table: 'roles', on: { left: 'users.roleId', right: 'roles.id', operator: '=' } },
|
|
880
|
+
* [{ column: 'users.id' }, { column: 'users.fullname' }, { column: 'roles.name', alias: 'roleName' }]
|
|
881
|
+
* );
|
|
882
|
+
* ```
|
|
207
883
|
*/
|
|
208
|
-
findWithPagination(filter?:
|
|
884
|
+
findWithPagination(filter?: Filter<T>, order?: Record<string, string> | false, join?: JoinType | JoinType[] | false, select?: SqlSelectInput | false, page?: number, limit?: number): Promise<PaginationResult<T>>;
|
|
209
885
|
}
|
|
886
|
+
/**
|
|
887
|
+
* Instance model dùng ở những vị trí không cần biết entity cụ thể, ví dụ container
|
|
888
|
+
* `Models` hay khai báo `modifieds`.
|
|
889
|
+
*
|
|
890
|
+
* Vì sao dùng `any` thay vì `ModelRow`: `BaseModel<T>` là **bất biến** (invariant)
|
|
891
|
+
* trong `T` — `T` xuất hiện ở cả vị trí tham số (`save(item: Partial<T>)`) lẫn kiểu
|
|
892
|
+
* trả về (`Promise<T | false>`) — nên `BaseModel<UserItem>` không thể gán cho
|
|
893
|
+
* `BaseModel<ModelRow>`. Nếu ràng buộc bằng `ModelRow` thì container sẽ không nhận
|
|
894
|
+
* được bất kỳ model nào khai báo entity bằng `interface`.
|
|
895
|
+
*
|
|
896
|
+
* Ở các vị trí này không có hàm nào của model được gọi, nên ràng buộc chỉ mang tính
|
|
897
|
+
* gợi ý; kiểu chính xác của từng model vẫn được giữ nguyên qua `InstanceType` trong
|
|
898
|
+
* `Models<T>`.
|
|
899
|
+
*/
|
|
900
|
+
type AnyBaseModel = BaseModel<any>;
|
|
901
|
+
/** @see {@link AnyBaseModel} — phiên bản interface cho các vị trí tương tự. */
|
|
902
|
+
type AnyIBaseModel = IBaseModel<any>;
|
|
210
903
|
//#endregion
|
|
211
904
|
//#region src/Common.d.ts
|
|
212
905
|
/**
|
|
@@ -214,19 +907,56 @@ declare abstract class BaseModel<T extends Record<string, any> & {
|
|
|
214
907
|
* @param value Chuỗi mật khẩu gốc
|
|
215
908
|
* @returns Chuỗi mật khẩu đã được hash
|
|
216
909
|
*/
|
|
217
|
-
declare function makePassword(value:
|
|
910
|
+
declare function makePassword(value: string | Buffer): Promise<string>;
|
|
218
911
|
/**
|
|
219
912
|
* 🔐 Kiểm tra mật khẩu với hash
|
|
220
913
|
* @param value Mật khẩu người dùng nhập
|
|
221
914
|
* @param hashedValue Mật khẩu đã hash lưu trong DB
|
|
222
915
|
* @returns true nếu khớp, false nếu không
|
|
223
916
|
*/
|
|
224
|
-
declare function checkPassword(value:
|
|
917
|
+
declare function checkPassword(value: string | Buffer, hashedValue: string): Promise<boolean>;
|
|
918
|
+
/**
|
|
919
|
+
* 🛡️ Escape các ký tự đặc biệt của HTML.
|
|
920
|
+
*
|
|
921
|
+
* Đây là cách **loại bỏ rủi ro XSS triệt để** thay vì "xoá thẻ nguy hiểm": escape
|
|
922
|
+
* làm cho mọi thẻ HTML trở thành **văn bản hiển thị**, nên không tồn tại payload nào
|
|
923
|
+
* lọt qua được — kể cả `onerror`, `onload`, `javascript:`, `<svg>`, `<iframe>`...
|
|
924
|
+
*
|
|
925
|
+
* Phạm vi: escape `& < > " ' \`` — đủ cho cả ngữ cảnh văn bản lẫn thuộc tính HTML.
|
|
926
|
+
*
|
|
927
|
+
* Lưu ý: escape **mọi thứ**, kể cả thẻ vô hại. Nếu cần lưu rich-text (HTML do
|
|
928
|
+
* người dùng soạn bằng trình soạn thảo), hãy dùng thư viện sanitizer chuyên dụng
|
|
929
|
+
* (`sanitize-html`, `DOMPurify`) thay vì escape.
|
|
930
|
+
*
|
|
931
|
+
* @param input Chuỗi cần escape
|
|
932
|
+
* @returns Chuỗi đã escape; input không phải string trả `''`
|
|
933
|
+
*
|
|
934
|
+
* @example
|
|
935
|
+
* ```ts
|
|
936
|
+
* escapeHtml('<img src=x onerror=alert(1)>');
|
|
937
|
+
* // => '<img src=x onerror=alert(1)>' — hiển thị an toàn, không thực thi
|
|
938
|
+
* ```
|
|
939
|
+
*/
|
|
940
|
+
declare function escapeHtml(input: string): string;
|
|
225
941
|
/**
|
|
226
|
-
*
|
|
942
|
+
* 🎲 Tạo chuỗi ngẫu nhiên bằng CSPRNG (`node:crypto`).
|
|
943
|
+
*
|
|
944
|
+
* Dùng `crypto.randomInt()` chứ không phải `Math.random()`: `Math.random()` dùng
|
|
945
|
+
* thuật toán PRNG không phải sinh hoàn toàn và **không đảm bảo an toàn về mật mã**,
|
|
946
|
+
* tức chuỗi sinh ra có thể bị dự đoán nếu dùng cho OTP, token đặt lại mật khẩu hay
|
|
947
|
+
* khoá phiên.
|
|
948
|
+
*
|
|
949
|
+
* Tập ký tự sinh ra giống hệt bản cũ nên chuỗi trả về tương thích ngược.
|
|
950
|
+
*
|
|
227
951
|
* @param length Độ dài chuỗi
|
|
228
|
-
* @param az Có bao gồm chữ cái hay không
|
|
229
|
-
* @returns Chuỗi
|
|
952
|
+
* @param az Có bao gồm chữ cái hay không (mặc định chỉ chữ số)
|
|
953
|
+
* @returns Chuỗi ngẫu nhiên
|
|
954
|
+
*
|
|
955
|
+
* @example
|
|
956
|
+
* ```ts
|
|
957
|
+
* randomText(6); // '428193' — OTP
|
|
958
|
+
* randomText(32, true); // 32 ký tự chữ + số — CSRF token
|
|
959
|
+
* ```
|
|
230
960
|
*/
|
|
231
961
|
declare function randomText(length: number, az?: boolean): string;
|
|
232
962
|
/**
|
|
@@ -250,7 +980,7 @@ declare function slugify(text: string): string;
|
|
|
250
980
|
* @param headers Header bổ sung
|
|
251
981
|
* @returns JSON response
|
|
252
982
|
*/
|
|
253
|
-
declare function callFetchApi(url: string, data?:
|
|
983
|
+
declare function callFetchApi(url: string, data?: unknown, type?: string, ctype?: string, headers?: Record<string, string>): Promise<any>;
|
|
254
984
|
/**
|
|
255
985
|
* 🧠 Tạo chuỗi keyword phục vụ search
|
|
256
986
|
* @param text Chuỗi đầu vào
|
|
@@ -300,7 +1030,7 @@ declare function toMySQLDateNowVN(): string;
|
|
|
300
1030
|
* @param df Giá trị mặc định
|
|
301
1031
|
* @returns Giá trị hoặc default
|
|
302
1032
|
*/
|
|
303
|
-
declare function getValue(item:
|
|
1033
|
+
declare function getValue<T>(item: Record<string, unknown> | T | null | undefined, key: keyof T | string, df?: unknown): unknown;
|
|
304
1034
|
/**
|
|
305
1035
|
* ➕ Tính tổng 1 cột trong mảng object
|
|
306
1036
|
* @param items Mảng object
|
|
@@ -357,28 +1087,28 @@ declare function getTrueFlaseStatus(id: string | boolean | number, color?: boole
|
|
|
357
1087
|
* @param arr2 Mảng thứ hai
|
|
358
1088
|
* @returns Mảng chứa các phần tử chỉ xuất hiện ở một trong hai mảng
|
|
359
1089
|
*/
|
|
360
|
-
declare function getDiffArr(arr1:
|
|
1090
|
+
declare function getDiffArr<T>(arr1: T[], arr2: T[]): T[];
|
|
361
1091
|
/**
|
|
362
1092
|
* 📦 Kiểm tra mảng arr1 có phải là tập con của arr2 hay không
|
|
363
1093
|
* @param arr1 Mảng cần kiểm tra
|
|
364
1094
|
* @param arr2 Mảng cha
|
|
365
1095
|
* @returns true nếu mọi phần tử arr1 đều tồn tại trong arr2
|
|
366
1096
|
*/
|
|
367
|
-
declare function isSubArr(arr1:
|
|
1097
|
+
declare function isSubArr<T>(arr1: T[], arr2: T[]): boolean;
|
|
368
1098
|
/**
|
|
369
1099
|
* ➖ Lấy các phần tử chỉ có trong mảng thứ nhất
|
|
370
1100
|
* @param arr1 Mảng gốc
|
|
371
1101
|
* @param arr2 Mảng so sánh
|
|
372
1102
|
* @returns Mảng các phần tử chỉ tồn tại trong arr1
|
|
373
1103
|
*/
|
|
374
|
-
declare function getArrOnlyInFirst(arr1:
|
|
1104
|
+
declare function getArrOnlyInFirst<T>(arr1: T[], arr2: T[]): T[];
|
|
375
1105
|
/**
|
|
376
1106
|
* 📤 Lấy danh sách giá trị của một cột trong mảng object
|
|
377
1107
|
* @param arr Mảng object
|
|
378
1108
|
* @param field Tên field cần lấy
|
|
379
1109
|
* @returns Mảng giá trị của field
|
|
380
1110
|
*/
|
|
381
|
-
declare function getArrColumn(arr:
|
|
1111
|
+
declare function getArrColumn<T extends Record<string, unknown>>(arr: T[], field: keyof T | string): any[];
|
|
382
1112
|
/**
|
|
383
1113
|
* ➕ Tính tổng một cột trong mảng object với điều kiện tùy chọn
|
|
384
1114
|
* @param arr Mảng object
|
|
@@ -386,7 +1116,7 @@ declare function getArrColumn(arr: any[], field: string): any[];
|
|
|
386
1116
|
* @param conditions Điều kiện lọc (optional)
|
|
387
1117
|
* @returns Tổng giá trị thỏa điều kiện
|
|
388
1118
|
*/
|
|
389
|
-
declare function sumArrColumn<T extends Record<string,
|
|
1119
|
+
declare function sumArrColumn<T extends Record<string, unknown>>(arr: T[], sumColumn: keyof T, conditions?: Partial<T>): number;
|
|
390
1120
|
/**
|
|
391
1121
|
* 🔢 Chuyển chuỗi số sang Decimal.js an toàn
|
|
392
1122
|
* @param value Chuỗi hoặc số (có thể chứa ký tự khác)
|
|
@@ -399,7 +1129,7 @@ declare function decimalNumber(value: string | number): Decimal.Decimal;
|
|
|
399
1129
|
* @param df Giá trị mặc định nếu parse lỗi
|
|
400
1130
|
* @returns Object | Array | df
|
|
401
1131
|
*/
|
|
402
|
-
declare function stringToJson(input:
|
|
1132
|
+
declare function stringToJson<T = Record<string, unknown>>(input: unknown, df?: T): any;
|
|
403
1133
|
/**
|
|
404
1134
|
* 🌐 Lấy danh sách ngôn ngữ
|
|
405
1135
|
* @param ids Danh sách id cần lọc hoặc false để lấy tất cả
|
|
@@ -411,20 +1141,92 @@ declare function getLangs(ids?: number[] | false): {
|
|
|
411
1141
|
}[];
|
|
412
1142
|
//#endregion
|
|
413
1143
|
//#region src/Csrf.d.ts
|
|
1144
|
+
/**
|
|
1145
|
+
* Kiểm tra xem phương thức HTTP có phải là method làm thay đổi trạng thái (POST, PUT, PATCH, DELETE) hay không.
|
|
1146
|
+
*
|
|
1147
|
+
* @param method Tên phương thức HTTP (vd: 'GET', 'POST', 'delete')
|
|
1148
|
+
* @returns `true` nếu method thuộc POST, PUT, PATCH, DELETE; ngược lại `false`
|
|
1149
|
+
*/
|
|
414
1150
|
declare function isStateChangingMethod(method: string): boolean;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
1151
|
+
interface CsrfRequest {
|
|
1152
|
+
method?: string;
|
|
1153
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
1154
|
+
body?: Record<string, unknown> | null;
|
|
1155
|
+
}
|
|
1156
|
+
interface CsrfSession {
|
|
1157
|
+
csrfToken?: string;
|
|
1158
|
+
}
|
|
1159
|
+
interface CsrfProtectionOptions {
|
|
1160
|
+
expectedToken?: string;
|
|
1161
|
+
}
|
|
1162
|
+
interface CsrfProtectionResult {
|
|
420
1163
|
allowed: boolean;
|
|
421
|
-
reason: string;
|
|
422
|
-
}
|
|
1164
|
+
reason: string | null;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Trích xuất CSRF token từ request Fastify/Express.
|
|
1168
|
+
* Token có thể được gửi qua header (`x-csrf-token`, `X-CSRF-Token`) hoặc body (`csrfToken`, `csrf_token`).
|
|
1169
|
+
*
|
|
1170
|
+
* @param req Đối tượng request
|
|
1171
|
+
* @returns Chuỗi CSRF token hoặc rỗng nếu không tìm thấy
|
|
1172
|
+
*/
|
|
1173
|
+
declare function getCsrfTokenFromRequest(req: CsrfRequest | null | undefined): string;
|
|
1174
|
+
/**
|
|
1175
|
+
* Xác thực bảo vệ chống CSRF cho request.
|
|
1176
|
+
* - Với GET/HEAD/OPTIONS...: bỏ qua kiểm tra, luôn cho phép.
|
|
1177
|
+
* - Với POST/PUT/PATCH/DELETE: so sánh token trong session với token gửi từ request.
|
|
1178
|
+
*
|
|
1179
|
+
* Token được so sánh bằng `safeEqual()` (constant-time) để không bị lộ qua
|
|
1180
|
+
* timing attack.
|
|
1181
|
+
*
|
|
1182
|
+
* @param req Đối tượng request
|
|
1183
|
+
* @param session Đối tượng session chứa `csrfToken`
|
|
1184
|
+
* @param options Tùy chọn bổ sung, có thể truyền `expectedToken` nếu không nằm trong session
|
|
1185
|
+
* @returns Đối tượng `{ allowed: boolean, reason: string | null }`
|
|
1186
|
+
*
|
|
1187
|
+
* @example
|
|
1188
|
+
* ```ts
|
|
1189
|
+
* const check = ensureCsrfProtection(req, session);
|
|
1190
|
+
* if (!check.allowed) {
|
|
1191
|
+
* throw new ClientError('CSRF token không hợp lệ', 403);
|
|
1192
|
+
* }
|
|
1193
|
+
* ```
|
|
1194
|
+
*/
|
|
1195
|
+
declare function ensureCsrfProtection(req: CsrfRequest | null | undefined, session: CsrfSession | null | undefined, options?: CsrfProtectionOptions): CsrfProtectionResult;
|
|
1196
|
+
//#endregion
|
|
1197
|
+
//#region src/Crypto.d.ts
|
|
1198
|
+
/**
|
|
1199
|
+
* So sánh hai chuỗi bí mật theo cách chống **timing attack**.
|
|
1200
|
+
*
|
|
1201
|
+
* `===` / `!==` dừng so sánh ngay khi gặp ký tự khác nhau, nên thời gian thực thi
|
|
1202
|
+
* phản ánh vị trí ký tự đầu tiên lệch → kẻ tấn công có thể dò từng ký tự secret.
|
|
1203
|
+
* `timingSafeEqual` so sánh toàn bộ byte trong thời gian cố định.
|
|
1204
|
+
*
|
|
1205
|
+
* Cả hai chuỗi được băm SHA-256 trước nhằm:
|
|
1206
|
+
* - luôn có cùng độ dài 32 byte để `timingSafeEqual` không ném lỗi khi hai chuỗi
|
|
1207
|
+
* khác độ dài;
|
|
1208
|
+
* - không làm lộ độ dài của secret qua thời gian chạy.
|
|
1209
|
+
*
|
|
1210
|
+
* @param a Chuỗi thứ nhất (ví dụ token người dùng gửi lên)
|
|
1211
|
+
* @param b Chuỗi thứ hai (ví dụ secret lưu trong session/env)
|
|
1212
|
+
* @returns `true` khi hai chuỗi giống nhau
|
|
1213
|
+
*
|
|
1214
|
+
* @example
|
|
1215
|
+
* ```ts
|
|
1216
|
+
* safeEqual(req.headers['x-api-key'] ?? '', process.env.API_KEY ?? '');
|
|
1217
|
+
* ```
|
|
1218
|
+
*/
|
|
1219
|
+
declare function safeEqual(a: string, b: string): boolean;
|
|
423
1220
|
//#endregion
|
|
424
1221
|
//#region src/FastRequest.d.ts
|
|
425
1222
|
/**
|
|
426
1223
|
* Kiểu dữ liệu file upload tạm
|
|
427
1224
|
*/
|
|
1225
|
+
type RequestValue = string | number | boolean | null | undefined | RequestValue[] | {
|
|
1226
|
+
[key: string]: RequestValue;
|
|
1227
|
+
};
|
|
1228
|
+
type RequestBody = Record<string, RequestValue | unknown>;
|
|
1229
|
+
type FilterType = string;
|
|
428
1230
|
interface FileType$1 {
|
|
429
1231
|
filename: string;
|
|
430
1232
|
basename: string;
|
|
@@ -449,7 +1251,7 @@ interface FileType$1 {
|
|
|
449
1251
|
* - Import / export / upload file
|
|
450
1252
|
*/
|
|
451
1253
|
declare class FastRequest {
|
|
452
|
-
[x: string]:
|
|
1254
|
+
[x: string]: unknown;
|
|
453
1255
|
/** Fastify request gốc */
|
|
454
1256
|
private req;
|
|
455
1257
|
/** Body sau khi parse */
|
|
@@ -465,13 +1267,19 @@ declare class FastRequest {
|
|
|
465
1267
|
*/
|
|
466
1268
|
constructor(req: FastifyRequest);
|
|
467
1269
|
/**
|
|
468
|
-
*
|
|
1270
|
+
* Parse dữ liệu request vào `body`/`files` để dùng cho các getter.
|
|
469
1271
|
*
|
|
470
|
-
*
|
|
471
|
-
* -
|
|
472
|
-
*
|
|
473
|
-
* -
|
|
474
|
-
*
|
|
1272
|
+
* Điều kiện xử lý:
|
|
1273
|
+
* - `req.body` truthy được dùng nguyên trạng; nhánh multipart chỉ chạy khi
|
|
1274
|
+
* body falsy và `req.isMultipart()` trả `true`.
|
|
1275
|
+
* - Multipart field trùng tên: giá trị đầu giữ scalar, lần thứ hai trở đi
|
|
1276
|
+
* được gom thành mảng theo thứ tự.
|
|
1277
|
+
* - File chỉ được ghi khi `FileUpload.checkDefFile(...)` trả `true`; nếu ghi
|
|
1278
|
+
* lỗi, thông báo được log nhưng entry file vẫn được thêm với đường dẫn đích.
|
|
1279
|
+
* - Thư mục `uploads/tmb` được tạo nếu chưa tồn tại. Tên file dùng basename
|
|
1280
|
+
* đã slugify nhưng không được bảo đảm unique nếu các field trùng tên file.
|
|
1281
|
+
*
|
|
1282
|
+
* @returns `Promise<void>`
|
|
475
1283
|
*/
|
|
476
1284
|
start: () => Promise<void>;
|
|
477
1285
|
/**
|
|
@@ -488,54 +1296,226 @@ declare class FastRequest {
|
|
|
488
1296
|
/** Check AJAX request */
|
|
489
1297
|
isAjax(): boolean;
|
|
490
1298
|
/**
|
|
491
|
-
* Lấy
|
|
1299
|
+
* Lấy một giá trị từ body đã được `start()` parse.
|
|
1300
|
+
*
|
|
1301
|
+
* Điều kiện xử lý:
|
|
1302
|
+
* - `name` rỗng: trả toàn bộ `this.body` mà không filter.
|
|
1303
|
+
* - Field không tồn tại: trả `defaultValue`.
|
|
1304
|
+
* - Field là object có thuộc tính `value !== undefined`: lấy `field.value`.
|
|
1305
|
+
* - Các trường hợp còn lại: chuyển qua `FastRequest.filterSafeData()`.
|
|
1306
|
+
*
|
|
1307
|
+
* `defaultValue` được truyền xuống filter, nên còn được dùng khi `int`/`number`
|
|
1308
|
+
* không chuyển đổi được. Giá trị có sẵn kiểu `number` luôn được giữ nguyên.
|
|
1309
|
+
*
|
|
1310
|
+
* @param name Tên field; chuỗi rỗng dùng để lấy toàn bộ body
|
|
1311
|
+
* @param defaultValue Giá trị khi thiếu field hoặc chuyển kiểu số thất bại
|
|
1312
|
+
* @param type Kiểu filter, xem `FastRequest.filterSafeData`
|
|
1313
|
+
* @returns Toàn bộ body hoặc giá trị đã xử lý
|
|
1314
|
+
* @throws {Error} `invalid_max_safe_integer` hoặc `invalid_min_safe_integer`
|
|
1315
|
+
* khi giá trị số vượt giới hạn an toàn
|
|
1316
|
+
*/
|
|
1317
|
+
getPost(name?: string, defaultValue?: unknown, type?: string): any;
|
|
1318
|
+
/**
|
|
1319
|
+
* Lấy giá trị của field đa ngôn ngữ có tên dạng `field{lang}`.
|
|
1320
|
+
*
|
|
1321
|
+
* Điều kiện xử lý:
|
|
1322
|
+
* - `name` rỗng hoặc không khớp đúng mẫu `field{lang}`: trả `defaultValue`.
|
|
1323
|
+
* - `field` trong body là chuỗi JSON: parse thành object/array trước khi đọc key `lang`.
|
|
1324
|
+
* - Body JSON không hợp lệ hoặc không đúng cấu trúc được xem như không có key `lang`.
|
|
1325
|
+
* - Giá trị tìm thấy được chuyển qua `FastRequest.filterSafeData()` với `defaultValue`.
|
|
1326
|
+
*
|
|
1327
|
+
* Với field lồng không tồn tại, kết quả phụ thuộc `type`: `int`/`number` trả
|
|
1328
|
+
* `defaultValue`; các loại chuỗi thông thường trả `''`, còn `raw` có thể trả `undefined`.
|
|
1329
|
+
*
|
|
1330
|
+
* @param name Tên theo mẫu `field{lang}`
|
|
1331
|
+
* @param defaultValue Giá trị khi tên sai hoặc dùng làm fallback của filter số
|
|
1332
|
+
* @param type Kiểu filter, xem `FastRequest.filterSafeData`
|
|
1333
|
+
* @returns Giá trị đa ngôn ngữ đã filter
|
|
1334
|
+
*/
|
|
1335
|
+
getLangPost(name: string, defaultValue?: unknown, type?: string): any;
|
|
1336
|
+
/**
|
|
1337
|
+
* Lấy một route param từ `request.params`.
|
|
1338
|
+
*
|
|
1339
|
+
* Điều kiện xử lý:
|
|
1340
|
+
* - Không có `params`, hoặc `params[name]` là falsy (`undefined`, `null`, `''`,
|
|
1341
|
+
* `0`, `false`, `NaN`): trả `defaultValue`.
|
|
1342
|
+
* - Giá trị truthy được chuyển qua `FastRequest.filterSafeData()`.
|
|
1343
|
+
*
|
|
1344
|
+
* `defaultValue` không được truyền vào filter. Vì vậy khi `type='int'` hoặc
|
|
1345
|
+
* `type='number'` nhưng chuyển đổi thất bại, hàm trả `''` (fallback mặc định
|
|
1346
|
+
* của filter), không trả `defaultValue` đã truyền cho hàm.
|
|
1347
|
+
*
|
|
1348
|
+
* @param name Tên route param
|
|
1349
|
+
* @param defaultValue Giá trị khi param không tồn tại hoặc có giá trị falsy
|
|
1350
|
+
* @param type Kiểu filter, xem `FastRequest.filterSafeData`
|
|
1351
|
+
* @returns Giá trị param đã filter hoặc `defaultValue`
|
|
1352
|
+
* @throws {Error} Khi chuyển số vượt giới hạn `Number.MIN_SAFE_INTEGER` đến
|
|
1353
|
+
* `Number.MAX_SAFE_INTEGER`
|
|
1354
|
+
*/
|
|
1355
|
+
getParam(name: string, defaultValue?: unknown, type?: string): any;
|
|
1356
|
+
/**
|
|
1357
|
+
* Lấy một query-string value hoặc toàn bộ query từ `request.query`.
|
|
1358
|
+
*
|
|
1359
|
+
* Điều kiện xử lý:
|
|
1360
|
+
* - `name` rỗng: trả toàn bộ `request.query` mà không filter.
|
|
1361
|
+
* - `query[name]` là `undefined`: trả `defaultValue`.
|
|
1362
|
+
* - Giá trị tồn tại (kể cả `null`, `''`, `0`, `false`): chuyển qua
|
|
1363
|
+
* `FastRequest.filterSafeData()`.
|
|
1364
|
+
*
|
|
1365
|
+
* `defaultValue` không được truyền vào filter. Vì vậy khi chuyển `int`/`number`
|
|
1366
|
+
* thất bại, hàm trả `''` chứ không trả `defaultValue` của hàm.
|
|
1367
|
+
*
|
|
1368
|
+
* @param name Tên query; chuỗi rỗng dùng để lấy toàn bộ query
|
|
1369
|
+
* @param defaultValue Giá trị khi query không có key
|
|
1370
|
+
* @param type Kiểu filter, xem `FastRequest.filterSafeData`
|
|
1371
|
+
* @returns Toàn bộ query hoặc giá trị đã filter
|
|
1372
|
+
* @throws {Error} Khi chuyển số vượt giới hạn an toàn
|
|
1373
|
+
*/
|
|
1374
|
+
get(name?: string, defaultValue?: unknown, type?: string): any;
|
|
1375
|
+
/**
|
|
1376
|
+
* Kiểm tra key query có tồn tại hay không.
|
|
1377
|
+
*
|
|
1378
|
+
* Với một tên, tồn tại nghĩa là `query[name] !== undefined`; vì vậy `null`, chuỗi
|
|
1379
|
+
* rỗng, `0` và `false` vẫn được xem là có. Với mảng tên, tất cả key phải tồn tại
|
|
1380
|
+
* (`every`); mảng rỗng trả `true`.
|
|
1381
|
+
*
|
|
1382
|
+
* @param names Một tên query hoặc danh sách tên
|
|
1383
|
+
* @returns `true` khi toàn bộ tên được yêu cầu tồn tại
|
|
1384
|
+
*/
|
|
1385
|
+
has(names: string | string[]): boolean;
|
|
1386
|
+
/**
|
|
1387
|
+
* Kiểm tra field body đã parse có tồn tại hay không.
|
|
492
1388
|
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
1389
|
+
* Với một tên, tồn tại nghĩa là `body[name] !== undefined`; vì vậy `null`, chuỗi
|
|
1390
|
+
* rỗng, `0` và `false` vẫn được xem là có. Với mảng tên, tất cả field phải tồn
|
|
1391
|
+
* tại (`every`); mảng rỗng trả `true`.
|
|
496
1392
|
*
|
|
497
|
-
* @
|
|
1393
|
+
* @param names Một tên field hoặc danh sách tên field
|
|
1394
|
+
* @returns `true` khi toàn bộ field được yêu cầu tồn tại
|
|
498
1395
|
*/
|
|
499
|
-
|
|
1396
|
+
hasPost(names: string | string[]): boolean;
|
|
500
1397
|
/**
|
|
501
|
-
* Lấy
|
|
1398
|
+
* Lấy header dưới dạng chuỗi.
|
|
502
1399
|
*
|
|
503
|
-
*
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
*
|
|
508
|
-
*/
|
|
509
|
-
getParam(name: string, defaultValue?: any, type?: string): any;
|
|
510
|
-
/**
|
|
511
|
-
* Lấy query string
|
|
1400
|
+
* Key được chuyển về chữ thường để phù hợp header của Node/Fastify. Giá trị được
|
|
1401
|
+
* ép sang chuỗi; header thiếu hoặc có giá trị falsy sau ép chuỗi sẽ trả `''`.
|
|
1402
|
+
*
|
|
1403
|
+
* @param key Tên header không phân biệt hoa thường
|
|
1404
|
+
* @returns Giá trị header hoặc chuỗi rỗng
|
|
512
1405
|
*/
|
|
513
|
-
get(name?: string, defaultValue?: any, type?: string): any;
|
|
514
|
-
/** Check tồn tại query */
|
|
515
|
-
has(names: string | string[]): boolean;
|
|
516
|
-
/** Check tồn tại post */
|
|
517
|
-
hasPost(names: string | string[]): boolean;
|
|
518
|
-
/** Lấy header */
|
|
519
1406
|
getHeader(key: string): string;
|
|
520
1407
|
/**
|
|
521
|
-
*
|
|
1408
|
+
* Lọc/chuyển kiểu một giá trị đầu vào.
|
|
1409
|
+
*
|
|
1410
|
+
* Điều kiện chung:
|
|
1411
|
+
* - Nếu `input` đã là `number`, hàm trả nguyên giá trị đó ngay và bỏ qua mọi
|
|
1412
|
+
* kiểm tra/filter còn lại. Vì vậy `int` không cắt phần thập phân của số đã có,
|
|
1413
|
+
* và số vượt safe range cũng không bị lỗi.
|
|
1414
|
+
* - `defaultValue` chỉ được dùng khi chuyển `int`/`number` ra `NaN`.
|
|
1415
|
+
* - `type` không thuộc các nhánh chuyên biệt sẽ so sánh `typeof input === type`;
|
|
1416
|
+
* không khớp thì trả `''`.
|
|
1417
|
+
*
|
|
1418
|
+
* Các kiểu được hỗ trợ:
|
|
1419
|
+
* - `ids`: mảng ID number; loại NaN và loại trùng, giữ thứ tự xuất hiện.
|
|
1420
|
+
* - `stringIds`: như `ids` nhưng chuyển từng ID thành chuỗi.
|
|
1421
|
+
* - `strings`: mảng chuỗi; mỗi phần tử được `stripTags`, loại trùng.
|
|
1422
|
+
* - `dQuoteIds`: mảng ID number, mỗi ID thành chuỗi `%"id"%`.
|
|
1423
|
+
* - `int`: `Number(input)`, kiểm tra safe integer rồi `Math.trunc`.
|
|
1424
|
+
* - `number`: `Number(input)`, giữ nguyên phần thập phân, kiểm tra safe range.
|
|
1425
|
+
* - `decimal`: `Decimal`; mọi ký tự ngoài số và dấu `.` bị loại.
|
|
1426
|
+
* - `stripTags`: bỏ **mọi** thẻ rồi trim. An toàn khi xuất ra dạng text, nhưng
|
|
1427
|
+
* có thể mất dữ liệu (ví dụ `a < b and c > d` thành `a d`).
|
|
1428
|
+
* - `html`: escape `& < > " ' \`` (xem `escapeHtml`) — **không thể bypass**, nhưng
|
|
1429
|
+
* thẻ vô hại cũng thành text. Dùng khi lưu dữ liệu người dùng rồi render HTML.
|
|
1430
|
+
* - `date`: chuỗi ISO hợp lệ thành `yyyy-MM-dd`, ngày sai trả `null`.
|
|
1431
|
+
* - `datetime`: chuỗi ISO hợp lệ thành `yyyy-MM-dd HH:mm:ss`, ngày sai trả `null`.
|
|
1432
|
+
* - `raw`: JSON string được parse; JSON sai hoặc input không phải string được giữ nguyên.
|
|
1433
|
+
* - Ngoài ra có thể dùng tên `typeof`, ví dụ `boolean`, `object`, `string`.
|
|
1434
|
+
*
|
|
1435
|
+
* `date`, `datetime`, `html` và `stripTags` chỉ được áp dụng khi input là string.
|
|
1436
|
+
* Với input không phù hợp, chúng rơi xuống kiểm tra `typeof` và thường trả `''`.
|
|
522
1437
|
*
|
|
523
1438
|
* @param input Dữ liệu đầu vào
|
|
524
|
-
* @param type Kiểu filter
|
|
525
|
-
* @param defaultValue
|
|
1439
|
+
* @param type Kiểu filter/chuyển đổi; mặc định `stripTags`
|
|
1440
|
+
* @param defaultValue Fallback khi `int`/`number` không parse được; mặc định `''`
|
|
1441
|
+
* @returns Giá trị đã filter/chuyển kiểu
|
|
1442
|
+
* @throws {Error} `invalid_max_safe_integer` hoặc `invalid_min_safe_integer` khi
|
|
1443
|
+
* input không phải number sẵn có và giá trị chuyển đổi vượt safe range
|
|
1444
|
+
*/
|
|
1445
|
+
static filterSafeData(input: unknown, type?: string, defaultValue?: unknown): any;
|
|
1446
|
+
/**
|
|
1447
|
+
* Parse raw input.
|
|
1448
|
+
*
|
|
1449
|
+
* - Input không phải string được trả nguyên trạng.
|
|
1450
|
+
* - String là JSON hợp lệ được parse thành kiểu tương ứng, kể cả null, boolean,
|
|
1451
|
+
* number, string, array hoặc object.
|
|
1452
|
+
* - JSON không hợp lệ được trả nguyên chuỗi gốc.
|
|
1453
|
+
*
|
|
1454
|
+
* @param input Giá trị cần parse
|
|
1455
|
+
* @returns Giá trị đã parse hoặc input gốc
|
|
526
1456
|
*/
|
|
527
|
-
static filterSafeData(input: any, type?: string, defaultValue?: any): any;
|
|
528
|
-
/** Parse raw JSON input */
|
|
529
1457
|
private static parseRawInput;
|
|
530
|
-
/**
|
|
1458
|
+
/**
|
|
1459
|
+
/**
|
|
1460
|
+
* Escape HTML — biến mọi thẻ thành văn bản hiển thị.
|
|
1461
|
+
*
|
|
1462
|
+
* > ✅ **Không còn payload nào lọt qua.** Trước đây hàm này dùng regex để xoá
|
|
1463
|
+
* > riêng các thẻ `script`/`style`/`meta`, khiến các vector khác lọt nguyên vẹn
|
|
1464
|
+
* > (đã kiểm chứng): `<img src=x onerror=...>`, `<svg/onload=...>`,
|
|
1465
|
+
* > `<a href="javascript:...">`, `<iframe>`, `<body onload=...>`.
|
|
1466
|
+
* >
|
|
1467
|
+
* > Nay escape `& < > " ' \``, nên mọi thẻ đều thành text — không thể bypass.
|
|
1468
|
+
* > Đổi lại, **thẻ vô hại cũng bị escape**: `<p>Keep</p>` trở thành
|
|
1469
|
+
* > `<p>Keep</p>`. Nếu cần lưu rich-text, hãy dùng thư viện sanitizer
|
|
1470
|
+
* > chuyên dụng (`sanitize-html`, `DOMPurify`).
|
|
1471
|
+
*
|
|
1472
|
+
* @param input Chuỗi cần escape
|
|
1473
|
+
* @returns Chuỗi đã escape; input không phải string trả `''`
|
|
1474
|
+
*/
|
|
531
1475
|
private static filterHtml;
|
|
532
|
-
/**
|
|
1476
|
+
/**
|
|
1477
|
+
* Chuẩn hóa input thành mảng ID number.
|
|
1478
|
+
*
|
|
1479
|
+
* - Chuỗi JSON array được parse và dùng làm mảng.
|
|
1480
|
+
* - Chuỗi không phải JSON được thử `Number()`; số hợp lệ thành mảng một phần tử.
|
|
1481
|
+
* - Chuỗi không parse được trả `[]`.
|
|
1482
|
+
* - Mọi phần tử được `Number()`, bỏ `NaN`, rồi khử trùng; thứ tự đầu được giữ.
|
|
1483
|
+
* - Input không phải array/string trả `[]`.
|
|
1484
|
+
*
|
|
1485
|
+
* @param input Một ID, danh sách ID hoặc chuỗi JSON
|
|
1486
|
+
* @returns Mảng ID number duy nhất
|
|
1487
|
+
*/
|
|
533
1488
|
private static filterIds;
|
|
534
|
-
/**
|
|
1489
|
+
/**
|
|
1490
|
+
* Lọc mảng ID rồi chuyển từng ID sang chuỗi.
|
|
1491
|
+
*
|
|
1492
|
+
* Chuỗi JSON không phải array hoặc input không hợp lệ trả `[]`; mảng hợp lệ đi
|
|
1493
|
+
* qua `filterIds()` nên vẫn loại `NaN` và trùng trước khi chuyển `String`.
|
|
1494
|
+
*
|
|
1495
|
+
* @param input Một ID, danh sách ID hoặc chuỗi JSON
|
|
1496
|
+
* @returns Mảng ID dạng chuỗi duy nhất
|
|
1497
|
+
*/
|
|
535
1498
|
private static filterStringIds;
|
|
536
|
-
/**
|
|
1499
|
+
/**
|
|
1500
|
+
* Lọc và khử trùng một mảng giá trị.
|
|
1501
|
+
*
|
|
1502
|
+
* Từng phần tử được xử lý bằng `filterSafeData(..., 'stripTags')`; phần tử số được
|
|
1503
|
+
* giữ nguyên, chuỗi được bỏ thẻ và trim, giá trị khác thường trả `''`. Kết quả
|
|
1504
|
+
* được đưa vào `Set`, vì vậy so sánh trùng dùng semantics của JavaScript.
|
|
1505
|
+
*
|
|
1506
|
+
* @param input Danh sách giá trị
|
|
1507
|
+
* @returns Danh sách đã lọc và khử trùng
|
|
1508
|
+
*/
|
|
537
1509
|
private static filterStrings;
|
|
538
|
-
/**
|
|
1510
|
+
/**
|
|
1511
|
+
* Chuyển mảng ID thành từng mẫu dạng `%"id"%`.
|
|
1512
|
+
*
|
|
1513
|
+
* Trước hết ID được ép number, loại `NaN` và trùng giống `filterIds()`; sau đó
|
|
1514
|
+
* mỗi ID được bọc trong hai dấu `%` và một cặp dấu nháy kép.
|
|
1515
|
+
*
|
|
1516
|
+
* @param input Danh sách ID
|
|
1517
|
+
* @returns Danh sách mẫu `%"id"%`
|
|
1518
|
+
*/
|
|
539
1519
|
private static filterDQuoteIds;
|
|
540
1520
|
}
|
|
541
1521
|
//#endregion
|
|
@@ -565,11 +1545,26 @@ declare class FileUpload {
|
|
|
565
1545
|
constructor(files: {
|
|
566
1546
|
[key: string]: FileType[];
|
|
567
1547
|
}, uploadType?: string);
|
|
1548
|
+
/**
|
|
1549
|
+
* Nối `subPath` vào `baseDir` và bảo đảm kết quả vẫn nằm bên trong `baseDir`.
|
|
1550
|
+
*
|
|
1551
|
+
* Chặn **path traversal**: nếu không có bước này thì `folders = '../../etc'`
|
|
1552
|
+
* sẽ khiến file được ghi ra ngoài thư mục upload.
|
|
1553
|
+
*
|
|
1554
|
+
* @param baseDir Thư mục gốc (thư mục upload)
|
|
1555
|
+
* @param subPath Đường dẫn tương đối do ứng dụng truyền vào
|
|
1556
|
+
* @returns Đường dẫn tuyệt đối, chắc chắn nằm trong `baseDir`
|
|
1557
|
+
* @throws {Error} `invalid_upload_folder` khi `subPath` thoát ra khỏi `baseDir`
|
|
1558
|
+
*/
|
|
1559
|
+
private static resolveInside;
|
|
568
1560
|
/**
|
|
569
1561
|
* Copy danh sách file từ src sang dist
|
|
570
1562
|
* Dùng khi cần nhân bản file đã tồn tại
|
|
571
1563
|
*/
|
|
572
|
-
copyFiles(files:
|
|
1564
|
+
copyFiles(files: Array<{
|
|
1565
|
+
src: string;
|
|
1566
|
+
dist: string;
|
|
1567
|
+
}>): Promise<void>;
|
|
573
1568
|
/**
|
|
574
1569
|
* Upload 1 file theo field name
|
|
575
1570
|
* @returns đường dẫn file sau upload hoặc false
|
|
@@ -585,6 +1580,11 @@ declare class FileUpload {
|
|
|
585
1580
|
* - Tự tạo tên file theo timestamp
|
|
586
1581
|
* - Tạo folder nếu chưa tồn tại
|
|
587
1582
|
* - Move file từ temp sang thư mục upload
|
|
1583
|
+
*
|
|
1584
|
+
* `folders` và `file.filename` đều được kiểm tra để không thoát khỏi thư mục
|
|
1585
|
+
* upload (path traversal).
|
|
1586
|
+
*
|
|
1587
|
+
* @throws {Error} `invalid_upload_folder` khi `folders` hoặc tên file chứa đường dẫn thoát ra ngoài
|
|
588
1588
|
*/
|
|
589
1589
|
upload(file: FileType, folders?: string): Promise<string | false>;
|
|
590
1590
|
/**
|
|
@@ -598,6 +1598,8 @@ declare class FileUpload {
|
|
|
598
1598
|
static removeFiles: (pathFiles: string[] | boolean, uploadType?: string) => boolean;
|
|
599
1599
|
/**
|
|
600
1600
|
* Xóa 1 file theo đường dẫn
|
|
1601
|
+
*
|
|
1602
|
+
* Đường dẫn phải nằm trong thư mục upload, nếu không sẽ bị từ chối.
|
|
601
1603
|
*/
|
|
602
1604
|
static removeFile: (fileName: string | false, uploadType?: string) => boolean;
|
|
603
1605
|
/**
|
|
@@ -627,28 +1629,190 @@ declare class FileUpload {
|
|
|
627
1629
|
}
|
|
628
1630
|
//#endregion
|
|
629
1631
|
//#region src/JWTApp.d.ts
|
|
1632
|
+
interface JwtUser {
|
|
1633
|
+
id: number;
|
|
1634
|
+
fullname: string;
|
|
1635
|
+
}
|
|
1636
|
+
interface JwtRequest {
|
|
1637
|
+
ip?: string;
|
|
1638
|
+
headers: Record<string, string | string[] | undefined>;
|
|
1639
|
+
}
|
|
1640
|
+
interface JwtPayload {
|
|
1641
|
+
iss?: string;
|
|
1642
|
+
aud?: string;
|
|
1643
|
+
sub: number;
|
|
1644
|
+
name: string;
|
|
1645
|
+
ip?: string;
|
|
1646
|
+
us?: string;
|
|
1647
|
+
iat: number;
|
|
1648
|
+
exp: number;
|
|
1649
|
+
expired: number;
|
|
1650
|
+
accessToken: string;
|
|
1651
|
+
[key: string]: unknown;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Lớp tiện ích xử lý JSON Web Token (JWT) cho ứng dụng.
|
|
1655
|
+
* Hỗ trợ tạo token, xác thực token từ Authorization header hoặc Cookie,
|
|
1656
|
+
* và kiểm tra khóa bảo mật hệ thống.
|
|
1657
|
+
*/
|
|
630
1658
|
declare class JWTApp {
|
|
631
|
-
|
|
1659
|
+
/**
|
|
1660
|
+
* Tạo JWT token cho người dùng dựa trên thông tin IP, User-Agent và cấu hình env.
|
|
1661
|
+
* Biến môi trường sử dụng: `JWT_KEY`, `JWT_AUD`, `JWT_ISS`, `JWT_TIMEOUT` (mặc định 3600 giây).
|
|
1662
|
+
*
|
|
1663
|
+
* @param user Thông tin người dùng (yêu cầu thuộc tính `id`, `fullname`)
|
|
1664
|
+
* @param req Request Fastify/Express để lấy `ip` và `user-agent`
|
|
1665
|
+
* @returns Chuỗi JWT token đã được ký
|
|
1666
|
+
* @throws {Error} `missing_jwt_key` khi chưa cấu hình `JWT_KEY`
|
|
1667
|
+
*
|
|
1668
|
+
* @example
|
|
1669
|
+
* ```ts
|
|
1670
|
+
* const token = JWTApp.createToken({ id: 1, fullname: 'Admin' }, req);
|
|
1671
|
+
* ```
|
|
1672
|
+
*/
|
|
1673
|
+
static createToken(user: JwtUser, req: JwtRequest): string;
|
|
1674
|
+
/**
|
|
1675
|
+
* Xác thực JWT token từ request (header Bearer hoặc Cookie).
|
|
1676
|
+
* Kiểm tra tính hợp lệ của token, thời gian hết hạn, và so khớp User-Agent
|
|
1677
|
+
* cùng IP của client.
|
|
1678
|
+
*
|
|
1679
|
+
* Hạn dùng được kiểm tra hai lần: `exp` (giây, do `jsonwebtoken` kiểm tra) và
|
|
1680
|
+
* trường tuỳ chỉnh `expired` (mili-giây) do `createToken()` ghi kèm.
|
|
1681
|
+
*
|
|
1682
|
+
* @param req FastifyRequest (có thể kèm `cookies`)
|
|
1683
|
+
* @param isCookie `true` nếu đọc từ cookie `accessToken`, `false` nếu đọc từ header `Authorization: Bearer <token>`
|
|
1684
|
+
* @returns Payload đã giải mã và xác thực kèm thuộc tính `accessToken`
|
|
1685
|
+
* @throws {Error | ClientError} Khi thiếu `JWT_KEY`, token bị thiếu, không hợp lệ
|
|
1686
|
+
* (sai chữ ký, sai user-agent, sai ip, sai user ID) hoặc đã hết hạn
|
|
1687
|
+
*
|
|
1688
|
+
* @example
|
|
1689
|
+
* ```ts
|
|
1690
|
+
* const userPayload = JWTApp.verifyToken(request);
|
|
1691
|
+
* ```
|
|
1692
|
+
*/
|
|
632
1693
|
static verifyToken(req: FastifyRequest & {
|
|
633
1694
|
cookies?: Record<string, string | undefined>;
|
|
634
|
-
}, isCookie?: boolean):
|
|
1695
|
+
}, isCookie?: boolean): JwtPayload;
|
|
1696
|
+
/**
|
|
1697
|
+
* Kiểm tra Secret Key được truyền vào có khớp với biến môi trường `JWT_SECRET_KEY` hay không.
|
|
1698
|
+
* Thường dùng cho các webhook nội bộ hoặc giao tiếp giữa các service.
|
|
1699
|
+
*
|
|
1700
|
+
* So sánh dùng `safeEqual()` (constant-time) để không bị lộ secret qua
|
|
1701
|
+
* timing attack.
|
|
1702
|
+
*
|
|
1703
|
+
* @param key Chuỗi secret key cần kiểm tra
|
|
1704
|
+
* @throws {Error} Khi biến môi trường `JWT_SECRET_KEY` chưa được thiết lập
|
|
1705
|
+
* @throws {ClientError} Khi khóa không khớp
|
|
1706
|
+
*
|
|
1707
|
+
* @example
|
|
1708
|
+
* ```ts
|
|
1709
|
+
* JWTApp.checkSecretKey(apiKeyHeader);
|
|
1710
|
+
* ```
|
|
1711
|
+
*/
|
|
635
1712
|
static checkSecretKey(key: string): void;
|
|
636
1713
|
}
|
|
637
1714
|
//#endregion
|
|
638
1715
|
//#region src/Models.d.ts
|
|
639
|
-
|
|
1716
|
+
/**
|
|
1717
|
+
* Kiểu constructor cho class kế thừa từ BaseModel
|
|
1718
|
+
*
|
|
1719
|
+
* Dùng `AnyBaseModel` làm ràng buộc: `BaseModel<T>` bất biến trong `T` nên ràng
|
|
1720
|
+
* buộc bằng `ModelRow` sẽ loại bỏ mọi model khai báo entity bằng `interface`.
|
|
1721
|
+
* @see {@link AnyBaseModel}
|
|
1722
|
+
*/
|
|
1723
|
+
type ModelConstructor<T extends AnyBaseModel = AnyBaseModel> = new (pool: Pool) => T;
|
|
1724
|
+
/**
|
|
1725
|
+
* Kiểu map các model instance tương ứng với ModelConstructor
|
|
1726
|
+
*/
|
|
640
1727
|
type Models<T extends Record<string, ModelConstructor>> = { [K in keyof T]: InstanceType<T[K]>; };
|
|
1728
|
+
/**
|
|
1729
|
+
* Khởi tạo danh sách instance của các model từ modelMap và connection pool.
|
|
1730
|
+
*
|
|
1731
|
+
* @param modelMap Đối tượng chứa các class model constructor
|
|
1732
|
+
* @param pool MySQL Connection Pool
|
|
1733
|
+
* @returns Đối tượng chứa các instance model tương ứng
|
|
1734
|
+
*
|
|
1735
|
+
* @example
|
|
1736
|
+
* ```ts
|
|
1737
|
+
* const models = createModels({ user: UserModel, product: ProductModel }, pool);
|
|
1738
|
+
* ```
|
|
1739
|
+
*/
|
|
641
1740
|
declare function createModels<T extends Record<string, ModelConstructor>>(modelMap: T, pool: Pool): Models<T>;
|
|
1741
|
+
/**
|
|
1742
|
+
* Khởi tạo singleton container chứa các model toàn ứng dụng.
|
|
1743
|
+
* Nếu đã được khởi tạo trước đó, hàm trả về đối tượng hiện tại mà không khởi tạo lại.
|
|
1744
|
+
*
|
|
1745
|
+
* @param modelMap Đối tượng chứa các class model constructor
|
|
1746
|
+
* @param pool MySQL Connection Pool
|
|
1747
|
+
* @returns Singleton container chứa các instance model
|
|
1748
|
+
*
|
|
1749
|
+
* @example
|
|
1750
|
+
* ```ts
|
|
1751
|
+
* initModels({ user: UserModel, post: PostModel }, pool);
|
|
1752
|
+
* ```
|
|
1753
|
+
*/
|
|
642
1754
|
declare function initModels<T extends Record<string, ModelConstructor>>(modelMap: T, pool: Pool): Models<T>;
|
|
1755
|
+
/**
|
|
1756
|
+
* Lấy singleton container chứa các model đã khởi tạo.
|
|
1757
|
+
*
|
|
1758
|
+
* @template T Kiểu container models mong muốn
|
|
1759
|
+
* @returns Đối tượng các instance model
|
|
1760
|
+
* @throws {Error} Khi models chưa được gọi `initModels()`
|
|
1761
|
+
*
|
|
1762
|
+
* @example
|
|
1763
|
+
* ```ts
|
|
1764
|
+
* const { user, post } = getModels<AppModels>();
|
|
1765
|
+
* ```
|
|
1766
|
+
*/
|
|
643
1767
|
declare function getModels<T>(): T;
|
|
644
1768
|
//#endregion
|
|
645
1769
|
//#region src/MySQLSessionStore.d.ts
|
|
1770
|
+
type SessionData = Session;
|
|
1771
|
+
type SessionCallbackError = Error | null | undefined;
|
|
1772
|
+
type SessionResultCallback = (err: SessionCallbackError, session?: SessionData | null) => void;
|
|
1773
|
+
type SessionDoneCallback = (err?: SessionCallbackError) => void;
|
|
1774
|
+
interface SessionTableRow {
|
|
1775
|
+
data: string;
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Bộ lưu trữ Session MySQL cho @fastify/session.
|
|
1779
|
+
* Tự động tạo hoặc cập nhật session trong bảng `sessions` với thời gian hết hạn 14 ngày.
|
|
1780
|
+
*/
|
|
646
1781
|
declare class MySQLSessionStore implements SessionStore {
|
|
647
1782
|
private pool;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1783
|
+
/**
|
|
1784
|
+
* Số ngày session còn hiệu lực.
|
|
1785
|
+
*
|
|
1786
|
+
* Có thể chỉnh qua biến môi trường `SESSION_TTL_DAYS` (mặc định 14). Giá trị
|
|
1787
|
+
* không hợp lệ sẽ rơi về mặc định.
|
|
1788
|
+
*/
|
|
1789
|
+
private readonly ttlDays;
|
|
1790
|
+
/**
|
|
1791
|
+
* Khởi tạo session store với MySQL connection pool.
|
|
1792
|
+
* @param pool MySQL Pool
|
|
1793
|
+
* @param ttlDays Số ngày hết hạn; bỏ trống thì đọc `SESSION_TTL_DAYS` hoặc dùng 14
|
|
1794
|
+
*/
|
|
1795
|
+
constructor(pool: Pool, ttlDays?: number);
|
|
1796
|
+
/**
|
|
1797
|
+
* Lấy thông tin session theo session ID (`sid`).
|
|
1798
|
+
* @param sid Mã định danh session
|
|
1799
|
+
* @param cb Callback trả về lỗi hoặc session data (null nếu không tìm thấy)
|
|
1800
|
+
*/
|
|
1801
|
+
get(sid: string, cb: SessionResultCallback): Promise<void>;
|
|
1802
|
+
/**
|
|
1803
|
+
* Lưu hoặc cập nhật session data theo session ID (`sid`).
|
|
1804
|
+
* Thời hạn session được gia hạn mỗi lần lưu, mặc định 14 ngày.
|
|
1805
|
+
* @param sid Mã định danh session
|
|
1806
|
+
* @param session Dữ liệu session cần lưu
|
|
1807
|
+
* @param cb Callback hoàn tất
|
|
1808
|
+
*/
|
|
1809
|
+
set(sid: string, session: SessionData, cb: SessionDoneCallback): Promise<void>;
|
|
1810
|
+
/**
|
|
1811
|
+
* Xóa session khỏi cơ sở dữ liệu theo session ID (`sid`).
|
|
1812
|
+
* @param sid Mã định danh session cần hủy
|
|
1813
|
+
* @param cb Callback hoàn tất
|
|
1814
|
+
*/
|
|
1815
|
+
destroy(sid: string, cb: SessionDoneCallback): Promise<void>;
|
|
652
1816
|
}
|
|
653
1817
|
//#endregion
|
|
654
1818
|
//#region src/Route.d.ts
|
|
@@ -664,10 +1828,19 @@ declare class MySQLSessionStore implements SessionStore {
|
|
|
664
1828
|
* @property method HTTP method (get | post | put | delete...)
|
|
665
1829
|
* @property rateLimit Giới hạn request (optional)
|
|
666
1830
|
*/
|
|
1831
|
+
/**
|
|
1832
|
+
* Controller của route: một class được khởi tạo rồi gọi action theo tên.
|
|
1833
|
+
*
|
|
1834
|
+
* Không yêu cầu `Record<string, ...>` vì một class thông thường không có
|
|
1835
|
+
* implicit index signature, nên ràng buộc đó sẽ khiến `route.addGS(link, MyController, module)`
|
|
1836
|
+
* — đúng như README hướng dẫn — không compile. Việc controller có thật sự có
|
|
1837
|
+
* action tương ứng được kiểm tra lúc chạy ở phía đăng ký route.
|
|
1838
|
+
*/
|
|
1839
|
+
type RouteController = new (...args: never[]) => object;
|
|
667
1840
|
interface RouterType {
|
|
668
1841
|
link: string;
|
|
669
1842
|
module: string;
|
|
670
|
-
controller:
|
|
1843
|
+
controller: RouteController;
|
|
671
1844
|
action: string;
|
|
672
1845
|
method?: string;
|
|
673
1846
|
rateLimit?: number;
|
|
@@ -691,6 +1864,21 @@ interface RouterType {
|
|
|
691
1864
|
* - Khai báo routing tập trung
|
|
692
1865
|
* - Auto register route cho Fastify / Express
|
|
693
1866
|
*/
|
|
1867
|
+
interface AddGsOptions {
|
|
1868
|
+
/**
|
|
1869
|
+
* Định nghĩa route param cho `:code` trong `detail/:code`, `edit/:code` và
|
|
1870
|
+
* `delete/:code`.
|
|
1871
|
+
*
|
|
1872
|
+
* Mặc định `':code([0-9]+)'` — chỉ nhận mã dạng số. Truyền giá trị khác khi
|
|
1873
|
+
* mã nghiệp vụ không phải số, ví dụ:
|
|
1874
|
+
* - UUID: `':code([0-9a-fA-F-]{36})'`
|
|
1875
|
+
* - slug: `':code([a-z0-9-]+)'`
|
|
1876
|
+
* - chuỗi tuỳ ý: `':code'`
|
|
1877
|
+
*
|
|
1878
|
+
* @default ':code([0-9]+)'
|
|
1879
|
+
*/
|
|
1880
|
+
codeParam?: string;
|
|
1881
|
+
}
|
|
694
1882
|
declare class Route {
|
|
695
1883
|
/** Danh sách route */
|
|
696
1884
|
private routes;
|
|
@@ -700,7 +1888,7 @@ declare class Route {
|
|
|
700
1888
|
* @param router RouterType
|
|
701
1889
|
* @returns method (mặc định: GET)
|
|
702
1890
|
*/
|
|
703
|
-
static getMethod(router:
|
|
1891
|
+
static getMethod(router: Pick<RouterType, 'method'>): string;
|
|
704
1892
|
/**
|
|
705
1893
|
* Thêm danh sách route với prefix
|
|
706
1894
|
*
|
|
@@ -726,14 +1914,21 @@ declare class Route {
|
|
|
726
1914
|
* - POST /copy
|
|
727
1915
|
* - POST /import
|
|
728
1916
|
* - GET /export
|
|
729
|
-
* -
|
|
1917
|
+
* - POST /delete/:code
|
|
730
1918
|
* - POST /delete
|
|
731
1919
|
*
|
|
1920
|
+
* Mọi route làm thay đổi trạng thái đều dùng `POST`. Route `POST /delete/:code`
|
|
1921
|
+
* là `POST` (không phải `GET`) vì `GET` sẽ khiến thao tác xóa bị kích hoạt
|
|
1922
|
+
* bởi link, thẻ `<img>`, prefetch của trình duyệt — đồng thời `GET` không nằm
|
|
1923
|
+
* trong danh sách method mà `ensureCsrfProtection()` kiểm tra nên sẽ bị bỏ qua
|
|
1924
|
+
* CSRF.
|
|
1925
|
+
*
|
|
732
1926
|
* @param link Base URL (vd: /admin/product)
|
|
733
1927
|
* @param controller Controller xử lý
|
|
734
1928
|
* @param module Tên module
|
|
1929
|
+
* @param options Tùy chọn, xem {@link AddGsOptions} (mặc định `:code` chỉ nhận số)
|
|
735
1930
|
*/
|
|
736
|
-
addGS(link: string, controller:
|
|
1931
|
+
addGS(link: string, controller: RouteController, module: string, options?: AddGsOptions): void;
|
|
737
1932
|
/**
|
|
738
1933
|
* Lấy toàn bộ danh sách route
|
|
739
1934
|
*
|
|
@@ -743,27 +1938,153 @@ declare class Route {
|
|
|
743
1938
|
}
|
|
744
1939
|
//#endregion
|
|
745
1940
|
//#region src/Utils.d.ts
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1941
|
+
type AuditValue = string | number | boolean | null | undefined | AuditValue[] | {
|
|
1942
|
+
[key: string]: AuditValue;
|
|
1943
|
+
};
|
|
1944
|
+
type SanitizedAuditValue = string | number | boolean | null | undefined | SanitizedAuditValue[] | {
|
|
1945
|
+
[key: string]: SanitizedAuditValue;
|
|
1946
|
+
};
|
|
1947
|
+
type AuditRecord = Record<string, unknown>;
|
|
1948
|
+
/**
|
|
1949
|
+
* Loại bỏ các trường thông tin nhạy cảm (mật khẩu, token, secret, api key...) khỏi payload để ghi audit log an toàn.
|
|
1950
|
+
* Hàm duyệt đệ quy qua các object và mảng lồng nhau.
|
|
1951
|
+
*
|
|
1952
|
+
* @param value Dữ liệu cần làm sạch
|
|
1953
|
+
* @param parentKey Tên key cha (phục vụ đệ quy)
|
|
1954
|
+
* @returns Dữ liệu đã được loại bỏ các field nhạy cảm
|
|
1955
|
+
*/
|
|
1956
|
+
declare function sanitizeAuditPayload(value: unknown, parentKey?: string): SanitizedAuditValue;
|
|
1957
|
+
/**
|
|
1958
|
+
* Tạo dữ liệu phục vụ ghi nhận audit log giữa bản ghi mới và bản ghi cũ.
|
|
1959
|
+
* Tự động loại trừ các trường mặc định (`updatedAt`, `createdAt`, `__v`) cùng các trường chỉ định,
|
|
1960
|
+
* và khử trùng thông tin nhạy cảm khỏi log.
|
|
1961
|
+
*
|
|
1962
|
+
* @param item Bản ghi mới sau khi cập nhật
|
|
1963
|
+
* @param itemOld Bản ghi cũ trước khi cập nhật
|
|
1964
|
+
* @param ignoreFields Danh sách tên các trường cần bỏ qua không tính toán chênh lệch
|
|
1965
|
+
* @returns Đối tượng chứa:
|
|
1966
|
+
* - `difference`: Chi tiết các trường thay đổi `{ field: { old, new } }`
|
|
1967
|
+
* - `item`: Bản ghi mới đã khử trùng
|
|
1968
|
+
*
|
|
1969
|
+
* @example
|
|
1970
|
+
* ```ts
|
|
1971
|
+
* const audit = buildAuditLogData(newData, oldData, ['lastActive']);
|
|
1972
|
+
* ```
|
|
1973
|
+
*/
|
|
1974
|
+
declare function buildAuditLogData(item: AuditRecord | null | undefined, itemOld: AuditRecord | null | undefined, ignoreFields?: string[]): {
|
|
1975
|
+
difference: SanitizedAuditValue;
|
|
1976
|
+
item: SanitizedAuditValue;
|
|
750
1977
|
};
|
|
751
|
-
|
|
1978
|
+
/**
|
|
1979
|
+
* Xác định chính sách nguồn gốc CORS (origin) dựa trên danh sách origins cấu hình và môi trường chạy.
|
|
1980
|
+
*
|
|
1981
|
+
* @param corsOrigins Danh sách allowed origins (mảng hoặc chuỗi)
|
|
1982
|
+
* @param isProduction Cờ xác định môi trường production (boolean hoặc 1/0)
|
|
1983
|
+
* @returns
|
|
1984
|
+
* - Nếu có cấu hình `corsOrigins`: trả về chính `corsOrigins`
|
|
1985
|
+
* - Nếu không có: trả về `false` (chặn) trên production, hoặc `true` (cho phép tất cả) trên dev/local
|
|
1986
|
+
*/
|
|
1987
|
+
declare function getCorsOriginPolicy(corsOrigins: string | string[] | null | undefined, isProduction: boolean | number): string | boolean | string[];
|
|
752
1988
|
//#endregion
|
|
753
1989
|
//#region src/Validation.d.ts
|
|
1990
|
+
type ValidationValue = string | number | boolean | null | undefined | ValidationValue[] | {
|
|
1991
|
+
[key: string]: ValidationValue;
|
|
1992
|
+
};
|
|
1993
|
+
/** Dữ liệu cần validate; là object phẳng `field -> value`. */
|
|
1994
|
+
type ValidationItem = Record<string, ValidationValue>;
|
|
1995
|
+
type ValidationRules = Record<string, string>;
|
|
1996
|
+
interface ValidationEntry {
|
|
1997
|
+
field: string;
|
|
1998
|
+
value: ValidationValue;
|
|
1999
|
+
rules: string;
|
|
2000
|
+
fieldName: string;
|
|
2001
|
+
/**
|
|
2002
|
+
* Danh sách args dự phòng cho rule không có tham số.
|
|
2003
|
+
* Giữ theo cấu trúc cũ (`filed`) nên thường là `undefined` khi tạo từ `runValidate()`.
|
|
2004
|
+
*/
|
|
2005
|
+
filed?: string[];
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Bộ validator dùng rule dạng chuỗi, ví dụ `required|minLen(6)`.
|
|
2009
|
+
*
|
|
2010
|
+
* Quy ước chung:
|
|
2011
|
+
* - `runValidate()` chuẩn hóa `null`/`undefined` của field thành chuỗi rỗng.
|
|
2012
|
+
* - Rule có tham số dùng `tenRule(doiSo1,doiSo2)`; các đối số luôn là chuỗi.
|
|
2013
|
+
* - Rule chạy theo thứ tự trong từng chuỗi; các field được validate tuần tự.
|
|
2014
|
+
* `skip=true` dừng ngay tại lỗi đầu và trả `false`.
|
|
2015
|
+
* - Nên kết hợp `required` hoặc `requiredId` với các rule regex/định dạng vì
|
|
2016
|
+
* nhiều rule định dạng coi giá trị rỗng là hợp lệ.
|
|
2017
|
+
*
|
|
2018
|
+
* Validator hoàn toàn thuần: không truy vấn database và không phụ thuộc model.
|
|
2019
|
+
* Tên hiển thị trong thông báo lỗi chính là tên field (`validate`).
|
|
2020
|
+
* Việc kiểm tra trùng lặp phải tự thực hiện bằng query ở tầng nghiệp vụ.
|
|
2021
|
+
*
|
|
2022
|
+
* Lưu ý: phần mô tả ngay trên từng rule mô tả chính xác điều kiện đang được
|
|
2023
|
+
* kiểm tra bởi implementation hiện tại, kể cả các giới hạn của biểu thức chính quy.
|
|
2024
|
+
*/
|
|
754
2025
|
declare class Validation {
|
|
755
|
-
|
|
756
|
-
constructor(md?: IBaseModel<any>);
|
|
2026
|
+
/** Danh sách thông báo lỗi của lần validate hiện tại. */
|
|
757
2027
|
private errors;
|
|
758
|
-
runValidate(item: any, vdObject?: Record<string, any>): Promise<void>;
|
|
759
2028
|
/**
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
2029
|
+
* Validate toàn bộ field theo cấu hình rule.
|
|
2030
|
+
*
|
|
2031
|
+
* Điều kiện xử lý:
|
|
2032
|
+
* - Mỗi lần gọi đều xoá lỗi của lần trước, nên một instance có thể tái
|
|
2033
|
+
* sử dụng cho nhiều request mà không bị dính lỗi cũ.
|
|
2034
|
+
* - `validate` không có entry: dừng ngay và không ghi lỗi.
|
|
2035
|
+
* - Chỉ các own enumerable field của `validate` được validate.
|
|
2036
|
+
* - `item` không tồn tại vẫn được xử lý; mỗi field nullish được đổi thành `''`.
|
|
2037
|
+
* - Tên hiển thị lấy trực tiếp từ tên field trong `validate`; validator không
|
|
2038
|
+
* gọi tới model nên không có tên hiển thị tùy chỉnh.
|
|
2039
|
+
* - Sau khi chạy mọi rule, nếu danh sách lỗi không rỗng thì ném một `Error` chứa
|
|
2040
|
+
* toàn bộ thông báo nối bằng `, `.
|
|
2041
|
+
* - Thông báo lỗi được tra theo `lang` trong từ điển i18n (khóa `validate_<rule>`).
|
|
2042
|
+
* Ngôn ngữ chưa có bản dịch sẽ dùng thông báo chung `validate_default`.
|
|
2043
|
+
* - Tên rule phải nằm trong allowlist `RULES`; tên từ `Object.prototype`
|
|
2044
|
+
* (`toString`, `valueOf`, `constructor`...) bị từ chối.
|
|
2045
|
+
*
|
|
2046
|
+
* @param item Dữ liệu cần validate
|
|
2047
|
+
* @param validate Map `field -> rule1|rule2`; rule phải là chuỗi
|
|
2048
|
+
* @param lang Mã ngôn ngữ cho thông báo lỗi (mặc định `'vi'`)
|
|
2049
|
+
* @returns `Promise<void>`
|
|
2050
|
+
* @throws {Error} Khi có ít nhất một rule fail hoặc rule không tồn tại
|
|
2051
|
+
*
|
|
2052
|
+
* @example
|
|
2053
|
+
* ```ts
|
|
2054
|
+
* const rules = { email: 'required|email', age: 'required|rangeNum(18,60)' };
|
|
2055
|
+
*
|
|
2056
|
+
* await validator.runValidate({ email: 'sai', age: 10 }, rules, 'vi');
|
|
2057
|
+
* // throw: "Email không đúng định dạng, age phải nằm trong khoản từ 18 đến 60"
|
|
2058
|
+
*
|
|
2059
|
+
* await validator.runValidate({ email: 'sai', age: 10 }, rules, 'en');
|
|
2060
|
+
* // throw: "Email is not in the correct format, age must be between 18 and 60"
|
|
2061
|
+
* ```
|
|
2062
|
+
*/
|
|
2063
|
+
runValidate(item: ValidationItem | null | undefined, validate?: ValidationRules, lang?: string): Promise<void>;
|
|
2064
|
+
/**
|
|
2065
|
+
* Chạy danh sách rule đã chuẩn hóa và ghi lỗi khi rule trả false.
|
|
2066
|
+
*
|
|
2067
|
+
* Điều kiện xử lý:
|
|
2068
|
+
* - `validation` được duyệt tuần tự; mỗi entry cần `rules`, `value` và
|
|
2069
|
+
* `fieldName` (`filed` được đọc để giữ tương thích nhưng thường là undefined).
|
|
2070
|
+
* - `rules` phải là chuỗi được phân tách bằng `|`; chỉ phần có tham số được
|
|
2071
|
+
* trim trong `getRuleArgs()`. Tên rule không có tham số không được trim trước khi tra cứu.
|
|
2072
|
+
* - Với rule có tham số, `args` là mảng chuỗi từ `getRuleArgs()`; với rule không
|
|
2073
|
+
* có tham số, `args` lấy từ `dtVal.filed` (thường `undefined` trong dữ liệu
|
|
2074
|
+
* do `runValidate()` tạo và được `addError()` xử lý như danh sách rỗng).
|
|
2075
|
+
* - `skip=true` dừng ngay tại rule fail đầu tiên; rule/field sau đó không chạy.
|
|
2076
|
+
* - Mọi rule đều thuần và đồng bộ; không có rule nào truy vấn database.
|
|
2077
|
+
* - Rule không tồn tại hoặc tên rule rỗng sẽ ném lỗi từ `this[method]()`.
|
|
2078
|
+
* - `passed` được trả về nhưng caller `runValidate()` kiểm tra `getErrors()` thay vì
|
|
2079
|
+
* dùng giá trị này.
|
|
2080
|
+
*
|
|
2081
|
+
* @param validation Danh sách field/value/rules đã chuẩn hóa
|
|
2082
|
+
* @param skip Dừng ngay sau rule fail đầu tiên
|
|
2083
|
+
* @param lang Mã ngôn ngữ cho thông báo lỗi (mặc định `'vi'`)
|
|
2084
|
+
* @returns `false` nếu có ít nhất một rule fail; ngược lại `true`
|
|
2085
|
+
* @throws {Error} Khi không gọi được rule hoặc rule async phát sinh lỗi
|
|
765
2086
|
*/
|
|
766
|
-
validate(validation:
|
|
2087
|
+
validate(validation: ValidationEntry[], skip?: boolean, lang?: string): Promise<boolean>;
|
|
767
2088
|
/**
|
|
768
2089
|
* Determine if a given rule has arguments, Ex: max(4)
|
|
769
2090
|
*
|
|
@@ -772,27 +2093,44 @@ declare class Validation {
|
|
|
772
2093
|
*/
|
|
773
2094
|
private isruleHasArgs;
|
|
774
2095
|
/**
|
|
775
|
-
*
|
|
2096
|
+
* Tách tên rule khỏi phần tham số trong ngoặc.
|
|
776
2097
|
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
2098
|
+
* `minLen(6)` trả `minLen`; phần trước dấu `(` đầu tiên được giữ nguyên.
|
|
2099
|
+
*
|
|
2100
|
+
* @param rule Chuỗi rule
|
|
2101
|
+
* @returns Tên rule
|
|
779
2102
|
*/
|
|
780
2103
|
private getRuleName;
|
|
781
2104
|
/**
|
|
782
|
-
*
|
|
2105
|
+
* Kiểm tra `hasRuleArgs()` rồi lấy đối số của rule.
|
|
783
2106
|
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
2107
|
+
* Điều kiện:
|
|
2108
|
+
* - Hàm chỉ được gọi sau `isruleHasArgs(rule) === true`.
|
|
2109
|
+
* - Lấy phần sau dấu `(` đầu tiên; nếu có `)` ở cuối thì cắt đi rồi tách theo
|
|
2110
|
+
* `,`. Không trim từng đối số, không escape và không parse số.
|
|
2111
|
+
* - `rule()` với phần giữa rỗng trả `['']`; nếu gọi trực tiếp với rule không
|
|
2112
|
+
* có `(` thì `args` là `undefined` và có thể ném lỗi tại `endsWith`.
|
|
2113
|
+
*
|
|
2114
|
+
* @param rule Chuỗi rule như `minLen(6)` hoặc `rangeNum(1,10)`
|
|
2115
|
+
* @returns Mảng đối số nguyên bản
|
|
786
2116
|
*/
|
|
787
2117
|
private getRuleArgs;
|
|
788
2118
|
/**
|
|
789
|
-
*
|
|
2119
|
+
* Thêm thông báo cho rule fail.
|
|
790
2120
|
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
2121
|
+
* Điều kiện:
|
|
2122
|
+
* - `rule` rỗng: không làm gì.
|
|
2123
|
+
* - Có message mặc định: thay `{placeholder}`, `{value}` và `{0}`, `{1}`...
|
|
2124
|
+
* bằng dữ liệu thực; value không phải string bị thay bằng chuỗi rỗng.
|
|
2125
|
+
* Giá trị `args` mặc định là `[]`; nếu truyền `null` thì có thể lỗi khi gọi `forEach`.
|
|
2126
|
+
* - Message mặc định là chuỗi rỗng: không ghi lỗi ở đây (rule phải tự xử lý).
|
|
2127
|
+
* - Không có message mặc định: ghi thông báo tổng quát bằng tên field.
|
|
795
2128
|
*
|
|
2129
|
+
* @param rule Tên rule không có tham số
|
|
2130
|
+
* @param placeholder Tên field hiển thị
|
|
2131
|
+
* @param value Giá trị đã validate
|
|
2132
|
+
* @param args Tham số rule để điền vào message
|
|
2133
|
+
* @param lang Mã ngôn ngữ dùng để tra thông báo trong từ điển
|
|
796
2134
|
*/
|
|
797
2135
|
private addError;
|
|
798
2136
|
/**
|
|
@@ -815,144 +2153,370 @@ declare class Validation {
|
|
|
815
2153
|
/** ************** Validations ************** **/
|
|
816
2154
|
/** *********************************************** **/
|
|
817
2155
|
/**
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
2156
|
+
* Rule `required`: yêu cầu value khác nullish và chuỗi không rỗng sau trim.
|
|
2157
|
+
*
|
|
2158
|
+
* Mọi giá trị khác nullish đều hợp lệ, kể cả `0`, `false`, `NaN`, mảng/object rỗng;
|
|
2159
|
+
* chỉ chuỗi có `trim() === ''` bị từ chối.
|
|
2160
|
+
*
|
|
2161
|
+
* @param value Giá trị field
|
|
2162
|
+
* @returns `true` khi có giá trị theo điều kiện trên
|
|
821
2163
|
*/
|
|
822
2164
|
private required;
|
|
823
2165
|
/**
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
2166
|
+
* Rule `requiredId`: yêu cầu ID khác nullish, chuỗi không rỗng sau trim và
|
|
2167
|
+
* không bằng số `0`.
|
|
2168
|
+
*
|
|
2169
|
+
* `false`, object, mảng và các số khác 0 vẫn hợp lệ; chuỗi `'0'` cũng hợp lệ
|
|
2170
|
+
* vì điều kiện chỉ chặn number `0`.
|
|
2171
|
+
*
|
|
2172
|
+
* @param value Giá trị ID
|
|
2173
|
+
* @returns `true` khi ID đạt điều kiện
|
|
827
2174
|
*/
|
|
828
2175
|
private requiredId;
|
|
829
2176
|
/**
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
2177
|
+
* Rule `minLen`: độ dài chuỗi phải lớn hơn hoặc bằng `parseInt(args[0])`.
|
|
2178
|
+
*
|
|
2179
|
+
* Giá trị được kỳ vọng là string; rule không tự kiểm tra `typeof` và không trim.
|
|
2180
|
+
* `parseInt` không có radix nên có thể đọc tiền tố của chuỗi tham số.
|
|
2181
|
+
*
|
|
2182
|
+
* @param str Chuỗi cần đo
|
|
2183
|
+
* @param args Mảng tham số, `args[0]` là độ dài tối thiểu
|
|
2184
|
+
* @returns `true` khi độ dài đạt yêu cầu
|
|
834
2185
|
*/
|
|
835
2186
|
private minLen;
|
|
2187
|
+
/**
|
|
2188
|
+
* Rule `equalLen`: độ dài chuỗi phải bằng `parseInt(args[0])` chính xác.
|
|
2189
|
+
*
|
|
2190
|
+
* Không trim và không kiểm tra kiểu đầu vào; giá trị được kỳ vọng là string.
|
|
2191
|
+
*
|
|
2192
|
+
* @param str Chuỗi cần đo
|
|
2193
|
+
* @param args Mảng tham số, `args[0]` là độ dài yêu cầu
|
|
2194
|
+
* @returns `true` khi độ dài bằng yêu cầu
|
|
2195
|
+
*/
|
|
836
2196
|
private equalLen;
|
|
837
2197
|
/**
|
|
838
|
-
*
|
|
2198
|
+
* Rule `maxLen`: độ dài chuỗi phải nhỏ hơn hoặc bằng `parseInt(args[0])`.
|
|
839
2199
|
*
|
|
840
|
-
*
|
|
841
|
-
* @param array args(max)
|
|
2200
|
+
* Không trim và không kiểm tra `typeof`; giá trị được kỳ vọng là string.
|
|
842
2201
|
*
|
|
843
|
-
* @
|
|
2202
|
+
* @param str Chuỗi cần đo
|
|
2203
|
+
* @param args Mảng tham số, `args[0]` là độ dài tối đa
|
|
2204
|
+
* @returns `true` khi độ dài không vượt quá
|
|
844
2205
|
*/
|
|
845
2206
|
private maxLen;
|
|
846
2207
|
/**
|
|
847
|
-
*
|
|
2208
|
+
* Rule `rangeNum`: value phải nằm trong khoảng đóng từ `args[0]` đến `args[1]`.
|
|
848
2209
|
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
2210
|
+
* Hai bound được đọc bằng `parseInt`; phép so sánh JavaScript có thể coerce
|
|
2211
|
+
* value. Không tự kiểm tra `typeof`, không kiểm tra số nguyên và không trim.
|
|
2212
|
+
*
|
|
2213
|
+
* @param num Giá trị cần kiểm tra
|
|
2214
|
+
* @param args Mảng tham số `[min, max]`
|
|
2215
|
+
* @returns `true` khi `min <= num <= max`
|
|
852
2216
|
*/
|
|
853
2217
|
private rangeNum;
|
|
854
2218
|
/**
|
|
855
|
-
*
|
|
2219
|
+
* Rule `max`: value phải nhỏ hơn hoặc bằng `Number(args[0])`.
|
|
856
2220
|
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
2221
|
+
* Bound được parse bằng `Number`, không phải `parseInt`; không tự kiểm tra kiểu
|
|
2222
|
+
* value. `Number('abc')` thành `NaN` nên phép so sánh thường fail.
|
|
2223
|
+
*
|
|
2224
|
+
* @param num Giá trị cần kiểm tra
|
|
2225
|
+
* @param args Mảng tham số, `args[0]` là ngưỡng trên
|
|
2226
|
+
* @returns Kết quả phép so sánh `num <= Number(args[0])`
|
|
860
2227
|
*/
|
|
861
2228
|
private max;
|
|
2229
|
+
/**
|
|
2230
|
+
* Rule `min`: value phải lớn hơn hoặc bằng `Number(args[0])`.
|
|
2231
|
+
*
|
|
2232
|
+
* Bound được parse bằng `Number`, không phải `parseInt`; không tự kiểm tra kiểu
|
|
2233
|
+
* value. `Number('abc')` thành `NaN` nên phép so sánh thường fail.
|
|
2234
|
+
*
|
|
2235
|
+
* @param num Giá trị cần kiểm tra
|
|
2236
|
+
* @param args Mảng tham số, `args[0]` là ngưỡng dưới
|
|
2237
|
+
* @returns Kết quả phép so sánh `num >= Number(args[0])`
|
|
2238
|
+
*/
|
|
862
2239
|
private min;
|
|
863
2240
|
/**
|
|
864
|
-
*
|
|
2241
|
+
* Rule `integer`: `parseInt(value)` không phải `NaN`.
|
|
865
2242
|
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
2243
|
+
* Đây không phải kiểm tra số nguyên nghiêm ngặt: `parseInt('12abc')` là 12,
|
|
2244
|
+
* `parseInt('1.9')` là 1, và chuỗi rỗng thường fail. Giá trị falsy không được
|
|
2245
|
+
* guard riêng trong method này.
|
|
2246
|
+
*
|
|
2247
|
+
* @param value Giá trị cần parse
|
|
2248
|
+
* @returns `true` nếu `parseInt(value)` không phải `NaN`
|
|
2249
|
+
*/
|
|
2250
|
+
/**
|
|
2251
|
+
* Rule `integer`: giá trị phải là một số nguyên hợp lệ.
|
|
2252
|
+
*
|
|
2253
|
+
* Dùng `Number()` + `Number.isInteger()` thay vì `parseInt()` vì `parseInt`
|
|
2254
|
+
* chỉ đọc **tiền tố** nên `'12abc'` và `'12.5'` đều bị coi là hợp lệ.
|
|
2255
|
+
* Chuỗi rỗng / toàn khoảng trắng bị từ chối vì `Number('')` là `0`.
|
|
2256
|
+
*
|
|
2257
|
+
* @param value Giá trị cần kiểm tra
|
|
2258
|
+
* @returns `true` khi value là số nguyên
|
|
868
2259
|
*/
|
|
869
2260
|
private integer;
|
|
870
2261
|
/**
|
|
871
|
-
*
|
|
2262
|
+
* Rule `inArray`: giá trị phải thuộc danh sách tham số của rule.
|
|
872
2263
|
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
2264
|
+
* - Primitive (chuỗi/số): so sánh membership bằng `includes` trên `String(value)`.
|
|
2265
|
+
* - Object/array: **mọi value bên trong** phải thuộc danh sách; object rỗng pass.
|
|
2266
|
+
* - So sánh luôn là so sánh chuỗi nên `inArray(1,2)` nhận cả `1` và `'1'`.
|
|
2267
|
+
*
|
|
2268
|
+
* Trước đây rule dùng toán tử `in`, mà `in` trên mảng chỉ kiểm tra **index**
|
|
2269
|
+
* (`'a' in ['a']` là `false`) chứ không kiểm tra membership — khiến
|
|
2270
|
+
* `inArray(a)` với giá trị `'a'` luôn thất bại.
|
|
2271
|
+
*
|
|
2272
|
+
* @param value Giá trị hoặc object/array cần kiểm tra
|
|
2273
|
+
* @param arr Danh sách giá trị hợp lệ, lấy từ tham số rule `inArray(a,b,c)`
|
|
2274
|
+
* @returns `true` khi mọi giá trị được kiểm tra đều thuộc `arr`
|
|
876
2275
|
*/
|
|
877
2276
|
private inArray;
|
|
2277
|
+
/**
|
|
2278
|
+
* Rule `inArrayNumber`: kiểm tra value hoặc từng phần tử object/array nằm trong
|
|
2279
|
+
* danh sách số.
|
|
2280
|
+
*
|
|
2281
|
+
* - `arr` được chuyển từng phần tử sang `Number` trước khi so sánh.
|
|
2282
|
+
* - Mảng: kiểm tra từng phần tử. Object thường: kiểm tra từng **value**.
|
|
2283
|
+
* - `null`/`undefined` trả `false` thay vì ném `TypeError` — trước đây
|
|
2284
|
+
* `typeof null === 'object'` khiến nhánh object được gọi `.every` trên `null`.
|
|
2285
|
+
* - Với primitive: so sánh nghiêm ngặt, nên chuỗi `'1'` không khớp số `1`.
|
|
2286
|
+
*
|
|
2287
|
+
* @param value Giá trị hoặc danh sách cần kiểm tra
|
|
2288
|
+
* @param arr Danh sách giá trị được phép
|
|
2289
|
+
* @returns `true` khi tất cả giá trị được chuyển số nằm trong danh sách
|
|
2290
|
+
*/
|
|
878
2291
|
private inArrayNumber;
|
|
879
2292
|
/**
|
|
880
|
-
*
|
|
2293
|
+
* Rule `alphaNum`: chỉ cho phép chữ ASCII `[a-zA-Z0-9]`, ít nhất một ký tự.
|
|
881
2294
|
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
2295
|
+
* Giá trị truthy được `RegExp.test()` ép sang chuỗi theo JavaScript; không có
|
|
2296
|
+
* guard `typeof` riêng. Chuỗi có khoảng trắng, dấu hoặc ký tự Unicode không hợp lệ.
|
|
2297
|
+
*
|
|
2298
|
+
* @param value Giá trị cần kiểm tra
|
|
2299
|
+
* @returns `true` khi value falsy hoặc chỉ gồm chữ/số ASCII
|
|
884
2300
|
*/
|
|
885
2301
|
private alphaNum;
|
|
886
2302
|
/**
|
|
887
|
-
*
|
|
2303
|
+
* Rule `number`: `true` khi value là số nằm trong khoảng an toàn.
|
|
888
2304
|
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
2305
|
+
* Trước đây rule này **ném** `INVALID_SAFE_NUMBER` khi số vượt khoảng trong
|
|
2306
|
+
* khi 18 rule còn lại đều trả `false`. Hệ quả là `runValidate()` dừng giữa
|
|
2307
|
+
* chừng, các field phía sau không được kiểm tra, và lỗi trả về có dạng khác
|
|
2308
|
+
* hẳn so với lỗi validate thông thường. Nay mọi rule đều tuân theo cùng một
|
|
2309
|
+
* hợp đồng: trả boolean.
|
|
2310
|
+
*
|
|
2311
|
+
* `NaN` giờ bị từ chối (trước đây lọt qua vì mọi phép so sánh với `NaN` đều
|
|
2312
|
+
* cho `false`).
|
|
2313
|
+
*
|
|
2314
|
+
* @param value Giá trị cần kiểm tra
|
|
2315
|
+
* @returns `true` khi value là số hữu hạn trong khoảng `MIN/MAX_SAFE_INTEGER`
|
|
891
2316
|
*/
|
|
892
2317
|
private number;
|
|
893
2318
|
/**
|
|
894
|
-
*
|
|
2319
|
+
* Rule `alphaNumWithSpaces`: chỉ cho phép chữ/số ASCII và khoảng trắng ASCII.
|
|
895
2320
|
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
2321
|
+
* Regex là `/^[a-z0-9 ]+$/i`, nên phải có ít nhất một ký tự và chỉ cho phép
|
|
2322
|
+
* khoảng trắng ASCII; space ở đầu/cuối vẫn được phép, còn tab, dấu và ký tự
|
|
2323
|
+
* Unicode không hợp lệ. Value falsy được coi là hợp lệ và trả `true`.
|
|
2324
|
+
*
|
|
2325
|
+
* @param value Giá trị cần kiểm tra
|
|
2326
|
+
* @returns `true` khi value rỗng/falsy hoặc chỉ gồm chữ/số/space ASCII
|
|
898
2327
|
*/
|
|
899
2328
|
private alphaNumWithSpaces;
|
|
900
2329
|
/**
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
*
|
|
2330
|
+
* Rule `password`: yêu cầu mật khẩu 8-32 ký tự trong regex hiện tại.
|
|
2331
|
+
*
|
|
2332
|
+
* Regex bắt buộc có ít nhất một chữ số, chữ thường, chữ hoa và một ký tự đặc
|
|
2333
|
+
* biệt trong tập `@#!%^&*`; phần ký tự được phép là chữ/số và các ký tự này.
|
|
2334
|
+
* Có `$` ở cuối nên chuỗi dài hơn 32 ký tự hoặc chứa ký tự lạ đều bị từ chối;
|
|
2335
|
+
* value falsy được coi là hợp lệ.
|
|
2336
|
+
*
|
|
2337
|
+
* @param value Mật khẩu cần kiểm tra
|
|
2338
|
+
* @returns `true` khi value falsy hoặc match regex
|
|
906
2339
|
*/
|
|
907
2340
|
private password;
|
|
908
2341
|
/**
|
|
909
|
-
|
|
910
|
-
|
|
2342
|
+
* Rule `phoneVn`: regex số điện thoại Việt Nam hiện tại.
|
|
2343
|
+
*
|
|
2344
|
+
* Pattern là `/^[03|05|07|08|09]{2}[0-9]{8}/`: hai ký tự đầu phải thuộc tập ký
|
|
2345
|
+
* tự `0,3,|,5,7,8,9`, sau đó tối thiểu tám chữ số. Pattern không có `$`, nên
|
|
2346
|
+
* chuỗi dài hơn vẫn có thể match; falsy value được trả `true`.
|
|
2347
|
+
*
|
|
2348
|
+
/**
|
|
2349
|
+
* Rule `phoneVn`: số điện thoại di động Việt Nam gồm 10 chữ số.
|
|
2350
|
+
*
|
|
2351
|
+
* Theo quy hoạch số thuê bao của Bộ TT&TT, định dạng số di động là
|
|
2352
|
+
* `03xx xxx xxx`, `05xx xxx xxx`, `07xx xxx xxx`, `08xx xxx xxx`,
|
|
2353
|
+
* `09xx xxx xxx` — tức **chữ số thứ hai** phải thuộc `{3, 5, 7, 8, 9}`
|
|
2354
|
+
* (không có khối `01`, `02`, `04`, `06` cho di động).
|
|
2355
|
+
*
|
|
2356
|
+
* Cố ý **không** giới hạn chữ số thứ ba: quy hoạch không ràng buộc vị trí này
|
|
2357
|
+
* và các đầu số `090` (MobiFone), `091` (VinaPhone) đều đang được sử dụng.
|
|
2358
|
+
* Ép về `[2-9]` sẽ chặn nhầm khách hàng thật.
|
|
2359
|
+
*
|
|
2360
|
+
* Đầu vào được chuẩn hoá trước khi kiểm tra:
|
|
2361
|
+
* - Bỏ khoảng trắng và các dấu phân cách `.`, `-`, `(`, `)`.
|
|
2362
|
+
* - Đưa mã quốc gia `+84` / `84` về dạng nội địa có số `0` ở đầu.
|
|
2363
|
+
* - Bỏ tiền tố quốc tế `00`.
|
|
2364
|
+
* Nhờ vậy `0900 000 000`, `0900-000-000` và `+84 900 000 000` đều hợp lệ.
|
|
2365
|
+
*
|
|
2366
|
+
* Giá trị falsy (`''`, `null`, `undefined`) được coi là hợp lệ — nhất quán với
|
|
2367
|
+
* `email`, `alphaNum`, `password`; nên kết hợp với `required` khi bắt buộc.
|
|
2368
|
+
*
|
|
2369
|
+
* @param value Số điện thoại cần kiểm tra
|
|
2370
|
+
* @returns `true` khi value falsy hoặc là số di động VN hợp lệ
|
|
2371
|
+
*/
|
|
911
2372
|
private phoneVn;
|
|
912
2373
|
/**
|
|
913
|
-
*
|
|
2374
|
+
* Rule `equals`: so sánh `String(value)` với `args[0]`.
|
|
914
2375
|
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
2376
|
+
* Tham số rule luôn là chuỗi nên phải chuẩn hoá về chuỗi để so sánh — trước đây
|
|
2377
|
+
* so sánh nghiêm ngặt khiến số `1` không bằng chuỗi `'1'`, dù `equals(1)` rõ
|
|
2378
|
+
* ràng là muốn so với số `1`.
|
|
2379
|
+
*
|
|
2380
|
+
* @param value Giá trị cần so sánh
|
|
2381
|
+
* @param args Mảng tham số, giá trị đích ở `args[0]`
|
|
2382
|
+
* @returns `true` khi hai bên bằng nhau sau khi chuẩn hoá chuỗi
|
|
918
2383
|
*/
|
|
919
2384
|
private equals;
|
|
920
2385
|
/**
|
|
921
|
-
*
|
|
2386
|
+
* Rule `notEqual`: phủ định của {@link Validation.equals}.
|
|
922
2387
|
*
|
|
923
|
-
* @param
|
|
924
|
-
* @param
|
|
925
|
-
* @
|
|
2388
|
+
* @param value Giá trị cần so sánh
|
|
2389
|
+
* @param args Mảng tham số, giá trị bị cấm ở `args[0]`
|
|
2390
|
+
* @returns `true` khi hai bên khác nhau sau khi chuẩn hoá chuỗi
|
|
926
2391
|
*/
|
|
927
2392
|
private notEqual;
|
|
928
2393
|
/**
|
|
929
|
-
*
|
|
2394
|
+
* Tách chuỗi rule theo dấu `|`, **tôn trọng ký tự escape `\|`**.
|
|
930
2395
|
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
2396
|
+
* `|` vừa là dấu phân cách rule vừa là toán tử alternation `(a|b)` trong
|
|
2397
|
+
* regex. Nếu tách thẳng thì `regex(/^(?:x|y)=\d$/)` bị cắt thành hai rule
|
|
2398
|
+
* và hỏng. Vì vậy `\|` được hiểu là dấu `|` literal và dấu `\` bị bỏ đi.
|
|
2399
|
+
*
|
|
2400
|
+
* @param rules Chuỗi rule, ví dụ `required|regex(/^a\|b$/)`
|
|
2401
|
+
* @returns Mỗi phần tử là một tên rule
|
|
933
2402
|
*/
|
|
934
|
-
private
|
|
935
|
-
/** *********************************************** **/
|
|
936
|
-
/** ************ Database Validations *********** **/
|
|
937
|
-
/** *********************************************** **/
|
|
2403
|
+
private static splitRules;
|
|
938
2404
|
/**
|
|
939
|
-
*
|
|
2405
|
+
* Lấy nguyên văn pattern của rule `regex(...)`.
|
|
940
2406
|
*
|
|
941
|
-
* @
|
|
942
|
-
*
|
|
943
|
-
*
|
|
2407
|
+
* Cố ý KHÔNG dùng {@link Validation.getRuleArgs} vì hàm đó tách tham số theo
|
|
2408
|
+
* dấu phẩy, trong khi pattern regex rất hay chứa dấu phẩy:
|
|
2409
|
+
* `/a{2,3}/`, `/[a,b]/`, `/^(x,y)$/` — những pattern đó sẽ bị cắt đôi.
|
|
2410
|
+
*
|
|
2411
|
+
* @param rule Chuỗi rule dạng `regex(/pattern/flags)`
|
|
2412
|
+
* @returns Chuỗi pattern, hoặc `''` nếu không trích được
|
|
2413
|
+
*/
|
|
2414
|
+
private getRegexPattern;
|
|
2415
|
+
/**
|
|
2416
|
+
* Rule `regex`: kiểm tra giá trị có khớp biểu thức chính quy tuỳ chỉnh hay không.
|
|
2417
|
+
*
|
|
2418
|
+
* - Nhận cả hai dạng có và không có dấu nháy bao:
|
|
2419
|
+
* `regex(/^[A-Z]{2}\d{6}$/)`, `regex(/^ab+c$/i)`, `regex(^[A-Z]+$)`.
|
|
2420
|
+
* - Dấu `|` bên trong pattern phải viết thành `\|`, vì `|` là dấu phân cách
|
|
2421
|
+
* rule: `regex(/^(?:x\|y)=1$/)`. Có thể tránh bằng lớp ký tự `[xy]`.
|
|
2422
|
+
* - Cờ `g` và `y` bị bỏ qua: `RegExp.test()` với cờ global có trạng thái
|
|
2423
|
+
* `lastIndex` nên kết quả sẽ phụ thuộc thứ tự gọi.
|
|
2424
|
+
* - **Pattern không được neo tự động**: `regex(/abc/)` cũng khớp `xxabcxx`.
|
|
2425
|
+
* Hãy tự viết `^...$` nếu cần khớp trọn vẹn.
|
|
2426
|
+
* - Giá trị falsy trả `true`, đúng như các rule khác — hãy kết hợp `required`.
|
|
2427
|
+
*
|
|
2428
|
+
* ⚠️ Pattern do ứng dụng tự viết trong chuỗi rule, không lấy từ dữ liệu người
|
|
2429
|
+
* dùng. Tuy vậy nên tránh biểu thức có khả năng **catastrophic backtracking**
|
|
2430
|
+
* (ReDoS) như `/(a+)+$/`.
|
|
2431
|
+
*
|
|
2432
|
+
* @param value Giá trị cần kiểm tra
|
|
2433
|
+
* @param args `args[0]` là pattern
|
|
2434
|
+
* @returns `true` khi value falsy hoặc khớp pattern
|
|
2435
|
+
*
|
|
2436
|
+
* @example
|
|
2437
|
+
* ```ts
|
|
2438
|
+
* const rules = { so: 'required|regex(/^0\d{9}$/)' };
|
|
2439
|
+
* await validator.runValidate({ so: '0900000000' }, rules); // ok
|
|
2440
|
+
* await validator.runValidate({ so: 'sai' }, rules); // throw lỗi
|
|
2441
|
+
* ```
|
|
2442
|
+
*/
|
|
2443
|
+
private regex;
|
|
2444
|
+
/**
|
|
2445
|
+
* Rule `email`: kiểm tra định dạng email ASCII.
|
|
2446
|
+
*
|
|
2447
|
+
* - Local part cho phép ký tự đặc biệt hợp lệ của RFC 5322 kể cả `+`
|
|
2448
|
+
* (ví dụ `ten+the@gmail.com`) và chấp nhận subdomain lồng nhau.
|
|
2449
|
+
* - TLD là **2 chữ cái ASCII trở lên**, không giới hạn 4 ký tự — trước đây
|
|
2450
|
+
* giới hạn `{2,4}` nên từ chối các TLD dài hợp lệ như `.technology`.
|
|
2451
|
+
* - Không cho phép dấu chấm liên tiếp và không cho phép dấu chấm ở đầu/cuối
|
|
2452
|
+
* local part.
|
|
2453
|
+
* - Không hỗ trợ email quốc tế viết bằng ký tự Unicode (IDN) và không trim.
|
|
2454
|
+
* `FastRequest.filterSafeData(..., 'stripTags')` đã trim sẵn theo mặc
|
|
2455
|
+
* định nên đường thông thường không bị vấn đề khoảng trắng.
|
|
2456
|
+
*
|
|
2457
|
+
* @param email Giá trị cần kiểm tra
|
|
2458
|
+
* @returns `true` khi email falsy hoặc khớp định dạng
|
|
944
2459
|
*/
|
|
945
|
-
private
|
|
2460
|
+
private email;
|
|
946
2461
|
/** *********************************************** **/
|
|
947
2462
|
/** ************ Default Messages *********** **/
|
|
948
2463
|
/** *********************************************** **/
|
|
2464
|
+
/**
|
|
2465
|
+
* Tra thông báo mặc định của một rule theo ngôn ngữ.
|
|
2466
|
+
*
|
|
2467
|
+
* Khóa trong từ điển có dạng `validate_<tênRule>` (xem `src/lang/vi.ts`, `src/lang/en.ts`).
|
|
2468
|
+
* Ứng dụng có thể ghi đè hoặc bổ sung ngôn ngữ mới bằng `initI18n()`.
|
|
2469
|
+
*
|
|
2470
|
+
* @param rule Tên rule, ví dụ `minLen`
|
|
2471
|
+
* @param lang Mã ngôn ngữ (mặc định `'vi'`)
|
|
2472
|
+
* @returns Chuỗi thông báo, hoặc `null` nếu rule chưa có bản dịch
|
|
2473
|
+
*/
|
|
949
2474
|
private static defaultMessages;
|
|
2475
|
+
/**
|
|
2476
|
+
* Tra bản dịch cho `key` theo ngôn ngữ, có lùi về tiếng Việt.
|
|
2477
|
+
*
|
|
2478
|
+
* `t()` trả về chính `key` khi không tìm thấy, nên hàm này dùng giá trị đó
|
|
2479
|
+
* làm tín hiệu "thiếu bản dịch" và thử lại với `'vi'` trước khi kết luận.
|
|
2480
|
+
*
|
|
2481
|
+
* @param key Khoá trong từ điển, ví dụ `validate_email`
|
|
2482
|
+
* @param lang Mã ngôn ngữ
|
|
2483
|
+
* @returns Chuỗi đã dịch, hoặc chính `key` nếu không có ở ngôn ngữ nào
|
|
2484
|
+
*/
|
|
2485
|
+
private static translate;
|
|
950
2486
|
}
|
|
951
2487
|
//#endregion
|
|
952
2488
|
//#region src/I18n.d.ts
|
|
953
2489
|
type LanguageDictionary = Record<string, string>;
|
|
954
2490
|
type I18nDictionaries = Record<string, LanguageDictionary>;
|
|
2491
|
+
/**
|
|
2492
|
+
* Khởi tạo hoặc cập nhật từ điển đa ngôn ngữ (i18n).
|
|
2493
|
+
* Hỗ trợ ghi đè các từ khóa mặc định (vi, en) hoặc bổ sung ngôn ngữ mới.
|
|
2494
|
+
*
|
|
2495
|
+
* @param data Đối tượng chứa các từ điển ngôn ngữ cần khởi tạo/bổ sung
|
|
2496
|
+
*
|
|
2497
|
+
* @example
|
|
2498
|
+
* ```ts
|
|
2499
|
+
* initI18n({
|
|
2500
|
+
* vi: { welcome: 'Xin chào' },
|
|
2501
|
+
* en: { welcome: 'Welcome' },
|
|
2502
|
+
* ja: { welcome: 'いらっしゃいませ' }
|
|
2503
|
+
* });
|
|
2504
|
+
* ```
|
|
2505
|
+
*/
|
|
955
2506
|
declare function initI18n(data: I18nDictionaries): void;
|
|
2507
|
+
/**
|
|
2508
|
+
* Dịch một khóa ngôn ngữ theo mã ngôn ngữ tương ứng.
|
|
2509
|
+
* Nếu không tìm thấy khóa trong từ điển, hàm trả về chính khóa đó.
|
|
2510
|
+
*
|
|
2511
|
+
* @param key Khóa cần dịch (vd: 'invalid_403')
|
|
2512
|
+
* @param lang Mã ngôn ngữ (mặc định: 'vi')
|
|
2513
|
+
* @returns Chuỗi bản dịch tương ứng hoặc chính key nếu chưa được định nghĩa
|
|
2514
|
+
*
|
|
2515
|
+
* @example
|
|
2516
|
+
* ```ts
|
|
2517
|
+
* const msg = t('invalid_403', 'vi'); // 'Truy cập không được phép'
|
|
2518
|
+
* ```
|
|
2519
|
+
*/
|
|
956
2520
|
declare function t(key: string, lang?: string): string;
|
|
957
2521
|
//#endregion
|
|
958
2522
|
//#region src/HMAC.d.ts
|
|
@@ -975,7 +2539,7 @@ declare function t(key: string, lang?: string): string;
|
|
|
975
2539
|
* const signature = createSignature(data);
|
|
976
2540
|
* ```
|
|
977
2541
|
*/
|
|
978
|
-
declare function createSignature(data: Record<string,
|
|
2542
|
+
declare function createSignature(data: Record<string, unknown>): string;
|
|
979
2543
|
/**
|
|
980
2544
|
* Creates an HMAC-SHA256 signature for the given data using the Web Crypto API.
|
|
981
2545
|
*
|
|
@@ -998,13 +2562,23 @@ declare function createSignature(data: Record<string, any>): string;
|
|
|
998
2562
|
* const signature = await createHmac(data, secret);
|
|
999
2563
|
* ```
|
|
1000
2564
|
*/
|
|
1001
|
-
declare function createHmac(data: Record<string,
|
|
2565
|
+
declare function createHmac(data: Record<string, unknown>, secret: string): Promise<string>;
|
|
1002
2566
|
//#endregion
|
|
1003
2567
|
//#region src/Error.d.ts
|
|
2568
|
+
/**
|
|
2569
|
+
* Lỗi phía client (Bad Request hoặc lỗi tương tự)
|
|
2570
|
+
* Dùng để phân biệt lỗi do người dùng gửi lên với lỗi hệ thống 500
|
|
2571
|
+
*/
|
|
1004
2572
|
declare class ClientError extends Error {
|
|
1005
2573
|
readonly statusCode: number;
|
|
1006
2574
|
readonly code?: string | undefined;
|
|
2575
|
+
/**
|
|
2576
|
+
* Khởi tạo lỗi ClientError
|
|
2577
|
+
* @param message Thông điệp lỗi
|
|
2578
|
+
* @param statusCode Mã trạng thái HTTP (mặc định: 400)
|
|
2579
|
+
* @param code Mã lỗi tùy chọn phục vụ định danh lỗi (vd: invalid_user)
|
|
2580
|
+
*/
|
|
1007
2581
|
constructor(message: string, statusCode?: number, code?: string | undefined);
|
|
1008
2582
|
}
|
|
1009
2583
|
//#endregion
|
|
1010
|
-
export { BaseModel, ClientError, FastRequest, FileUpload, IBaseModel, JWTApp, JoinType, ModelConstructor, Models, ModifiedType, MySQLSessionStore, PaginationResult, Route, RouterType, Validation, buildAuditLogData, callFetchApi, checkPassword, createHmac, createModels, createSignature, decimalNumber, ensureCsrfProtection, formatDate, generateKeywords, getActiveStatus, getArrColumn, getArrOnlyInFirst, getCorsOriginPolicy, getCsrfTokenFromRequest, getDaysInMonth, getDiffArr, getLangs, getModels, getMonthRange, getPublishStatus, getTrueFlaseStatus, getValue, initI18n, initModels, isStateChangingMethod, isSubArr, makePassword, parseDate, randomText, removeVietnameseTones, sanitizeAuditPayload, slugify, stringToJson, sumArrCol, sumArrColumn, t, toMySQLDateNowVN, toRoman };
|
|
2584
|
+
export { AddGsOptions, AnyBaseModel, AnyIBaseModel, AuditRecord, AuditValue, BaseModel, ClientError, CsrfProtectionOptions, CsrfProtectionResult, CsrfRequest, CsrfSession, FastRequest, FileUpload, Filter, FilterType, FilterValue, IBaseModel, JWTApp, JoinType, JwtPayload, JwtRequest, JwtUser, LooseRecord, LooseValue, ModelConstructor, ModelEntity, ModelRow, Models, ModifiedType, MySQLSessionStore, PaginationResult, QueryResult, QueryRows, RawQueryResult, RequestBody, RequestValue, Route, RouteController, RouterType, SanitizedAuditValue, SaveResult, SessionCallbackError, SessionData, SessionDoneCallback, SessionResultCallback, SessionTableRow, SqlJoinCondition, SqlJoinOperator, SqlParam, SqlSelectField, SqlSelectInput, Validation, ValidationEntry, ValidationItem, ValidationRules, ValidationValue, assertJoinType, assertOrderDirection, buildAuditLogData, buildJoinCondition, buildJoinTable, buildSelectList, callFetchApi, checkPassword, createHmac, createModels, createSignature, decimalNumber, ensureCsrfProtection, escapeColumnReference, escapeHtml, escapeIdentifier, escapeQualifiedName, formatDate, generateKeywords, getActiveStatus, getArrColumn, getArrOnlyInFirst, getCorsOriginPolicy, getCsrfTokenFromRequest, getDaysInMonth, getDiffArr, getLangs, getModels, getMonthRange, getPublishStatus, getTrueFlaseStatus, getValue, initI18n, initModels, isStateChangingMethod, isSubArr, makePassword, parseDate, randomText, removeVietnameseTones, safeEqual, sanitizeAuditPayload, slugify, stringToJson, sumArrCol, sumArrColumn, t, toError, toMySQLDateNowVN, toRoman };
|