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.
@@ -0,0 +1,782 @@
1
+ "use strict";
2
+
3
+ const SQL_DATABASES = [
4
+ "mysql",
5
+ "postgres",
6
+ "sqlite3",
7
+ "mssql",
8
+ "cockroachdb",
9
+ "oracle",
10
+ ];
11
+ const NOSQL_DATABASES = ["mongodb", "redis", "dynamodb"];
12
+
13
+ /**
14
+ * Format a Date as YYYYMMDDHHMMSS (14-digit string).
15
+ * @param {Date} date
16
+ * @returns {string}
17
+ */
18
+ function migrationTimestamp(date) {
19
+ const y = String(date.getFullYear()).padStart(4, "0");
20
+ const mo = String(date.getMonth() + 1).padStart(2, "0");
21
+ const d = String(date.getDate()).padStart(2, "0");
22
+ const h = String(date.getHours()).padStart(2, "0");
23
+ const mi = String(date.getMinutes()).padStart(2, "0");
24
+ const s = String(date.getSeconds()).padStart(2, "0");
25
+ return `${y}${mo}${d}${h}${mi}${s}`;
26
+ }
27
+
28
+ /**
29
+ * Returns true if the database is a SQL database.
30
+ * @param {string} database
31
+ * @returns {boolean}
32
+ */
33
+ function isSql(database) {
34
+ return SQL_DATABASES.includes(database);
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Environment variable config map (DRY: shared by .env and .env.example)
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * @typedef {Object} EnvVarDef
43
+ * @property {string} key - Variable name
44
+ * @property {string} defaultValue - Value for .env
45
+ * @property {string} placeholder - Value for .env.example
46
+ */
47
+
48
+ /** @type {Record<string, EnvVarDef[]>} */
49
+ const DB_ENV_MAP = {
50
+ mysql: [
51
+ { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
52
+ { key: "DB_PORT", defaultValue: "3306", placeholder: "3306" },
53
+ { key: "DB_NAME", defaultValue: "my_app", placeholder: "your_database" },
54
+ { key: "DB_USER", defaultValue: "root", placeholder: "your_user" },
55
+ { key: "DB_PASS", defaultValue: "password", placeholder: "your_password" },
56
+ ],
57
+ postgres: [
58
+ { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
59
+ { key: "DB_PORT", defaultValue: "5432", placeholder: "5432" },
60
+ { key: "DB_NAME", defaultValue: "my_app", placeholder: "your_database" },
61
+ { key: "DB_USER", defaultValue: "postgres", placeholder: "your_user" },
62
+ { key: "DB_PASS", defaultValue: "password", placeholder: "your_password" },
63
+ ],
64
+ cockroachdb: [
65
+ { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
66
+ { key: "DB_PORT", defaultValue: "26257", placeholder: "26257" },
67
+ { key: "DB_NAME", defaultValue: "my_app", placeholder: "your_database" },
68
+ { key: "DB_USER", defaultValue: "root", placeholder: "your_user" },
69
+ { key: "DB_PASS", defaultValue: "password", placeholder: "your_password" },
70
+ ],
71
+ sqlite3: [
72
+ { key: "DB_NAME", defaultValue: "./data.db", placeholder: "./data.db" },
73
+ ],
74
+ mongodb: [
75
+ { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
76
+ { key: "DB_PORT", defaultValue: "27017", placeholder: "27017" },
77
+ { key: "DB_NAME", defaultValue: "my_app", placeholder: "your_database" },
78
+ { key: "DB_USER", defaultValue: "", placeholder: "your_user" },
79
+ { key: "DB_PASS", defaultValue: "", placeholder: "your_password" },
80
+ ],
81
+ mssql: [
82
+ { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
83
+ { key: "DB_PORT", defaultValue: "1433", placeholder: "1433" },
84
+ { key: "DB_NAME", defaultValue: "my_app", placeholder: "your_database" },
85
+ { key: "DB_USER", defaultValue: "sa", placeholder: "your_user" },
86
+ { key: "DB_PASS", defaultValue: "password", placeholder: "your_password" },
87
+ ],
88
+ oracle: [
89
+ { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
90
+ { key: "DB_PORT", defaultValue: "1521", placeholder: "1521" },
91
+ { key: "DB_NAME", defaultValue: "my_app", placeholder: "your_database" },
92
+ { key: "DB_USER", defaultValue: "system", placeholder: "your_user" },
93
+ { key: "DB_PASS", defaultValue: "password", placeholder: "your_password" },
94
+ ],
95
+ redis: [
96
+ { key: "DB_HOST", defaultValue: "localhost", placeholder: "localhost" },
97
+ { key: "DB_PORT", defaultValue: "6379", placeholder: "6379" },
98
+ { key: "DB_PASS", defaultValue: "", placeholder: "your_password" },
99
+ ],
100
+ dynamodb: [
101
+ { key: "AWS_REGION", defaultValue: "us-east-1", placeholder: "us-east-1" },
102
+ {
103
+ key: "AWS_ENDPOINT",
104
+ defaultValue: "http://localhost:8000",
105
+ placeholder: "http://localhost:8000",
106
+ },
107
+ {
108
+ key: "AWS_ACCESS_KEY_ID",
109
+ defaultValue: "local",
110
+ placeholder: "your_access_key",
111
+ },
112
+ {
113
+ key: "AWS_SECRET_ACCESS_KEY",
114
+ defaultValue: "local",
115
+ placeholder: "your_secret_key",
116
+ },
117
+ ],
118
+ };
119
+
120
+ const REDIS_SESSION_VARS = [
121
+ { key: "REDIS_HOST", defaultValue: "localhost", placeholder: "localhost" },
122
+ { key: "REDIS_PORT", defaultValue: "6379", placeholder: "6379" },
123
+ { key: "REDIS_PASS", defaultValue: "", placeholder: "your_password" },
124
+ ];
125
+
126
+ /**
127
+ * Build env file content from the config map.
128
+ * @param {import('./types').InitAnswers} answers
129
+ * @param {'default'|'placeholder'} mode
130
+ * @returns {string}
131
+ */
132
+ function buildEnvContent(answers, mode) {
133
+ const pick = mode === "placeholder" ? "placeholder" : "defaultValue";
134
+ const lines = [];
135
+ lines.push("# Server");
136
+ lines.push("PORT=3000");
137
+ lines.push("");
138
+ lines.push("# Database");
139
+
140
+ const vars = DB_ENV_MAP[answers.database] || [];
141
+ for (const v of vars) {
142
+ lines.push(`${v.key}=${v[pick]}`);
143
+ }
144
+
145
+ // Session secret
146
+ lines.push("");
147
+ lines.push("# Session");
148
+ lines.push(
149
+ `SESSION_SECRET=${mode === "placeholder" ? "your_session_secret" : "change-me"}`,
150
+ );
151
+
152
+ // Redis session env vars when session is redis and database is not redis
153
+ if (answers.session === "redis" && answers.database !== "redis") {
154
+ lines.push("");
155
+ lines.push("# Redis Session");
156
+ for (const v of REDIS_SESSION_VARS) {
157
+ lines.push(`${v.key}=${v[pick]}`);
158
+ }
159
+ }
160
+
161
+ lines.push("");
162
+ return lines.join("\n");
163
+ }
164
+
165
+ /**
166
+ * Generate .env file content.
167
+ * @param {import('./types').InitAnswers} answers
168
+ * @returns {string}
169
+ */
170
+ function generateEnvFile(answers) {
171
+ return buildEnvContent(answers, "default");
172
+ }
173
+
174
+ /**
175
+ * Generate .env.example file content with placeholder values.
176
+ * @param {import('./types').InitAnswers} answers
177
+ * @returns {string}
178
+ */
179
+ function generateEnvExample(answers) {
180
+ return buildEnvContent(answers, "placeholder");
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // App.js generator (template-literal based)
185
+ // ---------------------------------------------------------------------------
186
+
187
+ /**
188
+ * Generate the db.connect() block for the selected database.
189
+ * @param {string} database
190
+ * @returns {string}
191
+ */
192
+ function dbConnectBlock(database) {
193
+ if (database === "dynamodb") {
194
+ return `db.connect({
195
+ region: process.env.AWS_REGION,
196
+ endpoint: process.env.AWS_ENDPOINT,
197
+ credentials: {
198
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
199
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
200
+ },
201
+ });`;
202
+ }
203
+ if (database === "redis") {
204
+ return `db.connect({
205
+ host: process.env.DB_HOST || "localhost",
206
+ port: process.env.DB_PORT || 6379,
207
+ password: process.env.DB_PASS,
208
+ });`;
209
+ }
210
+ if (database === "sqlite3") {
211
+ return `db.connect({
212
+ database: process.env.DB_NAME || "./data.db",
213
+ });`;
214
+ }
215
+ return `db.connect({
216
+ host: process.env.DB_HOST || "localhost",
217
+ port: process.env.DB_PORT,
218
+ database: process.env.DB_NAME,
219
+ user: process.env.DB_USER,
220
+ password: process.env.DB_PASS,
221
+ });`;
222
+ }
223
+
224
+ /**
225
+ * Generate session middleware block for app.js.
226
+ * @param {import('./types').InitAnswers} answers
227
+ * @returns {string}
228
+ */
229
+ function sessionBlock(answers) {
230
+ if (answers.session === "redis") {
231
+ const redisConfig =
232
+ answers.database === "redis"
233
+ ? ` host: process.env.DB_HOST || "localhost",
234
+ port: process.env.DB_PORT || 6379,
235
+ password: process.env.DB_PASS,`
236
+ : ` host: process.env.REDIS_HOST || "localhost",
237
+ port: process.env.REDIS_PORT || 6379,
238
+ password: process.env.REDIS_PASS,`;
239
+
240
+ return `
241
+ // Session with Redis store
242
+ const redisClient = new Redis({
243
+ ${redisConfig}
244
+ });
245
+ app.use(session({
246
+ store: new RedisStore({ client: redisClient }),
247
+ secret: process.env.SESSION_SECRET || "change-me",
248
+ resave: false,
249
+ saveUninitialized: false,
250
+ }));`;
251
+ }
252
+
253
+ const label = answers.session === "database" ? "database" : "in-memory";
254
+ return `
255
+ // Session with ${label} store
256
+ app.use(session({
257
+ secret: process.env.SESSION_SECRET || "change-me",
258
+ resave: false,
259
+ saveUninitialized: false,
260
+ }));`;
261
+ }
262
+
263
+ /**
264
+ * Generate the app.js file content.
265
+ * @param {import('./types').InitAnswers} answers
266
+ * @returns {string}
267
+ */
268
+ function generateAppJs(answers) {
269
+ const frameworkPkg =
270
+ answers.framework === "ultimate-express" ? "ultimate-express" : "express";
271
+
272
+ // Imports
273
+ let imports = `const express = require("${frameworkPkg}");
274
+ const { init, db } = require("db-model-router");
275
+ const session = require("express-session");`;
276
+
277
+ if (answers.session === "redis") {
278
+ imports += `\nconst RedisStore = require("connect-redis").default;
279
+ const { Redis } = require("ioredis");`;
280
+ }
281
+ if (answers.rateLimiting) {
282
+ imports += `\nconst rateLimit = require("express-rate-limit");`;
283
+ }
284
+ if (answers.helmet) {
285
+ imports += `\nconst helmet = require("helmet");`;
286
+ }
287
+ imports += `\nconst logger = require("./middleware/logger");`;
288
+
289
+ // Rate limiting block
290
+ const rateLimitBlock = answers.rateLimiting
291
+ ? `app.use(rateLimit({
292
+ windowMs: 15 * 60 * 1000,
293
+ max: 100,
294
+ standardHeaders: true,
295
+ legacyHeaders: false,
296
+ }));`
297
+ : "";
298
+
299
+ const helmetBlock = answers.helmet ? `app.use(helmet());` : "";
300
+
301
+ return `${imports}
302
+
303
+ // Load environment variables
304
+ require("dotenv").config();
305
+
306
+ // Initialize database adapter
307
+ init("${answers.database}");
308
+ ${dbConnectBlock(answers.database)}
309
+
310
+ const app = express();
311
+ const PORT = process.env.PORT || 3000;
312
+
313
+ // Middleware
314
+ app.use(express.json());
315
+ app.use(express.urlencoded({ extended: true }));
316
+ ${helmetBlock ? helmetBlock + "\n" : ""}${rateLimitBlock ? rateLimitBlock + "\n" : ""}${sessionBlock(answers)}
317
+ app.use(logger);
318
+
319
+ // Health check
320
+ app.get("/health", (req, res) => {
321
+ res.json({ status: "ok", timestamp: new Date().toISOString() });
322
+ });
323
+
324
+ // Error handler
325
+ app.use((err, req, res, next) => {
326
+ console.error(err.stack);
327
+ res.status(500).json({ type: "danger", message: "Internal Server Error" });
328
+ });
329
+
330
+ app.listen(PORT, () => {
331
+ console.log(\`Server running on port \${PORT}\`);
332
+ });
333
+
334
+ module.exports = app;
335
+ `;
336
+ }
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // Logger middleware generator
340
+ // ---------------------------------------------------------------------------
341
+
342
+ /**
343
+ * Generate middleware/logger.js content.
344
+ * @param {import('./types').InitAnswers} answers
345
+ * @returns {string}
346
+ */
347
+ function generateLoggerMiddleware(answers) {
348
+ if (answers.logger) {
349
+ return `const mung = require("express-mung");
350
+
351
+ /**
352
+ * Request/response logger middleware using express-mung.
353
+ * Logs request details, response body, and response time.
354
+ */
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\`,
361
+ );
362
+ console.log(" Request headers:", JSON.stringify(req.headers));
363
+ console.log(" Response body:", JSON.stringify(body));
364
+ return body;
365
+ });
366
+
367
+ function startTimer(req, res, next) {
368
+ req._startTime = Date.now();
369
+ next();
370
+ }
371
+
372
+ module.exports = [startTimer, logger];
373
+ `;
374
+ }
375
+
376
+ return `/**
377
+ * Simple request logger middleware.
378
+ * Logs method, URL, status code, and response time.
379
+ */
380
+ module.exports = function logger(req, res, next) {
381
+ const start = Date.now();
382
+ const { method, originalUrl } = req;
383
+
384
+ res.on("finish", () => {
385
+ const duration = Date.now() - start;
386
+ const status = res.statusCode;
387
+ const level = status >= 400 ? "WARN" : "INFO";
388
+ console.log(
389
+ \`[\${new Date().toISOString()}] [\${level}] \${method} \${originalUrl} \${status} \${duration}ms\`,
390
+ );
391
+ });
392
+
393
+ next();
394
+ };
395
+ `;
396
+ }
397
+
398
+ // ---------------------------------------------------------------------------
399
+ // Migration script generators (Fix 3: migrate.js now checks tracking table)
400
+ // ---------------------------------------------------------------------------
401
+
402
+ /**
403
+ * Generate migrate.js script content.
404
+ * Checks _migrations tracking table before running each migration.
405
+ * @param {import('./types').InitAnswers} answers
406
+ * @returns {string}
407
+ */
408
+ function generateMigrateScript(answers) {
409
+ const isNoSql = NOSQL_DATABASES.includes(answers.database);
410
+
411
+ if (isNoSql) {
412
+ return `#!/usr/bin/env node
413
+ "use strict";
414
+
415
+ const fs = require("fs");
416
+ const path = require("path");
417
+ const crypto = require("crypto");
418
+ require("dotenv").config();
419
+
420
+ const { init, db } = require("db-model-router");
421
+
422
+ init("${answers.database}");
423
+
424
+ const migrationsDir = path.join(__dirname, "migrations");
425
+
426
+ async function getExecutedMigrations() {
427
+ try {
428
+ const result = await db.get("_migrations");
429
+ return new Set((result || []).map(r => r.filename));
430
+ } catch (e) {
431
+ return new Set();
432
+ }
433
+ }
434
+
435
+ async function recordMigration(filename, checksum) {
436
+ await db.insert("_migrations", {
437
+ filename,
438
+ executed_at: new Date().toISOString(),
439
+ checksum,
440
+ });
441
+ }
442
+
443
+ async function migrate() {
444
+ const files = fs.readdirSync(migrationsDir)
445
+ .filter(f => f.endsWith(".js"))
446
+ .sort();
447
+
448
+ const executed = await getExecutedMigrations();
449
+ let ran = 0;
450
+
451
+ for (const file of files) {
452
+ if (executed.has(file)) {
453
+ console.log(\` Skipping (already executed): \${file}\`);
454
+ continue;
455
+ }
456
+ const filePath = path.join(migrationsDir, file);
457
+ const content = fs.readFileSync(filePath, "utf8");
458
+ const checksum = crypto.createHash("md5").update(content).digest("hex");
459
+
460
+ const migration = require(filePath);
461
+ console.log(\` Running migration: \${file}\`);
462
+ await migration.up(db);
463
+ await recordMigration(file, checksum);
464
+ console.log(\` Completed: \${file}\`);
465
+ ran++;
466
+ }
467
+
468
+ if (ran === 0) {
469
+ console.log("No pending migrations.");
470
+ } else {
471
+ console.log(\`\\n\${ran} migration(s) complete.\`);
472
+ }
473
+ process.exit(0);
474
+ }
475
+
476
+ migrate().catch(err => {
477
+ console.error("Migration failed:", err);
478
+ process.exit(1);
479
+ });
480
+ `;
481
+ }
482
+
483
+ return `#!/usr/bin/env node
484
+ "use strict";
485
+
486
+ const fs = require("fs");
487
+ const path = require("path");
488
+ const crypto = require("crypto");
489
+ require("dotenv").config();
490
+
491
+ const { init, db } = require("db-model-router");
492
+
493
+ init("${answers.database}");
494
+
495
+ const migrationsDir = path.join(__dirname, "migrations");
496
+
497
+ async function getExecutedMigrations() {
498
+ try {
499
+ const result = await db.query("SELECT filename FROM _migrations");
500
+ return new Set((result || []).map(r => r.filename));
501
+ } catch (e) {
502
+ // Table may not exist yet (first run)
503
+ return new Set();
504
+ }
505
+ }
506
+
507
+ async function recordMigration(filename, checksum) {
508
+ await db.query(
509
+ "INSERT INTO _migrations (filename, checksum) VALUES (?, ?)",
510
+ [filename, checksum]
511
+ );
512
+ }
513
+
514
+ async function migrate() {
515
+ const files = fs.readdirSync(migrationsDir)
516
+ .filter(f => f.endsWith(".sql"))
517
+ .sort();
518
+
519
+ const executed = await getExecutedMigrations();
520
+ let ran = 0;
521
+
522
+ for (const file of files) {
523
+ if (executed.has(file)) {
524
+ console.log(\` Skipping (already executed): \${file}\`);
525
+ continue;
526
+ }
527
+ const filePath = path.join(migrationsDir, file);
528
+ const content = fs.readFileSync(filePath, "utf8");
529
+ const checksum = crypto.createHash("md5").update(content).digest("hex");
530
+
531
+ console.log(\` Running migration: \${file}\`);
532
+ await db.query(content);
533
+ await recordMigration(file, checksum);
534
+ console.log(\` Completed: \${file}\`);
535
+ ran++;
536
+ }
537
+
538
+ if (ran === 0) {
539
+ console.log("No pending migrations.");
540
+ } else {
541
+ console.log(\`\\n\${ran} migration(s) complete.\`);
542
+ }
543
+ process.exit(0);
544
+ }
545
+
546
+ migrate().catch(err => {
547
+ console.error("Migration failed:", err);
548
+ process.exit(1);
549
+ });
550
+ `;
551
+ }
552
+
553
+ /**
554
+ * Generate add_migration.js script content.
555
+ * @param {import('./types').InitAnswers} answers
556
+ * @returns {string}
557
+ */
558
+ function generateAddMigrationScript(answers) {
559
+ const isNoSql = NOSQL_DATABASES.includes(answers.database);
560
+ const ext = isNoSql ? "js" : "sql";
561
+ const template = isNoSql
562
+ ? `"use strict";\\n\\nmodule.exports = {\\n async up(db) {\\n // Write your migration here\\n },\\n\\n async down(db) {\\n // Write your rollback here\\n },\\n};\\n`
563
+ : `-- Write your migration SQL here\\n`;
564
+
565
+ return `#!/usr/bin/env node
566
+ "use strict";
567
+
568
+ const fs = require("fs");
569
+ const path = require("path");
570
+
571
+ const migrationsDir = path.join(__dirname, "migrations");
572
+
573
+ function migrationTimestamp() {
574
+ const now = new Date();
575
+ const y = String(now.getFullYear()).padStart(4, "0");
576
+ const mo = String(now.getMonth() + 1).padStart(2, "0");
577
+ const d = String(now.getDate()).padStart(2, "0");
578
+ const h = String(now.getHours()).padStart(2, "0");
579
+ const mi = String(now.getMinutes()).padStart(2, "0");
580
+ const s = String(now.getSeconds()).padStart(2, "0");
581
+ return \`\${y}\${mo}\${d}\${h}\${mi}\${s}\`;
582
+ }
583
+
584
+ const name = process.argv[2] || "migration";
585
+ const filename = \`\${migrationTimestamp()}_\${name}.${ext}\`;
586
+ const filePath = path.join(migrationsDir, filename);
587
+
588
+ if (!fs.existsSync(migrationsDir)) {
589
+ fs.mkdirSync(migrationsDir, { recursive: true });
590
+ }
591
+
592
+ fs.writeFileSync(filePath, "${template}");
593
+ console.log(\`Created migration: \${filename}\`);
594
+ `;
595
+ }
596
+
597
+ // ---------------------------------------------------------------------------
598
+ // Initial migration + session migration generators
599
+ // ---------------------------------------------------------------------------
600
+
601
+ /**
602
+ * Generate the initial migration file that creates the _migrations tracking table.
603
+ * @param {import('./types').InitAnswers} answers
604
+ * @param {Date} [date]
605
+ * @returns {{ filename: string, content: string }}
606
+ */
607
+ function generateInitialMigration(answers, date) {
608
+ const ts = migrationTimestamp(date || new Date());
609
+
610
+ if (isSql(answers.database)) {
611
+ let content;
612
+ if (answers.database === "postgres" || answers.database === "cockroachdb") {
613
+ content = `CREATE TABLE IF NOT EXISTS _migrations (
614
+ id SERIAL PRIMARY KEY,
615
+ filename VARCHAR(255) NOT NULL UNIQUE,
616
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
617
+ checksum VARCHAR(64) NOT NULL
618
+ );
619
+ `;
620
+ } else if (answers.database === "mssql") {
621
+ content = `CREATE TABLE _migrations (
622
+ id INT IDENTITY(1,1) PRIMARY KEY,
623
+ filename VARCHAR(255) NOT NULL UNIQUE,
624
+ executed_at DATETIME DEFAULT GETDATE(),
625
+ checksum VARCHAR(64) NOT NULL
626
+ );
627
+ `;
628
+ } else if (answers.database === "oracle") {
629
+ content = `CREATE TABLE _migrations (
630
+ id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
631
+ filename VARCHAR2(255) NOT NULL UNIQUE,
632
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
633
+ checksum VARCHAR2(64) NOT NULL
634
+ );
635
+ `;
636
+ } else {
637
+ // mysql, sqlite3
638
+ content = `CREATE TABLE IF NOT EXISTS _migrations (
639
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
640
+ filename VARCHAR(255) NOT NULL UNIQUE,
641
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
642
+ checksum VARCHAR(64) NOT NULL
643
+ );
644
+ `;
645
+ }
646
+ return { filename: `${ts}_create_migrations_table.sql`, content };
647
+ }
648
+
649
+ // NoSQL databases
650
+ let content;
651
+ 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
+ },
659
+
660
+ async down(db) {
661
+ await db.collection("_migrations").drop();
662
+ },
663
+ };
664
+ `;
665
+ } 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
+ },
673
+
674
+ async down(db) {
675
+ await db.del("_migrations");
676
+ },
677
+ };
678
+ `;
679
+ } else {
680
+ // 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
+ },
693
+
694
+ async down(db) {
695
+ const { DeleteTableCommand } = require("@aws-sdk/client-dynamodb");
696
+ await db.send(new DeleteTableCommand({ TableName: "_migrations" }));
697
+ },
698
+ };
699
+ `;
700
+ }
701
+
702
+ return { filename: `${ts}_create_migrations_table.js`, content };
703
+ }
704
+
705
+ /**
706
+ * Generate the session migration file for SQL databases with database session store.
707
+ * @param {import('./types').InitAnswers} answers
708
+ * @param {Date} [date]
709
+ * @returns {{ filename: string, content: string } | null}
710
+ */
711
+ function generateSessionMigration(answers, date) {
712
+ if (answers.session !== "database" || !isSql(answers.database)) {
713
+ return null;
714
+ }
715
+
716
+ const ts = migrationTimestamp(date || new Date());
717
+
718
+ let content;
719
+ if (answers.database === "postgres" || answers.database === "cockroachdb") {
720
+ content = `CREATE TABLE IF NOT EXISTS sessions (
721
+ sid VARCHAR(255) PRIMARY KEY,
722
+ sess TEXT NOT NULL,
723
+ expired_at TIMESTAMP NOT NULL
724
+ );
725
+ CREATE INDEX IF NOT EXISTS idx_sessions_expired ON sessions(expired_at);
726
+ `;
727
+ } else if (answers.database === "mssql") {
728
+ content = `CREATE TABLE sessions (
729
+ sid VARCHAR(255) PRIMARY KEY,
730
+ sess TEXT NOT NULL,
731
+ expired_at DATETIME NOT NULL
732
+ );
733
+ CREATE INDEX idx_sessions_expired ON sessions(expired_at);
734
+ `;
735
+ } else if (answers.database === "oracle") {
736
+ content = `CREATE TABLE sessions (
737
+ sid VARCHAR2(255) PRIMARY KEY,
738
+ sess CLOB NOT NULL,
739
+ expired_at TIMESTAMP NOT NULL
740
+ );
741
+ CREATE INDEX idx_sessions_expired ON sessions(expired_at);
742
+ `;
743
+ } else {
744
+ // mysql, sqlite3
745
+ content = `CREATE TABLE IF NOT EXISTS sessions (
746
+ sid VARCHAR(255) PRIMARY KEY,
747
+ sess TEXT NOT NULL,
748
+ expired_at TIMESTAMP NOT NULL
749
+ );
750
+ CREATE INDEX idx_sessions_expired ON sessions(expired_at);
751
+ `;
752
+ }
753
+
754
+ return { filename: `${ts}_create_sessions_table.sql`, content };
755
+ }
756
+
757
+ /**
758
+ * Generate .gitignore content.
759
+ * @returns {string}
760
+ */
761
+ function generateGitignore() {
762
+ return `node_modules/
763
+ .env
764
+ *.db
765
+ `;
766
+ }
767
+
768
+ module.exports = {
769
+ migrationTimestamp,
770
+ isSql,
771
+ generateAppJs,
772
+ generateEnvFile,
773
+ generateEnvExample,
774
+ generateLoggerMiddleware,
775
+ generateMigrateScript,
776
+ generateAddMigrationScript,
777
+ generateInitialMigration,
778
+ generateSessionMigration,
779
+ generateGitignore,
780
+ SQL_DATABASES,
781
+ NOSQL_DATABASES,
782
+ };