@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 +229 -0
- package/dist/index.d.mts +952 -0
- package/dist/index.mjs +1 -0
- package/package.json +61 -0
- package/scripts/postinstall.cjs +80 -0
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.
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|