tss-stack 1.2.3 → 1.3.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/bin/cli.js +518 -510
- package/package.json +1 -1
- package/src/generators/backend.js +404 -358
- package/src/generators/database.js +52 -55
- package/src/generators/frontend.js +794 -542
- package/src/generators/utils.js +113 -60
|
@@ -1,358 +1,404 @@
|
|
|
1
|
-
const fs = require("fs-extra");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
|
|
4
|
-
const { toPascal, toRoute } = require("./utils");
|
|
5
|
-
|
|
6
|
-
async function generateBackend(config) {
|
|
7
|
-
const { dbName, port, tables, needsAuth, targetDir } = config;
|
|
8
|
-
const root = path.join(targetDir, "backend-project");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
res.json({ message: "
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
res.json({ message: "
|
|
199
|
-
})
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const { toPascal, toRoute, inferReportConfig } = require("./utils");
|
|
5
|
+
|
|
6
|
+
async function generateBackend(config) {
|
|
7
|
+
const { dbName, port, tables, needsAuth, needsReports, targetDir } = config;
|
|
8
|
+
const root = path.join(targetDir, "backend-project");
|
|
9
|
+
|
|
10
|
+
// ── package.json ───────────────────────────────────────────────────────
|
|
11
|
+
const dependencies = {
|
|
12
|
+
express: "^4.18.2",
|
|
13
|
+
mysql2: "^3.6.0",
|
|
14
|
+
cors: "^2.8.5",
|
|
15
|
+
dotenv: "^16.3.1",
|
|
16
|
+
helmet: "^7.1.0",
|
|
17
|
+
"express-session": "^1.17.3",
|
|
18
|
+
bcryptjs: "^2.4.3",
|
|
19
|
+
"express-rate-limit": "^7.1.5",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
await fs.outputFile(
|
|
23
|
+
path.join(root, "package.json"),
|
|
24
|
+
JSON.stringify(
|
|
25
|
+
{
|
|
26
|
+
name: "backend-project",
|
|
27
|
+
version: "1.0.0",
|
|
28
|
+
scripts: { dev: "nodemon server.js", start: "node server.js" },
|
|
29
|
+
dependencies: needsAuth
|
|
30
|
+
? dependencies
|
|
31
|
+
: {
|
|
32
|
+
express: dependencies.express,
|
|
33
|
+
mysql2: dependencies.mysql2,
|
|
34
|
+
cors: dependencies.cors,
|
|
35
|
+
dotenv: dependencies.dotenv,
|
|
36
|
+
helmet: dependencies.helmet,
|
|
37
|
+
},
|
|
38
|
+
devDependencies: { nodemon: "^3.0.1" },
|
|
39
|
+
},
|
|
40
|
+
null,
|
|
41
|
+
2
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// ── .env.example ───────────────────────────────────────────────────────
|
|
46
|
+
await fs.outputFile(
|
|
47
|
+
path.join(root, ".env.example"),
|
|
48
|
+
`DB_HOST=localhost
|
|
49
|
+
DB_USER=root
|
|
50
|
+
DB_PASSWORD=your_password_here
|
|
51
|
+
DB_NAME=${dbName}
|
|
52
|
+
PORT=${port}
|
|
53
|
+
SESSION_SECRET=change_me_to_random_string
|
|
54
|
+
CLIENT_URL=http://localhost:5173
|
|
55
|
+
NODE_ENV=development
|
|
56
|
+
`
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// ── .gitignore ─────────────────────────────────────────────────────────
|
|
60
|
+
await fs.outputFile(
|
|
61
|
+
path.join(root, ".gitignore"),
|
|
62
|
+
`node_modules/\n.env\n.env.local\n*.log\nnpm-debug.log*\n.DS_Store\n`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ── config/db.js — promise-based pool ─────────────────────────────────
|
|
66
|
+
await fs.outputFile(
|
|
67
|
+
path.join(root, "config", "db.js"),
|
|
68
|
+
`const mysql = require("mysql2");
|
|
69
|
+
require("dotenv").config();
|
|
70
|
+
|
|
71
|
+
// Promise-based pool — use "await db.query(...)" everywhere
|
|
72
|
+
const pool = mysql.createPool({
|
|
73
|
+
host: process.env.DB_HOST,
|
|
74
|
+
user: process.env.DB_USER,
|
|
75
|
+
password: process.env.DB_PASSWORD,
|
|
76
|
+
database: process.env.DB_NAME,
|
|
77
|
+
waitForConnections: true,
|
|
78
|
+
connectionLimit: 10,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
pool.getConnection((err, connection) => {
|
|
82
|
+
if (err) {
|
|
83
|
+
console.error("[ERROR] MySQL connection failed:", err.message);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
console.log("[✓] MySQL connected");
|
|
87
|
+
connection.release();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
module.exports = pool.promise();
|
|
91
|
+
`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// ── middleware/auth.js ─────────────────────────────────────────────────
|
|
95
|
+
if (needsAuth) {
|
|
96
|
+
await fs.outputFile(
|
|
97
|
+
path.join(root, "middleware", "auth.js"),
|
|
98
|
+
`module.exports = (req, res, next) => {
|
|
99
|
+
if (req.session && req.session.user) return next();
|
|
100
|
+
return res.status(401).json({ message: "Unauthorized" });
|
|
101
|
+
};
|
|
102
|
+
`
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// ── routes/auth.js ─────────────────────────────────────────────────
|
|
106
|
+
await fs.outputFile(
|
|
107
|
+
path.join(root, "routes", "auth.js"),
|
|
108
|
+
`const express = require("express");
|
|
109
|
+
const bcrypt = require("bcryptjs");
|
|
110
|
+
const rateLimit = require("express-rate-limit");
|
|
111
|
+
const router = express.Router();
|
|
112
|
+
const db = require("../config/db");
|
|
113
|
+
const isAuthenticated = require("../middleware/auth");
|
|
114
|
+
|
|
115
|
+
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 50 });
|
|
116
|
+
|
|
117
|
+
router.post("/register", authLimiter, async (req, res) => {
|
|
118
|
+
try {
|
|
119
|
+
const { username, password } = req.body;
|
|
120
|
+
if (!username || !password)
|
|
121
|
+
return res.status(400).json({ message: "Username and password required" });
|
|
122
|
+
if (password.length < 6)
|
|
123
|
+
return res.status(400).json({ message: "Password must be at least 6 characters" });
|
|
124
|
+
|
|
125
|
+
const hash = await bcrypt.hash(password, 10);
|
|
126
|
+
try {
|
|
127
|
+
await db.query("INSERT INTO users (username, password) VALUES (?, ?)", [username, hash]);
|
|
128
|
+
res.json({ message: "User registered successfully" });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (err.code === "ER_DUP_ENTRY")
|
|
131
|
+
return res.status(400).json({ message: "Username already exists" });
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
res.status(500).json({ error: err.message });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
router.post("/login", authLimiter, async (req, res) => {
|
|
140
|
+
try {
|
|
141
|
+
const { username, password } = req.body;
|
|
142
|
+
if (!username || !password)
|
|
143
|
+
return res.status(400).json({ message: "Username and password required" });
|
|
144
|
+
|
|
145
|
+
const [results] = await db.query(
|
|
146
|
+
"SELECT id, username, password FROM users WHERE username = ? LIMIT 1",
|
|
147
|
+
[username]
|
|
148
|
+
);
|
|
149
|
+
if (results.length === 0)
|
|
150
|
+
return res.status(401).json({ message: "Invalid credentials" });
|
|
151
|
+
|
|
152
|
+
const user = results[0];
|
|
153
|
+
const match = await bcrypt.compare(password, user.password);
|
|
154
|
+
if (!match) return res.status(401).json({ message: "Invalid credentials" });
|
|
155
|
+
|
|
156
|
+
req.session.user = { id: user.id, username: user.username };
|
|
157
|
+
res.json({ message: "Login successful", user: req.session.user });
|
|
158
|
+
} catch (err) {
|
|
159
|
+
res.status(500).json({ error: err.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
router.get("/me", isAuthenticated, (req, res) => res.json(req.session.user));
|
|
164
|
+
|
|
165
|
+
router.post("/logout", (req, res) => {
|
|
166
|
+
req.session.destroy((err) => {
|
|
167
|
+
if (err) return res.status(500).json({ error: "Logout failed" });
|
|
168
|
+
res.json({ message: "Logged out" });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
module.exports = router;
|
|
173
|
+
`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── One route file per table ───────────────────────────────────────────
|
|
178
|
+
for (const table of tables) {
|
|
179
|
+
const routeName = toRoute(table.name);
|
|
180
|
+
const insertFields = table.fields.join(", ");
|
|
181
|
+
const placeholders = table.fields.map(() => "?").join(", ");
|
|
182
|
+
const values = table.fields.map((f) => `req.body.${f}`).join(", ");
|
|
183
|
+
const updateSet = table.fields.map((f) => `${f} = ?`).join(", ");
|
|
184
|
+
const updateValues = [...table.fields.map((f) => `req.body.${f}`), "req.params.id"].join(", ");
|
|
185
|
+
|
|
186
|
+
let route = `const express = require("express");
|
|
187
|
+
const router = express.Router();
|
|
188
|
+
const db = require("../config/db");
|
|
189
|
+
${needsAuth ? 'const isAuthenticated = require("../middleware/auth");' : ""}
|
|
190
|
+
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
if (table.operations.includes("insert")) {
|
|
194
|
+
route += `router.post("/", ${needsAuth ? "isAuthenticated, " : ""}async (req, res) => {
|
|
195
|
+
try {
|
|
196
|
+
const sql = "INSERT INTO ${table.name} (${insertFields}) VALUES (${placeholders})";
|
|
197
|
+
await db.query(sql, [${values}]);
|
|
198
|
+
res.json({ message: "${toPascal(table.name)} created" });
|
|
199
|
+
} catch (err) {
|
|
200
|
+
res.status(500).json({ error: err.message });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (table.operations.includes("select")) {
|
|
208
|
+
route += `router.get("/", ${needsAuth ? "isAuthenticated, " : ""}async (req, res) => {
|
|
209
|
+
try {
|
|
210
|
+
const [rows] = await db.query("SELECT * FROM ${table.name}");
|
|
211
|
+
res.json(rows);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
res.status(500).json({ error: err.message });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (table.operations.includes("update")) {
|
|
221
|
+
route += `router.put("/:id", ${needsAuth ? "isAuthenticated, " : ""}async (req, res) => {
|
|
222
|
+
try {
|
|
223
|
+
const sql = "UPDATE ${table.name} SET ${updateSet} WHERE id = ?";
|
|
224
|
+
await db.query(sql, [${updateValues}]);
|
|
225
|
+
res.json({ message: "${toPascal(table.name)} updated" });
|
|
226
|
+
} catch (err) {
|
|
227
|
+
res.status(500).json({ error: err.message });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (table.operations.includes("delete")) {
|
|
235
|
+
route += `router.delete("/:id", ${needsAuth ? "isAuthenticated, " : ""}async (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
await db.query("DELETE FROM ${table.name} WHERE id = ?", [req.params.id]);
|
|
238
|
+
res.json({ message: "${toPascal(table.name)} deleted" });
|
|
239
|
+
} catch (err) {
|
|
240
|
+
res.status(500).json({ error: err.message });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
route += `module.exports = router;\n`;
|
|
248
|
+
await fs.outputFile(path.join(root, "routes", `${routeName}.js`), route);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── STEP 6 — reports.js route ──────────────────────────────────────────
|
|
252
|
+
// Generated only when needsReports is true AND at least one table opted in.
|
|
253
|
+
// Uses table-specific aggregate SELECT queries built from inferReportConfig,
|
|
254
|
+
// so each endpoint returns real KPI data rather than a raw SELECT *.
|
|
255
|
+
// The table whitelist prevents SQL injection via the :table param.
|
|
256
|
+
const reportTables = needsReports ? tables.filter((t) => t.reports) : [];
|
|
257
|
+
|
|
258
|
+
if (reportTables.length > 0) {
|
|
259
|
+
const allowedTablesList = reportTables.map((t) => `"${t.name}"`).join(", ");
|
|
260
|
+
|
|
261
|
+
// Build one SQL SELECT per table that aggregates metric fields
|
|
262
|
+
const tableQueryMap = reportTables.map((t) => {
|
|
263
|
+
const rc = inferReportConfig(t);
|
|
264
|
+
|
|
265
|
+
// Build SELECT clause: SUM/COUNT per metric + COUNT(*) as total_records
|
|
266
|
+
const selectParts = ["COUNT(*) AS total_records"];
|
|
267
|
+
|
|
268
|
+
for (const metric of rc.metrics) {
|
|
269
|
+
selectParts.push(`SUM(${metric}) AS ${metric}_sum`);
|
|
270
|
+
selectParts.push(`AVG(${metric}) AS ${metric}_avg`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const dim of rc.dimensions) {
|
|
274
|
+
selectParts.push(`${dim}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// If there are dimensions, group by them; otherwise a single aggregate row
|
|
278
|
+
const groupBy = rc.dimensions.length > 0
|
|
279
|
+
? `GROUP BY ${rc.dimensions.join(", ")}`
|
|
280
|
+
: "";
|
|
281
|
+
|
|
282
|
+
return ` "${t.name}": \`SELECT ${selectParts.join(", ")} FROM ${t.name} ${groupBy}\`.trim(),`;
|
|
283
|
+
}).join("\n");
|
|
284
|
+
|
|
285
|
+
await fs.outputFile(
|
|
286
|
+
path.join(root, "routes", "reports.js"),
|
|
287
|
+
`const express = require("express");
|
|
288
|
+
const router = express.Router();
|
|
289
|
+
const db = require("../config/db");
|
|
290
|
+
${needsAuth ? 'const isAuthenticated = require("../middleware/auth");' : ""}
|
|
291
|
+
|
|
292
|
+
// Whitelist of tables that have reporting enabled.
|
|
293
|
+
// This prevents arbitrary table names from reaching the database.
|
|
294
|
+
const ALLOWED_TABLES = [${allowedTablesList}];
|
|
295
|
+
|
|
296
|
+
// Pre-built aggregate queries per table — generated from your field names.
|
|
297
|
+
// Edit the SQL here if you need different aggregations.
|
|
298
|
+
const TABLE_QUERIES = {
|
|
299
|
+
${tableQueryMap}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// GET /reports/:table
|
|
303
|
+
// Returns aggregated KPI data for the requested table.
|
|
304
|
+
// Example: GET /reports/stock_out → { total_records, unit_price_sum, ... }
|
|
305
|
+
router.get("/:table", ${needsAuth ? "isAuthenticated, " : ""}async (req, res) => {
|
|
306
|
+
const { table } = req.params;
|
|
307
|
+
|
|
308
|
+
if (!ALLOWED_TABLES.includes(table)) {
|
|
309
|
+
return res.status(400).json({ message: "Report not available for this table" });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const [rows] = await db.query(TABLE_QUERIES[table]);
|
|
314
|
+
res.json({ table, data: rows });
|
|
315
|
+
} catch (err) {
|
|
316
|
+
res.status(500).json({ error: err.message });
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// GET /reports
|
|
321
|
+
// Returns a summary across all reportable tables in one request.
|
|
322
|
+
// The frontend Reports page uses this single endpoint.
|
|
323
|
+
router.get("/", ${needsAuth ? "isAuthenticated, " : ""}async (req, res) => {
|
|
324
|
+
try {
|
|
325
|
+
const results = {};
|
|
326
|
+
|
|
327
|
+
for (const table of ALLOWED_TABLES) {
|
|
328
|
+
const [rows] = await db.query(TABLE_QUERIES[table]);
|
|
329
|
+
results[table] = rows;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
res.json(results);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
res.status(500).json({ error: err.message });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
module.exports = router;
|
|
339
|
+
`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── server.js ──────────────────────────────────────────────────────────
|
|
344
|
+
const routeImports = tables
|
|
345
|
+
.map((t) => `const ${toPascal(t.name)}Route = require("./routes/${toRoute(t.name)}");`)
|
|
346
|
+
.join("\n");
|
|
347
|
+
|
|
348
|
+
const routeMounts = tables
|
|
349
|
+
.map((t) => `app.use("/${toRoute(t.name)}", ${toPascal(t.name)}Route);`)
|
|
350
|
+
.join("\n");
|
|
351
|
+
|
|
352
|
+
const authImport = needsAuth ? 'const authRoutes = require("./routes/auth");' : "";
|
|
353
|
+
const authMount = needsAuth ? 'app.use("/auth", authRoutes);' : "";
|
|
354
|
+
const reportsImport = reportTables.length > 0 ? 'const reportsRoute = require("./routes/reports");' : "";
|
|
355
|
+
const reportsMount = reportTables.length > 0 ? 'app.use("/reports", reportsRoute);' : "";
|
|
356
|
+
|
|
357
|
+
const sessionMiddleware = needsAuth
|
|
358
|
+
? `const session = require("express-session");
|
|
359
|
+
|
|
360
|
+
app.use(session({
|
|
361
|
+
secret: process.env.SESSION_SECRET || "change_me",
|
|
362
|
+
resave: false,
|
|
363
|
+
saveUninitialized: false,
|
|
364
|
+
cookie: { httpOnly: true, sameSite: "lax", secure: false },
|
|
365
|
+
}));
|
|
366
|
+
`
|
|
367
|
+
: "";
|
|
368
|
+
|
|
369
|
+
await fs.outputFile(
|
|
370
|
+
path.join(root, "server.js"),
|
|
371
|
+
`const express = require("express");
|
|
372
|
+
const cors = require("cors");
|
|
373
|
+
const helmet = require("helmet");
|
|
374
|
+
require("dotenv").config();
|
|
375
|
+
|
|
376
|
+
const app = express();
|
|
377
|
+
const db = require("./config/db");
|
|
378
|
+
${authImport}
|
|
379
|
+
${reportsImport}
|
|
380
|
+
${routeImports}
|
|
381
|
+
|
|
382
|
+
app.use(helmet());
|
|
383
|
+
app.use(cors({
|
|
384
|
+
origin: process.env.CLIENT_URL || "http://localhost:5173",
|
|
385
|
+
credentials: true,
|
|
386
|
+
}));
|
|
387
|
+
app.use(express.json());
|
|
388
|
+
${sessionMiddleware}
|
|
389
|
+
app.get("/health", (req, res) => res.json({ ok: true }));
|
|
390
|
+
${authMount}
|
|
391
|
+
${reportsMount}
|
|
392
|
+
${routeMounts}
|
|
393
|
+
|
|
394
|
+
const port = process.env.PORT || ${port};
|
|
395
|
+
app.listen(port, () => {
|
|
396
|
+
console.log(\`[✓] Server running on port \${port}\`);
|
|
397
|
+
});
|
|
398
|
+
`
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
console.log(" [✓] backend files");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = { generateBackend };
|