@seip/blue-bird 1.0.2 → 1.1.1
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/.env_example +26 -12
- package/AGENTS.md +98 -26
- package/README.md +80 -23
- package/core/app.js +42 -0
- package/core/cache.js +62 -30
- package/core/cli/docker.js +236 -59
- package/core/cli/init.js +135 -9
- package/core/database.js +205 -20
- package/core/hash.js +201 -0
- package/core/index.d.ts +194 -137
- package/core/upload.js +83 -57
- package/core/ws.js +227 -210
- package/docker/docker-compose.sqlite.yml +69 -0
- package/frontend/js/utils.js +557 -557
- package/package.json +68 -66
package/core/cli/init.js
CHANGED
|
@@ -28,8 +28,9 @@ class ProjectInit {
|
|
|
28
28
|
let title = "Blue-Bird";
|
|
29
29
|
let port = 3000;
|
|
30
30
|
let appUrl = "http://localhost:3000";
|
|
31
|
-
let dbType = "
|
|
31
|
+
let dbType = "sqlite";
|
|
32
32
|
let dbName = "blue_bird";
|
|
33
|
+
let dbFile = "database/blue_bird.db";
|
|
33
34
|
let dbUser = "root";
|
|
34
35
|
let dbPassword = "root";
|
|
35
36
|
let dbPort = 3306;
|
|
@@ -55,11 +56,17 @@ class ProjectInit {
|
|
|
55
56
|
appUrl = await ask("Application URL", defaultAppUrl);
|
|
56
57
|
|
|
57
58
|
const dbTypeAns = await ask(
|
|
58
|
-
"Which database do you want to configure? (
|
|
59
|
-
"
|
|
59
|
+
"Which database do you want to configure? (sqlite / mysql / postgres / none)",
|
|
60
|
+
"sqlite",
|
|
60
61
|
);
|
|
61
62
|
const cleanDbTypeAns = dbTypeAns.toLowerCase().trim();
|
|
62
63
|
if (
|
|
64
|
+
cleanDbTypeAns === "sqlite" ||
|
|
65
|
+
cleanDbTypeAns === "sql" ||
|
|
66
|
+
cleanDbTypeAns === "sqlite3"
|
|
67
|
+
) {
|
|
68
|
+
dbType = "sqlite";
|
|
69
|
+
} else if (
|
|
63
70
|
cleanDbTypeAns === "postgres" ||
|
|
64
71
|
cleanDbTypeAns === "postgresql" ||
|
|
65
72
|
cleanDbTypeAns === "pg" ||
|
|
@@ -72,7 +79,15 @@ class ProjectInit {
|
|
|
72
79
|
dbType = "none";
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
if (dbType
|
|
82
|
+
if (dbType === "sqlite") {
|
|
83
|
+
dbName = await ask("Database Name", dbName);
|
|
84
|
+
const defaultDbFile = `database/${dbName}.db`;
|
|
85
|
+
dbFile = await ask("Database File Path", defaultDbFile);
|
|
86
|
+
const dbDir = path.dirname(path.join(this.appDir, dbFile));
|
|
87
|
+
if (!fs.existsSync(dbDir)) {
|
|
88
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
} else if (dbType !== "none") {
|
|
76
91
|
dbName = await ask("Database Name", dbName);
|
|
77
92
|
dbUser = await ask(
|
|
78
93
|
"Database User",
|
|
@@ -103,8 +118,6 @@ class ProjectInit {
|
|
|
103
118
|
"docker",
|
|
104
119
|
".env_example",
|
|
105
120
|
"AGENTS.md",
|
|
106
|
-
"index.js",
|
|
107
|
-
".gitignore"
|
|
108
121
|
];
|
|
109
122
|
|
|
110
123
|
try {
|
|
@@ -128,12 +141,32 @@ class ProjectInit {
|
|
|
128
141
|
}
|
|
129
142
|
});
|
|
130
143
|
|
|
144
|
+
const gitignoreContent = `node_modules\nlogs\n.env\npackage-lock.json\n*.db\n*.sqlite\n*.sqlite-wal\n*.sqlite-shm\n*.db-wal\n*.db-shm\ntest/\n\nbackups/*.sql\nbackups/*.db\n`;
|
|
145
|
+
const gitignoreDest = path.join(this.appDir, ".gitignore");
|
|
146
|
+
const gitignoreSrc = path.join(this.sourceDir, ".gitignore");
|
|
147
|
+
|
|
148
|
+
if (!fs.existsSync(gitignoreDest)) {
|
|
149
|
+
if (fs.existsSync(gitignoreSrc)) {
|
|
150
|
+
fs.copyFileSync(gitignoreSrc, gitignoreDest);
|
|
151
|
+
console.log(chalk.green("[OK] Copied .gitignore to root."));
|
|
152
|
+
} else {
|
|
153
|
+
fs.writeFileSync(gitignoreDest, gitignoreContent, "utf-8");
|
|
154
|
+
console.log(chalk.green("[OK] Created .gitignore file."));
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
console.log(
|
|
158
|
+
chalk.yellow("[SKIP] .gitignore already exists, skipping."),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
131
162
|
const composeTemplateName =
|
|
132
163
|
dbType === "postgres"
|
|
133
164
|
? "docker-compose.postgres.yml"
|
|
134
165
|
: dbType === "mysql"
|
|
135
166
|
? "docker-compose.mysql.yml"
|
|
136
|
-
: "
|
|
167
|
+
: dbType === "sqlite"
|
|
168
|
+
? "docker-compose.sqlite.yml"
|
|
169
|
+
: "docker-compose.none.yml";
|
|
137
170
|
const composeSrc = path.join(
|
|
138
171
|
this.sourceDir,
|
|
139
172
|
"docker",
|
|
@@ -166,8 +199,15 @@ class ProjectInit {
|
|
|
166
199
|
let envContent = fs.readFileSync(envExamplePath, "utf-8");
|
|
167
200
|
|
|
168
201
|
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
202
|
+
const composeProjectName =
|
|
203
|
+
title
|
|
204
|
+
.toLowerCase()
|
|
205
|
+
.trim()
|
|
206
|
+
.replace(/\s+/g, "-")
|
|
207
|
+
.replace(/[^a-z0-9_-]/g, "") || "blue-bird";
|
|
169
208
|
|
|
170
209
|
const updates = {
|
|
210
|
+
COMPOSE_PROJECT_NAME: composeProjectName,
|
|
171
211
|
TITLE: title,
|
|
172
212
|
PORT: port,
|
|
173
213
|
APP_URL: appUrl,
|
|
@@ -175,13 +215,19 @@ class ProjectInit {
|
|
|
175
215
|
DB_TYPE: dbType,
|
|
176
216
|
};
|
|
177
217
|
|
|
178
|
-
if (dbType === "
|
|
218
|
+
if (dbType === "sqlite") {
|
|
219
|
+
updates.DB_TYPE = "sqlite";
|
|
220
|
+
updates.DB_FILE = dbFile;
|
|
221
|
+
updates.DATABASE_URL = `sqlite:${dbFile}`;
|
|
222
|
+
} else if (dbType === "mysql") {
|
|
223
|
+
updates.DB_TYPE = "mysql";
|
|
179
224
|
updates.DB_NAME = dbName;
|
|
180
225
|
updates.DB_USER = dbUser;
|
|
181
226
|
updates.DB_PASSWORD = dbPassword;
|
|
182
227
|
updates.DB_PORT = dbPort;
|
|
183
228
|
updates.DATABASE_URL = `mysql://${dbUser}:${dbPassword}@localhost:${dbPort}/${dbName}`;
|
|
184
229
|
} else if (dbType === "postgres") {
|
|
230
|
+
updates.DB_TYPE = "postgres";
|
|
185
231
|
updates.DB_NAME = dbName;
|
|
186
232
|
updates.DB_USER = dbUser;
|
|
187
233
|
updates.DB_PASSWORD = dbPassword;
|
|
@@ -190,10 +236,14 @@ class ProjectInit {
|
|
|
190
236
|
}
|
|
191
237
|
|
|
192
238
|
const lines = envContent.split(/\r?\n/);
|
|
239
|
+
let foundComposeProjectName = false;
|
|
193
240
|
const updatedLines = lines.map((line) => {
|
|
194
241
|
const match = line.match(/^([A-Z_]+)=(.+)/);
|
|
195
242
|
if (match) {
|
|
196
243
|
const key = match[1];
|
|
244
|
+
if (key === "COMPOSE_PROJECT_NAME") {
|
|
245
|
+
foundComposeProjectName = true;
|
|
246
|
+
}
|
|
197
247
|
if (updates[key] !== undefined) {
|
|
198
248
|
const value = updates[key];
|
|
199
249
|
if (typeof value === "string" && !value.startsWith('"')) {
|
|
@@ -204,6 +254,21 @@ class ProjectInit {
|
|
|
204
254
|
}
|
|
205
255
|
return line;
|
|
206
256
|
});
|
|
257
|
+
|
|
258
|
+
if (!foundComposeProjectName && updates.COMPOSE_PROJECT_NAME) {
|
|
259
|
+
const titleIdx = updatedLines.findIndex((l) => l.startsWith("TITLE="));
|
|
260
|
+
if (titleIdx !== -1) {
|
|
261
|
+
updatedLines.splice(
|
|
262
|
+
titleIdx,
|
|
263
|
+
0,
|
|
264
|
+
`COMPOSE_PROJECT_NAME="${updates.COMPOSE_PROJECT_NAME}"`,
|
|
265
|
+
);
|
|
266
|
+
} else {
|
|
267
|
+
updatedLines.push(
|
|
268
|
+
`COMPOSE_PROJECT_NAME="${updates.COMPOSE_PROJECT_NAME}"`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
207
272
|
envContent = updatedLines.join("\n");
|
|
208
273
|
|
|
209
274
|
fs.writeFileSync(envPath, envContent, "utf-8");
|
|
@@ -218,7 +283,9 @@ class ProjectInit {
|
|
|
218
283
|
);
|
|
219
284
|
try {
|
|
220
285
|
let packagesToInstall = ["redis"];
|
|
221
|
-
if (dbType === "
|
|
286
|
+
if (dbType === "sqlite") {
|
|
287
|
+
packagesToInstall.push("better-sqlite3");
|
|
288
|
+
} else if (dbType === "postgres") {
|
|
222
289
|
packagesToInstall.push("pg");
|
|
223
290
|
} else {
|
|
224
291
|
packagesToInstall.push("mysql2");
|
|
@@ -323,6 +390,63 @@ class ProjectInit {
|
|
|
323
390
|
}
|
|
324
391
|
}
|
|
325
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Handles adding on-demand modules and dependencies.
|
|
395
|
+
* @param {string} [feature]
|
|
396
|
+
*/
|
|
397
|
+
function addCommand(feature) {
|
|
398
|
+
if (!feature) {
|
|
399
|
+
console.log(chalk.yellow("Usage: npx blue-bird add <feature>"));
|
|
400
|
+
console.log("Available features to add:");
|
|
401
|
+
console.log(" upload | multer - Installs multer for file uploads");
|
|
402
|
+
console.log(" ws | websocket - Installs ws for WebSockets");
|
|
403
|
+
console.log(" redis - Installs redis for distributed caching and sessions");
|
|
404
|
+
console.log(" sqlite - Installs better-sqlite3 for local SQLite database");
|
|
405
|
+
console.log(" mysql - Installs mysql2 for MySQL database");
|
|
406
|
+
console.log(" postgres | pg - Installs pg for PostgreSQL database");
|
|
407
|
+
console.log(" bcrypt - Installs bcrypt for password hashing");
|
|
408
|
+
console.log(" swagger - Installs swagger-ui-express");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const cleanFeature = feature.toLowerCase().trim();
|
|
413
|
+
const packageMap = {
|
|
414
|
+
upload: "multer",
|
|
415
|
+
multer: "multer",
|
|
416
|
+
ws: "ws",
|
|
417
|
+
websocket: "ws",
|
|
418
|
+
websockets: "ws",
|
|
419
|
+
redis: "redis",
|
|
420
|
+
sqlite: "better-sqlite3",
|
|
421
|
+
"better-sqlite3": "better-sqlite3",
|
|
422
|
+
mysql: "mysql2",
|
|
423
|
+
mysql2: "mysql2",
|
|
424
|
+
postgres: "pg",
|
|
425
|
+
postgresql: "pg",
|
|
426
|
+
pg: "pg",
|
|
427
|
+
bcrypt: "bcrypt",
|
|
428
|
+
swagger: "swagger-ui-express",
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const pkgName = packageMap[cleanFeature];
|
|
432
|
+
if (!pkgName) {
|
|
433
|
+
console.error(chalk.red(`[ERROR] Unknown feature '${feature}'.`));
|
|
434
|
+
console.log("Available features: upload, ws, redis, sqlite, mysql, postgres, bcrypt, swagger");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
console.log(chalk.cyan(`[INFO] Installing ${pkgName}...`));
|
|
439
|
+
try {
|
|
440
|
+
execSync(`npm install ${pkgName}`, {
|
|
441
|
+
stdio: "inherit",
|
|
442
|
+
cwd: process.cwd(),
|
|
443
|
+
});
|
|
444
|
+
console.log(chalk.green(`[OK] Successfully installed ${pkgName}.`));
|
|
445
|
+
} catch (err) {
|
|
446
|
+
console.error(chalk.red(`[ERROR] Failed to install ${pkgName}:`), err.message);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
326
450
|
const initializer = new ProjectInit();
|
|
327
451
|
|
|
328
452
|
const args = process.argv.slice(2);
|
|
@@ -331,4 +455,6 @@ const command = args[0];
|
|
|
331
455
|
if (command === "route") import("./route.js");
|
|
332
456
|
else if (command === "swagger-install") import("./swagger.js");
|
|
333
457
|
else if (command === "docker") import("./docker.js");
|
|
458
|
+
else if (command === "add") addCommand(args[1]);
|
|
334
459
|
else initializer.run();
|
|
460
|
+
|
package/core/database.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import crypto from "node:crypto";
|
|
2
4
|
import { getRedisClient } from "./cache.js";
|
|
3
5
|
|
|
@@ -15,14 +17,20 @@ if (
|
|
|
15
17
|
DB_TYPE = "postgres";
|
|
16
18
|
} else if (process.env.DATABASE_URL.startsWith("mysql://")) {
|
|
17
19
|
DB_TYPE = "mysql";
|
|
20
|
+
} else if (
|
|
21
|
+
process.env.DATABASE_URL.startsWith("sqlite://") ||
|
|
22
|
+
process.env.DATABASE_URL.startsWith("sqlite:")
|
|
23
|
+
) {
|
|
24
|
+
DB_TYPE = "sqlite";
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
27
|
if (!DB_TYPE) {
|
|
21
|
-
DB_TYPE = "
|
|
28
|
+
DB_TYPE = "sqlite";
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
let mysqlPromise = null;
|
|
25
32
|
let pgPromise = null;
|
|
33
|
+
let sqlitePromise = null;
|
|
26
34
|
|
|
27
35
|
if (DB_TYPE === "postgres") {
|
|
28
36
|
try {
|
|
@@ -40,23 +48,79 @@ if (DB_TYPE === "postgres") {
|
|
|
40
48
|
"[DATABASE ERROR] mysql2 package is not installed. Database wrapper is disabled.",
|
|
41
49
|
);
|
|
42
50
|
}
|
|
51
|
+
} else if (DB_TYPE === "sqlite") {
|
|
52
|
+
try {
|
|
53
|
+
sqlitePromise = await import("better-sqlite3");
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(
|
|
56
|
+
"[DATABASE ERROR] better-sqlite3 package is not installed. Database wrapper is disabled.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
/**
|
|
46
|
-
* Database class wrapping mysql2 and pg with reconnection retries, connection pooling, and query caching.
|
|
62
|
+
* Database class wrapping better-sqlite3, mysql2, and pg with reconnection retries, connection pooling, and query caching.
|
|
47
63
|
*/
|
|
48
64
|
class Database {
|
|
49
65
|
/**
|
|
50
66
|
* Initializes config from DATABASE_URL or DB_* environment variables.
|
|
51
|
-
* For default Database use .env DB_HOST, DB_USER, DB_PASSWORD...
|
|
52
|
-
* @param {number} [connectionLimit=10] - Maximum number of connections in the pool.
|
|
53
|
-
* @param {number} [queueLimit=0] - Maximum number of queued connections.
|
|
54
|
-
* @param {Object} [config={}] - Additional configuration options: DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT, DB_TYPE.
|
|
67
|
+
* For default Database use .env DB_HOST, DB_USER, DB_PASSWORD... or DB_FILE for SQLite.
|
|
68
|
+
* @param {number} [connectionLimit=10] - Maximum number of connections in the pool (MySQL/Postgres).
|
|
69
|
+
* @param {number} [queueLimit=0] - Maximum number of queued connections (MySQL/Postgres).
|
|
70
|
+
* @param {Object} [config={}] - Additional configuration options: DB_FILE, DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT, DB_TYPE.
|
|
71
|
+
* @example const connection = new Database(10, 0, { DB_TYPE: "sqlite", DB_FILE: "database/blue_bird.db" });
|
|
55
72
|
* @example const connection = new Database(10, 0, { DB_HOST: "localhost", DB_USER: "root", DB_PASSWORD: "password", DB_NAME: "blue_bird", DB_PORT: 3306, DB_TYPE: "mysql" });
|
|
56
73
|
*/
|
|
57
74
|
constructor(connectionLimit = 10, queueLimit = 0, config = {}) {
|
|
58
75
|
this.pool = null;
|
|
59
|
-
this.
|
|
76
|
+
this.db = null;
|
|
77
|
+
this.type = config.DB_TYPE || DB_TYPE;
|
|
78
|
+
|
|
79
|
+
let defaultSqliteFile = process.env.DB_FILE || "database/blue_bird.db";
|
|
80
|
+
let busyTimeout = 5000;
|
|
81
|
+
let journalMode = "WAL";
|
|
82
|
+
let synchronous = "NORMAL";
|
|
83
|
+
|
|
84
|
+
if (process.env.DATABASE_URL && !process.env.DATABASE_URL.startsWith("#")) {
|
|
85
|
+
try {
|
|
86
|
+
if (
|
|
87
|
+
process.env.DATABASE_URL.startsWith("sqlite://") ||
|
|
88
|
+
process.env.DATABASE_URL.startsWith("sqlite:")
|
|
89
|
+
) {
|
|
90
|
+
const rawUrl = process.env.DATABASE_URL;
|
|
91
|
+
const cleanUrl = rawUrl.replace(/^sqlite:\/\/|^sqlite:/, "");
|
|
92
|
+
const [filePath, queryStr] = cleanUrl.split("?");
|
|
93
|
+
if (filePath) {
|
|
94
|
+
defaultSqliteFile = filePath;
|
|
95
|
+
}
|
|
96
|
+
if (queryStr) {
|
|
97
|
+
const params = new URLSearchParams(queryStr);
|
|
98
|
+
if (params.has("busy_timeout")) {
|
|
99
|
+
busyTimeout =
|
|
100
|
+
parseInt(params.get("busy_timeout"), 10) || busyTimeout;
|
|
101
|
+
}
|
|
102
|
+
if (params.has("journal_mode")) {
|
|
103
|
+
journalMode = params.get("journal_mode").toUpperCase();
|
|
104
|
+
}
|
|
105
|
+
if (params.has("synchronous")) {
|
|
106
|
+
synchronous = params.get("synchronous").toUpperCase();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(
|
|
112
|
+
"[DATABASE ERROR] Failed to parse SQLite DATABASE_URL:",
|
|
113
|
+
err.message,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.sqliteConfig = {
|
|
119
|
+
filename: config.DB_FILE || defaultSqliteFile,
|
|
120
|
+
busyTimeout: config.busyTimeout || busyTimeout,
|
|
121
|
+
journalMode: config.journalMode || journalMode,
|
|
122
|
+
synchronous: config.synchronous || synchronous,
|
|
123
|
+
};
|
|
60
124
|
|
|
61
125
|
this.config = {
|
|
62
126
|
...config,
|
|
@@ -101,13 +165,46 @@ class Database {
|
|
|
101
165
|
}
|
|
102
166
|
|
|
103
167
|
/**
|
|
104
|
-
* Creates the database connection pool
|
|
168
|
+
* Creates the database connection pool or SQLite instance with retries on failure.
|
|
105
169
|
* @param {number} [retries=3] - Number of connection attempts.
|
|
106
|
-
* @returns {Promise<boolean>} True if connection
|
|
170
|
+
* @returns {Promise<boolean>} True if connection was created.
|
|
107
171
|
*/
|
|
108
172
|
async init(retries = 3) {
|
|
109
|
-
if (!mysqlPromise && !pgPromise) return false;
|
|
110
|
-
if (this.pool) return true;
|
|
173
|
+
if (!mysqlPromise && !pgPromise && !sqlitePromise) return false;
|
|
174
|
+
if (this.pool || this.db) return true;
|
|
175
|
+
|
|
176
|
+
if (this.type === "sqlite" && sqlitePromise) {
|
|
177
|
+
try {
|
|
178
|
+
const BetterSqlite = sqlitePromise.default || sqlitePromise;
|
|
179
|
+
const dbFilePath = path.isAbsolute(this.sqliteConfig.filename)
|
|
180
|
+
? this.sqliteConfig.filename
|
|
181
|
+
: path.resolve(process.cwd(), this.sqliteConfig.filename);
|
|
182
|
+
|
|
183
|
+
const dir = path.dirname(dbFilePath);
|
|
184
|
+
if (!fs.existsSync(dir)) {
|
|
185
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.db = new BetterSqlite(dbFilePath, {
|
|
189
|
+
timeout: this.sqliteConfig.busyTimeout,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
this.db.pragma(`journal_mode = ${this.sqliteConfig.journalMode}`);
|
|
193
|
+
this.db.pragma(`synchronous = ${this.sqliteConfig.synchronous}`);
|
|
194
|
+
this.db.pragma("foreign_keys = ON");
|
|
195
|
+
this.db.pragma(`busy_timeout = ${this.sqliteConfig.busyTimeout}`);
|
|
196
|
+
this.db.pragma("temp_store = MEMORY");
|
|
197
|
+
|
|
198
|
+
return true;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error(
|
|
201
|
+
"[DATABASE ERROR] Failed to initialize SQLite database:",
|
|
202
|
+
err.message,
|
|
203
|
+
);
|
|
204
|
+
this.db = null;
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
111
208
|
|
|
112
209
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
113
210
|
try {
|
|
@@ -138,7 +235,7 @@ class Database {
|
|
|
138
235
|
|
|
139
236
|
/**
|
|
140
237
|
* Runs a SQL query with parameters and formatting options.
|
|
141
|
-
* Supports
|
|
238
|
+
* Supports SQLite, MySQL, and PostgreSQL (converting ? to $1, $2 for Postgres automatically).
|
|
142
239
|
*
|
|
143
240
|
* @param {string} sql - SQL query string.
|
|
144
241
|
* @param {Array} [params=[]] - Query parameter array.
|
|
@@ -150,8 +247,8 @@ class Database {
|
|
|
150
247
|
* @example const insert_id = await connection.query("INSERT INTO users (name, email, password) VALUES (?, ?, ?)", ["John Doe", "john@example.com", "password"]);
|
|
151
248
|
*/
|
|
152
249
|
async query(sql, params = [], options = {}) {
|
|
153
|
-
if (!mysqlPromise && !pgPromise) return false;
|
|
154
|
-
if (!this.pool) {
|
|
250
|
+
if (!mysqlPromise && !pgPromise && !sqlitePromise) return false;
|
|
251
|
+
if (!this.pool && !this.db) {
|
|
155
252
|
const initialized = await this.init();
|
|
156
253
|
if (!initialized) return false;
|
|
157
254
|
}
|
|
@@ -159,8 +256,8 @@ class Database {
|
|
|
159
256
|
const queryOptions =
|
|
160
257
|
typeof options === "string" ? { [options]: true } : options;
|
|
161
258
|
const cleanSql = sql.trim();
|
|
162
|
-
const isSelect =
|
|
163
|
-
const isInsert =
|
|
259
|
+
const isSelect = /^(select|pragma|explain)/i.test(cleanSql);
|
|
260
|
+
const isInsert = /^insert/i.test(cleanSql);
|
|
164
261
|
|
|
165
262
|
const redisClient = getRedisClient();
|
|
166
263
|
let cacheKey = null;
|
|
@@ -201,7 +298,42 @@ class Database {
|
|
|
201
298
|
}
|
|
202
299
|
|
|
203
300
|
try {
|
|
204
|
-
if (this.type === "
|
|
301
|
+
if (this.type === "sqlite" && this.db) {
|
|
302
|
+
const stmt = this.db.prepare(cleanSql);
|
|
303
|
+
|
|
304
|
+
if (isSelect) {
|
|
305
|
+
if (queryOptions.return_row) {
|
|
306
|
+
const row = stmt.get(...params);
|
|
307
|
+
const result = row || null;
|
|
308
|
+
if (cacheKey && queryOptions.cache && redisClient) {
|
|
309
|
+
await redisClient
|
|
310
|
+
.set(cacheKey, JSON.stringify(result), {
|
|
311
|
+
EX: parseInt(queryOptions.cache),
|
|
312
|
+
})
|
|
313
|
+
.catch(() => {});
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const rows = stmt.all(...params);
|
|
319
|
+
if (cacheKey && queryOptions.cache && redisClient) {
|
|
320
|
+
await redisClient
|
|
321
|
+
.set(cacheKey, JSON.stringify(rows), {
|
|
322
|
+
EX: parseInt(queryOptions.cache),
|
|
323
|
+
})
|
|
324
|
+
.catch(() => {});
|
|
325
|
+
}
|
|
326
|
+
return rows;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (isInsert) {
|
|
330
|
+
const info = stmt.run(...params);
|
|
331
|
+
return Number(info.lastInsertRowid);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const info = stmt.run(...params);
|
|
335
|
+
return info.changes;
|
|
336
|
+
} else if (this.type === "postgres" && pgPromise) {
|
|
205
337
|
let paramIndex = 1;
|
|
206
338
|
const pgSql = cleanSql.replace(/\?/g, () => `$${paramIndex++}`);
|
|
207
339
|
const res = await this.pool.query(pgSql, params);
|
|
@@ -305,13 +437,48 @@ class Database {
|
|
|
305
437
|
* });
|
|
306
438
|
*/
|
|
307
439
|
async transaction(callback) {
|
|
308
|
-
if (!mysqlPromise && !pgPromise) throw new Error("[DATABASE ERROR] No database driver available.");
|
|
309
|
-
if (!this.pool) {
|
|
440
|
+
if (!mysqlPromise && !pgPromise && !sqlitePromise) throw new Error("[DATABASE ERROR] No database driver available.");
|
|
441
|
+
if (!this.pool && !this.db) {
|
|
310
442
|
const initialized = await this.init();
|
|
311
443
|
if (!initialized) throw new Error("[DATABASE ERROR] Failed to initialize database pool.");
|
|
312
444
|
}
|
|
313
445
|
|
|
314
|
-
if (this.type === "
|
|
446
|
+
if (this.type === "sqlite" && this.db) {
|
|
447
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
448
|
+
try {
|
|
449
|
+
const tx = {
|
|
450
|
+
query: async (sql, params = [], options = {}) => {
|
|
451
|
+
const queryOptions = typeof options === "string" ? { [options]: true } : options;
|
|
452
|
+
const cleanSql = sql.trim();
|
|
453
|
+
const isSelect = /^(select|pragma|explain)/i.test(cleanSql);
|
|
454
|
+
const isInsert = /^insert/i.test(cleanSql);
|
|
455
|
+
|
|
456
|
+
const stmt = this.db.prepare(cleanSql);
|
|
457
|
+
if (isSelect) {
|
|
458
|
+
if (queryOptions.return_row) {
|
|
459
|
+
const row = stmt.get(...params);
|
|
460
|
+
return row || null;
|
|
461
|
+
}
|
|
462
|
+
return stmt.all(...params);
|
|
463
|
+
}
|
|
464
|
+
if (isInsert) {
|
|
465
|
+
const info = stmt.run(...params);
|
|
466
|
+
return Number(info.lastInsertRowid);
|
|
467
|
+
}
|
|
468
|
+
const info = stmt.run(...params);
|
|
469
|
+
return info.changes;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const result = await callback(tx);
|
|
474
|
+
this.db.exec("COMMIT");
|
|
475
|
+
return result;
|
|
476
|
+
} catch (err) {
|
|
477
|
+
this.db.exec("ROLLBACK");
|
|
478
|
+
console.error("[DATABASE ERROR] Transaction rolled back:", err.message);
|
|
479
|
+
throw err;
|
|
480
|
+
}
|
|
481
|
+
} else if (this.type === "postgres" && pgPromise) {
|
|
315
482
|
const client = await this.pool.connect();
|
|
316
483
|
try {
|
|
317
484
|
await client.query("BEGIN");
|
|
@@ -394,6 +561,24 @@ class Database {
|
|
|
394
561
|
async executeTransaction(callback) {
|
|
395
562
|
return this.transaction(callback);
|
|
396
563
|
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Closes the database connection pool or SQLite instance.
|
|
567
|
+
*/
|
|
568
|
+
async close() {
|
|
569
|
+
if (this.db) {
|
|
570
|
+
this.db.close();
|
|
571
|
+
this.db = null;
|
|
572
|
+
}
|
|
573
|
+
if (this.pool) {
|
|
574
|
+
if (typeof this.pool.end === "function") {
|
|
575
|
+
await this.pool.end();
|
|
576
|
+
}
|
|
577
|
+
this.pool = null;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
397
580
|
}
|
|
398
581
|
|
|
399
582
|
export { Database, DB_TYPE };
|
|
583
|
+
|
|
584
|
+
|