@fastify-core/base
v1.1.2
Published
`@fastify-core/base` là thư viện backend cốt lõi dành cho các ứng dụng Fastify / Node.js, cung cấp hệ thống module toàn diện: tầng dữ liệu MySQL (BaseModel), quản lý container Model (Models), xử lý Request & Multipart Form-data (FastRequest), quản lý File
Readme
@fastify-core/base
@fastify-core/base là thư viện backend cốt lõi dành cho các ứng dụng Fastify / Node.js, cung cấp hệ thống module toàn diện: tầng dữ liệu MySQL (BaseModel), quản lý container Model (Models), xử lý Request & Multipart Form-data (FastRequest), quản lý File Upload (FileUpload), xác thực JWT (JWTApp), chống tấn công CSRF, bộ kiểm tra tính hợp lệ dữ liệu (Validation), định tuyến tập trung (Route), đa ngôn ngữ (I18n), tạo chữ ký HMAC (HMAC) và các tiện ích bảo mật, xử lý dữ liệu thông dụng (Utils & Common).
🚀 Tính năng nổi bật
- BaseModel: Lớp trừu tượng cho thao tác cơ sở dữ liệu MySQL (CRUD, Where Builder nâng cao hỗ trợ
$in,$like,$between,$or, phân trang, UPSERT, quan hệ hiển thị liên kếtmodifiedSync/getMapName). - Models Container: Quản lý Dependency Injection singleton cho toàn bộ các model trong hệ thống (
createModels,initModels,getModels). - FastRequest: Wrapper bao bọc FastifyRequest, hỗ trợ stream multipart/form-data an toàn, tự động lưu file tạm thời, trích xuất và lọc dữ liệu (sanitization) chống XSS/SQL Injection.
- FileUpload: Quản lý di chuyển, sao chép, xóa tập tin và kiểm tra định dạng file (ảnh, tài liệu Office, video).
- JWTApp: Tạo và xác thực JSON Web Token gắn liền với IP và User-Agent của Client để chống giả mạo phiên đăng nhập.
- CSRF Protection: Bảo vệ các request làm thay đổi trạng thái (POST, PUT, PATCH, DELETE) qua Header và Session; token được so sánh bằng
safeEqual()chống timing attack. - Validation: Bộ quy tắc kiểm tra dữ liệu chuỗi phong phú (
required,requiredId,minLen,rangeNum,email,phoneVn,password). Validator hoàn toàn thuần, không truy vấn database. Rule được tra cứu trong allowlist tường minh nên không thể bị gọi nhầm vào method củaObject.prototype, và có thể kiểm tra mẫu riêng của ứng dụng bằng ruleregex(xem mục 4.1). - Route: Định tuyến module hóa, tự động gắn tiền tố (prefix) và sinh nhanh 9 route CRUD chuẩn (
addGS). Mọi route làm thay đổi trạng thái đều dùngPOST. - I18n: Hệ thống đa ngôn ngữ hỗ trợ tiếng Việt và tiếng Anh mặc định, cho phép ghi đè và mở rộng từ ứng dụng.
- HMAC: Tạo chữ ký điện tử HMAC-SHA256 (hỗ trợ cả Node.js
cryptovà Web Crypto API). - ClientError & Utils: Quản lý lỗi phản hồi phía client và tiện ích sanitize audit log loại bỏ dữ liệu nhạy cảm.
- Crypto:
safeEqual()so sánh chuỗi bí mật theo thời gian cố định (constant-time) chống timing attack. - Common Utilities: Băm mật khẩu bằng Argon2, tạo slug, sinh keyword tìm kiếm, định dạng ngày tháng theo múi giờ Việt Nam, xử lý số Decimal.js độ chính xác cao, sinh chuỗi ngẫu nhiên bằng CSPRNG (
crypto.randomInt).
📋 Yêu cầu hệ thống
- Node.js:
>= 24 - Fastify:
>= 5.0.0 - MySQL: MySQL 8.x hoặc MariaDB (thư viện
mysql2) - Module System: ESM (ECMAScript Modules)
📦 Cài đặt
npm install @fastify-core/baseĐồng thời cài đặt các peerDependencies cần thiết nếu dự án chưa có:
npm install fastify mysql2 @fastify/session argon2 date-fns date-fns-tz decimal.js jsonwebtoken dotenv@fastify/multipart là peer dependency tuỳ chọn: chỉ cần khi dùng FastRequest với
multipart/form-data (upload file). Hãy đăng ký plugin này trước khi dùng FastRequest:
import multipart from '@fastify/multipart';
await app.register(multipart);📚 Hướng dẫn sử dụng chi tiết
1. BaseModel & Models (Tầng cơ sở dữ liệu)
Định nghĩa Model
import { BaseModel } from '@fastify-core/base';
import type { Pool } from 'mysql2/promise';
export interface UserItem {
id: number;
fullname: string;
email: string;
roleId: number;
status: number;
}
export class UserModel extends BaseModel<UserItem> {
table = 'users';
constructor(pool: Pool) {
super(pool);
}
}
fieldName()vẫn có sẵn để ứng dụng tự dùng (ví dụ dựng thông báo lỗi), nhưngValidationkhông còn gọi tới nó vì validator không phụ thuộc model.
Quản lý tập trung với initModels & getModels
import mysql from 'mysql2/promise';
import { initModels, getModels } from '@fastify-core/base';
import { UserModel } from './models/UserModel.js';
const pool = mysql.createPool({ host: 'localhost', user: 'root', database: 'app_db' });
// Khởi tạo một lần duy nhất khi ứng dụng khởi động
export const appModels = initModels({
user: UserModel
}, pool);
export type AppModels = typeof appModels;
// Lấy instance model ở bất kỳ controller nào
const { user } = getModels<AppModels>();
// 1. Tìm kiếm với bộ lọc phong phú ($in, $like, $between, $or)
const users = await user.find({
status: 1,
roleId: { $in: [1, 2] },
fullname: { $like: 'Nguyen' }
}, { id: 'DESC' }, false, ['id', 'fullname', 'email'], 10);
// Khi cần chọn cột của bảng JOIN, dùng tên cột hoặc column reference an toàn:
const usersWithRoles = await user.find(
{},
{ id: 'DESC' },
{
table: 'roles',
on: { left: 'users.roleId', right: 'roles.id', operator: '=' },
fields: ['name']
},
[{ column: 'users.id' }, { column: 'roles.name', alias: 'roleName' }]
);
// 2. Phân trang
const result = await user.findWithPagination(
{ status: 1 },
{ id: 'DESC' },
false,
false,
1, // page
20 // limit
);
// 3. Thêm mới hoặc cập nhật tự động (nếu item có id > 0 thực hiện UPDATE, ngược lại INSERT)
const savedUser = await user.save({ fullname: 'Nguyen Van A', email: '[email protected]' });
// 4. Xóa mềm với archive table (deleteMany tự động chuyển dữ liệu sang bảng <table>_rm)
const deleted = await user.deleteMany({ status: 0 }); // Xóa tất cả user status=0, lưu vào users_rm
// Table `users_rm` được tạo tự động với schema y hệt `users` + deletedAt, deletedBy⚠️ Lưu ý bảo mật SQL
- Tên bảng / tên cột trong
filter,data,orderđược thư viện escape tự động (nhân đôi backtick), an toàn ngay cả khi key đến từ request.selectchỉ nhận tên cột /table.column/table.*hoặc{ column, alias }; không nhận SQL thô, hàm, aggregate hay subquery.join.onchỉ nhận phép so sánh đơn giản giữa các cột (ví dụ"users.id = profiles.user_id"hoặc{ left, right, operator }); không nhận SQL thô.join.typedùng whitelist (LEFT JOIN,INNER JOIN...), sai sẽ néminvalid_join_type.order: key là tên cột (hỗ trợalias.col), value chỉ nhận'ASC'hoặc'DESC'— sai sẽ néminvalid_order_direction.group/having: hiện không có tham số tương ứng trongfindhoặcfindWithPagination; không tự nối SQL từ request vào các mệnh đề này. Nếu cần, hãy dùng truy vấn riêng với giá trị bind.- Các câu SQL cần biểu thức phức tạp nên viết bằng truy vấn riêng với giá trị bind, không nối dữ liệu request vào SQL.
2. FastRequest (Xử lý Request & Upload Tạm)
FastRequest xử lý cả JSON body và multipart/form-data. Khi upload qua multipart, file tự động được kiểm tra và lưu tạm vào uploads/tmb.
import Fastify from 'fastify';
import multipart from '@fastify/multipart';
import { FastRequest } from '@fastify-core/base';
const app = Fastify();
app.register(multipart);
app.post('/api/users', async (request, reply) => {
const req = new FastRequest(request);
await req.start(); // Bắt buộc gọi để parse body và stream file tạm
try {
// Đọc dữ liệu an toàn với các bộ lọc sanitization
const fullname = req.getPost('fullname', '', 'stripTags');
const age = req.getPost('age', 0, 'int');
const amount = req.getPost('amount', 0, 'decimal');
const birthday = req.getPost('birthday', null, 'date'); // Chuỗi ISO sang 'yyyy-MM-dd'
const userIds = req.getPost('userIds', [], 'ids'); // Mảng hoặc chuỗi JSON ID thành number[] duy nhất
const avatarFiles = req.files['avatar'];
return { fullname, age, userIds, filesCount: avatarFiles?.length };
} finally {
// Tự động xóa các file tạm trong uploads/tmb
await req.end();
}
});3. FileUpload (Quản lý & Lưu trữ File)
import { FileUpload } from '@fastify-core/base';
// req.files lấy từ FastRequest.files
const uploader = new FileUpload(req.files, 'static'); // Gốc: public/static
// Kiểm tra loại file (ImgFile | OfficeFile | VideoFile)
await uploader.checkFile('avatar', 'ImgFile');
// Lưu file vào public/static/avatars
const savedPath = await uploader.uploadFile('avatar', 'avatars');
// Kết quả trả về: "avatars/1711370000000___avatar.png"
// Xóa file không còn dùng
FileUpload.removeFile(savedPath, 'static');4. Validation (Xác thực dữ liệu)
import { Validation } from '@fastify-core/base';
// Validator thuần: khởi tạo không cần truyền model
const validator = new Validation();
const inputData = {
email: '[email protected]',
password: 'Password123@',
phone: '0912345678',
age: 25
};
const rules = {
email: 'required|email',
password: 'required|password',
phone: 'required|phoneVn',
age: 'required|integer|rangeNum(18,60)'
};
try {
await validator.runValidate(inputData, rules);
console.log('Dữ liệu hợp lệ!');
} catch (error) {
console.error('Lỗi xác thực:', error instanceof Error ? error.message : String(error));
}Đa ngôn ngữ cho thông báo lỗi
Tham số thứ 3 của runValidate() nhận mã ngôn ngữ (mặc định 'vi').
Thông báo được tra theo khóa validate_<tênRule> trong từ điển i18n:
import { Validation, initI18n } from '@fastify-core/base';
const validator = new Validation();
const rules = { email: 'required|email', age: 'required|rangeNum(18,60)' };
const input = { email: 'sai', age: 10 };
await validator.runValidate(input, rules, 'vi');
// throw: "Email không đúng định dạng, age phải nằm trong khoản từ 18 đến 60"
await validator.runValidate(input, rules, 'en');
// throw: "Email is not in the correct format, age must be between 18 and 60"
// Thêm ngôn ngữ mới hoặc ghi đè thông báo có sẵn
initI18n({
ja: {
validate_required: '{placeholder} は必須です',
validate_email: '{placeholder} の形式が不正です'
},
en: { validate_email: 'Please provide a valid {placeholder}' }
});- Ngôn ngữ chưa có trong từ điển, hoặc thiếu key, sẽ tự lùi về tiếng Việt — luôn nhận được thông báo đọc được.
BaseModel.validate(item, rules, lang)vàBaseModel.vdSave(item, rules, conn, lang)cũng nhậnlang.- Vì
langlà tham số tường minh nên an toàn khi Fastify xử lý nhiều request song song với ngôn ngữ khác nhau.
Kiểm tra trùng lặp: rule
uniqueđã được gỡ bỏ vì truy vấn database. Hãy tự kiểm tra ở tầng nghiệp vụ trước khi lưu:const existing = await userModel.findOne({ email: inputData.email }); if (existing) throw new Error('Email đã được sử dụng');Ràng buộc
UNIQUEở tầng database vẫn nên được giữ như lớp bảo vệ cuối cùng.
4.1 Rule regex — kiểm tra mẫu tuỳ chỉnh
Ngoài 19 rule có sẵn, rule regex cho phép kiểm tra một mẫu riêng của ứng dụng
(mã văn bản, biển mã số, mã đơn hàng…) ngay trong chuỗi rule:
import { Validation } from '@fastify-core/base';
const validator = new Validation();
const rules = {
so: 'required|regex(/^0\d{9}$/)',
maVanBan: 'regex(/^[A-Z]{2}\d{6}$/)'
};
await validator.runValidate({ so: '0900000000', maVanBan: 'AB123456' }, rules); // okCách viết pattern
| Cách viết | Ví dụ |
|---|---|
| Có dấu nháy bao | regex(/^[A-Z]{2}\d{6}$/) |
| Không có dấu nháy bao | regex(^[A-Z]+$) |
| Kèm cờ | regex(/^ab+c$/i) — cờ g, y bị bỏ qua vì gây trạng thái lastIndex |
| Có dấu phẩy | regex(/a{2,3}/), regex(/^[a,b]{3}$/) — giữ nguyên, không bị tách |
Lưu ý quan trọng
- Dấu
|phải escape thành\|vì|là dấu phân cách rule:- ✅
regex(/^(?:x\|y)=1$/) - ❌
regex(/^(?:x|y)=1$/)→ bị tách thành 2 rule và báoMethod doesnt exists - Cách thay thế: dùng lớp ký tự
regex(/^[xy]=1$/)
- ✅
- Không neo tự động —
regex(/abc/)cũng khớpxxabcxx. Hãy tự viết^...$. - Giá trị rỗng luôn hợp lệ, đúng như các rule khác → dùng kèm
required. - Tránh biểu thức có catastrophic backtracking (ReDoS) như
/(a+)+$/. - Pattern hỏng hoặc thiếu sẽ bị coi là không hợp lệ (không ném lỗi).
- Thông báo lỗi lấy từ khoá i18n
validate_regex:"{placeholder} không đúng định dạng yêu cầu".
5. JWT & Session (JWTApp & MySQLSessionStore)
import { JWTApp, MySQLSessionStore } from '@fastify-core/base';
// 1. Tạo JWT Token kèm IP và User-Agent bảo mật chống giả mạo
const token = JWTApp.createToken({ id: 1, fullname: 'Administrator' }, request);
// 2. Xác thực JWT Token từ Header hoặc Cookie
const userPayload = JWTApp.verifyToken(request); // Header Authorization: Bearer <token>
// const userPayload = JWTApp.verifyToken(request, true); // Cookie: accessToken
// 3. MySQL Session Store dùng cho @fastify/session
const sessionStore = new MySQLSessionStore(pool);6. CSRF Protection
import { ensureCsrfProtection, ClientError } from '@fastify-core/base';
app.addHook('preHandler', async (request, reply) => {
const session = (request as { session?: { csrfToken?: string } }).session;
const check = ensureCsrfProtection(request, session);
if (!check.allowed) {
throw new ClientError('Mã CSRF token không hợp lệ', 403, check.reason || 'csrf_forbidden');
}
});7. Định tuyến tập trung (Route)
import { Route } from '@fastify-core/base';
import { ProductController } from './controllers/ProductController.js';
const route = new Route();
// Nhóm route tùy biến với prefix
route.add('/admin/products', [
{ link: '/status', module: 'product', controller: ProductController, action: 'updateStatus', method: 'post' }
]);
// Tự động sinh nhanh bộ 9 route CRUD chuẩn:
// GET /getList, GET /detail/:code, POST /create, POST /edit/:code, POST /copy,
// POST /import, GET /export, POST /delete/:code, POST /delete
route.addGS('/admin/products', ProductController, 'product');8. Đa ngôn ngữ (I18n)
import { t, initI18n } from '@fastify-core/base';
initI18n({
vi: { welcome: 'Chào mừng bạn!' },
en: { welcome: 'Welcome!' }
});
console.log(t('invalid_403', 'vi')); // 'Truy cập không được phép'
console.log(t('welcome', 'en')); // 'Welcome!'9. HMAC Signature
import { createSignature, createHmac } from '@fastify-core/base';
// Ký bằng Node.js crypto (dùng biến môi trường HMAC_SECRET)
const signatureNode = createSignature({ orderId: 100, amount: 250000 });
// Ký bằng Web Crypto API (Browser / Edge runtime)
const signatureWeb = await createHmac({ orderId: 100, amount: 250000 }, 'secret_key');10. Tiện ích thường dùng (Common & Utils)
import {
makePassword,
checkPassword,
slugify,
randomText,
removeVietnameseTones,
generateKeywords,
toMySQLDateNowVN,
formatDate,
sumArrColumn,
decimalNumber,
sanitizeAuditPayload,
buildAuditLogData
} from '@fastify-core/base';
// Mật khẩu Argon2
const hashed = await makePassword('mySecretPass');
const isOk = await checkPassword('mySecretPass', hashed);
// Chuỗi & Ký tự
const slug = slugify('Khoá học Lập trình Web'); // 'khoa-hoc-lap-trinh-web'
const code = randomText(6); // Chuỗi số ngẫu nhiên 6 chữ số
// Thời gian theo timezone Việt Nam
const nowVN = toMySQLDateNowVN(); // '2026-03-25 15:30:00'
// Audit log an toàn (tự động loại bỏ password, token, secret, api key...)
const audit = buildAuditLogData(newData, oldData, ['lastActive']);
// Chuỗi ngẫu nhiên CSPRNG (dùng cho OTP, CSRF token, session id...)
randomText(6); // '428193' — chỉ chữ số
randomText(32, true); // 32 ký tự chữ + số
// Escape HTML — biến mọi thẻ thành text, không thể bypass XSS
escapeHtml('<img src=x onerror=alert(1)>');
// => '<img src=x onerror=alert(1)>'
// So sánh chuỗi bí mật chống timing attack
import { safeEqual } from '@fastify-core/base';
if (safeEqual(req.headers['x-api-key'] ?? '', process.env.API_KEY ?? '')) {
// khớp
}🔐 Cấu hình biến môi trường (.env)
| Biến | Ý nghĩa | Mặc định |
|---|---|---|
| JWT_KEY | Khóa bí mật ký và giải mã JWT token | Bắt buộc |
| JWT_SECRET_KEY | Khóa bảo mật đối chiếu cho JWTApp.checkSecretKey | Bắt buộc nếu dùng |
| JWT_ISS | Issuer của JWT token | Tuỳ chọn |
| JWT_AUD | Audience của JWT token | Tuỳ chọn |
| JWT_TIMEOUT | Thời gian hết hạn của JWT token (giây) | 3600 |
| HMAC_SECRET | Khóa bí mật dùng cho hàm createSignature | Bắt buộc nếu dùng |
🧪 Testing
Thư viện đi kèm bộ test trong thư mục test/ gồm unit test và e2e test, chạy bằng test runner built-in của Node.js (node --test) nên không cần thêm dependency.
npm run test:unit # chỉ unit test
npm run test:e2e # chỉ e2e test
npm run test:node # toàn bộ test trong thư mục test/
npm test # tsc --noEmit + oxlint + sql regression + test:nodeChi tiết về cấu trúc, quy ước và các hành vi được ghi nhận: test/README.md.
🛡️ XSS — cách dùng đúng filter của FastRequest
Cả hai filter html và stripTags đã được kiểm chứng không còn payload nào lọt
qua:
| Payload | html (escape) | stripTags (bỏ thẻ) |
|---|---|---|
| <script>alert(1)</script> | <script>... | alert(1) |
| <img src=x onerror=alert(1)> | <img ...> | (rỗng) |
| <svg/onload=alert(1)> | <svg/...> | (rỗng) |
| <a href="javascript:alert(1)"> | <a href="... | x |
| <iframe src="//evil.com"> | <iframe ...> | (rỗng) |
| <body onload=alert(1)> | <body ...> | (rỗng) |
5. Test & Coverage
Thư viện được kiểm thử bằng node:test (ESM). Unit tests import từ bản build (dist/index.mjs), vì vậy trước khi chạy kiểm thử cần build source:
npm run buildChạy test
# Tất cả test
npm test
# Unit tests (phân tích SQL, model, validation, routing...)
npm run test:unit
# Unit + integration (E2E)
npm run test:e2e
# Chỉ unit + E2E (bỏ SQL regression)
npm run test:nodeBáo cáo coverage (c8)
npm run test:coverageMặc định c8 coverage đúng mã nguồn src/ (bản build làm source map). Đồ họa thể hiện:
| Thư mục | % Stmts | % Branches | % Funcs | % Lines |
|---|---|---|---|---|
| Tổng | 96.18 | 89.44 | 84.08 | 96.18 |
| src/dialect | 59.18 | 81.81 | 44.11 | 59.18 |
| src/MySQLSessionStore.ts | 57.73 | 100 | 0 | 57.73 |
| src/PgSessionStore.ts | 59.34 | 100 | 0 | 59.34 |
| src/FastRequest.ts | 88.37 | 95.04 | 100 | 88.37 |
| src/FileUpload.ts | 97.77 | 94.28 | 100 | 97.77 |
| src/JWTApp.ts | 99.08 | 82.85 | 100 | 99.08 |
| src/Validation.ts | 99.37 | 90.65 | 100 | 99.37 |
| src/Sql.ts | 100 | 95.89 | 100 | 100 |
| src/Utils.ts | 97.08 | 96.15 | 100 | 97.08 |
| src/Models.ts, Route.ts, HMAC.ts, I18n.ts | 100 | 100 | 100 | 100 |
| src/lang/en.ts, vi.ts | 100 | 100 | 100 | 100 |
Điểm còn thiếu chủ yếu nằm ở MySQL/PostgreSQL dialect và session stores (chưa có test E2E phụ thuộc DB thật). Các file dữ liệu (validation, SQL builder, routing, utils, i18n) đều đạt gần 100%.
Lưu ý:
PgSessionStoređã được phát triển để hỗ trợ PostgreSQL nhưng chưa có test bằng cách giả lập; nếu phát triển cơ sở dữ liệu thật, một số phương thức nhưget,set,destroycó thể cần được ghi đè bằng 1 fake pool để kiểm thử.
| <math><mtext>...<img onerror=alert(1)> (mXSS) | toàn bộ bị escape | (rỗng) |
Chọn filter nào
| | html | stripTags |
|---|---|---|
| Cơ chế | escape & < > " ' \`` | xoá mọi thẻ |
| Bypass | không thể | không thể |
| Giữ nguyên văn bản thuần | ✅ | ✅ |
| Giữ thẻ HTML | ❌ (thành text) | ❌ (bị xoá) |
| Mất dữ liệu ở văn bản thuần | không | **có** — a < b→a b` |
- Muốn giữ nguyên nội dung để render HTML → dùng
html(escape an toàn nhất). - Muốn lấy text thuần (hiển thị, notification, CSV) → dùng
stripTags, nhưng nhớ nó sẽ nuốt mất các ký tự</>trong văn bản thường. - Cần lưu rich-text (nội dung từ trình soạn thảo WYSIWYG) →
htmlvàstripTagsđều không phù hợp; hãy tự thêm thư viện chuyên dụng (sanitize-htmlcho server,DOMPurifycho trình duyệt).
Hàm escapeHtml() được export riêng để dùng độc lập nếu bạn cần escape thủ công.
⚠️ Thay đổi phá vỡ (breaking changes)
Lần hardening bảo mật
| Thay đổi | Ảnh hưởng | Cách xử lý |
|---|---|---|
| Route.addGS() sinh POST /delete/:code thay vì GET | Client gọi GET sẽ nhận 404 | Đổi method sang POST và gửi CSRF token |
| JWTApp.verifyToken() từ chối token hết hạn | Token cũ hết hiệu lực ngay | Xây dựng luồng refresh token |
| JWTApp ném missing_jwt_key khi thiếu JWT_KEY | App khởi động lỗi nếu chưa cấu hình | Đặt JWT_KEY trong .env |
| FileUpload.upload()/removeFile()/copyFiles() từ chối đường dẫn chứa .. | Tham số folders cũ chứa ../ sẽ ném invalid_upload_folder | Dùng đường dẫn tương đối, ví dụ images/2026 |
| Validation chỉ chấp nhận rule trong allowlist | Rule tự định nghĩa không còn chạy | Chỉ dùng 19 rule được liệt kê |
| Validation.runValidate() xoá lỗi cũ ở mỗi lần gọi | Không còn cần clearErrors() thủ công | Bỏ các lời gọi clearErrors() dư thừa |
| Validation rule phoneVn chỉ nhận đúng 10 chữ số với tiền tố 03/05/07/08/09 | Chuỗi dài hơn hoặc có ký tự thừa bị từ chối | Bỏ khoảng trắng/ký tự thừa trước khi validate |
| Validation rule integer từ chối '12abc' và 1.5 | Giá trị trước đây hợp lệ giờ bị từ chối | Ép kiểu số trước khi validate |
| Validation rule password chặn chuỗi > 32 ký tự hoặc chứa ký tự lạ | Mật khẩu dài bị từ chối | Ràng buộc độ dài 8–32 như tài liệu |
| Validation rule inArray kiểm tra giá trị (membership) thay vì index của mảng | inArray(a) với 'a' giờ pass (trước luôn fail) | Không cần làm gì — đây là sửa lỗi |
| BaseModel.update() nhận toán tử nâng cao và tôn trọng soft delete | SQL UPDATE thay đổi; update() bỏ qua bản ghi đã xoá mềm | Dùng buildWhere điều kiện mong muốn |
| BaseModel.update() ném invalid_update_filter (thay vì thông báo tiếng Việt) | Bắt lỗi theo message cần đổi | Bắt theo invalid_update_filter |
| BaseModel.updateAdv() không còn nuốt lỗi thành invalid_update_is_deleted | Lỗi database thật được trả về | Bắt lỗi theo message gốc |
| BaseModel.isField() không còn dùng SHOW COLUMNS ... LIKE ? | SQL thay đổi; schema được cache lại | Gọi clearSchemaCache() sau khi migrate |
| Rule number trả false thay vì ném INVALID_SAFE_NUMBER | Lỗi vượt safe range nay đi qua con đường validate thông thường; NaN cũng bị từ chối | Bỏ catch dựa trên INVALID_SAFE_NUMBER |
| Rule email chấp nhận TLD ≥ 2 chữ cái và ký tự +; chặn dấu chấm liên tiếp | Email trước đây bị từ chối giờ hợp lệ | Không cần làm gì — đây là sửa lỗi |
| Rule equals/notEqual so sánh sau khi chuẩn hoá chuỗi | Số 1 giờ khớp equals(1) | Không cần làm gì |
| Rule inArrayNumber trả false với null/undefined, duyệt value của object | Trước đây ném TypeError | Không cần làm gì |
| modifiedSync() gom truy vấn theo (model, khoá) | Số lệnh SELECT giảm từ N xuống 1 mỗi cặp | Không cần làm gì |
| saveResult() là API mới, vdSave() dùng nó nên không còn đọc this.errors | save()/getErrors() giữ nguyên nhưng vẫn là state dùng chung | Dùng saveResult() cho code đồng thời |
| Route.addGS() nhận thêm tham số options.codeParam | Không bắt buộc, mặc định giữ nguyên | Truyền { codeParam: ':code([0-9a-f-]{36})' } cho UUID |
| Filter html giờ ESCAPE thay vì xoá thẻ — đóng lỗ hổng XSS | <p>a</p> → <p>a</p> thay vì giữ nguyên | Nếu cần rich-text, tự thêm sanitize-html/DOMPurify |
| IBaseModel.query() khai báo Promise<RawQueryResult> | Khớp với mysql2; khai báo modifieds với model thật giờ typecheck được | Dùng RawQueryResult nếu tự định nghĩa interface |
| Dấu \| trong chuỗi rule nay mang nghĩa là ký tự \| literal | inArray(a\|b) kiểm tra giá trị a|b thay vì bị tách thành 2 rule | Bỏ dấu \ nếu trước đó dùng \| theo nghĩa cũ |
Lần sửa lỗi P0
| Thay đổi | Ảnh hưởng | Cách xử lý |
|---|---|---|
| types trỏ đúng dist/index.d.mts | TypeScript dùng được kiểu | Không cần làm gì |
| BaseModel.deleteMany({}) ném lỗi | Xóa toàn bộ bảng không còn khả thi qua filter rỗng | Luôn truyền điều kiện lọc |
| BaseModel<T> chấp nhận entity khai báo bằng interface | Đúng như README hướng dẫn | Không cần làm gì |
| RouteController không đòi index signature | addGS(link, MyController, module) typecheck được | Không cần làm gì |
| Xóa cơ chế isDeleted flag (soft delete bằng cột) | isDeleted = true không còn hoạt động; buildWhere không còn tự thêm isDeleted = 0 | Chuyển sang dùng deleteMany() với archive table (tự tạo <table>_rm) |
| BaseModel.deleteMany() giờ dùng archive mode | Dữ liệu bị xóa chuyển sang bảng <table>_rm thay vì cập nhật cột | Gọi BaseModel.createArchiveTables(pool) lúc khởi động để tạo sẵn archive tables |
📄 Bản quyền
Giấy phép mã nguồn mở: ISC.
🐘 Hỗ trợ nhiều cơ sở dữ liệu (MySQL / PostgreSQL)
Phần lớn logic của BaseModel không phụ thuộc vào driver; chỉ cần thay dialect
thì chạy được cả MySQL và PostgreSQL. mysql2 và pg đều được khai báo optional,
bạn chỉ cần cài 1 trong 2.
Chọn dialect khi khởi tạo Model
import { BaseModel } from '@fastify-core/base';
import { MySqlDialect, PgDialect } from '@fastify-core/base';
import type { DbPool } from '@fastify-core/base';
export interface UserItem {
id: number;
fullname: string;
email: string;
roleId: number;
status: number;
}
export class UserModel extends BaseModel<UserItem> {
table = 'users';
fieldName(key: string) { return key; }
constructor(pool: DbPool, dialect: MySqlDialect | PgDialect = new MySqlDialect()) {
super(pool, dialect);
}
}- MySQL — không truyền dialect (mặc định
MySqlDialect):import mysql from 'mysql2/promise'; const pool = mysql.createPool({ host: 'localhost', user: 'root', database: 'app_db' }); const userModel = new UserModel(pool); // ← MySQL - PostgreSQL — truyền
PgDialect(và không khai báomysql2trong app):import { Pool } from 'pg'; import { PgDialect } from '@fastify-core/base'; const pool = new Pool({ host: 'localhost', user: 'postgres', database: 'app_db' }); const userModel = new UserModel(pool, new PgDialect()); // ← PostgreSQL
new UserModel(mysqlPool)giữ nguyên nguyên API cũ — backward compatible 100% cho code đang dùng MySQL. Code PostgreSQL chỉ cần thêmnew PgDialect()vào constructor.
Quản lý tập trung với initModels & getModels (có dialect)
import { initModels, getModels } from '@fastify-core/base';
import { UserModel } from './models/UserModel.js';
export const appModels = initModels({ user: UserModel }, pool, new PgDialect());
export type AppModels = typeof appModels;
const { user } = getModels<AppModels>();Những lưu ý khi chuyển sang PostgreSQL
| Hiện tại (MySQL) | PostgreSQL |
|---|---|
| Placeholder ? | $1, $2, $3... (được auto convert). Không dùng $n thủ công. |
| Backtick \tên`| Dấu nháy kép"tên"|
|INSERTtrảinsertId|INSERT ... RETURNING "id"|
|UPDATE/DELETEtrảaffectedRows| TrảrowCount|
|INSERT ... ON DUPLICATE KEY UPDATE ... VALUES(...)|ON CONFLICT ("id") DO UPDATE SET ... EXCLUDED.|
|SHOW COLUMNS|information_schema.columns|
| Boolean bind là1/0| Truyềntrue/false|
|MySQLSessionStore(14 ngày) | DùngPgSessionStore` (14 ngày mặc định) |
Cài đặt
# MySQL
npm i mysql2 @fastify-core/base
# PostgreSQL
npm i pg @fastify-core/base