create-numz-app 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 ADDED
@@ -0,0 +1,176 @@
1
+ # project-gen-ai
2
+
3
+ Generate a full **Express backend + React frontend** CRUD project from a database name and SQL `CREATE TABLE` statements. One command → working app.
4
+
5
+ ## What it generates
6
+
7
+ For every table in your SQL:
8
+
9
+ | Input | Output |
10
+ |---|---|
11
+ | Table with plain columns | React page with typed inputs (text / number / date) |
12
+ | Column with `FOREIGN KEY` | React `<select>` dropdown fetching from the referenced table |
13
+ | All tables | Express route file with GET / GET:id / POST / PUT / DELETE |
14
+ | DB name | `db.js` MySQL pool + `.env` template |
15
+
16
+ ---
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ # From npm (once published)
22
+ npm install -g project-gen-ai
23
+
24
+ # Or run locally from this folder
25
+ npm install
26
+ npm link # makes `project-gen` available globally
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Usage
32
+
33
+ ```bash
34
+ project-gen
35
+ ```
36
+
37
+ The CLI will ask you:
38
+
39
+ | Question | Example answer |
40
+ |---|---|
41
+ | Project name | `sims` |
42
+ | MySQL database name | `simsdb` |
43
+ | Output directory | *(leave blank = current folder)* |
44
+ | Paste your SQL | *(paste all CREATE TABLE blocks, press Enter)* |
45
+
46
+ ### Example SQL input
47
+
48
+ ```sql
49
+ CREATE TABLE Users (
50
+ UserID INT PRIMARY KEY AUTO_INCREMENT,
51
+ Username VARCHAR(100) NOT NULL,
52
+ Password VARCHAR(255) NOT NULL,
53
+ Role VARCHAR(50) DEFAULT 'staff'
54
+ );
55
+
56
+ CREATE TABLE SpareParts (
57
+ PartID INT PRIMARY KEY AUTO_INCREMENT,
58
+ Name VARCHAR(100) NOT NULL,
59
+ Category VARCHAR(50),
60
+ Quantity INT DEFAULT 0,
61
+ UnitPrice DECIMAL(10,2),
62
+ TotalPrice DECIMAL(10,2)
63
+ );
64
+
65
+ CREATE TABLE StockIn (
66
+ StockInID INT PRIMARY KEY AUTO_INCREMENT,
67
+ PartID INT NOT NULL,
68
+ StockInQuantity INT NOT NULL,
69
+ StockInDate DATE NOT NULL,
70
+ FOREIGN KEY (PartID) REFERENCES SpareParts(PartID)
71
+ );
72
+
73
+ CREATE TABLE StockOut (
74
+ StockOutID INT PRIMARY KEY AUTO_INCREMENT,
75
+ PartID INT NOT NULL,
76
+ UserID INT NOT NULL,
77
+ StockOutQuantity INT,
78
+ StockOutUnitPrice DECIMAL(10,2),
79
+ StockOutTotalPrice DECIMAL(10,2),
80
+ StockOutDate DATE,
81
+ FOREIGN KEY (PartID) REFERENCES SpareParts(PartID),
82
+ FOREIGN KEY (UserID) REFERENCES Users(UserID)
83
+ );
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Generated project structure
89
+
90
+ ```
91
+ sims/
92
+ ├── backend/
93
+ │ ├── package.json
94
+ │ ├── server.js ← Express app, registers all routers
95
+ │ ├── db.js ← mysql2 connection pool
96
+ │ ├── .env ← DB credentials template
97
+ │ └── routes/
98
+ │ ├── users.js
99
+ │ ├── spareparts.js
100
+ │ ├── stockin.js
101
+ │ └── stockout.js
102
+ └── frontend/
103
+ ├── package.json
104
+ ├── vite.config.js ← proxies /api → localhost:5000
105
+ ├── index.html
106
+ └── src/
107
+ ├── main.jsx
108
+ ├── index.css
109
+ ├── App.jsx ← react-router-dom routes
110
+ ├── Navbar.jsx
111
+ └── pages/
112
+ ├── Login.jsx
113
+ ├── Users.jsx
114
+ ├── SpareParts.jsx
115
+ ├── StockIn.jsx ← PartID = dropdown ✓
116
+ └── StockOut.jsx ← PartID + UserID = dropdowns ✓
117
+ ```
118
+
119
+ ---
120
+
121
+ ## After generation — next steps
122
+
123
+ ```bash
124
+ # 1. Create the MySQL database and run your SQL schema
125
+ mysql -u root -p -e "CREATE DATABASE simsdb;"
126
+ mysql -u root -p simsdb < schema.sql
127
+
128
+ # 2. Start the backend
129
+ cd sims/backend
130
+ npm install
131
+ # Edit .env with your DB credentials
132
+ npm run dev # nodemon server.js on port 5000
133
+
134
+ # 3. Start the frontend
135
+ cd sims/frontend
136
+ npm install
137
+ npm run dev # Vite on http://localhost:5173
138
+ ```
139
+
140
+ ---
141
+
142
+ ## How FK detection works
143
+
144
+ The parser reads `FOREIGN KEY (col) REFERENCES table(col)` constraints.
145
+ When generating a React page, any column that is a FK becomes a `<select>` dropdown:
146
+
147
+ ```jsx
148
+ <select name="PartID" value={form.PartID} onChange={handleChange} className="border p-2 rounded">
149
+ <option value="">Select SpareParts</option>
150
+ {sparePartsList.map((r) => (
151
+ <option key={r.PartID} value={r.PartID}>{r.Name}</option>
152
+ ))}
153
+ </select>
154
+ ```
155
+
156
+ The display label for each option is chosen automatically — it looks for a column named `Name`, `name`, `Title`, `Username`, etc. on the referenced table.
157
+
158
+ ---
159
+
160
+ ## SQL type → input type mapping
161
+
162
+ | SQL type | HTML input type |
163
+ |---|---|
164
+ | INT, BIGINT, SMALLINT | `number` |
165
+ | DECIMAL, FLOAT, DOUBLE | `number` |
166
+ | DATE | `date` |
167
+ | DATETIME, TIMESTAMP | `datetime-local` |
168
+ | VARCHAR, CHAR, ENUM | `text` |
169
+ | TEXT, LONGTEXT | `text` |
170
+
171
+ ---
172
+
173
+ ## Tech stack of generated project
174
+
175
+ **Backend:** Node.js · Express · mysql2 · dotenv · cors
176
+ **Frontend:** React 18 · Vite · Tailwind CSS · Axios · React Router v6
package/bin/cli.js ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // bin/cli.js — interactive CLI
3
+
4
+ import prompts from 'prompts';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import path from 'path';
8
+ import { generateProject } from '../src/index.js';
9
+
10
+ console.log('');
11
+ console.log(chalk.bold.blue('╔══════════════════════════════════════╗'));
12
+ console.log(chalk.bold.blue('║ numz — Full-Stack Builder ║'));
13
+ console.log(chalk.bold.blue('╚══════════════════════════════════════╝'));
14
+ console.log(chalk.gray(' Paste SQL → get Express + React CRUD app\n'));
15
+
16
+ const answers = await prompts([
17
+ {
18
+ type: 'text',
19
+ name: 'projectName',
20
+ message: 'Project name (folder to create):',
21
+ initial: 'my-app',
22
+ validate: (v) => /^[\w-]+$/.test(v.trim()) || 'Letters, numbers, hyphens only',
23
+ },
24
+ {
25
+ type: 'text',
26
+ name: 'dbName',
27
+ message: 'MySQL database name:',
28
+ validate: (v) => v.trim().length > 0 || 'Required',
29
+ },
30
+ {
31
+ type: 'text',
32
+ name: 'outputDir',
33
+ message: 'Output directory (blank = current folder):',
34
+ initial: '',
35
+ },
36
+ {
37
+ type: 'text',
38
+ name: 'sql',
39
+ message: chalk.yellow('Paste your SQL CREATE TABLE statements then press Enter:'),
40
+ validate: (v) =>
41
+ v.toLowerCase().includes('create table') || 'Must include at least one CREATE TABLE',
42
+ },
43
+ ], {
44
+ onCancel: () => { console.log(chalk.red('\nCancelled.')); process.exit(1); },
45
+ });
46
+
47
+ const outputDir = answers.outputDir.trim()
48
+ ? path.resolve(answers.outputDir.trim())
49
+ : process.cwd();
50
+
51
+ console.log('');
52
+ const spinner = ora('Generating project files…').start();
53
+
54
+ try {
55
+ const { baseDir, files } = generateProject({
56
+ dbName: answers.dbName.trim(),
57
+ sql: answers.sql.trim(),
58
+ projectName: answers.projectName.trim(),
59
+ outputDir,
60
+ });
61
+
62
+ spinner.succeed(chalk.green(`Done! ${files.length} files written.\n`));
63
+
64
+ console.log(chalk.bold('📁 ' + baseDir));
65
+ for (const f of files) console.log(chalk.gray(' ├── ') + chalk.cyan(f));
66
+
67
+ console.log(chalk.bold.yellow('\n▶ Next steps:\n'));
68
+ console.log(` cd ${answers.projectName}/backend → npm install → edit .env → npm run dev`);
69
+ console.log(` cd ${answers.projectName}/frontend → npm install → npm run dev`);
70
+ console.log(`\n Then open ${chalk.bold('http://localhost:5173')}\n`);
71
+
72
+ } catch (err) {
73
+ spinner.fail(chalk.red('Error: ' + err.message));
74
+ process.exit(1);
75
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "create-numz-app",
3
+ "version": "1.0.0",
4
+ "description": "Generate a full Express + React CRUD project from a DB name and SQL CREATE TABLE statements.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-numz-app": "./bin/cli.js",
8
+ "numz": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/cli.js"
12
+ },
13
+ "dependencies": {
14
+ "chalk": "^5.3.0",
15
+ "ora": "^8.1.0",
16
+ "prompts": "^2.4.2"
17
+ },
18
+ "engines": { "node": ">=18.0.0" },
19
+ "license": "MIT"
20
+ }
@@ -0,0 +1,132 @@
1
+ // generator/backend.js — generates Express route files + server/db boilerplate
2
+
3
+ export function generateRoute(table) {
4
+ const { tableName, columns, primaryKey } = table;
5
+
6
+ // Only non-PK, non-autoincrement columns go in INSERT/UPDATE
7
+ const writeCols = columns.filter((c) => !c.isPK && !c.isAutoIncrement);
8
+ const colNames = writeCols.map((c) => c.name);
9
+
10
+ const destructure = colNames.join(', ');
11
+ const insertCols = colNames.join(', ');
12
+ const placeholders = colNames.map(() => '?').join(', ');
13
+ const updateSet = colNames.map((c) => `${c}=?`).join(', ');
14
+ const values = colNames.join(', ');
15
+
16
+ return `import express from 'express';
17
+ import db from '../db.js';
18
+
19
+ const router = express.Router();
20
+
21
+ // GET all
22
+ router.get('/', (req, res) => {
23
+ const sql = 'SELECT * FROM ${tableName}';
24
+ db.query(sql, (err, results) => {
25
+ if (err) return res.status(500).json({ error: err.message });
26
+ res.json(results);
27
+ });
28
+ });
29
+
30
+ // GET one by ID
31
+ router.get('/:id', (req, res) => {
32
+ const sql = 'SELECT * FROM ${tableName} WHERE ${primaryKey} = ?';
33
+ db.query(sql, [req.params.id], (err, results) => {
34
+ if (err) return res.status(500).json({ error: err.message });
35
+ if (!results.length) return res.status(404).json({ message: 'Not found' });
36
+ res.json(results[0]);
37
+ });
38
+ });
39
+
40
+ // POST — create
41
+ router.post('/', (req, res) => {
42
+ const { ${destructure} } = req.body;
43
+ const sql = 'INSERT INTO ${tableName} (${insertCols}) VALUES (${placeholders})';
44
+ db.query(sql, [${values}], (err, result) => {
45
+ if (err) return res.status(500).json({ error: err.message });
46
+ res.status(201).json({ message: 'Added successfully', id: result.insertId });
47
+ });
48
+ });
49
+
50
+ // PUT — update
51
+ router.put('/:id', (req, res) => {
52
+ const { ${destructure} } = req.body;
53
+ const sql = 'UPDATE ${tableName} SET ${updateSet} WHERE ${primaryKey} = ?';
54
+ db.query(sql, [${values}, req.params.id], (err) => {
55
+ if (err) return res.status(500).json({ error: err.message });
56
+ res.json({ message: 'Updated successfully' });
57
+ });
58
+ });
59
+
60
+ // DELETE
61
+ router.delete('/:id', (req, res) => {
62
+ const sql = 'DELETE FROM ${tableName} WHERE ${primaryKey} = ?';
63
+ db.query(sql, [req.params.id], (err) => {
64
+ if (err) return res.status(500).json({ error: err.message });
65
+ res.json({ message: 'Deleted successfully' });
66
+ });
67
+ });
68
+
69
+ export default router;
70
+ `;
71
+ }
72
+
73
+ export function generateServer(tables) {
74
+ const imports = tables
75
+ .map((t) => `import ${camel(t.tableName)}Router from './routes/${t.tableName.toLowerCase()}.js';`)
76
+ .join('\n');
77
+ const uses = tables
78
+ .map((t) => `app.use('/api/${t.tableName.toLowerCase()}', ${camel(t.tableName)}Router);`)
79
+ .join('\n');
80
+
81
+ return `import express from 'express';
82
+ import cors from 'cors';
83
+ import dotenv from 'dotenv';
84
+ ${imports}
85
+
86
+ dotenv.config();
87
+ const app = express();
88
+
89
+ app.use(cors({ origin: 'http://localhost:5173', credentials: true }));
90
+ app.use(express.json());
91
+
92
+ ${uses}
93
+
94
+ const PORT = process.env.PORT || 5000;
95
+ app.listen(PORT, () => console.log('Server running on port ' + PORT));
96
+ `;
97
+ }
98
+
99
+ export function generateDb(dbName) {
100
+ return `import mysql from 'mysql2';
101
+ import dotenv from 'dotenv';
102
+ dotenv.config();
103
+
104
+ const db = mysql.createPool({
105
+ host: process.env.DB_HOST || 'localhost',
106
+ user: process.env.DB_USER || 'root',
107
+ password: process.env.DB_PASSWORD || '',
108
+ database: process.env.DB_NAME || '${dbName}',
109
+ });
110
+
111
+ export default db;
112
+ `;
113
+ }
114
+
115
+ export function generateEnv(dbName) {
116
+ return `DB_HOST=localhost\nDB_USER=root\nDB_PASSWORD=your_password\nDB_NAME=${dbName}\nPORT=5000\n`;
117
+ }
118
+
119
+ export function generateBackendPackage(name) {
120
+ return JSON.stringify({
121
+ name: `${name}-backend`,
122
+ version: '1.0.0',
123
+ type: 'module',
124
+ scripts: { start: 'node server.js', dev: 'nodemon server.js' },
125
+ dependencies: { cors: '^2.8.5', dotenv: '^16.0.0', express: '^4.18.2', mysql2: '^3.6.0' },
126
+ devDependencies: { nodemon: '^3.0.0' },
127
+ }, null, 2);
128
+ }
129
+
130
+ function camel(str) {
131
+ return str.charAt(0).toLowerCase() + str.slice(1);
132
+ }
@@ -0,0 +1,331 @@
1
+ // generator/frontend.js — generates React pages, FK columns become <select> dropdowns
2
+
3
+ export function generatePage(table, allTables) {
4
+ const { tableName, columns, primaryKey, foreignKeys } = table;
5
+
6
+ // Form columns = everything except PK / autoincrement
7
+ const formCols = columns.filter((c) => !c.isPK && !c.isAutoIncrement);
8
+
9
+ // Quick FK lookup by column name
10
+ const fkMap = {};
11
+ for (const fk of foreignKeys) fkMap[fk.column] = fk;
12
+
13
+ // For each FK, determine the state var name + display column
14
+ const fkInfos = foreignKeys.map((fk) => {
15
+ const refTable = allTables.find((t) => t.tableName.toLowerCase() === fk.refTable.toLowerCase());
16
+ const displayCol = pickDisplayCol(refTable);
17
+ const stateVar = camel(fk.refTable) + 'List';
18
+ const setter = 'set' + pascal(fk.refTable) + 'List';
19
+ return { ...fk, displayCol, stateVar, setter };
20
+ });
21
+
22
+ // Initial form state: { ColA: '', ColB: '', ... }
23
+ const formInit = formCols.map((c) => `${c.name}: ''`).join(', ');
24
+
25
+ // FK state declarations
26
+ const fkStateLines = fkInfos
27
+ .map((f) => ` const [${f.stateVar}, ${f.setter}] = useState([]);`)
28
+ .join('\n');
29
+
30
+ // Inside fetchData: fetch each FK table
31
+ const fkFetchLines = fkInfos
32
+ .map((f) => ` const _${f.stateVar} = await axios.get('/api/${f.refTable.toLowerCase()}');\n ${f.setter}(_${f.stateVar}.data);`)
33
+ .join('\n');
34
+
35
+ // Form fields: FK → <select>, others → <input>
36
+ const formFields = formCols.map((col) => {
37
+ const fk = fkMap[col.name];
38
+ if (fk) {
39
+ const info = fkInfos.find((f) => f.column === col.name);
40
+ return (
41
+ ` <select name="${col.name}" value={form.${col.name}} onChange={handleChange} className="border p-2 rounded">\n` +
42
+ ` <option value="">Select ${fk.refTable}</option>\n` +
43
+ ` {${info.stateVar}.map((r) => (\n` +
44
+ ` <option key={r.${fk.refColumn}} value={r.${fk.refColumn}}>{r.${info.displayCol}}</option>\n` +
45
+ ` ))}\n` +
46
+ ` </select>`
47
+ );
48
+ }
49
+ const inputType = toInputType(col.type);
50
+ return ` <input name="${col.name}" type="${inputType}" placeholder="${col.name}" value={form.${col.name}} onChange={handleChange} className="border p-2 rounded" />`;
51
+ }).join('\n');
52
+
53
+ // Table headers
54
+ const ths = columns.map((c) => ` <th className="p-2 border">${c.name}</th>`).join('\n');
55
+
56
+ // Table body cells
57
+ const tds = columns.map((c) => ` <td className="p-2 border">{r.${c.name}}</td>`).join('\n');
58
+
59
+ // setForm on edit
60
+ const editFields = formCols.map((c) => `${c.name}: r.${c.name}`).join(', ');
61
+
62
+ const name = pascal(tableName);
63
+ const api = tableName.toLowerCase();
64
+
65
+ return `import { useState, useEffect } from 'react';
66
+ import axios from 'axios';
67
+ import Navbar from '../Navbar';
68
+
69
+ function ${name}() {
70
+ const [form, setForm] = useState({ ${formInit} });
71
+ const [records, setRecords] = useState([]);
72
+ const [editID, setEditID] = useState(null);
73
+ const [msg, setMsg] = useState('');
74
+ ${fkStateLines}
75
+
76
+ const fetchData = async () => {
77
+ const res = await axios.get('/api/${api}');
78
+ setRecords(res.data);
79
+ ${fkFetchLines}
80
+ };
81
+
82
+ useEffect(() => { fetchData(); }, []);
83
+
84
+ const handleChange = (e) => setForm({ ...form, [e.target.name]: e.target.value });
85
+
86
+ const handleSubmit = async () => {
87
+ if (editID) {
88
+ await axios.put(\`/api/${api}/\${editID}\`, form);
89
+ setMsg('Updated successfully');
90
+ setEditID(null);
91
+ } else {
92
+ await axios.post('/api/${api}', form);
93
+ setMsg('Added successfully');
94
+ }
95
+ setForm({ ${formInit} });
96
+ fetchData();
97
+ };
98
+
99
+ const handleEdit = (r) => {
100
+ setForm({ ${editFields} });
101
+ setEditID(r.${primaryKey});
102
+ };
103
+
104
+ const handleDelete = async (id) => {
105
+ if (!window.confirm('Delete this record?')) return;
106
+ await axios.delete(\`/api/${api}/\${id}\`);
107
+ setMsg('Deleted successfully');
108
+ fetchData();
109
+ };
110
+
111
+ return (
112
+ <div>
113
+ <Navbar />
114
+ <div className="p-6">
115
+ <h2 className="text-xl font-bold mb-4 text-blue-700">${name}</h2>
116
+
117
+ <div className="grid grid-cols-2 gap-3 mb-4">
118
+ ${formFields}
119
+ </div>
120
+
121
+ <button onClick={handleSubmit} className="bg-blue-700 text-white px-4 py-2 rounded hover:bg-blue-800">
122
+ {editID ? 'Update' : 'Add ${name}'}
123
+ </button>
124
+ {editID && (
125
+ <button onClick={() => { setEditID(null); setForm({ ${formInit} }); }}
126
+ className="ml-2 bg-gray-400 text-white px-4 py-2 rounded hover:bg-gray-500">
127
+ Cancel
128
+ </button>
129
+ )}
130
+ {msg && <p className="text-green-600 mt-2 text-sm">{msg}</p>}
131
+
132
+ <h3 className="text-lg font-bold mt-6 mb-2">${name} Records</h3>
133
+ <div className="overflow-x-auto">
134
+ <table className="w-full border text-sm">
135
+ <thead className="bg-blue-700 text-white">
136
+ <tr>
137
+ ${ths}
138
+ <th className="p-2 border">Actions</th>
139
+ </tr>
140
+ </thead>
141
+ <tbody>
142
+ {records.map((r) => (
143
+ <tr key={r.${primaryKey}} className="hover:bg-gray-50">
144
+ ${tds}
145
+ <td className="p-2 border whitespace-nowrap">
146
+ <button onClick={() => handleEdit(r)} className="bg-yellow-400 px-2 py-1 rounded mr-1 text-xs">Edit</button>
147
+ <button onClick={() => handleDelete(r.${primaryKey})} className="bg-red-500 text-white px-2 py-1 rounded text-xs">Delete</button>
148
+ </td>
149
+ </tr>
150
+ ))}
151
+ </tbody>
152
+ </table>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ export default ${name};
160
+ `;
161
+ }
162
+
163
+ export function generateApp(tables) {
164
+ const imports = tables.map((t) => `import ${pascal(t.tableName)} from './pages/${pascal(t.tableName)}';`).join('\n');
165
+ const routes = tables.map((t) => ` <Route path="/${t.tableName.toLowerCase()}" element={<${pascal(t.tableName)} />} />`).join('\n');
166
+ const first = tables[0]?.tableName.toLowerCase() ?? 'home';
167
+ return `import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
168
+ import Login from './pages/Login';
169
+ ${imports}
170
+
171
+ function App() {
172
+ return (
173
+ <Router>
174
+ <Routes>
175
+ <Route path="/" element={<Navigate to="/login" />} />
176
+ <Route path="/login" element={<Login />} />
177
+ ${routes}
178
+ </Routes>
179
+ </Router>
180
+ );
181
+ }
182
+
183
+ export default App;
184
+ `;
185
+ }
186
+
187
+ export function generateNavbar(tables) {
188
+ const links = tables
189
+ .map((t) => ` <Link to="/${t.tableName.toLowerCase()}" className="hover:text-blue-300 font-medium">${pascal(t.tableName)}</Link>`)
190
+ .join('\n');
191
+ return `import { Link } from 'react-router-dom';
192
+
193
+ function Navbar() {
194
+ return (
195
+ <nav className="bg-blue-700 text-white px-6 py-3 flex gap-6 items-center shadow">
196
+ <span className="font-bold text-lg mr-4">SIMS</span>
197
+ ${links}
198
+ </nav>
199
+ );
200
+ }
201
+
202
+ export default Navbar;
203
+ `;
204
+ }
205
+
206
+ export function generateLogin(firstPage) {
207
+ return `import { useState } from 'react';
208
+ import axios from 'axios';
209
+ import { useNavigate } from 'react-router-dom';
210
+
211
+ function Login() {
212
+ const [Username, setUsername] = useState('');
213
+ const [Password, setPassword] = useState('');
214
+ const [msg, setMsg] = useState('');
215
+ const navigate = useNavigate();
216
+
217
+ const handleLogin = async () => {
218
+ try {
219
+ const res = await axios.post('/api/users/login', { Username, Password }, { withCredentials: true });
220
+ localStorage.setItem('user', JSON.stringify(res.data.user));
221
+ if (res.data.message === 'Login successful') {
222
+ navigate('/${firstPage}');
223
+ } else {
224
+ setMsg(res.data.message);
225
+ }
226
+ } catch {
227
+ setMsg('Connection error — is the backend running?');
228
+ }
229
+ };
230
+
231
+ return (
232
+ <div className="flex justify-center items-center h-screen bg-gray-100">
233
+ <div className="bg-white p-8 rounded shadow-md w-80">
234
+ <h2 className="text-2xl font-bold mb-6 text-center text-blue-700">Login</h2>
235
+ <input type="text" placeholder="Username" value={Username}
236
+ onChange={(e) => setUsername(e.target.value)}
237
+ className="w-full border p-2 rounded mb-3 focus:outline-none focus:border-blue-500" />
238
+ <input type="password" placeholder="Password" value={Password}
239
+ onChange={(e) => setPassword(e.target.value)}
240
+ className="w-full border p-2 rounded mb-4 focus:outline-none focus:border-blue-500" />
241
+ <button onClick={handleLogin}
242
+ className="w-full bg-blue-700 text-white py-2 rounded hover:bg-blue-800">
243
+ Login
244
+ </button>
245
+ {msg && <p className="text-red-500 mt-3 text-center text-sm">{msg}</p>}
246
+ </div>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ export default Login;
252
+ `;
253
+ }
254
+
255
+ export function generateFrontendPackage(name) {
256
+ return JSON.stringify({
257
+ name: `${name}-frontend`,
258
+ version: '1.0.0',
259
+ type: 'module',
260
+ scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
261
+ dependencies: { axios: '^1.6.0', react: '^18.2.0', 'react-dom': '^18.2.0', 'react-router-dom': '^6.20.0' },
262
+ devDependencies: { '@vitejs/plugin-react': '^4.2.0', autoprefixer: '^10.4.16', postcss: '^8.4.31', tailwindcss: '^3.3.5', vite: '^5.0.0' },
263
+ }, null, 2);
264
+ }
265
+
266
+ export function generateViteConfig() {
267
+ return `import { defineConfig } from 'vite';
268
+ import react from '@vitejs/plugin-react';
269
+
270
+ export default defineConfig({
271
+ plugins: [react()],
272
+ server: { proxy: { '/api': 'http://localhost:5000' } },
273
+ });
274
+ `;
275
+ }
276
+
277
+ export function generateMain() {
278
+ return `import React from 'react';
279
+ import ReactDOM from 'react-dom/client';
280
+ import App from './App';
281
+ import './index.css';
282
+
283
+ ReactDOM.createRoot(document.getElementById('root')).render(
284
+ <React.StrictMode><App /></React.StrictMode>
285
+ );
286
+ `;
287
+ }
288
+
289
+ export function generateCSS() {
290
+ return `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`;
291
+ }
292
+
293
+ export function generateHTML(title) {
294
+ return `<!DOCTYPE html>
295
+ <html lang="en">
296
+ <head>
297
+ <meta charset="UTF-8" />
298
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
299
+ <title>${title}</title>
300
+ </head>
301
+ <body>
302
+ <div id="root"></div>
303
+ <script type="module" src="/src/main.jsx"></script>
304
+ </body>
305
+ </html>
306
+ `;
307
+ }
308
+
309
+ // ── helpers ────────────────────────────────────────────────────────────────
310
+
311
+ function pascal(str) { return str.charAt(0).toUpperCase() + str.slice(1); }
312
+ function camel(str) { return str.charAt(0).toLowerCase() + str.slice(1); }
313
+
314
+ function toInputType(type) {
315
+ if (type === 'date') return 'date';
316
+ if (type === 'datetime') return 'datetime-local';
317
+ if (type === 'number' || type === 'decimal') return 'number';
318
+ return 'text';
319
+ }
320
+
321
+ // Pick the best display column for a FK dropdown label
322
+ function pickDisplayCol(refTable) {
323
+ if (!refTable) return 'id';
324
+ const preferred = ['name', 'title', 'username', 'label', 'description'];
325
+ for (const col of refTable.columns) {
326
+ if (preferred.includes(col.name.toLowerCase())) return col.name;
327
+ }
328
+ // fallback: first non-PK column
329
+ const nonPK = refTable.columns.filter((c) => !c.isPK);
330
+ return nonPK[0]?.name ?? refTable.columns[0]?.name ?? 'id';
331
+ }
package/src/index.js ADDED
@@ -0,0 +1,45 @@
1
+ // src/index.js — orchestrator: parse SQL → generate all files → write to disk
2
+ import path from 'path';
3
+ import { parseSQL } from './parser.js';
4
+ import { generateRoute, generateServer, generateDb, generateEnv, generateBackendPackage } from './generator/backend.js';
5
+ import { generatePage, generateApp, generateNavbar, generateLogin, generateFrontendPackage, generateViteConfig, generateMain, generateCSS, generateHTML } from './generator/frontend.js';
6
+ import { writeFiles } from './writer.js';
7
+
8
+ export function generateProject({ dbName, sql, projectName, outputDir }) {
9
+ const outDir = outputDir || process.cwd();
10
+
11
+ // 1. Parse SQL
12
+ const tables = parseSQL(sql);
13
+ if (!tables.length) throw new Error('No CREATE TABLE statements found.');
14
+
15
+ const baseDir = path.join(outDir, projectName);
16
+ const files = {};
17
+
18
+ // 2. Backend files
19
+ files['backend/package.json'] = generateBackendPackage(projectName);
20
+ files['backend/server.js'] = generateServer(tables);
21
+ files['backend/db.js'] = generateDb(dbName);
22
+ files['backend/.env'] = generateEnv(dbName);
23
+ for (const t of tables) {
24
+ files[`backend/routes/${t.tableName.toLowerCase()}.js`] = generateRoute(t);
25
+ }
26
+
27
+ // 3. Frontend files
28
+ files['frontend/package.json'] = generateFrontendPackage(projectName);
29
+ files['frontend/vite.config.js'] = generateViteConfig();
30
+ files['frontend/index.html'] = generateHTML(projectName);
31
+ files['frontend/src/main.jsx'] = generateMain();
32
+ files['frontend/src/index.css'] = generateCSS();
33
+ files['frontend/src/App.jsx'] = generateApp(tables);
34
+ files['frontend/src/Navbar.jsx'] = generateNavbar(tables);
35
+ files['frontend/src/pages/Login.jsx'] = generateLogin(tables[0].tableName.toLowerCase());
36
+ for (const t of tables) {
37
+ files[`frontend/src/pages/${pascal(t.tableName)}.jsx`] = generatePage(t, tables);
38
+ }
39
+
40
+ // 4. Write everything
41
+ const written = writeFiles(baseDir, files);
42
+ return { baseDir, files: written };
43
+ }
44
+
45
+ function pascal(str) { return str.charAt(0).toUpperCase() + str.slice(1); }
package/src/parser.js ADDED
@@ -0,0 +1,99 @@
1
+ // parser.js — converts raw SQL into structured table objects
2
+
3
+ export function parseSQL(sql) {
4
+ const tables = [];
5
+
6
+ // Match every CREATE TABLE block
7
+ const blocks = [...sql.matchAll(
8
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?(\w+)[`"']?\s*\(([^;]+)\)\s*;?/gis
9
+ )];
10
+
11
+ for (const [, tableName, body] of blocks) {
12
+ const columns = [];
13
+ const foreignKeys = [];
14
+ let primaryKey = null;
15
+
16
+ // Split on commas that are NOT inside parentheses
17
+ const lines = splitOnComma(body);
18
+
19
+ for (const rawLine of lines) {
20
+ const line = rawLine.trim();
21
+ if (!line) continue;
22
+ const up = line.toUpperCase();
23
+
24
+ // FOREIGN KEY line
25
+ const fkMatch = line.match(
26
+ /(?:CONSTRAINT\s+\w+\s+)?FOREIGN\s+KEY\s*\(\s*[`"']?(\w+)[`"']?\s*\)\s*REFERENCES\s+[`"']?(\w+)[`"']?\s*\(\s*[`"']?(\w+)[`"']?\s*\)/i
27
+ );
28
+ if (fkMatch) {
29
+ foreignKeys.push({ column: fkMatch[1], refTable: fkMatch[2], refColumn: fkMatch[3] });
30
+ continue;
31
+ }
32
+
33
+ // Standalone PRIMARY KEY line
34
+ const pkLine = line.match(/^PRIMARY\s+KEY\s*\(\s*[`"']?(\w+)[`"']?\s*\)/i);
35
+ if (pkLine) { primaryKey = pkLine[1]; continue; }
36
+
37
+ // Skip index / unique / check / key lines
38
+ if (up.match(/^(UNIQUE|INDEX|KEY|CHECK|CONSTRAINT)\b/)) continue;
39
+
40
+ // Parse as column
41
+ const col = parseColumn(line);
42
+ if (col) {
43
+ if (col.isPK) primaryKey = col.name;
44
+ columns.push(col);
45
+ }
46
+ }
47
+
48
+ tables.push({ tableName: tableName.trim(), columns, primaryKey, foreignKeys });
49
+ }
50
+
51
+ return tables;
52
+ }
53
+
54
+ // Split a string on commas that are not inside ()
55
+ function splitOnComma(str) {
56
+ const parts = [];
57
+ let depth = 0;
58
+ let cur = '';
59
+ for (const ch of str) {
60
+ if (ch === '(') depth++;
61
+ else if (ch === ')') depth--;
62
+ if (ch === ',' && depth === 0) { parts.push(cur); cur = ''; }
63
+ else cur += ch;
64
+ }
65
+ if (cur.trim()) parts.push(cur);
66
+ return parts;
67
+ }
68
+
69
+ // Parse one column definition line
70
+ function parseColumn(line) {
71
+ // Column name is the first word (strip backticks/quotes)
72
+ const nameMatch = line.match(/^[`"']?(\w+)[`"']?\s+(\w+)/);
73
+ if (!nameMatch) return null;
74
+
75
+ const name = nameMatch[1];
76
+ const rawType = nameMatch[2].toUpperCase();
77
+ const up = line.toUpperCase();
78
+
79
+ const isPK = up.includes('PRIMARY KEY');
80
+ const isAutoIncrement = up.includes('AUTO_INCREMENT');
81
+ const notNull = up.includes('NOT NULL');
82
+
83
+ const defMatch = line.match(/DEFAULT\s+['"]?([^,'")\s]+)['"]?/i);
84
+ const defaultValue = defMatch ? defMatch[1] : null;
85
+
86
+ const type = resolveType(rawType);
87
+ return { name, rawType, type, isPK, isAutoIncrement, notNull, defaultValue };
88
+ }
89
+
90
+ // Map SQL type → simple category for the frontend generator
91
+ function resolveType(t) {
92
+ if (['INT','INTEGER','BIGINT','SMALLINT','TINYINT','MEDIUMINT'].includes(t)) return 'number';
93
+ if (['DECIMAL','FLOAT','DOUBLE','NUMERIC','REAL'].includes(t)) return 'decimal';
94
+ if (t === 'DATE') return 'date';
95
+ if (['DATETIME','TIMESTAMP'].includes(t)) return 'datetime';
96
+ if (['BOOLEAN','BOOL','BIT'].includes(t)) return 'boolean';
97
+ if (['TEXT','LONGTEXT','MEDIUMTEXT','TINYTEXT'].includes(t)) return 'textarea';
98
+ return 'text';
99
+ }
package/src/writer.js ADDED
@@ -0,0 +1,14 @@
1
+ // writer.js — writes a { 'relative/path': 'content' } map to disk
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ export function writeFiles(baseDir, fileMap) {
6
+ const written = [];
7
+ for (const [rel, content] of Object.entries(fileMap)) {
8
+ const full = path.join(baseDir, rel);
9
+ fs.mkdirSync(path.dirname(full), { recursive: true });
10
+ fs.writeFileSync(full, content, 'utf8');
11
+ written.push(rel);
12
+ }
13
+ return written;
14
+ }