db-model-router 1.0.3 → 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.
Files changed (97) hide show
  1. package/README.md +283 -25
  2. package/TODO.md +14 -0
  3. package/dbmr.schema.json +333 -0
  4. package/demo/.dockerignore +7 -0
  5. package/demo/.env.example +13 -0
  6. package/demo/Dockerfile +20 -0
  7. package/demo/app.js +37 -0
  8. package/demo/commons/add_migration.js +43 -0
  9. package/demo/commons/db.js +17 -0
  10. package/demo/commons/migrate.js +65 -0
  11. package/demo/commons/security.js +30 -0
  12. package/demo/commons/session.js +13 -0
  13. package/demo/dbmr.schema.json +362 -0
  14. package/demo/docs/llm.md +197 -0
  15. package/demo/llms.txt +70 -0
  16. package/demo/middleware/logger.js +67 -0
  17. package/demo/migrations/20260430155808_create_migrations_table.sql +6 -0
  18. package/demo/migrations/20260430155809_create_tables.sql +207 -0
  19. package/demo/models/addresses.js +22 -0
  20. package/demo/models/cart_items.js +18 -0
  21. package/demo/models/carts.js +16 -0
  22. package/demo/models/categories.js +20 -0
  23. package/demo/models/coupons.js +23 -0
  24. package/demo/models/order_items.js +21 -0
  25. package/demo/models/orders.js +25 -0
  26. package/demo/models/payments.js +21 -0
  27. package/demo/models/product_images.js +18 -0
  28. package/demo/models/product_reviews.js +20 -0
  29. package/demo/models/product_variants.js +20 -0
  30. package/demo/models/products.js +30 -0
  31. package/demo/models/shipments.js +19 -0
  32. package/demo/models/users.js +19 -0
  33. package/demo/models/wishlists.js +15 -0
  34. package/demo/openapi.json +5872 -0
  35. package/demo/package-lock.json +2810 -0
  36. package/demo/package.json +34 -0
  37. package/demo/routes/addresses.js +6 -0
  38. package/demo/routes/carts/cart_items.js +7 -0
  39. package/demo/routes/carts.js +6 -0
  40. package/demo/routes/categories.js +6 -0
  41. package/demo/routes/coupons.js +6 -0
  42. package/demo/routes/docs.js +18 -0
  43. package/demo/routes/health.js +35 -0
  44. package/demo/routes/index.js +39 -0
  45. package/demo/routes/orders/order_items.js +7 -0
  46. package/demo/routes/orders/payments.js +7 -0
  47. package/demo/routes/orders/shipments.js +7 -0
  48. package/demo/routes/orders.js +6 -0
  49. package/demo/routes/products/product_images.js +7 -0
  50. package/demo/routes/products/product_reviews.js +7 -0
  51. package/demo/routes/products/product_variants.js +7 -0
  52. package/demo/routes/products.js +6 -0
  53. package/demo/routes/users.js +6 -0
  54. package/demo/routes/wishlists.js +6 -0
  55. package/docker-compose.yml +1 -1
  56. package/package.json +16 -7
  57. package/scripts/demo-create.js +47 -0
  58. package/skill/SKILL.md +464 -0
  59. package/skill/references/cockroachdb.md +49 -0
  60. package/skill/references/dynamodb.md +53 -0
  61. package/skill/references/mongodb.md +56 -0
  62. package/skill/references/mssql.md +55 -0
  63. package/skill/references/oracle.md +52 -0
  64. package/skill/references/postgres.md +50 -0
  65. package/skill/references/redis.md +53 -0
  66. package/skill/references/sqlite3.md +43 -0
  67. package/src/cli/commands/generate.js +58 -17
  68. package/src/cli/commands/help.js +185 -0
  69. package/src/cli/commands/init.js +42 -14
  70. package/src/cli/commands/inspect.js +21 -3
  71. package/src/cli/diff-engine.js +52 -22
  72. package/src/cli/generate-docs-route.js +31 -0
  73. package/src/cli/generate-migration.js +356 -0
  74. package/src/cli/generate-model.js +5 -4
  75. package/src/cli/generate-route.js +79 -45
  76. package/src/cli/init/dependencies.js +17 -5
  77. package/src/cli/init/generators.js +1073 -64
  78. package/src/cli/init/prompt.js +37 -5
  79. package/src/cli/init.js +148 -25
  80. package/src/cli/main.js +90 -10
  81. package/src/cockroachdb/db.js +90 -59
  82. package/src/commons/route.js +20 -20
  83. package/src/commons/validator.js +58 -1
  84. package/src/dynamodb/db.js +50 -27
  85. package/src/index.js +2 -0
  86. package/src/mongodb/db.js +1 -0
  87. package/src/mssql/db.js +89 -61
  88. package/src/mysql/db.js +1 -0
  89. package/src/oracle/db.js +1 -0
  90. package/src/postgres/db.js +61 -41
  91. package/src/redis/db.js +1 -0
  92. package/src/schema/schema-parser.js +43 -1
  93. package/src/schema/schema-printer.js +8 -5
  94. package/src/schema/schema-to-meta.js +4 -0
  95. package/src/schema/schema-validator.js +20 -1
  96. package/src/sqlite3/db.js +1 -0
  97. package/docs/SKILL.md +0 -374
@@ -2,6 +2,7 @@
2
2
 
3
3
  const SQL_DATABASES = [
4
4
  "mysql",
5
+ "mariadb",
5
6
  "postgres",
6
7
  "sqlite3",
7
8
  "mssql",
@@ -54,6 +55,13 @@ const DB_ENV_MAP = {
54
55
  { key: "DB_USER", defaultValue: "root", placeholder: "your_user" },
55
56
  { key: "DB_PASS", defaultValue: "password", placeholder: "your_password" },
56
57
  ],
58
+ mariadb: [
59
+ { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
60
+ { key: "DB_PORT", defaultValue: "3306", placeholder: "3306" },
61
+ { key: "DB_NAME", defaultValue: "my_app", placeholder: "your_database" },
62
+ { key: "DB_USER", defaultValue: "root", placeholder: "your_user" },
63
+ { key: "DB_PASS", defaultValue: "password", placeholder: "your_password" },
64
+ ],
57
65
  postgres: [
58
66
  { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
59
67
  { key: "DB_PORT", defaultValue: "5432", placeholder: "5432" },
@@ -69,7 +77,11 @@ const DB_ENV_MAP = {
69
77
  { key: "DB_PASS", defaultValue: "password", placeholder: "your_password" },
70
78
  ],
71
79
  sqlite3: [
72
- { key: "DB_NAME", defaultValue: "./data.db", placeholder: "./data.db" },
80
+ {
81
+ key: "DB_NAME",
82
+ defaultValue: "./data/data.db",
83
+ placeholder: "./data/data.db",
84
+ },
73
85
  ],
74
86
  mongodb: [
75
87
  { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
@@ -123,13 +135,36 @@ const REDIS_SESSION_VARS = [
123
135
  { key: "REDIS_PASS", defaultValue: "", placeholder: "your_password" },
124
136
  ];
125
137
 
138
+ /**
139
+ * Generate a random alphanumeric password.
140
+ * @param {number} [length=24]
141
+ * @returns {string}
142
+ */
143
+ function randomPassword(length) {
144
+ const chars =
145
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
146
+ const len = length || 24;
147
+ let result = "";
148
+ const crypto = require("crypto");
149
+ const bytes = crypto.randomBytes(len);
150
+ for (let i = 0; i < len; i++) {
151
+ result += chars[bytes[i] % chars.length];
152
+ }
153
+ return result;
154
+ }
155
+
126
156
  /**
127
157
  * Build env file content from the config map.
128
158
  * @param {import('./types').InitAnswers} answers
129
159
  * @param {'default'|'placeholder'} mode
160
+ * @param {object} [secrets] - generated secrets to keep in sync with docker-compose
161
+ * @param {string} [secrets.dbPass] - database password
162
+ * @param {string} [secrets.redisPass] - redis session password
163
+ * @param {string} [secrets.sessionSecret] - session secret
130
164
  * @returns {string}
131
165
  */
132
- function buildEnvContent(answers, mode) {
166
+ function buildEnvContent(answers, mode, secrets) {
167
+ const s = secrets || {};
133
168
  const pick = mode === "placeholder" ? "placeholder" : "defaultValue";
134
169
  const lines = [];
135
170
  lines.push("# Server");
@@ -139,14 +174,19 @@ function buildEnvContent(answers, mode) {
139
174
 
140
175
  const vars = DB_ENV_MAP[answers.database] || [];
141
176
  for (const v of vars) {
142
- lines.push(`${v.key}=${v[pick]}`);
177
+ // Override password with generated secret in default mode
178
+ if (mode === "default" && v.key === "DB_PASS" && s.dbPass) {
179
+ lines.push(`${v.key}=${s.dbPass}`);
180
+ } else {
181
+ lines.push(`${v.key}=${v[pick]}`);
182
+ }
143
183
  }
144
184
 
145
185
  // Session secret
146
186
  lines.push("");
147
187
  lines.push("# Session");
148
188
  lines.push(
149
- `SESSION_SECRET=${mode === "placeholder" ? "your_session_secret" : "change-me"}`,
189
+ `SESSION_SECRET=${mode === "placeholder" ? "your_session_secret" : s.sessionSecret || "change-me"}`,
150
190
  );
151
191
 
152
192
  // Redis session env vars when session is redis and database is not redis
@@ -154,7 +194,29 @@ function buildEnvContent(answers, mode) {
154
194
  lines.push("");
155
195
  lines.push("# Redis Session");
156
196
  for (const v of REDIS_SESSION_VARS) {
157
- lines.push(`${v.key}=${v[pick]}`);
197
+ if (mode === "default" && v.key === "REDIS_PASS" && s.redisPass) {
198
+ lines.push(`${v.key}=${s.redisPass}`);
199
+ } else {
200
+ lines.push(`${v.key}=${v[pick]}`);
201
+ }
202
+ }
203
+ }
204
+
205
+ // Logging
206
+ if (answers.logger) {
207
+ lines.push("");
208
+ lines.push("# Logging");
209
+ lines.push(
210
+ `APP_NAME=${mode === "placeholder" ? "your_app_name" : "my-app"}`,
211
+ );
212
+ lines.push(`LOG_LEVEL=${mode === "placeholder" ? "info" : "info"}`);
213
+ // LOKI_HOST: empty by default, set a URL to enable Loki transport
214
+ if (answers.loki && mode === "default") {
215
+ lines.push("LOKI_HOST=http://localhost:3100");
216
+ } else {
217
+ lines.push(
218
+ `LOKI_HOST=${mode === "placeholder" ? "http://your-loki-host:3100" : ""}`,
219
+ );
158
220
  }
159
221
  }
160
222
 
@@ -165,10 +227,11 @@ function buildEnvContent(answers, mode) {
165
227
  /**
166
228
  * Generate .env file content.
167
229
  * @param {import('./types').InitAnswers} answers
230
+ * @param {object} [secrets] - generated secrets
168
231
  * @returns {string}
169
232
  */
170
- function generateEnvFile(answers) {
171
- return buildEnvContent(answers, "default");
233
+ function generateEnvFile(answers, secrets) {
234
+ return buildEnvContent(answers, "default", secrets);
172
235
  }
173
236
 
174
237
  /**
@@ -209,7 +272,7 @@ function dbConnectBlock(database) {
209
272
  }
210
273
  if (database === "sqlite3") {
211
274
  return `db.connect({
212
- database: process.env.DB_NAME || "./data.db",
275
+ database: process.env.DB_NAME || "./data/data.db",
213
276
  });`;
214
277
  }
215
278
  return `db.connect({
@@ -221,6 +284,40 @@ function dbConnectBlock(database) {
221
284
  });`;
222
285
  }
223
286
 
287
+ /**
288
+ * Return just the connect config properties (indented, no wrapper).
289
+ * Used by generateDbModule where the caller controls the object name.
290
+ * @param {string} database
291
+ * @returns {string}
292
+ */
293
+ function dbConnectArgs(database) {
294
+ if (database === "dynamodb") {
295
+ return ` region: process.env.AWS_REGION,
296
+ endpoint: process.env.AWS_ENDPOINT,
297
+ credentials: {
298
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
299
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
300
+ },
301
+ `;
302
+ }
303
+ if (database === "redis") {
304
+ return ` host: process.env.DB_HOST || "localhost",
305
+ port: process.env.DB_PORT || 6379,
306
+ password: process.env.DB_PASS,
307
+ `;
308
+ }
309
+ if (database === "sqlite3") {
310
+ return ` database: process.env.DB_NAME || "./data/data.db",
311
+ `;
312
+ }
313
+ return ` host: process.env.DB_HOST || "localhost",
314
+ port: process.env.DB_PORT,
315
+ database: process.env.DB_NAME,
316
+ user: process.env.DB_USER,
317
+ password: process.env.DB_PASS,
318
+ `;
319
+ }
320
+
224
321
  /**
225
322
  * Generate session middleware block for app.js.
226
323
  * @param {import('./types').InitAnswers} answers
@@ -341,35 +438,79 @@ module.exports = app;
341
438
 
342
439
  /**
343
440
  * Generate middleware/logger.js content.
441
+ * When logger is enabled, uses Winston with winston-loki transport for Grafana.
344
442
  * @param {import('./types').InitAnswers} answers
345
443
  * @returns {string}
346
444
  */
347
445
  function generateLoggerMiddleware(answers) {
348
446
  if (answers.logger) {
349
- return `const mung = require("express-mung");
447
+ return `import winston from "winston";
350
448
 
351
449
  /**
352
- * Request/response logger middleware using express-mung.
353
- * Logs request details, response body, and response time.
450
+ * Winston logger with Console transport.
451
+ * If LOKI_HOST is set in .env, adds a Loki transport for Grafana visualization.
354
452
  */
355
- const logger = mung.json(function transform(body, req, res) {
356
- const duration = Date.now() - req._startTime;
357
- const status = res.statusCode;
358
- const level = status >= 400 ? "WARN" : "INFO";
359
- console.log(
360
- \`[\${new Date().toISOString()}] [\${level}] \${req.method} \${req.originalUrl} \${status} \${duration}ms\`,
453
+ const transports = [
454
+ new winston.transports.Console({
455
+ format: winston.format.combine(
456
+ winston.format.colorize(),
457
+ winston.format.printf(({ timestamp, level, message, ...meta }) => {
458
+ const metaStr = Object.keys(meta).length > 1
459
+ ? " " + JSON.stringify(meta)
460
+ : "";
461
+ return \`[\${timestamp}] [\${level}] \${message}\${metaStr}\`;
462
+ }),
463
+ ),
464
+ }),
465
+ ];
466
+
467
+ // Add Loki transport only when LOKI_HOST is configured
468
+ if (process.env.LOKI_HOST) {
469
+ const { default: LokiTransport } = await import("winston-loki");
470
+ transports.push(
471
+ new LokiTransport({
472
+ host: process.env.LOKI_HOST,
473
+ labels: { app: process.env.APP_NAME || "app" },
474
+ json: true,
475
+ onConnectionError: (err) => console.error("Loki connection error:", err),
476
+ }),
361
477
  );
362
- console.log(" Request headers:", JSON.stringify(req.headers));
363
- console.log(" Response body:", JSON.stringify(body));
364
- return body;
478
+ }
479
+
480
+ const logger = winston.createLogger({
481
+ level: process.env.LOG_LEVEL || "info",
482
+ format: winston.format.combine(
483
+ winston.format.timestamp(),
484
+ winston.format.json(),
485
+ ),
486
+ defaultMeta: { service: process.env.APP_NAME || "app" },
487
+ transports,
365
488
  });
366
489
 
367
- function startTimer(req, res, next) {
368
- req._startTime = Date.now();
490
+ /**
491
+ * Express middleware that logs every request/response.
492
+ */
493
+ function requestLogger(req, res, next) {
494
+ const start = Date.now();
495
+
496
+ res.on("finish", () => {
497
+ const duration = Date.now() - start;
498
+ const level = res.statusCode >= 400 ? "warn" : "info";
499
+ logger.log({
500
+ level,
501
+ message: \`\${req.method} \${req.originalUrl} \${res.statusCode} \${duration}ms\`,
502
+ method: req.method,
503
+ url: req.originalUrl,
504
+ status: res.statusCode,
505
+ duration,
506
+ });
507
+ });
508
+
369
509
  next();
370
510
  }
371
511
 
372
- module.exports = [startTimer, logger];
512
+ requestLogger.logger = logger;
513
+ export default requestLogger;
373
514
  `;
374
515
  }
375
516
 
@@ -377,7 +518,7 @@ module.exports = [startTimer, logger];
377
518
  * Simple request logger middleware.
378
519
  * Logs method, URL, status code, and response time.
379
520
  */
380
- module.exports = function logger(req, res, next) {
521
+ export default function logger(req, res, next) {
381
522
  const start = Date.now();
382
523
  const { method, originalUrl } = req;
383
524
 
@@ -391,7 +532,7 @@ module.exports = function logger(req, res, next) {
391
532
  });
392
533
 
393
534
  next();
394
- };
535
+ }
395
536
  `;
396
537
  }
397
538
 
@@ -649,53 +790,41 @@ function generateInitialMigration(answers, date) {
649
790
  // NoSQL databases
650
791
  let content;
651
792
  if (answers.database === "mongodb") {
652
- content = `"use strict";
653
-
654
- module.exports = {
655
- async up(db) {
656
- await db.createCollection("_migrations");
657
- await db.collection("_migrations").createIndex({ filename: 1 }, { unique: true });
658
- },
793
+ content = `export async function up(db) {
794
+ await db.createCollection("_migrations");
795
+ await db.collection("_migrations").createIndex({ filename: 1 }, { unique: true });
796
+ }
659
797
 
660
- async down(db) {
661
- await db.collection("_migrations").drop();
662
- },
663
- };
798
+ export async function down(db) {
799
+ await db.collection("_migrations").drop();
800
+ }
664
801
  `;
665
802
  } else if (answers.database === "redis") {
666
- content = `"use strict";
667
-
668
- module.exports = {
669
- async up(db) {
670
- // _migrations hash key will be created on first HSET
671
- console.log("Redis migration tracking initialized using hash key: _migrations");
672
- },
803
+ content = `export async function up(db) {
804
+ // _migrations hash key will be created on first HSET
805
+ console.log("Redis migration tracking initialized using hash key: _migrations");
806
+ }
673
807
 
674
- async down(db) {
675
- await db.del("_migrations");
676
- },
677
- };
808
+ export async function down(db) {
809
+ await db.del("_migrations");
810
+ }
678
811
  `;
679
812
  } else {
680
813
  // dynamodb
681
- content = `"use strict";
682
-
683
- module.exports = {
684
- async up(db) {
685
- const { CreateTableCommand } = require("@aws-sdk/client-dynamodb");
686
- await db.send(new CreateTableCommand({
687
- TableName: "_migrations",
688
- KeySchema: [{ AttributeName: "filename", KeyType: "HASH" }],
689
- AttributeDefinitions: [{ AttributeName: "filename", AttributeType: "S" }],
690
- BillingMode: "PAY_PER_REQUEST",
691
- }));
692
- },
814
+ content = `import { CreateTableCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb";
815
+
816
+ export async function up(db) {
817
+ await db.send(new CreateTableCommand({
818
+ TableName: "_migrations",
819
+ KeySchema: [{ AttributeName: "filename", KeyType: "HASH" }],
820
+ AttributeDefinitions: [{ AttributeName: "filename", AttributeType: "S" }],
821
+ BillingMode: "PAY_PER_REQUEST",
822
+ }));
823
+ }
693
824
 
694
- async down(db) {
695
- const { DeleteTableCommand } = require("@aws-sdk/client-dynamodb");
696
- await db.send(new DeleteTableCommand({ TableName: "_migrations" }));
697
- },
698
- };
825
+ export async function down(db) {
826
+ await db.send(new DeleteTableCommand({ TableName: "_migrations" }));
827
+ }
699
828
  `;
700
829
  }
701
830
 
@@ -754,6 +883,409 @@ CREATE INDEX idx_sessions_expired ON sessions(expired_at);
754
883
  return { filename: `${ts}_create_sessions_table.sql`, content };
755
884
  }
756
885
 
886
+ // ---------------------------------------------------------------------------
887
+ // Docker Compose generator
888
+ // ---------------------------------------------------------------------------
889
+
890
+ /**
891
+ * Docker image and config for each supported database.
892
+ */
893
+ const DOCKER_DB_MAP = {
894
+ mysql: {
895
+ image: "mysql:latest",
896
+ port: "3306:3306",
897
+ env: (secrets) => ({
898
+ MYSQL_ROOT_PASSWORD: secrets.dbPass,
899
+ MYSQL_DATABASE: "my_app",
900
+ }),
901
+ volumes: ["./data/mysql:/var/lib/mysql"],
902
+ },
903
+ mariadb: {
904
+ image: "mariadb:latest",
905
+ port: "3306:3306",
906
+ env: (secrets) => ({
907
+ MARIADB_ROOT_PASSWORD: secrets.dbPass,
908
+ MARIADB_DATABASE: "my_app",
909
+ }),
910
+ volumes: ["./data/mariadb:/var/lib/mysql"],
911
+ },
912
+ postgres: {
913
+ image: "postgres:alpine",
914
+ port: "5432:5432",
915
+ env: (secrets) => ({
916
+ POSTGRES_USER: "postgres",
917
+ POSTGRES_PASSWORD: secrets.dbPass,
918
+ POSTGRES_DB: "my_app",
919
+ }),
920
+ volumes: ["./data/postgres:/var/lib/postgresql/data"],
921
+ },
922
+ cockroachdb: {
923
+ image: "cockroachdb/cockroach:latest",
924
+ port: "26257:26257",
925
+ command: "start-single-node --insecure",
926
+ env: () => ({}),
927
+ volumes: ["./data/cockroachdb:/cockroach/cockroach-data"],
928
+ },
929
+ mongodb: {
930
+ image: "mongo:latest",
931
+ port: "27017:27017",
932
+ env: (secrets) => ({
933
+ MONGO_INITDB_ROOT_USERNAME: "root",
934
+ MONGO_INITDB_ROOT_PASSWORD: secrets.dbPass,
935
+ MONGO_INITDB_DATABASE: "my_app",
936
+ }),
937
+ volumes: ["./data/mongodb:/data/db"],
938
+ },
939
+ mssql: {
940
+ image: "mcr.microsoft.com/mssql/server:latest",
941
+ port: "1433:1433",
942
+ env: (secrets) => ({
943
+ ACCEPT_EULA: "Y",
944
+ MSSQL_SA_PASSWORD: secrets.dbPass,
945
+ }),
946
+ volumes: ["./data/mssql:/var/opt/mssql"],
947
+ },
948
+ oracle: {
949
+ image: "gvenzl/oracle-xe:latest",
950
+ port: "1521:1521",
951
+ env: (secrets) => ({
952
+ ORACLE_PASSWORD: secrets.dbPass,
953
+ APP_USER: "system",
954
+ APP_USER_PASSWORD: secrets.dbPass,
955
+ }),
956
+ volumes: ["./data/oracle:/opt/oracle/oradata"],
957
+ },
958
+ redis: {
959
+ image: "redis:alpine",
960
+ port: "6379:6379",
961
+ command: null, // set dynamically if password
962
+ env: () => ({}),
963
+ volumes: ["./data/redis:/data"],
964
+ },
965
+ dynamodb: {
966
+ image: "amazon/dynamodb-local:latest",
967
+ port: "8000:8000",
968
+ env: () => ({}),
969
+ volumes: [],
970
+ },
971
+ };
972
+
973
+ /**
974
+ * CloudBeaver JDBC driver IDs and URL templates per database.
975
+ */
976
+ const CLOUDBEAVER_DB_MAP = {
977
+ mysql: {
978
+ provider: "mysql",
979
+ driver: "mysql8",
980
+ urlTemplate: (host, port, dbName) =>
981
+ `jdbc:mysql://${host}:${port}/${dbName}`,
982
+ },
983
+ mariadb: {
984
+ provider: "mysql",
985
+ driver: "mariaDB",
986
+ urlTemplate: (host, port, dbName) =>
987
+ `jdbc:mariadb://${host}:${port}/${dbName}`,
988
+ },
989
+ postgres: {
990
+ provider: "postgresql",
991
+ driver: "postgres-jdbc",
992
+ urlTemplate: (host, port, dbName) =>
993
+ `jdbc:postgresql://${host}:${port}/${dbName}`,
994
+ },
995
+ cockroachdb: {
996
+ provider: "postgresql",
997
+ driver: "postgres-jdbc",
998
+ urlTemplate: (host, port, dbName) =>
999
+ `jdbc:postgresql://${host}:${port}/${dbName}`,
1000
+ },
1001
+ mssql: {
1002
+ provider: "sqlserver",
1003
+ driver: "mssql_jdbc_ms_new",
1004
+ urlTemplate: (host, port, dbName) =>
1005
+ `jdbc:sqlserver://${host}:${port};databaseName=${dbName};trustServerCertificate=true`,
1006
+ },
1007
+ oracle: {
1008
+ provider: "oracle",
1009
+ driver: "oracle_thin",
1010
+ urlTemplate: (host, port, dbName) =>
1011
+ `jdbc:oracle:thin:@${host}:${port}/${dbName}`,
1012
+ },
1013
+ mongodb: {
1014
+ provider: "mongodb",
1015
+ driver: "mongodb",
1016
+ urlTemplate: (host, port, dbName) => `mongodb://${host}:${port}/${dbName}`,
1017
+ },
1018
+ };
1019
+
1020
+ /**
1021
+ * Generate CloudBeaver data-sources.json for auto-connecting to the project database.
1022
+ * @param {import('./types').InitAnswers} answers
1023
+ * @param {object} secrets
1024
+ * @returns {string|null}
1025
+ */
1026
+ function generateCloudBeaverDataSources(answers, secrets) {
1027
+ const cbDb = CLOUDBEAVER_DB_MAP[answers.database];
1028
+ if (!cbDb) return null;
1029
+
1030
+ const dbConfig = DOCKER_DB_MAP[answers.database];
1031
+ if (!dbConfig) return null;
1032
+
1033
+ const host = answers.database; // service name in docker-compose
1034
+ const port = dbConfig.port.split(":")[1];
1035
+ const dbName = "my_app";
1036
+
1037
+ // Determine user/pass based on adapter
1038
+ let user = "root";
1039
+ let pass = secrets.dbPass;
1040
+ if (answers.database === "postgres" || answers.database === "cockroachdb")
1041
+ user = "postgres";
1042
+ if (answers.database === "mssql") user = "sa";
1043
+ if (answers.database === "oracle") user = "system";
1044
+ if (answers.database === "mongodb") user = "root";
1045
+
1046
+ const connId = `${answers.database}-project-db`;
1047
+ const url = cbDb.urlTemplate(host, port, dbName);
1048
+
1049
+ const config = {
1050
+ folders: {},
1051
+ connections: {
1052
+ [connId]: {
1053
+ provider: cbDb.provider,
1054
+ driver: cbDb.driver,
1055
+ name: `${answers.database} - my_app`,
1056
+ "save-password": true,
1057
+ configuration: {
1058
+ host: host,
1059
+ port: port,
1060
+ database: dbName,
1061
+ url: url,
1062
+ configurationType: "MANUAL",
1063
+ type: "dev",
1064
+ auth: "native",
1065
+ userName: user,
1066
+ userPassword: pass,
1067
+ },
1068
+ },
1069
+ },
1070
+ };
1071
+
1072
+ return JSON.stringify(config, null, 2) + "\n";
1073
+ }
1074
+
1075
+ /**
1076
+ * Generate docker-compose.yml content.
1077
+ * @param {import('./types').InitAnswers} answers
1078
+ * @param {object} secrets - { dbPass, redisPass }
1079
+ * @returns {string|null} null if no Docker service needed (e.g. sqlite3)
1080
+ */
1081
+ function generateDockerCompose(answers, secrets) {
1082
+ // sqlite3 runs in-process, no Docker needed
1083
+ if (answers.database === "sqlite3") return null;
1084
+
1085
+ const dbConfig = DOCKER_DB_MAP[answers.database];
1086
+ if (!dbConfig) return null;
1087
+
1088
+ const services = {};
1089
+
1090
+ // --- Primary database service ---
1091
+ const dbService = {
1092
+ container_name: `${answers.database}_db`,
1093
+ image: dbConfig.image,
1094
+ ports: [dbConfig.port],
1095
+ restart: "unless-stopped",
1096
+ };
1097
+
1098
+ const envVars = dbConfig.env(secrets);
1099
+ if (Object.keys(envVars).length > 0) {
1100
+ dbService.environment = envVars;
1101
+ }
1102
+ if (dbConfig.command) {
1103
+ dbService.command = dbConfig.command;
1104
+ }
1105
+ // Redis with password
1106
+ if (answers.database === "redis" && secrets.dbPass) {
1107
+ dbService.command = `redis-server --requirepass ${secrets.dbPass}`;
1108
+ }
1109
+ if (dbConfig.volumes && dbConfig.volumes.length > 0) {
1110
+ dbService.volumes = dbConfig.volumes;
1111
+ }
1112
+
1113
+ services[answers.database] = dbService;
1114
+
1115
+ // --- Redis session service (if session=redis and db is not already redis) ---
1116
+ if (answers.session === "redis" && answers.database !== "redis") {
1117
+ const redisService = {
1118
+ container_name: "redis_session",
1119
+ image: "redis:alpine",
1120
+ ports: ["6379:6379"],
1121
+ restart: "unless-stopped",
1122
+ };
1123
+ if (secrets.redisPass) {
1124
+ redisService.command = `redis-server --requirepass ${secrets.redisPass}`;
1125
+ }
1126
+ redisService.volumes = ["./data/redis:/data"];
1127
+ services["redis"] = redisService;
1128
+ }
1129
+
1130
+ // --- CloudBeaver service (for SQL/MongoDB databases) ---
1131
+ const hasCbSupport = !!CLOUDBEAVER_DB_MAP[answers.database];
1132
+ if (hasCbSupport) {
1133
+ services["cloudbeaver"] = {
1134
+ container_name: "cloudbeaver",
1135
+ image: "dbeaver/cloudbeaver:latest",
1136
+ ports: ["8978:8978"],
1137
+ restart: "unless-stopped",
1138
+ environment: {
1139
+ CB_SERVER_NAME: "CloudBeaver",
1140
+ CB_ADMIN_NAME: "cbadmin",
1141
+ CB_ADMIN_PASSWORD: secrets.dbPass,
1142
+ },
1143
+ volumes: [
1144
+ "./data/cloudbeaver:/opt/cloudbeaver/workspace",
1145
+ "./.cloudbeaver/data-sources.json:/opt/cloudbeaver/workspace/GlobalConfiguration/.dbeaver/data-sources.json:ro",
1146
+ ],
1147
+ depends_on: [answers.database],
1148
+ };
1149
+ }
1150
+
1151
+ // --- Loki + Grafana (when logger + loki are enabled) ---
1152
+ if (answers.loki) {
1153
+ services["loki"] = {
1154
+ container_name: "loki",
1155
+ image: "grafana/loki:latest",
1156
+ ports: ["3100:3100"],
1157
+ restart: "unless-stopped",
1158
+ command: "-config.file=/etc/loki/local-config.yaml",
1159
+ volumes: ["./data/loki:/loki"],
1160
+ };
1161
+
1162
+ services["grafana"] = {
1163
+ container_name: "grafana",
1164
+ image: "grafana/grafana:latest",
1165
+ ports: ["3001:3000"],
1166
+ restart: "unless-stopped",
1167
+ environment: {
1168
+ GF_SECURITY_ADMIN_USER: "admin",
1169
+ GF_SECURITY_ADMIN_PASSWORD: secrets.dbPass,
1170
+ GF_AUTH_ANONYMOUS_ENABLED: "true",
1171
+ },
1172
+ volumes: [
1173
+ "./data/grafana:/var/lib/grafana",
1174
+ "./.grafana/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml:ro",
1175
+ ],
1176
+ depends_on: ["loki"],
1177
+ };
1178
+ }
1179
+
1180
+ // --- Build YAML manually (no dependency needed) ---
1181
+ const lines = [];
1182
+ lines.push("services:");
1183
+
1184
+ for (const [name, svc] of Object.entries(services)) {
1185
+ lines.push(` ${name}:`);
1186
+ lines.push(` container_name: ${svc.container_name}`);
1187
+ lines.push(` image: ${svc.image}`);
1188
+ if (svc.command) {
1189
+ lines.push(` command: ${svc.command}`);
1190
+ }
1191
+ if (svc.ports && svc.ports.length > 0) {
1192
+ lines.push(" ports:");
1193
+ for (const p of svc.ports) {
1194
+ lines.push(` - "${p}"`);
1195
+ }
1196
+ }
1197
+ if (svc.environment && Object.keys(svc.environment).length > 0) {
1198
+ lines.push(" environment:");
1199
+ for (const [k, v] of Object.entries(svc.environment)) {
1200
+ lines.push(` ${k}: "${v}"`);
1201
+ }
1202
+ }
1203
+ if (svc.volumes && svc.volumes.length > 0) {
1204
+ lines.push(" volumes:");
1205
+ for (const v of svc.volumes) {
1206
+ lines.push(` - ${v}`);
1207
+ }
1208
+ }
1209
+ if (svc.depends_on && svc.depends_on.length > 0) {
1210
+ lines.push(" depends_on:");
1211
+ for (const d of svc.depends_on) {
1212
+ lines.push(` - ${d}`);
1213
+ }
1214
+ }
1215
+ lines.push(` restart: unless-stopped`);
1216
+ lines.push("");
1217
+ }
1218
+
1219
+ return lines.join("\n");
1220
+ }
1221
+
1222
+ /**
1223
+ * Generate Dockerfile for the project.
1224
+ * Uses multi-stage build with node:alpine for a lean production image.
1225
+ * @param {import('./types').InitAnswers} answers
1226
+ * @param {string} [outputDir] - relative output directory for source files
1227
+ * @returns {string}
1228
+ */
1229
+ function generateDockerfile(answers, outputDir) {
1230
+ const copyDirs = ["commons", "middleware", "route", "migrations"]
1231
+ .map((d) => {
1232
+ const src = outputDir ? `${outputDir}/${d}` : d;
1233
+ return `COPY ${src}/ ./${src}/`;
1234
+ })
1235
+ .join("\n");
1236
+
1237
+ return `FROM node:alpine
1238
+
1239
+ WORKDIR /app
1240
+
1241
+ # Install dependencies
1242
+ COPY package*.json ./
1243
+ RUN npm ci --omit=dev
1244
+
1245
+ # Copy application files
1246
+ COPY app.js ./
1247
+ ${copyDirs}
1248
+
1249
+ # Expose port
1250
+ EXPOSE 3000
1251
+
1252
+ # Start the application
1253
+ CMD ["node", "app.js"]
1254
+ `;
1255
+ }
1256
+
1257
+ /**
1258
+ * Generate Grafana datasource provisioning file for auto-connecting Loki.
1259
+ * @returns {string}
1260
+ */
1261
+ function generateGrafanaDatasources() {
1262
+ return `apiVersion: 1
1263
+
1264
+ datasources:
1265
+ - name: Loki
1266
+ type: loki
1267
+ access: proxy
1268
+ url: http://loki:3100
1269
+ isDefault: true
1270
+ editable: false
1271
+ `;
1272
+ }
1273
+
1274
+ /**
1275
+ * Generate .dockerignore content.
1276
+ * @returns {string}
1277
+ */
1278
+ function generateDockerignore() {
1279
+ return `node_modules
1280
+ npm-debug.log
1281
+ .env
1282
+ .env.example
1283
+ .git
1284
+ .gitignore
1285
+ data
1286
+ `;
1287
+ }
1288
+
757
1289
  /**
758
1290
  * Generate .gitignore content.
759
1291
  * @returns {string}
@@ -762,13 +1294,478 @@ function generateGitignore() {
762
1294
  return `node_modules/
763
1295
  .env
764
1296
  *.db
1297
+ data/
1298
+ .cloudbeaver/
1299
+ `;
1300
+ }
1301
+
1302
+ // ---------------------------------------------------------------------------
1303
+ // Commons: session.js generator
1304
+ // ---------------------------------------------------------------------------
1305
+
1306
+ /**
1307
+ * Generate commons/session.js — session configuration module.
1308
+ * @param {import('./types').InitAnswers} answers
1309
+ * @returns {string}
1310
+ */
1311
+ function generateSessionJs(answers) {
1312
+ let imports = `import session from "express-session";\n`;
1313
+
1314
+ if (answers.session === "redis") {
1315
+ imports += `import { RedisStore } from "connect-redis";\nimport ioredis from "ioredis";\n\nconst { Redis } = ioredis;\n`;
1316
+ }
1317
+
1318
+ let storeSetup = "";
1319
+ let storeOption = "";
1320
+
1321
+ if (answers.session === "redis") {
1322
+ const redisConfig =
1323
+ answers.database === "redis"
1324
+ ? ` host: process.env.DB_HOST || "localhost",\n port: process.env.DB_PORT || 6379,\n password: process.env.DB_PASS,`
1325
+ : ` host: process.env.REDIS_HOST || "localhost",\n port: process.env.REDIS_PORT || 6379,\n password: process.env.REDIS_PASS,`;
1326
+
1327
+ storeSetup = `\nconst redisClient = new Redis({\n${redisConfig}\n});\n`;
1328
+ storeOption = `\n store: new RedisStore({ client: redisClient }),`;
1329
+ }
1330
+
1331
+ return `${imports}${storeSetup}
1332
+ /**
1333
+ * Configure and return session middleware.
1334
+ * Session store: ${answers.session}
1335
+ */
1336
+ export default function configureSession() {
1337
+ return session({${storeOption}
1338
+ secret: process.env.SESSION_SECRET || "change-me",
1339
+ resave: false,
1340
+ saveUninitialized: false,
1341
+ });
1342
+ }
1343
+ `;
1344
+ }
1345
+
1346
+ // ---------------------------------------------------------------------------
1347
+ // Commons: migrate.js generator (standalone script)
1348
+ // ---------------------------------------------------------------------------
1349
+
1350
+ /**
1351
+ * Generate commons/migrate.js — migration runner module.
1352
+ * Works as both an importable module and a standalone script.
1353
+ * @param {import('./types').InitAnswers} answers
1354
+ * @param {string} [outputDir] - relative output directory
1355
+ * @returns {string}
1356
+ */
1357
+ function generateMigrateModule(answers, outputDir) {
1358
+ const isNoSql = NOSQL_DATABASES.includes(answers.database);
1359
+ // commons/migrate.js and migrations/ are sibling dirs inside the same outputDir
1360
+ const migrationsRel = "../migrations";
1361
+
1362
+ if (isNoSql) {
1363
+ return `#!/usr/bin/env node
1364
+ import fs from "fs";
1365
+ import path from "path";
1366
+ import crypto from "crypto";
1367
+ import { fileURLToPath } from "url";
1368
+
1369
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1370
+
1371
+ /**
1372
+ * Run all pending migrations from the migrations directory.
1373
+ * @param {object} db - db-model-router db instance
1374
+ * @param {string} migrationsDir - absolute path to migrations folder
1375
+ */
1376
+ export default async function runMigrations(db, migrationsDir) {
1377
+ const files = fs.readdirSync(migrationsDir)
1378
+ .filter(f => f.endsWith(".js"))
1379
+ .sort();
1380
+
1381
+ let executed;
1382
+ try {
1383
+ const result = await db.get("_migrations");
1384
+ executed = new Set((result || []).map(r => r.filename));
1385
+ } catch (e) {
1386
+ executed = new Set();
1387
+ }
1388
+
1389
+ let ran = 0;
1390
+ for (const file of files) {
1391
+ if (executed.has(file)) {
1392
+ console.log(\` Skipping (already executed): \${file}\`);
1393
+ continue;
1394
+ }
1395
+ const filePath = path.join(migrationsDir, file);
1396
+ const content = fs.readFileSync(filePath, "utf8");
1397
+ const checksum = crypto.createHash("md5").update(content).digest("hex");
1398
+
1399
+ const migration = await import(filePath);
1400
+ console.log(\` Running migration: \${file}\`);
1401
+ await migration.up(db);
1402
+ await db.insert("_migrations", {
1403
+ filename: file,
1404
+ executed_at: new Date().toISOString(),
1405
+ checksum,
1406
+ });
1407
+ console.log(\` Completed: \${file}\`);
1408
+ ran++;
1409
+ }
1410
+
1411
+ if (ran === 0) {
1412
+ console.log("No pending migrations.");
1413
+ } else {
1414
+ console.log(\`\\n\${ran} migration(s) complete.\`);
1415
+ }
1416
+ }
1417
+
1418
+ // Run as standalone script
1419
+ const isMain = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
1420
+ if (isMain) {
1421
+ await import("dotenv/config");
1422
+ const pkg = await import("db-model-router");
1423
+ const mod = pkg.default || pkg;
1424
+ mod.init("${answers.database}");
1425
+ const migrationsDir = path.join(__dirname, "${migrationsRel}");
1426
+ runMigrations(mod.db, migrationsDir)
1427
+ .then(() => process.exit(0))
1428
+ .catch(err => { console.error("Migration failed:", err); process.exit(1); });
1429
+ }
1430
+ `;
1431
+ }
1432
+
1433
+ return `#!/usr/bin/env node
1434
+ import fs from "fs";
1435
+ import path from "path";
1436
+ import crypto from "crypto";
1437
+ import { fileURLToPath } from "url";
1438
+
1439
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1440
+
1441
+ /**
1442
+ * Run all pending SQL migrations from the migrations directory.
1443
+ * @param {object} db - db-model-router db instance
1444
+ * @param {string} migrationsDir - absolute path to migrations folder
1445
+ */
1446
+ export default async function runMigrations(db, migrationsDir) {
1447
+ const files = fs.readdirSync(migrationsDir)
1448
+ .filter(f => f.endsWith(".sql"))
1449
+ .sort();
1450
+
1451
+ let executed;
1452
+ try {
1453
+ const result = await db.query("SELECT filename FROM _migrations");
1454
+ executed = new Set((result || []).map(r => r.filename));
1455
+ } catch (e) {
1456
+ executed = new Set();
1457
+ }
1458
+
1459
+ let ran = 0;
1460
+ for (const file of files) {
1461
+ if (executed.has(file)) {
1462
+ console.log(\` Skipping (already executed): \${file}\`);
1463
+ continue;
1464
+ }
1465
+ const filePath = path.join(migrationsDir, file);
1466
+ const content = fs.readFileSync(filePath, "utf8");
1467
+ const checksum = crypto.createHash("md5").update(content).digest("hex");
1468
+
1469
+ console.log(\` Running migration: \${file}\`);
1470
+ await db.query(content);
1471
+ await db.query(
1472
+ "INSERT INTO _migrations (filename, checksum) VALUES (?, ?)",
1473
+ [file, checksum]
1474
+ );
1475
+ console.log(\` Completed: \${file}\`);
1476
+ ran++;
1477
+ }
1478
+
1479
+ if (ran === 0) {
1480
+ console.log("No pending migrations.");
1481
+ } else {
1482
+ console.log(\`\\n\${ran} migration(s) complete.\`);
1483
+ }
1484
+ }
1485
+
1486
+ // Run as standalone script
1487
+ const isMain = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
1488
+ if (isMain) {
1489
+ await import("dotenv/config");
1490
+ const pkg = await import("db-model-router");
1491
+ const mod = pkg.default || pkg;
1492
+ mod.init("${answers.database}");
1493
+ const migrationsDir = path.join(__dirname, "${migrationsRel}");
1494
+ runMigrations(mod.db, migrationsDir)
1495
+ .then(() => process.exit(0))
1496
+ .catch(err => { console.error("Migration failed:", err); process.exit(1); });
1497
+ }
1498
+ `;
1499
+ }
1500
+
1501
+ // ---------------------------------------------------------------------------
1502
+ // Commons: add_migration.js generator (standalone script)
1503
+ // ---------------------------------------------------------------------------
1504
+
1505
+ /**
1506
+ * Generate commons/add_migration.js — migration creation helper module.
1507
+ * Works as both an importable module and a standalone script.
1508
+ * @param {import('./types').InitAnswers} answers
1509
+ * @param {string} [outputDir] - relative output directory
1510
+ * @returns {string}
1511
+ */
1512
+ function generateAddMigrationModule(answers, outputDir) {
1513
+ const isNoSql = NOSQL_DATABASES.includes(answers.database);
1514
+ const ext = isNoSql ? "js" : "sql";
1515
+ const template = isNoSql
1516
+ ? `export async function up(db) {\\n // Write your migration here\\n}\\n\\nexport async function down(db) {\\n // Write your rollback here\\n}\\n`
1517
+ : `-- Write your migration SQL here\\n`;
1518
+ const migrationsRel = "../migrations";
1519
+
1520
+ return `#!/usr/bin/env node
1521
+ import fs from "fs";
1522
+ import path from "path";
1523
+ import { fileURLToPath } from "url";
1524
+
1525
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1526
+
1527
+ /**
1528
+ * Create a new timestamped migration file.
1529
+ * @param {string} migrationsDir - absolute path to migrations folder
1530
+ * @param {string} [name] - migration name (default: "migration")
1531
+ * @returns {string} the created filename
1532
+ */
1533
+ export default function addMigration(migrationsDir, name) {
1534
+ const migrationName = name || "migration";
1535
+ const now = new Date();
1536
+ const y = String(now.getFullYear()).padStart(4, "0");
1537
+ const mo = String(now.getMonth() + 1).padStart(2, "0");
1538
+ const d = String(now.getDate()).padStart(2, "0");
1539
+ const h = String(now.getHours()).padStart(2, "0");
1540
+ const mi = String(now.getMinutes()).padStart(2, "0");
1541
+ const s = String(now.getSeconds()).padStart(2, "0");
1542
+ const ts = \`\${y}\${mo}\${d}\${h}\${mi}\${s}\`;
1543
+
1544
+ const filename = \`\${ts}_\${migrationName}.${ext}\`;
1545
+ const filePath = path.join(migrationsDir, filename);
1546
+
1547
+ if (!fs.existsSync(migrationsDir)) {
1548
+ fs.mkdirSync(migrationsDir, { recursive: true });
1549
+ }
1550
+
1551
+ fs.writeFileSync(filePath, "${template}");
1552
+ console.log(\`Created migration: \${filename}\`);
1553
+ return filename;
1554
+ }
1555
+
1556
+ // Run as standalone script
1557
+ const isMain = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
1558
+ if (isMain) {
1559
+ const migrationsDir = path.join(__dirname, "${migrationsRel}");
1560
+ const name = process.argv[2] || "migration";
1561
+ addMigration(migrationsDir, name);
1562
+ }
1563
+ `;
1564
+ }
1565
+
1566
+ // ---------------------------------------------------------------------------
1567
+ // Commons: security.js generator (helmet + header overrides)
1568
+ // ---------------------------------------------------------------------------
1569
+
1570
+ /**
1571
+ * Generate commons/security.js — helmet and custom header security middleware.
1572
+ * @param {import('./types').InitAnswers} answers
1573
+ * @returns {string}
1574
+ */
1575
+ function generateSecurityJs(answers) {
1576
+ let imports = "";
1577
+ if (answers.helmet) {
1578
+ imports += `import helmet from "helmet";\n`;
1579
+ }
1580
+ if (answers.rateLimiting) {
1581
+ imports += `import rateLimit from "express-rate-limit";\n`;
1582
+ }
1583
+
1584
+ return `${imports}
1585
+ /**
1586
+ * Apply security middleware to the Express app.
1587
+ * Includes: ${answers.helmet ? "Helmet, " : ""}${answers.rateLimiting ? "rate limiting, " : ""}custom security headers.
1588
+ * @param {import("express").Application} app
1589
+ */
1590
+ export default function applySecurity(app) {
1591
+ ${answers.helmet ? ` // Helmet — sets various HTTP headers for security\n app.use(helmet());\n` : " // Helmet is not enabled. Install and enable via --helmet flag.\n"}
1592
+ ${answers.rateLimiting ? ` // Rate limiting\n app.use(rateLimit({\n windowMs: 15 * 60 * 1000,\n max: 100,\n standardHeaders: true,\n legacyHeaders: false,\n }));\n` : ""}
1593
+ // Custom security headers (override or extend as needed)
1594
+ app.use((req, res, next) => {
1595
+ res.setHeader("X-Content-Type-Options", "nosniff");
1596
+ res.setHeader("X-Frame-Options", "DENY");
1597
+ res.setHeader("X-XSS-Protection", "1; mode=block");
1598
+ res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
1599
+ res.removeHeader("X-Powered-By");
1600
+ next();
1601
+ });
1602
+ }
1603
+ `;
1604
+ }
1605
+
1606
+ // ---------------------------------------------------------------------------
1607
+ // Route: health.js generator
1608
+ // ---------------------------------------------------------------------------
1609
+
1610
+ /**
1611
+ * Generate route/health.js — health check route.
1612
+ * @returns {string}
1613
+ */
1614
+ function generateHealthRoute() {
1615
+ return `import express from "express";
1616
+
1617
+ const router = express.Router();
1618
+
1619
+ /**
1620
+ * GET /health
1621
+ * Returns server health status, uptime, memory, and database connectivity.
1622
+ */
1623
+ router.get("/", async (req, res) => {
1624
+ const health = {
1625
+ status: "ok",
1626
+ timestamp: new Date().toISOString(),
1627
+ uptime: process.uptime(),
1628
+ memory: process.memoryUsage(),
1629
+ db: { connected: false },
1630
+ };
1631
+
1632
+ try {
1633
+ if (global.db && typeof global.db.query === "function") {
1634
+ await global.db.query("SELECT NOW()");
1635
+ health.db.connected = true;
1636
+ } else if (global.db && typeof global.db.get === "function") {
1637
+ // NoSQL adapters (mongodb, redis, dynamodb)
1638
+ health.db.connected = true;
1639
+ }
1640
+ } catch (err) {
1641
+ health.status = "degraded";
1642
+ health.db.error = err.message;
1643
+ }
1644
+
1645
+ const statusCode = health.status === "ok" ? 200 : 503;
1646
+ res.status(statusCode).json(health);
1647
+ });
1648
+
1649
+ export default router;
1650
+ `;
1651
+ }
1652
+
1653
+ // ---------------------------------------------------------------------------
1654
+ // Route: index.js generator — mounts all route modules
1655
+ // ---------------------------------------------------------------------------
1656
+
1657
+ /**
1658
+ * Generate route/index.js — central route mounting file.
1659
+ * @returns {string}
1660
+ */
1661
+ function generateRouteIndexFile() {
1662
+ return `import express from "express";
1663
+ import healthRoute from "./health.js";
1664
+
1665
+ const router = express.Router();
1666
+
1667
+ router.use("/health", healthRoute);
1668
+
1669
+ export default router;
1670
+ `;
1671
+ }
1672
+
1673
+ // ---------------------------------------------------------------------------
1674
+ // Commons: db.js generator — database init, connect, and global.db
1675
+ // ---------------------------------------------------------------------------
1676
+
1677
+ /**
1678
+ * Generate commons/db.js — database initialization and connection module.
1679
+ * Sets global.db so the db instance is accessible across the application.
1680
+ * @param {import('./types').InitAnswers} answers
1681
+ * @returns {string}
1682
+ */
1683
+ function generateDbModule(answers) {
1684
+ return `import "dotenv/config";
1685
+ import dbModelRouter from "db-model-router";
1686
+
1687
+ // Initialize database adapter
1688
+ dbModelRouter.init("${answers.database}");
1689
+
1690
+ // Connect to database
1691
+ dbModelRouter.db.connect({
1692
+ ${dbConnectArgs(answers.database)}});
1693
+
1694
+ // Make db available globally across the application
1695
+ const db = dbModelRouter.db;
1696
+ global.db = db;
1697
+
1698
+ export { db };
1699
+ export default db;
1700
+ `;
1701
+ }
1702
+
1703
+ // ---------------------------------------------------------------------------
1704
+ // Updated app.js generator — links commons and route/index
1705
+ // ---------------------------------------------------------------------------
1706
+
1707
+ /**
1708
+ * Generate the app.js file content (v2 — uses commons modules and route/index).
1709
+ * @param {import('./types').InitAnswers} answers
1710
+ * @param {string} [outputDir] - relative output directory for source files (e.g. "backend")
1711
+ * @returns {string}
1712
+ */
1713
+ function generateAppJsV2(answers, outputDir) {
1714
+ const frameworkPkg =
1715
+ answers.framework === "ultimate-express" ? "ultimate-express" : "express";
1716
+
1717
+ const commonsPrefix = outputDir ? `./${outputDir}/commons` : "./commons";
1718
+ const routePrefix = outputDir ? `./${outputDir}/routes` : "./routes";
1719
+ const middlewarePrefix = outputDir
1720
+ ? `./${outputDir}/middleware`
1721
+ : "./middleware";
1722
+
1723
+ return `import express from "${frameworkPkg}";
1724
+ import "${commonsPrefix}/db.js";
1725
+ import configureSession from "${commonsPrefix}/session.js";
1726
+ import applySecurity from "${commonsPrefix}/security.js";
1727
+ import logger from "${middlewarePrefix}/logger.js";
1728
+ import route from "${routePrefix}/index.js";
1729
+
1730
+ const app = express();
1731
+ const PORT = process.env.PORT || 3000;
1732
+
1733
+ // Middleware
1734
+ app.use(express.json());
1735
+ app.use(express.urlencoded({ extended: true }));
1736
+
1737
+ // Security (helmet, rate limiting, custom headers)
1738
+ applySecurity(app);
1739
+
1740
+ // Session
1741
+ app.use(configureSession());
1742
+
1743
+ // Logger
1744
+ app.use(logger);
1745
+
1746
+ // Routes
1747
+ app.use(route);
1748
+
1749
+ // Error handler
1750
+ app.use((err, req, res, next) => {
1751
+ console.error(err.stack);
1752
+ res.status(500).json({ type: "danger", message: "Internal Server Error" });
1753
+ });
1754
+
1755
+ app.listen(PORT, () => {
1756
+ console.log(\`Server running on port \${PORT}\`);
1757
+ });
1758
+
1759
+ export default app;
765
1760
  `;
766
1761
  }
767
1762
 
768
1763
  module.exports = {
769
1764
  migrationTimestamp,
770
1765
  isSql,
1766
+ randomPassword,
771
1767
  generateAppJs,
1768
+ generateAppJsV2,
772
1769
  generateEnvFile,
773
1770
  generateEnvExample,
774
1771
  generateLoggerMiddleware,
@@ -777,6 +1774,18 @@ module.exports = {
777
1774
  generateInitialMigration,
778
1775
  generateSessionMigration,
779
1776
  generateGitignore,
1777
+ generateDockerfile,
1778
+ generateDockerignore,
1779
+ generateGrafanaDatasources,
1780
+ generateDockerCompose,
1781
+ generateCloudBeaverDataSources,
1782
+ generateSessionJs,
1783
+ generateMigrateModule,
1784
+ generateAddMigrationModule,
1785
+ generateSecurityJs,
1786
+ generateHealthRoute,
1787
+ generateRouteIndexFile,
1788
+ generateDbModule,
780
1789
  SQL_DATABASES,
781
1790
  NOSQL_DATABASES,
782
1791
  };