db-model-router 1.0.4 → 1.0.5
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 +110 -16
- package/TODO.md +14 -0
- package/dbmr.schema.json +333 -0
- package/demo/.dockerignore +7 -0
- package/demo/.env.example +13 -0
- package/demo/Dockerfile +20 -0
- package/demo/app.js +37 -0
- package/demo/commons/add_migration.js +43 -0
- package/demo/commons/db.js +17 -0
- package/demo/commons/migrate.js +65 -0
- package/demo/commons/security.js +30 -0
- package/demo/commons/session.js +13 -0
- package/demo/dbmr.schema.json +362 -0
- package/demo/docs/llm.md +197 -0
- package/demo/llms.txt +70 -0
- package/demo/middleware/logger.js +67 -0
- package/demo/migrations/20260430155808_create_migrations_table.sql +6 -0
- package/demo/migrations/20260430155809_create_tables.sql +207 -0
- package/demo/models/addresses.js +22 -0
- package/demo/models/cart_items.js +18 -0
- package/demo/models/carts.js +16 -0
- package/demo/models/categories.js +20 -0
- package/demo/models/coupons.js +23 -0
- package/demo/models/order_items.js +21 -0
- package/demo/models/orders.js +25 -0
- package/demo/models/payments.js +21 -0
- package/demo/models/product_images.js +18 -0
- package/demo/models/product_reviews.js +20 -0
- package/demo/models/product_variants.js +20 -0
- package/demo/models/products.js +30 -0
- package/demo/models/shipments.js +19 -0
- package/demo/models/users.js +19 -0
- package/demo/models/wishlists.js +15 -0
- package/demo/openapi.json +5872 -0
- package/demo/package-lock.json +2810 -0
- package/demo/package.json +34 -0
- package/demo/routes/addresses.js +6 -0
- package/demo/routes/carts/cart_items.js +7 -0
- package/demo/routes/carts.js +6 -0
- package/demo/routes/categories.js +6 -0
- package/demo/routes/coupons.js +6 -0
- package/demo/routes/docs.js +18 -0
- package/demo/routes/health.js +35 -0
- package/demo/routes/index.js +39 -0
- package/demo/routes/orders/order_items.js +7 -0
- package/demo/routes/orders/payments.js +7 -0
- package/demo/routes/orders/shipments.js +7 -0
- package/demo/routes/orders.js +6 -0
- package/demo/routes/products/product_images.js +7 -0
- package/demo/routes/products/product_reviews.js +7 -0
- package/demo/routes/products/product_variants.js +7 -0
- package/demo/routes/products.js +6 -0
- package/demo/routes/users.js +6 -0
- package/demo/routes/wishlists.js +6 -0
- package/docker-compose.yml +1 -1
- package/package.json +8 -7
- package/scripts/demo-create.js +47 -0
- package/skill/SKILL.md +464 -0
- package/skill/references/cockroachdb.md +49 -0
- package/skill/references/dynamodb.md +53 -0
- package/skill/references/mongodb.md +56 -0
- package/skill/references/mssql.md +55 -0
- package/skill/references/oracle.md +52 -0
- package/skill/references/postgres.md +50 -0
- package/skill/references/redis.md +53 -0
- package/skill/references/sqlite3.md +43 -0
- package/src/cli/commands/generate.js +58 -17
- package/src/cli/commands/help.js +11 -6
- package/src/cli/commands/init.js +2 -2
- package/src/cli/commands/inspect.js +1 -0
- package/src/cli/diff-engine.js +52 -22
- package/src/cli/generate-docs-route.js +31 -0
- package/src/cli/generate-migration.js +356 -0
- package/src/cli/generate-route.js +52 -24
- package/src/cli/init/dependencies.js +3 -0
- package/src/cli/init/generators.js +1 -1
- package/src/cli/init.js +8 -8
- package/src/cockroachdb/db.js +90 -59
- package/src/commons/route.js +20 -20
- package/src/commons/validator.js +58 -1
- package/src/dynamodb/db.js +50 -27
- package/src/mongodb/db.js +1 -0
- package/src/mssql/db.js +89 -61
- package/src/mysql/db.js +1 -0
- package/src/oracle/db.js +1 -0
- package/src/postgres/db.js +61 -41
- package/src/redis/db.js +1 -0
- package/src/schema/schema-parser.js +43 -1
- package/src/schema/schema-printer.js +7 -0
- package/src/schema/schema-validator.js +17 -0
- package/src/sqlite3/db.js +1 -0
- package/docs/SKILL.md +0 -419
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate database migration SQL/JS from parsed schema tables.
|
|
5
|
+
*
|
|
6
|
+
* Produces CREATE TABLE statements (or equivalent) for each table,
|
|
7
|
+
* with proper column types, constraints, and indexes per adapter.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Column type mapping per adapter
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map a dbmr column rule to a SQL column type for the given adapter.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} rule - e.g. "required|string", "auto_increment", "integer"
|
|
18
|
+
* @param {string} adapter - e.g. "postgres", "mysql", "sqlite3"
|
|
19
|
+
* @returns {{ sqlType: string, nullable: boolean, isAutoIncrement: boolean }}
|
|
20
|
+
*/
|
|
21
|
+
function mapColumnType(rule, adapter) {
|
|
22
|
+
const parts = rule.split("|");
|
|
23
|
+
const isRequired = parts.includes("required");
|
|
24
|
+
const baseType = parts.filter((p) => p !== "required")[0] || "string";
|
|
25
|
+
|
|
26
|
+
let sqlType;
|
|
27
|
+
let isAutoIncrement = false;
|
|
28
|
+
|
|
29
|
+
switch (baseType) {
|
|
30
|
+
case "auto_increment":
|
|
31
|
+
isAutoIncrement = true;
|
|
32
|
+
sqlType = autoIncrementType(adapter);
|
|
33
|
+
break;
|
|
34
|
+
case "string":
|
|
35
|
+
sqlType = stringType(adapter);
|
|
36
|
+
break;
|
|
37
|
+
case "integer":
|
|
38
|
+
sqlType = integerType(adapter);
|
|
39
|
+
break;
|
|
40
|
+
case "numeric":
|
|
41
|
+
sqlType = numericType(adapter);
|
|
42
|
+
break;
|
|
43
|
+
case "boolean":
|
|
44
|
+
sqlType = booleanType(adapter);
|
|
45
|
+
break;
|
|
46
|
+
case "datetime":
|
|
47
|
+
sqlType = datetimeType(adapter);
|
|
48
|
+
break;
|
|
49
|
+
case "object":
|
|
50
|
+
sqlType = jsonType(adapter);
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
sqlType = stringType(adapter);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
sqlType,
|
|
58
|
+
nullable: !isRequired && !isAutoIncrement,
|
|
59
|
+
isAutoIncrement,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function autoIncrementType(adapter) {
|
|
64
|
+
switch (adapter) {
|
|
65
|
+
case "postgres":
|
|
66
|
+
case "cockroachdb":
|
|
67
|
+
return "SERIAL";
|
|
68
|
+
case "mssql":
|
|
69
|
+
return "INT IDENTITY(1,1)";
|
|
70
|
+
case "oracle":
|
|
71
|
+
return "NUMBER GENERATED BY DEFAULT AS IDENTITY";
|
|
72
|
+
case "sqlite3":
|
|
73
|
+
return "INTEGER";
|
|
74
|
+
default:
|
|
75
|
+
// mysql, mariadb
|
|
76
|
+
return "INT AUTO_INCREMENT";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stringType(adapter) {
|
|
81
|
+
if (adapter === "oracle") return "VARCHAR2(255)";
|
|
82
|
+
return "VARCHAR(255)";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function integerType(adapter) {
|
|
86
|
+
if (adapter === "oracle") return "NUMBER(10)";
|
|
87
|
+
return "INTEGER";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function numericType(adapter) {
|
|
91
|
+
if (adapter === "oracle") return "NUMBER(12,2)";
|
|
92
|
+
if (adapter === "mssql") return "DECIMAL(12,2)";
|
|
93
|
+
return "DECIMAL(12,2)";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function booleanType(adapter) {
|
|
97
|
+
if (adapter === "oracle") return "NUMBER(1)";
|
|
98
|
+
if (adapter === "mssql") return "BIT";
|
|
99
|
+
if (adapter === "mysql" || adapter === "mariadb") return "TINYINT(1)";
|
|
100
|
+
return "BOOLEAN";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function datetimeType(adapter) {
|
|
104
|
+
if (adapter === "mssql") return "DATETIME";
|
|
105
|
+
return "TIMESTAMP";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function jsonType(adapter) {
|
|
109
|
+
if (adapter === "postgres" || adapter === "cockroachdb") return "JSONB";
|
|
110
|
+
if (adapter === "oracle") return "CLOB";
|
|
111
|
+
if (adapter === "mssql") return "NVARCHAR(MAX)";
|
|
112
|
+
if (adapter === "sqlite3") return "TEXT";
|
|
113
|
+
return "JSON";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// SQL migration generators
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Generate a CREATE TABLE SQL statement for a single table.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} tableName
|
|
124
|
+
* @param {object} tableDef - parsed table definition from schema
|
|
125
|
+
* @param {string} adapter
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function generateCreateTableSQL(tableName, tableDef, adapter) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
const pk = tableDef.pk;
|
|
131
|
+
|
|
132
|
+
for (const [colName, rule] of Object.entries(tableDef.columns)) {
|
|
133
|
+
const { sqlType, nullable, isAutoIncrement } = mapColumnType(rule, adapter);
|
|
134
|
+
let line = ` ${quoteIdent(colName, adapter)} ${sqlType}`;
|
|
135
|
+
|
|
136
|
+
if (colName === pk) {
|
|
137
|
+
line += " PRIMARY KEY";
|
|
138
|
+
if (isAutoIncrement && adapter === "sqlite3") {
|
|
139
|
+
// SQLite needs AUTOINCREMENT after PRIMARY KEY for INTEGER type
|
|
140
|
+
line = ` ${quoteIdent(colName, adapter)} INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
if (!nullable) line += " NOT NULL";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Default values for timestamps
|
|
147
|
+
if (tableDef.timestamps) {
|
|
148
|
+
if (
|
|
149
|
+
colName === tableDef.timestamps.created_at ||
|
|
150
|
+
colName === tableDef.timestamps.modified_at
|
|
151
|
+
) {
|
|
152
|
+
line += defaultTimestamp(adapter);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Default for boolean softDelete column
|
|
157
|
+
if (tableDef.softDelete && colName === tableDef.softDelete) {
|
|
158
|
+
line += defaultBoolean(adapter);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
lines.push(line);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Unique constraints (excluding PK which is already PRIMARY KEY)
|
|
165
|
+
if (tableDef.unique && tableDef.unique.length > 0) {
|
|
166
|
+
const uniqueCols = tableDef.unique.filter((c) => c !== pk);
|
|
167
|
+
for (const col of uniqueCols) {
|
|
168
|
+
lines.push(` UNIQUE (${quoteIdent(col, adapter)})`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const createPrefix =
|
|
173
|
+
adapter === "oracle" || adapter === "mssql"
|
|
174
|
+
? `CREATE TABLE ${quoteIdent(tableName, adapter)}`
|
|
175
|
+
: `CREATE TABLE IF NOT EXISTS ${quoteIdent(tableName, adapter)}`;
|
|
176
|
+
|
|
177
|
+
return `${createPrefix} (\n${lines.join(",\n")}\n);\n`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Quote an identifier based on adapter.
|
|
182
|
+
*/
|
|
183
|
+
function quoteIdent(name, adapter) {
|
|
184
|
+
// Most adapters are fine without quoting for simple names
|
|
185
|
+
// but we keep it safe for reserved words
|
|
186
|
+
if (adapter === "mssql") return `[${name}]`;
|
|
187
|
+
if (adapter === "oracle") return `"${name.toUpperCase()}"`;
|
|
188
|
+
return name;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Default timestamp expression per adapter.
|
|
193
|
+
*/
|
|
194
|
+
function defaultTimestamp(adapter) {
|
|
195
|
+
switch (adapter) {
|
|
196
|
+
case "mssql":
|
|
197
|
+
return " DEFAULT GETDATE()";
|
|
198
|
+
case "oracle":
|
|
199
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
200
|
+
case "sqlite3":
|
|
201
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
202
|
+
default:
|
|
203
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Default boolean false expression per adapter.
|
|
209
|
+
*/
|
|
210
|
+
function defaultBoolean(adapter) {
|
|
211
|
+
if (adapter === "oracle" || adapter === "mssql") return " DEFAULT 0";
|
|
212
|
+
if (adapter === "mysql" || adapter === "mariadb") return " DEFAULT 0";
|
|
213
|
+
return " DEFAULT FALSE";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// NoSQL migration generators
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generate a MongoDB migration JS file for a single table (collection).
|
|
222
|
+
*/
|
|
223
|
+
function generateMongoDBMigration(tableName, tableDef) {
|
|
224
|
+
const uniqueCols = (tableDef.unique || []).filter((c) => c !== tableDef.pk);
|
|
225
|
+
const indexLines = uniqueCols
|
|
226
|
+
.map(
|
|
227
|
+
(col) =>
|
|
228
|
+
` await db.collection("${tableName}").createIndex({ ${col}: 1 }, { unique: true });`,
|
|
229
|
+
)
|
|
230
|
+
.join("\n");
|
|
231
|
+
|
|
232
|
+
return `"use strict";
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
async up(db) {
|
|
236
|
+
await db.createCollection("${tableName}");
|
|
237
|
+
${indexLines ? indexLines + "\n" : ""} },
|
|
238
|
+
|
|
239
|
+
async down(db) {
|
|
240
|
+
await db.collection("${tableName}").drop();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate a DynamoDB migration JS file for a single table.
|
|
248
|
+
*/
|
|
249
|
+
function generateDynamoDBMigration(tableName, tableDef) {
|
|
250
|
+
const pk = tableDef.pk;
|
|
251
|
+
return `import { CreateTableCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb";
|
|
252
|
+
|
|
253
|
+
export async function up(db) {
|
|
254
|
+
await db.send(new CreateTableCommand({
|
|
255
|
+
TableName: "${tableName}",
|
|
256
|
+
KeySchema: [{ AttributeName: "${pk}", KeyType: "HASH" }],
|
|
257
|
+
AttributeDefinitions: [{ AttributeName: "${pk}", AttributeType: "N" }],
|
|
258
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function down(db) {
|
|
263
|
+
await db.send(new DeleteTableCommand({ TableName: "${tableName}" }));
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Generate a Redis migration JS file (Redis doesn't need table creation,
|
|
270
|
+
* but we create a placeholder for consistency).
|
|
271
|
+
*/
|
|
272
|
+
function generateRedisMigration(tableName) {
|
|
273
|
+
return `"use strict";
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
async up(db) {
|
|
277
|
+
// Redis is schema-less. This migration is a placeholder.
|
|
278
|
+
// Data for "${tableName}" will be stored as hash keys: ${tableName}:<id>
|
|
279
|
+
console.log("Redis: ${tableName} collection ready (schema-less).");
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async down(db) {
|
|
283
|
+
// Warning: this deletes ALL keys matching the pattern
|
|
284
|
+
// In production, use SCAN instead of KEYS
|
|
285
|
+
const keys = await db.keys("${tableName}:*");
|
|
286
|
+
if (keys.length > 0) {
|
|
287
|
+
await db.del(...keys);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Public API
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
const SQL_ADAPTERS = [
|
|
299
|
+
"mysql",
|
|
300
|
+
"mariadb",
|
|
301
|
+
"postgres",
|
|
302
|
+
"sqlite3",
|
|
303
|
+
"mssql",
|
|
304
|
+
"cockroachdb",
|
|
305
|
+
"oracle",
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Generate migration file content for all tables in the schema.
|
|
310
|
+
*
|
|
311
|
+
* For SQL adapters: produces a single .sql file with all CREATE TABLE statements.
|
|
312
|
+
* For NoSQL adapters: produces one .js file per table.
|
|
313
|
+
*
|
|
314
|
+
* @param {object} schema - parsed schema from parseSchema()
|
|
315
|
+
* @returns {Array<{ filename: string, content: string }>}
|
|
316
|
+
*/
|
|
317
|
+
function generateMigrationFiles(schema) {
|
|
318
|
+
const adapter = schema.adapter;
|
|
319
|
+
const tables = schema.tables;
|
|
320
|
+
const tableNames = Object.keys(tables).sort();
|
|
321
|
+
|
|
322
|
+
if (SQL_ADAPTERS.includes(adapter)) {
|
|
323
|
+
// Single SQL file with all CREATE TABLE statements
|
|
324
|
+
const statements = [];
|
|
325
|
+
for (const name of tableNames) {
|
|
326
|
+
statements.push(generateCreateTableSQL(name, tables[name], adapter));
|
|
327
|
+
}
|
|
328
|
+
const content = statements.join("\n");
|
|
329
|
+
return [{ filename: `create_tables.sql`, content }];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// NoSQL: one file per table
|
|
333
|
+
const files = [];
|
|
334
|
+
for (const name of tableNames) {
|
|
335
|
+
let content;
|
|
336
|
+
if (adapter === "mongodb") {
|
|
337
|
+
content = generateMongoDBMigration(name, tables[name]);
|
|
338
|
+
} else if (adapter === "dynamodb") {
|
|
339
|
+
content = generateDynamoDBMigration(name, tables[name]);
|
|
340
|
+
} else if (adapter === "redis") {
|
|
341
|
+
content = generateRedisMigration(name);
|
|
342
|
+
} else {
|
|
343
|
+
// Fallback
|
|
344
|
+
content = `// Migration for ${name}\n`;
|
|
345
|
+
}
|
|
346
|
+
files.push({ filename: `create_${name}.js`, content });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return files;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
generateMigrationFiles,
|
|
354
|
+
generateCreateTableSQL,
|
|
355
|
+
mapColumnType,
|
|
356
|
+
};
|
|
@@ -58,8 +58,15 @@ export default route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
|
|
|
58
58
|
/**
|
|
59
59
|
* Generate the routes index file that mounts all routes on an express Router.
|
|
60
60
|
* Supports parent-child nesting: parent/:pk/child
|
|
61
|
+
*
|
|
62
|
+
* Child routes are placed in subfolders: routes/<parent>/<child>.js
|
|
63
|
+
* Children are only mounted under their parent path (no duplicate top-level route).
|
|
64
|
+
*
|
|
65
|
+
* @param {string[]} tableNames
|
|
66
|
+
* @param {Array<{parent, child, foreignKey}>} relationships
|
|
67
|
+
* @param {{ includeDocs?: boolean }} [options]
|
|
61
68
|
*/
|
|
62
|
-
function generateRoutesIndexFile(tableNames, relationships = []) {
|
|
69
|
+
function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
|
|
63
70
|
let imports = `import express from "express";\n\nconst router = express.Router();\n\n`;
|
|
64
71
|
|
|
65
72
|
// Collect child tables that are nested under parents
|
|
@@ -68,35 +75,42 @@ function generateRoutesIndexFile(tableNames, relationships = []) {
|
|
|
68
75
|
nestedChildren.add(rel.child);
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
// Import top-level routes only (not children)
|
|
71
79
|
for (const table of tableNames) {
|
|
80
|
+
if (nestedChildren.has(table)) continue;
|
|
72
81
|
const varName = safeVarName(table);
|
|
73
82
|
imports += `import ${varName}Route from "./${table}.js";\n`;
|
|
74
83
|
}
|
|
75
|
-
|
|
84
|
+
|
|
85
|
+
// Import child routes from subfolders
|
|
76
86
|
for (const rel of relationships) {
|
|
77
87
|
const varName = safeVarName(rel.child);
|
|
78
|
-
imports += `import ${varName}ChildRoute from "./${rel.
|
|
88
|
+
imports += `import ${varName}ChildRoute from "./${rel.parent}/${rel.child}.js";\n`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Import docs route if openapi is generated
|
|
92
|
+
if (options.includeDocs) {
|
|
93
|
+
imports += `import docsRoute from "./docs.js";\n`;
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
imports += "\n";
|
|
82
97
|
|
|
83
|
-
// Mount
|
|
98
|
+
// Mount docs route first
|
|
99
|
+
if (options.includeDocs) {
|
|
100
|
+
imports += `router.use("/docs", docsRoute);\n`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Mount top-level routes
|
|
84
104
|
for (const table of tableNames) {
|
|
85
105
|
if (nestedChildren.has(table)) continue;
|
|
86
106
|
const varName = safeVarName(table);
|
|
87
107
|
imports += `router.use("/${table}", ${varName}Route);\n`;
|
|
88
108
|
}
|
|
89
109
|
|
|
90
|
-
// Mount
|
|
110
|
+
// Mount child routes under parent path
|
|
91
111
|
for (const rel of relationships) {
|
|
92
112
|
const childVar = safeVarName(rel.child);
|
|
93
|
-
imports += `router.use("/${rel.parent}/:${rel.
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Also mount children as top-level for direct access
|
|
97
|
-
for (const rel of relationships) {
|
|
98
|
-
const varName = safeVarName(rel.child);
|
|
99
|
-
imports += `router.use("/${rel.child}", ${varName}Route);\n`;
|
|
113
|
+
imports += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
imports += "\nexport default router;\n";
|
|
@@ -387,7 +401,7 @@ async function main() {
|
|
|
387
401
|
const fkColumn = parent.replace(/s$/, "") + "_id";
|
|
388
402
|
// Only add if both tables exist in our model set
|
|
389
403
|
if (tableNames.includes(parent) && tableNames.includes(child)) {
|
|
390
|
-
relationships.push({ parent, child, fkColumn });
|
|
404
|
+
relationships.push({ parent, child, foreignKey: fkColumn });
|
|
391
405
|
}
|
|
392
406
|
}
|
|
393
407
|
}
|
|
@@ -398,23 +412,36 @@ async function main() {
|
|
|
398
412
|
fs.mkdirSync(routesDir, { recursive: true });
|
|
399
413
|
}
|
|
400
414
|
|
|
415
|
+
// Collect child tables to skip top-level route files
|
|
416
|
+
const nestedChildren = new Set();
|
|
417
|
+
for (const rel of relationships) {
|
|
418
|
+
nestedChildren.add(rel.child);
|
|
419
|
+
}
|
|
420
|
+
|
|
401
421
|
for (const table of tableNames) {
|
|
422
|
+
if (nestedChildren.has(table)) continue;
|
|
402
423
|
const filePath = path.join(routesDir, table + ".js");
|
|
403
424
|
fs.writeFileSync(filePath, generateRouteFile(table, modelsRelPath));
|
|
404
425
|
console.log(` Created ${filePath}`);
|
|
405
426
|
}
|
|
406
427
|
|
|
407
|
-
// Write child route files
|
|
428
|
+
// Write child route files in subfolders: routes/<parent>/<child>.js
|
|
408
429
|
for (const rel of relationships) {
|
|
409
|
-
const
|
|
410
|
-
|
|
430
|
+
const parentDir = path.join(routesDir, rel.parent);
|
|
431
|
+
if (!fs.existsSync(parentDir)) {
|
|
432
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
433
|
+
}
|
|
434
|
+
const filePath = path.join(parentDir, `${rel.child}.js`);
|
|
435
|
+
const childModelsRelPath = path
|
|
436
|
+
.relative(parentDir, path.resolve(modelsDir))
|
|
437
|
+
.replace(/\\/g, "/");
|
|
411
438
|
fs.writeFileSync(
|
|
412
439
|
filePath,
|
|
413
440
|
generateChildRouteFile(
|
|
414
441
|
rel.child,
|
|
415
442
|
rel.parent,
|
|
416
|
-
rel.
|
|
417
|
-
|
|
443
|
+
rel.foreignKey,
|
|
444
|
+
childModelsRelPath,
|
|
418
445
|
),
|
|
419
446
|
);
|
|
420
447
|
console.log(` Created ${filePath}`);
|
|
@@ -473,7 +500,7 @@ async function main() {
|
|
|
473
500
|
console.log(` Created ${testPath}`);
|
|
474
501
|
}
|
|
475
502
|
|
|
476
|
-
// Generate child route test files
|
|
503
|
+
// Generate child route test files in subfolders
|
|
477
504
|
for (const rel of relationships) {
|
|
478
505
|
let pk = "id";
|
|
479
506
|
const modelPath = path.join(modelsDir, rel.child + ".js");
|
|
@@ -484,13 +511,14 @@ async function main() {
|
|
|
484
511
|
);
|
|
485
512
|
if (meta && meta.primary_key) pk = meta.primary_key;
|
|
486
513
|
}
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
514
|
+
const parentTestDir = path.join(testsDir, rel.parent);
|
|
515
|
+
if (!fs.existsSync(parentTestDir)) {
|
|
516
|
+
fs.mkdirSync(parentTestDir, { recursive: true });
|
|
517
|
+
}
|
|
518
|
+
const testPath = path.join(parentTestDir, `${rel.child}.test.js`);
|
|
491
519
|
fs.writeFileSync(
|
|
492
520
|
testPath,
|
|
493
|
-
generateChildTestFile(rel.child, rel.parent, rel.
|
|
521
|
+
generateChildTestFile(rel.child, rel.parent, rel.foreignKey, pk),
|
|
494
522
|
);
|
|
495
523
|
console.log(` Created ${testPath}`);
|
|
496
524
|
}
|
|
@@ -1715,7 +1715,7 @@ function generateAppJsV2(answers, outputDir) {
|
|
|
1715
1715
|
answers.framework === "ultimate-express" ? "ultimate-express" : "express";
|
|
1716
1716
|
|
|
1717
1717
|
const commonsPrefix = outputDir ? `./${outputDir}/commons` : "./commons";
|
|
1718
|
-
const routePrefix = outputDir ? `./${outputDir}/
|
|
1718
|
+
const routePrefix = outputDir ? `./${outputDir}/routes` : "./routes";
|
|
1719
1719
|
const middlewarePrefix = outputDir
|
|
1720
1720
|
? `./${outputDir}/middleware`
|
|
1721
1721
|
: "./middleware";
|
package/src/cli/init.js
CHANGED
|
@@ -95,7 +95,7 @@ function generateFiles(answers, outputDir) {
|
|
|
95
95
|
path.join(srcBase, "middleware"),
|
|
96
96
|
path.join(srcBase, "migrations"),
|
|
97
97
|
path.join(srcBase, "commons"),
|
|
98
|
-
path.join(srcBase, "
|
|
98
|
+
path.join(srcBase, "routes"),
|
|
99
99
|
];
|
|
100
100
|
// SQLite3 needs a data/ folder for the database file
|
|
101
101
|
if (answers.database === "sqlite3") {
|
|
@@ -189,15 +189,15 @@ function generateFiles(answers, outputDir) {
|
|
|
189
189
|
if (safeWriteFile(dbPath, generateDbModule(answers)))
|
|
190
190
|
files.push(path.join(srcBase, "commons/db.js"));
|
|
191
191
|
|
|
192
|
-
//
|
|
193
|
-
const healthPath = path.join(srcBase, "
|
|
192
|
+
// routes/health.js
|
|
193
|
+
const healthPath = path.join(srcBase, "routes", "health.js");
|
|
194
194
|
if (safeWriteFile(healthPath, generateHealthRoute()))
|
|
195
|
-
files.push(path.join(srcBase, "
|
|
195
|
+
files.push(path.join(srcBase, "routes/health.js"));
|
|
196
196
|
|
|
197
|
-
//
|
|
198
|
-
const routeIndexPath = path.join(srcBase, "
|
|
197
|
+
// routes/index.js
|
|
198
|
+
const routeIndexPath = path.join(srcBase, "routes", "index.js");
|
|
199
199
|
if (safeWriteFile(routeIndexPath, generateRouteIndexFile()))
|
|
200
|
-
files.push(path.join(srcBase, "
|
|
200
|
+
files.push(path.join(srcBase, "routes/index.js"));
|
|
201
201
|
|
|
202
202
|
// Initial migration (inside outputDir/migrations)
|
|
203
203
|
const initialMigration = generateInitialMigration(answers);
|
|
@@ -365,7 +365,7 @@ Options:
|
|
|
365
365
|
--db <name> Alias for --database
|
|
366
366
|
--session <type> Session store: memory, redis, database
|
|
367
367
|
--output <dir> Directory for backend source files (e.g. --output backend).
|
|
368
|
-
package.json stays in root; index.js, commons/,
|
|
368
|
+
package.json stays in root; index.js, commons/, routes/,
|
|
369
369
|
middleware/, and migrations/ go inside the output folder.
|
|
370
370
|
--rateLimiting Enable rate limiting (express-rate-limit)
|
|
371
371
|
--helmet Enable Helmet security headers
|