@seip/blue-bird 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.env_example +34 -36
  2. package/AGENTS.md +174 -241
  3. package/LICENSE +21 -21
  4. package/README.md +312 -343
  5. package/{index.js → backend/index.js} +22 -30
  6. package/backend/routes/api.js +57 -57
  7. package/core/app.js +338 -402
  8. package/core/auth.js +262 -256
  9. package/core/cache.js +174 -174
  10. package/core/cli/docker.js +488 -370
  11. package/core/cli/init.js +332 -238
  12. package/core/cli/route.js +42 -42
  13. package/core/config.js +52 -52
  14. package/core/database.js +263 -182
  15. package/core/debug.js +248 -248
  16. package/core/logger.js +115 -115
  17. package/core/middleware.js +27 -27
  18. package/core/router.js +144 -144
  19. package/core/swagger.js +40 -40
  20. package/core/upload.js +77 -77
  21. package/core/validate.js +380 -380
  22. package/docker/Dockerfile +16 -16
  23. package/docker/docker-compose.dev.yml +6 -0
  24. package/docker/docker-compose.mysql.yml +92 -0
  25. package/docker/docker-compose.none.yml +68 -0
  26. package/docker/docker-compose.postgres.yml +93 -0
  27. package/docker/nginx.conf +98 -106
  28. package/docker-compose.yml +92 -93
  29. package/frontend/about.html +98 -0
  30. package/frontend/css/app.css +0 -0
  31. package/frontend/favicon.ico +0 -0
  32. package/frontend/index.html +141 -0
  33. package/frontend/js/bundle.js +8 -0
  34. package/package.json +64 -72
  35. package/backend/logs/2026-07-14/info.log +0 -48
  36. package/frontend/astro.config.mjs +0 -35
  37. package/frontend/public/css/app.css +0 -319
  38. package/frontend/public/favicon.ico +0 -0
  39. package/frontend/src/http/api.js +0 -29
  40. package/frontend/src/layouts/Layout.astro +0 -20
  41. package/frontend/src/pages/about.astro +0 -54
  42. package/frontend/src/pages/index.astro +0 -110
package/core/config.js CHANGED
@@ -1,52 +1,52 @@
1
- import path from "path";
2
-
3
- let _cachedProps = null;
4
-
5
- /**
6
- * Configuration class to manage application-wide settings and environment variables.
7
- */
8
- class Config {
9
- /**
10
- * Returns the base directory of the application.
11
- * @returns {string} The current working directory.
12
- */
13
- static dirname() {
14
- return process.cwd();
15
- }
16
-
17
- /**
18
- * Retrieves application properties from environment variables or default values.
19
- * Results are cached after first call for performance.
20
- * @returns {{debug: boolean, descriptionMeta: string, keywordsMeta: string, titleMeta: string, authorMeta: string, description: string, title: string, version: string, langMeta: string, host: string, appUrl: string, port: number, static: {path: string, options: Object}}} The configuration properties object.
21
- * @example
22
- * const props = Config.props();
23
- * console.log(props);
24
- */
25
- static props() {
26
- if (_cachedProps) return _cachedProps;
27
-
28
- const portRaw = parseInt(process.env.PORT);
29
-
30
- _cachedProps = {
31
- debug: process.env.DEBUG === "true",
32
- descriptionMeta: process.env.DESCRIPTION_META || "",
33
- keywordsMeta: process.env.KEYWORDS_META || "",
34
- titleMeta: process.env.TITLE_META || "",
35
- authorMeta: process.env.AUTHOR_META || "",
36
- description: process.env.DESCRIPTION || "",
37
- title: process.env.TITLE || "",
38
- version: process.env.VERSION || "1.0.0",
39
- langMeta: process.env.LANGMETA || "en",
40
- host: process.env.HOST || "http://localhost",
41
- appUrl: process.env.APP_URL || process.env.HOST || "http://localhost",
42
- port: Number.isNaN(portRaw) ? 3000 : portRaw,
43
- jwtSecret: process.env.JWT_SECRET,
44
- static: {
45
- path: process.env.STATIC_PATH || "frontend/public",
46
- options: {},
47
- },
48
- };
49
- return _cachedProps;
50
- }
51
- }
52
- export default Config;
1
+ import path from "path";
2
+
3
+ let _cachedProps = null;
4
+
5
+ /**
6
+ * Configuration class to manage application-wide settings and environment variables.
7
+ */
8
+ class Config {
9
+ /**
10
+ * Returns the base directory of the application.
11
+ * @returns {string} The current working directory.
12
+ */
13
+ static dirname() {
14
+ return process.cwd();
15
+ }
16
+
17
+ /**
18
+ * Retrieves application properties from environment variables or default values.
19
+ * Results are cached after first call for performance.
20
+ * @returns {{debug: boolean, descriptionMeta: string, keywordsMeta: string, titleMeta: string, authorMeta: string, description: string, title: string, version: string, langMeta: string, host: string, appUrl: string, port: number, static: {path: string, options: Object}}} The configuration properties object.
21
+ * @example
22
+ * const props = Config.props();
23
+ * console.log(props);
24
+ */
25
+ static props() {
26
+ if (_cachedProps) return _cachedProps;
27
+
28
+ const portRaw = parseInt(process.env.PORT);
29
+
30
+ _cachedProps = {
31
+ debug: process.env.DEBUG === "true",
32
+ descriptionMeta: process.env.DESCRIPTION_META || "",
33
+ keywordsMeta: process.env.KEYWORDS_META || "",
34
+ titleMeta: process.env.TITLE_META || "",
35
+ authorMeta: process.env.AUTHOR_META || "",
36
+ description: process.env.DESCRIPTION || "",
37
+ title: process.env.TITLE || "",
38
+ version: process.env.VERSION || "1.0.0",
39
+ langMeta: process.env.LANGMETA || "en",
40
+ host: process.env.HOST || "http://localhost",
41
+ appUrl: process.env.APP_URL || process.env.HOST || "http://localhost",
42
+ port: Number.isNaN(portRaw) ? 3000 : portRaw,
43
+ jwtSecret: process.env.JWT_SECRET,
44
+ static: {
45
+ path: process.env.STATIC_PATH || "frontend/public",
46
+ options: {},
47
+ },
48
+ };
49
+ return _cachedProps;
50
+ }
51
+ }
52
+ export default Config;
package/core/database.js CHANGED
@@ -1,182 +1,263 @@
1
- import crypto from "node:crypto";
2
- import { getRedisClient } from "./cache.js";
3
-
4
- let mysqlPromise = null;
5
- try {
6
- mysqlPromise = await import("mysql2/promise");
7
- } catch (err) {
8
- console.error(
9
- "[DATABASE ERROR] mysql2 package is not installed. Database wrapper is disabled.",
10
- );
11
- }
12
-
13
- /**
14
- * Database class wrapping mysql2 with reconnection retries, connection pool, and query caching.
15
- */
16
- class Database {
17
- /**
18
- * Initializes config from DATABASE_URL or DB_* environment variables.
19
- */
20
- constructor(connectionLimit = 10, queueLimit = 0) {
21
- this.pool = null;
22
- this.config = {
23
- host: process.env.DB_HOST || "localhost",
24
- user: process.env.DB_USER || "root",
25
- password: process.env.DB_PASSWORD || "root",
26
- database: process.env.DB_NAME || "blue_bird",
27
- port: parseInt(process.env.DB_PORT) || 3306,
28
- charset: "utf8mb4",
29
- waitForConnections: true,
30
- connectionLimit: connectionLimit,
31
- queueLimit: queueLimit,
32
- };
33
-
34
- if (
35
- process.env.DATABASE_URL &&
36
- process.env.DATABASE_URL.startsWith("mysql://")
37
- ) {
38
- try {
39
- const url = new URL(process.env.DATABASE_URL);
40
- this.config.host = url.hostname;
41
- this.config.port = parseInt(url.port) || 3306;
42
- this.config.user = url.username;
43
- this.config.password = url.password;
44
- this.config.database = url.pathname.substring(1);
45
- } catch (err) {
46
- console.error(
47
- "[DATABASE ERROR] Failed to parse DATABASE_URL:",
48
- err.message,
49
- );
50
- }
51
- }
52
- }
53
-
54
- /**
55
- * Creates the MySQL connection pool with 3 retry attempts on failure.
56
- * @param {number} [retries=3] - Number of connection attempts.
57
- * @returns {Promise<boolean>} True if connection pool was created.
58
- */
59
- async init(retries = 3) {
60
- if (!mysqlPromise) return false;
61
- if (this.pool) return true;
62
-
63
- for (let attempt = 1; attempt <= retries; attempt++) {
64
- try {
65
- this.pool = mysqlPromise.createPool(this.config);
66
- await this.pool.query("SELECT 1");
67
- return true;
68
- } catch (err) {
69
- this.pool = null;
70
- if (attempt === retries) {
71
- console.error(
72
- `[DATABASE ERROR] Connection failed after ${retries} attempts:`,
73
- err.message,
74
- );
75
- return false;
76
- }
77
- await new Promise((resolve) => setTimeout(resolve, 1000));
78
- }
79
- }
80
- return false;
81
- }
82
-
83
- /**
84
- * Runs a SQL query with parameters and formatting options.
85
- *
86
- * @param {string} sql - SQL query string.
87
- * @param {Array} [params=[]] - Query parameter array.
88
- * @param {Object|string} [options={}] - Query options. Supports 'return_row', 'return_rows', and 'cache' (seconds).
89
- * @returns {Promise<*>| int | boolean} Formatted query result or false on error, or insert id of insert query.
90
- * @example select
91
- * const result = await connection.query("SELECT * FROM users", [], { return_row: true, cache: 60 });
92
- * @example insert
93
- * const result = await connection.query("INSERT INTO users (name, email, password) VALUES (?, ?, ?)", ["John Doe", "[EMAIL_ADDRESS]", "123456"]);
94
- * @example update
95
- * const result = await connection.query("UPDATE users SET name = ? WHERE id = ?", ["John Doe", 1]);
96
- * @example delete
97
- * const result = await connection.query("DELETE FROM users WHERE id = ?", [1]);
98
- */
99
- async query(sql, params = [], options = {}) {
100
- if (!mysqlPromise) return false;
101
- if (!this.pool) {
102
- const initialized = await this.init();
103
- if (!initialized) return false;
104
- }
105
-
106
- const queryOptions =
107
- typeof options === "string" ? { [options]: true } : options;
108
- const cleanSql = sql.trim();
109
- const isSelect = cleanSql.toLowerCase().startsWith("select");
110
- const isInsert = cleanSql.toLowerCase().startsWith("insert");
111
-
112
- const redisClient = getRedisClient();
113
- let cacheKey = null;
114
- const isDebug = queryOptions.debug ?? false;
115
- if (isDebug) {
116
- console.log("[DATABASE DEBUG] SQL:", sql);
117
- console.log("[DATABASE DEBUG] PARAMS:", params);
118
- console.log("[DATABASE DEBUG] OPTIONS:", options);
119
- }
120
-
121
- if (isSelect && queryOptions.cache && redisClient) {
122
- const hash = crypto
123
- .createHash("md5")
124
- .update(cleanSql + JSON.stringify(params))
125
- .digest("hex");
126
- cacheKey = `db:${hash}`;
127
- try {
128
- if (isDebug) {
129
- console.log("[DATABASE DEBUG ][Redis] CACHE KEY:", cacheKey);
130
- }
131
- const cached = await redisClient.get(cacheKey);
132
- if (cached) {
133
- if (isDebug) {
134
- console.log("[DATABASE DEBUG ][Redis] CACHE HIT");
135
- }
136
- return JSON.parse(cached);
137
- } else {
138
- if (isDebug) {
139
- console.log("[DATABASE DEBUG ][Redis] CACHE MISS");
140
- }
141
- }
142
- } catch (err) {
143
- console.error(
144
- "[DATABASE ERROR] Failed to get cached data:",
145
- err.message,
146
- );
147
- }
148
- }
149
-
150
- try {
151
- const [results] = await this.pool.execute(cleanSql, params);
152
-
153
- if (isSelect) {
154
- const rows = Array.isArray(results) ? results : [];
155
- if (cacheKey && queryOptions.cache && redisClient) {
156
- await redisClient
157
- .set(cacheKey, JSON.stringify(rows), {
158
- EX: parseInt(queryOptions.cache),
159
- })
160
- .catch(() => {});
161
- }
162
- if (queryOptions.return_row) {
163
- return rows.length > 0 ? rows[0] : null;
164
- }
165
- return rows;
166
- }
167
-
168
- if (isInsert) {
169
- return results.insertId || results;
170
- }
171
-
172
- return results;
173
- } catch (err) {
174
- console.error("[DATABASE ERROR] Query execution failed:", err.message);
175
- throw err;
176
- }
177
- }
178
- }
179
-
180
- const connection = new Database();
181
- export default connection;
182
- export { Database };
1
+ import crypto from "node:crypto";
2
+ import { getRedisClient } from "./cache.js";
3
+
4
+ let DB_TYPE = (process.env.DB_TYPE || "").toLowerCase();
5
+
6
+ if (
7
+ !DB_TYPE &&
8
+ process.env.DATABASE_URL &&
9
+ !process.env.DATABASE_URL.startsWith("#")
10
+ ) {
11
+ if (
12
+ process.env.DATABASE_URL.startsWith("postgres://") ||
13
+ process.env.DATABASE_URL.startsWith("postgresql://")
14
+ ) {
15
+ DB_TYPE = "postgres";
16
+ } else if (process.env.DATABASE_URL.startsWith("mysql://")) {
17
+ DB_TYPE = "mysql";
18
+ }
19
+ }
20
+ if (!DB_TYPE) {
21
+ DB_TYPE = "mysql";
22
+ }
23
+
24
+ let mysqlPromise = null;
25
+ let pgPromise = null;
26
+
27
+ if (DB_TYPE === "postgres") {
28
+ try {
29
+ pgPromise = await import("pg");
30
+ } catch (err) {
31
+ console.error(
32
+ "[DATABASE ERROR] pg package is not installed. Database wrapper is disabled.",
33
+ );
34
+ }
35
+ } else if (DB_TYPE === "mysql") {
36
+ try {
37
+ mysqlPromise = await import("mysql2/promise");
38
+ } catch (err) {
39
+ console.error(
40
+ "[DATABASE ERROR] mysql2 package is not installed. Database wrapper is disabled.",
41
+ );
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Database class wrapping mysql2 and pg with reconnection retries, connection pooling, and query caching.
47
+ */
48
+ class Database {
49
+ /**
50
+ * 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.
55
+ * @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
+ */
57
+ constructor(connectionLimit = 10, queueLimit = 0, config = {}) {
58
+ this.pool = null;
59
+ this.type = DB_TYPE;
60
+
61
+ this.config = {
62
+ ...config,
63
+ host: process.env.DB_HOST || "localhost",
64
+ user:
65
+ process.env.DB_USER || (this.type === "postgres" ? "postgres" : "root"),
66
+ password: process.env.DB_PASSWORD || "root",
67
+ database: process.env.DB_NAME || "blue_bird",
68
+ port:
69
+ parseInt(process.env.DB_PORT) ||
70
+ (this.type === "postgres" ? 5432 : 3306),
71
+ charset: "utf8mb4",
72
+ waitForConnections: true,
73
+ connectionLimit: connectionLimit,
74
+ queueLimit: queueLimit,
75
+ max: connectionLimit,
76
+ };
77
+
78
+ if (process.env.DATABASE_URL && !process.env.DATABASE_URL.startsWith("#")) {
79
+ try {
80
+ if (
81
+ process.env.DATABASE_URL.startsWith("mysql://") ||
82
+ process.env.DATABASE_URL.startsWith("postgres://") ||
83
+ process.env.DATABASE_URL.startsWith("postgresql://")
84
+ ) {
85
+ const url = new URL(process.env.DATABASE_URL);
86
+ this.config.host = url.hostname;
87
+ this.config.port =
88
+ parseInt(url.port) || (this.type === "postgres" ? 5432 : 3306);
89
+ this.config.user = url.username;
90
+ this.config.password = url.password;
91
+ this.config.database = url.pathname.substring(1);
92
+ this.config.connectionString = process.env.DATABASE_URL;
93
+ }
94
+ } catch (err) {
95
+ console.error(
96
+ "[DATABASE ERROR] Failed to parse DATABASE_URL:",
97
+ err.message,
98
+ );
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Creates the database connection pool with 3 retry attempts on failure.
105
+ * @param {number} [retries=3] - Number of connection attempts.
106
+ * @returns {Promise<boolean>} True if connection pool was created.
107
+ */
108
+ async init(retries = 3) {
109
+ if (!mysqlPromise && !pgPromise) return false;
110
+ if (this.pool) return true;
111
+
112
+ for (let attempt = 1; attempt <= retries; attempt++) {
113
+ try {
114
+ if (this.type === "postgres" && pgPromise) {
115
+ const pg = pgPromise.default || pgPromise;
116
+ this.pool = new pg.Pool(this.config);
117
+ await this.pool.query("SELECT 1");
118
+ } else if (mysqlPromise) {
119
+ const { max, connectionString, ...mysqlConfig } = this.config;
120
+ this.pool = mysqlPromise.createPool(mysqlConfig);
121
+ await this.pool.query("SELECT 1");
122
+ }
123
+ return true;
124
+ } catch (err) {
125
+ this.pool = null;
126
+ if (attempt === retries) {
127
+ console.error(
128
+ `[DATABASE ERROR] Connection failed after ${retries} attempts:`,
129
+ err.message,
130
+ );
131
+ return false;
132
+ }
133
+ await new Promise((resolve) => setTimeout(resolve, 1000));
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * Runs a SQL query with parameters and formatting options.
141
+ * Supports both MySQL and PostgreSQL (converting ? to $1, $2 for Postgres automatically).
142
+ *
143
+ * @param {string} sql - SQL query string.
144
+ * @param {Array} [params=[]] - Query parameter array.
145
+ * @param {Object|string} [options={}] - Query options. Supports 'return_row', 'return_rows', and 'cache' (seconds).
146
+ * @returns {Promise<*>| int | boolean} Formatted query result or false on error, or insert id of insert query.
147
+ * @example const result = await connection.query("SELECT * FROM users WHERE id = ?", [1], "return_row");
148
+ * @example const result = await connection.query("SELECT * FROM users WHERE id = ?", [1], { cache: 60 });
149
+ * @example const result = await connection.query("SELECT * FROM users WHERE id = ?", [1], { debug: true });
150
+ * @example const insert_id = await connection.query("INSERT INTO users (name, email, password) VALUES (?, ?, ?)", ["John Doe", "john@example.com", "password"]);
151
+ */
152
+ async query(sql, params = [], options = {}) {
153
+ if (!mysqlPromise && !pgPromise) return false;
154
+ if (!this.pool) {
155
+ const initialized = await this.init();
156
+ if (!initialized) return false;
157
+ }
158
+
159
+ const queryOptions =
160
+ typeof options === "string" ? { [options]: true } : options;
161
+ const cleanSql = sql.trim();
162
+ const isSelect = cleanSql.toLowerCase().startsWith("select");
163
+ const isInsert = cleanSql.toLowerCase().startsWith("insert");
164
+
165
+ const redisClient = getRedisClient();
166
+ let cacheKey = null;
167
+ const isDebug = queryOptions.debug ?? false;
168
+ if (isDebug) {
169
+ console.log("[DATABASE DEBUG] SQL:", sql);
170
+ console.log("[DATABASE DEBUG] PARAMS:", params);
171
+ console.log("[DATABASE DEBUG] OPTIONS:", options);
172
+ }
173
+
174
+ if (isSelect && queryOptions.cache && redisClient) {
175
+ const hash = crypto
176
+ .createHash("md5")
177
+ .update(cleanSql + JSON.stringify(params))
178
+ .digest("hex");
179
+ cacheKey = `db:${hash}`;
180
+ try {
181
+ if (isDebug) {
182
+ console.log("[DATABASE DEBUG][Redis] CACHE KEY:", cacheKey);
183
+ }
184
+ const cached = await redisClient.get(cacheKey);
185
+ if (cached) {
186
+ if (isDebug) {
187
+ console.log("[DATABASE DEBUG][Redis] CACHE HIT");
188
+ }
189
+ return JSON.parse(cached);
190
+ } else {
191
+ if (isDebug) {
192
+ console.log("[DATABASE DEBUG][Redis] CACHE MISS");
193
+ }
194
+ }
195
+ } catch (err) {
196
+ console.error(
197
+ "[DATABASE ERROR] Failed to get cached data:",
198
+ err.message,
199
+ );
200
+ }
201
+ }
202
+
203
+ try {
204
+ if (this.type === "postgres" && pgPromise) {
205
+ let paramIndex = 1;
206
+ const pgSql = cleanSql.replace(/\?/g, () => `$${paramIndex++}`);
207
+ const res = await this.pool.query(pgSql, params);
208
+
209
+ if (isSelect) {
210
+ const rows = res.rows || [];
211
+ if (cacheKey && queryOptions.cache && redisClient) {
212
+ await redisClient
213
+ .set(cacheKey, JSON.stringify(rows), {
214
+ EX: parseInt(queryOptions.cache),
215
+ })
216
+ .catch(() => {});
217
+ }
218
+ if (queryOptions.return_row) {
219
+ return rows.length > 0 ? rows[0] : null;
220
+ }
221
+ return rows;
222
+ }
223
+
224
+ if (isInsert) {
225
+ if (res.rows && res.rows.length > 0) {
226
+ return res.rows[0].id || res.rows[0];
227
+ }
228
+ return res.rowCount;
229
+ }
230
+
231
+ return res.rowCount;
232
+ } else {
233
+ const [results] = await this.pool.execute(cleanSql, params);
234
+
235
+ if (isSelect) {
236
+ const rows = Array.isArray(results) ? results : [];
237
+ if (cacheKey && queryOptions.cache && redisClient) {
238
+ await redisClient
239
+ .set(cacheKey, JSON.stringify(rows), {
240
+ EX: parseInt(queryOptions.cache),
241
+ })
242
+ .catch(() => {});
243
+ }
244
+ if (queryOptions.return_row) {
245
+ return rows.length > 0 ? rows[0] : null;
246
+ }
247
+ return rows;
248
+ }
249
+
250
+ if (isInsert) {
251
+ return results.insertId || results;
252
+ }
253
+
254
+ return results;
255
+ }
256
+ } catch (err) {
257
+ console.error("[DATABASE ERROR] Query execution failed:", err.message);
258
+ throw err;
259
+ }
260
+ }
261
+ }
262
+
263
+ export { Database, DB_TYPE };