@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/.env_example +25 -12
- package/AGENTS.md +97 -25
- package/README.md +80 -23
- package/core/app.js +42 -0
- package/core/cache.js +62 -30
- package/core/cli/docker.js +219 -42
- package/core/cli/init.js +92 -8
- package/core/database.js +205 -20
- package/core/hash.js +201 -0
- package/core/index.d.ts +57 -0
- package/core/upload.js +83 -57
- package/core/ws.js +18 -1
- package/docker/Dockerfile +4 -0
- package/docker/docker-compose.sqlite.yml +69 -0
- package/package.json +4 -2
package/core/upload.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import multer from "multer";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import Config from "./config.js";
|
|
@@ -6,72 +5,99 @@ import Config from "./config.js";
|
|
|
6
5
|
const __dirname = Config.dirname();
|
|
7
6
|
const props = Config.props();
|
|
8
7
|
|
|
8
|
+
let multer = null;
|
|
9
|
+
try {
|
|
10
|
+
multer = (await import("multer")).default || (await import("multer"));
|
|
11
|
+
} catch {
|
|
12
|
+
// multer not installed
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
16
|
* Upload helper to manage file uploads using multer.
|
|
11
17
|
*/
|
|
12
18
|
class Upload {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Asserts that the multer package is available.
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
static _ensureMulter() {
|
|
24
|
+
if (!multer) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"[UPLOAD ERROR] 'multer' package is not installed. Install it with: npm install multer or npx blue-bird add upload",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Configures storage for uploaded files.
|
|
33
|
+
* @param {string} folder - The destination folder within the static path.
|
|
34
|
+
* @returns {import('multer').StorageEngine}
|
|
35
|
+
* @example
|
|
36
|
+
* const storage = Upload.storage("uploads");
|
|
37
|
+
*/
|
|
38
|
+
static storage(folder = "uploads") {
|
|
39
|
+
this._ensureMulter();
|
|
40
|
+
const dest = path.join(__dirname, props.static.path, folder);
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
cb(null, dest);
|
|
30
|
-
},
|
|
31
|
-
filename: (req, file, cb) => {
|
|
32
|
-
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
|
33
|
-
cb(null, uniqueSuffix + path.extname(file.originalname));
|
|
34
|
-
}
|
|
35
|
-
});
|
|
42
|
+
if (!fs.existsSync(dest)) {
|
|
43
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const { folder = "uploads", fileSize = 5000000, allowedTypes = [] } = options;
|
|
46
|
+
return multer.diskStorage({
|
|
47
|
+
destination: (req, file, cb) => {
|
|
48
|
+
cb(null, dest);
|
|
49
|
+
},
|
|
50
|
+
filename: (req, file, cb) => {
|
|
51
|
+
const uniqueSuffix =
|
|
52
|
+
Date.now() + "-" + Math.round(Math.random() * 1e9);
|
|
53
|
+
cb(null, uniqueSuffix + path.extname(file.originalname));
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Returns a multer instance for single or multiple file uploads.
|
|
60
|
+
* @param {Object} options - Multer options.
|
|
61
|
+
* @param {string} [options.folder='uploads'] - Destination folder.
|
|
62
|
+
* @param {number} [options.fileSize=5000000] - Max file size in bytes (default 5MB).
|
|
63
|
+
* @param {Array<string>} [options.allowedTypes=[]] - Allowed mime types (e.g. ['image/png', 'image/jpeg']).
|
|
64
|
+
* @returns {import('multer').Multer}
|
|
65
|
+
* @example
|
|
66
|
+
* const upload = Upload.disk({ folder: "uploads", fileSize: 5000000, allowedTypes: ["image/png", "image/jpeg"] });
|
|
67
|
+
*/
|
|
68
|
+
static disk(options = {}) {
|
|
69
|
+
this._ensureMulter();
|
|
70
|
+
const {
|
|
71
|
+
folder = "uploads",
|
|
72
|
+
fileSize = 5000000,
|
|
73
|
+
allowedTypes = [],
|
|
74
|
+
} = options;
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
return multer({
|
|
77
|
+
storage: this.storage(folder),
|
|
78
|
+
limits: { fileSize },
|
|
79
|
+
fileFilter: (req, file, cb) => {
|
|
80
|
+
if (allowedTypes.length > 0 && !allowedTypes.includes(file.mimetype)) {
|
|
81
|
+
return cb(new Error("File type not allowed"), false);
|
|
82
|
+
}
|
|
83
|
+
cb(null, true);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Helper to get the public URL of an uploaded file.
|
|
90
|
+
* @param {string} filename - The name of the file.
|
|
91
|
+
* @param {string} [folder='uploads'] - The folder where the file is stored.
|
|
92
|
+
* @returns {string} The full public URL.
|
|
93
|
+
* @example
|
|
94
|
+
* const url = Upload.url("file.jpg", "uploads");
|
|
95
|
+
*/
|
|
96
|
+
static url(filename, folder = "uploads") {
|
|
97
|
+
const appUrl = props.appUrl ?? `${props.host}:${props.port}`;
|
|
98
|
+
return `${appUrl}/${folder}/${filename}`;
|
|
99
|
+
}
|
|
75
100
|
}
|
|
76
101
|
|
|
77
102
|
export default Upload;
|
|
103
|
+
|
package/core/ws.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
let WebSocketServer = null;
|
|
2
|
+
let WebSocket = null;
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
const wsModule = await import("ws");
|
|
6
|
+
WebSocketServer = wsModule.WebSocketServer || wsModule.default?.WebSocketServer;
|
|
7
|
+
WebSocket = wsModule.WebSocket || wsModule.default?.WebSocket;
|
|
8
|
+
} catch {
|
|
9
|
+
// ws not installed
|
|
10
|
+
}
|
|
11
|
+
|
|
2
12
|
import { getRedisClient } from "./cache.js";
|
|
3
13
|
import Auth from "./auth.js";
|
|
4
14
|
|
|
@@ -15,6 +25,12 @@ class WebSocketManager {
|
|
|
15
25
|
* @param {boolean} [options.auth=false] - Require valid Auth JWT token on connection.
|
|
16
26
|
*/
|
|
17
27
|
constructor(server, options = {}) {
|
|
28
|
+
if (!WebSocketServer || !WebSocket) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"[WS ERROR] 'ws' package is not installed. Install it with: npm install ws or npx blue-bird add ws",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
this.server = server;
|
|
19
35
|
this.path = options.path || "/ws";
|
|
20
36
|
this.requireAuth = options.auth ?? false;
|
|
@@ -31,6 +47,7 @@ class WebSocketManager {
|
|
|
31
47
|
this._setupRedisPubSub();
|
|
32
48
|
}
|
|
33
49
|
|
|
50
|
+
|
|
34
51
|
/**
|
|
35
52
|
* Attaches the HTTP Upgrade listener to the Express HTTP server instance.
|
|
36
53
|
* @private
|
package/docker/Dockerfile
CHANGED
|
@@ -4,10 +4,14 @@ ENV NODE_ENV=production
|
|
|
4
4
|
|
|
5
5
|
WORKDIR /app
|
|
6
6
|
|
|
7
|
+
# Install native build tools for C++ addons (better-sqlite3, bcrypt)
|
|
8
|
+
RUN apk add --no-cache python3 make g++
|
|
9
|
+
|
|
7
10
|
COPY package*.json ./
|
|
8
11
|
|
|
9
12
|
RUN npm ci --omit=dev && npm install -g pm2
|
|
10
13
|
|
|
14
|
+
|
|
11
15
|
COPY . .
|
|
12
16
|
|
|
13
17
|
EXPOSE 3000
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
- ./database:/app/database
|
|
14
|
+
env_file:
|
|
15
|
+
- .env
|
|
16
|
+
environment:
|
|
17
|
+
- NODE_ENV=production
|
|
18
|
+
- DEBUG=false
|
|
19
|
+
- DB_TYPE=sqlite
|
|
20
|
+
- DB_FILE=${DB_FILE:-database/blue_bird.db}
|
|
21
|
+
- DATABASE_URL=${DATABASE_URL:-sqlite:database/blue_bird.db}
|
|
22
|
+
- REDIS_HOST=redis
|
|
23
|
+
- REDIS_PORT=6379
|
|
24
|
+
- PORT=3000
|
|
25
|
+
depends_on:
|
|
26
|
+
redis:
|
|
27
|
+
condition: service_healthy
|
|
28
|
+
networks:
|
|
29
|
+
- bluebird_net
|
|
30
|
+
profiles:
|
|
31
|
+
- prod
|
|
32
|
+
|
|
33
|
+
nginx:
|
|
34
|
+
image: nginx:1.27-alpine
|
|
35
|
+
container_name: ${TITLE:-bluebird}-nginx
|
|
36
|
+
restart: unless-stopped
|
|
37
|
+
ports:
|
|
38
|
+
- "${PORT:-3000}:80"
|
|
39
|
+
volumes:
|
|
40
|
+
- .:/app:ro
|
|
41
|
+
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
42
|
+
networks:
|
|
43
|
+
- bluebird_net
|
|
44
|
+
profiles:
|
|
45
|
+
- prod
|
|
46
|
+
|
|
47
|
+
redis:
|
|
48
|
+
image: redis:7-alpine
|
|
49
|
+
container_name: ${TITLE:-bluebird}-redis
|
|
50
|
+
restart: unless-stopped
|
|
51
|
+
ports:
|
|
52
|
+
- "${REDIS_PORT:-6379}:6379"
|
|
53
|
+
volumes:
|
|
54
|
+
- redis_data:/data
|
|
55
|
+
networks:
|
|
56
|
+
- bluebird_net
|
|
57
|
+
healthcheck:
|
|
58
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
59
|
+
interval: 5s
|
|
60
|
+
timeout: 3s
|
|
61
|
+
retries: 5
|
|
62
|
+
|
|
63
|
+
volumes:
|
|
64
|
+
redis_data:
|
|
65
|
+
|
|
66
|
+
networks:
|
|
67
|
+
bluebird_net:
|
|
68
|
+
name: ${TITLE:-bluebird}_network
|
|
69
|
+
driver: bridge
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seip/blue-bird",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Express opinionated API framework with built-in JWT auth, validation, and caching",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "core/index.d.ts",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"init": "node core/cli/init.js",
|
|
35
35
|
"route": "node core/cli/route.js",
|
|
36
36
|
"swagger-install": "node core/cli/swagger.js",
|
|
37
|
-
"docker": "node core/cli/init.js docker"
|
|
37
|
+
"docker": "node core/cli/init.js docker",
|
|
38
|
+
"test": "node test/test_suite.js"
|
|
38
39
|
},
|
|
39
40
|
"publishConfig": {
|
|
40
41
|
"access": "public"
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
".env_example"
|
|
50
51
|
],
|
|
51
52
|
"dependencies": {
|
|
53
|
+
"better-sqlite3": "^13.0.3",
|
|
52
54
|
"chalk": "^5.6.2",
|
|
53
55
|
"compression": "^1.8.1",
|
|
54
56
|
"cookie-parser": "^1.4.7",
|