@seip/blue-bird 1.1.0 → 1.1.2

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/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 = "mysql";
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.type = DB_TYPE;
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 with 3 retry attempts on failure.
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 pool was created.
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 both MySQL and PostgreSQL (converting ? to $1, $2 for Postgres automatically).
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 = cleanSql.toLowerCase().startsWith("select");
163
- const isInsert = cleanSql.toLowerCase().startsWith("insert");
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 === "postgres" && pgPromise) {
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 === "postgres" && pgPromise) {
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
+
package/core/hash.js ADDED
@@ -0,0 +1,201 @@
1
+ import crypto from "node:crypto";
2
+
3
+ let bcryptModule = null;
4
+ try {
5
+ bcryptModule = (await import("bcrypt")).default || (await import("bcrypt"));
6
+ } catch {
7
+ // bcrypt not installed, scrypt native is used
8
+ }
9
+
10
+ /**
11
+ * High-performance Password Hashing class.
12
+ * Uses native node:crypto scrypt by default (zero npm dependencies, NIST recommended),
13
+ * with seamless support for bcrypt when installed or verifying bcrypt hashes.
14
+ */
15
+ class Hash {
16
+ /**
17
+ * Hashes a plain text password using scrypt (default) or bcrypt.
18
+ * @param {string} password - The plain text password.
19
+ * @param {Object} [options={}] - Hashing options.
20
+ * @param {string} [options.driver="scrypt"] - Hashing driver ('scrypt' or 'bcrypt').
21
+ * @param {number} [options.rounds=10] - Salt rounds for bcrypt (if driver is 'bcrypt').
22
+ * @param {number} [options.N=16384] - CPU/memory cost parameter for scrypt.
23
+ * @param {number} [options.r=8] - Block size for scrypt.
24
+ * @param {number} [options.p=1] - Parallelization parameter for scrypt.
25
+ * @returns {Promise<string>} Formatted password hash string.
26
+ * @example
27
+ * const hash = await Hash.make("mySecretPassword");
28
+ * // Using bcrypt:
29
+ * const bcryptHash = await Hash.make("mySecretPassword", { driver: "bcrypt" });
30
+ */
31
+ static async make(password, options = {}) {
32
+ if (typeof password !== "string" || !password) {
33
+ throw new Error("[HASH ERROR] Password must be a non-empty string.");
34
+ }
35
+
36
+ const driver = (options.driver || "scrypt").toLowerCase();
37
+
38
+ if (driver === "bcrypt") {
39
+ if (!bcryptModule) {
40
+ throw new Error(
41
+ "[HASH ERROR] 'bcrypt' package is not installed. Run 'npm install bcrypt' or 'npx blue-bird add bcrypt', or use the default scrypt driver.",
42
+ );
43
+ }
44
+ const rounds = options.rounds || 10;
45
+ return bcryptModule.hash(password, rounds);
46
+ }
47
+
48
+ // Default: native scrypt with random salt
49
+ const N = options.N || 16384;
50
+ const r = options.r || 8;
51
+ const p = options.p || 1;
52
+ const keylen = 64;
53
+ const salt = crypto.randomBytes(16);
54
+
55
+ return new Promise((resolve, reject) => {
56
+ crypto.scrypt(
57
+ password,
58
+ salt,
59
+ keylen,
60
+ { N, r, p, maxmem: 32 * 1024 * 1024 },
61
+ (err, derivedKey) => {
62
+ if (err) return reject(err);
63
+ const hashString = `$scrypt$N=${N},r=${r},p=${p}$${salt.toString("hex")}$${derivedKey.toString("hex")}`;
64
+ resolve(hashString);
65
+ },
66
+ );
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Alias for make().
72
+ * @param {string} password - The plain text password.
73
+ * @param {Object} [options={}] - Options.
74
+ * @returns {Promise<string>}
75
+ */
76
+ static async hash(password, options = {}) {
77
+ return this.make(password, options);
78
+ }
79
+
80
+ /**
81
+ * Verifies a plain text password against a hash string.
82
+ * Automatically detects scrypt or bcrypt hash formats.
83
+ * Uses timing-safe comparison to protect against side-channel attacks.
84
+ *
85
+ * @param {string} password - The plain text password to check.
86
+ * @param {string} hash - The stored hash string.
87
+ * @returns {Promise<boolean>} True if password matches, false otherwise.
88
+ * @example
89
+ * const isValid = await Hash.verify("mySecretPassword", storedHash);
90
+ */
91
+ static async verify(password, hash) {
92
+ if (
93
+ typeof password !== "string" ||
94
+ !password ||
95
+ typeof hash !== "string" ||
96
+ !hash
97
+ ) {
98
+ return false;
99
+ }
100
+
101
+ // 1. Detect bcrypt hash format ($2a$, $2b$, $2y$)
102
+ if (/^\$2[aby]\$\d{2}\$/.test(hash)) {
103
+ if (!bcryptModule) {
104
+ console.error(
105
+ "[HASH ERROR] A bcrypt hash was detected, but the 'bcrypt' package is not installed. Run 'npm install bcrypt' or 'npx blue-bird add bcrypt'.",
106
+ );
107
+ return false;
108
+ }
109
+ try {
110
+ return await bcryptModule.compare(password, hash);
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ // 2. Detect native scrypt format ($scrypt$N=...,r=...,p=...$salt$hash)
117
+ if (hash.startsWith("$scrypt$")) {
118
+ const parts = hash.split("$");
119
+ // Format: ["", "scrypt", "N=16384,r=8,p=1", "saltHex", "hashHex"]
120
+ if (parts.length !== 5) return false;
121
+
122
+ const paramsStr = parts[2];
123
+ const saltHex = parts[3];
124
+ const originalHashHex = parts[4];
125
+
126
+ if (!paramsStr || !saltHex || !originalHashHex) return false;
127
+
128
+ let N = 16384,
129
+ r = 8,
130
+ p = 1;
131
+ paramsStr.split(",").forEach((param) => {
132
+ const [k, v] = param.split("=");
133
+ if (k === "N") N = parseInt(v, 10) || N;
134
+ if (k === "r") r = parseInt(v, 10) || r;
135
+ if (k === "p") p = parseInt(v, 10) || p;
136
+ });
137
+
138
+ const salt = Buffer.from(saltHex, "hex");
139
+ const originalHash = Buffer.from(originalHashHex, "hex");
140
+
141
+ return new Promise((resolve) => {
142
+ crypto.scrypt(
143
+ password,
144
+ salt,
145
+ originalHash.length,
146
+ { N, r, p, maxmem: 32 * 1024 * 1024 },
147
+ (err, derivedKey) => {
148
+ if (err) return resolve(false);
149
+ try {
150
+ const matches = crypto.timingSafeEqual(originalHash, derivedKey);
151
+ resolve(matches);
152
+ } catch {
153
+ resolve(false);
154
+ }
155
+ },
156
+ );
157
+ });
158
+ }
159
+
160
+ return false;
161
+ }
162
+
163
+ /**
164
+ * Alias for verify().
165
+ * @param {string} password - The plain text password.
166
+ * @param {string} hash - The stored hash string.
167
+ * @returns {Promise<boolean>}
168
+ */
169
+ static async check(password, hash) {
170
+ return this.verify(password, hash);
171
+ }
172
+
173
+ /**
174
+ * Checks if a given hash needs to be rehashed to match updated security parameters.
175
+ * @param {string} hash - The stored hash string.
176
+ * @param {Object} [options={}] - Target options.
177
+ * @returns {boolean} True if the hash should be regenerated.
178
+ */
179
+ static needsRehash(hash, options = {}) {
180
+ if (!hash || typeof hash !== "string") return true;
181
+ const targetDriver = (options.driver || "scrypt").toLowerCase();
182
+
183
+ if (targetDriver === "bcrypt") {
184
+ return !/^\$2[aby]\$\d{2}\$/.test(hash);
185
+ }
186
+
187
+ if (!hash.startsWith("$scrypt$")) return true;
188
+
189
+ const parts = hash.split("$");
190
+ if (parts.length !== 5) return true;
191
+
192
+ const paramsStr = parts[2];
193
+ const targetN = options.N || 16384;
194
+ const targetR = options.r || 8;
195
+ const targetP = options.p || 1;
196
+
197
+ return !paramsStr.includes(`N=${targetN},r=${targetR},p=${targetP}`);
198
+ }
199
+ }
200
+
201
+ export default Hash;
package/core/index.d.ts CHANGED
@@ -11,6 +11,52 @@ declare global {
11
11
  */
12
12
  success(data?: any, message?: string, statusCode?: number): Response;
13
13
 
14
+ /**
15
+ * Sends a standardized HTTP 200 OK success response.
16
+ * @param data Data payload.
17
+ * @param message Success message.
18
+ */
19
+ ok(data?: any, message?: string): Response;
20
+
21
+ /**
22
+ * Sends a standardized HTTP 201 Created success response.
23
+ * @param data Data payload.
24
+ * @param message Success message.
25
+ */
26
+ created(data?: any, message?: string): Response;
27
+
28
+ /**
29
+ * Sends a standardized HTTP 400 Bad Request error response.
30
+ * @param message Error message.
31
+ * @param errors Detailed errors array or object.
32
+ */
33
+ badRequest(message?: string, errors?: any): Response;
34
+
35
+ /**
36
+ * Sends a standardized HTTP 401 Unauthorized error response.
37
+ * @param message Error message.
38
+ */
39
+ unauthorized(message?: string): Response;
40
+
41
+ /**
42
+ * Sends a standardized HTTP 403 Forbidden error response.
43
+ * @param message Error message.
44
+ */
45
+ forbidden(message?: string): Response;
46
+
47
+ /**
48
+ * Sends a standardized HTTP 404 Not Found error response.
49
+ * @param message Error message.
50
+ */
51
+ notFound(message?: string): Response;
52
+
53
+ /**
54
+ * Sends a standardized HTTP 500 Internal Server Error response.
55
+ * @param message Error message.
56
+ * @param errors Error details.
57
+ */
58
+ serverError(message?: string, errors?: any): Response;
59
+
14
60
  /**
15
61
  * Sends a standardized JSON error response.
16
62
  * @param message Error message.
@@ -100,6 +146,14 @@ export class Validator {
100
146
  validate(data: Record<string, any>): { valid: boolean; errors: any[] };
101
147
  }
102
148
 
149
+ export class Hash {
150
+ static make(password: string, options?: { driver?: "scrypt" | "bcrypt"; rounds?: number; N?: number; r?: number; p?: number }): Promise<string>;
151
+ static hash(password: string, options?: { driver?: "scrypt" | "bcrypt"; rounds?: number; N?: number; r?: number; p?: number }): Promise<string>;
152
+ static verify(password: string, hash: string): Promise<boolean>;
153
+ static check(password: string, hash: string): Promise<boolean>;
154
+ static needsRehash(hash: string, options?: { driver?: "scrypt" | "bcrypt"; N?: number; r?: number; p?: number }): boolean;
155
+ }
156
+
103
157
  export class Auth {
104
158
  static encrypt(payload: any, secret: string): string;
105
159
  static decrypt(data: string, secret: string): any;
@@ -117,6 +171,7 @@ export class Cache {
117
171
  static delete(keys: string | string[]): Promise<boolean>;
118
172
  static del(keys: string | string[]): Promise<boolean>;
119
173
  static clear(): Promise<boolean>;
174
+ static getMode(): string;
120
175
  }
121
176
 
122
177
  export function getRedisClient(): any;
@@ -132,6 +187,8 @@ export class Database {
132
187
  ): Promise<{ data: any[]; total: number; page: number; limit: number; totalPages: number }>;
133
188
  transaction<T = any>(callback: (tx: { query: (sql: string, params?: any[], options?: any) => Promise<any> }) => Promise<T>): Promise<T>;
134
189
  executeTransaction<T = any>(callback: (tx: { query: (sql: string, params?: any[], options?: any) => Promise<any> }) => Promise<T>): Promise<T>;
190
+ close(): Promise<void>;
135
191
  }
136
192
 
137
193
  export default App;
194
+