tss-stack 1.2.3 → 1.3.1
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/bin/cli.js +592 -510
- package/package.json +1 -1
- package/src/generators/backend.js +404 -358
- package/src/generators/database.js +169 -55
- package/src/generators/frontend.js +794 -542
- package/src/generators/utils.js +113 -60
|
@@ -1,55 +1,169 @@
|
|
|
1
|
-
const fs = require("fs-extra");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
|
|
4
|
-
const { toPascal, escapeSqlIdentifier, inferSqlType } = require("./utils");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const { toPascal, escapeSqlIdentifier, inferSqlType } = require("./utils");
|
|
5
|
+
|
|
6
|
+
/* ==========================================================================
|
|
7
|
+
TOPOLOGICAL SORT
|
|
8
|
+
Ensures tables are created in the right order so that a FOREIGN KEY
|
|
9
|
+
never references a table that hasn't been created yet.
|
|
10
|
+
|
|
11
|
+
Example: stock_in references spare_parts
|
|
12
|
+
→ spare_parts must appear before stock_in in the SQL file
|
|
13
|
+
|
|
14
|
+
Algorithm: Kahn's algorithm (BFS-based topological sort)
|
|
15
|
+
If a cycle is detected (table A references B which references A),
|
|
16
|
+
we fall back to the original order and add a warning comment.
|
|
17
|
+
========================================================================== */
|
|
18
|
+
|
|
19
|
+
function sortTablesByDependency(tables) {
|
|
20
|
+
// Build a map of tableName → table object for quick lookup
|
|
21
|
+
const tableMap = new Map(tables.map((t) => [t.name, t]));
|
|
22
|
+
|
|
23
|
+
// in-degree = number of tables this table depends on
|
|
24
|
+
const inDegree = new Map(tables.map((t) => [t.name, 0]));
|
|
25
|
+
|
|
26
|
+
// adjacency list: if A depends on B, then B → [A, ...]
|
|
27
|
+
const dependents = new Map(tables.map((t) => [t.name, []]));
|
|
28
|
+
|
|
29
|
+
for (const table of tables) {
|
|
30
|
+
const fks = table.foreignKeys || [];
|
|
31
|
+
for (const fk of fks) {
|
|
32
|
+
// Only count dependencies on tables we know about
|
|
33
|
+
if (!tableMap.has(fk.refTable)) continue;
|
|
34
|
+
// Ignore self-references (table referencing itself)
|
|
35
|
+
if (fk.refTable === table.name) continue;
|
|
36
|
+
|
|
37
|
+
inDegree.set(table.name, (inDegree.get(table.name) || 0) + 1);
|
|
38
|
+
dependents.get(fk.refTable).push(table.name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Start with tables that have no dependencies
|
|
43
|
+
const queue = tables
|
|
44
|
+
.filter((t) => inDegree.get(t.name) === 0)
|
|
45
|
+
.map((t) => t.name);
|
|
46
|
+
|
|
47
|
+
const sorted = [];
|
|
48
|
+
|
|
49
|
+
while (queue.length > 0) {
|
|
50
|
+
const current = queue.shift();
|
|
51
|
+
sorted.push(tableMap.get(current));
|
|
52
|
+
|
|
53
|
+
for (const dependent of dependents.get(current) || []) {
|
|
54
|
+
const newDegree = inDegree.get(dependent) - 1;
|
|
55
|
+
inDegree.set(dependent, newDegree);
|
|
56
|
+
if (newDegree === 0) queue.push(dependent);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If not all tables were sorted, a cycle exists — fall back to original order
|
|
61
|
+
if (sorted.length !== tables.length) {
|
|
62
|
+
return { tables, hasCycle: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { tables: sorted, hasCycle: false };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ==========================================================================
|
|
69
|
+
GENERATE DATABASE SQL
|
|
70
|
+
========================================================================== */
|
|
71
|
+
|
|
72
|
+
async function generateDatabase(config) {
|
|
73
|
+
const { dbName, tables, needsAuth, targetDir } = config;
|
|
74
|
+
|
|
75
|
+
// Sort tables so referenced tables are created first
|
|
76
|
+
const { tables: sortedTables, hasCycle } = sortTablesByDependency(tables);
|
|
77
|
+
|
|
78
|
+
let sql = `-- ======================================================
|
|
79
|
+
-- Database: ${dbName}
|
|
80
|
+
-- Generated automatically
|
|
81
|
+
-- ======================================================
|
|
82
|
+
|
|
83
|
+
CREATE DATABASE IF NOT EXISTS ${escapeSqlIdentifier(dbName)};
|
|
84
|
+
USE ${escapeSqlIdentifier(dbName)};
|
|
85
|
+
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
if (hasCycle) {
|
|
89
|
+
sql += `-- WARNING: A circular foreign key reference was detected.
|
|
90
|
+
-- Tables are in their original order. You may need to adjust
|
|
91
|
+
-- the CREATE TABLE statements manually.
|
|
92
|
+
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// users table always first when auth is enabled,
|
|
97
|
+
// since other tables may FK to it
|
|
98
|
+
if (needsAuth) {
|
|
99
|
+
sql += `CREATE TABLE IF NOT EXISTS users (
|
|
100
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
101
|
+
username VARCHAR(100) NOT NULL UNIQUE,
|
|
102
|
+
password VARCHAR(255) NOT NULL,
|
|
103
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const table of sortedTables) {
|
|
110
|
+
const fks = table.foreignKeys || [];
|
|
111
|
+
|
|
112
|
+
sql += `-- ${toPascal(table.name)} table\n`;
|
|
113
|
+
sql += `CREATE TABLE IF NOT EXISTS ${escapeSqlIdentifier(table.name)} (\n`;
|
|
114
|
+
sql += ` id INT AUTO_INCREMENT PRIMARY KEY,\n`;
|
|
115
|
+
|
|
116
|
+
// ── Column definitions ─────────────────────────────────────────────
|
|
117
|
+
for (const field of table.fields) {
|
|
118
|
+
const sqlType = inferSqlType(field);
|
|
119
|
+
|
|
120
|
+
// If this field is a FK, it must allow NULL only when ON DELETE SET NULL
|
|
121
|
+
// is chosen. Otherwise keep NOT NULL.
|
|
122
|
+
const fk = fks.find((f) => f.field === field);
|
|
123
|
+
const nullable = fk && fk.onDelete === "SET NULL" ? "NULL" : "NOT NULL";
|
|
124
|
+
|
|
125
|
+
sql += ` ${escapeSqlIdentifier(field)} ${sqlType} ${nullable},\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
sql += ` created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n`;
|
|
129
|
+
sql += ` updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`;
|
|
130
|
+
|
|
131
|
+
// ── FOREIGN KEY constraints ────────────────────────────────────────
|
|
132
|
+
// Written as inline table constraints after all columns.
|
|
133
|
+
// Format:
|
|
134
|
+
// CONSTRAINT fk_<table>_<field>
|
|
135
|
+
// FOREIGN KEY (<field>)
|
|
136
|
+
// REFERENCES <refTable> (<refColumn>)
|
|
137
|
+
// ON DELETE <behaviour>
|
|
138
|
+
if (fks.length > 0) {
|
|
139
|
+
sql += `,\n`;
|
|
140
|
+
|
|
141
|
+
fks.forEach((fk, i) => {
|
|
142
|
+
const constraintName = `fk_${table.name}_${fk.field}`;
|
|
143
|
+
const isLast = i === fks.length - 1;
|
|
144
|
+
|
|
145
|
+
sql += ` CONSTRAINT ${escapeSqlIdentifier(constraintName)}\n`;
|
|
146
|
+
sql += ` FOREIGN KEY (${escapeSqlIdentifier(fk.field)})\n`;
|
|
147
|
+
sql += ` REFERENCES ${escapeSqlIdentifier(fk.refTable)} (${escapeSqlIdentifier(fk.refColumn)})\n`;
|
|
148
|
+
sql += ` ON DELETE ${fk.onDelete}`;
|
|
149
|
+
sql += isLast ? "\n" : ",\n";
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
sql += `\n`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
sql += `);\n\n`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const outputPath = path.join(
|
|
159
|
+
targetDir,
|
|
160
|
+
"backend-project",
|
|
161
|
+
"config",
|
|
162
|
+
"database.sql"
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
await fs.outputFile(outputPath, sql);
|
|
166
|
+
console.log(" [✓] database.sql");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { generateDatabase };
|