@fastify-core/base 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # Fastify Core
2
+
3
+ Fastify Core là một library backend dùng cho ứng dụng Fastify, tập trung vào các thành phần thường gặp trong hệ thống API: model layer, request handling, file upload, JWT, validation, route và utility helpers.
4
+
5
+ Dự án này được thiết kế để dùng như một package npm cho các ứng dụng Node.js / Fastify, đặc biệt là khi bạn cần một nền tảng backend nhanh, gọn và có sẵn các helper phổ biến.
6
+
7
+ ## Tính năng
8
+
9
+ - BaseModel cho truy vấn MySQL CRUD, where builder, pagination, query raw
10
+ - FastRequest để đọc request, xử lý form-data, export file upload
11
+ - FileUpload để upload, resize/check type, move file giữa thư mục
12
+ - JWTApp để tạo và verify token
13
+ - Validation để validate dữ liệu theo rule
14
+ - Route để quản lý danh sách route theo module
15
+ - Common utilities cho password, slug, date, random, keyword, v.v.
16
+ - CSRF protection helper cho request có thay đổi state
17
+
18
+ ## Yêu cầu
19
+
20
+ - Node.js >= 24
21
+ - Fastify >= 5
22
+ - MySQL / mysql2
23
+ - TypeScript (khuyến nghị)
24
+
25
+ ## Cài đặt
26
+
27
+ ```bash
28
+ npm install @fastify-core/base
29
+ ```
30
+
31
+ ## Import
32
+
33
+ ```ts
34
+ import {
35
+ BaseModel,
36
+ FastRequest,
37
+ FileUpload,
38
+ JWTApp,
39
+ Route,
40
+ Validation,
41
+ makePassword,
42
+ checkPassword,
43
+ slugify,
44
+ ensureCsrfProtection,
45
+ } from '@fastify-core/base';
46
+ ```
47
+
48
+ ## Ví dụ sử dụng
49
+
50
+ ### 1. Tạo model
51
+
52
+ ```ts
53
+ import { BaseModel } from '@fastify-core/base';
54
+ import type { Pool } from 'mysql2/promise';
55
+
56
+ export class UserModel extends BaseModel<any> {
57
+ table = 'users';
58
+
59
+ constructor(pool: Pool) {
60
+ super(pool);
61
+ }
62
+ }
63
+ ```
64
+
65
+ ### 2. Validate dữ liệu
66
+
67
+ ```ts
68
+ import { Validation } from '@fastify-core/base';
69
+
70
+ const payload = {
71
+ email: 'admin@example.com',
72
+ password: '123456',
73
+ };
74
+
75
+ const rules = {
76
+ email: 'required|minLen(5)',
77
+ password: 'required|minLen(6)',
78
+ };
79
+
80
+ try {
81
+ await new Validation().runValidate(payload, rules);
82
+ console.log('Valid');
83
+ } catch (error: any) {
84
+ console.error(error.message);
85
+ }
86
+ ```
87
+
88
+ ### 3. Xử lý request
89
+
90
+ ```ts
91
+ import { FastRequest } from '@fastify-core/base';
92
+
93
+ fastify.post('/user', async (request) => {
94
+ const req = new FastRequest(request as any);
95
+ await req.start();
96
+
97
+ const name = req.getPost('name', '', 'stripTags');
98
+ const age = req.getPost('age', 0, 'int');
99
+
100
+ await req.end();
101
+
102
+ return { name, age };
103
+ });
104
+ ```
105
+
106
+ ### 4. Upload file
107
+
108
+ ```ts
109
+ import { FileUpload } from '@fastify-core/base';
110
+
111
+ const files = { avatar: [/* file object */] } as any;
112
+ const uploader = new FileUpload(files, 'static');
113
+
114
+ const uploaded = await uploader.uploadFile('avatar', 'users');
115
+ console.log(uploaded);
116
+ ```
117
+
118
+ ### 5. JWT
119
+
120
+ ```ts
121
+ import { JWTApp } from '@fastify-core/base';
122
+
123
+ const token = JWTApp.createToken({ id: 1, fullname: 'Admin' }, request);
124
+ const payload = JWTApp.verifyToken(request);
125
+ ```
126
+
127
+ ### 6. CSRF
128
+
129
+ ```ts
130
+ import { ensureCsrfProtection } from '@fastify-core/base';
131
+
132
+ const result = ensureCsrfProtection(request, session);
133
+ if (!result.allowed) {
134
+ throw new Error(result.reason || 'csrf_invalid');
135
+ }
136
+ ```
137
+
138
+ ## API chính
139
+
140
+ ### BaseModel
141
+
142
+ BaseModel hỗ trợ các method cơ bản như:
143
+
144
+ - `findOne(filter)`
145
+ - `find(filter)`
146
+ - `save(item, conn?)`
147
+ - `update(data, filter, conn?)`
148
+ - `query(sql, params?, conn?)`
149
+ - `buildWhere(filter, als?)`
150
+ - `getConnection()`
151
+
152
+ ### FastRequest
153
+
154
+ - `start()`
155
+ - `end()`
156
+ - `isPost()`
157
+ - `isGet()`
158
+ - `getPost(name, defaultValue, type)`
159
+ - `getParam(name, defaultValue, type)`
160
+ - `get(name, defaultValue, type)`
161
+ - `getHeader(key)`
162
+
163
+ ### FileUpload
164
+
165
+ - `uploadFile(fieldName, folders)`
166
+ - `uploadFiles(fieldName, folders)`
167
+ - `checkFile(fieldName, type)`
168
+ - `copyFiles(files)`
169
+ - `removeFile(fileName, uploadType)`
170
+ - `removeFiles(paths, uploadType)`
171
+
172
+ ### Validation
173
+
174
+ Validation hỗ trợ rule như:
175
+
176
+ - `required`
177
+ - `requiredId`
178
+ - `minLen(6)`
179
+ - `maxLen(255)`
180
+ - `equalLen(10)`
181
+ - `rangeNum(1, 50)`
182
+ - `min(10)`
183
+ - `max(100)`
184
+ - `integer`
185
+
186
+ ## Common helpers
187
+
188
+ Một số utility có sẵn:
189
+
190
+ - `makePassword(value)`
191
+ - `checkPassword(value, hash)`
192
+ - `randomText(length)`
193
+ - `removeVietnameseTones(str)`
194
+ - `slugify(text)`
195
+ - `generateKeywords(text)`
196
+ - `parseDate(date)`
197
+ - `formatDate(date, format)`
198
+ - `toMySQLDateNowVN()`
199
+ - `sumArrCol(items, col)`
200
+
201
+ ## Route
202
+
203
+ Route giúp quản lý route tập trung và sinh CRUD nhanh:
204
+
205
+ ```ts
206
+ const route = new Route();
207
+ route.add('/admin/user', [
208
+ { link: '/list', module: 'user', controller: UserController, action: 'index' },
209
+ { link: '/detail/:id', module: 'user', controller: UserController, action: 'detail' },
210
+ ]);
211
+
212
+ route.addGS('/admin/product', ProductController, 'product');
213
+ ```
214
+
215
+ ## Environment variables
216
+
217
+ Một số tính năng JWT hoặc session cần biến môi trường:
218
+
219
+ ```bash
220
+ JWT_SECRET_KEY=your_secret_key
221
+ JWT_KEY=your_verify_token
222
+ JWT_AUD=your_audience
223
+ JWT_ISS=your_issuer
224
+ JWT_TIMEOUT=3600
225
+ ```
226
+
227
+ ## Lưu ý về publish package
228
+
229
+ Package hiện đang cấu hình export theo kiểu ESM, nên khi dùng trong môi trường Node/TypeScript cần đảm bảo project của bạn hỗ trợ ESM nếu import theo kiểu module.
@@ -0,0 +1,952 @@
1
+ import * as Decimal from "decimal.js";
2
+ import { Pool, PoolConnection } from "mysql2/promise";
3
+ import { FastifyRequest } from "fastify";
4
+ import { SessionStore } from "@fastify/session";
5
+ //#region src/BaseModel.d.ts
6
+ interface IBaseModel<T = any> {
7
+ table: string;
8
+ isDeleted: boolean;
9
+ errors: string[];
10
+ fieldName(key: string): string;
11
+ save(item: Partial<T>, conn?: PoolConnection): Promise<T | false>;
12
+ findOne(filter: Record<string, any>): Promise<T | null>;
13
+ find(filter: Record<string, any>): Promise<T[]>;
14
+ query(sql: string, params?: any[], conn?: PoolConnection): any;
15
+ update(data: Record<string, any>, filter: Record<string, any>, conn?: PoolConnection): Promise<number>;
16
+ }
17
+ interface PaginationResult<T> {
18
+ page: number;
19
+ length: number;
20
+ pageTotal: number;
21
+ recordTotal: number;
22
+ items: T[];
23
+ }
24
+ interface JoinType {
25
+ table: string;
26
+ on: string;
27
+ type: string;
28
+ fields: string[];
29
+ }
30
+ interface ModifiedType {
31
+ [field: string]: {
32
+ smodel: IBaseModel<any>;
33
+ tkey: string;
34
+ skey: string;
35
+ fmap: string;
36
+ };
37
+ }
38
+ /**
39
+ * Abstract base model class.
40
+ * Provides common database operations such as
41
+ * CRUD, query building, pagination and validation.
42
+ *
43
+ * @template T Entity type, must contain `id`
44
+ */
45
+ declare abstract class BaseModel<T extends Record<string, any> & {
46
+ id: number;
47
+ }> {
48
+ abstract table: string;
49
+ protected pool: Pool;
50
+ isDeleted: boolean;
51
+ errors: string[];
52
+ vdObject: any;
53
+ modifieds: ModifiedType | undefined;
54
+ /**
55
+ * Create model instance
56
+ * @param pool MySQL connection pool
57
+ */
58
+ constructor(pool: Pool);
59
+ fieldName(key: string): string;
60
+ getErrors(): string[];
61
+ getConnection(): Promise<PoolConnection>;
62
+ /**
63
+ * Validate data using Validation rules
64
+ *
65
+ * @param item Data to validate
66
+ * @param vdObject Validation rules
67
+ */
68
+ validate(item: Partial<T>, vdObject?: Record<string, any>): Promise<void>;
69
+ /**
70
+ * Execute SQL query using pool or provided connection
71
+ */
72
+ query(sql: string, params?: any[], conn?: PoolConnection): Promise<[import("mysql2").QueryResult, import("mysql2").FieldPacket[]]>;
73
+ /**
74
+ * Synchronize mapped fields from related models
75
+ *
76
+ * Used to auto-fill display fields from foreign keys
77
+ *
78
+ * @param data Target data
79
+ */
80
+ modifiedSync<T extends Record<string, any>>(data: any): Promise<T>;
81
+ /**
82
+ * Build SQL WHERE clause from filter object
83
+ *
84
+ * Supports advanced operators such as:
85
+ * `$in`, `$nin`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$like`
86
+ *
87
+ * @param filter Query filter object
88
+ * @param als Optional table alias
89
+ */
90
+ buildWhere(filter: Record<string, any>, als?: string): {
91
+ sql: string;
92
+ params: any[];
93
+ };
94
+ /**
95
+ * Count records matching filter
96
+ *
97
+ * @param filter Query conditions
98
+ */
99
+ count(filter?: Record<string, any>): Promise<number>;
100
+ /**
101
+ * Insert a new record
102
+ *
103
+ * @param data Insert data
104
+ * @param conn Optional DB connection
105
+ */
106
+ insert(data: Record<string, any>, conn?: PoolConnection): Promise<number>;
107
+ /**
108
+ * Update records by condition
109
+ */
110
+ update(data: Record<string, any>, filter: Record<string, any>, conn?: PoolConnection): Promise<number>;
111
+ /**
112
+ * Advanced update with dynamic where conditions
113
+ *
114
+ * @param data Update values
115
+ * @param filter Query conditions
116
+ * @param conn Optional DB connection
117
+ */
118
+ updateAdv(data?: Record<string, any>, filter?: Record<string, any>, conn?: PoolConnection): Promise<any>;
119
+ /**
120
+ * Insert or update record by primary key
121
+ *
122
+ * - If `item.id` exists → update
123
+ * - Otherwise → insert
124
+ *
125
+ * @param item Entity data
126
+ * @param conn Optional DB connection
127
+ */
128
+ save(item: Partial<T>, conn?: PoolConnection): Promise<T | false>;
129
+ /**
130
+ * Validate and save record
131
+ *
132
+ * @throws Error when validation or save fails
133
+ */
134
+ vdSave(item: any, vdObject?: any | false, conn?: PoolConnection): Promise<T | null>;
135
+ /**
136
+ * Batch update multiple records using CASE WHEN
137
+ *
138
+ * @param rows Data rows
139
+ * @param key Primary key field
140
+ */
141
+ updateMany(rows: any[], key: string, conn?: PoolConnection): Promise<void>;
142
+ /**
143
+ * Bulk insert or update (UPSERT) records
144
+ *
145
+ * @param rows Data rows
146
+ * @param key Primary key field
147
+ */
148
+ updateAndCreateMany(rows: any[], key: string, conn?: PoolConnection): Promise<void>;
149
+ /**
150
+ * Find records with advanced options
151
+ *
152
+ * @param filter Query conditions
153
+ * @param order Sort options
154
+ * @param join Join configuration
155
+ * @param select Custom select fields
156
+ * @param limit Limit result count
157
+ */
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
+ deleteById(id: number, conn?: PoolConnection): Promise<boolean>;
163
+ deleteMany(filter: Record<string, any>, limit?: number, conn?: PoolConnection): Promise<boolean>;
164
+ isField(field: string, table?: string): Promise<boolean>;
165
+ isFields(fields?: string[]): Promise<string[]>;
166
+ /**
167
+ * Get mapped display value from a related model.
168
+ *
169
+ * This method is commonly used to convert a foreign key value
170
+ * into a human-readable field (e.g. `name`, `title`) from another table.
171
+ *
172
+ * It will only query the related model when:
173
+ * - `fieldVal` is provided
174
+ * - `fieldVal` is different from the previous value (`itemOld`)
175
+ *
176
+ * @param field Target field name in current model
177
+ * @param fieldVal New value of the field (usually a foreign key ID)
178
+ * @param mdClass Related model class instance used to fetch data
179
+ * @param itemOld Previous record data (used to detect value changes)
180
+ * @param fieldMap Field name in related model to map from (default: `"name"`)
181
+ *
182
+ * @returns Mapped display value from related model,
183
+ * or empty string if not found or unchanged
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * // Convert categoryId → categoryName
188
+ * const categoryName = await productModel.getMapName(
189
+ * "categoryId",
190
+ * product.categoryId,
191
+ * categoryModel,
192
+ * oldProduct,
193
+ * "name"
194
+ * );
195
+ * ```
196
+ */
197
+ getMapName(field: string, fieldVal: any, mdClass: any, itemOld?: any, fieldMap?: string): Promise<any>;
198
+ /**
199
+ * Find records with pagination support
200
+ *
201
+ * @param filter Query conditions
202
+ * @param order Sort options
203
+ * @param join Join configuration
204
+ * @param select Custom select fields
205
+ * @param page Page number (starts from 1)
206
+ * @param limit Items per page
207
+ */
208
+ findWithPagination(filter?: Record<string, any>, order?: Record<string, string> | false, join?: JoinType | JoinType[] | false, select?: string | string[] | false, page?: number, limit?: number): Promise<PaginationResult<T>>;
209
+ }
210
+ //#endregion
211
+ //#region src/Common.d.ts
212
+ /**
213
+ * 🔐 Mã hoá mật khẩu bằng argon2
214
+ * @param value Chuỗi mật khẩu gốc
215
+ * @returns Chuỗi mật khẩu đã được hash
216
+ */
217
+ declare function makePassword(value: any): Promise<string>;
218
+ /**
219
+ * 🔐 Kiểm tra mật khẩu với hash
220
+ * @param value Mật khẩu người dùng nhập
221
+ * @param hashedValue Mật khẩu đã hash lưu trong DB
222
+ * @returns true nếu khớp, false nếu không
223
+ */
224
+ declare function checkPassword(value: any, hashedValue: any): Promise<boolean>;
225
+ /**
226
+ * 🔢 Tạo chuỗi random
227
+ * @param length Độ dài chuỗi
228
+ * @param az Có bao gồm chữ cái hay không
229
+ * @returns Chuỗi random
230
+ */
231
+ declare function randomText(length: number, az?: boolean): string;
232
+ /**
233
+ * 🅰️ Loại bỏ dấu tiếng Việt, chuyển về chữ thường
234
+ * @param str Chuỗi đầu vào
235
+ * @returns Chuỗi không dấu, lowercase
236
+ */
237
+ declare function removeVietnameseTones(str: string): string;
238
+ /**
239
+ * 🔗 Tạo slug thân thiện URL
240
+ * @param text Chuỗi đầu vào
241
+ * @returns slug dạng kebab-case
242
+ */
243
+ declare function slugify(text: string): string;
244
+ /**
245
+ * 🌐 Gọi API bằng fetch
246
+ * @param url Endpoint API
247
+ * @param data Payload gửi đi
248
+ * @param type Method HTTP (POST | GET | PUT | DELETE)
249
+ * @param ctype Content-Type
250
+ * @param headers Header bổ sung
251
+ * @returns JSON response
252
+ */
253
+ declare function callFetchApi(url: string, data?: any, type?: string, ctype?: string, headers?: Record<string, any>): Promise<any>;
254
+ /**
255
+ * 🧠 Tạo chuỗi keyword phục vụ search
256
+ * @param text Chuỗi đầu vào
257
+ * @returns Chuỗi keyword, phân tách bằng dấu phẩy
258
+ */
259
+ declare function generateKeywords(text?: string): string;
260
+ /**
261
+ * 📅 Parse ngày an toàn
262
+ * @param date Chuỗi hoặc Date
263
+ * @returns Date object hợp lệ
264
+ * @throws Error nếu ngày không hợp lệ
265
+ */
266
+ declare function parseDate(date: string | Date): Date;
267
+ /**
268
+ * 📅 Format ngày theo pattern
269
+ * @param date Date hoặc string
270
+ * @param format Pattern (YYYY-MM-DD HH:mm:ss)
271
+ * @returns Chuỗi ngày đã format
272
+ */
273
+ declare function formatDate(date: Date, format?: string): string;
274
+ /**
275
+ * 📆 Lấy khoảng thời gian của tháng
276
+ * @param year Năm
277
+ * @param month Tháng (1-12)
278
+ * @returns Object { start, end }
279
+ */
280
+ declare function getMonthRange(year: number, month: number): {
281
+ start: Date;
282
+ end: Date;
283
+ };
284
+ /**
285
+ * 📆 Lấy danh sách ngày trong tháng
286
+ * @param year Năm
287
+ * @param month Tháng (1-12)
288
+ * @returns Mảng số ngày
289
+ */
290
+ declare function getDaysInMonth(year: number, month: number): number[];
291
+ /**
292
+ * 🇻🇳 Lấy thời gian hiện tại theo timezone Việt Nam (MySQL format)
293
+ * @returns Chuỗi datetime yyyy-MM-dd HH:mm:ss
294
+ */
295
+ declare function toMySQLDateNowVN(): string;
296
+ /**
297
+ * 📥 Lấy giá trị object an toàn
298
+ * @param item Object
299
+ * @param key Tên key
300
+ * @param df Giá trị mặc định
301
+ * @returns Giá trị hoặc default
302
+ */
303
+ declare function getValue(item: any, key: any, df?: any): any;
304
+ /**
305
+ * ➕ Tính tổng 1 cột trong mảng object
306
+ * @param items Mảng object
307
+ * @param col Tên cột
308
+ * @returns Tổng giá trị
309
+ */
310
+ declare function sumArrCol<T extends Record<string, number>>(items: T[], col: keyof T): number;
311
+ /**
312
+ * 🏛️ Chuyển số nguyên sang chữ số La Mã
313
+ * @param num Số nguyên dương cần chuyển (>= 1)
314
+ * @returns Chuỗi chữ số La Mã (VD: 4 -> IV, 9 -> IX)
315
+ * @note Không kiểm tra giới hạn trên, nên chỉ dùng cho số hợp lý (thường < 4000)
316
+ */
317
+ declare function toRoman(num: number): string;
318
+ /**
319
+ * 🔘 Lấy trạng thái Hoạt động / Khóa
320
+ * @param id Giá trị trạng thái (1 | 0 | false)
321
+ * @param color Có trả về HTML badge hay không
322
+ * @returns
323
+ * - Nếu id === false: trả về toàn bộ danh sách trạng thái
324
+ * - Nếu color = true: trả về chuỗi HTML badge
325
+ * - Ngược lại: trả về tên trạng thái
326
+ */
327
+ declare function getActiveStatus(id: string | boolean | number, color?: boolean): string | {
328
+ id: number;
329
+ name: string;
330
+ color: string;
331
+ }[];
332
+ /**
333
+ * 📢 Lấy trạng thái Xuất bản / Nháp
334
+ * @param id Giá trị trạng thái (1 | 0 | false)
335
+ * @param color Có trả về HTML badge hay không
336
+ * @returns Tên trạng thái, badge HTML hoặc danh sách trạng thái
337
+ */
338
+ declare function getPublishStatus(id: string | boolean | number, color?: boolean): string | {
339
+ id: number;
340
+ name: string;
341
+ color: string;
342
+ }[];
343
+ /**
344
+ * ✅❌ Lấy trạng thái Có / Không
345
+ * @param id Giá trị boolean dạng số (1 | 0 | false)
346
+ * @param color Có trả về HTML badge hay không
347
+ * @returns Tên trạng thái hoặc badge HTML
348
+ */
349
+ declare function getTrueFlaseStatus(id: string | boolean | number, color?: boolean): string | {
350
+ id: number;
351
+ name: string;
352
+ color: string;
353
+ }[];
354
+ /**
355
+ * 🔀 Lấy các phần tử khác nhau giữa 2 mảng
356
+ * @param arr1 Mảng thứ nhất
357
+ * @param arr2 Mảng thứ hai
358
+ * @returns Mảng chứa các phần tử chỉ xuất hiện ở một trong hai mảng
359
+ */
360
+ declare function getDiffArr(arr1: any[], arr2: any[]): any[];
361
+ /**
362
+ * 📦 Kiểm tra mảng arr1 có phải là tập con của arr2 hay không
363
+ * @param arr1 Mảng cần kiểm tra
364
+ * @param arr2 Mảng cha
365
+ * @returns true nếu mọi phần tử arr1 đều tồn tại trong arr2
366
+ */
367
+ declare function isSubArr(arr1: any[], arr2: any[]): boolean;
368
+ /**
369
+ * ➖ Lấy các phần tử chỉ có trong mảng thứ nhất
370
+ * @param arr1 Mảng gốc
371
+ * @param arr2 Mảng so sánh
372
+ * @returns Mảng các phần tử chỉ tồn tại trong arr1
373
+ */
374
+ declare function getArrOnlyInFirst(arr1: any[], arr2: any[]): any[];
375
+ /**
376
+ * 📤 Lấy danh sách giá trị của một cột trong mảng object
377
+ * @param arr Mảng object
378
+ * @param field Tên field cần lấy
379
+ * @returns Mảng giá trị của field
380
+ */
381
+ declare function getArrColumn(arr: any[], field: string): any[];
382
+ /**
383
+ * ➕ Tính tổng một cột trong mảng object với điều kiện tùy chọn
384
+ * @param arr Mảng object
385
+ * @param sumColumn Tên cột cần tính tổng
386
+ * @param conditions Điều kiện lọc (optional)
387
+ * @returns Tổng giá trị thỏa điều kiện
388
+ */
389
+ declare function sumArrColumn<T extends Record<string, any>>(arr: T[], sumColumn: keyof T, conditions?: Partial<T>): number;
390
+ /**
391
+ * 🔢 Chuyển chuỗi số sang Decimal.js an toàn
392
+ * @param value Chuỗi hoặc số (có thể chứa ký tự khác)
393
+ * @returns Decimal instance
394
+ */
395
+ declare function decimalNumber(value: string | number): Decimal.Decimal;
396
+ /**
397
+ * 🧾 Parse JSON an toàn từ nhiều kiểu dữ liệu
398
+ * @param input Dữ liệu đầu vào (string | object | array)
399
+ * @param df Giá trị mặc định nếu parse lỗi
400
+ * @returns Object | Array | df
401
+ */
402
+ declare function stringToJson(input: any, df?: Record<string, any>): any;
403
+ /**
404
+ * 🌐 Lấy danh sách ngôn ngữ
405
+ * @param ids Danh sách id cần lọc hoặc false để lấy tất cả
406
+ * @returns Mảng ngôn ngữ
407
+ */
408
+ declare function getLangs(ids?: number[] | false): {
409
+ id: number;
410
+ name: string;
411
+ }[];
412
+ //#endregion
413
+ //#region src/Csrf.d.ts
414
+ declare function isStateChangingMethod(method: string): boolean;
415
+ declare function getCsrfTokenFromRequest(req: any): any;
416
+ declare function ensureCsrfProtection(req: any, session: any, options?: any): {
417
+ allowed: boolean;
418
+ reason: null;
419
+ } | {
420
+ allowed: boolean;
421
+ reason: string;
422
+ };
423
+ //#endregion
424
+ //#region src/FastRequest.d.ts
425
+ /**
426
+ * Kiểu dữ liệu file upload tạm
427
+ */
428
+ interface FileType$1 {
429
+ filename: string;
430
+ basename: string;
431
+ ext: string;
432
+ mimetype: string;
433
+ path: string;
434
+ }
435
+ /**
436
+ * FastRequest
437
+ * ==================================================
438
+ * Class wrapper cho FastifyRequest
439
+ *
440
+ * Công dụng chính:
441
+ * - Parse request body (json, multipart/form-data)
442
+ * - Xử lý upload file tạm thời
443
+ * - Lấy dữ liệu GET / POST / PARAM / HEADER
444
+ * - Filter & validate dữ liệu đầu vào an toàn
445
+ *
446
+ * Thường dùng cho:
447
+ * - Controller
448
+ * - API xử lý form
449
+ * - Import / export / upload file
450
+ */
451
+ declare class FastRequest {
452
+ [x: string]: any;
453
+ /** Fastify request gốc */
454
+ private req;
455
+ /** Body sau khi parse */
456
+ private body;
457
+ /** Danh sách file upload theo field */
458
+ files: {
459
+ [key: string]: FileType$1[];
460
+ };
461
+ /**
462
+ * Khởi tạo Request wrapper
463
+ *
464
+ * @param req FastifyRequest
465
+ */
466
+ constructor(req: FastifyRequest);
467
+ /**
468
+ * Start xử lý request
469
+ *
470
+ * Công dụng:
471
+ * - Parse body thường (JSON)
472
+ * - Parse multipart/form-data
473
+ * - Lưu file upload vào thư mục tạm
474
+ * - Gom field & file theo đúng format
475
+ */
476
+ start: () => Promise<void>;
477
+ /**
478
+ * Cleanup request
479
+ *
480
+ * Công dụng:
481
+ * - Xóa file upload tạm sau khi xử lý xong
482
+ */
483
+ end: () => Promise<void>;
484
+ /** Check method POST */
485
+ isPost(): boolean;
486
+ /** Check method GET */
487
+ isGet(): boolean;
488
+ /** Check AJAX request */
489
+ isAjax(): boolean;
490
+ /**
491
+ * Lấy dữ liệu POST
492
+ *
493
+ * @param name Tên field
494
+ * @param defaultValue Giá trị mặc định
495
+ * @param type Kiểu filter
496
+ *
497
+ * @returns Giá trị đã filter
498
+ */
499
+ getPost(name?: string, defaultValue?: any, type?: string): any;
500
+ /**
501
+ * Lấy dữ liệu POST dạng đa ngôn ngữ
502
+ *
503
+ * Ví dụ field: title{vi}, title{en}
504
+ */
505
+ getLangPost(name: string, defaultValue?: any, type?: string): any;
506
+ /**
507
+ * Lấy param từ URL
508
+ */
509
+ getParam(name: string, defaultValue?: any, type?: string): any;
510
+ /**
511
+ * Lấy query string
512
+ */
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
+ getHeader(key: string): string;
520
+ /**
521
+ * Filter & validate dữ liệu an toàn
522
+ *
523
+ * @param input Dữ liệu đầu vào
524
+ * @param type Kiểu filter
525
+ * @param defaultValue Giá trị fallback
526
+ */
527
+ static filterSafeData(input: any, type?: string, defaultValue?: any): any;
528
+ /** Parse raw JSON input */
529
+ private static parseRawInput;
530
+ /** Filter HTML tránh XSS */
531
+ private static filterHtml;
532
+ /** Filter mảng ID number */
533
+ private static filterIds;
534
+ /** Filter mảng ID string */
535
+ private static filterStringIds;
536
+ /** Filter mảng string */
537
+ private static filterStrings;
538
+ /** Filter ID dạng LIKE "%id%" */
539
+ private static filterDQuoteIds;
540
+ }
541
+ //#endregion
542
+ //#region src/FileUpload.d.ts
543
+ /**
544
+ * Kiểu dữ liệu file upload
545
+ */
546
+ interface FileType {
547
+ filename: string;
548
+ basename: string;
549
+ ext: string;
550
+ mimetype: string;
551
+ path: string;
552
+ }
553
+ /**
554
+ * Class xử lý upload / kiểm tra / quản lý file
555
+ */
556
+ declare class FileUpload {
557
+ /**
558
+ * Danh sách file nhận từ request (theo field)
559
+ */
560
+ private files;
561
+ /**
562
+ * Loại thư mục upload (static | public | uploads)
563
+ */
564
+ private uploadType;
565
+ constructor(files: {
566
+ [key: string]: FileType[];
567
+ }, uploadType?: string);
568
+ /**
569
+ * Copy danh sách file từ src sang dist
570
+ * Dùng khi cần nhân bản file đã tồn tại
571
+ */
572
+ copyFiles(files: any[]): Promise<void>;
573
+ /**
574
+ * Upload 1 file theo field name
575
+ * @returns đường dẫn file sau upload hoặc false
576
+ */
577
+ uploadFile(fieldName?: string, folders?: string): Promise<string | false>;
578
+ /**
579
+ * Upload nhiều file theo field name
580
+ * @returns mảng đường dẫn file sau upload
581
+ */
582
+ uploadFiles(fieldName?: string, folders?: string): Promise<string[]>;
583
+ /**
584
+ * Upload 1 file cụ thể
585
+ * - Tự tạo tên file theo timestamp
586
+ * - Tạo folder nếu chưa tồn tại
587
+ * - Move file từ temp sang thư mục upload
588
+ */
589
+ upload(file: FileType, folders?: string): Promise<string | false>;
590
+ /**
591
+ * Kiểm tra file upload theo loại (OfficeFile, ImgFile, VideoFile, ...)
592
+ * Gọi dynamic method: check + type
593
+ */
594
+ checkFile(fieldName?: string, type?: string): Promise<void>;
595
+ /**
596
+ * Xóa nhiều file
597
+ */
598
+ static removeFiles: (pathFiles: string[] | boolean, uploadType?: string) => boolean;
599
+ /**
600
+ * Xóa 1 file theo đường dẫn
601
+ */
602
+ static removeFile: (fileName: string | false, uploadType?: string) => boolean;
603
+ /**
604
+ * Đổi tên file bằng cách thêm timestamp
605
+ */
606
+ static renameFile: (fileName: string) => string;
607
+ /**
608
+ * Lấy thư mục upload theo loại
609
+ */
610
+ static getUploadDir(uploadType?: string): string;
611
+ /**
612
+ * Check file mặc định (ảnh + office)
613
+ */
614
+ static checkDefFile(fileName: string, ftype: string, isTry?: boolean): boolean;
615
+ /**
616
+ * Check file Office
617
+ */
618
+ static checkOfficeFile(fileName: string, ftype: string): void;
619
+ /**
620
+ * Check file hình ảnh
621
+ */
622
+ static checkImgFile(fileName: string, ftype: string): void;
623
+ /**
624
+ * Check file video
625
+ */
626
+ static checkVideoFile(fileName: string, ftype: string): void;
627
+ }
628
+ //#endregion
629
+ //#region src/JWTApp.d.ts
630
+ declare class JWTApp {
631
+ static createToken(user: any, req: any): string;
632
+ static verifyToken(req: FastifyRequest & {
633
+ cookies?: any;
634
+ }): any;
635
+ static checkSecretKey(key: string): void;
636
+ }
637
+ //#endregion
638
+ //#region src/Models.d.ts
639
+ type ModelConstructor<T extends BaseModel<any> = BaseModel<any>> = new (pool: Pool) => T;
640
+ type Models<T extends Record<string, ModelConstructor>> = { [K in keyof T]: InstanceType<T[K]>; };
641
+ declare function createModels<T extends Record<string, ModelConstructor>>(modelMap: T, pool: Pool): Models<T>;
642
+ declare function initModels<T extends Record<string, ModelConstructor>>(modelMap: T, pool: Pool): Models<T>;
643
+ declare function getModels<T>(): T;
644
+ //#endregion
645
+ //#region src/MySQLSessionStore.d.ts
646
+ declare class MySQLSessionStore implements SessionStore {
647
+ private pool;
648
+ constructor(pool: Pool);
649
+ get(sid: string, cb: (err: any, session?: any | null) => void): Promise<void>;
650
+ set(sid: string, session: any, cb: (err?: any) => void): Promise<void>;
651
+ destroy(sid: string, cb: (err?: any) => void): Promise<void>;
652
+ }
653
+ //#endregion
654
+ //#region src/Route.d.ts
655
+ /**
656
+ * RouterType
657
+ * ==================================================
658
+ * Interface mô tả 1 route trong hệ thống
659
+ *
660
+ * @property link Đường dẫn URL
661
+ * @property module Tên module (phân quyền / phân nhóm)
662
+ * @property controller Controller xử lý request
663
+ * @property action Method trong controller
664
+ * @property method HTTP method (get | post | put | delete...)
665
+ * @property rateLimit Giới hạn request (optional)
666
+ */
667
+ interface RouterType {
668
+ link: string;
669
+ module: string;
670
+ controller: any;
671
+ action: string;
672
+ method?: string;
673
+ rateLimit?: number;
674
+ rateLimitConfig?: {
675
+ max: number;
676
+ timeWindow: string;
677
+ blockDuration?: number;
678
+ };
679
+ }
680
+ /**
681
+ * Route
682
+ * ==================================================
683
+ * Class quản lý danh sách route toàn hệ thống
684
+ *
685
+ * Công dụng chính:
686
+ * - Gom route theo module
687
+ * - Tự động prefix URL
688
+ * - Sinh nhanh các route CRUD chuẩn (GS)
689
+ *
690
+ * Thường dùng cho:
691
+ * - Khai báo routing tập trung
692
+ * - Auto register route cho Fastify / Express
693
+ */
694
+ declare class Route {
695
+ /** Danh sách route */
696
+ private routes;
697
+ /**
698
+ * Lấy HTTP method của route
699
+ *
700
+ * @param router RouterType
701
+ * @returns method (mặc định: GET)
702
+ */
703
+ static getMethod(router: any): any;
704
+ /**
705
+ * Thêm danh sách route với prefix
706
+ *
707
+ * @param prefix Prefix URL (vd: /admin/user)
708
+ * @param routeList Danh sách route con
709
+ *
710
+ * @example
711
+ * route.add('/admin/user', [
712
+ * { link: '/list', module: 'user', controller: UserController, action: 'index' }
713
+ * ])
714
+ */
715
+ add(prefix: string, routeList?: RouterType[]): void;
716
+ /**
717
+ * addGS (Get - Set)
718
+ * ==================================================
719
+ * Tự động sinh bộ route CRUD chuẩn cho 1 module
720
+ *
721
+ * Các route được tạo:
722
+ * - GET /getList
723
+ * - GET /detail/:code
724
+ * - POST /create
725
+ * - POST /edit/:code
726
+ * - POST /copy
727
+ * - POST /import
728
+ * - GET /export
729
+ * - GET /delete/:code
730
+ * - POST /delete
731
+ *
732
+ * @param link Base URL (vd: /admin/product)
733
+ * @param controller Controller xử lý
734
+ * @param module Tên module
735
+ */
736
+ addGS(link: string, controller: any, module: string): void;
737
+ /**
738
+ * Lấy toàn bộ danh sách route
739
+ *
740
+ * @returns RouterType[]
741
+ */
742
+ getRouter(): RouterType[];
743
+ }
744
+ //#endregion
745
+ //#region src/Utils.d.ts
746
+ declare function sanitizeAuditPayload(value: any, parentKey?: string): any;
747
+ declare function buildAuditLogData(item: any, itemOld: any, ignoreFields?: never[]): {
748
+ difference: any;
749
+ item: any;
750
+ };
751
+ declare function getCorsOriginPolicy(corsOrigins: any, isProduction: boolean | number): any;
752
+ //#endregion
753
+ //#region src/Validation.d.ts
754
+ declare class Validation {
755
+ private md?;
756
+ constructor(md?: IBaseModel<any>);
757
+ private errors;
758
+ runValidate(item: any, vdObject?: Record<string, any>): Promise<void>;
759
+ /**
760
+ * Start the validation using values and rules passed in data
761
+ * @param array data
762
+ * @param bool skip To skip validations as soon as one of the rules fails+
763
+ * @throws Error if rule method doesn't exist
764
+ * @return bool
765
+ */
766
+ validate(validation: any, skip?: boolean): Promise<boolean>;
767
+ /**
768
+ * Determine if a given rule has arguments, Ex: max(4)
769
+ *
770
+ * @param string rule
771
+ * @return bool
772
+ */
773
+ private isruleHasArgs;
774
+ /**
775
+ * get rule name for rules that have args
776
+ *
777
+ * @param string rule
778
+ * @return string
779
+ */
780
+ private getRuleName;
781
+ /**
782
+ * get arguments for rules that have args
783
+ *
784
+ * @param string rule
785
+ * @return array
786
+ */
787
+ private getRuleArgs;
788
+ /**
789
+ * Add an error
790
+ *
791
+ * @param string rule
792
+ * @param string placeholder for filed
793
+ * @param mixed value
794
+ * @param array args
795
+ *
796
+ */
797
+ private addError;
798
+ /**
799
+ * Checks if validation has passed.
800
+ *
801
+ * @return bool
802
+ */
803
+ passes(): boolean;
804
+ /**
805
+ * get all errors
806
+ * @return array
807
+ */
808
+ getErrors(): string[];
809
+ /**
810
+ * clear all existing errors
811
+ * @return bool
812
+ */
813
+ clearErrors(): void;
814
+ /** *********************************************** **/
815
+ /** ************** Validations ************** **/
816
+ /** *********************************************** **/
817
+ /**
818
+ * Is value not empty?
819
+ * @param mixed value
820
+ * @return bool
821
+ */
822
+ private required;
823
+ /**
824
+ * Is value not empty?
825
+ * @param mixed value
826
+ * @return bool
827
+ */
828
+ private requiredId;
829
+ /**
830
+ * min string length
831
+ * @param string str
832
+ * @param array args(min)
833
+ * @return bool
834
+ */
835
+ private minLen;
836
+ private equalLen;
837
+ /**
838
+ * max string length
839
+ *
840
+ * @param string str
841
+ * @param array args(max)
842
+ *
843
+ * @return bool
844
+ */
845
+ private maxLen;
846
+ /**
847
+ * check if number between given range of numbers
848
+ *
849
+ * @param int num
850
+ * @param array args(min,max)
851
+ * @return bool
852
+ */
853
+ private rangeNum;
854
+ /**
855
+ * check if number between given range of numbers
856
+ *
857
+ * @param int num
858
+ * @param array args(min,max)
859
+ * @return bool
860
+ */
861
+ private max;
862
+ private min;
863
+ /**
864
+ * check if value is a valid number
865
+ *
866
+ * @param string|integer value
867
+ * @return bool
868
+ */
869
+ private integer;
870
+ /**
871
+ * check if value(s) is in a given array
872
+ *
873
+ * @param string|array value
874
+ * @param array arr
875
+ * @return bool
876
+ */
877
+ private inArray;
878
+ private inArrayNumber;
879
+ /**
880
+ * check if value is contains alphabetic characters and numbers
881
+ *
882
+ * @param mixed value
883
+ * @return bool
884
+ */
885
+ private alphaNum;
886
+ /**
887
+ * check if value is contains alphabetic characters and numbers
888
+ *
889
+ * @param mixed value
890
+ * @return bool
891
+ */
892
+ private number;
893
+ /**
894
+ * check if value is contains alphabetic characters, numbers and spaces
895
+ *
896
+ * @param mixed value
897
+ * @return bool
898
+ */
899
+ private alphaNumWithSpaces;
900
+ /**
901
+ * check if password has at least
902
+ * - one lowercase letter
903
+ * - one uppercase letter
904
+ * - one number
905
+ * - one special(non-word) character
906
+ */
907
+ private password;
908
+ /**
909
+ * - Phone VN
910
+ */
911
+ private phoneVn;
912
+ /**
913
+ * check if value is equals to another value(strings)
914
+ *
915
+ * @param string value
916
+ * @param array args(value)
917
+ * @return bool
918
+ */
919
+ private equals;
920
+ /**
921
+ * check if value is not equal to another value(strings)
922
+ *
923
+ * @param string value
924
+ * @param array args(value)
925
+ * @return bool
926
+ */
927
+ private notEqual;
928
+ /**
929
+ * check if value is a valid email
930
+ *
931
+ * @param string email
932
+ * @return bool
933
+ */
934
+ private email;
935
+ /** *********************************************** **/
936
+ /** ************ Database Validations *********** **/
937
+ /** *********************************************** **/
938
+ /**
939
+ * check if a value of a column is unique.
940
+ *
941
+ * @param string value
942
+ * @param array args(table, column)
943
+ * @return bool
944
+ */
945
+ private unique;
946
+ /** *********************************************** **/
947
+ /** ************ Default Messages *********** **/
948
+ /** *********************************************** **/
949
+ private static defaultMessages;
950
+ }
951
+ //#endregion
952
+ export { BaseModel, FastRequest, FileUpload, IBaseModel, JWTApp, JoinType, ModelConstructor, Models, ModifiedType, MySQLSessionStore, PaginationResult, Route, RouterType, Validation, buildAuditLogData, callFetchApi, checkPassword, createModels, decimalNumber, ensureCsrfProtection, formatDate, generateKeywords, getActiveStatus, getArrColumn, getArrOnlyInFirst, getCorsOriginPolicy, getCsrfTokenFromRequest, getDaysInMonth, getDiffArr, getLangs, getModels, getMonthRange, getPublishStatus, getTrueFlaseStatus, getValue, initModels, isStateChangingMethod, isSubArr, makePassword, parseDate, randomText, removeVietnameseTones, sanitizeAuditPayload, slugify, stringToJson, sumArrCol, sumArrColumn, toMySQLDateNowVN, toRoman };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import e from"argon2";import{format as t}from"date-fns-tz";import*as n from"decimal.js";import r from"fs";import i from"path";import{format as a,isValid as o,parseISO as s}from"date-fns";import{pipeline as c}from"stream/promises";import l from"jsonwebtoken";var u=class e{md;constructor(e){this.md=e}errors=[];async runValidate(e,t={}){if(Object.entries(t).length===0)return;let n=[];for(let r in t){if(!Object.prototype.hasOwnProperty.call(t,r))continue;let i=t[r],a=e?.id||0,o=e?.[r]??``,s=this?.md?.fieldName(r)??r;n.push({field:r,value:o,rules:i,fieldName:s,id:a})}if(n.length&&(await this.validate(n),this.getErrors().length)){let e=this.getErrors().join(`, `);throw Error(e)}}async validate(e,t=!1){let n=!0;for await(let r of e){let e=r.filed,i=r.id,a=r.fieldName,o=r.value,s=r.rules;s=s.split(`|`);for await(let r of s){let s=r,c=[];if(this.isruleHasArgs(r)?(s=this.getRuleName(r),c=this.getRuleArgs(r)):c=e,!(s in this))throw Error(`Method doesnt exists: `+s);if(!await this[s](o,c,i)&&(this.addError(s,a,o,c),n=!1,t))return!1}}return n}isruleHasArgs(e){return e.split(`(`)[1]!==void 0}getRuleName(e){return e.split(`(`)[0]}getRuleArgs(e){e=e.trim();let t=e.split(`(`)[1];return t=t.endsWith(`)`)?t.slice(0,-1):t,t.split(`,`)}addError(t,n,r,i=[]){if(t){let a=e.defaultMessages(t);a?a.trim()!==``&&(r=typeof r==`string`?r:``,a=a.replace(`{placeholder}`,n),a=a.replace(`{value}`,r),i=typeof i==`string`?[i]:i,i.forEach((e,t)=>{a=a.replace(`{${t}}`,e)}),this.errors.push(a)):this.errors.push(`The value you entered for `+n+` is invalid`)}}passes(){return this.errors.length===0}getErrors(){return this.errors}clearErrors(){this.errors=[]}required(e){return e==null?!1:typeof e!=`string`||e.trim()!==``}requiredId(e){if(e==null)return!1;if(typeof e==`string`){if(e.trim()===``)return!1}else if(typeof e==`number`&&e===0)return!1;return!0}minLen(e,t){return e.length>=parseInt(t[0])}equalLen(e,t){return e.length===parseInt(t[0])}maxLen(e,t){return e.length<=parseInt(t[0])}rangeNum(e,t){return e>=parseInt(t[0])&&e<=parseInt(t[1])}max(e,t){return e<=Number(t[0])}min(e,t){return e>=Number(t[0])}integer(e){return!Number.isNaN(parseInt(e))}inArray(e,t){if(typeof e==`object`){for(let n in e)if(!(n in t))return!1;return!0}return e in t}inArrayNumber(e,t){let n=t.map(Number);return typeof e==`object`?e.every(e=>n.includes(e)):n.includes(e)}alphaNum(e){return!e||/^[a-z0-9]+$/i.test(e)}number(e){if(typeof e==`number`&&(e>2**53-1||e<-(2**53-1)))throw Error(`INVALID_SAFE_NUMBER`);return typeof e==`number`}alphaNumWithSpaces(e){return!e||/^[a-z0-9 ]+$/i.test(e)}password(e){return!e||/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#!%^&*])[0-9a-zA-Z@#!%^&*.]{8,32}/.test(e)}phoneVn(e){return!e||/^[03|05|07|08|09]{2}[0-9]{8}/.test(e)}equals(e,t){return e===t[0]}notEqual(e,t){return e!==t[0]}email(e){return!e||/^([A-Za-z0-9_.-])+@([A-Za-z0-9_.-]+)\.([A-Za-z]{2,4})$/.test(e)}async unique(e,t,n){try{if(!this.md)throw Error(`invalid_md`);let r=`SELECT id FROM \`${t[0].trim()}\` WHERE \`${t[1].trim()}\` = ?`,i=[e];this.md.isDeleted&&(r+=` AND isDeleted = 0`),n&&(r+=` AND id != ?`,i.push(n));let[a]=await this.md.query(r,i);return a.length===0}catch(e){return console.log(e),!1}}static defaultMessages(e){return{required:`{placeholder} không thể trống`,requiredId:`{placeholder} không hợp lệ`,minLen:`{placeholder} không thể nhỏ hơn {0} ký tự`,equalLen:`{placeholder} phải có {0} ký tự`,phoneVn:`{placeholder} không phải thuê bao Việt Nam`,findOne:`{placeholder} không tồn tại`,maxLen:`{placeholder} không thể lớn hơn {0} ký tự`,min:`{placeholder} không thể nhỏ hơn {0}`,max:`{placeholder} không thể lớn hơn {0}`,rangeNum:`{placeholder} phải nằm trong khoản từ {0} đến {1}`,integer:`{placeholder} phải là số`,number:`{placeholder} không đúng định dạng`,inArray:`{placeholder} không hợp lệ`,inArrayNumber:`{placeholder} không hợp lệ`,alphaNum:`Chỉ cho phép chữ cái và số cho {placeholder}`,alphaNumWithSpaces:`Chỉ cho phép chữ cái, số và khoảng trắng cho {placeholder}`,password:`Vui lòng nhập mật khẩu dài 8-32 ký tự, có ký tự chữ số, chữ hoa và chữ thường, ký tự @#$!%^&*`,equals:`{placeholder} không đúng`,notEqual:`{placeholder} không thể bằng {0}`,email:`Email không đúng định dạng`,unique:`{placeholder} đã được sử dụng`}[e]??null}},d=class{pool;isDeleted=!1;errors=[];vdObject;modifieds;constructor(e){this.pool=e}fieldName(e){return e}getErrors(){return this.errors}async getConnection(){return await this.pool.getConnection()}async validate(e,t={}){await new u(this).runValidate(e,t)}async query(e,t,n){return n?n.query(e,t):this.pool.query(e,t)}async modifiedSync(e){if(!this.modifieds)return e;for(let t in this.modifieds){let{smodel:n,tkey:r,skey:i,fmap:a}=this.modifieds[t];if(!e[r])continue;let o=await n.findOne({[i]:e[r]});o&&(e[t]=o[a]||``)}return e}buildWhere(e,t){let n={...e};this.isDeleted&&n.isDeleted===void 0?n.isDeleted=0:this.isDeleted||delete n.isDeleted;let r=[],i=e=>e.split(`.`).map(e=>`\`${e}\``).join(`.`),a=e=>typeof e==`boolean`?+!!e:e,o=e=>{let n=[];for(let s in e){let c=e[s];if(s===`$or`&&Array.isArray(c)){let e=c.map(e=>o(e)).filter(Boolean).map(e=>`(${e})`);e.length&&n.push(`(${e.join(` OR `)})`);continue}if(s===`$and`&&Array.isArray(c)){let e=c.map(e=>o(e)).filter(Boolean).map(e=>`(${e})`);e.length&&n.push(`(${e.join(` AND `)})`);continue}let l=t?`${i(t)}.${i(s)}`:i(s);if(c===null){n.push(`${l} IS NULL`);continue}if(typeof c==`object`&&c&&!Array.isArray(c)){let e=[];for(let t in c){let n=c[t];switch(t){case`$in`:n.length?(e.push(`${l} IN (${n.map(()=>`?`).join(`,`)})`),r.push(...n.map(a))):e.push(`1=0`);break;case`$nin`:n.length?(e.push(`${l} NOT IN (${n.map(()=>`?`).join(`,`)})`),r.push(...n.map(a))):e.push(`1=1`);break;case`$ne`:n===null?e.push(`${l} IS NOT NULL`):(e.push(`${l} <> ?`),r.push(a(n)));break;case`$gt`:e.push(`${l} > ?`),r.push(a(n));break;case`$gte`:e.push(`${l} >= ?`),r.push(a(n));break;case`$lt`:e.push(`${l} < ?`),r.push(a(n));break;case`$lte`:e.push(`${l} <= ?`),r.push(a(n));break;case`$between`:if(!Array.isArray(n)||n.length!==2)throw Error(`$between requires [min, max]`);e.push(`${l} BETWEEN ? AND ?`),r.push(a(n[0]),a(n[1]));break;case`$like`:e.push(`${l} LIKE ?`),r.push(`%${n}%`);break;case`$likeIn`:if(!n.length)e.push(`1=0`);else{let t=n.map(()=>`${l} LIKE ?`).join(` OR `);e.push(`(${t})`),r.push(...n.map(e=>`%${e}%`))}break;default:throw Error(`Unsupported operator: ${t}`)}}e.length&&n.push(`(${e.join(` AND `)})`)}else n.push(`${l} = ?`),r.push(a(c))}return n.join(` AND `)};return{sql:o(n)||`1=1`,params:r}}async count(e={}){let t=`SELECT COUNT(*) AS total FROM \`${this.table}\``,n=[],r=this.buildWhere(e);r.sql&&(t+=` WHERE ${r.sql}`,n=r.params);let[i]=await this.query(t,n);return i?.[0]?.total??0}async insert(e,t){let n=Object.keys(e),r=Object.values(e),i=Array(n.length).fill(`?`).join(`, `),a=`INSERT INTO \`${this.table}\` (\`${n.join("`, `")}\`) VALUES (${i})`,[o]=await this.query(a,r,t);return o.insertId}async update(e,t,n){let r=Object.keys(e),i=Object.keys(t);if(r.length===0)return 0;if(i.length===0)throw Error(`Điều kiện lọc (filter) không được rỗng.`);let a=Object.values(e),o=r.map(e=>`\`${e}\`=?`).join(`, `),s=Object.values(t),c=i.map(e=>`\`${e}\`=?`).join(` AND `),l=[...a,...s],u=`UPDATE \`${this.table}\` SET ${o} WHERE ${c}`,[d]=await this.query(u,l,n);return d.affectedRows}async updateAdv(e={},t={},n){try{if(!Object.keys(e).length)return;let r=this.buildWhere(t),i=[],a=[];for(let[t,n]of Object.entries(e))i.push(`\`${t}\` = ?`),a.push(n);let o=`UPDATE \`${this.table}\` SET ${i.join(`, `)}`;r.sql&&(o+=` WHERE ${r.sql}`,a.push(...r.params));let[s]=await this.query(o,a,n);return s.affectedRows}catch(e){throw console.log(e.message),Error(`invalid_update_is_deleted`)}}async save(e,t){try{if(e.id){let n={...e};if(delete n.id,Object.keys(n).length===0)return e;let r={id:e.id};return await this.update(n,r,t),e}{let n=e;return e.id=await this.insert(n,t),e}}catch(e){return console.log(e.message),this.errors.push(e.message),!1}}async vdSave(e,t=!1,n){e=await this.modifiedSync(e),t&&await this.validate(e,t);let r=await this.save(e,n);if(!r){let e=this.getErrors().join(`, `);throw Error(e)}return r}async updateMany(e,t,n){if(e.length)try{let r=Object.keys(e[0]).filter(e=>e!==t),i=e.map(e=>e[t]),a=r.map(n=>`\`${n}\` = CASE \`${t}\` ${e.map(e=>`WHEN ? THEN ?`).join(` `)} ELSE \`${n}\` END`).join(`, `),o=[];r.forEach(n=>{e.forEach(e=>{o.push(e[t]),o.push(e[n])})}),o.push(...i);let s=`UPDATE \`${this.table}\` SET ${a} WHERE \`${t}\` IN (${i.map(()=>`?`).join(`,`)});`;await this.query(s,o,n)}catch(e){throw console.log(e.message),Error(`invalid_update_many`)}}async updateAndCreateMany(e,t,n){if(e.length)try{let r=Object.keys(e[0]),i=r.map(e=>`\`${e}\``),a=e.map(()=>`(${r.map(()=>`?`).join(`,`)})`).join(`,`),o=e.flatMap(e=>r.map(t=>e[t])),s=r.filter(e=>e!==t).map(e=>`\`${e}\` = VALUES(\`${e}\`)`).join(`, `),c=`INSERT INTO \`${this.table}\` (${i.join(`,`)}) VALUES ${a}`;s&&(c+=` ON DUPLICATE KEY UPDATE ${s}`),c+=`;`,await this.query(c,o,n)}catch(e){throw console.log(e.message),Error(`invalid_update_and_create`)}}async find(e={},t=!1,n=!1,r=!1,i=0){let a=`SELECT * FROM \`${this.table}\``,o=[];if(n){let e=Array.isArray(n)?n:[n],t=[];for(let n of e){let e=n.type?n.type.toUpperCase():`LEFT JOIN`;a+=` ${e} \`${n.table}\` ON ${n.on}`,n.fields&&n.fields.length&&t.push(n.fields.map(e=>`\`${n.table}\`.${e} AS ${n.table}_${e}`).join(`, `))}t.length&&(a=a.replace(`SELECT *`,`SELECT \`${this.table}\`.*, ${t.join(`, `)}`))}let s=this.buildWhere(e);if(s.sql&&(a+=` WHERE ${s.sql}`,o=o.concat(s.params)),r&&(Array.isArray(r)&&(r=r.join(`, `)),a=a.replace(`SELECT *`,`SELECT ${r}`)),t){let e=Object.entries(t).map(([e,t])=>`${e} ${t.toUpperCase()}`);e.length&&(a+=` ORDER BY ${e.join(`, `)}`)}i&&(a+=` LIMIT ?`,o.push(i));let[c]=await this.query(a,o);return c}async findOne(e={},t=!1,n=!1,r=!1){let i=await this.find(e,t,n,r,1);return i&&i.length?i[0]:null}async findById(e,t={}){return t.id=e,await this.findOne(t)}async deleteOne(e,t){return await this.deleteMany(e,1,t)}async deleteById(e,t){let n={};return n.id=e,await this.deleteOne(n,t)}async deleteMany(e,t=0,n){let r=this.buildWhere(e);if(!r.sql)throw Error(`deleteMany requires a filter condition`);let i=`DELETE FROM \`${this.table}\` WHERE ${r.sql}`;t&&(i+=` LIMIT `+t);let[a]=await this.query(i,r.params,n);return!!a.affectedRows&&a.affectedRows>0}async isField(e,t=this.table){return(await this.query(`SHOW COLUMNS FROM \`${t}\` LIKE '${e}'`)).length>0}async isFields(e=[]){if(!e.length)return[];let t=(await this.query(`SHOW COLUMNS FROM \`${this.table}\``)).map(e=>e.Field);if(e.filter(e=>!t.includes(e)).length>0)throw Error(`invalid_fields`);return e}async getMapName(e,t,n,r,i=`name`){if(t&&t!==r?.[e]){let r=await n.findOne({[e]:t});return r&&r[i]?r[i]:``}return``}async findWithPagination(e={},t=!1,n=!1,r=!1,i=1,a=10){i=Math.max(1,i);let o=(i-1)*a,s=`FROM \`${this.table}\``,c=[],l=`SELECT \`${this.table}\`.*`;if(n){let e=Array.isArray(n)?n:[n],t=[];for(let n of e){let e=n.type?n.type.toUpperCase():`LEFT JOIN`;s+=` ${e} \`${n.table}\` ON ${n.on}`,n.fields?.length&&t.push(n.fields.map(e=>`\`${n.table}\`.${e} AS ${n.table}_${e}`).join(`, `))}t.length&&(l=`SELECT \`${this.table}\`.*, ${t.join(`, `)}`)}let u=this.buildWhere(e);u.sql&&(s+=` WHERE ${u.sql}`,c=c.concat(u.params)),r&&(Array.isArray(r)&&(r=r.join(`, `)),l=`SELECT ${r}`);let d=`SELECT COUNT(*) AS total ${s}`,[f]=await this.query(d,c),p=f?.[0]?.total||0,m=`${l} ${s}`;if(t){let e=Object.entries(t).map(([e,t])=>`${e} ${t.toUpperCase()}`);e.length&&(m+=` ORDER BY ${e.join(`, `)}`)}m+=` LIMIT ? OFFSET ?`;let h=c.concat([a,o]),[g]=await this.query(m,h);return{page:i,length:a,pageTotal:Math.ceil(p/a),recordTotal:p,items:g||[]}}};async function f(t){return await e.hash(t)}async function p(t,n){return await e.verify(n,t)}function m(e,t=!1){let n=``,r=`0123456789`;r=t?`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`+r:r;for(let t=0;t<e;t++)n+=r.charAt(Math.floor(Math.random()*r.length));return n}function h(e){return e?String(e).normalize(`NFD`).replace(/[\u0300-\u036f]/g,``).replace(/đ/g,`d`).replace(/Đ/g,`D`).toLowerCase():``}function g(e){return e.toString().toLowerCase().normalize(`NFD`).replace(/[\u0300-\u036f]/g,``).replace(/[^a-z0-9\s-]/g,``).trim().replace(/\s+/g,`-`).replace(/-+/g,`-`)}async function _(e,t={},n=`POST`,r=`application/json`,i={}){let a={method:n,headers:{"Content-Type":r,...i}};return n===`POST`&&(a.body=JSON.stringify(t)),await(await fetch(e,a)).json()}function v(e=``){return e?e.toLowerCase().replace(/[^a-z0-9\s]/g,``).split(/\s+/).filter((e,t,n)=>e.length>2&&n.indexOf(e)===t).join(`, `):``}function y(e){let t=e instanceof Date?e:new Date(e);if(isNaN(t.getTime()))throw Error(`Ngày không hợp lệ: `+e);return t}function b(e,t=`YYYY-MM-DD HH:mm:ss`){if(!e)return``;let n=typeof e==`string`?new Date(e):e;if(isNaN(n.getTime()))return``;let r=e=>e.toString().padStart(2,`0`),i={YYYY:n.getFullYear().toString(),MM:r(n.getMonth()+1),DD:r(n.getDate()),HH:r(n.getHours()),mm:r(n.getMinutes()),ss:r(n.getSeconds())};return t.replace(/YYYY|MM|DD|HH|mm|ss/g,e=>i[e])}function x(e,t){return{start:new Date(e,t-1,1),end:new Date(e,t,0,23,59,59,999)}}function S(e,t){let n=new Date(e,t,0).getDate();return Array.from({length:n},(e,t)=>t+1)}function C(){return t(new Date,`yyyy-MM-dd HH:mm:ss`,{timeZone:`Asia/Ho_Chi_Minh`})}function w(e,t,n=``){return e?.[t]===void 0?n:e[t]}function T(e,t){return e?.reduce((e,n)=>e+(n[t]??0),0)||0}function E(e){let t=[{value:1e3,numeral:`M`},{value:900,numeral:`CM`},{value:500,numeral:`D`},{value:400,numeral:`CD`},{value:100,numeral:`C`},{value:90,numeral:`XC`},{value:50,numeral:`L`},{value:40,numeral:`XL`},{value:10,numeral:`X`},{value:9,numeral:`IX`},{value:5,numeral:`V`},{value:4,numeral:`IV`},{value:1,numeral:`I`}],n=``;for(let{value:r,numeral:i}of t)for(;e>=r;)n+=i,e-=r;return n}function D(e,t=!1){let n=[{id:1,name:`Hoạt động`,color:`success`},{id:0,name:`Khóa`,color:`danger`}];if(e===!1)return n;let r=n.find(t=>t.id===e);return r?t?`<span class="badge badge-sm bg-gradient-${r.color}">${r.name}</span>`:r.name:``}function O(e,t=!1){let n=[{id:1,name:`Xuất bản`,color:`success`},{id:0,name:`Nháp`,color:`danger`}];if(e===!1)return n;let r=n.find(t=>t.id===e);return r?t?`<span class="badge badge-sm bg-gradient-${r.color}">${r.name}</span>`:r.name:``}function k(e,t=!1){let n=[{id:1,name:`Có`,color:`success`},{id:0,name:`Không`,color:`secondary`}];if(e===!1)return n;let r=n.find(t=>t.id===e);return r?t?`<span class="badge badge-sm bg-gradient-${r.color}">${r.name}</span>`:r.name:``}function A(e,t){let n=new Set(e),r=new Set(t);return[...e.filter(e=>!r.has(e)),...t.filter(e=>!n.has(e))]}function j(e,t){let n=new Set(t);return e.every(e=>n.has(e))}function M(e,t){if(!Array.isArray(e)||!Array.isArray(t))return[];let n=new Set(t);return e.filter(e=>!n.has(e))}function N(e,t){if(!Array.isArray(e)||e.length===0)return[];if(!t)return e;let n=Array(e.length);for(let r=0;r<e.length;r++){let i=e[r];n[r]=i&&typeof i==`object`?i[t]:void 0}return n}function P(e,t,n){return e.reduce((e,r)=>{let i=!0;if(n&&(i=Object.entries(n).every(([e,t])=>r[e]===t)),i){let n=r[t];return e+(typeof n==`number`?n:0)}return e},0)}function F(e){let t=String(e).replace(/[^\d.]/g,``);return t?new n.Decimal(t):new n.Decimal(0)}function I(e,t={}){if(Array.isArray(e)||typeof e==`object`&&e)return e;if(typeof e==`number`)return t;if(typeof e==`string`)try{let n=JSON.parse(e);return Array.isArray(n)||typeof n==`object`&&n?n:t}catch(e){return console.log(e),t}return t}function L(e=!1){let t=[{id:1,name:`Tiếng Việt`}];return e===!1?t:t.filter(t=>e.includes(t.id))}const R=new Set([`POST`,`PUT`,`PATCH`,`DELETE`]);function z(e){return R.has(String(e).toUpperCase())}function B(e){let t=e?.headers?.[`x-csrf-token`]||e?.headers?.[`X-CSRF-Token`],n=e?.body?.csrfToken||e?.body?.csrf_token;return t||n||``}function V(e,t,n={}){if(!z(e?.method||`GET`))return{allowed:!0,reason:null};let r=t?.csrfToken||n.expectedToken||``,i=B(e);return!r||!i||r!==i?{allowed:!1,reason:`missing_csrf_token`}:{allowed:!0,reason:null}}var H=class e{files;uploadType=`static`;constructor(e,t=`static`){this.files=e,this.uploadType=t}async copyFiles(t){if(t.length){let n=e.getUploadDir(this.uploadType);for await(let e of t){let t=i.join(n,e.src);if(r.existsSync(t)){let a=i.join(n,e.dist);await r.promises.copyFile(t,a)}}}}async uploadFile(e=`filedname`,t=``){try{let n=this.files[e];return!n||!n[0]?!1:await this.upload(n[0],t)}catch(e){throw Error(e)}}async uploadFiles(e=`filedname`,t=``){try{let n=[],r=this.files[e];if(!r||!r.length)return[];for await(let e of r){let r=await this.upload(e,t);r&&n.push(r)}return n}catch(e){throw Error(e)}}async upload(t,n=``){try{let a=Date.now()+`___`+t.filename,o=e.getUploadDir(this.uploadType),s=n?i.join(o,n):o;r.existsSync(s)||r.mkdirSync(s,{recursive:!0});let c=i.join(s,a);return r.existsSync(t.path)?(await r.promises.rename(t.path,c),n?i.posix.join(n,a):a):!1}catch(e){throw Error(e)}}async checkFile(t=`file`,n=`OfficeFile`){try{let r=this.files[t];if(!r)return;for await(let t of r){let r=`check`+n;if(!(r in e))throw Error(`Method does not exist: `+r);e[r](t.filename,t.mimetype);return}return}catch(e){throw Error(e)}}static removeFiles=(e,t=`static`)=>{if(e===!1||!Array.isArray(e))return!1;for(let n of e)this.removeFile(n,t);return!0};static removeFile=(t,n=`static`)=>{if(!t)return!1;let a=i.join(e.getUploadDir(n),t);return r.existsSync(a)&&r.unlinkSync(a),!0};static renameFile=e=>{let t=i.extname(e);return`${i.basename(e,t)}_${Date.now()}${t}`};static getUploadDir(e=`static`){return e==`static`?i.join(i.resolve(),`public`,`static`):e==`public`?i.join(i.resolve(),`public`):e==`uploads`?i.join(i.resolve(),`uploads`):``}static checkDefFile(e,t,n=!0){if(!/(.*)\.(png|jpeg|jpg|ico|pdf|xls|xlsx|doc|docx|ppt|pptx)$/i.test(e)){if(n)throw Error(`Định dạng tập tin ${e} không được phép`);return!1}if(![`image/x-icon`,`image/jpeg`,`image/png`,`image/jpg`,`application/pdf`,`application/msword`,`application/vnd.ms-excel`,`application/vnd.ms-powerpoint`,`application/vnd.openxmlformats-officedocument.presentationml.presentation`,`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,`application/vnd.openxmlformats-officedocument.wordprocessingml.document`].includes(t)){if(n)throw Error(`Định dạng tập tin ${e} không được phép`);return!1}return!0}static checkOfficeFile(e,t){try{if(!/(.*)\.(pdf|xls|xlsx|doc|docx|ppt|pptx)$/i.test(e)||![`application/pdf`,`application/msword`,`application/vnd.ms-excel`,`application/vnd.ms-powerpoint`,`application/vnd.openxmlformats-officedocument.presentationml.presentation`,`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,`application/vnd.openxmlformats-officedocument.wordprocessingml.document`].includes(t))throw Error(`Tập tin ${e} không đúng định dạng (.pdf,.xls,.xlsx,.doc,.docx,.ppt,.pptx)`)}catch(e){throw Error(e)}}static checkImgFile(e,t){try{if(!/(.*)\.(png|jpeg|jpg|ico)$/i.test(e)||![`image/jpeg`,`image/png`,`image/jpg`,`image/x-icon`].includes(t))throw Error(`Ảnh ${e} không đúng định dạng (.png,.jpeg,.jpg,.ico)`)}catch(e){throw Error(e)}}static checkVideoFile(e,t){try{if(!/(.*)\.(mp4|mkv|webm|mov|avi)$/i.test(e)||![`video/mp4`,`video/mkv`,`video/webm`,`video/mov`,`video/avi`].includes(t))throw Error(`Video ${e} không đúng định dạng (.mp4,.mkv,.webm,.mov,.avi)`)}catch(e){throw Error(e)}}},U=class e{req;body={};files={};constructor(e){this.req=e}start=async()=>{if(this.req.body)this.body=this.req.body;else if(this.req.isMultipart()){let e=i.join(i.resolve(),`uploads`,`tmb`);r.existsSync(e)||r.mkdirSync(e,{recursive:!0});for await(let t of this.req.parts())if(t.type===`field`)this.body[t.fieldname]===void 0?this.body[t.fieldname]=t.value:Array.isArray(this.body[t.fieldname])?this.body[t.fieldname].push(t.value):this.body[t.fieldname]=[this.body[t.fieldname],t.value];else if(H.checkDefFile(t.filename,t.mimetype,!1)){let n=i.extname(t.filename),a=g(i.basename(t.filename,n)),o=a+n,s=i.join(e,o);try{await c(t.file,r.createWriteStream(s))}catch(e){e.code===`ABORT_ERR`?console.error(`Người dùng hủy upload.`):e.code===`ERR_STREAM_PREMATURE_CLOSE`?console.error(`Stream đóng sớm bất thường.`):console.error(`Lỗi upload:`,e.message)}this.files[t.fieldname]===void 0&&(this.files[t.fieldname]=[]),this.files[t.fieldname].push({filename:o,basename:a,path:s,mimetype:t.mimetype,ext:n})}}};end=async()=>{let e=Object.values(this.files);for await(let t of e)for await(let e of t)r.existsSync(e.path)&&r.unlinkSync(e.path)};isPost(){return this.req.method===`POST`}isGet(){return this.req.method===`GET`}isAjax(){return this.req.headers[`x-requested-with`]===`XMLHttpRequest`}getPost(t=``,n=``,r=`stripTags`){if(t===``)return this.body;if(!this.hasPost(t))return n;let i=this.body[t];return i?.value!==void 0&&(i=i.value),e.filterSafeData(i,r,n)}getLangPost(t,n=``,r=`stripTags`){if(!t)return n;let i=t.match(/^([^{]+)\{([^}]+)\}$/);if(!i)return n;let a=i[1],o=i[2],s=this.body?.[a];typeof s==`string`&&(s=I(s));let c=s?.[o];return e.filterSafeData(c,r,n)}getParam(t,n=null,r=`stripTags`){let i=this.req.params;return!i||!i[t]?n:e.filterSafeData(i[t],r)}get(t=``,n=``,r=`stripTags`){let i=this.req.query;return t===``?i:this.has(t)?e.filterSafeData(i[t],r):n}has(e){let t=this.req.query;return Array.isArray(e)?e.every(e=>t[e]!==void 0):t[e]!==void 0}hasPost(e){return Array.isArray(e)?e.every(e=>this.body[e]!==void 0):this.body[e]!==void 0}getHeader(e){return this.req.headers[e.toLowerCase()]?.toString()||``}static filterSafeData(t,n=`stripTags`,r=``){if(typeof t==`number`)return t;if(n===`ids`)return Array.isArray(t)?e.filterIds(t):e.filterIds([t]);if(n===`stringIds`)return Array.isArray(t)?e.filterStringIds(t):e.filterStringIds([t]);if(n===`strings`)return Array.isArray(t)?e.filterStrings(t):e.filterStrings([t]);if(n===`dQuoteIds`)return Array.isArray(t)?e.filterDQuoteIds(t):e.filterDQuoteIds([t]);if(n===`int`||n===`number`){let e=Number(t);if(e>2**53-1)throw Error(`invalid_max_safe_integer`);if(e<-(2**53-1))throw Error(`invalid_min_safe_integer`);return Number.isNaN(e)?r:n===`int`?Math.trunc(e):e}if(n===`decimal`)return F(t);if(typeof t==`string`){if(n===`stripTags`)return t.replace(/<[^>]*>?/gm,``).trim();if(n===`html`)return e.filterHtml(t);if(n===`date`||n===`datetime`){let e=s(t);return o(e)?a(e,n===`date`?`yyyy-MM-dd`:`yyyy-MM-dd HH:mm:ss`):null}}return n===`raw`?e.parseRawInput(t):typeof t===n?t:``}static parseRawInput(e){if(typeof e!=`string`)return e;try{return JSON.parse(e)}catch{return e}}static filterHtml(e){return typeof e==`string`?e.replace(/<(script|style|meta)[^>]*>[\s\S]*?<\/\1>|<(script|style|meta)[^>]*\/?>/gi,``):``}static filterIds(e){if(typeof e==`string`){let t=I(e,void 0);if(Array.isArray(t))e=t;else if(!Number.isNaN(Number(e)))e=[Number(e)];else return[]}return Array.isArray(e)?[...new Set(e.map(e=>Number(e)).filter(e=>!Number.isNaN(e)))]:[]}static filterStringIds(e){return typeof e==`string`&&(e=I(e,[])),Array.isArray(e)?this.filterIds(e).map(e=>e.toString()):[]}static filterStrings(e){return[...new Set(e.map(e=>this.filterSafeData(e,`stripTags`)))]}static filterDQuoteIds(e){return this.filterIds(e).map(e=>`%"${e}"%`)}},W=class{static createToken(e,t){let{JWT_KEY:n,JWT_AUD:r,JWT_ISS:i}=process.env,a=Number(process.env.JWT_TIMEOUT||3600),o=t.ip,s=t.headers[`user-agent`],c=Math.floor(Date.now()/1e3),u={iss:i,aud:r,sub:e.id,name:e.fullname,ip:o,us:s,iat:c,exp:c+a,expired:(c+a)*1e3};return l.sign(u,n||``,{algorithm:`HS256`})}static verifyToken(e){let{JWT_KEY:t}=process.env,n=e.cookies?.accessToken;if(!n)throw Error(`invalid_cookie_access_token`);let r=l.verify(n,t||``,{ignoreExpiration:!0});if(!r?.sub)throw Error(`invalid_userId_access_token`);let i=e.headers[`user-agent`]||`unkowwn`;if(r.us!==i)throw Error(`invalid_us_access_token`);if(r.ip&&r.ip!==e.ip)throw Error(`invalid_ip_access_token`);return r.accessToken=n,r}static checkSecretKey(e){if(!process.env.JWT_SECRET_KEY)throw Error(`missing_jwt_secret_key`);if(e!=process.env.JWT_SECRET_KEY)throw Error(`invalid_secret_key`)}};let G=null;function K(e,t){return Object.fromEntries(Object.entries(e).map(([e,n])=>[e,new n(t)]))}function q(e,t){return G||=K(e,t),G}function J(){if(!G)throw Error(`Models chưa init`);return G}var Y=class{pool;constructor(e){this.pool=e}async get(e,t){try{let[n]=await this.pool.query(`SELECT data FROM sessions WHERE sid = ?`,[e]);if(!n.length)return t(null,null);t(null,JSON.parse(n[0].data))}catch(e){t(e)}}async set(e,t,n){try{let r=JSON.stringify(t);await this.pool.query(`INSERT INTO sessions (sid, data, expires) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 14 DAY)) ON DUPLICATE KEY UPDATE data = VALUES(data), expires = VALUES(expires)`,[e,r]),n(null)}catch(e){n(e)}}async destroy(e,t){try{await this.pool.query(`DELETE FROM sessions WHERE sid = ?`,[e]),t(null)}catch(e){t(e)}}},X=class{routes=[];static getMethod(e){return e.method?e.method:`get`}add(e,t=[]){let n=[];t.forEach(t=>{t.link=e+t.link,n.push(t)}),this.routes=this.routes.length?[...this.routes,...n]:n}addGS(e,t,n){let r=[{link:e+`/getList`,module:n,controller:t,action:`index`},{link:e+`/detail/:code([0-9]+)`,module:n,controller:t,action:`detail`},{link:e+`/create`,module:n,controller:t,action:`update`,method:`post`},{link:e+`/edit/:code([0-9]+)`,module:n,controller:t,action:`update`,method:`post`},{link:e+`/copy`,module:n,controller:t,action:`copy`,method:`post`},{link:e+`/import`,module:n,controller:t,action:`import`,method:`post`},{link:e+`/export`,module:n,controller:t,action:`export`},{link:e+`/delete/:code([0-9]+)`,module:n,controller:t,action:`deleteOne`},{link:e+`/delete`,module:n,controller:t,action:`delete`,method:`post`}];this.routes=this.routes.length?[...this.routes,...r]:r}getRouter(){return this.routes}};const Z=new Set([`password`,`token`,`accessToken`,`refreshToken`,`secret`,`apiKey`,`authorization`]);function Q(e,t=``){return e==null||typeof e==`string`||typeof e==`number`||typeof e==`boolean`?e:Array.isArray(e)?e.map(e=>Q(e,t)):typeof e==`object`?Object.entries(e).reduce((e,[t,n])=>{let r=t.toLowerCase();return Z.has(r)||r.includes(`password`)||r.includes(`token`)||r.includes(`secret`)||(e[t]=Q(n,t)),e},{}):e}function $(e,t,n=[]){let r=new Set([...n,`updatedAt`,`createdAt`,`__v`]),i={},a={};return Object.keys(e??{}).forEach(n=>{if(r.has(n))return;let o=t?.[n],s=e?.[n];o!==s&&(i[n]={old:o,new:s}),a[n]=s}),{difference:Q(i),item:Q(a)}}function ee(e,t){return e.length>0?e:!t}export{d as BaseModel,U as FastRequest,H as FileUpload,W as JWTApp,Y as MySQLSessionStore,X as Route,u as Validation,$ as buildAuditLogData,_ as callFetchApi,p as checkPassword,K as createModels,F as decimalNumber,V as ensureCsrfProtection,b as formatDate,v as generateKeywords,D as getActiveStatus,N as getArrColumn,M as getArrOnlyInFirst,ee as getCorsOriginPolicy,B as getCsrfTokenFromRequest,S as getDaysInMonth,A as getDiffArr,L as getLangs,J as getModels,x as getMonthRange,O as getPublishStatus,k as getTrueFlaseStatus,w as getValue,q as initModels,z as isStateChangingMethod,j as isSubArr,f as makePassword,y as parseDate,m as randomText,h as removeVietnameseTones,Q as sanitizeAuditPayload,g as slugify,I as stringToJson,T as sumArrCol,P as sumArrColumn,C as toMySQLDateNowVN,E as toRoman};
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@fastify-core/base",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "scripts/postinstall.cjs",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "tsc --noEmit && oxlint .",
20
+ "build": "tsdown",
21
+ "pack": "npm run build && npm pack",
22
+ "prepublishOnly": "npm run build",
23
+ "postinstall": "node scripts/postinstall.cjs"
24
+ },
25
+ "keywords": [],
26
+ "author": "MIT",
27
+ "license": "ISC",
28
+ "peerDependencies": {
29
+ "@fastify/mysql": "^5.0.2",
30
+ "@fastify/session": "^11.1.2",
31
+ "argon2": "^0.45.0",
32
+ "date-fns": "^4.4.0",
33
+ "date-fns-tz": "^3.2.0",
34
+ "decimal.js": "^10.6.0",
35
+ "dotenv": "^17.4.2",
36
+ "fastify": "^5.10.0",
37
+ "jsonwebtoken": "^9.0.3",
38
+ "mysql2": "^3.23.3"
39
+ },
40
+ "devDependencies": {
41
+ "@fastify/mysql": "^5.0.2",
42
+ "@fastify/session": "^11.1.2",
43
+ "@types/jsonwebtoken": "^9.0.10",
44
+ "@types/node": "^26.2.0",
45
+ "argon2": "^0.45.0",
46
+ "date-fns": "^4.4.0",
47
+ "date-fns-tz": "^3.2.0",
48
+ "decimal.js": "^10.6.0",
49
+ "dotenv": "^17.4.2",
50
+ "fastify": "^5.10.0",
51
+ "jsonwebtoken": "^9.0.3",
52
+ "mysql2": "^3.23.3",
53
+ "oxlint": "^1.74.0",
54
+ "ts-node": "^10.9.2",
55
+ "tsdown": "^0.22.14",
56
+ "typescript": "^6.0.3"
57
+ },
58
+ "engines": {
59
+ "node": ">=24"
60
+ }
61
+ }
@@ -0,0 +1,80 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+
5
+ try {
6
+ // 1. Xác định thư mục gốc dự án
7
+ const projectRoot = process.env.INIT_CWD || (() => {
8
+ let currentDir = __dirname;
9
+ while (currentDir !== path.parse(currentDir).root) {
10
+ if (path.basename(currentDir) === 'node_modules') {
11
+ return path.dirname(currentDir);
12
+ }
13
+ currentDir = path.dirname(currentDir);
14
+ }
15
+ return null;
16
+ })();
17
+
18
+ if (projectRoot) {
19
+ const projectPackageJsonPath = path.join(projectRoot, 'package.json');
20
+
21
+ // 🌟 THAY ĐỔI Ở ĐÂY: Nếu file package.json KHÔNG tồn tại, tự động tạo mới một file rỗng hợp lệ
22
+ if (!fs.existsSync(projectPackageJsonPath)) {
23
+ const defaultPkg = {
24
+ name: path.basename(projectRoot) || "my-admin-project",
25
+ version: "1.0.0",
26
+ private: true,
27
+ dependencies: {}
28
+ };
29
+ fs.writeFileSync(projectPackageJsonPath, JSON.stringify(defaultPkg, null, 2), 'utf-8');
30
+ }
31
+
32
+ // 2. Đọc peerDependencies của thư viện
33
+ const myPackageJsonPath = path.resolve(__dirname, '../package.json');
34
+ const myPackageJson = JSON.parse(fs.readFileSync(myPackageJsonPath, 'utf-8'));
35
+ const peerDeps = myPackageJson.peerDependencies || {};
36
+ const peerDepsStr = JSON.stringify(peerDeps);
37
+
38
+ // 3. Tách một tiến trình độc lập chạy bằng Node.js ngầm
39
+ const child = spawn('node', ['-e', `
40
+ setTimeout(() => {
41
+ const fs = require('fs');
42
+ try {
43
+ const targetPath = '${projectPackageJsonPath.replace(/\\/g, '\\\\')}';
44
+
45
+ // Đọc file (lúc này chắc chắn đã tồn tại vì ta đã tạo ở tiến trình cha)
46
+ let projectPkg = {};
47
+ if (fs.existsSync(targetPath)) {
48
+ projectPkg = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
49
+ }
50
+
51
+ if (!projectPkg.dependencies) projectPkg.dependencies = {};
52
+
53
+ const peerDeps = ${peerDepsStr};
54
+ let hasChanged = false;
55
+
56
+ Object.keys(peerDeps).forEach((dep) => {
57
+ const inDeps = projectPkg.dependencies && projectPkg.dependencies[dep];
58
+ const inDevDeps = projectPkg.devDependencies && projectPkg.devDependencies[dep];
59
+ if (!inDeps && !inDevDeps) {
60
+ projectPkg.dependencies[dep] = peerDeps[dep];
61
+ hasChanged = true;
62
+ }
63
+ });
64
+
65
+ if (hasChanged) {
66
+ fs.writeFileSync(targetPath, JSON.stringify(projectPkg, null, 2), 'utf-8');
67
+ console.log('\\n\\x1b[32m%s\\x1b[0m', '✅ [MaterialUI Admin] Đã tự động tạo và chèn các dependencies thành công!');
68
+ }
69
+ } catch(e) {}
70
+ }, 1500);
71
+ `], {
72
+ detached: true,
73
+ stdio: 'ignore'
74
+ });
75
+
76
+ child.unref();
77
+ }
78
+ } catch (error) {
79
+ // Bỏ qua lỗi dev
80
+ }