4bnode 3.5.2 → 4.0.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.
Files changed (43) hide show
  1. package/README.md +139 -114
  2. package/add-api.js +288 -156
  3. package/add-crud.js +226 -0
  4. package/add-login.js +152 -319
  5. package/add-mongo-delete.js +67 -138
  6. package/add-mongo-insert.js +87 -204
  7. package/add-mongo-read.js +100 -216
  8. package/add-mongo-schema.js +431 -199
  9. package/add-mongo-update.js +89 -202
  10. package/add-mongo.js +125 -218
  11. package/add-serialport.js +53 -98
  12. package/add-socket.js +43 -66
  13. package/add-websocket.js +68 -140
  14. package/index.js +288 -155
  15. package/lib/env.js +18 -0
  16. package/lib/indexFile.js +87 -0
  17. package/lib/names.js +21 -0
  18. package/lib/project.js +122 -0
  19. package/lib/routeFile.js +56 -0
  20. package/lib/ui.js +161 -0
  21. package/package.json +9 -16
  22. package/skeleton/dev-api.js +2785 -0
  23. package/skeleton/index.js +10 -3
  24. package/skeleton/package.json +9 -5
  25. package/skeleton/public/fonts/NotoSans-Bold.woff2 +0 -0
  26. package/skeleton/public/fonts/NotoSans-Light.woff2 +0 -0
  27. package/skeleton/public/fonts/NotoSans-Medium.woff2 +0 -0
  28. package/skeleton/public/fonts/NotoSans-Regular.woff2 +0 -0
  29. package/skeleton/public/fonts/NotoSans-SemiBold.woff2 +0 -0
  30. package/skeleton/public/images/mongo.png +0 -0
  31. package/skeleton/public/images/mysql.png +0 -0
  32. package/skeleton/public/images/postgresql.png +0 -0
  33. package/skeleton/public/images/sqlite.png +0 -0
  34. package/skeleton/public/index.html +7562 -64
  35. package/README_4bnode.md +0 -197
  36. package/src/auth/passport.js +0 -21
  37. package/src/db.js +0 -13
  38. package/test-server/.env.development +0 -1
  39. package/test-server/.env.production +0 -1
  40. package/test-server/index.js +0 -82
  41. package/test-server/package-lock.json +0 -1267
  42. package/test-server/package.json +0 -20
  43. package/test-server/public/index.html +0 -90
package/add-api.js CHANGED
@@ -2,199 +2,331 @@
2
2
 
3
3
  import fs from "fs";
4
4
  import path from "path";
5
- import readline from "readline";
6
- import chalk from "chalk";
7
-
8
- const rl = readline.createInterface({
9
- input: process.stdin,
10
- output: process.stdout,
11
- });
5
+ import { select, confirm, input, checkbox } from "@inquirer/prompts";
6
+ import { toCamelCase, sanitizeName } from "./lib/names.js";
7
+ import {
8
+ getProjectRoot,
9
+ listModels,
10
+ getModelSchemaDetailed,
11
+ } from "./lib/project.js";
12
+ import { addRouteRegistration } from "./lib/indexFile.js";
13
+ import {
14
+ showHeader,
15
+ showTaskDone,
16
+ showError,
17
+ showFileAction,
18
+ installDeps,
19
+ } from "./lib/ui.js";
12
20
 
13
21
  const methods = ["get", "post", "put", "delete", "patch"];
14
22
 
15
- function toCamelCase(str) {
16
- return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
23
+ function checkDuplicateMethod(apiFile, method) {
24
+ const content = fs.readFileSync(apiFile, "utf8");
25
+ return new RegExp(`router\\.${method}\\(`).test(content);
17
26
  }
18
27
 
19
- function checkDuplicateApi(endpointName) {
20
- const projectRoot = process.cwd();
21
- const apiDir = path.join(projectRoot, "src", "routes");
22
- const apiFile = path.join(apiDir, `${endpointName}.js`);
23
- return fs.existsSync(apiFile);
28
+ function formatFieldLabel(field) {
29
+ let label = `${field.name} (${field.type}`;
30
+ if (field.required) label += ", required";
31
+ if (field.unique) label += ", unique";
32
+ if (field.default !== undefined) label += `, default: ${field.default}`;
33
+ label += ")";
34
+ return label;
24
35
  }
25
36
 
26
- function checkDuplicateMethod(apiFile, method) {
27
- const apiContent = fs.readFileSync(apiFile, "utf8");
28
- const methodPattern = new RegExp(`router\\.${method}\\(`);
29
- return methodPattern.test(apiContent);
30
- }
37
+ async function collectCustomFields() {
38
+ const customFields = [];
39
+ let addMore = true;
31
40
 
32
- function createApi(endpointName, method, isAuthorized, selectedPath) {
33
- const projectRoot = process.cwd();
34
- const apiDir = path.join(projectRoot, "src", "routes");
35
- const apiFile = path.join(apiDir, `${endpointName}.js`);
36
- const variableName = toCamelCase(endpointName);
41
+ while (addMore) {
42
+ const fieldName = await input({
43
+ message: "Enter field name:",
44
+ validate: (v) => (v.trim() ? true : "Field name cannot be empty."),
45
+ });
37
46
 
38
- if (!fs.existsSync(apiDir)) {
39
- fs.mkdirSync(apiDir, { recursive: true });
40
- }
47
+ const fieldType = await select({
48
+ message: `Type for "${fieldName}":`,
49
+ choices: [
50
+ { name: "String", value: "String" },
51
+ { name: "Number", value: "Number" },
52
+ { name: "Boolean", value: "Boolean" },
53
+ { name: "Date", value: "Date" },
54
+ { name: "Array", value: "Array" },
55
+ { name: "Object", value: "Object" },
56
+ { name: "File (multipart)", value: "File" },
57
+ ],
58
+ });
41
59
 
42
- if (fs.existsSync(apiFile) && checkDuplicateMethod(apiFile, method)) {
43
- console.error(
44
- chalk.red(
45
- `API endpoint ${selectedPath} with method ${method.toUpperCase()} already exists. Choose a different method or endpoint name.`
46
- )
47
- );
48
- rl.close();
49
- return;
60
+ customFields.push({ name: fieldName.trim(), type: fieldType });
61
+
62
+ addMore = await confirm({
63
+ message: "Add another field?",
64
+ default: false,
65
+ });
50
66
  }
51
67
 
52
- let apiContent;
53
- const authMiddleware = isAuthorized ? "auth, " : "";
54
- const methodContent = `
55
- router.${method.toLowerCase()}('/', ${authMiddleware}async (req, res) => {
56
- res.json({ msg: '${endpointName} ${method.toUpperCase()} endpoint works' });
57
- });
58
- `;
68
+ return customFields;
69
+ }
59
70
 
60
- if (fs.existsSync(apiFile)) {
61
- // Read existing content and append the new method if not already present
62
- apiContent = fs.readFileSync(apiFile, "utf8");
63
- if (!apiContent.includes(methodContent.trim())) {
64
- const insertionIndex = apiContent.lastIndexOf("export default router;");
65
- apiContent =
66
- apiContent.slice(0, insertionIndex) +
67
- methodContent.trim() +
68
- "\n\n" +
69
- apiContent.slice(insertionIndex);
70
- }
71
- } else {
72
- // Create new content for the API file
73
- apiContent = `
74
- import express from 'express';
75
- ${isAuthorized ? "import auth from '../middleware/auth.js';" : ""}
76
- const router = express.Router();
71
+ function buildMethodBlock({
72
+ method,
73
+ endpointName,
74
+ authMiddleware,
75
+ fields,
76
+ multerUploadLine,
77
+ }) {
78
+ const middlewares = [];
79
+ if (authMiddleware) middlewares.push("auth");
80
+ if (multerUploadLine) middlewares.push(multerUploadLine);
77
81
 
78
- router.${method.toLowerCase()}('/', ${authMiddleware}async (req, res) => {
79
- res.json({ msg: '${endpointName} ${method.toUpperCase()} endpoint works' });
80
- });
82
+ const middlewareStr = middlewares.length
83
+ ? middlewares.join(", ") + ", "
84
+ : "";
81
85
 
82
- export default router;
83
- `;
86
+ if (fields.length === 0) {
87
+ return `
88
+ router.${method}('/', ${middlewareStr}async (req, res) => {
89
+ res.json({ msg: '${endpointName} ${method.toUpperCase()} endpoint works' });
90
+ });`;
84
91
  }
85
92
 
86
- fs.writeFileSync(apiFile, apiContent.trim(), "utf8");
93
+ const dataSource =
94
+ method === "get" || method === "delete" ? "req.query" : "req.body";
87
95
 
88
- // Update index.js to include the new route
89
- const indexPath = path.join(projectRoot, "index.js");
90
- const routeImport = `import ${variableName} from './src/routes/${endpointName}.js';\n`;
91
- const routeUse = `app.use('${selectedPath}', ${variableName});\n`;
96
+ const bodyFields = fields.filter((f) => f.type !== "File");
97
+ const fileFields = fields.filter((f) => f.type === "File");
92
98
 
93
- let indexContent = fs.readFileSync(indexPath, "utf8");
94
- if (!indexContent.includes(routeImport)) {
95
- indexContent = routeImport + indexContent;
99
+ let destructLines = "";
100
+ if (bodyFields.length > 0) {
101
+ const fieldNames = bodyFields.map((f) => f.name).join(", ");
102
+ destructLines += ` const { ${fieldNames} } = ${dataSource};\n`;
96
103
  }
97
-
98
- // Ensure there's no duplicate app.use entry
99
- if (!indexContent.includes(routeUse)) {
100
- // Check if `app.listen` or `server.listen` exists and replace accordingly
101
- if (indexContent.includes("app.listen(port")) {
102
- indexContent = indexContent.replace(
103
- "app.listen(port",
104
- routeUse + "app.listen(port"
105
- );
106
- } else if (indexContent.includes("server.listen(port")) {
107
- indexContent = indexContent.replace(
108
- "server.listen(port",
109
- routeUse + "server.listen(port"
110
- );
104
+ if (fileFields.length > 0) {
105
+ if (fileFields.length === 1) {
106
+ destructLines += ` const ${fileFields[0].name} = req.file;\n`;
107
+ } else {
108
+ for (const f of fileFields) {
109
+ destructLines += ` const ${f.name} = req.files?.['${f.name}']?.[0];\n`;
110
+ }
111
111
  }
112
112
  }
113
113
 
114
- fs.writeFileSync(indexPath, indexContent, "utf8");
114
+ return `
115
+ router.${method}('/', ${middlewareStr}async (req, res) => {
116
+ ${destructLines}
117
+ // TODO: Add your logic here
115
118
 
116
- console.log(
117
- chalk.green(
118
- `API endpoint ${selectedPath} with method ${method.toUpperCase()} created/updated successfully.`
119
- )
120
- );
121
- rl.close();
119
+ res.json({ msg: '${endpointName} ${method.toUpperCase()} endpoint works' });
120
+ });`;
122
121
  }
123
122
 
124
- function exitWithError(message) {
125
- console.error(chalk.red(message));
126
- rl.close();
127
- }
123
+ async function main() {
124
+ let endpointName = process.argv[2];
125
+ if (!endpointName) {
126
+ endpointName = await input({
127
+ message: "Enter endpoint name:",
128
+ validate: (v) => {
129
+ try {
130
+ sanitizeName(v);
131
+ return true;
132
+ } catch (e) {
133
+ return e.message;
134
+ }
135
+ },
136
+ });
137
+ }
138
+ endpointName = sanitizeName(endpointName);
128
139
 
129
- const apiPathOptions = [
130
- (endpointName) => `/${endpointName}`,
131
- (endpointName) => `/api/${endpointName}`,
132
- ];
140
+ showHeader("add-api", `Creating endpoint: ${endpointName}`);
133
141
 
134
- function promptApiPath(endpointName, callback) {
135
- console.log(chalk.cyan("Choose API path:"));
136
- apiPathOptions.forEach((option, index) => {
137
- console.log(chalk.cyan(`${index + 1}. ${option(endpointName)}`));
142
+ const projectRoot = getProjectRoot();
143
+ const apiDir = path.join(projectRoot, "src", "routes");
144
+ const apiFile = path.join(apiDir, `${endpointName}.js`);
145
+ const variableName = toCamelCase(endpointName);
146
+
147
+ const selectedPath = `/api/${endpointName}`;
148
+
149
+ const method = await select({
150
+ message: "Choose HTTP method:",
151
+ choices: methods.map((m) => ({ name: m.toUpperCase(), value: m })),
138
152
  });
139
153
 
140
- rl.question(
141
- chalk.yellow("Enter the number for the API path: "),
142
- (pathAnswer) => {
143
- const pathIndex = parseInt(pathAnswer, 10) - 1;
144
- if (pathIndex >= 0 && pathIndex < apiPathOptions.length) {
145
- const selectedPath = apiPathOptions[pathIndex](endpointName);
146
- callback(selectedPath);
147
- } else {
148
- exitWithError("Invalid selection. Please choose a valid number.");
154
+ let isAuthorized = false;
155
+ const authFilePath = path.join(projectRoot, "src", "middleware", "auth.js");
156
+ if (fs.existsSync(authFilePath)) {
157
+ isAuthorized = await confirm({
158
+ message: "Should this API be authorized?",
159
+ default: false,
160
+ });
161
+ }
162
+
163
+ // --- Field selection ---
164
+ const allFields = [];
165
+ const modelFiles = listModels();
166
+
167
+ if (modelFiles.length > 0) {
168
+ const useModel = await confirm({
169
+ message: "Select fields from an existing model?",
170
+ default: true,
171
+ });
172
+
173
+ if (useModel) {
174
+ const selectedModelFile = await select({
175
+ message: "Select model:",
176
+ choices: modelFiles.map((f) => ({
177
+ name: f,
178
+ value: path.basename(f, ".js"),
179
+ })),
180
+ });
181
+
182
+ const modelFields = getModelSchemaDetailed(selectedModelFile);
183
+
184
+ if (modelFields.length > 0) {
185
+ const selectedFieldNames = await checkbox({
186
+ message: "Select fields to use from model:",
187
+ choices: modelFields.map((f) => ({
188
+ name: formatFieldLabel(f),
189
+ value: f.name,
190
+ checked: true,
191
+ })),
192
+ });
193
+
194
+ for (const name of selectedFieldNames) {
195
+ const field = modelFields.find((f) => f.name === name);
196
+ allFields.push({ name, type: field ? field.type : "String" });
197
+ }
149
198
  }
150
199
  }
151
- );
152
- }
200
+ }
153
201
 
154
- if (process.argv.length !== 3) {
155
- exitWithError("Usage: add-api <endpointName>");
156
- }
202
+ // --- Add custom fields ---
203
+ const addCustom = await confirm({
204
+ message: "Add custom fields (received from client)?",
205
+ default: allFields.length === 0,
206
+ });
207
+
208
+ if (addCustom) {
209
+ const customFields = await collectCustomFields();
210
+ allFields.push(...customFields);
211
+ }
157
212
 
158
- const endpointName = process.argv[2];
213
+ // --- Auto-detect multipart from File fields ---
214
+ const hasFileFields = allFields.some((f) => f.type === "File");
159
215
 
160
- promptApiPath(endpointName, (selectedPath) => {
161
- console.log(chalk.cyan("Choose HTTP method:"));
162
- methods.forEach((method, index) => {
163
- console.log(chalk.cyan(`${index + 1}. ${method}`));
216
+ // --- Duplicate check ---
217
+ if (fs.existsSync(apiFile) && checkDuplicateMethod(apiFile, method)) {
218
+ showError(
219
+ `Endpoint ${selectedPath} [${method.toUpperCase()}] already exists.`
220
+ );
221
+ process.exit(1);
222
+ }
223
+
224
+ if (!fs.existsSync(apiDir)) {
225
+ fs.mkdirSync(apiDir, { recursive: true });
226
+ }
227
+
228
+ // --- Build multer config ---
229
+ let multerImport = "";
230
+ let multerSetup = "";
231
+ let multerUploadLine = "";
232
+ const needsMulter = hasFileFields;
233
+
234
+ if (needsMulter) {
235
+ multerImport = "import multer from 'multer';\n";
236
+ multerSetup = `\nconst upload = multer({ dest: 'uploads/' });\n`;
237
+
238
+ const fileFields = allFields.filter((f) => f.type === "File");
239
+ if (fileFields.length === 0) {
240
+ multerUploadLine = "upload.none()";
241
+ } else if (fileFields.length === 1) {
242
+ multerUploadLine = `upload.single('${fileFields[0].name}')`;
243
+ } else {
244
+ const fieldsArg = fileFields
245
+ .map((f) => `{ name: '${f.name}', maxCount: 1 }`)
246
+ .join(", ");
247
+ multerUploadLine = `upload.fields([${fieldsArg}])`;
248
+ }
249
+ }
250
+
251
+ // --- Build method block ---
252
+ const authMiddleware = isAuthorized ? "auth, " : "";
253
+ const methodBlock = buildMethodBlock({
254
+ method,
255
+ endpointName,
256
+ authMiddleware: isAuthorized,
257
+ fields: allFields,
258
+ multerUploadLine,
164
259
  });
165
260
 
166
- rl.question(
167
- chalk.yellow("Enter the number for the HTTP method: "),
168
- (answer) => {
169
- const methodIndex = parseInt(answer, 10) - 1;
170
- if (methodIndex >= 0 && methodIndex < methods.length) {
171
- const projectRoot = process.cwd();
172
- const authFilePath = path.join(
173
- projectRoot,
174
- "src",
175
- "middleware",
176
- "auth.js"
177
- );
178
-
179
- if (fs.existsSync(authFilePath)) {
180
- rl.question(
181
- chalk.yellow("Should this API be authorized? (y/n): "),
182
- (authAnswer) => {
183
- const isAuthorized = authAnswer.trim().toLowerCase() === "y";
184
- createApi(
185
- endpointName,
186
- methods[methodIndex],
187
- isAuthorized,
188
- selectedPath
189
- );
190
- }
191
- );
192
- } else {
193
- createApi(endpointName, methods[methodIndex], false, selectedPath);
194
- }
195
- } else {
196
- exitWithError("Invalid selection. Please choose a valid number.");
197
- }
261
+ // --- Write route file ---
262
+ let apiContent;
263
+ if (fs.existsSync(apiFile)) {
264
+ apiContent = fs.readFileSync(apiFile, "utf8");
265
+
266
+ // Add multer import if needed and not already present
267
+ if (needsMulter && !apiContent.includes("import multer")) {
268
+ const firstImportEnd = apiContent.indexOf("\n");
269
+ apiContent =
270
+ apiContent.slice(0, firstImportEnd + 1) +
271
+ multerImport +
272
+ apiContent.slice(firstImportEnd + 1);
273
+ }
274
+ if (needsMulter && !apiContent.includes("const upload = multer")) {
275
+ const routerLine = apiContent.indexOf("const router = express.Router();");
276
+ const afterRouter =
277
+ routerLine + "const router = express.Router();".length;
278
+ apiContent =
279
+ apiContent.slice(0, afterRouter) +
280
+ multerSetup +
281
+ apiContent.slice(afterRouter);
198
282
  }
283
+
284
+ const exportIndex = apiContent.lastIndexOf("export default router;");
285
+ apiContent =
286
+ apiContent.slice(0, exportIndex) +
287
+ methodBlock.trim() +
288
+ "\n\n" +
289
+ apiContent.slice(exportIndex);
290
+ showFileAction("updated", `src/routes/${endpointName}.js`);
291
+ } else {
292
+ apiContent = `import express from 'express';
293
+ ${needsMulter ? multerImport : ""}${isAuthorized ? "import auth from '../middleware/auth.js';\n" : ""}const router = express.Router();
294
+ ${needsMulter ? multerSetup : ""}
295
+ ${methodBlock.trim()}
296
+
297
+ export default router;`;
298
+ showFileAction("created", `src/routes/${endpointName}.js`);
299
+ }
300
+
301
+ fs.writeFileSync(apiFile, apiContent.trim() + "\n", "utf8");
302
+
303
+ addRouteRegistration(
304
+ `import ${variableName} from './src/routes/${endpointName}.js';`,
305
+ `app.use('${selectedPath}', ${variableName});`
199
306
  );
307
+ showFileAction("updated", "index.js");
308
+
309
+ // Install multer if needed
310
+ if (needsMulter) {
311
+ await installDeps("multer");
312
+ }
313
+
314
+ const summary = [
315
+ isAuthorized ? "Protected with auth middleware" : "No auth middleware",
316
+ `Content type: ${hasFileFields ? "multipart/form-data" : "application/json"}`,
317
+ `Route file: src/routes/${endpointName}.js`,
318
+ ];
319
+ if (allFields.length > 0) {
320
+ summary.push(
321
+ `Fields: ${allFields.map((f) => `${f.name} (${f.type})`).join(", ")}`
322
+ );
323
+ }
324
+
325
+ showTaskDone(`API endpoint ready: ${method.toUpperCase()} ${selectedPath}`, summary);
326
+ }
327
+
328
+ main().catch((err) => {
329
+ if (err.name === "ExitPromptError") process.exit(0);
330
+ showError(err.message);
331
+ process.exit(1);
200
332
  });