@dqcai/sqlite 2.0.0 → 2.0.3
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 +1612 -361
- package/lib/core/base-service.d.ts +2 -15
- package/lib/core/base-service.d.ts.map +1 -1
- package/lib/core/database-manager.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +1 -1
- package/lib/index.mjs.map +1 -1
- package/lib/index.umd.js +1 -1
- package/lib/index.umd.js.map +1 -1
- package/lib/types.d.ts +16 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @dqcai/sqlite - A Universal SQLite Library (@dqcai/sqlite v2.0.
|
|
1
|
+
# @dqcai/sqlite - A Universal SQLite Library (@dqcai/sqlite v2.0.1)
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
@@ -19,6 +19,8 @@ UniversalSQLite là một thư viện SQLite toàn diện, hỗ trợ đa nền
|
|
|
19
19
|
- **Adapters**: Tự động detect môi trường, hỗ trợ register adapter tùy chỉnh.
|
|
20
20
|
- **Type-Safe**: Đầy đủ types TypeScript cho schema, query, và operations.
|
|
21
21
|
- **Utilities**: CSVImporter, MigrationManager, BaseService cho service layer.
|
|
22
|
+
- **DatabaseManager**: Quản lý kết nối, schema và vai trò người dùng
|
|
23
|
+
- **BaseService**: Lớp cơ sở cho CRUD operations
|
|
22
24
|
|
|
23
25
|
## Installation
|
|
24
26
|
|
|
@@ -32,480 +34,1729 @@ yarn add @dqcai/sqlite@2.0.0
|
|
|
32
34
|
|
|
33
35
|
Đối với React Native, đảm bảo cài đặt các dependencies cần thiết cho adapter (nếu sử dụng adapter cụ thể như react-native-sqlite-storage).
|
|
34
36
|
|
|
35
|
-
##
|
|
37
|
+
## Cài đặt
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
```bash
|
|
40
|
+
npm install @dqcai/sqlite
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 1. Cấu hình Schema Database
|
|
44
|
+
|
|
45
|
+
Trước tiên, định nghĩa schema cho cơ sở dữ liệu:
|
|
39
46
|
|
|
40
47
|
```typescript
|
|
41
48
|
import { DatabaseSchema } from '@dqcai/sqlite';
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
// Schema cho database users
|
|
51
|
+
const userSchema: DatabaseSchema = {
|
|
52
|
+
version: "1.0.0",
|
|
53
|
+
database_name: "users",
|
|
54
|
+
description: "User management database",
|
|
47
55
|
schemas: {
|
|
48
56
|
users: {
|
|
49
|
-
description:
|
|
57
|
+
description: "User table",
|
|
50
58
|
cols: [
|
|
51
|
-
{ name: 'id', type: 'integer', primary_key: true, auto_increment: true },
|
|
52
|
-
{ name: 'name', type: 'string', nullable: false },
|
|
53
|
-
{ name: 'email', type: 'string', unique: true },
|
|
54
|
-
],
|
|
55
|
-
indexes: [{ name: 'idx_email', columns: ['email'], unique: true }],
|
|
56
|
-
foreign_keys: [],
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
```json
|
|
63
|
-
// schema.json ví dụ kiểu json
|
|
64
|
-
{
|
|
65
|
-
"version": "1.0.0",
|
|
66
|
-
"database_name": "example.db",
|
|
67
|
-
"description": "Schema for example application.",
|
|
68
|
-
"schemas": {
|
|
69
|
-
"users": {
|
|
70
|
-
"description": "User accounts table.",
|
|
71
|
-
"cols": [
|
|
72
59
|
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
name: "id",
|
|
61
|
+
type: "integer",
|
|
62
|
+
primary_key: true,
|
|
63
|
+
auto_increment: true,
|
|
64
|
+
nullable: false
|
|
76
65
|
},
|
|
77
66
|
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
67
|
+
name: "username",
|
|
68
|
+
type: "varchar",
|
|
69
|
+
nullable: false,
|
|
70
|
+
unique: true,
|
|
71
|
+
length: 50
|
|
81
72
|
},
|
|
82
73
|
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
name: "email",
|
|
75
|
+
type: "varchar",
|
|
76
|
+
nullable: false,
|
|
77
|
+
unique: true,
|
|
78
|
+
length: 100
|
|
86
79
|
},
|
|
87
80
|
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
name: "password",
|
|
82
|
+
type: "varchar",
|
|
83
|
+
nullable: false,
|
|
84
|
+
length: 255
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "created_at",
|
|
88
|
+
type: "datetime",
|
|
89
|
+
nullable: false,
|
|
90
|
+
default: "CURRENT_TIMESTAMP"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "updated_at",
|
|
94
|
+
type: "datetime",
|
|
95
|
+
nullable: true
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
indexes: [
|
|
99
|
+
{
|
|
100
|
+
name: "idx_username",
|
|
101
|
+
columns: ["username"],
|
|
102
|
+
unique: true
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "idx_email",
|
|
106
|
+
columns: ["email"],
|
|
107
|
+
unique: true
|
|
91
108
|
}
|
|
92
109
|
]
|
|
93
110
|
},
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
profiles: {
|
|
112
|
+
description: "User profiles table",
|
|
113
|
+
cols: [
|
|
114
|
+
{
|
|
115
|
+
name: "id",
|
|
116
|
+
type: "integer",
|
|
117
|
+
primary_key: true,
|
|
118
|
+
auto_increment: true
|
|
119
|
+
},
|
|
97
120
|
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
121
|
+
name: "user_id",
|
|
122
|
+
type: "integer",
|
|
123
|
+
nullable: false
|
|
101
124
|
},
|
|
102
125
|
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
name: "first_name",
|
|
127
|
+
type: "varchar",
|
|
128
|
+
length: 50
|
|
106
129
|
},
|
|
107
130
|
{
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
name: "last_name",
|
|
132
|
+
type: "varchar",
|
|
133
|
+
length: 50
|
|
110
134
|
},
|
|
111
135
|
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
name: "phone",
|
|
137
|
+
type: "varchar",
|
|
138
|
+
length: 20
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "address",
|
|
142
|
+
type: "text"
|
|
115
143
|
}
|
|
116
144
|
],
|
|
117
|
-
|
|
145
|
+
foreign_keys: [
|
|
118
146
|
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
name: "fk_profile_user",
|
|
148
|
+
column: "user_id",
|
|
149
|
+
references: {
|
|
150
|
+
table: "users",
|
|
151
|
+
column: "id"
|
|
124
152
|
},
|
|
125
|
-
|
|
153
|
+
on_delete: "CASCADE",
|
|
154
|
+
on_update: "CASCADE"
|
|
126
155
|
}
|
|
127
156
|
]
|
|
128
157
|
}
|
|
129
158
|
}
|
|
130
|
-
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Schema core cho hệ thống
|
|
162
|
+
const coreSchema: DatabaseSchema = {
|
|
163
|
+
version: "1.0.0",
|
|
164
|
+
database_name: "core",
|
|
165
|
+
description: "Core system database",
|
|
166
|
+
schemas: {
|
|
167
|
+
settings: {
|
|
168
|
+
description: "System settings",
|
|
169
|
+
cols: [
|
|
170
|
+
{
|
|
171
|
+
name: "key",
|
|
172
|
+
type: "varchar",
|
|
173
|
+
primary_key: true,
|
|
174
|
+
length: 100
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "value",
|
|
178
|
+
type: "text"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "description",
|
|
182
|
+
type: "text"
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 2. Setup cho React Native
|
|
191
|
+
|
|
192
|
+
### Cài đặt dependencies
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npm install react-native-sqlite-2
|
|
196
|
+
# Hoặc
|
|
197
|
+
npm install react-native-sqlite-storage
|
|
131
198
|
```
|
|
132
199
|
|
|
133
|
-
###
|
|
134
|
-
Sử dụng singleton UniversalSQLite.
|
|
200
|
+
### Tạo Adapter cho React Native
|
|
135
201
|
|
|
136
202
|
```typescript
|
|
137
|
-
|
|
203
|
+
// adapters/ReactNativeAdapter.ts
|
|
204
|
+
import { BaseAdapter } from '@dqcai/sqlite';
|
|
205
|
+
import SQLite from 'react-native-sqlite-2';
|
|
206
|
+
|
|
207
|
+
export class ReactNativeAdapter extends BaseAdapter {
|
|
208
|
+
isSupported(): boolean {
|
|
209
|
+
return typeof SQLite !== 'undefined';
|
|
210
|
+
}
|
|
138
211
|
|
|
139
|
-
|
|
212
|
+
async connect(path: string): Promise<any> {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const db = SQLite.openDatabase(
|
|
215
|
+
path,
|
|
216
|
+
'1.0',
|
|
217
|
+
'Database',
|
|
218
|
+
200000,
|
|
219
|
+
() => {
|
|
220
|
+
resolve(new ReactNativeConnection(db));
|
|
221
|
+
},
|
|
222
|
+
(error) => {
|
|
223
|
+
reject(error);
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
140
229
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
230
|
+
class ReactNativeConnection {
|
|
231
|
+
constructor(private db: any) {}
|
|
232
|
+
|
|
233
|
+
async execute(sql: string, params: any[] = []): Promise<any> {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
this.db.transaction((tx: any) => {
|
|
236
|
+
tx.executeSql(
|
|
237
|
+
sql,
|
|
238
|
+
params,
|
|
239
|
+
(tx: any, results: any) => {
|
|
240
|
+
const rows: any[] = [];
|
|
241
|
+
for (let i = 0; i < results.rows.length; i++) {
|
|
242
|
+
rows.push(results.rows.item(i));
|
|
243
|
+
}
|
|
244
|
+
resolve({
|
|
245
|
+
rows,
|
|
246
|
+
rowsAffected: results.rowsAffected,
|
|
247
|
+
lastInsertRowId: results.insertId
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
(tx: any, error: any) => {
|
|
251
|
+
reject(error);
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
144
257
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
258
|
+
async close(): Promise<void> {
|
|
259
|
+
// React Native SQLite không cần close thủ công
|
|
260
|
+
return Promise.resolve();
|
|
261
|
+
}
|
|
149
262
|
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Khởi tạo DatabaseManager (React Native)
|
|
150
266
|
|
|
151
|
-
|
|
267
|
+
```typescript
|
|
268
|
+
// services/DatabaseService.ts
|
|
269
|
+
import { DatabaseManager, DatabaseFactory } from '@dqcai/sqlite';
|
|
270
|
+
import { ReactNativeAdapter } from '../adapters/ReactNativeAdapter';
|
|
271
|
+
|
|
272
|
+
export class DatabaseService {
|
|
273
|
+
private static isInitialized = false;
|
|
274
|
+
|
|
275
|
+
static async initialize() {
|
|
276
|
+
if (this.isInitialized) return;
|
|
277
|
+
|
|
278
|
+
// Đăng ký adapter
|
|
279
|
+
DatabaseFactory.registerAdapter(new ReactNativeAdapter());
|
|
280
|
+
|
|
281
|
+
// Đăng ký schemas
|
|
282
|
+
DatabaseManager.registerSchemas({
|
|
283
|
+
core: coreSchema,
|
|
284
|
+
users: userSchema
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Đăng ký roles
|
|
288
|
+
DatabaseManager.registerRoles([
|
|
289
|
+
{
|
|
290
|
+
roleName: 'admin',
|
|
291
|
+
requiredDatabases: ['core', 'users'],
|
|
292
|
+
priority: 1
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
roleName: 'user',
|
|
296
|
+
requiredDatabases: ['core'],
|
|
297
|
+
optionalDatabases: ['users'],
|
|
298
|
+
priority: 2
|
|
299
|
+
}
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
// Khởi tạo core database
|
|
303
|
+
await DatabaseManager.initializeCoreConnection();
|
|
304
|
+
|
|
305
|
+
this.isInitialized = true;
|
|
306
|
+
console.log('DatabaseService initialized for React Native');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
static async setUserRole(roles: string[]) {
|
|
310
|
+
await DatabaseManager.setCurrentUserRoles(roles);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
static getConnection(dbKey: string) {
|
|
314
|
+
return DatabaseManager.get(dbKey);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static async closeAll() {
|
|
318
|
+
await DatabaseManager.closeAll();
|
|
319
|
+
this.isInitialized = false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
152
322
|
```
|
|
153
323
|
|
|
154
|
-
##
|
|
324
|
+
## 3. Setup cho Node.js
|
|
155
325
|
|
|
156
|
-
|
|
326
|
+
### Cài đặt dependencies
|
|
157
327
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
328
|
+
```bash
|
|
329
|
+
npm install sqlite3
|
|
330
|
+
# Hoặc
|
|
331
|
+
npm install better-sqlite3
|
|
332
|
+
```
|
|
163
333
|
|
|
164
|
-
###
|
|
165
|
-
Khai báo adapter tùy chỉnh bằng cách extend BaseAdapter và register với DatabaseFactory.
|
|
334
|
+
### Tạo Adapter cho Node.js
|
|
166
335
|
|
|
167
336
|
```typescript
|
|
168
|
-
|
|
169
|
-
import
|
|
337
|
+
// adapters/NodeAdapter.ts
|
|
338
|
+
import { BaseAdapter } from '@dqcai/sqlite';
|
|
339
|
+
import sqlite3 from 'sqlite3';
|
|
340
|
+
import path from 'path';
|
|
341
|
+
import fs from 'fs';
|
|
342
|
+
|
|
343
|
+
export class NodeAdapter extends BaseAdapter {
|
|
344
|
+
isSupported(): boolean {
|
|
345
|
+
return typeof process !== 'undefined' && process.versions?.node;
|
|
346
|
+
}
|
|
170
347
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
348
|
+
async connect(dbPath: string): Promise<any> {
|
|
349
|
+
const fullPath = path.resolve(dbPath);
|
|
350
|
+
|
|
351
|
+
// Tạo thư mục nếu chưa tồn tại
|
|
352
|
+
const dir = path.dirname(fullPath);
|
|
353
|
+
if (!fs.existsSync(dir)) {
|
|
354
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
const db = new sqlite3.Database(fullPath, (err) => {
|
|
359
|
+
if (err) {
|
|
360
|
+
reject(err);
|
|
361
|
+
} else {
|
|
362
|
+
resolve(new NodeConnection(db));
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
175
367
|
}
|
|
176
368
|
|
|
177
|
-
|
|
369
|
+
class NodeConnection {
|
|
370
|
+
constructor(private db: sqlite3.Database) {}
|
|
371
|
+
|
|
372
|
+
async execute(sql: string, params: any[] = []): Promise<any> {
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
const isSelect = sql.trim().toUpperCase().startsWith('SELECT');
|
|
375
|
+
|
|
376
|
+
if (isSelect) {
|
|
377
|
+
this.db.all(sql, params, (err, rows) => {
|
|
378
|
+
if (err) {
|
|
379
|
+
reject(err);
|
|
380
|
+
} else {
|
|
381
|
+
resolve({
|
|
382
|
+
rows: rows || [],
|
|
383
|
+
rowsAffected: 0
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
} else {
|
|
388
|
+
this.db.run(sql, params, function(err) {
|
|
389
|
+
if (err) {
|
|
390
|
+
reject(err);
|
|
391
|
+
} else {
|
|
392
|
+
resolve({
|
|
393
|
+
rows: [],
|
|
394
|
+
rowsAffected: this.changes,
|
|
395
|
+
lastInsertRowId: this.lastID
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async close(): Promise<void> {
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
this.db.close((err) => {
|
|
406
|
+
if (err) {
|
|
407
|
+
reject(err);
|
|
408
|
+
} else {
|
|
409
|
+
resolve();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
178
415
|
```
|
|
179
416
|
|
|
180
|
-
|
|
417
|
+
### Khởi tạo DatabaseManager (Node.js)
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// services/DatabaseService.ts
|
|
421
|
+
import { DatabaseManager, DatabaseFactory } from '@dqcai/sqlite';
|
|
422
|
+
import { NodeAdapter } from '../adapters/NodeAdapter';
|
|
423
|
+
import path from 'path';
|
|
424
|
+
|
|
425
|
+
export class DatabaseService {
|
|
426
|
+
private static isInitialized = false;
|
|
427
|
+
private static dbDirectory = './databases';
|
|
428
|
+
|
|
429
|
+
static async initialize() {
|
|
430
|
+
if (this.isInitialized) return;
|
|
431
|
+
|
|
432
|
+
// Đăng ký adapter
|
|
433
|
+
DatabaseFactory.registerAdapter(new NodeAdapter());
|
|
434
|
+
|
|
435
|
+
// Đăng ký schemas
|
|
436
|
+
DatabaseManager.registerSchemas({
|
|
437
|
+
core: coreSchema,
|
|
438
|
+
users: userSchema
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Đăng ký roles
|
|
442
|
+
DatabaseManager.registerRoles([
|
|
443
|
+
{
|
|
444
|
+
roleName: 'admin',
|
|
445
|
+
requiredDatabases: ['core', 'users'],
|
|
446
|
+
priority: 1
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
roleName: 'user',
|
|
450
|
+
requiredDatabases: ['core'],
|
|
451
|
+
optionalDatabases: ['users'],
|
|
452
|
+
priority: 2
|
|
453
|
+
}
|
|
454
|
+
]);
|
|
455
|
+
|
|
456
|
+
// Khởi tạo core database
|
|
457
|
+
await DatabaseManager.initializeCoreConnection();
|
|
458
|
+
|
|
459
|
+
this.isInitialized = true;
|
|
460
|
+
console.log('DatabaseService initialized for Node.js');
|
|
461
|
+
}
|
|
181
462
|
|
|
182
|
-
|
|
183
|
-
|
|
463
|
+
static async setUserRole(roles: string[]) {
|
|
464
|
+
await DatabaseManager.setCurrentUserRoles(roles);
|
|
465
|
+
}
|
|
184
466
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
```
|
|
467
|
+
static getConnection(dbKey: string) {
|
|
468
|
+
return DatabaseManager.get(dbKey);
|
|
469
|
+
}
|
|
189
470
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
471
|
+
static async closeAll() {
|
|
472
|
+
await DatabaseManager.closeAll();
|
|
473
|
+
this.isInitialized = false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
```
|
|
194
477
|
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
const dao = DatabaseFactory.createDAO('example_db.db', { createIfNotExists: true });
|
|
198
|
-
await dao.connect();
|
|
199
|
-
```
|
|
478
|
+
## 4. Tạo Services với BaseService
|
|
200
479
|
|
|
201
|
-
###
|
|
202
|
-
UniversalDAO cung cấp các method CRUD cơ bản.
|
|
480
|
+
### User Service
|
|
203
481
|
|
|
204
482
|
```typescript
|
|
205
|
-
//
|
|
206
|
-
|
|
483
|
+
// services/UserService.ts
|
|
484
|
+
import { BaseService } from '@dqcai/sqlite';
|
|
485
|
+
|
|
486
|
+
interface User {
|
|
487
|
+
id?: number;
|
|
488
|
+
username: string;
|
|
489
|
+
email: string;
|
|
490
|
+
password: string;
|
|
491
|
+
created_at?: string;
|
|
492
|
+
updated_at?: string;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
interface Profile {
|
|
496
|
+
id?: number;
|
|
497
|
+
user_id: number;
|
|
498
|
+
first_name?: string;
|
|
499
|
+
last_name?: string;
|
|
500
|
+
phone?: string;
|
|
501
|
+
address?: string;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export class UserService extends BaseService<User> {
|
|
505
|
+
constructor() {
|
|
506
|
+
super('users', 'users'); // schema name, table name
|
|
507
|
+
this.setPrimaryKeyFields(['id']);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Tạo user mới
|
|
511
|
+
async createUser(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User | null> {
|
|
512
|
+
try {
|
|
513
|
+
// Kiểm tra email đã tồn tại chưa
|
|
514
|
+
const existingUser = await this.findFirst({ email: userData.email });
|
|
515
|
+
if (existingUser) {
|
|
516
|
+
throw new Error('Email already exists');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Kiểm tra username đã tồn tại chưa
|
|
520
|
+
const existingUsername = await this.findFirst({ username: userData.username });
|
|
521
|
+
if (existingUsername) {
|
|
522
|
+
throw new Error('Username already exists');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const newUser = await this.create({
|
|
526
|
+
...userData,
|
|
527
|
+
created_at: new Date().toISOString()
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
return newUser;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
console.error('Error creating user:', error);
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
207
536
|
|
|
208
|
-
//
|
|
209
|
-
|
|
537
|
+
// Cập nhật user
|
|
538
|
+
async updateUser(id: number, userData: Partial<User>): Promise<User | null> {
|
|
539
|
+
try {
|
|
540
|
+
const updatedUser = await this.update(id, {
|
|
541
|
+
...userData,
|
|
542
|
+
updated_at: new Date().toISOString()
|
|
543
|
+
});
|
|
544
|
+
return updatedUser;
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.error('Error updating user:', error);
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
210
550
|
|
|
211
|
-
//
|
|
212
|
-
|
|
551
|
+
// Tìm user theo email
|
|
552
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
553
|
+
return await this.findFirst({ email });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Tìm user theo username
|
|
557
|
+
async findByUsername(username: string): Promise<User | null> {
|
|
558
|
+
return await this.findFirst({ username });
|
|
559
|
+
}
|
|
213
560
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
const
|
|
561
|
+
// Lấy tất cả users với phân trang
|
|
562
|
+
async getAllUsers(page: number = 1, limit: number = 10): Promise<User[]> {
|
|
563
|
+
const offset = (page - 1) * limit;
|
|
564
|
+
return await this.findAll({}, {
|
|
565
|
+
orderBy: [{ name: 'created_at', direction: 'DESC' }],
|
|
566
|
+
limit,
|
|
567
|
+
offset
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Xóa user (soft delete bằng cách cập nhật trường deleted_at)
|
|
572
|
+
async softDeleteUser(id: number): Promise<boolean> {
|
|
573
|
+
const result = await this.update(id, {
|
|
574
|
+
updated_at: new Date().toISOString(),
|
|
575
|
+
// deleted_at: new Date().toISOString() // nếu có field này trong schema
|
|
576
|
+
});
|
|
577
|
+
return result !== null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Đếm tổng số users
|
|
581
|
+
async getTotalUsers(): Promise<number> {
|
|
582
|
+
return await this.count();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Tìm kiếm users
|
|
586
|
+
async searchUsers(searchTerm: string): Promise<User[]> {
|
|
587
|
+
const dao = await this.init().then(() => this.dao!);
|
|
588
|
+
const sql = `
|
|
589
|
+
SELECT * FROM users
|
|
590
|
+
WHERE username LIKE ? OR email LIKE ?
|
|
591
|
+
ORDER BY created_at DESC
|
|
592
|
+
`;
|
|
593
|
+
const params = [`%${searchTerm}%`, `%${searchTerm}%`];
|
|
594
|
+
const result = await dao.execute(sql, params);
|
|
595
|
+
return result.rows as User[];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export class ProfileService extends BaseService<Profile> {
|
|
600
|
+
constructor() {
|
|
601
|
+
super('users', 'profiles');
|
|
602
|
+
this.setPrimaryKeyFields(['id']);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Tạo profile cho user
|
|
606
|
+
async createProfile(profileData: Omit<Profile, 'id'>): Promise<Profile | null> {
|
|
607
|
+
return await this.create(profileData);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Lấy profile theo user_id
|
|
611
|
+
async getProfileByUserId(userId: number): Promise<Profile | null> {
|
|
612
|
+
return await this.findFirst({ user_id: userId });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Cập nhật profile
|
|
616
|
+
async updateProfile(id: number, profileData: Partial<Profile>): Promise<Profile | null> {
|
|
617
|
+
return await this.update(id, profileData);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Lấy thông tin user và profile
|
|
621
|
+
async getUserWithProfile(userId: number): Promise<any> {
|
|
622
|
+
const dao = await this.init().then(() => this.dao!);
|
|
623
|
+
const sql = `
|
|
624
|
+
SELECT
|
|
625
|
+
u.id, u.username, u.email, u.created_at,
|
|
626
|
+
p.first_name, p.last_name, p.phone, p.address
|
|
627
|
+
FROM users u
|
|
628
|
+
LEFT JOIN profiles p ON u.id = p.user_id
|
|
629
|
+
WHERE u.id = ?
|
|
630
|
+
`;
|
|
631
|
+
const result = await dao.execute(sql, [userId]);
|
|
632
|
+
return result.rows[0] || null;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
217
635
|
```
|
|
218
636
|
|
|
219
|
-
###
|
|
220
|
-
Xây dựng query phức tạp.
|
|
637
|
+
### Core Service
|
|
221
638
|
|
|
222
639
|
```typescript
|
|
223
|
-
|
|
640
|
+
// services/CoreService.ts
|
|
641
|
+
import { BaseService } from '@dqcai/sqlite';
|
|
642
|
+
|
|
643
|
+
interface Setting {
|
|
644
|
+
key: string;
|
|
645
|
+
value: string;
|
|
646
|
+
description?: string;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export class SettingsService extends BaseService<Setting> {
|
|
650
|
+
constructor() {
|
|
651
|
+
super('core', 'settings');
|
|
652
|
+
this.setPrimaryKeyFields(['key']);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Lấy giá trị setting
|
|
656
|
+
async getSetting(key: string): Promise<string | null> {
|
|
657
|
+
const setting = await this.findById(key);
|
|
658
|
+
return setting?.value || null;
|
|
659
|
+
}
|
|
224
660
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
661
|
+
// Đặt giá trị setting
|
|
662
|
+
async setSetting(key: string, value: string, description?: string): Promise<void> {
|
|
663
|
+
const existing = await this.findById(key);
|
|
664
|
+
|
|
665
|
+
if (existing) {
|
|
666
|
+
await this.update(key, { value, description });
|
|
667
|
+
} else {
|
|
668
|
+
await this.create({ key, value, description });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Lấy tất cả settings
|
|
673
|
+
async getAllSettings(): Promise<Setting[]> {
|
|
674
|
+
return await this.findAll({}, {
|
|
675
|
+
orderBy: [{ name: 'key', direction: 'ASC' }]
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Xóa setting
|
|
680
|
+
async deleteSetting(key: string): Promise<boolean> {
|
|
681
|
+
return await this.delete(key);
|
|
682
|
+
}
|
|
230
683
|
|
|
231
|
-
|
|
684
|
+
// Lấy nhiều settings cùng lúc
|
|
685
|
+
async getMultipleSettings(keys: string[]): Promise<Record<string, string>> {
|
|
686
|
+
const dao = await this.init().then(() => this.dao!);
|
|
687
|
+
const placeholders = keys.map(() => '?').join(',');
|
|
688
|
+
const sql = `SELECT key, value FROM settings WHERE key IN (${placeholders})`;
|
|
689
|
+
const result = await dao.execute(sql, keys);
|
|
690
|
+
|
|
691
|
+
const settings: Record<string, string> = {};
|
|
692
|
+
result.rows.forEach(row => {
|
|
693
|
+
settings[row.key] = row.value;
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
return settings;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
232
699
|
```
|
|
233
700
|
|
|
234
|
-
|
|
235
|
-
|
|
701
|
+
## 5. Sử dụng trong ứng dụng
|
|
702
|
+
|
|
703
|
+
### Trong React Native
|
|
236
704
|
|
|
237
705
|
```typescript
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
706
|
+
// App.tsx hoặc index.js
|
|
707
|
+
import React, { useEffect, useState } from 'react';
|
|
708
|
+
import { View, Text, Button, Alert } from 'react-native';
|
|
709
|
+
import { DatabaseService } from './services/DatabaseService';
|
|
710
|
+
import { UserService, ProfileService } from './services/UserService';
|
|
711
|
+
import { SettingsService } from './services/CoreService';
|
|
712
|
+
|
|
713
|
+
const App = () => {
|
|
714
|
+
const [isDbReady, setIsDbReady] = useState(false);
|
|
715
|
+
const [userService] = useState(new UserService());
|
|
716
|
+
const [profileService] = useState(new ProfileService());
|
|
717
|
+
const [settingsService] = useState(new SettingsService());
|
|
718
|
+
|
|
719
|
+
useEffect(() => {
|
|
720
|
+
initializeDatabase();
|
|
721
|
+
|
|
722
|
+
return () => {
|
|
723
|
+
// Cleanup khi component unmount
|
|
724
|
+
DatabaseService.closeAll();
|
|
725
|
+
};
|
|
726
|
+
}, []);
|
|
727
|
+
|
|
728
|
+
const initializeDatabase = async () => {
|
|
729
|
+
try {
|
|
730
|
+
// Khởi tạo database
|
|
731
|
+
await DatabaseService.initialize();
|
|
732
|
+
|
|
733
|
+
// Đặt role cho user hiện tại
|
|
734
|
+
await DatabaseService.setUserRole(['user']);
|
|
735
|
+
|
|
736
|
+
// Khởi tạo services
|
|
737
|
+
await userService.init();
|
|
738
|
+
await profileService.init();
|
|
739
|
+
await settingsService.init();
|
|
740
|
+
|
|
741
|
+
setIsDbReady(true);
|
|
742
|
+
console.log('Database ready!');
|
|
743
|
+
} catch (error) {
|
|
744
|
+
console.error('Database initialization failed:', error);
|
|
745
|
+
Alert.alert('Error', 'Failed to initialize database');
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const handleCreateUser = async () => {
|
|
750
|
+
if (!isDbReady) return;
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
const newUser = await userService.createUser({
|
|
754
|
+
username: 'john_doe',
|
|
755
|
+
email: 'john@example.com',
|
|
756
|
+
password: 'hashed_password'
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
if (newUser) {
|
|
760
|
+
// Tạo profile cho user
|
|
761
|
+
await profileService.createProfile({
|
|
762
|
+
user_id: newUser.id!,
|
|
763
|
+
first_name: 'John',
|
|
764
|
+
last_name: 'Doe',
|
|
765
|
+
phone: '+1234567890'
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
Alert.alert('Success', 'User created successfully');
|
|
769
|
+
}
|
|
770
|
+
} catch (error) {
|
|
771
|
+
console.error('Error creating user:', error);
|
|
772
|
+
Alert.alert('Error', 'Failed to create user');
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const handleGetAllUsers = async () => {
|
|
777
|
+
if (!isDbReady) return;
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
const users = await userService.getAllUsers(1, 10);
|
|
781
|
+
console.log('Users:', users);
|
|
782
|
+
Alert.alert('Users', `Found ${users.length} users`);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.error('Error getting users:', error);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const handleSetSetting = async () => {
|
|
789
|
+
if (!isDbReady) return;
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
await settingsService.setSetting(
|
|
793
|
+
'app_version',
|
|
794
|
+
'1.0.0',
|
|
795
|
+
'Current app version'
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
const version = await settingsService.getSetting('app_version');
|
|
799
|
+
Alert.alert('Setting', `App version: ${version}`);
|
|
800
|
+
} catch (error) {
|
|
801
|
+
console.error('Error setting value:', error);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
return (
|
|
806
|
+
<View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
|
|
807
|
+
<Text>Database Status: {isDbReady ? 'Ready' : 'Initializing...'}</Text>
|
|
808
|
+
|
|
809
|
+
<Button
|
|
810
|
+
title="Create User"
|
|
811
|
+
onPress={handleCreateUser}
|
|
812
|
+
disabled={!isDbReady}
|
|
813
|
+
/>
|
|
814
|
+
|
|
815
|
+
<Button
|
|
816
|
+
title="Get All Users"
|
|
817
|
+
onPress={handleGetAllUsers}
|
|
818
|
+
disabled={!isDbReady}
|
|
819
|
+
/>
|
|
820
|
+
|
|
821
|
+
<Button
|
|
822
|
+
title="Set App Setting"
|
|
823
|
+
onPress={handleSetSetting}
|
|
824
|
+
disabled={!isDbReady}
|
|
825
|
+
/>
|
|
826
|
+
</View>
|
|
827
|
+
);
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
export default App;
|
|
246
831
|
```
|
|
247
832
|
|
|
248
|
-
###
|
|
249
|
-
Sử dụng CSVImporter hoặc importData.
|
|
833
|
+
### Trong Node.js
|
|
250
834
|
|
|
251
835
|
```typescript
|
|
252
|
-
|
|
253
|
-
|
|
836
|
+
// app.ts
|
|
837
|
+
import express from 'express';
|
|
838
|
+
import { DatabaseService } from './services/DatabaseService';
|
|
839
|
+
import { UserService, ProfileService } from './services/UserService';
|
|
840
|
+
import { SettingsService } from './services/CoreService';
|
|
254
841
|
|
|
255
|
-
|
|
256
|
-
|
|
842
|
+
const app = express();
|
|
843
|
+
app.use(express.json());
|
|
844
|
+
|
|
845
|
+
// Services
|
|
846
|
+
const userService = new UserService();
|
|
847
|
+
const profileService = new ProfileService();
|
|
848
|
+
const settingsService = new SettingsService();
|
|
849
|
+
|
|
850
|
+
// Khởi tạo database khi start server
|
|
851
|
+
async function initializeApp() {
|
|
852
|
+
try {
|
|
853
|
+
console.log('Initializing database...');
|
|
854
|
+
|
|
855
|
+
// Khởi tạo DatabaseService
|
|
856
|
+
await DatabaseService.initialize();
|
|
857
|
+
|
|
858
|
+
// Set role admin cho server
|
|
859
|
+
await DatabaseService.setUserRole(['admin']);
|
|
860
|
+
|
|
861
|
+
// Khởi tạo services
|
|
862
|
+
await userService.init();
|
|
863
|
+
await profileService.init();
|
|
864
|
+
await settingsService.init();
|
|
865
|
+
|
|
866
|
+
console.log('Database initialized successfully');
|
|
867
|
+
} catch (error) {
|
|
868
|
+
console.error('Database initialization failed:', error);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// API Routes
|
|
874
|
+
|
|
875
|
+
// POST /users - Tạo user mới
|
|
876
|
+
app.post('/users', async (req, res) => {
|
|
877
|
+
try {
|
|
878
|
+
const { username, email, password } = req.body;
|
|
879
|
+
|
|
880
|
+
if (!username || !email || !password) {
|
|
881
|
+
return res.status(400).json({ error: 'Missing required fields' });
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const user = await userService.createUser({
|
|
885
|
+
username,
|
|
886
|
+
email,
|
|
887
|
+
password // Nên hash password trước khi lưu
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
res.status(201).json({ success: true, user });
|
|
891
|
+
} catch (error: any) {
|
|
892
|
+
console.error('Error creating user:', error);
|
|
893
|
+
res.status(400).json({ error: error.message });
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// GET /users - Lấy danh sách users
|
|
898
|
+
app.get('/users', async (req, res) => {
|
|
899
|
+
try {
|
|
900
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
901
|
+
const limit = parseInt(req.query.limit as string) || 10;
|
|
902
|
+
|
|
903
|
+
const users = await userService.getAllUsers(page, limit);
|
|
904
|
+
const total = await userService.getTotalUsers();
|
|
905
|
+
|
|
906
|
+
res.json({
|
|
907
|
+
success: true,
|
|
908
|
+
users,
|
|
909
|
+
pagination: {
|
|
910
|
+
page,
|
|
911
|
+
limit,
|
|
912
|
+
total,
|
|
913
|
+
pages: Math.ceil(total / limit)
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
} catch (error: any) {
|
|
917
|
+
console.error('Error getting users:', error);
|
|
918
|
+
res.status(500).json({ error: error.message });
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// GET /users/:id - Lấy user theo ID
|
|
923
|
+
app.get('/users/:id', async (req, res) => {
|
|
924
|
+
try {
|
|
925
|
+
const id = parseInt(req.params.id);
|
|
926
|
+
const user = await userService.findById(id);
|
|
927
|
+
|
|
928
|
+
if (!user) {
|
|
929
|
+
return res.status(404).json({ error: 'User not found' });
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
res.json({ success: true, user });
|
|
933
|
+
} catch (error: any) {
|
|
934
|
+
console.error('Error getting user:', error);
|
|
935
|
+
res.status(500).json({ error: error.message });
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// PUT /users/:id - Cập nhật user
|
|
940
|
+
app.put('/users/:id', async (req, res) => {
|
|
941
|
+
try {
|
|
942
|
+
const id = parseInt(req.params.id);
|
|
943
|
+
const updates = req.body;
|
|
944
|
+
|
|
945
|
+
const user = await userService.updateUser(id, updates);
|
|
946
|
+
|
|
947
|
+
if (!user) {
|
|
948
|
+
return res.status(404).json({ error: 'User not found' });
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
res.json({ success: true, user });
|
|
952
|
+
} catch (error: any) {
|
|
953
|
+
console.error('Error updating user:', error);
|
|
954
|
+
res.status(400).json({ error: error.message });
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
// DELETE /users/:id - Xóa user
|
|
959
|
+
app.delete('/users/:id', async (req, res) => {
|
|
960
|
+
try {
|
|
961
|
+
const id = parseInt(req.params.id);
|
|
962
|
+
const success = await userService.delete(id);
|
|
963
|
+
|
|
964
|
+
if (!success) {
|
|
965
|
+
return res.status(404).json({ error: 'User not found' });
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
res.json({ success: true, message: 'User deleted' });
|
|
969
|
+
} catch (error: any) {
|
|
970
|
+
console.error('Error deleting user:', error);
|
|
971
|
+
res.status(500).json({ error: error.message });
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// POST /users/:id/profile - Tạo profile cho user
|
|
976
|
+
app.post('/users/:id/profile', async (req, res) => {
|
|
977
|
+
try {
|
|
978
|
+
const user_id = parseInt(req.params.id);
|
|
979
|
+
const profileData = { ...req.body, user_id };
|
|
980
|
+
|
|
981
|
+
const profile = await profileService.createProfile(profileData);
|
|
982
|
+
res.status(201).json({ success: true, profile });
|
|
983
|
+
} catch (error: any) {
|
|
984
|
+
console.error('Error creating profile:', error);
|
|
985
|
+
res.status(400).json({ error: error.message });
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// GET /users/:id/full - Lấy user với profile
|
|
990
|
+
app.get('/users/:id/full', async (req, res) => {
|
|
991
|
+
try {
|
|
992
|
+
const user_id = parseInt(req.params.id);
|
|
993
|
+
const userWithProfile = await profileService.getUserWithProfile(user_id);
|
|
994
|
+
|
|
995
|
+
if (!userWithProfile) {
|
|
996
|
+
return res.status(404).json({ error: 'User not found' });
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
res.json({ success: true, user: userWithProfile });
|
|
1000
|
+
} catch (error: any) {
|
|
1001
|
+
console.error('Error getting user with profile:', error);
|
|
1002
|
+
res.status(500).json({ error: error.message });
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
// Settings API
|
|
1007
|
+
app.get('/settings/:key', async (req, res) => {
|
|
1008
|
+
try {
|
|
1009
|
+
const value = await settingsService.getSetting(req.params.key);
|
|
1010
|
+
res.json({ success: true, value });
|
|
1011
|
+
} catch (error: any) {
|
|
1012
|
+
console.error('Error getting setting:', error);
|
|
1013
|
+
res.status(500).json({ error: error.message });
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
app.post('/settings', async (req, res) => {
|
|
1018
|
+
try {
|
|
1019
|
+
const { key, value, description } = req.body;
|
|
1020
|
+
await settingsService.setSetting(key, value, description);
|
|
1021
|
+
res.json({ success: true, message: 'Setting saved' });
|
|
1022
|
+
} catch (error: any) {
|
|
1023
|
+
console.error('Error setting value:', error);
|
|
1024
|
+
res.status(500).json({ error: error.message });
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// Graceful shutdown
|
|
1029
|
+
process.on('SIGINT', async () => {
|
|
1030
|
+
console.log('Shutting down server...');
|
|
1031
|
+
await DatabaseService.closeAll();
|
|
1032
|
+
process.exit(0);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
process.on('SIGTERM', async () => {
|
|
1036
|
+
console.log('Shutting down server...');
|
|
1037
|
+
await DatabaseService.closeAll();
|
|
1038
|
+
process.exit(0);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// Start server
|
|
1042
|
+
const PORT = process.env.PORT || 3000;
|
|
1043
|
+
|
|
1044
|
+
initializeApp().then(() => {
|
|
1045
|
+
app.listen(PORT, () => {
|
|
1046
|
+
console.log(`Server running on port ${PORT}`);
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
257
1049
|
```
|
|
258
1050
|
|
|
259
|
-
|
|
1051
|
+
## 6. Quản lý Database với DatabaseManager
|
|
1052
|
+
|
|
1053
|
+
### Mở/Đóng Connections
|
|
1054
|
+
|
|
260
1055
|
```typescript
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
1056
|
+
// DatabaseHelper.ts
|
|
1057
|
+
import { DatabaseManager } from '@dqcai/sqlite';
|
|
1058
|
+
|
|
1059
|
+
export class DatabaseHelper {
|
|
1060
|
+
|
|
1061
|
+
// Kiểm tra trạng thái kết nối
|
|
1062
|
+
static checkConnectionStatus(): void {
|
|
1063
|
+
const connections = DatabaseManager.getConnections();
|
|
1064
|
+
const count = DatabaseManager.getConnectionCount();
|
|
1065
|
+
const activeList = DatabaseManager.listConnections();
|
|
1066
|
+
|
|
1067
|
+
console.log('Active connections:', count);
|
|
1068
|
+
console.log('Connection list:', activeList);
|
|
1069
|
+
console.log('Connection details:', connections);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Đóng kết nối cụ thể
|
|
1073
|
+
static async closeSpecificConnection(dbKey: string): Promise<void> {
|
|
1074
|
+
try {
|
|
1075
|
+
await DatabaseManager.closeConnection(dbKey);
|
|
1076
|
+
console.log(`Connection ${dbKey} closed`);
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
console.error(`Error closing connection ${dbKey}:`, error);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Mở lại kết nối
|
|
1083
|
+
static async reopenConnection(dbKey: string): Promise<void> {
|
|
1084
|
+
try {
|
|
1085
|
+
const dao = await DatabaseManager.getLazyLoading(dbKey);
|
|
1086
|
+
console.log(`Connection ${dbKey} reopened`);
|
|
1087
|
+
return dao;
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
console.error(`Error reopening connection ${dbKey}:`, error);
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Đảm bảo kết nối tồn tại
|
|
1095
|
+
static async ensureConnection(dbKey: string): Promise<void> {
|
|
1096
|
+
try {
|
|
1097
|
+
const dao = await DatabaseManager.ensureDatabaseConnection(dbKey);
|
|
1098
|
+
console.log(`Connection ${dbKey} ensured`);
|
|
1099
|
+
return dao;
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
console.error(`Error ensuring connection ${dbKey}:`, error);
|
|
1102
|
+
throw error;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Thực hiện transaction cross-schema
|
|
1107
|
+
static async executeTransactionAcrossSchemas(
|
|
1108
|
+
schemas: string[],
|
|
1109
|
+
operations: (daos: Record<string, any>) => Promise<void>
|
|
1110
|
+
): Promise<void> {
|
|
1111
|
+
try {
|
|
1112
|
+
await DatabaseManager.executeCrossSchemaTransaction(schemas, operations);
|
|
1113
|
+
console.log('Cross-schema transaction completed successfully');
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
console.error('Cross-schema transaction failed:', error);
|
|
1116
|
+
throw error;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Event listeners cho reconnection
|
|
1121
|
+
static setupReconnectionHandlers(): void {
|
|
1122
|
+
DatabaseManager.onDatabaseReconnect('users', (dao) => {
|
|
1123
|
+
console.log('Users database reconnected');
|
|
1124
|
+
// Re-initialize services nếu cần
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
DatabaseManager.onDatabaseReconnect('core', (dao) => {
|
|
1128
|
+
console.log('Core database reconnected');
|
|
1129
|
+
// Re-initialize settings
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Health check tất cả connections
|
|
1134
|
+
static async performHealthCheck(): Promise<void> {
|
|
1135
|
+
const connections = DatabaseManager.getConnections();
|
|
1136
|
+
|
|
1137
|
+
for (const [dbKey, dao] of Object.entries(connections)) {
|
|
1138
|
+
try {
|
|
1139
|
+
await dao.execute('SELECT 1');
|
|
1140
|
+
console.log(`${dbKey}: Healthy`);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
console.error(`${dbKey}: Unhealthy -`, error);
|
|
1143
|
+
|
|
1144
|
+
// Thử reconnect nếu cần
|
|
1145
|
+
try {
|
|
1146
|
+
await DatabaseManager.ensureDatabaseConnection(dbKey);
|
|
1147
|
+
console.log(`${dbKey}: Reconnected successfully`);
|
|
1148
|
+
} catch (reconnectError) {
|
|
1149
|
+
console.error(`${dbKey}: Failed to reconnect -`, reconnectError);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
267
1154
|
}
|
|
268
1155
|
```
|
|
269
1156
|
|
|
270
|
-
|
|
271
|
-
|
|
1157
|
+
## 7. Import/Export Dữ liệu
|
|
1158
|
+
|
|
1159
|
+
### Import từ CSV
|
|
272
1160
|
|
|
273
1161
|
```typescript
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
1162
|
+
// services/DataImportService.ts
|
|
1163
|
+
import { DatabaseManager, ImportResult, ColumnMapping } from '@dqcai/sqlite';
|
|
1164
|
+
|
|
1165
|
+
export class DataImportService {
|
|
1166
|
+
|
|
1167
|
+
// Import users từ CSV
|
|
1168
|
+
static async importUsersFromCSV(csvData: string): Promise<ImportResult> {
|
|
1169
|
+
try {
|
|
1170
|
+
const result = await DatabaseManager.importFromCSV(
|
|
1171
|
+
'users', // database key
|
|
1172
|
+
'users', // table name
|
|
1173
|
+
csvData,
|
|
1174
|
+
{
|
|
1175
|
+
hasHeader: true,
|
|
1176
|
+
delimiter: ',',
|
|
1177
|
+
skipErrors: false,
|
|
1178
|
+
validateData: true,
|
|
1179
|
+
batchSize: 500,
|
|
1180
|
+
onProgress: (processed, total) => {
|
|
1181
|
+
console.log(`Import progress: ${processed}/${total}`);
|
|
1182
|
+
},
|
|
1183
|
+
onError: (error, rowIndex, rowData) => {
|
|
1184
|
+
console.error(`Row ${rowIndex} error:`, error, rowData);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
console.log('Import completed:', {
|
|
1190
|
+
total: result.totalRows,
|
|
1191
|
+
success: result.successRows,
|
|
1192
|
+
errors: result.errorRows,
|
|
1193
|
+
time: result.executionTime
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
return result;
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
console.error('Import failed:', error);
|
|
1199
|
+
throw error;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Import với column mapping
|
|
1204
|
+
static async importUsersWithMapping(
|
|
1205
|
+
data: Record<string, any>[],
|
|
1206
|
+
columnMappings: ColumnMapping[]
|
|
1207
|
+
): Promise<ImportResult> {
|
|
1208
|
+
try {
|
|
1209
|
+
const result = await DatabaseManager.importDataWithMapping(
|
|
1210
|
+
'users',
|
|
1211
|
+
'users',
|
|
1212
|
+
data,
|
|
1213
|
+
columnMappings,
|
|
1214
|
+
{
|
|
1215
|
+
batchSize: 1000,
|
|
1216
|
+
skipErrors: true,
|
|
1217
|
+
updateOnConflict: true,
|
|
1218
|
+
conflictColumns: ['email']
|
|
1219
|
+
}
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
return result;
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
console.error('Import with mapping failed:', error);
|
|
1225
|
+
throw error;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Bulk import nhiều bảng cùng lúc
|
|
1230
|
+
static async bulkImportData(importConfigs: Array<{
|
|
1231
|
+
databaseKey: string;
|
|
1232
|
+
tableName: string;
|
|
1233
|
+
data: Record<string, any>[];
|
|
1234
|
+
}>): Promise<void> {
|
|
1235
|
+
try {
|
|
1236
|
+
const result = await DatabaseManager.bulkImport(importConfigs);
|
|
1237
|
+
|
|
1238
|
+
console.log('Bulk import completed:', {
|
|
1239
|
+
totalDatabases: result.totalDatabases,
|
|
1240
|
+
successDatabases: result.successDatabases,
|
|
1241
|
+
executionTime: result.executionTime
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
// Log chi tiết từng bảng
|
|
1245
|
+
Object.entries(result.results).forEach(([key, importResult]) => {
|
|
1246
|
+
console.log(`${key}: ${importResult.successRows}/${importResult.totalRows} rows imported`);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// Log lỗi nếu có
|
|
1250
|
+
if (Object.keys(result.errors).length > 0) {
|
|
1251
|
+
console.error('Import errors:', result.errors);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
console.error('Bulk import failed:', error);
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
277
1258
|
}
|
|
278
1259
|
}
|
|
279
1260
|
|
|
280
|
-
|
|
281
|
-
|
|
1261
|
+
// Ví dụ sử dụng import service
|
|
1262
|
+
async function exampleImportUsage() {
|
|
1263
|
+
// CSV data mẫu
|
|
1264
|
+
const csvData = `username,email,password,first_name,last_name
|
|
1265
|
+
john_doe,john@example.com,password123,John,Doe
|
|
1266
|
+
jane_smith,jane@example.com,password456,Jane,Smith
|
|
1267
|
+
bob_wilson,bob@example.com,password789,Bob,Wilson`;
|
|
1268
|
+
|
|
1269
|
+
try {
|
|
1270
|
+
// Import từ CSV
|
|
1271
|
+
const importResult = await DataImportService.importUsersFromCSV(csvData);
|
|
1272
|
+
console.log('CSV Import result:', importResult);
|
|
1273
|
+
|
|
1274
|
+
// Import với column mapping
|
|
1275
|
+
const mappedData = [
|
|
1276
|
+
{ user_name: 'alice', user_email: 'alice@test.com', pwd: 'pass123' },
|
|
1277
|
+
{ user_name: 'charlie', user_email: 'charlie@test.com', pwd: 'pass456' }
|
|
1278
|
+
];
|
|
1279
|
+
|
|
1280
|
+
const columnMappings: ColumnMapping[] = [
|
|
1281
|
+
{ sourceColumn: 'user_name', targetColumn: 'username' },
|
|
1282
|
+
{ sourceColumn: 'user_email', targetColumn: 'email' },
|
|
1283
|
+
{
|
|
1284
|
+
sourceColumn: 'pwd',
|
|
1285
|
+
targetColumn: 'password',
|
|
1286
|
+
transform: (value) => `hashed_${value}` // Hash password
|
|
1287
|
+
}
|
|
1288
|
+
];
|
|
1289
|
+
|
|
1290
|
+
const mappingResult = await DataImportService.importUsersWithMapping(
|
|
1291
|
+
mappedData,
|
|
1292
|
+
columnMappings
|
|
1293
|
+
);
|
|
1294
|
+
console.log('Mapping Import result:', mappingResult);
|
|
1295
|
+
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
console.error('Import example failed:', error);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
282
1300
|
```
|
|
283
1301
|
|
|
284
|
-
###
|
|
285
|
-
Sử dụng DatabaseManager để quản lý role.
|
|
1302
|
+
### Export dữ liệu
|
|
286
1303
|
|
|
287
1304
|
```typescript
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
1305
|
+
// services/DataExportService.ts
|
|
1306
|
+
import { DatabaseManager } from '@dqcai/sqlite';
|
|
1307
|
+
|
|
1308
|
+
export class DataExportService {
|
|
1309
|
+
|
|
1310
|
+
// Export users ra CSV
|
|
1311
|
+
static async exportUsersToCSV(): Promise<string> {
|
|
1312
|
+
try {
|
|
1313
|
+
const dao = DatabaseManager.get('users');
|
|
1314
|
+
const sql = `
|
|
1315
|
+
SELECT u.username, u.email, u.created_at,
|
|
1316
|
+
p.first_name, p.last_name, p.phone
|
|
1317
|
+
FROM users u
|
|
1318
|
+
LEFT JOIN profiles p ON u.id = p.user_id
|
|
1319
|
+
ORDER BY u.created_at DESC
|
|
1320
|
+
`;
|
|
1321
|
+
|
|
1322
|
+
const result = await dao.execute(sql);
|
|
1323
|
+
|
|
1324
|
+
if (result.rows.length === 0) {
|
|
1325
|
+
return 'No data to export';
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// Tạo CSV header
|
|
1329
|
+
const headers = Object.keys(result.rows[0]);
|
|
1330
|
+
let csvContent = headers.join(',') + '\n';
|
|
1331
|
+
|
|
1332
|
+
// Thêm data rows
|
|
1333
|
+
result.rows.forEach(row => {
|
|
1334
|
+
const values = headers.map(header => {
|
|
1335
|
+
const value = row[header];
|
|
1336
|
+
// Escape quotes và wrap trong quotes nếu chứa comma
|
|
1337
|
+
if (value === null || value === undefined) {
|
|
1338
|
+
return '';
|
|
1339
|
+
}
|
|
1340
|
+
const stringValue = String(value);
|
|
1341
|
+
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
|
|
1342
|
+
return `"${stringValue.replace(/"/g, '""')}"`;
|
|
1343
|
+
}
|
|
1344
|
+
return stringValue;
|
|
1345
|
+
});
|
|
1346
|
+
csvContent += values.join(',') + '\n';
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
return csvContent;
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
console.error('Export to CSV failed:', error);
|
|
1352
|
+
throw error;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Export với điều kiện tùy chỉnh
|
|
1357
|
+
static async exportUsersWithConditions(
|
|
1358
|
+
whereClause?: string,
|
|
1359
|
+
params?: any[]
|
|
1360
|
+
): Promise<Record<string, any>[]> {
|
|
1361
|
+
try {
|
|
1362
|
+
const dao = DatabaseManager.get('users');
|
|
1363
|
+
let sql = `
|
|
1364
|
+
SELECT u.*, p.first_name, p.last_name, p.phone, p.address
|
|
1365
|
+
FROM users u
|
|
1366
|
+
LEFT JOIN profiles p ON u.id = p.user_id
|
|
1367
|
+
`;
|
|
1368
|
+
|
|
1369
|
+
if (whereClause) {
|
|
1370
|
+
sql += ` WHERE ${whereClause}`;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
sql += ` ORDER BY u.created_at DESC`;
|
|
1374
|
+
|
|
1375
|
+
const result = await dao.execute(sql, params || []);
|
|
1376
|
+
return result.rows;
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
console.error('Export with conditions failed:', error);
|
|
1379
|
+
throw error;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Export dữ liệu backup toàn bộ database
|
|
1384
|
+
static async createDatabaseBackup(dbKey: string): Promise<{
|
|
1385
|
+
tables: Record<string, any[]>;
|
|
1386
|
+
metadata: any;
|
|
1387
|
+
}> {
|
|
1388
|
+
try {
|
|
1389
|
+
const dao = DatabaseManager.get(dbKey);
|
|
1390
|
+
|
|
1391
|
+
// Lấy danh sách tables
|
|
1392
|
+
const tablesResult = await dao.execute(
|
|
1393
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
1394
|
+
);
|
|
1395
|
+
|
|
1396
|
+
const backup: Record<string, any[]> = {};
|
|
1397
|
+
|
|
1398
|
+
// Export từng table
|
|
1399
|
+
for (const tableRow of tablesResult.rows) {
|
|
1400
|
+
const tableName = tableRow.name;
|
|
1401
|
+
const dataResult = await dao.execute(`SELECT * FROM ${tableName}`);
|
|
1402
|
+
backup[tableName] = dataResult.rows;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Thêm metadata
|
|
1406
|
+
const dbInfo = await dao.getDatabaseInfo();
|
|
1407
|
+
|
|
1408
|
+
return {
|
|
1409
|
+
tables: backup,
|
|
1410
|
+
metadata: {
|
|
1411
|
+
...dbInfo,
|
|
1412
|
+
exportDate: new Date().toISOString(),
|
|
1413
|
+
version: await dao.getSchemaVersion()
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
console.error('Database backup failed:', error);
|
|
1418
|
+
throw error;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Ví dụ sử dụng export
|
|
1424
|
+
async function exampleExportUsage() {
|
|
1425
|
+
try {
|
|
1426
|
+
// Export users ra CSV
|
|
1427
|
+
const csvContent = await DataExportService.exportUsersToCSV();
|
|
1428
|
+
console.log('CSV Export:', csvContent);
|
|
1429
|
+
|
|
1430
|
+
// Export users với điều kiện
|
|
1431
|
+
const recentUsers = await DataExportService.exportUsersWithConditions(
|
|
1432
|
+
"u.created_at > ?",
|
|
1433
|
+
[new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()] // 30 ngày gần đây
|
|
1434
|
+
);
|
|
1435
|
+
console.log('Recent users:', recentUsers);
|
|
1436
|
+
|
|
1437
|
+
// Backup toàn bộ database users
|
|
1438
|
+
const backup = await DataExportService.createDatabaseBackup('users');
|
|
1439
|
+
console.log('Database backup:', backup);
|
|
1440
|
+
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
console.error('Export example failed:', error);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
291
1445
|
```
|
|
292
1446
|
|
|
293
|
-
##
|
|
1447
|
+
## 8. Best Practices & Tips
|
|
294
1448
|
|
|
295
|
-
###
|
|
1449
|
+
### Error Handling
|
|
296
1450
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
1451
|
+
```typescript
|
|
1452
|
+
// utils/ErrorHandler.ts
|
|
1453
|
+
export class DatabaseErrorHandler {
|
|
1454
|
+
|
|
1455
|
+
static handleServiceError(error: any, context: string): void {
|
|
1456
|
+
console.error(`Database Error in ${context}:`, {
|
|
1457
|
+
message: error.message,
|
|
1458
|
+
stack: error.stack,
|
|
1459
|
+
code: error.code,
|
|
1460
|
+
timestamp: new Date().toISOString()
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// Specific error handling
|
|
1464
|
+
if (error.message?.includes('UNIQUE constraint failed')) {
|
|
1465
|
+
throw new Error('Duplicate entry detected');
|
|
1466
|
+
} else if (error.message?.includes('database is locked')) {
|
|
1467
|
+
throw new Error('Database is busy, please try again');
|
|
1468
|
+
} else if (error.message?.includes('no such table')) {
|
|
1469
|
+
throw new Error('Database table not found');
|
|
1470
|
+
} else {
|
|
1471
|
+
throw new Error(`Database operation failed: ${error.message}`);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
301
1474
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
1475
|
+
static async withRetry<T>(
|
|
1476
|
+
operation: () => Promise<T>,
|
|
1477
|
+
maxRetries: number = 3,
|
|
1478
|
+
delay: number = 1000
|
|
1479
|
+
): Promise<T> {
|
|
1480
|
+
let lastError: any;
|
|
1481
|
+
|
|
1482
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
1483
|
+
try {
|
|
1484
|
+
return await operation();
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
lastError = error;
|
|
1487
|
+
|
|
1488
|
+
if (i < maxRetries - 1) {
|
|
1489
|
+
console.log(`Operation failed, retrying in ${delay}ms... (${i + 1}/${maxRetries})`);
|
|
1490
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1491
|
+
delay *= 2; // Exponential backoff
|
|
310
1492
|
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
throw lastError;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
311
1499
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
(error) => reject(error)
|
|
350
|
-
);
|
|
351
|
-
});
|
|
1500
|
+
// Sử dụng error handler
|
|
1501
|
+
class SafeUserService extends UserService {
|
|
1502
|
+
async createUser(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User | null> {
|
|
1503
|
+
return DatabaseErrorHandler.withRetry(async () => {
|
|
1504
|
+
try {
|
|
1505
|
+
return await super.createUser(userData);
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
DatabaseErrorHandler.handleServiceError(error, 'createUser');
|
|
1508
|
+
throw error; // Re-throw sau khi handle
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
### Performance Optimization
|
|
1516
|
+
|
|
1517
|
+
```typescript
|
|
1518
|
+
// utils/PerformanceOptimizer.ts
|
|
1519
|
+
export class PerformanceOptimizer {
|
|
1520
|
+
|
|
1521
|
+
// Batch operations để giảm số lần gọi database
|
|
1522
|
+
static async batchCreateUsers(
|
|
1523
|
+
userService: UserService,
|
|
1524
|
+
users: Array<Omit<User, 'id' | 'created_at' | 'updated_at'>>
|
|
1525
|
+
): Promise<void> {
|
|
1526
|
+
const batchSize = 100;
|
|
1527
|
+
|
|
1528
|
+
for (let i = 0; i < users.length; i += batchSize) {
|
|
1529
|
+
const batch = users.slice(i, i + batchSize);
|
|
1530
|
+
|
|
1531
|
+
await userService.executeTransaction(async () => {
|
|
1532
|
+
for (const userData of batch) {
|
|
1533
|
+
await userService.create({
|
|
1534
|
+
...userData,
|
|
1535
|
+
created_at: new Date().toISOString()
|
|
1536
|
+
});
|
|
352
1537
|
}
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
console.log(`Processed batch ${Math.ceil((i + batchSize) / batchSize)}`);
|
|
353
1541
|
}
|
|
354
|
-
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Index optimization
|
|
1545
|
+
static async optimizeQueries(dao: any): Promise<void> {
|
|
1546
|
+
// Enable query optimization
|
|
1547
|
+
await dao.execute('PRAGMA optimize');
|
|
1548
|
+
|
|
1549
|
+
// Analyze tables for better query planning
|
|
1550
|
+
await dao.execute('ANALYZE');
|
|
1551
|
+
|
|
1552
|
+
// Set optimal cache size
|
|
1553
|
+
await dao.execute('PRAGMA cache_size = 10000');
|
|
355
1554
|
|
|
356
|
-
|
|
1555
|
+
console.log('Database optimization completed');
|
|
1556
|
+
}
|
|
357
1557
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1558
|
+
// Connection pooling check
|
|
1559
|
+
static monitorConnections(): void {
|
|
1560
|
+
setInterval(() => {
|
|
1561
|
+
const count = DatabaseManager.getConnectionCount();
|
|
1562
|
+
const connections = DatabaseManager.listConnections();
|
|
1563
|
+
|
|
1564
|
+
console.log(`Active connections: ${count}`, connections);
|
|
1565
|
+
|
|
1566
|
+
if (count > 5) {
|
|
1567
|
+
console.warn('High number of database connections detected');
|
|
1568
|
+
}
|
|
1569
|
+
}, 30000); // Check every 30 seconds
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
```
|
|
363
1573
|
|
|
364
|
-
|
|
365
|
-
// Đăng ký adapter để thư viện biết cách kết nối
|
|
366
|
-
DatabaseFactory.registerAdapter(new ReactNativeAdapter()); // Đăng ký adapter cho môi trường
|
|
1574
|
+
### Testing Utilities
|
|
367
1575
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1576
|
+
```typescript
|
|
1577
|
+
// utils/TestHelpers.ts
|
|
1578
|
+
import { DatabaseManager } from '@dqcai/sqlite';
|
|
1579
|
+
|
|
1580
|
+
export class DatabaseTestHelpers {
|
|
1581
|
+
|
|
1582
|
+
static async setupTestDatabase(): Promise<void> {
|
|
1583
|
+
// Use in-memory database for testing
|
|
1584
|
+
const testSchema = {
|
|
1585
|
+
...userSchema,
|
|
1586
|
+
database_name: ':memory:'
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
DatabaseManager.registerSchema('test_users', testSchema);
|
|
1590
|
+
await DatabaseManager.initLazySchema(['test_users']);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
static async cleanupTestData(service: any): Promise<void> {
|
|
1594
|
+
await service.truncate();
|
|
1595
|
+
}
|
|
371
1596
|
|
|
372
|
-
|
|
373
|
-
|
|
1597
|
+
static async seedTestData(userService: UserService): Promise<User[]> {
|
|
1598
|
+
const testUsers = [
|
|
1599
|
+
{ username: 'test1', email: 'test1@example.com', password: 'pass1' },
|
|
1600
|
+
{ username: 'test2', email: 'test2@example.com', password: 'pass2' },
|
|
1601
|
+
{ username: 'test3', email: 'test3@example.com', password: 'pass3' }
|
|
1602
|
+
];
|
|
1603
|
+
|
|
1604
|
+
const createdUsers: User[] = [];
|
|
1605
|
+
for (const userData of testUsers) {
|
|
1606
|
+
const user = await userService.createUser(userData);
|
|
1607
|
+
if (user) createdUsers.push(user);
|
|
374
1608
|
}
|
|
375
1609
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const userToInsert = {
|
|
380
|
-
name: 'John Doe',
|
|
381
|
-
email: 'john.doe@example.com',
|
|
382
|
-
};
|
|
383
|
-
const insertResult = await dao.insert({
|
|
384
|
-
name: 'users',
|
|
385
|
-
cols: Object.entries(userToInsert).map(([name, value]) => ({ name, value })),
|
|
386
|
-
});
|
|
387
|
-
const newUserId = insertResult.lastInsertRowId;
|
|
388
|
-
console.log(`User inserted with ID: ${newUserId}`);
|
|
389
|
-
|
|
390
|
-
// 2. READ (Select)
|
|
391
|
-
console.log('Selecting user by email...');
|
|
392
|
-
const selectedUser = await dao.select({
|
|
393
|
-
name: 'users',
|
|
394
|
-
cols: [], // Chọn tất cả các cột
|
|
395
|
-
wheres: [{ name: 'email', value: 'john.doe@example.com' }],
|
|
396
|
-
});
|
|
397
|
-
console.log('Selected user:', selectedUser);
|
|
398
|
-
|
|
399
|
-
// 3. UPDATE
|
|
400
|
-
console.log('Updating user name...');
|
|
401
|
-
await dao.update({
|
|
402
|
-
name: 'users',
|
|
403
|
-
cols: [{ name: 'name', value: 'John Smith' }],
|
|
404
|
-
wheres: [{ name: 'id', value: newUserId }],
|
|
405
|
-
});
|
|
406
|
-
const updatedUser = await dao.select({
|
|
407
|
-
name: 'users',
|
|
408
|
-
cols: ['id', 'name'],
|
|
409
|
-
wheres: [{ name: 'id', value: newUserId }],
|
|
410
|
-
});
|
|
411
|
-
console.log('Updated user:', updatedUser);
|
|
1610
|
+
return createdUsers;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
412
1613
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
1614
|
+
// Example test
|
|
1615
|
+
describe('UserService Tests', () => {
|
|
1616
|
+
let userService: UserService;
|
|
1617
|
+
|
|
1618
|
+
beforeAll(async () => {
|
|
1619
|
+
await DatabaseTestHelpers.setupTestDatabase();
|
|
1620
|
+
userService = new UserService();
|
|
1621
|
+
await userService.init();
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
beforeEach(async () => {
|
|
1625
|
+
await DatabaseTestHelpers.cleanupTestData(userService);
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
afterAll(async () => {
|
|
1629
|
+
await DatabaseManager.closeAll();
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
test('should create user successfully', async () => {
|
|
1633
|
+
const userData = {
|
|
1634
|
+
username: 'testuser',
|
|
1635
|
+
email: 'test@example.com',
|
|
1636
|
+
password: 'password123'
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
const user = await userService.createUser(userData);
|
|
1640
|
+
expect(user).toBeTruthy();
|
|
1641
|
+
expect(user?.username).toBe(userData.username);
|
|
1642
|
+
expect(user?.email).toBe(userData.email);
|
|
1643
|
+
});
|
|
1644
|
+
});
|
|
1645
|
+
```
|
|
420
1646
|
|
|
421
|
-
|
|
422
|
-
name: 'users',
|
|
423
|
-
wheres: [{ name: 'id', value: newUserId }],
|
|
424
|
-
});
|
|
425
|
-
console.log('User exists after delete?', Object.keys(checkUser).length > 0);
|
|
426
|
-
}
|
|
1647
|
+
## 9. Troubleshooting Common Issues
|
|
427
1648
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1649
|
+
### Database Locked
|
|
1650
|
+
|
|
1651
|
+
```typescript
|
|
1652
|
+
// Giải quyết database locked
|
|
1653
|
+
const handleDatabaseLocked = async () => {
|
|
1654
|
+
try {
|
|
1655
|
+
// Enable WAL mode để tránh lock
|
|
1656
|
+
const dao = DatabaseManager.get('users');
|
|
1657
|
+
await dao.execute('PRAGMA journal_mode = WAL');
|
|
1658
|
+
await dao.execute('PRAGMA busy_timeout = 30000'); // 30 giây timeout
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
console.error('Error setting WAL mode:', error);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
### Connection Issues
|
|
1666
|
+
|
|
1667
|
+
```typescript
|
|
1668
|
+
// Kiểm tra và khôi phục kết nối
|
|
1669
|
+
const ensureConnectionHealth = async (dbKey: string) => {
|
|
1670
|
+
try {
|
|
1671
|
+
const dao = DatabaseManager.get(dbKey);
|
|
1672
|
+
await dao.execute('SELECT 1');
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
console.log(`Connection ${dbKey} unhealthy, reconnecting...`);
|
|
1675
|
+
await DatabaseManager.closeConnection(dbKey);
|
|
1676
|
+
await DatabaseManager.getLazyLoading(dbKey);
|
|
1677
|
+
console.log(`Connection ${dbKey} restored`);
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
## 10. Migration & Schema Updates
|
|
1683
|
+
|
|
1684
|
+
```typescript
|
|
1685
|
+
// migrations/001_add_user_status.ts
|
|
1686
|
+
import { UniversalDAO } from '@dqcai/sqlite';
|
|
1687
|
+
|
|
1688
|
+
export const migration_001 = {
|
|
1689
|
+
version: '1.0.1',
|
|
1690
|
+
description: 'Add status column to users table',
|
|
1691
|
+
|
|
1692
|
+
async up(dao: UniversalDAO): Promise<void> {
|
|
1693
|
+
await dao.execute(`
|
|
1694
|
+
ALTER TABLE users
|
|
1695
|
+
ADD COLUMN status VARCHAR(20) DEFAULT 'active'
|
|
1696
|
+
`);
|
|
1697
|
+
|
|
1698
|
+
await dao.execute(`
|
|
1699
|
+
CREATE INDEX idx_user_status ON users(status)
|
|
1700
|
+
`);
|
|
1701
|
+
},
|
|
1702
|
+
|
|
1703
|
+
async down(dao: UniversalDAO): Promise<void> {
|
|
1704
|
+
await dao.execute(`
|
|
1705
|
+
DROP INDEX IF EXISTS idx_user_status
|
|
1706
|
+
`);
|
|
1707
|
+
|
|
1708
|
+
// SQLite không hỗ trợ DROP COLUMN, cần recreate table
|
|
1709
|
+
await dao.execute(`
|
|
1710
|
+
CREATE TABLE users_backup AS
|
|
1711
|
+
SELECT id, username, email, password, created_at, updated_at
|
|
1712
|
+
FROM users
|
|
1713
|
+
`);
|
|
1714
|
+
|
|
1715
|
+
await dao.execute(`DROP TABLE users`);
|
|
1716
|
+
|
|
1717
|
+
await dao.execute(`
|
|
1718
|
+
CREATE TABLE users (
|
|
1719
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1720
|
+
username VARCHAR(50) UNIQUE NOT NULL,
|
|
1721
|
+
email VARCHAR(100) UNIQUE NOT NULL,
|
|
1722
|
+
password VARCHAR(255) NOT NULL,
|
|
1723
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
1724
|
+
updated_at DATETIME
|
|
1725
|
+
)
|
|
1726
|
+
`);
|
|
1727
|
+
|
|
1728
|
+
await dao.execute(`
|
|
1729
|
+
INSERT INTO users SELECT * FROM users_backup
|
|
1730
|
+
`);
|
|
1731
|
+
|
|
1732
|
+
await dao.execute(`DROP TABLE users_backup`);
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
// Chạy migration
|
|
1737
|
+
const runMigration = async () => {
|
|
1738
|
+
const dao = DatabaseManager.get('users');
|
|
1739
|
+
const currentVersion = await dao.getSchemaVersion();
|
|
1740
|
+
|
|
1741
|
+
if (currentVersion < '1.0.1') {
|
|
1742
|
+
await migration_001.up(dao);
|
|
1743
|
+
await dao.setSchemaVersion('1.0.1');
|
|
1744
|
+
console.log('Migration 001 completed');
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
## Kết luận
|
|
1750
|
+
|
|
1751
|
+
Universal SQLite cung cấp một giải pháp mạnh mẽ và linh hoạt để quản lý cơ sở dữ liệu SQLite across platforms. Với DatabaseManager và BaseService, bạn có thể:
|
|
1752
|
+
|
|
1753
|
+
- Dễ dàng quản lý nhiều database connections
|
|
1754
|
+
- Thực hiện CRUD operations một cách type-safe
|
|
1755
|
+
- Import/Export dữ liệu hiệu quả
|
|
1756
|
+
- Quản lý schema và migrations
|
|
1757
|
+
- Handle errors và performance optimization
|
|
439
1758
|
|
|
440
|
-
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### Example for Node.js
|
|
444
|
-
|
|
445
|
-
1. Cài đặt dependencies:
|
|
446
|
-
```bash
|
|
447
|
-
npm install better-sqlite3
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
2. Khai báo Adapter (tự detect, nhưng có thể tùy chỉnh):
|
|
451
|
-
```typescript
|
|
452
|
-
class NodeAdapter extends BaseAdapter {
|
|
453
|
-
async connect(path: string) {
|
|
454
|
-
const SQLite3 = require('better-sqlite3');
|
|
455
|
-
const db = new SQLite3(path);
|
|
456
|
-
return {
|
|
457
|
-
execute: (sql: string, params = []) => {
|
|
458
|
-
const stmt = db.prepare(sql);
|
|
459
|
-
const result = stmt.run(params);
|
|
460
|
-
return { rows: stmt.all(params), rowsAffected: result.changes };
|
|
461
|
-
},
|
|
462
|
-
close: () => db.close(),
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
isSupported() { return true; }
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
DatabaseFactory.registerAdapter(new NodeAdapter());
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
3. Tạo và Mở Database, CRUD:
|
|
472
|
-
```typescript
|
|
473
|
-
import UniversalSQLite from '@dqcai/sqlite';
|
|
474
|
-
|
|
475
|
-
const sqlite = UniversalSQLite.getInstance();
|
|
476
|
-
|
|
477
|
-
async function nodeExample() {
|
|
478
|
-
await sqlite.initializeFromSchema(exampleSchema);
|
|
479
|
-
const dao = await sqlite.connect('example_db');
|
|
480
|
-
|
|
481
|
-
// CRUD
|
|
482
|
-
await dao.insert({ name: 'users', cols: [{ name: 'name', value: 'NodeUser' }, { name: 'email', value: 'node@example.com' }] });
|
|
483
|
-
const users = await dao.selectAll({ name: 'users' });
|
|
484
|
-
console.log('Users:', users);
|
|
485
|
-
|
|
486
|
-
// Query Builder
|
|
487
|
-
const qb = sqlite.query('users');
|
|
488
|
-
const results = await qb.select('*').where('name', 'LIKE', '%User%').get();
|
|
489
|
-
|
|
490
|
-
// Migration
|
|
491
|
-
const migrationManager = sqlite.createMigrationManager();
|
|
492
|
-
migrationManager.addMigration({
|
|
493
|
-
version: '1.0.1',
|
|
494
|
-
description: 'Add age',
|
|
495
|
-
up: async (dao) => await dao.execute('ALTER TABLE users ADD COLUMN age INTEGER'),
|
|
496
|
-
down: async (dao) => await dao.execute('ALTER TABLE users DROP COLUMN age'),
|
|
497
|
-
});
|
|
498
|
-
await migrationManager.migrate();
|
|
499
|
-
|
|
500
|
-
// Import from File
|
|
501
|
-
const importer = sqlite.createCSVImporter();
|
|
502
|
-
await importer.importFromFile('users', './data.csv', { hasHeader: true });
|
|
503
|
-
|
|
504
|
-
await sqlite.closeAll();
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
nodeExample();
|
|
508
|
-
```
|
|
1759
|
+
Thư viện hỗ trợ tốt cho cả React Native và Node.js, giúp bạn xây dựng ứng dụng database-driven một cách nhất quán và maintainable.
|
|
509
1760
|
|
|
510
1761
|
## API Reference
|
|
511
1762
|
|