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 +176 -0
- package/bin/cli.js +75 -0
- package/package.json +20 -0
- package/src/generator/backend.js +132 -0
- package/src/generator/frontend.js +331 -0
- package/src/index.js +45 -0
- package/src/parser.js +99 -0
- package/src/writer.js +14 -0
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
|
+
}
|