create-fullstack-boilerplate 1.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 (71) hide show
  1. package/README.md +390 -0
  2. package/index.js +78 -0
  3. package/lib/addDB.js +77 -0
  4. package/lib/addRoute.js +264 -0
  5. package/lib/copyProject.js +25 -0
  6. package/lib/dataTypes.js +79 -0
  7. package/lib/installDeps.js +11 -0
  8. package/lib/prompts.js +289 -0
  9. package/lib/setupExtraDB.js +172 -0
  10. package/lib/setupMainDB.js +9 -0
  11. package/lib/testDBConnection.js +31 -0
  12. package/lib/utils.js +39 -0
  13. package/package.json +45 -0
  14. package/template/Backend/.env +7 -0
  15. package/template/Backend/DB/DBInit.js +28 -0
  16. package/template/Backend/DB/dbConfigs.js +4 -0
  17. package/template/Backend/Models/index.js +54 -0
  18. package/template/Backend/README.md +535 -0
  19. package/template/Backend/middleware/authMiddleware.js +19 -0
  20. package/template/Backend/package-lock.json +2997 -0
  21. package/template/Backend/package.json +32 -0
  22. package/template/Backend/routes/authRoutes.js +15 -0
  23. package/template/Backend/routes/dashboardRoutes.js +13 -0
  24. package/template/Backend/routes/index.js +15 -0
  25. package/template/Backend/routes/settingsRoutes.js +9 -0
  26. package/template/Backend/server.js +70 -0
  27. package/template/Backend/services/authService.js +68 -0
  28. package/template/Backend/services/cryptoService.js +14 -0
  29. package/template/Backend/services/dashboardService.js +39 -0
  30. package/template/Backend/services/settingsService.js +43 -0
  31. package/template/Frontend/.env +3 -0
  32. package/template/Frontend/README.md +576 -0
  33. package/template/Frontend/eslint.config.js +29 -0
  34. package/template/Frontend/index.html +13 -0
  35. package/template/Frontend/package-lock.json +3690 -0
  36. package/template/Frontend/package.json +39 -0
  37. package/template/Frontend/public/PMDLogo.png +0 -0
  38. package/template/Frontend/public/pp.jpg +0 -0
  39. package/template/Frontend/public/tabicon.png +0 -0
  40. package/template/Frontend/src/App.jsx +71 -0
  41. package/template/Frontend/src/assets/fonts/ArticulatCFDemiBold/font.woff +0 -0
  42. package/template/Frontend/src/assets/fonts/ArticulatCFDemiBold/font.woff2 +0 -0
  43. package/template/Frontend/src/assets/fonts/ArticulatCFNormal/font.woff +0 -0
  44. package/template/Frontend/src/assets/fonts/ArticulatCFNormal/font.woff2 +0 -0
  45. package/template/Frontend/src/assets/fonts/ArticulatCFRegular/font.woff +0 -0
  46. package/template/Frontend/src/assets/fonts/ArticulatCFRegular/font.woff2 +0 -0
  47. package/template/Frontend/src/assets/fonts/MixtaProRegularItalic/font.woff +0 -0
  48. package/template/Frontend/src/assets/fonts/MixtaProRegularItalic/font.woff2 +0 -0
  49. package/template/Frontend/src/assets/fonts/fonts_sohne/OTF/S/303/266hneMono-Buch.otf +0 -0
  50. package/template/Frontend/src/assets/fonts/fonts_sohne/OTF/S/303/266hneMono-Leicht.otf +0 -0
  51. package/template/Frontend/src/assets/fonts/fonts_sohne/WOFF2/soehne-mono-buch.woff2 +0 -0
  52. package/template/Frontend/src/assets/fonts/fonts_sohne/WOFF2/soehne-mono-leicht.woff2 +0 -0
  53. package/template/Frontend/src/components/Layout.jsx +61 -0
  54. package/template/Frontend/src/components/Loader.jsx +19 -0
  55. package/template/Frontend/src/components/ProtectedRoute.jsx +19 -0
  56. package/template/Frontend/src/components/Sidebar.jsx +286 -0
  57. package/template/Frontend/src/components/ThemeToggle.jsx +30 -0
  58. package/template/Frontend/src/config/axiosClient.js +46 -0
  59. package/template/Frontend/src/config/encryption.js +11 -0
  60. package/template/Frontend/src/config/routes.js +65 -0
  61. package/template/Frontend/src/contexts/AuthContext.jsx +144 -0
  62. package/template/Frontend/src/contexts/ThemeContext.jsx +69 -0
  63. package/template/Frontend/src/index.css +88 -0
  64. package/template/Frontend/src/main.jsx +11 -0
  65. package/template/Frontend/src/pages/Dashboard.jsx +137 -0
  66. package/template/Frontend/src/pages/Login.jsx +195 -0
  67. package/template/Frontend/src/pages/NotFound.jsx +70 -0
  68. package/template/Frontend/src/pages/Settings.jsx +69 -0
  69. package/template/Frontend/tailwind.config.js +90 -0
  70. package/template/Frontend/vite.config.js +37 -0
  71. package/template/Readme.md +0 -0
@@ -0,0 +1,264 @@
1
+ const path = require("path");
2
+ const { ensure } = require("./utils");
3
+ const { routePrompts } = require("./prompts");
4
+ const fs = ensure("fs-extra");
5
+
6
+ module.exports = async function addRoute() {
7
+ console.log(`\n➕ Add New Route to Backend\n`);
8
+
9
+ const targetDir = process.cwd();
10
+ const backendDir = path.join(targetDir, "backend");
11
+ const routesDir = path.join(backendDir, "routes");
12
+ const servicesDir = path.join(backendDir, "services");
13
+
14
+ // Check if we're in a valid project
15
+ if (!await fs.pathExists(backendDir)) {
16
+ console.log(`❌ Error: No backend folder found. Are you in a project root directory?`);
17
+ console.log(` Run this command from your project root (where backend/ folder exists).`);
18
+ return;
19
+ }
20
+
21
+ if (!await fs.pathExists(routesDir)) {
22
+ console.log(`❌ Error: No routes folder found in backend.`);
23
+ return;
24
+ }
25
+
26
+ try {
27
+ // Get route info from user
28
+ const routeInfo = await routePrompts();
29
+
30
+ const routeFileName = `${routeInfo.routeName}Routes.js`;
31
+ const serviceFileName = `${routeInfo.routeName}Service.js`;
32
+ const routeFilePath = path.join(routesDir, routeFileName);
33
+ const serviceFilePath = path.join(servicesDir, serviceFileName);
34
+
35
+ // Check if files already exist
36
+ if (await fs.pathExists(routeFilePath)) {
37
+ console.log(`⚠️ Route file ${routeFileName} already exists. Aborting.`);
38
+ return;
39
+ }
40
+
41
+ if (await fs.pathExists(serviceFilePath)) {
42
+ console.log(`⚠️ Service file ${serviceFileName} already exists. Aborting.`);
43
+ return;
44
+ }
45
+
46
+ // Create route file
47
+ const routeContent = generateRouteFile(routeInfo);
48
+ await fs.writeFile(routeFilePath, routeContent);
49
+ console.log(`✅ Created route file: routes/${routeFileName}`);
50
+
51
+ // Create service file
52
+ const serviceContent = generateServiceFile(routeInfo);
53
+ await fs.writeFile(serviceFilePath, serviceContent);
54
+ console.log(`✅ Created service file: services/${serviceFileName}`);
55
+
56
+ // Update routes/index.js
57
+ await updateRoutesIndex(routesDir, routeInfo);
58
+ console.log(`✅ Updated routes/index.js`);
59
+
60
+ console.log(`\n🎉 Route '${routeInfo.routeName}' added successfully!`);
61
+ console.log(`\n📝 Next steps:`);
62
+ console.log(` 1. Implement your business logic in services/${serviceFileName}`);
63
+ console.log(` 2. Add more endpoints in routes/${routeFileName}`);
64
+ console.log(` 3. Test your endpoints at ${routeInfo.routePath}`);
65
+
66
+ } catch (err) {
67
+ console.log(`❌ Error adding route:`, err.message);
68
+ }
69
+ };
70
+
71
+ function generateRouteFile(routeInfo) {
72
+ const requireAuth = routeInfo.needsAuth
73
+ ? `const { authenticateToken } = require('../middleware/authMiddleware');\n`
74
+ : '';
75
+
76
+ const authMiddleware = routeInfo.needsAuth ? 'authenticateToken, ' : '';
77
+
78
+ return `const express = require('express');
79
+ const router = express.Router();
80
+ ${requireAuth}const ${routeInfo.routeName}Service = require('../services/${routeInfo.routeName}Service');
81
+
82
+ // GET all ${routeInfo.routeName}
83
+ router.get('/', ${authMiddleware}${routeInfo.routeName}Service.getAll);
84
+
85
+ // GET single ${routeInfo.routeName} by ID
86
+ router.get('/:id', ${authMiddleware}${routeInfo.routeName}Service.getById);
87
+
88
+ // POST create new ${routeInfo.routeName}
89
+ router.post('/', ${authMiddleware}${routeInfo.routeName}Service.create);
90
+
91
+ // PUT update ${routeInfo.routeName}
92
+ router.put('/:id', ${authMiddleware}${routeInfo.routeName}Service.update);
93
+
94
+ // DELETE ${routeInfo.routeName}
95
+ router.delete('/:id', ${authMiddleware}${routeInfo.routeName}Service.delete);
96
+
97
+ module.exports = router;
98
+ `;
99
+ }
100
+
101
+ function generateServiceFile(routeInfo) {
102
+ const capitalizedName = routeInfo.routeName.charAt(0).toUpperCase() + routeInfo.routeName.slice(1);
103
+
104
+ return `// Import your database models here
105
+ // Example: const { YOUR_DB } = require('../Models');
106
+ // const { ${capitalizedName} } = YOUR_DB;
107
+
108
+ /**
109
+ * Get all ${routeInfo.routeName}
110
+ */
111
+ exports.getAll = async (req, res) => {
112
+ try {
113
+ // TODO: Implement your logic to fetch all ${routeInfo.routeName}
114
+ // Example:
115
+ // const items = await ${capitalizedName}.findAll();
116
+ // res.json(items);
117
+
118
+ res.json({
119
+ message: 'Get all ${routeInfo.routeName} - To be implemented',
120
+ data: []
121
+ });
122
+ } catch (error) {
123
+ console.error('Error fetching ${routeInfo.routeName}:', error);
124
+ res.status(500).json({ message: 'Error fetching ${routeInfo.routeName}', error: error.message });
125
+ }
126
+ };
127
+
128
+ /**
129
+ * Get ${routeInfo.routeName} by ID
130
+ */
131
+ exports.getById = async (req, res) => {
132
+ try {
133
+ const { id } = req.params;
134
+
135
+ // TODO: Implement your logic to fetch ${routeInfo.routeName} by ID
136
+ // Example:
137
+ // const item = await ${capitalizedName}.findByPk(id);
138
+ // if (!item) {
139
+ // return res.status(404).json({ message: '${capitalizedName} not found' });
140
+ // }
141
+ // res.json(item);
142
+
143
+ res.json({
144
+ message: \`Get ${routeInfo.routeName} with ID \${id} - To be implemented\`,
145
+ data: { id }
146
+ });
147
+ } catch (error) {
148
+ console.error('Error fetching ${routeInfo.routeName}:', error);
149
+ res.status(500).json({ message: 'Error fetching ${routeInfo.routeName}', error: error.message });
150
+ }
151
+ };
152
+
153
+ /**
154
+ * Create new ${routeInfo.routeName}
155
+ */
156
+ exports.create = async (req, res) => {
157
+ try {
158
+ const data = req.body;
159
+
160
+ // TODO: Implement your logic to create ${routeInfo.routeName}
161
+ // Example:
162
+ // const newItem = await ${capitalizedName}.create(data);
163
+ // res.status(201).json(newItem);
164
+
165
+ res.status(201).json({
166
+ message: 'Create ${routeInfo.routeName} - To be implemented',
167
+ data: data
168
+ });
169
+ } catch (error) {
170
+ console.error('Error creating ${routeInfo.routeName}:', error);
171
+ res.status(500).json({ message: 'Error creating ${routeInfo.routeName}', error: error.message });
172
+ }
173
+ };
174
+
175
+ /**
176
+ * Update ${routeInfo.routeName}
177
+ */
178
+ exports.update = async (req, res) => {
179
+ try {
180
+ const { id } = req.params;
181
+ const data = req.body;
182
+
183
+ // TODO: Implement your logic to update ${routeInfo.routeName}
184
+ // Example:
185
+ // const item = await ${capitalizedName}.findByPk(id);
186
+ // if (!item) {
187
+ // return res.status(404).json({ message: '${capitalizedName} not found' });
188
+ // }
189
+ // await item.update(data);
190
+ // res.json(item);
191
+
192
+ res.json({
193
+ message: \`Update ${routeInfo.routeName} with ID \${id} - To be implemented\`,
194
+ data: { id, ...data }
195
+ });
196
+ } catch (error) {
197
+ console.error('Error updating ${routeInfo.routeName}:', error);
198
+ res.status(500).json({ message: 'Error updating ${routeInfo.routeName}', error: error.message });
199
+ }
200
+ };
201
+
202
+ /**
203
+ * Delete ${routeInfo.routeName}
204
+ */
205
+ exports.delete = async (req, res) => {
206
+ try {
207
+ const { id } = req.params;
208
+
209
+ // TODO: Implement your logic to delete ${routeInfo.routeName}
210
+ // Example:
211
+ // const item = await ${capitalizedName}.findByPk(id);
212
+ // if (!item) {
213
+ // return res.status(404).json({ message: '${capitalizedName} not found' });
214
+ // }
215
+ // await item.destroy();
216
+ // res.json({ message: '${capitalizedName} deleted successfully' });
217
+
218
+ res.json({
219
+ message: \`Delete ${routeInfo.routeName} with ID \${id} - To be implemented\`,
220
+ data: { id }
221
+ });
222
+ } catch (error) {
223
+ console.error('Error deleting ${routeInfo.routeName}:', error);
224
+ res.status(500).json({ message: 'Error deleting ${routeInfo.routeName}', error: error.message });
225
+ }
226
+ };
227
+ `;
228
+ }
229
+
230
+ async function updateRoutesIndex(routesDir, routeInfo) {
231
+ const indexPath = path.join(routesDir, "index.js");
232
+ let content = await fs.readFile(indexPath, "utf8");
233
+
234
+ const requireStatement = `const ${routeInfo.routeName}Routes = require('./${routeInfo.routeName}Routes');\n`;
235
+ const authMiddleware = routeInfo.needsAuth ? 'authenticateToken, ' : '';
236
+ const useStatement = `router.use('${routeInfo.routePath}', ${authMiddleware}${routeInfo.routeName}Routes);\n`;
237
+
238
+ // Find where to insert require statement (after last require or at top)
239
+ const lastRequireMatch = content.match(/const.*require\(.*\);/g);
240
+ if (lastRequireMatch) {
241
+ const lastRequire = lastRequireMatch[lastRequireMatch.length - 1];
242
+ const requireIndex = content.lastIndexOf(lastRequire) + lastRequire.length;
243
+ content = content.slice(0, requireIndex) + '\n' + requireStatement + content.slice(requireIndex);
244
+ } else {
245
+ // Insert after the router definition
246
+ const routerMatch = content.match(/const router = express\.Router\(\);/);
247
+ if (routerMatch) {
248
+ const routerIndex = content.indexOf(routerMatch[0]) + routerMatch[0].length;
249
+ content = content.slice(0, routerIndex) + '\n\n' + requireStatement + content.slice(routerIndex);
250
+ }
251
+ }
252
+
253
+ // Find where to insert use statement (before module.exports)
254
+ const exportsMatch = content.match(/module\.exports = router;/);
255
+ if (exportsMatch) {
256
+ const exportsIndex = content.indexOf(exportsMatch[0]);
257
+ content = content.slice(0, exportsIndex) + useStatement + '\n' + content.slice(exportsIndex);
258
+ } else {
259
+ // Just append at the end
260
+ content += '\n' + useStatement;
261
+ }
262
+
263
+ await fs.writeFile(indexPath, content);
264
+ }
@@ -0,0 +1,25 @@
1
+ const { ensure, log } = require("./utils");
2
+ const fs = ensure("fs-extra");
3
+ const path = require("path");
4
+
5
+ module.exports = async function copyProject(src, dest) {
6
+ // Check if destination already exists
7
+ if (await fs.pathExists(dest)) {
8
+ throw new Error(`Directory '${path.basename(dest)}' already exists. Please choose a different project name.`);
9
+ }
10
+
11
+ // Check if source template exists
12
+ if (!await fs.pathExists(src)) {
13
+ throw new Error(`Template directory not found. Package may be corrupted.`);
14
+ }
15
+
16
+ log("Creating project directory...");
17
+ try {
18
+ await fs.copy(src, dest, {
19
+ filter: (item) => !item.includes("node_modules")
20
+ });
21
+ console.log("✅ Files copied successfully.");
22
+ } catch (err) {
23
+ throw new Error(`Failed to copy project files: ${err.message}`);
24
+ }
25
+ };
@@ -0,0 +1,79 @@
1
+ // Data type mappings for different SQL dialects
2
+ const dataTypesByDialect = {
3
+ mysql: [
4
+ { name: 'INTEGER', needsLength: false, description: 'Whole number' },
5
+ { name: 'BIGINT', needsLength: false, description: 'Large whole number' },
6
+ { name: 'FLOAT', needsLength: false, description: 'Floating point number' },
7
+ { name: 'DOUBLE', needsLength: false, description: 'Double precision number' },
8
+ { name: 'DECIMAL', needsLength: true, description: 'Exact decimal (e.g., 10,2 for currency)' },
9
+ { name: 'STRING', needsLength: true, description: 'Variable length string (VARCHAR)' },
10
+ { name: 'TEXT', needsLength: false, description: 'Long text' },
11
+ { name: 'BOOLEAN', needsLength: false, description: 'True/False' },
12
+ { name: 'DATE', needsLength: false, description: 'Date only' },
13
+ { name: 'DATETIME', needsLength: false, description: 'Date and time' },
14
+ { name: 'TIME', needsLength: false, description: 'Time only' },
15
+ { name: 'JSON', needsLength: false, description: 'JSON data' },
16
+ { name: 'BLOB', needsLength: false, description: 'Binary data' },
17
+ { name: 'ENUM', needsLength: false, description: 'Enumeration (list of values)' }
18
+ ],
19
+ mariadb: [
20
+ { name: 'INTEGER', needsLength: false, description: 'Whole number' },
21
+ { name: 'BIGINT', needsLength: false, description: 'Large whole number' },
22
+ { name: 'FLOAT', needsLength: false, description: 'Floating point number' },
23
+ { name: 'DOUBLE', needsLength: false, description: 'Double precision number' },
24
+ { name: 'DECIMAL', needsLength: true, description: 'Exact decimal (e.g., 10,2 for currency)' },
25
+ { name: 'STRING', needsLength: true, description: 'Variable length string (VARCHAR)' },
26
+ { name: 'TEXT', needsLength: false, description: 'Long text' },
27
+ { name: 'BOOLEAN', needsLength: false, description: 'True/False' },
28
+ { name: 'DATE', needsLength: false, description: 'Date only' },
29
+ { name: 'DATETIME', needsLength: false, description: 'Date and time' },
30
+ { name: 'TIME', needsLength: false, description: 'Time only' },
31
+ { name: 'JSON', needsLength: false, description: 'JSON data' },
32
+ { name: 'BLOB', needsLength: false, description: 'Binary data' },
33
+ { name: 'ENUM', needsLength: false, description: 'Enumeration (list of values)' }
34
+ ],
35
+ postgres: [
36
+ { name: 'INTEGER', needsLength: false, description: 'Whole number' },
37
+ { name: 'BIGINT', needsLength: false, description: 'Large whole number' },
38
+ { name: 'FLOAT', needsLength: false, description: 'Floating point number' },
39
+ { name: 'DOUBLE', needsLength: false, description: 'Double precision number' },
40
+ { name: 'DECIMAL', needsLength: true, description: 'Exact decimal (e.g., 10,2 for currency)' },
41
+ { name: 'STRING', needsLength: true, description: 'Variable length string (VARCHAR)' },
42
+ { name: 'TEXT', needsLength: false, description: 'Long text' },
43
+ { name: 'BOOLEAN', needsLength: false, description: 'True/False' },
44
+ { name: 'DATE', needsLength: false, description: 'Date only' },
45
+ { name: 'TIMESTAMP', needsLength: false, description: 'Date and time with timezone' },
46
+ { name: 'TIME', needsLength: false, description: 'Time only' },
47
+ { name: 'JSONB', needsLength: false, description: 'Binary JSON data (recommended)' },
48
+ { name: 'JSON', needsLength: false, description: 'JSON data' },
49
+ { name: 'BYTEA', needsLength: false, description: 'Binary data' },
50
+ { name: 'UUID', needsLength: false, description: 'Universally unique identifier' },
51
+ { name: 'ARRAY', needsLength: false, description: 'Array of values' },
52
+ { name: 'ENUM', needsLength: false, description: 'Enumeration (list of values)' }
53
+ ]
54
+ };
55
+
56
+ function getDataTypesForDialect(dialect) {
57
+ return dataTypesByDialect[dialect] || dataTypesByDialect.mysql;
58
+ }
59
+
60
+ function getDataTypeChoices(dialect) {
61
+ const types = getDataTypesForDialect(dialect);
62
+ return types.map(t => ({
63
+ name: `${t.name} - ${t.description}`,
64
+ value: t.name,
65
+ short: t.name
66
+ }));
67
+ }
68
+
69
+ function needsLength(dialect, dataType) {
70
+ const types = getDataTypesForDialect(dialect);
71
+ const type = types.find(t => t.name === dataType);
72
+ return type ? type.needsLength : false;
73
+ }
74
+
75
+ module.exports = {
76
+ getDataTypesForDialect,
77
+ getDataTypeChoices,
78
+ needsLength
79
+ };
@@ -0,0 +1,11 @@
1
+ const execa = require("execa");
2
+ const path = require("path");
3
+ const { log } = require("./utils");
4
+
5
+ module.exports = async function installDeps(targetDir) {
6
+ log("Installing backend dependencies...");
7
+ await execa("npm", ["install"], { cwd: path.join(targetDir, "backend"), stdio: "inherit" });
8
+
9
+ log("Installing frontend dependencies...");
10
+ await execa("npm", ["install"], { cwd: path.join(targetDir, "frontend"), stdio: "inherit" });
11
+ };
package/lib/prompts.js ADDED
@@ -0,0 +1,289 @@
1
+ const inquirer = require("inquirer");
2
+ const { getDataTypeChoices, needsLength } = require('./dataTypes');
3
+
4
+ module.exports.mainPrompts = () => {
5
+ return inquirer.prompt([
6
+ {
7
+ name: 'projectName',
8
+ message: 'Project name',
9
+ default: 'my-fullstack-app',
10
+ validate: (input) => {
11
+ if (!input || input.trim().length === 0) {
12
+ return 'Project name cannot be empty';
13
+ }
14
+ if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
15
+ return 'Project name can only contain letters, numbers, hyphens, and underscores';
16
+ }
17
+ return true;
18
+ }
19
+ },
20
+ {
21
+ type: 'list',
22
+ name: 'dbDialect',
23
+ message: 'Main DB Dialect:',
24
+ choices: ['mysql', 'mariadb', 'postgres']
25
+ },
26
+ {
27
+ type: 'confirm',
28
+ name: 'addExtraDB',
29
+ message: 'Add an extra database connection?',
30
+ default: false
31
+ },
32
+ {
33
+ type: 'confirm',
34
+ name: 'initGit',
35
+ message: 'Initialize Git repository?',
36
+ default: false
37
+ },
38
+ {
39
+ name: 'remoteRepo',
40
+ message: 'Enter remote Git repository URL:',
41
+ when: (answers) => answers.initGit === true,
42
+ validate: (input) => input.length > 0 ? true : "This field cannot be empty."
43
+ }
44
+ ]);
45
+ };
46
+
47
+ module.exports.extraDBPrompts = async () => {
48
+ const basicInfo = await inquirer.prompt([
49
+ {
50
+ name: 'dbKey',
51
+ message: 'DB Identifier (e.g., REPORTING_DB):',
52
+ validate: (input) => {
53
+ if (!input || input.trim().length === 0) {
54
+ return 'DB identifier cannot be empty';
55
+ }
56
+ if (!/^[A-Z][A-Z0-9_]*$/.test(input)) {
57
+ return 'DB identifier should be UPPERCASE with underscores (e.g., MY_DB)';
58
+ }
59
+ return true;
60
+ }
61
+ },
62
+ { name: 'database', message: 'DB Name:', validate: (input) => input.trim().length > 0 ? true : 'Cannot be empty' },
63
+ { name: 'username', message: 'DB Username:', validate: (input) => input.trim().length > 0 ? true : 'Cannot be empty' },
64
+ { name: 'password', message: 'DB Password:' },
65
+ { name: 'host', message: 'DB Host:', default: 'localhost' },
66
+ {
67
+ name: 'port',
68
+ message: 'DB Port:',
69
+ default: '3306',
70
+ validate: (input) => {
71
+ const port = parseInt(input);
72
+ if (isNaN(port) || port < 1 || port > 65535) {
73
+ return 'Please enter a valid port number (1-65535)';
74
+ }
75
+ return true;
76
+ }
77
+ },
78
+ {
79
+ type: 'list',
80
+ name: 'dialect',
81
+ message: 'DB Dialect:',
82
+ choices: ['mysql', 'mariadb', 'postgres']
83
+ },
84
+ {
85
+ name: 'tableName',
86
+ message: 'Model / Table Name:',
87
+ default: 'sample_table',
88
+ validate: (input) => {
89
+ if (!input || input.trim().length === 0) {
90
+ return 'Table name cannot be empty';
91
+ }
92
+ if (!/^[a-z][a-z0-9_]*$/.test(input)) {
93
+ return 'Table name should be lowercase with underscores (e.g., user_profiles)';
94
+ }
95
+ return true;
96
+ }
97
+ },
98
+ {
99
+ type: 'confirm',
100
+ name: 'defineAttributes',
101
+ message: 'Do you want to define table attributes now?',
102
+ default: false
103
+ }
104
+ ]);
105
+
106
+ // If user wants to define attributes, collect them
107
+ if (basicInfo.defineAttributes) {
108
+ basicInfo.attributes = await collectTableAttributes(basicInfo.dialect);
109
+ }
110
+
111
+ return basicInfo;
112
+ };
113
+
114
+ async function collectTableAttributes(dialect) {
115
+ const attributes = [];
116
+
117
+ console.log('\n📋 Define table attributes. Start with the primary key.\n');
118
+
119
+ // Primary key first
120
+ const pkAnswers = await inquirer.prompt([
121
+ {
122
+ name: 'name',
123
+ message: 'Primary key column name:',
124
+ default: 'id',
125
+ validate: (input) => {
126
+ if (!input || input.trim().length === 0) return 'Column name cannot be empty';
127
+ if (!/^[a-z][a-z0-9_]*$/.test(input)) return 'Use lowercase with underscores';
128
+ return true;
129
+ }
130
+ },
131
+ {
132
+ type: 'list',
133
+ name: 'type',
134
+ message: 'Primary key data type:',
135
+ choices: ['INTEGER', 'BIGINT', 'UUID', 'STRING'],
136
+ default: 'INTEGER'
137
+ },
138
+ {
139
+ type: 'confirm',
140
+ name: 'autoIncrement',
141
+ message: 'Auto-increment?',
142
+ default: true,
143
+ when: (answers) => answers.type === 'INTEGER' || answers.type === 'BIGINT'
144
+ }
145
+ ]);
146
+
147
+ attributes.push({
148
+ name: pkAnswers.name,
149
+ type: pkAnswers.type,
150
+ primaryKey: true,
151
+ autoIncrement: pkAnswers.autoIncrement || false,
152
+ allowNull: false
153
+ });
154
+
155
+ console.log('\n✅ Primary key added. Now add other attributes.\n');
156
+
157
+ // Collect other attributes
158
+ let addMore = true;
159
+ while (addMore) {
160
+ const attrAnswers = await inquirer.prompt([
161
+ {
162
+ name: 'name',
163
+ message: 'Column name:',
164
+ validate: (input) => {
165
+ if (!input || input.trim().length === 0) return 'Column name cannot be empty';
166
+ if (!/^[a-z][a-z0-9_]*$/.test(input)) return 'Use lowercase with underscores';
167
+ if (attributes.find(a => a.name === input)) return 'Column name already exists';
168
+ return true;
169
+ }
170
+ },
171
+ {
172
+ type: 'list',
173
+ name: 'type',
174
+ message: 'Data type:',
175
+ choices: getDataTypeChoices(dialect)
176
+ }
177
+ ]);
178
+
179
+ // Ask for length if needed
180
+ if (needsLength(dialect, attrAnswers.type)) {
181
+ const lengthAnswer = await inquirer.prompt([
182
+ {
183
+ name: 'length',
184
+ message: `Length/Precision for ${attrAnswers.type}:`,
185
+ default: attrAnswers.type === 'STRING' ? '255' : '10,2',
186
+ validate: (input) => {
187
+ if (!input || input.trim().length === 0) return 'Length cannot be empty';
188
+ return true;
189
+ }
190
+ }
191
+ ]);
192
+ attrAnswers.length = lengthAnswer.length;
193
+ }
194
+
195
+ // Ask for constraints
196
+ const constraints = await inquirer.prompt([
197
+ {
198
+ type: 'confirm',
199
+ name: 'allowNull',
200
+ message: 'Allow NULL values?',
201
+ default: true
202
+ },
203
+ {
204
+ type: 'confirm',
205
+ name: 'unique',
206
+ message: 'Should this be unique?',
207
+ default: false
208
+ },
209
+ {
210
+ type: 'confirm',
211
+ name: 'hasDefault',
212
+ message: 'Set a default value?',
213
+ default: false
214
+ }
215
+ ]);
216
+
217
+ if (constraints.hasDefault) {
218
+ const defaultAnswer = await inquirer.prompt([
219
+ {
220
+ name: 'defaultValue',
221
+ message: 'Default value:',
222
+ validate: (input) => input !== undefined && input !== '' ? true : 'Provide a default value'
223
+ }
224
+ ]);
225
+ attrAnswers.defaultValue = defaultAnswer.defaultValue;
226
+ }
227
+
228
+ attributes.push({
229
+ name: attrAnswers.name,
230
+ type: attrAnswers.type,
231
+ length: attrAnswers.length,
232
+ allowNull: constraints.allowNull,
233
+ unique: constraints.unique,
234
+ defaultValue: attrAnswers.defaultValue
235
+ });
236
+
237
+ const continueAnswer = await inquirer.prompt([
238
+ {
239
+ type: 'confirm',
240
+ name: 'addMore',
241
+ message: 'Add another attribute?',
242
+ default: true
243
+ }
244
+ ]);
245
+
246
+ addMore = continueAnswer.addMore;
247
+ }
248
+
249
+ console.log(`\n✅ Added ${attributes.length} attributes to the model.\n`);
250
+ return attributes;
251
+ }
252
+
253
+ module.exports.routePrompts = () => {
254
+ return inquirer.prompt([
255
+ {
256
+ name: 'routeName',
257
+ message: 'Route name (e.g., users, products):',
258
+ validate: (input) => {
259
+ if (!input || input.trim().length === 0) {
260
+ return 'Route name cannot be empty';
261
+ }
262
+ if (!/^[a-z][a-z0-9_]*$/.test(input)) {
263
+ return 'Route name should be lowercase with underscores (e.g., user_profiles)';
264
+ }
265
+ return true;
266
+ }
267
+ },
268
+ {
269
+ name: 'routePath',
270
+ message: 'Route path (e.g., /users):',
271
+ default: (answers) => `/${answers.routeName}`,
272
+ validate: (input) => {
273
+ if (!input || input.trim().length === 0) {
274
+ return 'Route path cannot be empty';
275
+ }
276
+ if (!input.startsWith('/')) {
277
+ return 'Route path should start with /';
278
+ }
279
+ return true;
280
+ }
281
+ },
282
+ {
283
+ type: 'confirm',
284
+ name: 'needsAuth',
285
+ message: 'Does this route require authentication?',
286
+ default: true
287
+ }
288
+ ]);
289
+ };