db-model-router 1.0.8 → 1.0.10

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.
@@ -5,6 +5,7 @@ const path = require("path");
5
5
  const { generateModelFile } = require("./generate-model.js");
6
6
  const {
7
7
  generateRouteFile,
8
+ generateParentRouteFile,
8
9
  generateChildRouteFile,
9
10
  generateRoutesIndexFile,
10
11
  generateTestFile,
@@ -55,8 +56,11 @@ function buildExpectedFiles(meta, relationships) {
55
56
 
56
57
  // Collect child tables
57
58
  const nestedChildren = new Set();
59
+ const childrenByParent = {};
58
60
  for (const rel of relationships) {
59
61
  nestedChildren.add(rel.child);
62
+ if (!childrenByParent[rel.parent]) childrenByParent[rel.parent] = [];
63
+ childrenByParent[rel.parent].push(rel);
60
64
  }
61
65
 
62
66
  // Model files
@@ -67,16 +71,24 @@ function buildExpectedFiles(meta, relationships) {
67
71
  // Route files (top-level only, skip children)
68
72
  for (const m of meta) {
69
73
  if (nestedChildren.has(m.table)) continue;
70
- expected.set(
71
- `routes/${m.table}.js`,
72
- generateRouteFile(m.table, modelsRelPath),
73
- );
74
+ const children = childrenByParent[m.table] || [];
75
+ if (children.length > 0) {
76
+ expected.set(
77
+ `routes/${m.table}/index.js`,
78
+ generateParentRouteFile(m.table, children),
79
+ );
80
+ } else {
81
+ expected.set(
82
+ `routes/${m.table}/index.js`,
83
+ generateRouteFile(m.table, modelsRelPath),
84
+ );
85
+ }
74
86
  }
75
87
 
76
- // Child route files in subfolders: routes/<parent>/<child>.js
88
+ // Child route files inside parent folders: routes/<parent>/<child>/index.js
77
89
  for (const rel of relationships) {
78
90
  expected.set(
79
- `routes/${rel.parent}/${rel.child}.js`,
91
+ `routes/${rel.parent}/${rel.child}/index.js`,
80
92
  generateChildRouteFile(
81
93
  rel.child,
82
94
  rel.parent,
@@ -100,7 +112,7 @@ function buildExpectedFiles(meta, relationships) {
100
112
  if (nestedChildren.has(m.table)) continue;
101
113
  expected.set(
102
114
  `test/${m.table}.test.js`,
103
- generateTestFile(m.table, m.primary_key),
115
+ generateTestFile(m.table, m.primary_key, m.structure),
104
116
  );
105
117
  }
106
118
 
@@ -11,17 +11,83 @@
11
11
  // Column type mapping per adapter
12
12
  // ---------------------------------------------------------------------------
13
13
 
14
+ /**
15
+ * Known base types — anything not in this set is treated as a validator.
16
+ */
17
+ const BASE_TYPES = new Set([
18
+ "auto_increment",
19
+ "string",
20
+ "integer",
21
+ "numeric",
22
+ "boolean",
23
+ "datetime",
24
+ "object",
25
+ ]);
26
+
27
+ /**
28
+ * Parse a dbmr column rule string into its components.
29
+ *
30
+ * Format: [required|]<type>[:<subtype>][|<validator>...]
31
+ *
32
+ * @param {string} rule - e.g. "required|string:text|minLength:10|maxLength:5000"
33
+ * @returns {{ isRequired: boolean, baseType: string, subType: string|null, validators: string[] }}
34
+ */
35
+ function parseColumnRule(rule) {
36
+ const parts = rule.split("|");
37
+ const isRequired = parts.includes("required");
38
+
39
+ let baseType = "string";
40
+ let subType = null;
41
+ const validators = [];
42
+
43
+ for (const part of parts) {
44
+ if (part === "required") continue;
45
+
46
+ // Check if this part is a base type (possibly with :subtype)
47
+ const colonIdx = part.indexOf(":");
48
+ const token = colonIdx > -1 ? part.slice(0, colonIdx) : part;
49
+
50
+ if (!baseType || baseType === "string") {
51
+ if (BASE_TYPES.has(token)) {
52
+ baseType = token;
53
+ subType = colonIdx > -1 ? part.slice(colonIdx + 1) : null;
54
+ continue;
55
+ }
56
+ }
57
+
58
+ // If we already found the base type, or this isn't a known type, it's a validator
59
+ if (BASE_TYPES.has(token) && baseType === "string" && token !== "string") {
60
+ baseType = token;
61
+ subType = colonIdx > -1 ? part.slice(colonIdx + 1) : null;
62
+ } else if (BASE_TYPES.has(part)) {
63
+ // Bare base type without colon, and we haven't set one yet
64
+ if (baseType === "string" && part !== "string") {
65
+ baseType = part;
66
+ } else if (baseType === "string" && part === "string") {
67
+ // already default, skip
68
+ } else {
69
+ validators.push(part);
70
+ }
71
+ } else if (BASE_TYPES.has(token)) {
72
+ baseType = token;
73
+ subType = colonIdx > -1 ? part.slice(colonIdx + 1) : null;
74
+ } else {
75
+ validators.push(part);
76
+ }
77
+ }
78
+
79
+ return { isRequired, baseType, subType, validators };
80
+ }
81
+
14
82
  /**
15
83
  * Map a dbmr column rule to a SQL column type for the given adapter.
16
84
  *
17
- * @param {string} rule - e.g. "required|string", "auto_increment", "integer"
85
+ * @param {string} rule - e.g. "required|string:text|minLength:10", "auto_increment", "integer:bigint"
18
86
  * @param {string} adapter - e.g. "postgres", "mysql", "sqlite3"
19
- * @returns {{ sqlType: string, nullable: boolean, isAutoIncrement: boolean }}
87
+ * @returns {{ sqlType: string, nullable: boolean, isAutoIncrement: boolean, validators: string[] }}
20
88
  */
21
89
  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";
90
+ const { isRequired, baseType, subType, validators } = parseColumnRule(rule);
25
91
 
26
92
  let sqlType;
27
93
  let isAutoIncrement = false;
@@ -32,13 +98,13 @@ function mapColumnType(rule, adapter) {
32
98
  sqlType = autoIncrementType(adapter);
33
99
  break;
34
100
  case "string":
35
- sqlType = stringType(adapter);
101
+ sqlType = stringType(adapter, subType);
36
102
  break;
37
103
  case "integer":
38
- sqlType = integerType(adapter);
104
+ sqlType = integerType(adapter, subType);
39
105
  break;
40
106
  case "numeric":
41
- sqlType = numericType(adapter);
107
+ sqlType = numericType(adapter, subType);
42
108
  break;
43
109
  case "boolean":
44
110
  sqlType = booleanType(adapter);
@@ -50,13 +116,14 @@ function mapColumnType(rule, adapter) {
50
116
  sqlType = jsonType(adapter);
51
117
  break;
52
118
  default:
53
- sqlType = stringType(adapter);
119
+ sqlType = stringType(adapter, subType);
54
120
  }
55
121
 
56
122
  return {
57
123
  sqlType,
58
124
  nullable: !isRequired && !isAutoIncrement,
59
125
  isAutoIncrement,
126
+ validators,
60
127
  };
61
128
  }
62
129
 
@@ -77,20 +144,140 @@ function autoIncrementType(adapter) {
77
144
  }
78
145
  }
79
146
 
80
- function stringType(adapter) {
81
- if (adapter === "oracle") return "VARCHAR2(255)";
82
- return "VARCHAR(255)";
147
+ function stringType(adapter, subType) {
148
+ if (!subType) {
149
+ if (adapter === "oracle") return "VARCHAR2(255)";
150
+ if (adapter === "mssql") return "NVARCHAR(255)";
151
+ return "VARCHAR(255)";
152
+ }
153
+
154
+ // varchar(N) or char(N) with explicit length
155
+ const varcharMatch = subType.match(/^varchar\((\d+)\)$/i);
156
+ if (varcharMatch) {
157
+ const n = varcharMatch[1];
158
+ if (adapter === "oracle") return `VARCHAR2(${n})`;
159
+ if (adapter === "mssql") return `NVARCHAR(${n})`;
160
+ return `VARCHAR(${n})`;
161
+ }
162
+
163
+ const charMatch = subType.match(/^char\((\d+)\)$/i);
164
+ if (charMatch) {
165
+ const n = charMatch[1];
166
+ if (adapter === "oracle") return `CHAR(${n})`;
167
+ if (adapter === "mssql") return `NCHAR(${n})`;
168
+ if (adapter === "sqlite3") return "TEXT";
169
+ return `CHAR(${n})`;
170
+ }
171
+
172
+ switch (subType.toLowerCase()) {
173
+ case "text":
174
+ if (adapter === "oracle") return "CLOB";
175
+ if (adapter === "mssql") return "NVARCHAR(MAX)";
176
+ return "TEXT";
177
+ case "mediumtext":
178
+ if (adapter === "mysql" || adapter === "mariadb") return "MEDIUMTEXT";
179
+ if (adapter === "oracle") return "CLOB";
180
+ if (adapter === "mssql") return "NVARCHAR(MAX)";
181
+ return "TEXT";
182
+ case "longtext":
183
+ if (adapter === "mysql" || adapter === "mariadb") return "LONGTEXT";
184
+ if (adapter === "oracle") return "CLOB";
185
+ if (adapter === "mssql") return "NVARCHAR(MAX)";
186
+ return "TEXT";
187
+ case "char":
188
+ if (adapter === "oracle") return "CHAR(255)";
189
+ if (adapter === "mssql") return "NCHAR(255)";
190
+ if (adapter === "sqlite3") return "TEXT";
191
+ return "CHAR(255)";
192
+ case "uuid":
193
+ if (adapter === "postgres" || adapter === "cockroachdb") return "UUID";
194
+ if (adapter === "mssql") return "UNIQUEIDENTIFIER";
195
+ if (adapter === "oracle") return "CHAR(36)";
196
+ return "CHAR(36)";
197
+ default:
198
+ if (adapter === "oracle") return "VARCHAR2(255)";
199
+ if (adapter === "mssql") return "NVARCHAR(255)";
200
+ return "VARCHAR(255)";
201
+ }
83
202
  }
84
203
 
85
- function integerType(adapter) {
86
- if (adapter === "oracle") return "NUMBER(10)";
87
- return "INTEGER";
204
+ function integerType(adapter, subType) {
205
+ if (!subType) {
206
+ if (adapter === "oracle") return "NUMBER(10)";
207
+ return "INTEGER";
208
+ }
209
+
210
+ switch (subType.toLowerCase()) {
211
+ case "tinyint":
212
+ if (adapter === "oracle") return "NUMBER(3)";
213
+ if (adapter === "mssql") return "TINYINT";
214
+ if (adapter === "postgres" || adapter === "cockroachdb")
215
+ return "SMALLINT";
216
+ if (adapter === "sqlite3") return "INTEGER";
217
+ return "TINYINT";
218
+ case "smallint":
219
+ if (adapter === "oracle") return "NUMBER(5)";
220
+ if (adapter === "sqlite3") return "INTEGER";
221
+ return "SMALLINT";
222
+ case "bigint":
223
+ if (adapter === "oracle") return "NUMBER(19)";
224
+ if (adapter === "sqlite3") return "INTEGER";
225
+ return "BIGINT";
226
+ case "unsigned":
227
+ if (adapter === "mysql" || adapter === "mariadb") return "INT UNSIGNED";
228
+ if (adapter === "oracle") return "NUMBER(10)";
229
+ // PostgreSQL, SQLite, MSSQL don't support UNSIGNED — use plain INT
230
+ return "INTEGER";
231
+ case "bigint_unsigned":
232
+ if (adapter === "mysql" || adapter === "mariadb")
233
+ return "BIGINT UNSIGNED";
234
+ if (adapter === "oracle") return "NUMBER(19)";
235
+ if (adapter === "sqlite3") return "INTEGER";
236
+ return "BIGINT";
237
+ default:
238
+ if (adapter === "oracle") return "NUMBER(10)";
239
+ return "INTEGER";
240
+ }
88
241
  }
89
242
 
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)";
243
+ function numericType(adapter, subType) {
244
+ if (!subType) {
245
+ if (adapter === "oracle") return "NUMBER(12,2)";
246
+ if (adapter === "mssql") return "DECIMAL(12,2)";
247
+ return "DECIMAL(12,2)";
248
+ }
249
+
250
+ // decimal(P,S) with explicit precision/scale
251
+ const decimalMatch = subType.match(/^decimal\((\d+),(\d+)\)$/i);
252
+ if (decimalMatch) {
253
+ const p = decimalMatch[1];
254
+ const s = decimalMatch[2];
255
+ if (adapter === "oracle") return `NUMBER(${p},${s})`;
256
+ return `DECIMAL(${p},${s})`;
257
+ }
258
+
259
+ switch (subType.toLowerCase()) {
260
+ case "float":
261
+ if (adapter === "oracle") return "FLOAT";
262
+ if (adapter === "postgres" || adapter === "cockroachdb") return "REAL";
263
+ if (adapter === "sqlite3") return "REAL";
264
+ return "FLOAT";
265
+ case "double":
266
+ if (adapter === "oracle") return "BINARY_DOUBLE";
267
+ if (adapter === "postgres" || adapter === "cockroachdb")
268
+ return "DOUBLE PRECISION";
269
+ if (adapter === "sqlite3") return "REAL";
270
+ if (adapter === "mssql") return "FLOAT";
271
+ return "DOUBLE";
272
+ case "money":
273
+ if (adapter === "postgres" || adapter === "cockroachdb") return "MONEY";
274
+ if (adapter === "mssql") return "MONEY";
275
+ if (adapter === "oracle") return "NUMBER(19,4)";
276
+ return "DECIMAL(19,4)";
277
+ default:
278
+ if (adapter === "oracle") return "NUMBER(12,2)";
279
+ return "DECIMAL(12,2)";
280
+ }
94
281
  }
95
282
 
96
283
  function booleanType(adapter) {
@@ -353,4 +540,5 @@ module.exports = {
353
540
  generateMigrationFiles,
354
541
  generateCreateTableSQL,
355
542
  mapColumnType,
543
+ parseColumnRule,
356
544
  };
@@ -166,15 +166,19 @@ function generateSimpleRoutesIndexFile(tableNames) {
166
166
  * Generate a test file for a route covering all CRUD methods.
167
167
  * Uses supertest + the app's express setup.
168
168
  */
169
- function generateTestFile(tableName, pk) {
169
+ function generateTestFile(tableName, pk, structure) {
170
170
  const varName = safeVarName(tableName);
171
+ const fakerFields = generateFakerFields(structure || {});
172
+ const fakerImport = `import { faker } from "@faker-js/faker";`;
173
+
171
174
  return `import assert from "assert";
172
175
  import express from "express";
173
176
  import request from "supertest";
174
177
  import "dotenv/config";
175
- import "#commons/db.js";
178
+ import "../commons/db.js";
176
179
  import dbModelRouter from "db-model-router";
177
- import { ${varName} } from "#models";
180
+ import { ${varName} } from "../models/index.js";
181
+ ${fakerImport}
178
182
 
179
183
  const { route } = dbModelRouter;
180
184
 
@@ -185,92 +189,186 @@ function createApp() {
185
189
  return app;
186
190
  }
187
191
 
192
+ ${fakerFields.helperFn}
193
+
188
194
  describe("${tableName} routes", function () {
189
195
  let app;
196
+ let createdId;
190
197
 
191
198
  before(function () {
192
199
  app = createApp();
193
200
  });
194
201
 
195
- describe("GET /${tableName}/", function () {
196
- it("should list records", async function () {
202
+ describe("CRUD lifecycle", function () {
203
+ it("POST /${tableName}/add — should insert a record", async function () {
204
+ const data = generateFakeData();
205
+ const res = await request(app)
206
+ .post("/${tableName}/add")
207
+ .send(data);
208
+ assert.strictEqual(res.status, 200, \`Expected 200, got \${res.status}: \${JSON.stringify(res.body)}\`);
209
+ createdId = res.body.${pk} || res.body.id;
210
+ assert.ok(createdId, "Response should contain the created record ID");
211
+ });
212
+
213
+ it("GET /${tableName}/ — should list records including created one", async function () {
197
214
  const res = await request(app).get("/${tableName}/");
198
215
  assert.strictEqual(res.status, 200);
199
216
  assert.ok(Array.isArray(res.body.data));
217
+ assert.ok(res.body.data.length > 0, "Should have at least one record");
200
218
  });
201
- });
202
219
 
203
- describe("POST /${tableName}/add", function () {
204
- it("should insert a single record", async function () {
205
- const res = await request(app)
206
- .post("/${tableName}/add")
207
- .send({});
208
- assert.ok([200, 201, 400, 422, 500].includes(res.status));
220
+ it("GET /${tableName}/:${pk} — should get the created record", async function () {
221
+ const res = await request(app).get(\`/${tableName}/\${createdId}\`);
222
+ assert.strictEqual(res.status, 200, \`Expected 200, got \${res.status}\`);
223
+ assert.strictEqual(String(res.body.${pk} || res.body.id), String(createdId));
209
224
  });
210
- });
211
225
 
212
- describe("POST /${tableName}/", function () {
213
- it("should bulk insert records", async function () {
226
+ it("PUT /${tableName}/:${pk} — should update the record", async function () {
227
+ const data = { ...generateFakeData(), ${pk}: createdId };
214
228
  const res = await request(app)
215
- .post("/${tableName}/")
216
- .send({ data: [] });
217
- assert.ok([200, 201, 400, 422, 500].includes(res.status));
229
+ .put(\`/${tableName}/\${createdId}\`)
230
+ .send(data);
231
+ assert.strictEqual(res.status, 200, \`Expected 200, got \${res.status}: \${JSON.stringify(res.body)}\`);
218
232
  });
219
- });
220
233
 
221
- describe("GET /${tableName}/:${pk}", function () {
222
- it("should get a record by ID", async function () {
223
- const res = await request(app).get("/${tableName}/1");
224
- assert.ok([200, 404].includes(res.status));
225
- });
226
- });
227
-
228
- describe("PUT /${tableName}/:${pk}", function () {
229
- it("should update a record", async function () {
234
+ it("PATCH /${tableName}/:${pk} — should partially update the record", async function () {
230
235
  const res = await request(app)
231
- .put("/${tableName}/1")
232
- .send({});
233
- assert.ok([200, 400, 404].includes(res.status));
236
+ .patch(\`/${tableName}/\${createdId}\`)
237
+ .send(${fakerFields.patchPayload});
238
+ assert.strictEqual(res.status, 200, \`Expected 200, got \${res.status}: \${JSON.stringify(res.body)}\`);
234
239
  });
235
- });
236
240
 
237
- describe("PATCH /${tableName}/:${pk}", function () {
238
- it("should partially update a record", async function () {
239
- const res = await request(app)
240
- .patch("/${tableName}/1")
241
- .send({});
242
- assert.ok([200, 400, 404].includes(res.status));
241
+ it("DELETE /${tableName}/:${pk} — should delete the record", async function () {
242
+ const res = await request(app).delete(\`/${tableName}/\${createdId}\`);
243
+ assert.ok([200, 204].includes(res.status), \`Expected 200/204, got \${res.status}\`);
243
244
  });
244
- });
245
245
 
246
- describe("DELETE /${tableName}/:${pk}", function () {
247
- it("should delete a record", async function () {
248
- const res = await request(app).delete("/${tableName}/1");
249
- assert.ok([200, 204, 404].includes(res.status));
246
+ it("GET /${tableName}/:${pk} — should return 404 after deletion", async function () {
247
+ const res = await request(app).get(\`/${tableName}/\${createdId}\`);
248
+ assert.strictEqual(res.status, 404);
250
249
  });
251
250
  });
252
251
 
253
- describe("PUT /${tableName}/", function () {
254
- it("should bulk update records", async function () {
252
+ describe("Bulk operations", function () {
253
+ it("POST /${tableName}/ — should bulk insert records", async function () {
254
+ const data = [generateFakeData(), generateFakeData()];
255
255
  const res = await request(app)
256
- .put("/${tableName}/")
257
- .send({ data: [] });
258
- assert.ok([200, 400, 422, 500].includes(res.status));
256
+ .post("/${tableName}/")
257
+ .send({ data });
258
+ assert.strictEqual(res.status, 200, \`Expected 200, got \${res.status}: \${JSON.stringify(res.body)}\`);
259
259
  });
260
260
  });
261
261
 
262
- describe("DELETE /${tableName}/", function () {
263
- it("should bulk delete records", async function () {
264
- const res = await request(app)
265
- .delete("/${tableName}/")
266
- .send({});
267
- assert.ok([200, 204, 400].includes(res.status));
262
+ describe("Error handling", function () {
263
+ it("GET /${tableName}/:${pk} — should return 404 for non-existent ID", async function () {
264
+ const res = await request(app).get("/${tableName}/999999");
265
+ assert.strictEqual(res.status, 404);
268
266
  });
269
267
  });
270
268
  });
271
269
  `;
272
270
  }
273
271
 
272
+ /**
273
+ * Generate a faker helper function string and patch payload based on model structure.
274
+ *
275
+ * @param {object} structure - Model structure { colName: "rule" }
276
+ * @returns {{ helperFn: string, patchPayload: string }}
277
+ */
278
+ function generateFakerFields(structure) {
279
+ const lines = [];
280
+ let firstStringCol = null;
281
+
282
+ for (const [col, rule] of Object.entries(structure)) {
283
+ const fakerCall = columnToFaker(col, rule);
284
+ lines.push(` ${col}: ${fakerCall},`);
285
+ if (!firstStringCol && rule.includes("string")) {
286
+ firstStringCol = col;
287
+ }
288
+ }
289
+
290
+ const helperFn = `function generateFakeData() {
291
+ return {
292
+ ${lines.join("\n")}
293
+ };
294
+ }`;
295
+
296
+ const patchPayload = firstStringCol
297
+ ? `{ ${firstStringCol}: faker.lorem.word() }`
298
+ : `{ ${Object.keys(structure)[0] || "name"}: faker.lorem.word() }`;
299
+
300
+ return { helperFn, patchPayload };
301
+ }
302
+
303
+ /**
304
+ * Map a column name + rule to an appropriate faker call.
305
+ */
306
+ function columnToFaker(col, rule) {
307
+ const lowerCol = col.toLowerCase();
308
+
309
+ // Name-based heuristics first
310
+ if (lowerCol === "email") return "faker.internet.email()";
311
+ if (lowerCol === "phone") return "faker.phone.number()";
312
+ if (lowerCol === "name" || lowerCol.endsWith("_name"))
313
+ return "faker.person.fullName()";
314
+ if (lowerCol === "url" || lowerCol.endsWith("_url"))
315
+ return "faker.internet.url()";
316
+ if (lowerCol === "slug") return "faker.helpers.slugify(faker.lorem.words(2))";
317
+ if (lowerCol === "password" || lowerCol === "password_hash")
318
+ return "faker.internet.password()";
319
+ if (lowerCol === "secret" || lowerCol === "key" || lowerCol === "token")
320
+ return "faker.string.alphanumeric(32)";
321
+ if (lowerCol === "title") return "faker.lorem.sentence()";
322
+ if (
323
+ lowerCol === "description" ||
324
+ lowerCol === "body" ||
325
+ lowerCol === "content"
326
+ )
327
+ return "faker.lorem.paragraph()";
328
+ if (lowerCol === "status")
329
+ return "faker.helpers.arrayElement(['active', 'inactive', 'pending'])";
330
+ if (lowerCol === "event_type")
331
+ return "faker.helpers.arrayElement(['user.created', 'order.placed', 'payment.received'])";
332
+ if (lowerCol === "currency") return "faker.finance.currencyCode()";
333
+ if (
334
+ lowerCol.includes("amount") ||
335
+ lowerCol.includes("price") ||
336
+ lowerCol.includes("total") ||
337
+ lowerCol.includes("subtotal")
338
+ )
339
+ return "parseFloat(faker.finance.amount())";
340
+ if (lowerCol.includes("quantity") || lowerCol.includes("count"))
341
+ return "faker.number.int({ min: 1, max: 100 })";
342
+ if (lowerCol === "unique_attribute") return "faker.string.uuid()";
343
+ if (lowerCol === "attributes") return "{ custom: faker.lorem.word() }";
344
+ if (lowerCol === "permission")
345
+ return "{ module: 'users', action: 'read', scope: 'tenant' }";
346
+ if (lowerCol === "response_body") return "faker.lorem.sentence()";
347
+ if (lowerCol === "response_status_code")
348
+ return "faker.helpers.arrayElement([200, 201, 400, 500])";
349
+
350
+ // Type-based fallback
351
+ const parts = rule.split("|");
352
+ const baseType = parts.filter((p) => p !== "required")[0] || "string";
353
+
354
+ switch (baseType) {
355
+ case "integer":
356
+ if (lowerCol.endsWith("_id"))
357
+ return "faker.number.int({ min: 1, max: 100 })";
358
+ return "faker.number.int({ min: 1, max: 1000 })";
359
+ case "numeric":
360
+ return "parseFloat(faker.finance.amount())";
361
+ case "boolean":
362
+ return "faker.datatype.boolean()";
363
+ case "object":
364
+ return "{ key: faker.lorem.word() }";
365
+ case "datetime":
366
+ return "faker.date.recent().toISOString()";
367
+ default:
368
+ return "faker.lorem.word()";
369
+ }
370
+ }
371
+
274
372
  /**
275
373
  * Generate a child route test file that tests the nested parent/:fk/child endpoints.
276
374
  */
@@ -62,6 +62,10 @@ function collectDependencies(answers) {
62
62
 
63
63
  // Dev dependencies
64
64
  devDependencies["nodemon"] = "latest";
65
+ devDependencies["mocha"] = "latest";
66
+ devDependencies["supertest"] = "latest";
67
+ devDependencies["dotenv-cli"] = "latest";
68
+ devDependencies["@faker-js/faker"] = "latest";
65
69
 
66
70
  // Swagger UI for API documentation
67
71
  dependencies["swagger-ui-express"] = "latest";
@@ -370,19 +370,16 @@ function generateRoutesIndex(tableNames, relationships, options) {
370
370
  code += `import saasPermissionsRoute from "#routes/roles/permissions/index.js";\n\n`;
371
371
 
372
372
  // --- dbmr schema-generated route imports (folder-based, skip SaaS-owned tables) ---
373
+ // Child routes are mounted inside their parent's index.js, not here
373
374
  const dbmrTables = tableNames.filter(
374
375
  (t) => !saasRouteModules.has(t) && !nestedChildren.has(t),
375
376
  );
376
- if (dbmrTables.length > 0 || relationships.length > 0) {
377
+ if (dbmrTables.length > 0) {
377
378
  code += `// Schema-generated routes\n`;
378
379
  }
379
380
  for (const table of dbmrTables) {
380
381
  code += `import ${safeVarName(table)}Route from "#routes/${table}/index.js";\n`;
381
382
  }
382
- for (const rel of relationships) {
383
- if (saasRouteModules.has(rel.child)) continue;
384
- code += `import ${safeVarName(rel.child)}ChildRoute from "#routes/${rel.parent}/${rel.child}/index.js";\n`;
385
- }
386
383
 
387
384
  if (options.includeDocs) {
388
385
  code += `import docsRoute from "#routes/docs.js";\n`;
@@ -403,14 +400,7 @@ function generateRoutesIndex(tableNames, relationships, options) {
403
400
  code += `router.use("/docs", docsRoute);\n`;
404
401
  }
405
402
 
406
- // --- Mount dbmr child routes before parent routes ---
407
- for (const rel of relationships) {
408
- if (saasRouteModules.has(rel.child)) continue;
409
- const childVar = safeVarName(rel.child);
410
- code += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
411
- }
412
-
413
- // --- Mount dbmr top-level routes ---
403
+ // --- Mount dbmr top-level routes (children are inside parent's index.js) ---
414
404
  if (dbmrTables.length > 0) {
415
405
  code += `\n// Schema-generated routes\n`;
416
406
  }