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.
- package/README.md +139 -114
- package/add-api.js +288 -156
- package/add-crud.js +226 -0
- package/add-login.js +152 -319
- package/add-mongo-delete.js +67 -138
- package/add-mongo-insert.js +87 -204
- package/add-mongo-read.js +100 -216
- package/add-mongo-schema.js +431 -199
- package/add-mongo-update.js +89 -202
- package/add-mongo.js +125 -218
- package/add-serialport.js +53 -98
- package/add-socket.js +43 -66
- package/add-websocket.js +68 -140
- package/index.js +288 -155
- package/lib/env.js +18 -0
- package/lib/indexFile.js +87 -0
- package/lib/names.js +21 -0
- package/lib/project.js +122 -0
- package/lib/routeFile.js +56 -0
- package/lib/ui.js +161 -0
- package/package.json +9 -16
- package/skeleton/dev-api.js +2785 -0
- package/skeleton/index.js +10 -3
- package/skeleton/package.json +9 -5
- package/skeleton/public/fonts/NotoSans-Bold.woff2 +0 -0
- package/skeleton/public/fonts/NotoSans-Light.woff2 +0 -0
- package/skeleton/public/fonts/NotoSans-Medium.woff2 +0 -0
- package/skeleton/public/fonts/NotoSans-Regular.woff2 +0 -0
- package/skeleton/public/fonts/NotoSans-SemiBold.woff2 +0 -0
- package/skeleton/public/images/mongo.png +0 -0
- package/skeleton/public/images/mysql.png +0 -0
- package/skeleton/public/images/postgresql.png +0 -0
- package/skeleton/public/images/sqlite.png +0 -0
- package/skeleton/public/index.html +7562 -64
- package/README_4bnode.md +0 -197
- package/src/auth/passport.js +0 -21
- package/src/db.js +0 -13
- package/test-server/.env.development +0 -1
- package/test-server/.env.production +0 -1
- package/test-server/index.js +0 -82
- package/test-server/package-lock.json +0 -1267
- package/test-server/package.json +0 -20
- 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
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
16
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
return methodPattern.test(apiContent);
|
|
30
|
-
}
|
|
37
|
+
async function collectCustomFields() {
|
|
38
|
+
const customFields = [];
|
|
39
|
+
let addMore = true;
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
const middlewareStr = middlewares.length
|
|
83
|
+
? middlewares.join(", ") + ", "
|
|
84
|
+
: "";
|
|
81
85
|
|
|
82
|
-
|
|
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
|
-
|
|
93
|
+
const dataSource =
|
|
94
|
+
method === "get" || method === "delete" ? "req.query" : "req.body";
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
const
|
|
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
|
|
94
|
-
if (
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
114
|
+
return `
|
|
115
|
+
router.${method}('/', ${middlewareStr}async (req, res) => {
|
|
116
|
+
${destructLines}
|
|
117
|
+
// TODO: Add your logic here
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
(endpointName) => `/${endpointName}`,
|
|
131
|
-
(endpointName) => `/api/${endpointName}`,
|
|
132
|
-
];
|
|
140
|
+
showHeader("add-api", `Creating endpoint: ${endpointName}`);
|
|
133
141
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
213
|
+
// --- Auto-detect multipart from File fields ---
|
|
214
|
+
const hasFileFields = allFields.some((f) => f.type === "File");
|
|
159
215
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
});
|