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.
- package/README.md +390 -0
- package/index.js +78 -0
- package/lib/addDB.js +77 -0
- package/lib/addRoute.js +264 -0
- package/lib/copyProject.js +25 -0
- package/lib/dataTypes.js +79 -0
- package/lib/installDeps.js +11 -0
- package/lib/prompts.js +289 -0
- package/lib/setupExtraDB.js +172 -0
- package/lib/setupMainDB.js +9 -0
- package/lib/testDBConnection.js +31 -0
- package/lib/utils.js +39 -0
- package/package.json +45 -0
- package/template/Backend/.env +7 -0
- package/template/Backend/DB/DBInit.js +28 -0
- package/template/Backend/DB/dbConfigs.js +4 -0
- package/template/Backend/Models/index.js +54 -0
- package/template/Backend/README.md +535 -0
- package/template/Backend/middleware/authMiddleware.js +19 -0
- package/template/Backend/package-lock.json +2997 -0
- package/template/Backend/package.json +32 -0
- package/template/Backend/routes/authRoutes.js +15 -0
- package/template/Backend/routes/dashboardRoutes.js +13 -0
- package/template/Backend/routes/index.js +15 -0
- package/template/Backend/routes/settingsRoutes.js +9 -0
- package/template/Backend/server.js +70 -0
- package/template/Backend/services/authService.js +68 -0
- package/template/Backend/services/cryptoService.js +14 -0
- package/template/Backend/services/dashboardService.js +39 -0
- package/template/Backend/services/settingsService.js +43 -0
- package/template/Frontend/.env +3 -0
- package/template/Frontend/README.md +576 -0
- package/template/Frontend/eslint.config.js +29 -0
- package/template/Frontend/index.html +13 -0
- package/template/Frontend/package-lock.json +3690 -0
- package/template/Frontend/package.json +39 -0
- package/template/Frontend/public/PMDLogo.png +0 -0
- package/template/Frontend/public/pp.jpg +0 -0
- package/template/Frontend/public/tabicon.png +0 -0
- package/template/Frontend/src/App.jsx +71 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFDemiBold/font.woff +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFDemiBold/font.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFNormal/font.woff +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFNormal/font.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFRegular/font.woff +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFRegular/font.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/MixtaProRegularItalic/font.woff +0 -0
- package/template/Frontend/src/assets/fonts/MixtaProRegularItalic/font.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/fonts_sohne/OTF/S/303/266hneMono-Buch.otf +0 -0
- package/template/Frontend/src/assets/fonts/fonts_sohne/OTF/S/303/266hneMono-Leicht.otf +0 -0
- package/template/Frontend/src/assets/fonts/fonts_sohne/WOFF2/soehne-mono-buch.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/fonts_sohne/WOFF2/soehne-mono-leicht.woff2 +0 -0
- package/template/Frontend/src/components/Layout.jsx +61 -0
- package/template/Frontend/src/components/Loader.jsx +19 -0
- package/template/Frontend/src/components/ProtectedRoute.jsx +19 -0
- package/template/Frontend/src/components/Sidebar.jsx +286 -0
- package/template/Frontend/src/components/ThemeToggle.jsx +30 -0
- package/template/Frontend/src/config/axiosClient.js +46 -0
- package/template/Frontend/src/config/encryption.js +11 -0
- package/template/Frontend/src/config/routes.js +65 -0
- package/template/Frontend/src/contexts/AuthContext.jsx +144 -0
- package/template/Frontend/src/contexts/ThemeContext.jsx +69 -0
- package/template/Frontend/src/index.css +88 -0
- package/template/Frontend/src/main.jsx +11 -0
- package/template/Frontend/src/pages/Dashboard.jsx +137 -0
- package/template/Frontend/src/pages/Login.jsx +195 -0
- package/template/Frontend/src/pages/NotFound.jsx +70 -0
- package/template/Frontend/src/pages/Settings.jsx +69 -0
- package/template/Frontend/tailwind.config.js +90 -0
- package/template/Frontend/vite.config.js +37 -0
- package/template/Readme.md +0 -0
package/lib/addRoute.js
ADDED
|
@@ -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
|
+
};
|
package/lib/dataTypes.js
ADDED
|
@@ -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
|
+
};
|