db-model-router 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/TODO.md +1 -0
- package/dbmr.schema.json +1 -1
- package/package.json +1 -1
- package/src/cli/commands/generate.js +38 -15
- package/src/cli/commands/help.js +1 -1
- package/src/cli/diff-engine.js +2 -1
- package/src/cli/generate-db-manager.js +1573 -0
- package/src/cli/generate-model.js +9 -4
- package/src/cli/generate-openapi.js +40 -13
- package/src/cli/generate-route.js +7 -7
- package/src/cli/init/generators.js +36 -30
- package/src/cli/main.js +2 -2
- package/src/sqlite3/db.js +11 -0
- package/demo/.dockerignore +0 -7
- package/demo/.env.example +0 -13
- package/demo/Dockerfile +0 -20
- package/demo/app.js +0 -37
- package/demo/commons/add_migration.js +0 -43
- package/demo/commons/db.js +0 -17
- package/demo/commons/migrate.js +0 -65
- package/demo/commons/security.js +0 -30
- package/demo/commons/session.js +0 -13
- package/demo/dbmr.schema.json +0 -362
- package/demo/docs/llm.md +0 -197
- package/demo/llms.txt +0 -70
- package/demo/middleware/logger.js +0 -67
- package/demo/migrations/20260430155808_create_migrations_table.sql +0 -6
- package/demo/migrations/20260430155809_create_tables.sql +0 -207
- package/demo/models/addresses.js +0 -22
- package/demo/models/cart_items.js +0 -18
- package/demo/models/carts.js +0 -16
- package/demo/models/categories.js +0 -20
- package/demo/models/coupons.js +0 -23
- package/demo/models/order_items.js +0 -21
- package/demo/models/orders.js +0 -25
- package/demo/models/payments.js +0 -21
- package/demo/models/product_images.js +0 -18
- package/demo/models/product_reviews.js +0 -20
- package/demo/models/product_variants.js +0 -20
- package/demo/models/products.js +0 -30
- package/demo/models/shipments.js +0 -19
- package/demo/models/users.js +0 -19
- package/demo/models/wishlists.js +0 -15
- package/demo/openapi.json +0 -5872
- package/demo/package-lock.json +0 -2810
- package/demo/package.json +0 -34
- package/demo/routes/addresses.js +0 -6
- package/demo/routes/carts/cart_items.js +0 -7
- package/demo/routes/carts.js +0 -6
- package/demo/routes/categories.js +0 -6
- package/demo/routes/coupons.js +0 -6
- package/demo/routes/docs.js +0 -18
- package/demo/routes/health.js +0 -35
- package/demo/routes/index.js +0 -39
- package/demo/routes/orders/order_items.js +0 -7
- package/demo/routes/orders/payments.js +0 -7
- package/demo/routes/orders/shipments.js +0 -7
- package/demo/routes/orders.js +0 -6
- package/demo/routes/products/product_images.js +0 -7
- package/demo/routes/products/product_reviews.js +0 -7
- package/demo/routes/products/product_variants.js +0 -7
- package/demo/routes/products.js +0 -6
- package/demo/routes/users.js +0 -6
- package/demo/routes/wishlists.js +0 -6
- package/src/cli/commands/generate-llm-docs.js +0 -418
|
@@ -544,7 +544,9 @@ function generateModelFile(m) {
|
|
|
544
544
|
if (opt.modified_at) parts.push(`modified_at: "${opt.modified_at}"`);
|
|
545
545
|
optionStr = `\n { ${parts.join(", ")} },`;
|
|
546
546
|
}
|
|
547
|
-
return `
|
|
547
|
+
return `import dbModelRouter from "db-model-router";
|
|
548
|
+
|
|
549
|
+
const { db, model } = dbModelRouter;
|
|
548
550
|
|
|
549
551
|
const ${varName} = model(
|
|
550
552
|
db,
|
|
@@ -554,7 +556,7 @@ const ${varName} = model(
|
|
|
554
556
|
${uniqueStr},${optionStr}
|
|
555
557
|
);
|
|
556
558
|
|
|
557
|
-
|
|
559
|
+
export default ${varName};
|
|
558
560
|
`;
|
|
559
561
|
}
|
|
560
562
|
|
|
@@ -563,11 +565,14 @@ function generateIndexFile(models) {
|
|
|
563
565
|
let exports = "";
|
|
564
566
|
for (const m of models) {
|
|
565
567
|
const varName = safeVarName(m.table);
|
|
566
|
-
imports += `
|
|
568
|
+
imports += `import ${varName} from "./${m.table}.js";\n`;
|
|
567
569
|
exports += ` ${varName},\n`;
|
|
568
570
|
}
|
|
569
571
|
return `${imports}
|
|
570
|
-
|
|
572
|
+
export {
|
|
573
|
+
${exports}};
|
|
574
|
+
|
|
575
|
+
export default {
|
|
571
576
|
${exports}};
|
|
572
577
|
`;
|
|
573
578
|
}
|
|
@@ -5,6 +5,13 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
5
5
|
const basePath = options.basePath || "/api";
|
|
6
6
|
const title = options.title || "REST Router API";
|
|
7
7
|
const version = options.version || "1.0.0";
|
|
8
|
+
const relationships = options.relationships || [];
|
|
9
|
+
|
|
10
|
+
// Build a lookup: child table -> { parent, foreignKey }
|
|
11
|
+
const childMap = {};
|
|
12
|
+
for (const rel of relationships) {
|
|
13
|
+
childMap[rel.child] = { parent: rel.parent, foreignKey: rel.foreignKey };
|
|
14
|
+
}
|
|
8
15
|
|
|
9
16
|
const spec = {
|
|
10
17
|
openapi: "3.0.3",
|
|
@@ -39,14 +46,34 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
const ref = { $ref: `#/components/schemas/${schemaName}` };
|
|
42
|
-
|
|
49
|
+
|
|
50
|
+
// Determine path prefix: nested under parent if this is a child table
|
|
51
|
+
let prefix;
|
|
52
|
+
const isChild = !!childMap[m.table];
|
|
53
|
+
let fkParam = null;
|
|
54
|
+
if (isChild) {
|
|
55
|
+
const { parent, foreignKey } = childMap[m.table];
|
|
56
|
+
prefix = `${basePath}/${parent}/{${foreignKey}}/${m.table}`;
|
|
57
|
+
fkParam = {
|
|
58
|
+
name: foreignKey,
|
|
59
|
+
in: "path",
|
|
60
|
+
required: true,
|
|
61
|
+
schema: { type: "string" },
|
|
62
|
+
description: `${capitalize(parent)} foreign key`,
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
prefix = `${basePath}/${m.table}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper: prepend FK path param for child routes
|
|
69
|
+
const withFk = (params) => (fkParam ? [fkParam, ...params] : params);
|
|
43
70
|
|
|
44
71
|
// GET / — list
|
|
45
72
|
spec.paths[`${prefix}/`] = {
|
|
46
73
|
get: {
|
|
47
74
|
tags: [tag],
|
|
48
75
|
summary: `List ${m.table}`,
|
|
49
|
-
parameters: [
|
|
76
|
+
parameters: withFk([
|
|
50
77
|
{
|
|
51
78
|
name: "page",
|
|
52
79
|
in: "query",
|
|
@@ -69,7 +96,7 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
69
96
|
in: "query",
|
|
70
97
|
schema: { type: "string", enum: ["json", "csv", "xml"] },
|
|
71
98
|
},
|
|
72
|
-
],
|
|
99
|
+
]),
|
|
73
100
|
responses: {
|
|
74
101
|
200: {
|
|
75
102
|
description: "Success",
|
|
@@ -141,7 +168,7 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
141
168
|
get: {
|
|
142
169
|
tags: [tag],
|
|
143
170
|
summary: `Get ${m.table} by ${pk}`,
|
|
144
|
-
parameters: [
|
|
171
|
+
parameters: withFk([
|
|
145
172
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
146
173
|
{ name: "select_columns", in: "query", schema: { type: "string" } },
|
|
147
174
|
{
|
|
@@ -149,7 +176,7 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
149
176
|
in: "query",
|
|
150
177
|
schema: { type: "string", enum: ["json", "csv", "xml"] },
|
|
151
178
|
},
|
|
152
|
-
],
|
|
179
|
+
]),
|
|
153
180
|
responses: {
|
|
154
181
|
200: {
|
|
155
182
|
description: "Success",
|
|
@@ -161,9 +188,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
161
188
|
post: {
|
|
162
189
|
tags: [tag],
|
|
163
190
|
summary: `Insert a ${m.table}`,
|
|
164
|
-
parameters: [
|
|
191
|
+
parameters: withFk([
|
|
165
192
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
166
|
-
],
|
|
193
|
+
]),
|
|
167
194
|
requestBody: { content: { "application/json": { schema: ref } } },
|
|
168
195
|
responses: {
|
|
169
196
|
200: {
|
|
@@ -175,9 +202,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
175
202
|
put: {
|
|
176
203
|
tags: [tag],
|
|
177
204
|
summary: `Update a ${m.table}`,
|
|
178
|
-
parameters: [
|
|
205
|
+
parameters: withFk([
|
|
179
206
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
180
|
-
],
|
|
207
|
+
]),
|
|
181
208
|
requestBody: { content: { "application/json": { schema: ref } } },
|
|
182
209
|
responses: {
|
|
183
210
|
200: {
|
|
@@ -190,9 +217,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
190
217
|
patch: {
|
|
191
218
|
tags: [tag],
|
|
192
219
|
summary: `Partial update a ${m.table}`,
|
|
193
|
-
parameters: [
|
|
220
|
+
parameters: withFk([
|
|
194
221
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
195
|
-
],
|
|
222
|
+
]),
|
|
196
223
|
requestBody: {
|
|
197
224
|
content: { "application/json": { schema: { type: "object" } } },
|
|
198
225
|
},
|
|
@@ -207,9 +234,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
207
234
|
delete: {
|
|
208
235
|
tags: [tag],
|
|
209
236
|
summary: `Delete a ${m.table}`,
|
|
210
|
-
parameters: [
|
|
237
|
+
parameters: withFk([
|
|
211
238
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
212
|
-
],
|
|
239
|
+
]),
|
|
213
240
|
responses: {
|
|
214
241
|
200: { description: "Deleted" },
|
|
215
242
|
404: { description: "Not Found" },
|
|
@@ -100,6 +100,12 @@ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
|
|
|
100
100
|
imports += `router.use("/docs", docsRoute);\n`;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// Mount child routes BEFORE parent routes to prevent path clashing
|
|
104
|
+
for (const rel of relationships) {
|
|
105
|
+
const childVar = safeVarName(rel.child);
|
|
106
|
+
imports += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
103
109
|
// Mount top-level routes
|
|
104
110
|
for (const table of tableNames) {
|
|
105
111
|
if (nestedChildren.has(table)) continue;
|
|
@@ -107,12 +113,6 @@ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
|
|
|
107
113
|
imports += `router.use("/${table}", ${varName}Route);\n`;
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
// Mount child routes under parent path
|
|
111
|
-
for (const rel of relationships) {
|
|
112
|
-
const childVar = safeVarName(rel.child);
|
|
113
|
-
imports += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
116
|
imports += "\nexport default router;\n";
|
|
117
117
|
return imports;
|
|
118
118
|
}
|
|
@@ -472,7 +472,7 @@ async function main() {
|
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
if (modelMeta.length > 0) {
|
|
475
|
-
const spec = generateOpenAPISpec(modelMeta);
|
|
475
|
+
const spec = generateOpenAPISpec(modelMeta, { relationships });
|
|
476
476
|
const specPath = path.join(routesDir, "openapi.json");
|
|
477
477
|
fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
|
|
478
478
|
console.log(` Created ${specPath}`);
|
|
@@ -367,21 +367,21 @@ function generateAppJs(answers) {
|
|
|
367
367
|
answers.framework === "ultimate-express" ? "ultimate-express" : "express";
|
|
368
368
|
|
|
369
369
|
// Imports
|
|
370
|
-
let imports = `
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
let imports = `import express from "${frameworkPkg}";
|
|
371
|
+
import { init, db } from "db-model-router";
|
|
372
|
+
import session from "express-session";`;
|
|
373
373
|
|
|
374
374
|
if (answers.session === "redis") {
|
|
375
|
-
imports += `\
|
|
376
|
-
|
|
375
|
+
imports += `\nimport RedisStore from "connect-redis";
|
|
376
|
+
import { Redis } from "ioredis";`;
|
|
377
377
|
}
|
|
378
378
|
if (answers.rateLimiting) {
|
|
379
|
-
imports += `\
|
|
379
|
+
imports += `\nimport rateLimit from "express-rate-limit";`;
|
|
380
380
|
}
|
|
381
381
|
if (answers.helmet) {
|
|
382
|
-
imports += `\
|
|
382
|
+
imports += `\nimport helmet from "helmet";`;
|
|
383
383
|
}
|
|
384
|
-
imports += `\
|
|
384
|
+
imports += `\nimport logger from "./middleware/logger.js";`;
|
|
385
385
|
|
|
386
386
|
// Rate limiting block
|
|
387
387
|
const rateLimitBlock = answers.rateLimiting
|
|
@@ -396,9 +396,7 @@ const { Redis } = require("ioredis");`;
|
|
|
396
396
|
const helmetBlock = answers.helmet ? `app.use(helmet());` : "";
|
|
397
397
|
|
|
398
398
|
return `${imports}
|
|
399
|
-
|
|
400
|
-
// Load environment variables
|
|
401
|
-
require("dotenv").config();
|
|
399
|
+
import "dotenv/config";
|
|
402
400
|
|
|
403
401
|
// Initialize database adapter
|
|
404
402
|
init("${answers.database}");
|
|
@@ -428,7 +426,7 @@ app.listen(PORT, () => {
|
|
|
428
426
|
console.log(\`Server running on port \${PORT}\`);
|
|
429
427
|
});
|
|
430
428
|
|
|
431
|
-
|
|
429
|
+
export default app;
|
|
432
430
|
`;
|
|
433
431
|
}
|
|
434
432
|
|
|
@@ -551,14 +549,15 @@ function generateMigrateScript(answers) {
|
|
|
551
549
|
|
|
552
550
|
if (isNoSql) {
|
|
553
551
|
return `#!/usr/bin/env node
|
|
554
|
-
|
|
552
|
+
import fs from "fs";
|
|
553
|
+
import path from "path";
|
|
554
|
+
import crypto from "crypto";
|
|
555
|
+
import { fileURLToPath } from "url";
|
|
556
|
+
import "dotenv/config";
|
|
555
557
|
|
|
556
|
-
|
|
557
|
-
const path = require("path");
|
|
558
|
-
const crypto = require("crypto");
|
|
559
|
-
require("dotenv").config();
|
|
558
|
+
import { init, db } from "db-model-router";
|
|
560
559
|
|
|
561
|
-
const
|
|
560
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
562
561
|
|
|
563
562
|
init("${answers.database}");
|
|
564
563
|
|
|
@@ -598,7 +597,7 @@ async function migrate() {
|
|
|
598
597
|
const content = fs.readFileSync(filePath, "utf8");
|
|
599
598
|
const checksum = crypto.createHash("md5").update(content).digest("hex");
|
|
600
599
|
|
|
601
|
-
const migration =
|
|
600
|
+
const migration = await import(filePath);
|
|
602
601
|
console.log(\` Running migration: \${file}\`);
|
|
603
602
|
await migration.up(db);
|
|
604
603
|
await recordMigration(file, checksum);
|
|
@@ -622,14 +621,15 @@ migrate().catch(err => {
|
|
|
622
621
|
}
|
|
623
622
|
|
|
624
623
|
return `#!/usr/bin/env node
|
|
625
|
-
|
|
624
|
+
import fs from "fs";
|
|
625
|
+
import path from "path";
|
|
626
|
+
import crypto from "crypto";
|
|
627
|
+
import { fileURLToPath } from "url";
|
|
628
|
+
import "dotenv/config";
|
|
626
629
|
|
|
627
|
-
|
|
628
|
-
const path = require("path");
|
|
629
|
-
const crypto = require("crypto");
|
|
630
|
-
require("dotenv").config();
|
|
630
|
+
import { init, db } from "db-model-router";
|
|
631
631
|
|
|
632
|
-
const
|
|
632
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
633
633
|
|
|
634
634
|
init("${answers.database}");
|
|
635
635
|
|
|
@@ -700,14 +700,15 @@ function generateAddMigrationScript(answers) {
|
|
|
700
700
|
const isNoSql = NOSQL_DATABASES.includes(answers.database);
|
|
701
701
|
const ext = isNoSql ? "js" : "sql";
|
|
702
702
|
const template = isNoSql
|
|
703
|
-
? `
|
|
703
|
+
? `export async function up(db) {\\n // Write your migration here\\n}\\n\\nexport async function down(db) {\\n // Write your rollback here\\n}\\n`
|
|
704
704
|
: `-- Write your migration SQL here\\n`;
|
|
705
705
|
|
|
706
706
|
return `#!/usr/bin/env node
|
|
707
|
-
|
|
707
|
+
import fs from "fs";
|
|
708
|
+
import path from "path";
|
|
709
|
+
import { fileURLToPath } from "url";
|
|
708
710
|
|
|
709
|
-
const
|
|
710
|
-
const path = require("path");
|
|
711
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
711
712
|
|
|
712
713
|
const migrationsDir = path.join(__dirname, "migrations");
|
|
713
714
|
|
|
@@ -1422,6 +1423,8 @@ if (isMain) {
|
|
|
1422
1423
|
const pkg = await import("db-model-router");
|
|
1423
1424
|
const mod = pkg.default || pkg;
|
|
1424
1425
|
mod.init("${answers.database}");
|
|
1426
|
+
mod.db.connect({
|
|
1427
|
+
${dbConnectArgs(answers.database)}});
|
|
1425
1428
|
const migrationsDir = path.join(__dirname, "${migrationsRel}");
|
|
1426
1429
|
runMigrations(mod.db, migrationsDir)
|
|
1427
1430
|
.then(() => process.exit(0))
|
|
@@ -1490,6 +1493,8 @@ if (isMain) {
|
|
|
1490
1493
|
const pkg = await import("db-model-router");
|
|
1491
1494
|
const mod = pkg.default || pkg;
|
|
1492
1495
|
mod.init("${answers.database}");
|
|
1496
|
+
mod.db.connect({
|
|
1497
|
+
${dbConnectArgs(answers.database)}});
|
|
1493
1498
|
const migrationsDir = path.join(__dirname, "${migrationsRel}");
|
|
1494
1499
|
runMigrations(mod.db, migrationsDir)
|
|
1495
1500
|
.then(() => process.exit(0))
|
|
@@ -1726,7 +1731,8 @@ import configureSession from "${commonsPrefix}/session.js";
|
|
|
1726
1731
|
import applySecurity from "${commonsPrefix}/security.js";
|
|
1727
1732
|
import logger from "${middlewarePrefix}/logger.js";
|
|
1728
1733
|
import route from "${routePrefix}/index.js";
|
|
1729
|
-
|
|
1734
|
+
import { fileURLToPath } from 'node:url';
|
|
1735
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1730
1736
|
const app = express();
|
|
1731
1737
|
const PORT = process.env.PORT || 3000;
|
|
1732
1738
|
|
package/src/cli/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { parseFlags, OutputContext } = require("./flags");
|
|
@@ -64,7 +64,7 @@ const COMMAND_FLAGS = {
|
|
|
64
64
|
["--routes", "Generate only route files"],
|
|
65
65
|
["--openapi", "Generate only OpenAPI spec"],
|
|
66
66
|
["--tests", "Generate only test files"],
|
|
67
|
-
["--
|
|
67
|
+
["--db-manager", "Generate DB Manager UI (SQL adapters only)"],
|
|
68
68
|
],
|
|
69
69
|
doctor: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
|
|
70
70
|
diff: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
|
package/src/sqlite3/db.js
CHANGED
|
@@ -16,6 +16,17 @@ function connect(config) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function query(sql, parameter = []) {
|
|
19
|
+
// Use exec() for multi-statement SQL (e.g., migration files)
|
|
20
|
+
if (
|
|
21
|
+
parameter.length === 0 &&
|
|
22
|
+
sql
|
|
23
|
+
.replace(/--[^\n]*/g, "")
|
|
24
|
+
.split(";")
|
|
25
|
+
.filter((s) => s.trim()).length > 1
|
|
26
|
+
) {
|
|
27
|
+
db.exec(sql);
|
|
28
|
+
return { changes: 0 };
|
|
29
|
+
}
|
|
19
30
|
const stmt = db.prepare(sql);
|
|
20
31
|
if (sql.trimStart().match(/^(SELECT|PRAGMA|WITH\s)/i)) {
|
|
21
32
|
return stmt.all(...parameter);
|
package/demo/.dockerignore
DELETED
package/demo/.env.example
DELETED
package/demo/Dockerfile
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
FROM node:alpine
|
|
2
|
-
|
|
3
|
-
WORKDIR /app
|
|
4
|
-
|
|
5
|
-
# Install dependencies
|
|
6
|
-
COPY package*.json ./
|
|
7
|
-
RUN npm ci --omit=dev
|
|
8
|
-
|
|
9
|
-
# Copy application files
|
|
10
|
-
COPY app.js ./
|
|
11
|
-
COPY commons/ ./commons/
|
|
12
|
-
COPY middleware/ ./middleware/
|
|
13
|
-
COPY route/ ./route/
|
|
14
|
-
COPY migrations/ ./migrations/
|
|
15
|
-
|
|
16
|
-
# Expose port
|
|
17
|
-
EXPOSE 3000
|
|
18
|
-
|
|
19
|
-
# Start the application
|
|
20
|
-
CMD ["node", "app.js"]
|
package/demo/app.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import "./commons/db.js";
|
|
3
|
-
import configureSession from "./commons/session.js";
|
|
4
|
-
import applySecurity from "./commons/security.js";
|
|
5
|
-
import logger from "./middleware/logger.js";
|
|
6
|
-
import route from "./routes/index.js";
|
|
7
|
-
|
|
8
|
-
const app = express();
|
|
9
|
-
const PORT = process.env.PORT || 3000;
|
|
10
|
-
|
|
11
|
-
// Middleware
|
|
12
|
-
app.use(express.json());
|
|
13
|
-
app.use(express.urlencoded({ extended: true }));
|
|
14
|
-
|
|
15
|
-
// Security (helmet, rate limiting, custom headers)
|
|
16
|
-
applySecurity(app);
|
|
17
|
-
|
|
18
|
-
// Session
|
|
19
|
-
app.use(configureSession());
|
|
20
|
-
|
|
21
|
-
// Logger
|
|
22
|
-
app.use(logger);
|
|
23
|
-
|
|
24
|
-
// Routes
|
|
25
|
-
app.use(route);
|
|
26
|
-
|
|
27
|
-
// Error handler
|
|
28
|
-
app.use((err, req, res, next) => {
|
|
29
|
-
console.error(err.stack);
|
|
30
|
-
res.status(500).json({ type: "danger", message: "Internal Server Error" });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
app.listen(PORT, () => {
|
|
34
|
-
console.log(`Server running on port ${PORT}`);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
export default app;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Create a new timestamped migration file.
|
|
10
|
-
* @param {string} migrationsDir - absolute path to migrations folder
|
|
11
|
-
* @param {string} [name] - migration name (default: "migration")
|
|
12
|
-
* @returns {string} the created filename
|
|
13
|
-
*/
|
|
14
|
-
export default function addMigration(migrationsDir, name) {
|
|
15
|
-
const migrationName = name || "migration";
|
|
16
|
-
const now = new Date();
|
|
17
|
-
const y = String(now.getFullYear()).padStart(4, "0");
|
|
18
|
-
const mo = String(now.getMonth() + 1).padStart(2, "0");
|
|
19
|
-
const d = String(now.getDate()).padStart(2, "0");
|
|
20
|
-
const h = String(now.getHours()).padStart(2, "0");
|
|
21
|
-
const mi = String(now.getMinutes()).padStart(2, "0");
|
|
22
|
-
const s = String(now.getSeconds()).padStart(2, "0");
|
|
23
|
-
const ts = `${y}${mo}${d}${h}${mi}${s}`;
|
|
24
|
-
|
|
25
|
-
const filename = `${ts}_${migrationName}.sql`;
|
|
26
|
-
const filePath = path.join(migrationsDir, filename);
|
|
27
|
-
|
|
28
|
-
if (!fs.existsSync(migrationsDir)) {
|
|
29
|
-
fs.mkdirSync(migrationsDir, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
fs.writeFileSync(filePath, "-- Write your migration SQL here\n");
|
|
33
|
-
console.log(`Created migration: ${filename}`);
|
|
34
|
-
return filename;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Run as standalone script
|
|
38
|
-
const isMain = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
39
|
-
if (isMain) {
|
|
40
|
-
const migrationsDir = path.join(__dirname, "../migrations");
|
|
41
|
-
const name = process.argv[2] || "migration";
|
|
42
|
-
addMigration(migrationsDir, name);
|
|
43
|
-
}
|
package/demo/commons/db.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import "dotenv/config";
|
|
2
|
-
import dbModelRouter from "db-model-router";
|
|
3
|
-
|
|
4
|
-
// Initialize database adapter
|
|
5
|
-
dbModelRouter.init("sqlite3");
|
|
6
|
-
|
|
7
|
-
// Connect to database
|
|
8
|
-
dbModelRouter.db.connect({
|
|
9
|
-
database: process.env.DB_NAME || "./data/data.db",
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// Make db available globally across the application
|
|
13
|
-
const db = dbModelRouter.db;
|
|
14
|
-
global.db = db;
|
|
15
|
-
|
|
16
|
-
export { db };
|
|
17
|
-
export default db;
|
package/demo/commons/migrate.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import crypto from "crypto";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Run all pending SQL migrations from the migrations directory.
|
|
11
|
-
* @param {object} db - db-model-router db instance
|
|
12
|
-
* @param {string} migrationsDir - absolute path to migrations folder
|
|
13
|
-
*/
|
|
14
|
-
export default async function runMigrations(db, migrationsDir) {
|
|
15
|
-
const files = fs.readdirSync(migrationsDir)
|
|
16
|
-
.filter(f => f.endsWith(".sql"))
|
|
17
|
-
.sort();
|
|
18
|
-
|
|
19
|
-
let executed;
|
|
20
|
-
try {
|
|
21
|
-
const result = await db.query("SELECT filename FROM _migrations");
|
|
22
|
-
executed = new Set((result || []).map(r => r.filename));
|
|
23
|
-
} catch (e) {
|
|
24
|
-
executed = new Set();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let ran = 0;
|
|
28
|
-
for (const file of files) {
|
|
29
|
-
if (executed.has(file)) {
|
|
30
|
-
console.log(` Skipping (already executed): ${file}`);
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
const filePath = path.join(migrationsDir, file);
|
|
34
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
35
|
-
const checksum = crypto.createHash("md5").update(content).digest("hex");
|
|
36
|
-
|
|
37
|
-
console.log(` Running migration: ${file}`);
|
|
38
|
-
await db.query(content);
|
|
39
|
-
await db.query(
|
|
40
|
-
"INSERT INTO _migrations (filename, checksum) VALUES (?, ?)",
|
|
41
|
-
[file, checksum]
|
|
42
|
-
);
|
|
43
|
-
console.log(` Completed: ${file}`);
|
|
44
|
-
ran++;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (ran === 0) {
|
|
48
|
-
console.log("No pending migrations.");
|
|
49
|
-
} else {
|
|
50
|
-
console.log(`\n${ran} migration(s) complete.`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Run as standalone script
|
|
55
|
-
const isMain = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
56
|
-
if (isMain) {
|
|
57
|
-
await import("dotenv/config");
|
|
58
|
-
const pkg = await import("db-model-router");
|
|
59
|
-
const mod = pkg.default || pkg;
|
|
60
|
-
mod.init("sqlite3");
|
|
61
|
-
const migrationsDir = path.join(__dirname, "../migrations");
|
|
62
|
-
runMigrations(mod.db, migrationsDir)
|
|
63
|
-
.then(() => process.exit(0))
|
|
64
|
-
.catch(err => { console.error("Migration failed:", err); process.exit(1); });
|
|
65
|
-
}
|
package/demo/commons/security.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import helmet from "helmet";
|
|
2
|
-
import rateLimit from "express-rate-limit";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Apply security middleware to the Express app.
|
|
6
|
-
* Includes: Helmet, rate limiting, custom security headers.
|
|
7
|
-
* @param {import("express").Application} app
|
|
8
|
-
*/
|
|
9
|
-
export default function applySecurity(app) {
|
|
10
|
-
// Helmet — sets various HTTP headers for security
|
|
11
|
-
app.use(helmet());
|
|
12
|
-
|
|
13
|
-
// Rate limiting
|
|
14
|
-
app.use(rateLimit({
|
|
15
|
-
windowMs: 15 * 60 * 1000,
|
|
16
|
-
max: 100,
|
|
17
|
-
standardHeaders: true,
|
|
18
|
-
legacyHeaders: false,
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
// Custom security headers (override or extend as needed)
|
|
22
|
-
app.use((req, res, next) => {
|
|
23
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
24
|
-
res.setHeader("X-Frame-Options", "DENY");
|
|
25
|
-
res.setHeader("X-XSS-Protection", "1; mode=block");
|
|
26
|
-
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
27
|
-
res.removeHeader("X-Powered-By");
|
|
28
|
-
next();
|
|
29
|
-
});
|
|
30
|
-
}
|
package/demo/commons/session.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import session from "express-session";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Configure and return session middleware.
|
|
5
|
-
* Session store: memory
|
|
6
|
-
*/
|
|
7
|
-
export default function configureSession() {
|
|
8
|
-
return session({
|
|
9
|
-
secret: process.env.SESSION_SECRET || "change-me",
|
|
10
|
-
resave: false,
|
|
11
|
-
saveUninitialized: false,
|
|
12
|
-
});
|
|
13
|
-
}
|