db-model-router 1.0.2 → 1.0.3

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.
@@ -108,6 +108,192 @@ function generateSimpleRoutesIndexFile(tableNames) {
108
108
  return generateRoutesIndexFile(tableNames, []);
109
109
  }
110
110
 
111
+ /**
112
+ * Generate a test file for a route covering all CRUD methods.
113
+ * Uses supertest + the app's express setup.
114
+ */
115
+ function generateTestFile(tableName, pk) {
116
+ const varName = safeVarName(tableName);
117
+ return `const assert = require("assert");
118
+ const express = require("express");
119
+ const request = require("supertest");
120
+ const { route } = require("db-model-router");
121
+
122
+ // Adjust the path to your model file as needed
123
+ const ${varName} = require("../models/${tableName}");
124
+
125
+ function createApp() {
126
+ const app = express();
127
+ app.use(express.json());
128
+ app.use("/${tableName}", route(${varName}));
129
+ return app;
130
+ }
131
+
132
+ describe("${tableName} routes", function () {
133
+ let app;
134
+
135
+ before(function () {
136
+ app = createApp();
137
+ });
138
+
139
+ describe("GET /${tableName}/", function () {
140
+ it("should list records", async function () {
141
+ const res = await request(app).get("/${tableName}/");
142
+ assert.strictEqual(res.status, 200);
143
+ assert.ok(Array.isArray(res.body.data));
144
+ });
145
+ });
146
+
147
+ describe("POST /${tableName}/add", function () {
148
+ it("should insert a single record", async function () {
149
+ const res = await request(app)
150
+ .post("/${tableName}/add")
151
+ .send({});
152
+ assert.ok([200, 201, 400].includes(res.status));
153
+ });
154
+ });
155
+
156
+ describe("POST /${tableName}/", function () {
157
+ it("should bulk insert records", async function () {
158
+ const res = await request(app)
159
+ .post("/${tableName}/")
160
+ .send({ data: [] });
161
+ assert.ok([200, 201, 400].includes(res.status));
162
+ });
163
+ });
164
+
165
+ describe("GET /${tableName}/:${pk}", function () {
166
+ it("should get a record by ID", async function () {
167
+ const res = await request(app).get("/${tableName}/1");
168
+ assert.ok([200, 404].includes(res.status));
169
+ });
170
+ });
171
+
172
+ describe("PUT /${tableName}/:${pk}", function () {
173
+ it("should update a record", async function () {
174
+ const res = await request(app)
175
+ .put("/${tableName}/1")
176
+ .send({});
177
+ assert.ok([200, 400, 404].includes(res.status));
178
+ });
179
+ });
180
+
181
+ describe("PATCH /${tableName}/:${pk}", function () {
182
+ it("should partially update a record", async function () {
183
+ const res = await request(app)
184
+ .patch("/${tableName}/1")
185
+ .send({});
186
+ assert.ok([200, 400, 404].includes(res.status));
187
+ });
188
+ });
189
+
190
+ describe("DELETE /${tableName}/:${pk}", function () {
191
+ it("should delete a record", async function () {
192
+ const res = await request(app).delete("/${tableName}/1");
193
+ assert.ok([200, 204, 404].includes(res.status));
194
+ });
195
+ });
196
+
197
+ describe("PUT /${tableName}/", function () {
198
+ it("should bulk update records", async function () {
199
+ const res = await request(app)
200
+ .put("/${tableName}/")
201
+ .send({ data: [] });
202
+ assert.ok([200, 400].includes(res.status));
203
+ });
204
+ });
205
+
206
+ describe("DELETE /${tableName}/", function () {
207
+ it("should bulk delete records", async function () {
208
+ const res = await request(app)
209
+ .delete("/${tableName}/")
210
+ .send({});
211
+ assert.ok([200, 204, 400].includes(res.status));
212
+ });
213
+ });
214
+ });
215
+ `;
216
+ }
217
+
218
+ /**
219
+ * Generate a child route test file that tests the nested parent/:fk/child endpoints.
220
+ */
221
+ function generateChildTestFile(childTable, parentTable, fkColumn, pk) {
222
+ const childVar = safeVarName(childTable);
223
+ return `const assert = require("assert");
224
+ const express = require("express");
225
+ const request = require("supertest");
226
+ const { route } = require("db-model-router");
227
+
228
+ const ${childVar} = require("../models/${childTable}");
229
+
230
+ function createApp() {
231
+ const app = express();
232
+ app.use(express.json());
233
+ app.use("/${parentTable}/:${fkColumn}/${childTable}", route(${childVar}, { ${fkColumn}: "params.${fkColumn}" }));
234
+ return app;
235
+ }
236
+
237
+ describe("${childTable} (child of ${parentTable}) routes", function () {
238
+ let app;
239
+ const parentId = 1;
240
+
241
+ before(function () {
242
+ app = createApp();
243
+ });
244
+
245
+ describe("GET /${parentTable}/:${fkColumn}/${childTable}/", function () {
246
+ it("should list child records scoped by parent", async function () {
247
+ const res = await request(app).get(\`/${parentTable}/\${parentId}/${childTable}/\`);
248
+ assert.strictEqual(res.status, 200);
249
+ assert.ok(Array.isArray(res.body.data));
250
+ });
251
+ });
252
+
253
+ describe("POST /${parentTable}/:${fkColumn}/${childTable}/add", function () {
254
+ it("should insert a child record", async function () {
255
+ const res = await request(app)
256
+ .post(\`/${parentTable}/\${parentId}/${childTable}/add\`)
257
+ .send({});
258
+ assert.ok([200, 201, 400].includes(res.status));
259
+ });
260
+ });
261
+
262
+ describe("GET /${parentTable}/:${fkColumn}/${childTable}/:${pk}", function () {
263
+ it("should get a child record by ID", async function () {
264
+ const res = await request(app).get(\`/${parentTable}/\${parentId}/${childTable}/1\`);
265
+ assert.ok([200, 404].includes(res.status));
266
+ });
267
+ });
268
+
269
+ describe("PUT /${parentTable}/:${fkColumn}/${childTable}/:${pk}", function () {
270
+ it("should update a child record", async function () {
271
+ const res = await request(app)
272
+ .put(\`/${parentTable}/\${parentId}/${childTable}/1\`)
273
+ .send({});
274
+ assert.ok([200, 400, 404].includes(res.status));
275
+ });
276
+ });
277
+
278
+ describe("PATCH /${parentTable}/:${fkColumn}/${childTable}/:${pk}", function () {
279
+ it("should partially update a child record", async function () {
280
+ const res = await request(app)
281
+ .patch(\`/${parentTable}/\${parentId}/${childTable}/1\`)
282
+ .send({});
283
+ assert.ok([200, 400, 404].includes(res.status));
284
+ });
285
+ });
286
+
287
+ describe("DELETE /${parentTable}/:${fkColumn}/${childTable}/:${pk}", function () {
288
+ it("should delete a child record", async function () {
289
+ const res = await request(app).delete(\`/${parentTable}/\${parentId}/${childTable}/1\`);
290
+ assert.ok([200, 204, 404].includes(res.status));
291
+ });
292
+ });
293
+ });
294
+ `;
295
+ }
296
+
111
297
  /**
112
298
  * Read model directory to discover table names from generated model files.
113
299
  * Looks for .js files that are not index.js.
@@ -262,6 +448,51 @@ async function main() {
262
448
  // OpenAPI generation is optional, don't fail
263
449
  }
264
450
 
451
+ // Generate test files for all routes
452
+ const testsDir = path.resolve(path.dirname(routesDir), "tests");
453
+ if (!fs.existsSync(testsDir)) {
454
+ fs.mkdirSync(testsDir, { recursive: true });
455
+ }
456
+
457
+ for (const table of tableNames) {
458
+ // Try to extract PK from model file
459
+ let pk = "id";
460
+ const modelPath = path.join(modelsDir, table + ".js");
461
+ if (fs.existsSync(modelPath)) {
462
+ const meta = parseModelFile(fs.readFileSync(modelPath, "utf8"), table);
463
+ if (meta && meta.primary_key) pk = meta.primary_key;
464
+ }
465
+ const testPath = path.join(testsDir, table + ".test.js");
466
+ fs.writeFileSync(testPath, generateTestFile(table, pk));
467
+ console.log(` Created ${testPath}`);
468
+ }
469
+
470
+ // Generate child route test files
471
+ for (const rel of relationships) {
472
+ let pk = "id";
473
+ const modelPath = path.join(modelsDir, rel.child + ".js");
474
+ if (fs.existsSync(modelPath)) {
475
+ const meta = parseModelFile(
476
+ fs.readFileSync(modelPath, "utf8"),
477
+ rel.child,
478
+ );
479
+ if (meta && meta.primary_key) pk = meta.primary_key;
480
+ }
481
+ const testPath = path.join(
482
+ testsDir,
483
+ `${rel.child}_child_of_${rel.parent}.test.js`,
484
+ );
485
+ fs.writeFileSync(
486
+ testPath,
487
+ generateChildTestFile(rel.child, rel.parent, rel.fkColumn, pk),
488
+ );
489
+ console.log(` Created ${testPath}`);
490
+ }
491
+
492
+ console.log(
493
+ `Generated ${tableNames.length + relationships.length} test file(s) in ${testsDir}`,
494
+ );
495
+
265
496
  process.exit(0);
266
497
  }
267
498
 
@@ -271,7 +502,9 @@ async function main() {
271
502
  function parseModelFile(content, tableName) {
272
503
  try {
273
504
  // Extract structure JSON
274
- const structMatch = content.match(/model\(\s*\n?\s*db,\s*\n?\s*"[^"]+",\s*\n?\s*(\{[\s\S]*?\}),/);
505
+ const structMatch = content.match(
506
+ /model\(\s*\n?\s*db,\s*\n?\s*"[^"]+",\s*\n?\s*(\{[\s\S]*?\}),/,
507
+ );
275
508
  if (!structMatch) return null;
276
509
  const structure = JSON.parse(structMatch[1]);
277
510
  // Extract primary key
@@ -281,7 +514,7 @@ function parseModelFile(content, tableName) {
281
514
  } catch (e) {
282
515
  return null;
283
516
  }
284
-
517
+ }
285
518
  function parseArgs(argv) {
286
519
  const args = {};
287
520
  for (let i = 0; i < argv.length; i++) {
@@ -341,6 +574,8 @@ module.exports = {
341
574
  generateRouteFile,
342
575
  generateChildRouteFile,
343
576
  generateRoutesIndexFile,
577
+ generateTestFile,
578
+ generateChildTestFile,
344
579
  discoverModels,
345
580
  safeVarName,
346
581
  };
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Maps each supported database to its driver package(s).
5
+ */
6
+ const DRIVER_MAP = {
7
+ mysql: ["mysql2"],
8
+ postgres: ["pg"],
9
+ sqlite3: ["better-sqlite3"],
10
+ mongodb: ["mongodb"],
11
+ mssql: ["mssql"],
12
+ cockroachdb: ["pg"],
13
+ oracle: ["oracledb"],
14
+ redis: ["ioredis"],
15
+ dynamodb: ["@aws-sdk/client-dynamodb", "@aws-sdk/lib-dynamodb"],
16
+ };
17
+
18
+ /**
19
+ * Collect dependencies and devDependencies based on user answers.
20
+ * @param {import('./types').InitAnswers} answers
21
+ * @returns {{ dependencies: Record<string, string>, devDependencies: Record<string, string> }}
22
+ */
23
+ function collectDependencies(answers) {
24
+ const dependencies = {};
25
+ const devDependencies = {};
26
+
27
+ // Always included
28
+ dependencies["db-model-router"] = "latest";
29
+ dependencies["dotenv"] = "latest";
30
+ dependencies[answers.framework] = "latest";
31
+ dependencies["express-session"] = "latest";
32
+
33
+ // Database driver(s)
34
+ const drivers = DRIVER_MAP[answers.database] || [];
35
+ for (const driver of drivers) {
36
+ dependencies[driver] = "latest";
37
+ }
38
+
39
+ // Session: redis
40
+ if (answers.session === "redis") {
41
+ dependencies["connect-redis"] = "latest";
42
+ // Only add ioredis if not already included via the database driver
43
+ if (answers.database !== "redis") {
44
+ dependencies["ioredis"] = "latest";
45
+ }
46
+ }
47
+
48
+ // Optional middleware
49
+ if (answers.rateLimiting) {
50
+ dependencies["express-rate-limit"] = "latest";
51
+ }
52
+ if (answers.helmet) {
53
+ dependencies["helmet"] = "latest";
54
+ }
55
+ if (answers.logger) {
56
+ dependencies["express-mung"] = "latest";
57
+ }
58
+
59
+ // Dev dependencies
60
+ devDependencies["nodemon"] = "latest";
61
+
62
+ return { dependencies, devDependencies };
63
+ }
64
+
65
+ /**
66
+ * Returns the 5 package.json scripts.
67
+ * @returns {Record<string, string>}
68
+ */
69
+ function getScripts() {
70
+ return {
71
+ start: "node app.js",
72
+ dev: "nodemon app.js",
73
+ test: 'echo "Error: no test specified" && exit 1',
74
+ migrate: "node migrate.js",
75
+ add_migration: "node add_migration.js",
76
+ };
77
+ }
78
+
79
+ module.exports = {
80
+ DRIVER_MAP,
81
+ collectDependencies,
82
+ getScripts,
83
+ };