@seip/blue-bird 0.7.6 → 0.9.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.
- package/.env_example +34 -34
- package/AGENTS.md +174 -249
- package/LICENSE +21 -21
- package/README.md +331 -367
- package/{index.js → backend/index.js} +22 -30
- package/backend/routes/api.js +57 -57
- package/core/app.js +338 -402
- package/core/auth.js +262 -256
- package/core/cache.js +174 -174
- package/core/cli/docker.js +488 -457
- package/core/cli/init.js +333 -337
- package/core/cli/route.js +42 -42
- package/core/config.js +52 -52
- package/core/database.js +263 -263
- package/core/debug.js +248 -248
- package/core/logger.js +115 -115
- package/core/middleware.js +27 -27
- package/core/router.js +144 -144
- package/core/swagger.js +40 -40
- package/core/upload.js +77 -77
- package/core/validate.js +380 -380
- package/docker/Dockerfile +16 -16
- package/docker/docker-compose.dev.yml +6 -0
- package/docker/docker-compose.mysql.yml +92 -92
- package/docker/docker-compose.none.yml +68 -68
- package/docker/docker-compose.postgres.yml +93 -93
- package/docker/nginx.conf +98 -106
- package/docker-compose.yml +92 -92
- package/frontend/about.html +103 -0
- package/frontend/images/favicon.ico +0 -0
- package/frontend/index.html +141 -0
- package/frontend/js/tailwind.js +8 -0
- package/package.json +64 -71
- package/frontend/astro.config.mjs +0 -35
- package/frontend/public/css/app.css +0 -319
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/http/api.js +0 -29
- package/frontend/src/layouts/Layout.astro +0 -20
- package/frontend/src/pages/about.astro +0 -54
- package/frontend/src/pages/index.astro +0 -110
package/core/cache.js
CHANGED
|
@@ -1,174 +1,174 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
|
|
3
|
-
const CACHE = {};
|
|
4
|
-
|
|
5
|
-
let redisClient = null;
|
|
6
|
-
let isRedisConnected = false;
|
|
7
|
-
const redisHost = process.env.REDIS_HOST ?? false;
|
|
8
|
-
const redisPort = process.env.REDIS_PORT ?? 6379;
|
|
9
|
-
const redisPassword = process.env.REDIS_PASSWORD || "";
|
|
10
|
-
const redisUrl = redisPassword
|
|
11
|
-
? `redis://:${redisPassword}@${redisHost}:${redisPort}`
|
|
12
|
-
: `redis://${redisHost}:${redisPort}`;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Initializes the Redis client connection if REDIS_HOST env is set.
|
|
16
|
-
* @returns {Promise<void>}
|
|
17
|
-
*/
|
|
18
|
-
async function initRedis() {
|
|
19
|
-
if (redisClient) return;
|
|
20
|
-
if (!redisHost) return;
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const { createClient } = await import("redis");
|
|
24
|
-
let host = redisHost;
|
|
25
|
-
if (host === "localhost" && fs.existsSync("/.dockerenv")) {
|
|
26
|
-
host = "redis";
|
|
27
|
-
}
|
|
28
|
-
const url = redisUrl;
|
|
29
|
-
|
|
30
|
-
redisClient = createClient({ url });
|
|
31
|
-
redisClient.on("error", () => {
|
|
32
|
-
isRedisConnected = false;
|
|
33
|
-
});
|
|
34
|
-
redisClient.on("ready", () => {
|
|
35
|
-
isRedisConnected = true;
|
|
36
|
-
});
|
|
37
|
-
redisClient.on("connect", () => {
|
|
38
|
-
isRedisConnected = true;
|
|
39
|
-
});
|
|
40
|
-
await redisClient.connect();
|
|
41
|
-
isRedisConnected = true;
|
|
42
|
-
} catch (err) {
|
|
43
|
-
redisClient = null;
|
|
44
|
-
isRedisConnected = false;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
initRedis().catch(() => {});
|
|
49
|
-
|
|
50
|
-
setInterval(() => {
|
|
51
|
-
const now = Date.now();
|
|
52
|
-
for (const key in CACHE) {
|
|
53
|
-
if (CACHE[key].expiry <= now) {
|
|
54
|
-
delete CACHE[key];
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}, 300000).unref();
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* High-performance Caching class supporting both local memory and Redis backends.
|
|
61
|
-
*/
|
|
62
|
-
class Cache {
|
|
63
|
-
/**
|
|
64
|
-
* Express middleware to cache route JSON and HTML responses.
|
|
65
|
-
* @param {number} [seconds=60] - Expiry time in seconds.
|
|
66
|
-
* @returns {Function} Express middleware.
|
|
67
|
-
*/
|
|
68
|
-
static middleware(seconds = 60) {
|
|
69
|
-
return async (req, res, next) => {
|
|
70
|
-
const key = req.originalUrl;
|
|
71
|
-
|
|
72
|
-
if (redisHost && !redisClient) {
|
|
73
|
-
await initRedis().catch(() => {});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (isRedisConnected && redisClient) {
|
|
77
|
-
try {
|
|
78
|
-
const cachedData = await redisClient.get(key);
|
|
79
|
-
if (cachedData) {
|
|
80
|
-
const cached = JSON.parse(cachedData);
|
|
81
|
-
if (cached.type === "json") {
|
|
82
|
-
return res.json(cached.data);
|
|
83
|
-
} else {
|
|
84
|
-
res.type("text/html");
|
|
85
|
-
res.set("X-Blue-Bird-Cache", "HIT");
|
|
86
|
-
return res.send(cached.data);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
} catch (err) {
|
|
90
|
-
isRedisConnected = false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!isRedisConnected || !redisClient) {
|
|
95
|
-
if (CACHE[key] && CACHE[key].expiry > Date.now()) {
|
|
96
|
-
const cached = CACHE[key];
|
|
97
|
-
if (cached.type === "json") {
|
|
98
|
-
res.set("X-Blue-Bird-Cache", "HIT");
|
|
99
|
-
return res.json(cached.data);
|
|
100
|
-
} else {
|
|
101
|
-
res.type("text/html");
|
|
102
|
-
res.set("X-Blue-Bird-Cache", "HIT");
|
|
103
|
-
return res.send(cached.data);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const originalJson = res.json.bind(res);
|
|
109
|
-
const originalSend = res.send.bind(res);
|
|
110
|
-
let cachedInRequest = false;
|
|
111
|
-
|
|
112
|
-
res.json = async (body) => {
|
|
113
|
-
if (!cachedInRequest) {
|
|
114
|
-
cachedInRequest = true;
|
|
115
|
-
const cacheObject = {
|
|
116
|
-
type: "json",
|
|
117
|
-
data: body,
|
|
118
|
-
expiry: Date.now() + seconds * 1000,
|
|
119
|
-
};
|
|
120
|
-
if (isRedisConnected && redisClient) {
|
|
121
|
-
try {
|
|
122
|
-
await redisClient.set(key, JSON.stringify(cacheObject), {
|
|
123
|
-
EX: seconds,
|
|
124
|
-
});
|
|
125
|
-
} catch (err) {
|
|
126
|
-
CACHE[key] = cacheObject;
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
CACHE[key] = cacheObject;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
res.set("X-Blue-Bird-Cache", "MISS");
|
|
133
|
-
return originalJson(body);
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
res.send = async (body) => {
|
|
137
|
-
if (!cachedInRequest && typeof body === "string") {
|
|
138
|
-
cachedInRequest = true;
|
|
139
|
-
const cacheObject = {
|
|
140
|
-
type: "html",
|
|
141
|
-
data: body,
|
|
142
|
-
expiry: Date.now() + seconds * 1000,
|
|
143
|
-
};
|
|
144
|
-
if (isRedisConnected && redisClient) {
|
|
145
|
-
try {
|
|
146
|
-
await redisClient.set(key, JSON.stringify(cacheObject), {
|
|
147
|
-
EX: seconds,
|
|
148
|
-
});
|
|
149
|
-
} catch (err) {
|
|
150
|
-
CACHE[key] = cacheObject;
|
|
151
|
-
}
|
|
152
|
-
} else {
|
|
153
|
-
CACHE[key] = cacheObject;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
res.set("X-Blue-Bird-Cache", "MISS");
|
|
157
|
-
return originalSend(body);
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
res.set("X-Blue-Bird-Cache", "MISS");
|
|
161
|
-
next();
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Returns the active Redis client if connected.
|
|
168
|
-
* @returns {Object|null} The Redis client instance or null.
|
|
169
|
-
*/
|
|
170
|
-
export function getRedisClient() {
|
|
171
|
-
return isRedisConnected ? redisClient : null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export default Cache;
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
const CACHE = {};
|
|
4
|
+
|
|
5
|
+
let redisClient = null;
|
|
6
|
+
let isRedisConnected = false;
|
|
7
|
+
const redisHost = process.env.REDIS_HOST ?? false;
|
|
8
|
+
const redisPort = process.env.REDIS_PORT ?? 6379;
|
|
9
|
+
const redisPassword = process.env.REDIS_PASSWORD || "";
|
|
10
|
+
const redisUrl = redisPassword
|
|
11
|
+
? `redis://:${redisPassword}@${redisHost}:${redisPort}`
|
|
12
|
+
: `redis://${redisHost}:${redisPort}`;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initializes the Redis client connection if REDIS_HOST env is set.
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
async function initRedis() {
|
|
19
|
+
if (redisClient) return;
|
|
20
|
+
if (!redisHost) return;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { createClient } = await import("redis");
|
|
24
|
+
let host = redisHost;
|
|
25
|
+
if (host === "localhost" && fs.existsSync("/.dockerenv")) {
|
|
26
|
+
host = "redis";
|
|
27
|
+
}
|
|
28
|
+
const url = redisUrl;
|
|
29
|
+
|
|
30
|
+
redisClient = createClient({ url });
|
|
31
|
+
redisClient.on("error", () => {
|
|
32
|
+
isRedisConnected = false;
|
|
33
|
+
});
|
|
34
|
+
redisClient.on("ready", () => {
|
|
35
|
+
isRedisConnected = true;
|
|
36
|
+
});
|
|
37
|
+
redisClient.on("connect", () => {
|
|
38
|
+
isRedisConnected = true;
|
|
39
|
+
});
|
|
40
|
+
await redisClient.connect();
|
|
41
|
+
isRedisConnected = true;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
redisClient = null;
|
|
44
|
+
isRedisConnected = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
initRedis().catch(() => {});
|
|
49
|
+
|
|
50
|
+
setInterval(() => {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
for (const key in CACHE) {
|
|
53
|
+
if (CACHE[key].expiry <= now) {
|
|
54
|
+
delete CACHE[key];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, 300000).unref();
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* High-performance Caching class supporting both local memory and Redis backends.
|
|
61
|
+
*/
|
|
62
|
+
class Cache {
|
|
63
|
+
/**
|
|
64
|
+
* Express middleware to cache route JSON and HTML responses.
|
|
65
|
+
* @param {number} [seconds=60] - Expiry time in seconds.
|
|
66
|
+
* @returns {Function} Express middleware.
|
|
67
|
+
*/
|
|
68
|
+
static middleware(seconds = 60) {
|
|
69
|
+
return async (req, res, next) => {
|
|
70
|
+
const key = req.originalUrl;
|
|
71
|
+
|
|
72
|
+
if (redisHost && !redisClient) {
|
|
73
|
+
await initRedis().catch(() => {});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isRedisConnected && redisClient) {
|
|
77
|
+
try {
|
|
78
|
+
const cachedData = await redisClient.get(key);
|
|
79
|
+
if (cachedData) {
|
|
80
|
+
const cached = JSON.parse(cachedData);
|
|
81
|
+
if (cached.type === "json") {
|
|
82
|
+
return res.json(cached.data);
|
|
83
|
+
} else {
|
|
84
|
+
res.type("text/html");
|
|
85
|
+
res.set("X-Blue-Bird-Cache", "HIT");
|
|
86
|
+
return res.send(cached.data);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
isRedisConnected = false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!isRedisConnected || !redisClient) {
|
|
95
|
+
if (CACHE[key] && CACHE[key].expiry > Date.now()) {
|
|
96
|
+
const cached = CACHE[key];
|
|
97
|
+
if (cached.type === "json") {
|
|
98
|
+
res.set("X-Blue-Bird-Cache", "HIT");
|
|
99
|
+
return res.json(cached.data);
|
|
100
|
+
} else {
|
|
101
|
+
res.type("text/html");
|
|
102
|
+
res.set("X-Blue-Bird-Cache", "HIT");
|
|
103
|
+
return res.send(cached.data);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const originalJson = res.json.bind(res);
|
|
109
|
+
const originalSend = res.send.bind(res);
|
|
110
|
+
let cachedInRequest = false;
|
|
111
|
+
|
|
112
|
+
res.json = async (body) => {
|
|
113
|
+
if (!cachedInRequest) {
|
|
114
|
+
cachedInRequest = true;
|
|
115
|
+
const cacheObject = {
|
|
116
|
+
type: "json",
|
|
117
|
+
data: body,
|
|
118
|
+
expiry: Date.now() + seconds * 1000,
|
|
119
|
+
};
|
|
120
|
+
if (isRedisConnected && redisClient) {
|
|
121
|
+
try {
|
|
122
|
+
await redisClient.set(key, JSON.stringify(cacheObject), {
|
|
123
|
+
EX: seconds,
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
CACHE[key] = cacheObject;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
CACHE[key] = cacheObject;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
res.set("X-Blue-Bird-Cache", "MISS");
|
|
133
|
+
return originalJson(body);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
res.send = async (body) => {
|
|
137
|
+
if (!cachedInRequest && typeof body === "string") {
|
|
138
|
+
cachedInRequest = true;
|
|
139
|
+
const cacheObject = {
|
|
140
|
+
type: "html",
|
|
141
|
+
data: body,
|
|
142
|
+
expiry: Date.now() + seconds * 1000,
|
|
143
|
+
};
|
|
144
|
+
if (isRedisConnected && redisClient) {
|
|
145
|
+
try {
|
|
146
|
+
await redisClient.set(key, JSON.stringify(cacheObject), {
|
|
147
|
+
EX: seconds,
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
CACHE[key] = cacheObject;
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
CACHE[key] = cacheObject;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
res.set("X-Blue-Bird-Cache", "MISS");
|
|
157
|
+
return originalSend(body);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
res.set("X-Blue-Bird-Cache", "MISS");
|
|
161
|
+
next();
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Returns the active Redis client if connected.
|
|
168
|
+
* @returns {Object|null} The Redis client instance or null.
|
|
169
|
+
*/
|
|
170
|
+
export function getRedisClient() {
|
|
171
|
+
return isRedisConnected ? redisClient : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default Cache;
|