zyket 1.2.10 → 1.2.12
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/.github/workflows/publish.yml +37 -37
- package/README.md +279 -279
- package/bin/cli.js +201 -201
- package/index.js +32 -32
- package/package.json +54 -50
- package/src/Middleware.js +3 -3
- package/src/extensions/Extension.js +10 -10
- package/src/extensions/bullboard/index.js +38 -38
- package/src/extensions/interactive-storage/index.js +162 -162
- package/src/extensions/interactive-storage/middlewares/MulterMiddleware.js +31 -31
- package/src/extensions/interactive-storage/routes/browse.js +31 -31
- package/src/extensions/interactive-storage/routes/create-folder.js +37 -37
- package/src/extensions/interactive-storage/routes/delete-folder.js +57 -57
- package/src/extensions/interactive-storage/routes/delete.js +41 -41
- package/src/extensions/interactive-storage/routes/download.js +47 -47
- package/src/extensions/interactive-storage/routes/info.js +37 -37
- package/src/extensions/interactive-storage/routes/upload.js +46 -46
- package/src/kernel/HTTPServer.js +31 -31
- package/src/kernel/index.js +78 -78
- package/src/services/Service.js +10 -10
- package/src/services/auth/auth.js +7 -7
- package/src/services/auth/index.js +199 -199
- package/src/services/bullmq/Worker.js +7 -7
- package/src/services/bullmq/index.js +92 -92
- package/src/services/cache/index.js +96 -96
- package/src/services/database/index.js +127 -127
- package/src/services/events/Event.js +6 -6
- package/src/services/events/index.js +59 -59
- package/src/services/express/Express.js +248 -248
- package/src/services/express/Middleware.js +7 -7
- package/src/services/express/RedirectResponse.js +8 -8
- package/src/services/express/Route.js +6 -6
- package/src/services/express/index.js +4 -4
- package/src/services/index.js +29 -29
- package/src/services/logger/index.js +80 -80
- package/src/services/s3/index.js +82 -82
- package/src/services/scheduler/Schedule.js +6 -6
- package/src/services/scheduler/index.js +47 -47
- package/src/services/socketio/Guard.js +10 -10
- package/src/services/socketio/Handler.js +10 -10
- package/src/services/socketio/SocketIO.js +159 -132
- package/src/services/socketio/index.js +4 -4
- package/src/services/template-manager/index.js +73 -73
- package/src/templates/default/config/cors.js +5 -1
- package/src/templates/default/config/swagger.js +15 -15
- package/src/templates/default/frontend/main.jsx +15 -15
- package/src/templates/default/frontend/src/hooks/useAuth.jsx +51 -51
- package/src/templates/default/frontend/src/hooks/useLayout.jsx +18 -18
- package/src/templates/default/frontend/src/layouts/auth/index.jsx +45 -45
- package/src/templates/default/frontend/src/layouts/auth/routes.js +17 -17
- package/src/templates/default/frontend/src/layouts/landing/index.jsx +61 -61
- package/src/templates/default/frontend/src/layouts/landing/routes.js +10 -10
- package/src/templates/default/frontend/src/layouts/panel/index.jsx +115 -115
- package/src/templates/default/frontend/src/layouts/panel/routes.js +10 -10
- package/src/templates/default/frontend/src/middlewares/LoggedMiddleware.jsx +21 -21
- package/src/templates/default/frontend/src/middlewares/NotLoggedMiddleware.jsx +14 -14
- package/src/templates/default/frontend/src/store/index.jsx +5 -5
- package/src/templates/default/frontend/src/store/storeAuth.jsx +14 -14
- package/src/templates/default/frontend/src/views/auth/index.jsx +4 -4
- package/src/templates/default/frontend/src/views/auth/register/index.jsx +4 -4
- package/src/templates/default/frontend/src/views/landing/index.jsx +4 -4
- package/src/templates/default/frontend/src/views/panel/dashboard/index.jsx +4 -4
- package/src/templates/default/frontend/styles.css +1 -1
- package/src/templates/default/src/guards/default.js +6 -6
- package/src/templates/default/src/handlers/connection.js +6 -6
- package/src/templates/default/src/handlers/message.js +8 -8
- package/src/templates/default/src/middlewares/default.js +7 -7
- package/src/templates/default/src/routes/[test]/message.js +26 -26
- package/src/templates/default/src/routes/index.js +22 -22
- package/src/templates/default/src/services/auth/auth.js +7 -7
- package/src/templates/default/src/services/auth/index.js +32 -32
- package/src/utils/EnvManager.js +65 -65
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
const Service = require("../Service");
|
|
2
|
-
const fs = require("fs");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const fg = require('fast-glob');
|
|
5
|
-
const Event = require("./Event");
|
|
6
|
-
|
|
7
|
-
module.exports = class EventService extends Service {
|
|
8
|
-
#container
|
|
9
|
-
events = {};
|
|
10
|
-
|
|
11
|
-
constructor(container) {
|
|
12
|
-
super("events");
|
|
13
|
-
this.#container = container;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
emit(eventName, payload) {
|
|
18
|
-
const event = this.events[eventName];
|
|
19
|
-
if (!event) throw new Error(`Event ${eventName} not found`);
|
|
20
|
-
this.#container.get('logger').debug(`Emitting event ${eventName}`);
|
|
21
|
-
return event.handle({ container: this.#container, payload });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
emitAsync(eventName, payload, timeout = 30000) {
|
|
25
|
-
const event = this.events[eventName];
|
|
26
|
-
if (!event) throw new Error(`Event ${eventName} not found`);
|
|
27
|
-
this.#container.get('logger').debug(`Emitting event ${eventName} asynchronously`);
|
|
28
|
-
return Promise.race([
|
|
29
|
-
event.handle({ container: this.#container, payload }),
|
|
30
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`Event ${eventName} timed out after ${timeout}ms`)), timeout))
|
|
31
|
-
]);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async boot() {
|
|
35
|
-
const events = await this.#loadEventsFromFolder(path.join(process.cwd(), "src", "events"));
|
|
36
|
-
this.#container.get('logger').info(`Loaded ${events.length} events`);
|
|
37
|
-
for (const evt of events) {
|
|
38
|
-
this.events[evt.name] = evt;
|
|
39
|
-
this.#container.get('logger').debug(`Event ${evt.name} initialized`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async #loadEventsFromFolder(eventsFolder) {
|
|
44
|
-
this.#createEventsFolder(eventsFolder);
|
|
45
|
-
const events = (await fg('**/*.js', { cwd: eventsFolder })).map((evt) => {
|
|
46
|
-
const event = require(path.join(eventsFolder, evt));
|
|
47
|
-
if(!(event.prototype instanceof Event)) throw new Error(`${evt} is not a valid handler`);
|
|
48
|
-
return new event(evt.replace('.js', ''));
|
|
49
|
-
});
|
|
50
|
-
return events;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
#createEventsFolder(eventsFolder, overwrite = false) {
|
|
54
|
-
if (fs.existsSync(eventsFolder) && !overwrite) return;
|
|
55
|
-
this.#container.get('logger').info(`Creating schedules folder at ${eventsFolder}`);
|
|
56
|
-
fs.mkdirSync(eventsFolder);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
const Service = require("../Service");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fg = require('fast-glob');
|
|
5
|
+
const Event = require("./Event");
|
|
6
|
+
|
|
7
|
+
module.exports = class EventService extends Service {
|
|
8
|
+
#container
|
|
9
|
+
events = {};
|
|
10
|
+
|
|
11
|
+
constructor(container) {
|
|
12
|
+
super("events");
|
|
13
|
+
this.#container = container;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
emit(eventName, payload) {
|
|
18
|
+
const event = this.events[eventName];
|
|
19
|
+
if (!event) throw new Error(`Event ${eventName} not found`);
|
|
20
|
+
this.#container.get('logger').debug(`Emitting event ${eventName}`);
|
|
21
|
+
return event.handle({ container: this.#container, payload });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
emitAsync(eventName, payload, timeout = 30000) {
|
|
25
|
+
const event = this.events[eventName];
|
|
26
|
+
if (!event) throw new Error(`Event ${eventName} not found`);
|
|
27
|
+
this.#container.get('logger').debug(`Emitting event ${eventName} asynchronously`);
|
|
28
|
+
return Promise.race([
|
|
29
|
+
event.handle({ container: this.#container, payload }),
|
|
30
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Event ${eventName} timed out after ${timeout}ms`)), timeout))
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async boot() {
|
|
35
|
+
const events = await this.#loadEventsFromFolder(path.join(process.cwd(), "src", "events"));
|
|
36
|
+
this.#container.get('logger').info(`Loaded ${events.length} events`);
|
|
37
|
+
for (const evt of events) {
|
|
38
|
+
this.events[evt.name] = evt;
|
|
39
|
+
this.#container.get('logger').debug(`Event ${evt.name} initialized`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async #loadEventsFromFolder(eventsFolder) {
|
|
44
|
+
this.#createEventsFolder(eventsFolder);
|
|
45
|
+
const events = (await fg('**/*.js', { cwd: eventsFolder })).map((evt) => {
|
|
46
|
+
const event = require(path.join(eventsFolder, evt));
|
|
47
|
+
if(!(event.prototype instanceof Event)) throw new Error(`${evt} is not a valid handler`);
|
|
48
|
+
return new event(evt.replace('.js', ''));
|
|
49
|
+
});
|
|
50
|
+
return events;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#createEventsFolder(eventsFolder, overwrite = false) {
|
|
54
|
+
if (fs.existsSync(eventsFolder) && !overwrite) return;
|
|
55
|
+
this.#container.get('logger').info(`Creating schedules folder at ${eventsFolder}`);
|
|
56
|
+
fs.mkdirSync(eventsFolder);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
60
|
}
|
|
@@ -1,249 +1,249 @@
|
|
|
1
|
-
const Service = require("../Service");
|
|
2
|
-
const express = require("express");
|
|
3
|
-
const cors = require("cors");
|
|
4
|
-
const Route = require("./Route");
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const fg = require('fast-glob');
|
|
8
|
-
const Middleware = require("./Middleware");
|
|
9
|
-
const swaggerJsDoc = require("swagger-jsdoc");
|
|
10
|
-
const swaggerUi = require("swagger-ui-express");
|
|
11
|
-
const RedirectResponse = require("./RedirectResponse");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
module.exports = class Express extends Service {
|
|
15
|
-
#container;
|
|
16
|
-
#app;
|
|
17
|
-
#httpServer;
|
|
18
|
-
|
|
19
|
-
constructor(container) {
|
|
20
|
-
super("express");
|
|
21
|
-
this.#container = container;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async boot({ httpServer } = {}) {
|
|
25
|
-
if (!httpServer) {
|
|
26
|
-
throw new Error("HTTP server is not available");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
this.#httpServer = httpServer;
|
|
30
|
-
this.#app = express();
|
|
31
|
-
|
|
32
|
-
this.#app.use(express.json({ limit: `100mb` }))
|
|
33
|
-
|
|
34
|
-
const corsOptions = await this.#loadCorsOrCreateDefault();
|
|
35
|
-
|
|
36
|
-
if(corsOptions) this.#app.use(cors(corsOptions));
|
|
37
|
-
|
|
38
|
-
// Swagger setup
|
|
39
|
-
const swaggerOptions = {
|
|
40
|
-
...(await this.#loadSwaggerOrCreateDefault()),
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const swaggerDocs = swaggerJsDoc(swaggerOptions);
|
|
44
|
-
this.#app.use(process?.env?.SWAGGER_PATH || "/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocs));
|
|
45
|
-
|
|
46
|
-
const routes = await this.#loadRoutesFromFolder(path.join(process.cwd(), "src", "routes"));
|
|
47
|
-
|
|
48
|
-
routes.forEach((route) => {
|
|
49
|
-
const methods = ['post', 'get', 'put', 'delete']
|
|
50
|
-
for (const methodName of methods) {
|
|
51
|
-
const method = route[methodName];
|
|
52
|
-
if(!method) continue;
|
|
53
|
-
this.#container.get('logger').debug(`Registering route: [${methodName}] ${route.path}`);
|
|
54
|
-
const middlewares = route?.middlewares?.[methodName] || [];
|
|
55
|
-
for (const mw of middlewares) {
|
|
56
|
-
if (!(mw instanceof Middleware)) {
|
|
57
|
-
throw new Error(`Middleware for route ${route.path} is not an instance of Middleware`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
this.#app[methodName](
|
|
62
|
-
route.path,
|
|
63
|
-
...middlewares.map(mw => async (req, res, next) => {
|
|
64
|
-
try {
|
|
65
|
-
await mw.handle({ container: this.#container, request: req, response: res, next })
|
|
66
|
-
} catch (error) {
|
|
67
|
-
this.#container.get('logger').error(`Error in middleware for route [${methodName}] ${route.path}: ${error.message}`);
|
|
68
|
-
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
69
|
-
}
|
|
70
|
-
}),
|
|
71
|
-
async (req, res) => {
|
|
72
|
-
try {
|
|
73
|
-
const routeResponse = await route[methodName]({ container: this.#container, request: req, response: res });
|
|
74
|
-
if (routeResponse instanceof RedirectResponse) return res.redirect(routeResponse.url);
|
|
75
|
-
|
|
76
|
-
// Check if response is a buffer (file download)
|
|
77
|
-
if (Buffer.isBuffer(routeResponse)) {
|
|
78
|
-
const filename = req.query.filename || 'download';
|
|
79
|
-
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
80
|
-
res.setHeader('Content-Type', 'application/octet-stream');
|
|
81
|
-
return res.send(routeResponse);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const status = routeResponse?.status || 200;
|
|
85
|
-
return res.status(status).json({
|
|
86
|
-
...routeResponse,
|
|
87
|
-
success: routeResponse?.success !== false,
|
|
88
|
-
});
|
|
89
|
-
} catch (error) {
|
|
90
|
-
this.#container.get('logger').error(`Error in route [${methodName}] ${route.path}: ${error.message}`);
|
|
91
|
-
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Attach Express to HTTP server - this allows dynamic route registration
|
|
98
|
-
this.#httpServer.removeAllListeners("request");
|
|
99
|
-
this.#httpServer.on("request", this.#app);
|
|
100
|
-
|
|
101
|
-
this.#container.get('logger').info(`Express is running on http://localhost:${httpServer.address().port}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async registerRoutes(routes) {
|
|
105
|
-
const methods = ['post', 'get', 'put', 'delete']
|
|
106
|
-
for (const route of routes) {
|
|
107
|
-
for (const methodName of methods) {
|
|
108
|
-
const method = route[methodName];
|
|
109
|
-
if(!method) continue;
|
|
110
|
-
this.#container.get('logger').debug(`Registering route: [${methodName}] ${route.path}`);
|
|
111
|
-
const middlewares = route?.middlewares?.[methodName] || [];
|
|
112
|
-
for (const mw of middlewares) {
|
|
113
|
-
if (!(mw instanceof Middleware)) {
|
|
114
|
-
throw new Error(`Middleware for route ${route.path} is not an instance of Middleware`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
this.#app[methodName](
|
|
118
|
-
route.path,
|
|
119
|
-
...middlewares.map(mw => async (req, res, next) => {
|
|
120
|
-
try {
|
|
121
|
-
await mw.handle({ container: this.#container, request: req, response: res, next })
|
|
122
|
-
} catch (error) {
|
|
123
|
-
this.#container.get('logger').error(`Error in middleware for route [${methodName}] ${route.path}: ${error.message}`);
|
|
124
|
-
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
125
|
-
}
|
|
126
|
-
}),
|
|
127
|
-
async (req, res) => {
|
|
128
|
-
try {
|
|
129
|
-
const routeResponse = await route[methodName]({ container: this.#container, request: req, response: res });
|
|
130
|
-
if (routeResponse instanceof RedirectResponse) return res.redirect(routeResponse.url);
|
|
131
|
-
|
|
132
|
-
// Check if response is a buffer (file download)
|
|
133
|
-
if (Buffer.isBuffer(routeResponse)) {
|
|
134
|
-
const filename = req.query.filename || 'download';
|
|
135
|
-
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
136
|
-
res.setHeader('Content-Type', 'application/octet-stream');
|
|
137
|
-
return res.send(routeResponse);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const status = routeResponse?.status || 200;
|
|
141
|
-
return res.status(status).json({
|
|
142
|
-
...routeResponse,
|
|
143
|
-
success: routeResponse?.success !== false,
|
|
144
|
-
});
|
|
145
|
-
} catch (error) {
|
|
146
|
-
this.#container.get('logger').error(`Error in route [${methodName}] ${route.path}: ${error.message}`);
|
|
147
|
-
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
this.#httpServer.removeAllListeners("request");
|
|
154
|
-
this.#httpServer.on("request", this.#app);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async regiterRawAllRoutes(path, handler) {
|
|
158
|
-
this.#app.all(path, handler);
|
|
159
|
-
|
|
160
|
-
this.#httpServer.removeAllListeners("request");
|
|
161
|
-
this.#httpServer.on("request", this.#app);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async #loadCorsOrCreateDefault() {
|
|
165
|
-
let corsOptions = {};
|
|
166
|
-
const corsConfigPath = path.join(process.cwd(), "config", "cors.js");
|
|
167
|
-
if (fs.existsSync(corsConfigPath)) {
|
|
168
|
-
this.#container.get('logger').info("Loading CORS configuration from config/cors.js");
|
|
169
|
-
corsOptions = require(corsConfigPath);
|
|
170
|
-
} else {
|
|
171
|
-
this.#container.get('logger').info("No CORS configuration found. Creating default config/cors.js");
|
|
172
|
-
fs.mkdirSync(path.join(process.cwd(), "config"), { recursive: true });
|
|
173
|
-
this.#container.get('template-manager').installFile('default/config/cors', corsConfigPath);
|
|
174
|
-
corsOptions = false;
|
|
175
|
-
}
|
|
176
|
-
return corsOptions;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async #loadRoutesFromFolder(routesFolder) {
|
|
180
|
-
this.#createRoutesFolder(routesFolder);
|
|
181
|
-
const routes = (await fg('**/*.js', { cwd: routesFolder })).map((rt) => {
|
|
182
|
-
const route = require(path.join(routesFolder, rt));
|
|
183
|
-
if(!(route.prototype instanceof Route)) throw new Error(`${rt} is not a valid route`);
|
|
184
|
-
let routePath = `/${rt.replace('.js', '')}`;
|
|
185
|
-
routePath = routePath.replaceAll('index', '/');
|
|
186
|
-
routePath = routePath.replaceAll('//', '/');
|
|
187
|
-
routePath = routePath.replace(/\[([^\]]+)\]/g, ':$1');
|
|
188
|
-
|
|
189
|
-
return new route(routePath);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
routes.sort((a, b) => {
|
|
193
|
-
const aSegments = a.path.split('/').filter(Boolean);
|
|
194
|
-
const bSegments = b.path.split('/').filter(Boolean);
|
|
195
|
-
const aDynCount = aSegments.filter(s => s.startsWith(':')).length;
|
|
196
|
-
const bDynCount = bSegments.filter(s => s.startsWith(':')).length;
|
|
197
|
-
if (aDynCount !== bDynCount) return aDynCount - bDynCount;
|
|
198
|
-
const aFirstDyn = aSegments.findIndex(s => s.startsWith(':'));
|
|
199
|
-
const bFirstDyn = bSegments.findIndex(s => s.startsWith(':'));
|
|
200
|
-
return (bFirstDyn === -1 ? Infinity : bFirstDyn) - (aFirstDyn === -1 ? Infinity : aFirstDyn);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
return routes;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
#createRoutesFolder(routesFolder, overwrite = false) {
|
|
207
|
-
if (fs.existsSync(routesFolder) && !overwrite) return;
|
|
208
|
-
this.#container.get('logger').info(`Creating routes folder at ${routesFolder}`);
|
|
209
|
-
fs.mkdirSync(routesFolder);
|
|
210
|
-
this.#container.get('template-manager').installFile('default/src/routes/index', path.join(routesFolder, "index.js"));
|
|
211
|
-
fs.mkdirSync(path.join(routesFolder, "[test]"));
|
|
212
|
-
this.#container.get('template-manager').installFile('default/src/routes/[test]/message', path.join(routesFolder, "[test]", "message.js"));
|
|
213
|
-
fs.mkdirSync(path.join(process.cwd(), "src", "middlewares"));
|
|
214
|
-
this.#container.get('template-manager').installFile('default/src/middlewares/default', path.join(process.cwd(), "src", "middlewares", "default.js"));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async #loadSwaggerOrCreateDefault() {
|
|
218
|
-
let swaggerOptions = {};
|
|
219
|
-
const swaggerConfigPath = path.join(process.cwd(), "config", "swagger.js");
|
|
220
|
-
if (fs.existsSync(swaggerConfigPath)) {
|
|
221
|
-
this.#container.get('logger').info("Loading Swagger configuration from config/swagger.js");
|
|
222
|
-
swaggerOptions = require(swaggerConfigPath);
|
|
223
|
-
} else {
|
|
224
|
-
this.#container.get('logger').info("No Swagger configuration found. Creating default config/swagger.js");
|
|
225
|
-
fs.mkdirSync(path.join(process.cwd(), "config"), { recursive: true });
|
|
226
|
-
this.#container.get('template-manager').installFile('default/config/swagger', swaggerConfigPath);
|
|
227
|
-
swaggerOptions = {
|
|
228
|
-
swaggerDefinition: {
|
|
229
|
-
openapi: '3.0.0',
|
|
230
|
-
info: {
|
|
231
|
-
title: "API Documentation",
|
|
232
|
-
version: require(path.join(process.cwd(), "package.json")).version || "1.0.0",
|
|
233
|
-
description: "API Documentation generated by Swagger",
|
|
234
|
-
},
|
|
235
|
-
servers: [
|
|
236
|
-
{ url: `http://localhost:3000` }
|
|
237
|
-
],
|
|
238
|
-
},
|
|
239
|
-
apis: [path.join(process.cwd(), "src", "routes", "**", "*.yml")]
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
return swaggerOptions;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
app() {
|
|
247
|
-
return this.#app;
|
|
248
|
-
}
|
|
1
|
+
const Service = require("../Service");
|
|
2
|
+
const express = require("express");
|
|
3
|
+
const cors = require("cors");
|
|
4
|
+
const Route = require("./Route");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const fg = require('fast-glob');
|
|
8
|
+
const Middleware = require("./Middleware");
|
|
9
|
+
const swaggerJsDoc = require("swagger-jsdoc");
|
|
10
|
+
const swaggerUi = require("swagger-ui-express");
|
|
11
|
+
const RedirectResponse = require("./RedirectResponse");
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
module.exports = class Express extends Service {
|
|
15
|
+
#container;
|
|
16
|
+
#app;
|
|
17
|
+
#httpServer;
|
|
18
|
+
|
|
19
|
+
constructor(container) {
|
|
20
|
+
super("express");
|
|
21
|
+
this.#container = container;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async boot({ httpServer } = {}) {
|
|
25
|
+
if (!httpServer) {
|
|
26
|
+
throw new Error("HTTP server is not available");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.#httpServer = httpServer;
|
|
30
|
+
this.#app = express();
|
|
31
|
+
|
|
32
|
+
this.#app.use(express.json({ limit: `100mb` }))
|
|
33
|
+
|
|
34
|
+
const corsOptions = await this.#loadCorsOrCreateDefault();
|
|
35
|
+
|
|
36
|
+
if(corsOptions) this.#app.use(cors(corsOptions));
|
|
37
|
+
|
|
38
|
+
// Swagger setup
|
|
39
|
+
const swaggerOptions = {
|
|
40
|
+
...(await this.#loadSwaggerOrCreateDefault()),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const swaggerDocs = swaggerJsDoc(swaggerOptions);
|
|
44
|
+
this.#app.use(process?.env?.SWAGGER_PATH || "/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocs));
|
|
45
|
+
|
|
46
|
+
const routes = await this.#loadRoutesFromFolder(path.join(process.cwd(), "src", "routes"));
|
|
47
|
+
|
|
48
|
+
routes.forEach((route) => {
|
|
49
|
+
const methods = ['post', 'get', 'put', 'delete']
|
|
50
|
+
for (const methodName of methods) {
|
|
51
|
+
const method = route[methodName];
|
|
52
|
+
if(!method) continue;
|
|
53
|
+
this.#container.get('logger').debug(`Registering route: [${methodName}] ${route.path}`);
|
|
54
|
+
const middlewares = route?.middlewares?.[methodName] || [];
|
|
55
|
+
for (const mw of middlewares) {
|
|
56
|
+
if (!(mw instanceof Middleware)) {
|
|
57
|
+
throw new Error(`Middleware for route ${route.path} is not an instance of Middleware`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.#app[methodName](
|
|
62
|
+
route.path,
|
|
63
|
+
...middlewares.map(mw => async (req, res, next) => {
|
|
64
|
+
try {
|
|
65
|
+
await mw.handle({ container: this.#container, request: req, response: res, next })
|
|
66
|
+
} catch (error) {
|
|
67
|
+
this.#container.get('logger').error(`Error in middleware for route [${methodName}] ${route.path}: ${error.message}`);
|
|
68
|
+
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const routeResponse = await route[methodName]({ container: this.#container, request: req, response: res });
|
|
74
|
+
if (routeResponse instanceof RedirectResponse) return res.redirect(routeResponse.url);
|
|
75
|
+
|
|
76
|
+
// Check if response is a buffer (file download)
|
|
77
|
+
if (Buffer.isBuffer(routeResponse)) {
|
|
78
|
+
const filename = req.query.filename || 'download';
|
|
79
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
80
|
+
res.setHeader('Content-Type', 'application/octet-stream');
|
|
81
|
+
return res.send(routeResponse);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const status = routeResponse?.status || 200;
|
|
85
|
+
return res.status(status).json({
|
|
86
|
+
...routeResponse,
|
|
87
|
+
success: routeResponse?.success !== false,
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.#container.get('logger').error(`Error in route [${methodName}] ${route.path}: ${error.message}`);
|
|
91
|
+
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Attach Express to HTTP server - this allows dynamic route registration
|
|
98
|
+
this.#httpServer.removeAllListeners("request");
|
|
99
|
+
this.#httpServer.on("request", this.#app);
|
|
100
|
+
|
|
101
|
+
this.#container.get('logger').info(`Express is running on http://localhost:${httpServer.address().port}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async registerRoutes(routes) {
|
|
105
|
+
const methods = ['post', 'get', 'put', 'delete']
|
|
106
|
+
for (const route of routes) {
|
|
107
|
+
for (const methodName of methods) {
|
|
108
|
+
const method = route[methodName];
|
|
109
|
+
if(!method) continue;
|
|
110
|
+
this.#container.get('logger').debug(`Registering route: [${methodName}] ${route.path}`);
|
|
111
|
+
const middlewares = route?.middlewares?.[methodName] || [];
|
|
112
|
+
for (const mw of middlewares) {
|
|
113
|
+
if (!(mw instanceof Middleware)) {
|
|
114
|
+
throw new Error(`Middleware for route ${route.path} is not an instance of Middleware`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.#app[methodName](
|
|
118
|
+
route.path,
|
|
119
|
+
...middlewares.map(mw => async (req, res, next) => {
|
|
120
|
+
try {
|
|
121
|
+
await mw.handle({ container: this.#container, request: req, response: res, next })
|
|
122
|
+
} catch (error) {
|
|
123
|
+
this.#container.get('logger').error(`Error in middleware for route [${methodName}] ${route.path}: ${error.message}`);
|
|
124
|
+
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const routeResponse = await route[methodName]({ container: this.#container, request: req, response: res });
|
|
130
|
+
if (routeResponse instanceof RedirectResponse) return res.redirect(routeResponse.url);
|
|
131
|
+
|
|
132
|
+
// Check if response is a buffer (file download)
|
|
133
|
+
if (Buffer.isBuffer(routeResponse)) {
|
|
134
|
+
const filename = req.query.filename || 'download';
|
|
135
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
136
|
+
res.setHeader('Content-Type', 'application/octet-stream');
|
|
137
|
+
return res.send(routeResponse);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const status = routeResponse?.status || 200;
|
|
141
|
+
return res.status(status).json({
|
|
142
|
+
...routeResponse,
|
|
143
|
+
success: routeResponse?.success !== false,
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.#container.get('logger').error(`Error in route [${methodName}] ${route.path}: ${error.message}`);
|
|
147
|
+
return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.#httpServer.removeAllListeners("request");
|
|
154
|
+
this.#httpServer.on("request", this.#app);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async regiterRawAllRoutes(path, handler) {
|
|
158
|
+
this.#app.all(path, handler);
|
|
159
|
+
|
|
160
|
+
this.#httpServer.removeAllListeners("request");
|
|
161
|
+
this.#httpServer.on("request", this.#app);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async #loadCorsOrCreateDefault() {
|
|
165
|
+
let corsOptions = {};
|
|
166
|
+
const corsConfigPath = path.join(process.cwd(), "config", "cors.js");
|
|
167
|
+
if (fs.existsSync(corsConfigPath)) {
|
|
168
|
+
this.#container.get('logger').info("Loading CORS configuration from config/cors.js");
|
|
169
|
+
corsOptions = require(corsConfigPath);
|
|
170
|
+
} else {
|
|
171
|
+
this.#container.get('logger').info("No CORS configuration found. Creating default config/cors.js");
|
|
172
|
+
fs.mkdirSync(path.join(process.cwd(), "config"), { recursive: true });
|
|
173
|
+
this.#container.get('template-manager').installFile('default/config/cors', corsConfigPath);
|
|
174
|
+
corsOptions = false;
|
|
175
|
+
}
|
|
176
|
+
return corsOptions;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async #loadRoutesFromFolder(routesFolder) {
|
|
180
|
+
this.#createRoutesFolder(routesFolder);
|
|
181
|
+
const routes = (await fg('**/*.js', { cwd: routesFolder })).map((rt) => {
|
|
182
|
+
const route = require(path.join(routesFolder, rt));
|
|
183
|
+
if(!(route.prototype instanceof Route)) throw new Error(`${rt} is not a valid route`);
|
|
184
|
+
let routePath = `/${rt.replace('.js', '')}`;
|
|
185
|
+
routePath = routePath.replaceAll('index', '/');
|
|
186
|
+
routePath = routePath.replaceAll('//', '/');
|
|
187
|
+
routePath = routePath.replace(/\[([^\]]+)\]/g, ':$1');
|
|
188
|
+
|
|
189
|
+
return new route(routePath);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
routes.sort((a, b) => {
|
|
193
|
+
const aSegments = a.path.split('/').filter(Boolean);
|
|
194
|
+
const bSegments = b.path.split('/').filter(Boolean);
|
|
195
|
+
const aDynCount = aSegments.filter(s => s.startsWith(':')).length;
|
|
196
|
+
const bDynCount = bSegments.filter(s => s.startsWith(':')).length;
|
|
197
|
+
if (aDynCount !== bDynCount) return aDynCount - bDynCount;
|
|
198
|
+
const aFirstDyn = aSegments.findIndex(s => s.startsWith(':'));
|
|
199
|
+
const bFirstDyn = bSegments.findIndex(s => s.startsWith(':'));
|
|
200
|
+
return (bFirstDyn === -1 ? Infinity : bFirstDyn) - (aFirstDyn === -1 ? Infinity : aFirstDyn);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return routes;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#createRoutesFolder(routesFolder, overwrite = false) {
|
|
207
|
+
if (fs.existsSync(routesFolder) && !overwrite) return;
|
|
208
|
+
this.#container.get('logger').info(`Creating routes folder at ${routesFolder}`);
|
|
209
|
+
fs.mkdirSync(routesFolder);
|
|
210
|
+
this.#container.get('template-manager').installFile('default/src/routes/index', path.join(routesFolder, "index.js"));
|
|
211
|
+
fs.mkdirSync(path.join(routesFolder, "[test]"));
|
|
212
|
+
this.#container.get('template-manager').installFile('default/src/routes/[test]/message', path.join(routesFolder, "[test]", "message.js"));
|
|
213
|
+
fs.mkdirSync(path.join(process.cwd(), "src", "middlewares"));
|
|
214
|
+
this.#container.get('template-manager').installFile('default/src/middlewares/default', path.join(process.cwd(), "src", "middlewares", "default.js"));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async #loadSwaggerOrCreateDefault() {
|
|
218
|
+
let swaggerOptions = {};
|
|
219
|
+
const swaggerConfigPath = path.join(process.cwd(), "config", "swagger.js");
|
|
220
|
+
if (fs.existsSync(swaggerConfigPath)) {
|
|
221
|
+
this.#container.get('logger').info("Loading Swagger configuration from config/swagger.js");
|
|
222
|
+
swaggerOptions = require(swaggerConfigPath);
|
|
223
|
+
} else {
|
|
224
|
+
this.#container.get('logger').info("No Swagger configuration found. Creating default config/swagger.js");
|
|
225
|
+
fs.mkdirSync(path.join(process.cwd(), "config"), { recursive: true });
|
|
226
|
+
this.#container.get('template-manager').installFile('default/config/swagger', swaggerConfigPath);
|
|
227
|
+
swaggerOptions = {
|
|
228
|
+
swaggerDefinition: {
|
|
229
|
+
openapi: '3.0.0',
|
|
230
|
+
info: {
|
|
231
|
+
title: "API Documentation",
|
|
232
|
+
version: require(path.join(process.cwd(), "package.json")).version || "1.0.0",
|
|
233
|
+
description: "API Documentation generated by Swagger",
|
|
234
|
+
},
|
|
235
|
+
servers: [
|
|
236
|
+
{ url: `http://localhost:3000` }
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
apis: [path.join(process.cwd(), "src", "routes", "**", "*.yml")]
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return swaggerOptions;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
app() {
|
|
247
|
+
return this.#app;
|
|
248
|
+
}
|
|
249
249
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
module.exports = class Middleware {
|
|
2
|
-
constructor() {
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
handle({ container, request, response, next }) {
|
|
6
|
-
throw new Error("You should implement 'handle()' method on your route");
|
|
7
|
-
}
|
|
1
|
+
module.exports = class Middleware {
|
|
2
|
+
constructor() {
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
handle({ container, request, response, next }) {
|
|
6
|
+
throw new Error("You should implement 'handle()' method on your route");
|
|
7
|
+
}
|
|
8
8
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
module.exports = class RedirectResponse {
|
|
2
|
-
url;
|
|
3
|
-
|
|
4
|
-
constructor (url) {
|
|
5
|
-
if (!url) throw new Error('url is required')
|
|
6
|
-
|
|
7
|
-
this.url = url
|
|
8
|
-
}
|
|
1
|
+
module.exports = class RedirectResponse {
|
|
2
|
+
url;
|
|
3
|
+
|
|
4
|
+
constructor (url) {
|
|
5
|
+
if (!url) throw new Error('url is required')
|
|
6
|
+
|
|
7
|
+
this.url = url
|
|
8
|
+
}
|
|
9
9
|
}
|