@seip/blue-bird 0.7.4 → 0.7.6
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 +4 -6
- package/AGENTS.md +20 -12
- package/README.md +38 -14
- package/core/cli/docker.js +128 -41
- package/core/cli/init.js +296 -198
- package/core/database.js +138 -57
- package/docker/docker-compose.mysql.yml +92 -0
- package/docker/docker-compose.none.yml +68 -0
- package/docker/docker-compose.postgres.yml +93 -0
- package/docker-compose.yml +1 -2
- package/frontend/src/http/api.js +14 -9
- package/package.json +3 -4
- package/backend/logs/2026-07-14/info.log +0 -48
package/core/database.js
CHANGED
|
@@ -1,47 +1,96 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { getRedisClient } from "./cache.js";
|
|
3
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
|
+
|
|
4
24
|
let mysqlPromise = null;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
)
|
|
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
|
+
}
|
|
11
43
|
}
|
|
12
44
|
|
|
13
45
|
/**
|
|
14
|
-
* Database class wrapping mysql2 with reconnection retries, connection
|
|
46
|
+
* Database class wrapping mysql2 and pg with reconnection retries, connection pooling, and query caching.
|
|
15
47
|
*/
|
|
16
48
|
class Database {
|
|
17
49
|
/**
|
|
18
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" });
|
|
19
56
|
*/
|
|
20
|
-
constructor(connectionLimit = 10, queueLimit = 0) {
|
|
57
|
+
constructor(connectionLimit = 10, queueLimit = 0, config = {}) {
|
|
21
58
|
this.pool = null;
|
|
59
|
+
this.type = DB_TYPE;
|
|
60
|
+
|
|
22
61
|
this.config = {
|
|
62
|
+
...config,
|
|
23
63
|
host: process.env.DB_HOST || "localhost",
|
|
24
|
-
user:
|
|
64
|
+
user:
|
|
65
|
+
process.env.DB_USER || (this.type === "postgres" ? "postgres" : "root"),
|
|
25
66
|
password: process.env.DB_PASSWORD || "root",
|
|
26
67
|
database: process.env.DB_NAME || "blue_bird",
|
|
27
|
-
port:
|
|
68
|
+
port:
|
|
69
|
+
parseInt(process.env.DB_PORT) ||
|
|
70
|
+
(this.type === "postgres" ? 5432 : 3306),
|
|
28
71
|
charset: "utf8mb4",
|
|
29
72
|
waitForConnections: true,
|
|
30
73
|
connectionLimit: connectionLimit,
|
|
31
74
|
queueLimit: queueLimit,
|
|
75
|
+
max: connectionLimit,
|
|
32
76
|
};
|
|
33
77
|
|
|
34
|
-
if (
|
|
35
|
-
process.env.DATABASE_URL &&
|
|
36
|
-
process.env.DATABASE_URL.startsWith("mysql://")
|
|
37
|
-
) {
|
|
78
|
+
if (process.env.DATABASE_URL && !process.env.DATABASE_URL.startsWith("#")) {
|
|
38
79
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|
|
45
94
|
} catch (err) {
|
|
46
95
|
console.error(
|
|
47
96
|
"[DATABASE ERROR] Failed to parse DATABASE_URL:",
|
|
@@ -52,18 +101,25 @@ class Database {
|
|
|
52
101
|
}
|
|
53
102
|
|
|
54
103
|
/**
|
|
55
|
-
* Creates the
|
|
104
|
+
* Creates the database connection pool with 3 retry attempts on failure.
|
|
56
105
|
* @param {number} [retries=3] - Number of connection attempts.
|
|
57
106
|
* @returns {Promise<boolean>} True if connection pool was created.
|
|
58
107
|
*/
|
|
59
108
|
async init(retries = 3) {
|
|
60
|
-
if (!mysqlPromise) return false;
|
|
109
|
+
if (!mysqlPromise && !pgPromise) return false;
|
|
61
110
|
if (this.pool) return true;
|
|
62
111
|
|
|
63
112
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
64
113
|
try {
|
|
65
|
-
this.
|
|
66
|
-
|
|
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
|
+
}
|
|
67
123
|
return true;
|
|
68
124
|
} catch (err) {
|
|
69
125
|
this.pool = null;
|
|
@@ -82,22 +138,19 @@ class Database {
|
|
|
82
138
|
|
|
83
139
|
/**
|
|
84
140
|
* Runs a SQL query with parameters and formatting options.
|
|
141
|
+
* Supports both MySQL and PostgreSQL (converting ? to $1, $2 for Postgres automatically).
|
|
85
142
|
*
|
|
86
143
|
* @param {string} sql - SQL query string.
|
|
87
144
|
* @param {Array} [params=[]] - Query parameter array.
|
|
88
145
|
* @param {Object|string} [options={}] - Query options. Supports 'return_row', 'return_rows', and 'cache' (seconds).
|
|
89
146
|
* @returns {Promise<*>| int | boolean} Formatted query result or false on error, or insert id of insert query.
|
|
90
|
-
* @example
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
*
|
|
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]);
|
|
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"]);
|
|
98
151
|
*/
|
|
99
152
|
async query(sql, params = [], options = {}) {
|
|
100
|
-
if (!mysqlPromise) return false;
|
|
153
|
+
if (!mysqlPromise && !pgPromise) return false;
|
|
101
154
|
if (!this.pool) {
|
|
102
155
|
const initialized = await this.init();
|
|
103
156
|
if (!initialized) return false;
|
|
@@ -126,17 +179,17 @@ class Database {
|
|
|
126
179
|
cacheKey = `db:${hash}`;
|
|
127
180
|
try {
|
|
128
181
|
if (isDebug) {
|
|
129
|
-
console.log("[DATABASE DEBUG
|
|
182
|
+
console.log("[DATABASE DEBUG][Redis] CACHE KEY:", cacheKey);
|
|
130
183
|
}
|
|
131
184
|
const cached = await redisClient.get(cacheKey);
|
|
132
185
|
if (cached) {
|
|
133
186
|
if (isDebug) {
|
|
134
|
-
console.log("[DATABASE DEBUG
|
|
187
|
+
console.log("[DATABASE DEBUG][Redis] CACHE HIT");
|
|
135
188
|
}
|
|
136
189
|
return JSON.parse(cached);
|
|
137
190
|
} else {
|
|
138
191
|
if (isDebug) {
|
|
139
|
-
console.log("[DATABASE DEBUG
|
|
192
|
+
console.log("[DATABASE DEBUG][Redis] CACHE MISS");
|
|
140
193
|
}
|
|
141
194
|
}
|
|
142
195
|
} catch (err) {
|
|
@@ -148,28 +201,58 @@ class Database {
|
|
|
148
201
|
}
|
|
149
202
|
|
|
150
203
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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;
|
|
161
222
|
}
|
|
162
|
-
|
|
163
|
-
|
|
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;
|
|
164
229
|
}
|
|
165
|
-
return rows;
|
|
166
|
-
}
|
|
167
230
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
249
|
|
|
172
|
-
|
|
250
|
+
if (isInsert) {
|
|
251
|
+
return results.insertId || results;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return results;
|
|
255
|
+
}
|
|
173
256
|
} catch (err) {
|
|
174
257
|
console.error("[DATABASE ERROR] Query execution failed:", err.message);
|
|
175
258
|
throw err;
|
|
@@ -177,6 +260,4 @@ class Database {
|
|
|
177
260
|
}
|
|
178
261
|
}
|
|
179
262
|
|
|
180
|
-
|
|
181
|
-
export default connection;
|
|
182
|
-
export { Database };
|
|
263
|
+
export { Database, DB_TYPE };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: docker/Dockerfile
|
|
6
|
+
container_name: ${TITLE:-bluebird}-app
|
|
7
|
+
restart: unless-stopped
|
|
8
|
+
expose:
|
|
9
|
+
- "3000"
|
|
10
|
+
volumes:
|
|
11
|
+
- .:/app
|
|
12
|
+
- /app/node_modules
|
|
13
|
+
env_file:
|
|
14
|
+
- .env
|
|
15
|
+
environment:
|
|
16
|
+
- NODE_ENV=production
|
|
17
|
+
- DEBUG=false
|
|
18
|
+
- DATABASE_URL=mysql://${DB_USER:-root}:${DB_PASSWORD:-root}@mysql:${DB_PORT:-3306}/${DB_NAME:-blue_bird}
|
|
19
|
+
- REDIS_HOST=redis
|
|
20
|
+
- REDIS_PORT=6379
|
|
21
|
+
- PORT=3000
|
|
22
|
+
depends_on:
|
|
23
|
+
mysql:
|
|
24
|
+
condition: service_healthy
|
|
25
|
+
redis:
|
|
26
|
+
condition: service_healthy
|
|
27
|
+
networks:
|
|
28
|
+
- bluebird_net
|
|
29
|
+
profiles:
|
|
30
|
+
- prod
|
|
31
|
+
|
|
32
|
+
nginx:
|
|
33
|
+
image: nginx:1.27-alpine
|
|
34
|
+
container_name: ${TITLE:-bluebird}-nginx
|
|
35
|
+
restart: unless-stopped
|
|
36
|
+
ports:
|
|
37
|
+
- "${PORT:-3000}:80"
|
|
38
|
+
volumes:
|
|
39
|
+
- .:/app:ro
|
|
40
|
+
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
41
|
+
depends_on:
|
|
42
|
+
app:
|
|
43
|
+
condition: service_started
|
|
44
|
+
networks:
|
|
45
|
+
- bluebird_net
|
|
46
|
+
profiles:
|
|
47
|
+
- prod
|
|
48
|
+
|
|
49
|
+
mysql:
|
|
50
|
+
image: mysql:8.0
|
|
51
|
+
container_name: ${TITLE:-bluebird}-mysql
|
|
52
|
+
restart: unless-stopped
|
|
53
|
+
environment:
|
|
54
|
+
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-root}
|
|
55
|
+
MYSQL_DATABASE: ${DB_NAME:-blue_bird}
|
|
56
|
+
ports:
|
|
57
|
+
- "${DB_PORT:-3306}:3306"
|
|
58
|
+
volumes:
|
|
59
|
+
- mysql_data:/var/lib/mysql
|
|
60
|
+
healthcheck:
|
|
61
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD:-root}"]
|
|
62
|
+
interval: 10s
|
|
63
|
+
timeout: 5s
|
|
64
|
+
retries: 5
|
|
65
|
+
start_period: 30s
|
|
66
|
+
networks:
|
|
67
|
+
- bluebird_net
|
|
68
|
+
|
|
69
|
+
redis:
|
|
70
|
+
image: redis:7-alpine
|
|
71
|
+
container_name: ${TITLE:-bluebird}-redis
|
|
72
|
+
restart: unless-stopped
|
|
73
|
+
ports:
|
|
74
|
+
- "${REDIS_PORT:-6379}:6379"
|
|
75
|
+
volumes:
|
|
76
|
+
- redis_data:/data
|
|
77
|
+
networks:
|
|
78
|
+
- bluebird_net
|
|
79
|
+
healthcheck:
|
|
80
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
81
|
+
interval: 5s
|
|
82
|
+
timeout: 3s
|
|
83
|
+
retries: 5
|
|
84
|
+
|
|
85
|
+
volumes:
|
|
86
|
+
mysql_data:
|
|
87
|
+
redis_data:
|
|
88
|
+
|
|
89
|
+
networks:
|
|
90
|
+
bluebird_net:
|
|
91
|
+
name: ${TITLE:-bluebird}_network
|
|
92
|
+
driver: bridge
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: docker/Dockerfile
|
|
6
|
+
container_name: ${TITLE:-bluebird}-app
|
|
7
|
+
restart: unless-stopped
|
|
8
|
+
expose:
|
|
9
|
+
- "3000"
|
|
10
|
+
volumes:
|
|
11
|
+
- .:/app
|
|
12
|
+
- /app/node_modules
|
|
13
|
+
env_file:
|
|
14
|
+
- .env
|
|
15
|
+
environment:
|
|
16
|
+
- NODE_ENV=production
|
|
17
|
+
- DEBUG=false
|
|
18
|
+
- REDIS_HOST=redis
|
|
19
|
+
- REDIS_PORT=6379
|
|
20
|
+
- PORT=3000
|
|
21
|
+
depends_on:
|
|
22
|
+
redis:
|
|
23
|
+
condition: service_healthy
|
|
24
|
+
networks:
|
|
25
|
+
- bluebird_net
|
|
26
|
+
profiles:
|
|
27
|
+
- prod
|
|
28
|
+
|
|
29
|
+
nginx:
|
|
30
|
+
image: nginx:1.27-alpine
|
|
31
|
+
container_name: ${TITLE:-bluebird}-nginx
|
|
32
|
+
restart: unless-stopped
|
|
33
|
+
ports:
|
|
34
|
+
- "${PORT:-3000}:80"
|
|
35
|
+
volumes:
|
|
36
|
+
- .:/app:ro
|
|
37
|
+
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
38
|
+
depends_on:
|
|
39
|
+
app:
|
|
40
|
+
condition: service_started
|
|
41
|
+
networks:
|
|
42
|
+
- bluebird_net
|
|
43
|
+
profiles:
|
|
44
|
+
- prod
|
|
45
|
+
|
|
46
|
+
redis:
|
|
47
|
+
image: redis:7-alpine
|
|
48
|
+
container_name: ${TITLE:-bluebird}-redis
|
|
49
|
+
restart: unless-stopped
|
|
50
|
+
ports:
|
|
51
|
+
- "${REDIS_PORT:-6379}:6379"
|
|
52
|
+
volumes:
|
|
53
|
+
- redis_data:/data
|
|
54
|
+
networks:
|
|
55
|
+
- bluebird_net
|
|
56
|
+
healthcheck:
|
|
57
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
58
|
+
interval: 5s
|
|
59
|
+
timeout: 3s
|
|
60
|
+
retries: 5
|
|
61
|
+
|
|
62
|
+
volumes:
|
|
63
|
+
redis_data:
|
|
64
|
+
|
|
65
|
+
networks:
|
|
66
|
+
bluebird_net:
|
|
67
|
+
name: ${TITLE:-bluebird}_network
|
|
68
|
+
driver: bridge
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: docker/Dockerfile
|
|
6
|
+
container_name: ${TITLE:-bluebird}-app
|
|
7
|
+
restart: unless-stopped
|
|
8
|
+
expose:
|
|
9
|
+
- "3000"
|
|
10
|
+
volumes:
|
|
11
|
+
- .:/app
|
|
12
|
+
- /app/node_modules
|
|
13
|
+
env_file:
|
|
14
|
+
- .env
|
|
15
|
+
environment:
|
|
16
|
+
- NODE_ENV=production
|
|
17
|
+
- DEBUG=false
|
|
18
|
+
- DATABASE_URL=postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-root}@postgres:${DB_PORT:-5432}/${DB_NAME:-blue_bird}?schema=public
|
|
19
|
+
- REDIS_HOST=redis
|
|
20
|
+
- REDIS_PORT=6379
|
|
21
|
+
- PORT=3000
|
|
22
|
+
depends_on:
|
|
23
|
+
postgres:
|
|
24
|
+
condition: service_healthy
|
|
25
|
+
redis:
|
|
26
|
+
condition: service_healthy
|
|
27
|
+
networks:
|
|
28
|
+
- bluebird_net
|
|
29
|
+
profiles:
|
|
30
|
+
- prod
|
|
31
|
+
|
|
32
|
+
nginx:
|
|
33
|
+
image: nginx:1.27-alpine
|
|
34
|
+
container_name: ${TITLE:-bluebird}-nginx
|
|
35
|
+
restart: unless-stopped
|
|
36
|
+
ports:
|
|
37
|
+
- "${PORT:-3000}:80"
|
|
38
|
+
volumes:
|
|
39
|
+
- .:/app:ro
|
|
40
|
+
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
41
|
+
depends_on:
|
|
42
|
+
app:
|
|
43
|
+
condition: service_started
|
|
44
|
+
networks:
|
|
45
|
+
- bluebird_net
|
|
46
|
+
profiles:
|
|
47
|
+
- prod
|
|
48
|
+
|
|
49
|
+
postgres:
|
|
50
|
+
image: postgres:18-alpine
|
|
51
|
+
container_name: ${TITLE:-bluebird}-postgres
|
|
52
|
+
restart: unless-stopped
|
|
53
|
+
environment:
|
|
54
|
+
POSTGRES_USER: ${DB_USER:-postgres}
|
|
55
|
+
POSTGRES_PASSWORD: ${DB_PASSWORD:-root}
|
|
56
|
+
POSTGRES_DB: ${DB_NAME:-blue_bird}
|
|
57
|
+
ports:
|
|
58
|
+
- "${DB_PORT:-5432}:5432"
|
|
59
|
+
volumes:
|
|
60
|
+
- postgres_data:/var/lib/postgresql
|
|
61
|
+
healthcheck:
|
|
62
|
+
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-blue_bird}"]
|
|
63
|
+
interval: 10s
|
|
64
|
+
timeout: 5s
|
|
65
|
+
retries: 5
|
|
66
|
+
start_period: 30s
|
|
67
|
+
networks:
|
|
68
|
+
- bluebird_net
|
|
69
|
+
|
|
70
|
+
redis:
|
|
71
|
+
image: redis:7-alpine
|
|
72
|
+
container_name: ${TITLE:-bluebird}-redis
|
|
73
|
+
restart: unless-stopped
|
|
74
|
+
ports:
|
|
75
|
+
- "${REDIS_PORT:-6379}:6379"
|
|
76
|
+
volumes:
|
|
77
|
+
- redis_data:/data
|
|
78
|
+
networks:
|
|
79
|
+
- bluebird_net
|
|
80
|
+
healthcheck:
|
|
81
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
82
|
+
interval: 5s
|
|
83
|
+
timeout: 3s
|
|
84
|
+
retries: 5
|
|
85
|
+
|
|
86
|
+
volumes:
|
|
87
|
+
postgres_data:
|
|
88
|
+
redis_data:
|
|
89
|
+
|
|
90
|
+
networks:
|
|
91
|
+
bluebird_net:
|
|
92
|
+
name: ${TITLE:-bluebird}_network
|
|
93
|
+
driver: bridge
|
package/docker-compose.yml
CHANGED
|
@@ -15,7 +15,7 @@ services:
|
|
|
15
15
|
environment:
|
|
16
16
|
- NODE_ENV=production
|
|
17
17
|
- DEBUG=false
|
|
18
|
-
- DATABASE_URL=mysql
|
|
18
|
+
- DATABASE_URL=mysql://${DB_USER:-root}:${DB_PASSWORD:-root}@mysql:${DB_PORT:-3306}/${DB_NAME:-blue_bird}
|
|
19
19
|
- REDIS_HOST=redis
|
|
20
20
|
- REDIS_PORT=6379
|
|
21
21
|
- PORT=3000
|
|
@@ -90,4 +90,3 @@ networks:
|
|
|
90
90
|
bluebird_net:
|
|
91
91
|
name: ${TITLE:-bluebird}_network
|
|
92
92
|
driver: bridge
|
|
93
|
-
|
package/frontend/src/http/api.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
const
|
|
1
|
+
const defaultPort = (import.meta.env.PORT || "3000").replace(
|
|
2
|
+
/^["']|["']$/g,
|
|
3
|
+
"",
|
|
4
|
+
);
|
|
2
5
|
const host = (import.meta.env.HOST || "localhost").replace(/^["']|["']$/g, "");
|
|
3
6
|
|
|
4
7
|
/**
|
|
@@ -7,18 +10,20 @@ const host = (import.meta.env.HOST || "localhost").replace(/^["']|["']$/g, "");
|
|
|
7
10
|
* @param {string} [path] - API path relative to the server root (e.g. "api/users").
|
|
8
11
|
* @param {URL} [requestUrl] - Current request URL (pass `Astro.url` from the page). Required in production.
|
|
9
12
|
* @returns {string} Absolute URL ready for fetch.
|
|
13
|
+
* @example apiUrl('api/', Astro.url)
|
|
10
14
|
*/
|
|
11
15
|
export function apiUrl(path = "", requestUrl) {
|
|
12
|
-
if (import.meta.env.DEV) {
|
|
13
|
-
return `http://${host}:${port}/${path}`;
|
|
14
|
-
}
|
|
15
16
|
if (!requestUrl) {
|
|
16
|
-
throw new Error(
|
|
17
|
+
throw new Error(
|
|
18
|
+
"apiUrl: requestUrl is required in production (pass Astro.url)",
|
|
19
|
+
);
|
|
17
20
|
}
|
|
18
21
|
const url = new URL(`/${path}`, requestUrl);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const runtimePort =
|
|
23
|
+
(typeof process !== "undefined" && process.env && process.env.PORT) ||
|
|
24
|
+
defaultPort;
|
|
25
|
+
url.hostname = "127.0.0.1";
|
|
26
|
+
url.port = runtimePort;
|
|
27
|
+
url.protocol = "http:";
|
|
23
28
|
return url.href;
|
|
24
29
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seip/blue-bird",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Express opinionated framework with HTML rendering, API architecture, built-in JWT auth, validation, caching, and SEO",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,9 +64,8 @@
|
|
|
64
64
|
"express-rate-limit": "^8.2.1",
|
|
65
65
|
"helmet": "^8.1.0",
|
|
66
66
|
"jsonwebtoken": "^9.0.2",
|
|
67
|
-
"multer": "^2.0.2",
|
|
68
|
-
"
|
|
69
|
-
"redis": "^4.7.0",
|
|
67
|
+
"multer": "^2.0.2",
|
|
68
|
+
"redis": "^6.1.0",
|
|
70
69
|
"xss": "^1.0.15"
|
|
71
70
|
}
|
|
72
71
|
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
2026-07-14 22:04:34 -::1 -[GET] /
|
|
2
|
-
2026-07-14 22:04:38 -::1 -[GET] /about
|
|
3
|
-
2026-07-14 22:04:38 -::1 -[GET] /about
|
|
4
|
-
2026-07-14 22:04:39 -::1 -[GET] /
|
|
5
|
-
2026-07-14 22:04:39 -::1 -[GET] /
|
|
6
|
-
2026-07-14 22:06:26 -::1 -[GET] /api/users
|
|
7
|
-
2026-07-14 22:08:02 -::1 -[GET] /api/users
|
|
8
|
-
2026-07-14 22:08:05 -::1 -[GET] /api/
|
|
9
|
-
2026-07-14 22:42:04 -::1 -[GET] /api/auth_generate
|
|
10
|
-
2026-07-14 22:42:05 -::1 -[GET] /api/auth_generate
|
|
11
|
-
2026-07-14 22:42:10 -::1 -[GET] /api/auth_verify
|
|
12
|
-
2026-07-14 22:42:12 -::1 -[GET] /api/auth_verify
|
|
13
|
-
2026-07-14 22:42:21 -::1 -[GET] /api/auth_logout
|
|
14
|
-
2026-07-14 22:42:24 -::1 -[GET] /api/auth_verify
|
|
15
|
-
2026-07-14 22:42:28 -::1 -[GET] /
|
|
16
|
-
2026-07-14 22:42:28 -::ffff:127.0.0.1 -[GET] /api/
|
|
17
|
-
2026-07-14 22:42:29 -::1 -[GET] /
|
|
18
|
-
2026-07-14 22:42:29 -::ffff:127.0.0.1 -[GET] /api/
|
|
19
|
-
2026-07-14 22:42:30 -::1 -[GET] /about
|
|
20
|
-
2026-07-14 22:42:31 -::1 -[GET] /about
|
|
21
|
-
2026-07-14 22:42:32 -::1 -[GET] /
|
|
22
|
-
2026-07-14 22:42:32 -::ffff:127.0.0.1 -[GET] /api/
|
|
23
|
-
2026-07-14 22:42:32 -::1 -[GET] /
|
|
24
|
-
2026-07-14 22:42:32 -::ffff:127.0.0.1 -[GET] /api/
|
|
25
|
-
2026-07-14 22:42:33 -::1 -[GET] /about
|
|
26
|
-
2026-07-14 22:42:35 -::1 -[GET] /
|
|
27
|
-
2026-07-14 22:42:35 -::ffff:127.0.0.1 -[GET] /api/
|
|
28
|
-
2026-07-14 22:42:37 -::1 -[GET] /about
|
|
29
|
-
2026-07-14 22:42:38 -::1 -[GET] /
|
|
30
|
-
2026-07-14 22:42:38 -::ffff:127.0.0.1 -[GET] /api/
|
|
31
|
-
2026-07-14 22:48:54 -::1 -[GET] /
|
|
32
|
-
2026-07-14 22:48:54 -::ffff:127.0.0.1 -[GET] /api/
|
|
33
|
-
2026-07-14 22:48:56 -::1 -[GET] /about
|
|
34
|
-
2026-07-14 22:48:58 -::1 -[GET] /about
|
|
35
|
-
2026-07-14 22:48:59 -::1 -[GET] /
|
|
36
|
-
2026-07-14 22:48:59 -::ffff:127.0.0.1 -[GET] /api/
|
|
37
|
-
2026-07-14 22:48:59 -::1 -[GET] /
|
|
38
|
-
2026-07-14 22:48:59 -::ffff:127.0.0.1 -[GET] /api/
|
|
39
|
-
2026-07-14 22:49:01 -::1 -[GET] /about
|
|
40
|
-
2026-07-14 22:49:02 -::1 -[GET] /
|
|
41
|
-
2026-07-14 22:49:02 -::ffff:127.0.0.1 -[GET] /api/
|
|
42
|
-
2026-07-14 22:49:03 -::1 -[GET] /about
|
|
43
|
-
2026-07-14 22:49:04 -::1 -[GET] /
|
|
44
|
-
2026-07-14 22:49:04 -::ffff:127.0.0.1 -[GET] /api/
|
|
45
|
-
2026-07-14 22:49:05 -::1 -[GET] /about
|
|
46
|
-
2026-07-14 22:49:07 -::1 -[GET] /
|
|
47
|
-
2026-07-14 22:49:07 -::ffff:127.0.0.1 -[GET] /api/
|
|
48
|
-
2026-07-14 22:49:07 -::1 -[GET] /about
|