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.
@@ -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
- async function generateDatabase(config) {
7
- const { dbName, tables, needsAuth, targetDir } = config;
8
-
9
- let sql = `-- ======================================================
10
- -- Database: ${dbName}
11
- -- Generated automatically
12
- -- ======================================================
13
-
14
- CREATE DATABASE IF NOT EXISTS ${escapeSqlIdentifier(dbName)};
15
- USE ${escapeSqlIdentifier(dbName)};
16
-
17
- `;
18
-
19
- if (needsAuth) {
20
- sql += `CREATE TABLE IF NOT EXISTS users (
21
- id INT AUTO_INCREMENT PRIMARY KEY,
22
- username VARCHAR(100) NOT NULL UNIQUE,
23
- password VARCHAR(255) NOT NULL,
24
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
25
- );
26
-
27
- `;
28
- }
29
-
30
- for (const table of tables) {
31
- sql += `-- ${toPascal(table.name)} table
32
- CREATE TABLE IF NOT EXISTS ${escapeSqlIdentifier(table.name)} (
33
- id INT AUTO_INCREMENT PRIMARY KEY,
34
- `;
35
-
36
- for (const field of table.fields) {
37
- sql += ` ${escapeSqlIdentifier(field)} ${inferSqlType(field)} NOT NULL,
38
- `;
39
- }
40
-
41
- sql += ` created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
42
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
43
- );
44
-
45
- `;
46
- }
47
-
48
- const outputPath = path.join(targetDir, "backend-project", "config", "database.sql");
49
- await fs.outputFile(outputPath, sql);
50
- console.log(" [✓] database.sql");
51
- }
52
-
53
- module.exports = {
54
- generateDatabase,
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 };