db-model-router 1.0.6 → 1.0.8
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/README.md +150 -11
- package/TODO.md +0 -15
- package/db-manager/.dbmanager.sqlite +0 -0
- package/db-manager/README.md +223 -0
- package/db-manager/adapter-proxy.js +361 -0
- package/db-manager/demo/cockroachdb.env +6 -0
- package/db-manager/demo/demo.sqlite +0 -0
- package/db-manager/demo/dynamodb.env +7 -0
- package/db-manager/demo/mongodb.env +4 -0
- package/db-manager/demo/mssql.env +6 -0
- package/db-manager/demo/mysql.env +6 -0
- package/db-manager/demo/oracle.env +6 -0
- package/db-manager/demo/postgres.env +6 -0
- package/db-manager/demo/redis.env +4 -0
- package/db-manager/demo/seeds/cockroachdb.sql +32 -0
- package/db-manager/demo/seeds/mssql.sql +32 -0
- package/db-manager/demo/seeds/mysql.sql +32 -0
- package/db-manager/demo/seeds/oracle.sql +43 -0
- package/db-manager/demo/seeds/postgres.sql +32 -0
- package/db-manager/demo/seeds/sqlite3.sql +32 -0
- package/db-manager/demo/sqlite3.env +2 -0
- package/db-manager/metadata-db.js +170 -0
- package/db-manager/public/.gitkeep +1 -0
- package/db-manager/public/css/style.css +1413 -0
- package/db-manager/public/js/app.js +1370 -0
- package/db-manager/routes/api.js +388 -0
- package/db-manager/routes/views.js +61 -0
- package/db-manager/server.js +39 -0
- package/db-manager/utils/build-filter-config.js +18 -0
- package/db-manager/utils/csv-export.js +59 -0
- package/db-manager/utils/export-filename.js +39 -0
- package/db-manager/utils/filter-tables.js +20 -0
- package/db-manager/utils/parse-filters.js +93 -0
- package/db-manager/utils/sort-state.js +35 -0
- package/db-manager/views/.gitkeep +1 -0
- package/db-manager/views/dashboard.ejs +53 -0
- package/db-manager/views/history.ejs +52 -0
- package/db-manager/views/index.ejs +35 -0
- package/db-manager/views/layout.ejs +31 -0
- package/db-manager/views/partials/data-panel.ejs +74 -0
- package/db-manager/views/partials/header.ejs +36 -0
- package/db-manager/views/partials/sidebar.ejs +30 -0
- package/db-manager/views/query.ejs +58 -0
- package/dbmr.schema.json +22 -44
- package/demo/.dockerignore +7 -0
- package/demo/.env.example +15 -0
- package/demo/Dockerfile +20 -0
- package/demo/app.js +39 -0
- package/demo/commons/add_migration.js +43 -0
- package/demo/commons/db.js +17 -0
- package/demo/commons/migrate.js +68 -0
- package/demo/commons/modules.js +18 -0
- package/demo/commons/password.js +36 -0
- package/demo/commons/security.js +30 -0
- package/demo/commons/session.js +13 -0
- package/demo/commons/webhook.js +81 -0
- package/demo/dbmr.schema.json +338 -0
- package/demo/middleware/authenticate.js +14 -0
- package/demo/middleware/hasPermission.js +30 -0
- package/demo/middleware/logger.js +67 -0
- package/demo/middleware/tenantIsolation.js +19 -0
- package/demo/migrations/20260510092158_create_migrations_table.sql +6 -0
- package/demo/migrations/20260510092159_create_saas_tables.sql +69 -0
- package/demo/migrations/20260510092159_create_tables.sql +193 -0
- package/demo/models/addresses.js +24 -0
- package/demo/models/cart_items.js +20 -0
- package/demo/models/carts.js +18 -0
- package/demo/models/categories.js +22 -0
- package/demo/models/coupons.js +25 -0
- package/demo/models/index.js +43 -0
- package/demo/models/order_items.js +23 -0
- package/demo/models/orders.js +27 -0
- package/demo/models/payments.js +23 -0
- package/demo/models/product_images.js +20 -0
- package/demo/models/product_reviews.js +22 -0
- package/demo/models/product_variants.js +22 -0
- package/demo/models/products.js +32 -0
- package/demo/models/role_permissions.js +17 -0
- package/demo/models/roles.js +17 -0
- package/demo/models/shipments.js +21 -0
- package/demo/models/tenants.js +18 -0
- package/demo/models/users.js +23 -0
- package/demo/models/webhook_logs.js +22 -0
- package/demo/models/webhooks.js +19 -0
- package/demo/models/wishlists.js +17 -0
- package/demo/openapi.json +7000 -0
- package/demo/package-lock.json +2827 -0
- package/demo/package.json +42 -0
- package/demo/routes/addresses/index.js +10 -0
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/cart_items/index.js +11 -0
- package/demo/routes/carts/index.js +14 -0
- package/demo/routes/categories/index.js +10 -0
- package/demo/routes/coupons/index.js +10 -0
- package/demo/routes/docs.js +18 -0
- package/demo/routes/health.js +35 -0
- package/demo/routes/index.js +54 -0
- package/demo/routes/orders/index.js +18 -0
- package/demo/routes/orders/order_items/index.js +11 -0
- package/demo/routes/orders/payments/index.js +11 -0
- package/demo/routes/orders/shipments/index.js +11 -0
- package/demo/routes/products/index.js +18 -0
- package/demo/routes/products/product_images/index.js +11 -0
- package/demo/routes/products/product_reviews/index.js +11 -0
- package/demo/routes/products/product_variants/index.js +11 -0
- package/demo/routes/roles/index.js +75 -0
- package/demo/routes/roles/permissions/index.js +47 -0
- package/demo/routes/tenants/index.js +45 -0
- package/demo/routes/users/index.js +45 -0
- package/demo/routes/wishlists/index.js +10 -0
- package/demo/seeds/saas-seed.js +329 -0
- package/docker-compose.yml +61 -0
- package/package.json +120 -113
- package/scripts/demo-create.js +1 -1
- package/skill/SKILL.md +119 -3
- package/src/cli/commands/db-manager.js +134 -0
- package/src/cli/commands/generate.js +106 -60
- package/src/cli/commands/help.js +0 -1
- package/src/cli/generate-route.js +66 -27
- package/src/cli/generate-saas-structure.js +129 -0
- package/src/cli/init/dependencies.js +1 -1
- package/src/cli/init/generators.js +6 -77
- package/src/cli/init.js +9 -2
- package/src/cli/main.js +8 -1
- package/src/cli/saas/generate-saas-middleware.js +110 -0
- package/src/cli/saas/generate-saas-migrations.js +480 -0
- package/src/cli/saas/generate-saas-models.js +211 -0
- package/src/cli/saas/generate-saas-openapi.js +419 -0
- package/src/cli/saas/generate-saas-routes.js +435 -0
- package/src/cli/saas/generate-saas-seeds.js +243 -0
- package/src/cli/saas/generate-saas-tests.js +473 -0
- package/src/cli/saas/generate-saas-utils.js +176 -0
- package/src/commons/kafka.js +139 -0
- package/src/commons/model.js +29 -9
- package/src/commons/route.js +6 -6
- package/src/index.js +2 -0
- package/src/mssql/db.js +41 -3
- package/src/mysql/db.js +3 -0
- package/src/postgres/db.js +6 -0
- package/src/cli/generate-db-manager.js +0 -1573
package/dbmr.schema.json
CHANGED
|
@@ -9,28 +9,6 @@
|
|
|
9
9
|
"loki": false
|
|
10
10
|
},
|
|
11
11
|
"tables": {
|
|
12
|
-
"users": {
|
|
13
|
-
"columns": {
|
|
14
|
-
"user_id": "auto_increment",
|
|
15
|
-
"name": "required|string",
|
|
16
|
-
"email": "required|string",
|
|
17
|
-
"password_hash": "required|string",
|
|
18
|
-
"phone": "string",
|
|
19
|
-
"avatar_url": "string",
|
|
20
|
-
"role": "required|string",
|
|
21
|
-
"is_deleted": "boolean",
|
|
22
|
-
"created_at": "datetime",
|
|
23
|
-
"updated_at": "datetime"
|
|
24
|
-
},
|
|
25
|
-
"pk": "user_id",
|
|
26
|
-
"unique": ["email"],
|
|
27
|
-
"softDelete": "is_deleted",
|
|
28
|
-
"timestamps": {
|
|
29
|
-
"created_at": "created_at",
|
|
30
|
-
"modified_at": "updated_at"
|
|
31
|
-
},
|
|
32
|
-
"parent": null
|
|
33
|
-
},
|
|
34
12
|
"addresses": {
|
|
35
13
|
"columns": {
|
|
36
14
|
"address_id": "auto_increment",
|
|
@@ -44,13 +22,13 @@
|
|
|
44
22
|
"country": "required|string",
|
|
45
23
|
"is_default": "boolean",
|
|
46
24
|
"created_at": "datetime",
|
|
47
|
-
"
|
|
25
|
+
"modified_at": "datetime"
|
|
48
26
|
},
|
|
49
27
|
"pk": "address_id",
|
|
50
28
|
"unique": ["address_id"],
|
|
51
29
|
"timestamps": {
|
|
52
30
|
"created_at": "created_at",
|
|
53
|
-
"modified_at": "
|
|
31
|
+
"modified_at": "modified_at"
|
|
54
32
|
},
|
|
55
33
|
"parent": null
|
|
56
34
|
},
|
|
@@ -65,13 +43,13 @@
|
|
|
65
43
|
"sort_order": "integer",
|
|
66
44
|
"is_active": "boolean",
|
|
67
45
|
"created_at": "datetime",
|
|
68
|
-
"
|
|
46
|
+
"modified_at": "datetime"
|
|
69
47
|
},
|
|
70
48
|
"pk": "category_id",
|
|
71
49
|
"unique": ["slug"],
|
|
72
50
|
"timestamps": {
|
|
73
51
|
"created_at": "created_at",
|
|
74
|
-
"modified_at": "
|
|
52
|
+
"modified_at": "modified_at"
|
|
75
53
|
},
|
|
76
54
|
"parent": null
|
|
77
55
|
},
|
|
@@ -97,14 +75,14 @@
|
|
|
97
75
|
"is_deleted": "boolean",
|
|
98
76
|
"meta": "object",
|
|
99
77
|
"created_at": "datetime",
|
|
100
|
-
"
|
|
78
|
+
"modified_at": "datetime"
|
|
101
79
|
},
|
|
102
80
|
"pk": "product_id",
|
|
103
81
|
"unique": ["sku", "slug"],
|
|
104
82
|
"softDelete": "is_deleted",
|
|
105
83
|
"timestamps": {
|
|
106
84
|
"created_at": "created_at",
|
|
107
|
-
"modified_at": "
|
|
85
|
+
"modified_at": "modified_at"
|
|
108
86
|
},
|
|
109
87
|
"parent": null
|
|
110
88
|
},
|
|
@@ -136,13 +114,13 @@
|
|
|
136
114
|
"attributes": "object",
|
|
137
115
|
"is_active": "boolean",
|
|
138
116
|
"created_at": "datetime",
|
|
139
|
-
"
|
|
117
|
+
"modified_at": "datetime"
|
|
140
118
|
},
|
|
141
119
|
"pk": "variant_id",
|
|
142
120
|
"unique": ["sku"],
|
|
143
121
|
"timestamps": {
|
|
144
122
|
"created_at": "created_at",
|
|
145
|
-
"modified_at": "
|
|
123
|
+
"modified_at": "modified_at"
|
|
146
124
|
},
|
|
147
125
|
"parent": "products"
|
|
148
126
|
},
|
|
@@ -157,13 +135,13 @@
|
|
|
157
135
|
"is_verified": "boolean",
|
|
158
136
|
"is_approved": "boolean",
|
|
159
137
|
"created_at": "datetime",
|
|
160
|
-
"
|
|
138
|
+
"modified_at": "datetime"
|
|
161
139
|
},
|
|
162
140
|
"pk": "review_id",
|
|
163
141
|
"unique": ["review_id"],
|
|
164
142
|
"timestamps": {
|
|
165
143
|
"created_at": "created_at",
|
|
166
|
-
"modified_at": "
|
|
144
|
+
"modified_at": "modified_at"
|
|
167
145
|
},
|
|
168
146
|
"parent": "products"
|
|
169
147
|
},
|
|
@@ -174,13 +152,13 @@
|
|
|
174
152
|
"session_id": "string",
|
|
175
153
|
"currency": "required|string",
|
|
176
154
|
"created_at": "datetime",
|
|
177
|
-
"
|
|
155
|
+
"modified_at": "datetime"
|
|
178
156
|
},
|
|
179
157
|
"pk": "cart_id",
|
|
180
158
|
"unique": ["cart_id"],
|
|
181
159
|
"timestamps": {
|
|
182
160
|
"created_at": "created_at",
|
|
183
|
-
"modified_at": "
|
|
161
|
+
"modified_at": "modified_at"
|
|
184
162
|
},
|
|
185
163
|
"parent": null
|
|
186
164
|
},
|
|
@@ -193,13 +171,13 @@
|
|
|
193
171
|
"quantity": "required|integer",
|
|
194
172
|
"unit_price": "required|numeric",
|
|
195
173
|
"created_at": "datetime",
|
|
196
|
-
"
|
|
174
|
+
"modified_at": "datetime"
|
|
197
175
|
},
|
|
198
176
|
"pk": "cart_item_id",
|
|
199
177
|
"unique": ["cart_item_id"],
|
|
200
178
|
"timestamps": {
|
|
201
179
|
"created_at": "created_at",
|
|
202
|
-
"modified_at": "
|
|
180
|
+
"modified_at": "modified_at"
|
|
203
181
|
},
|
|
204
182
|
"parent": "carts"
|
|
205
183
|
},
|
|
@@ -219,13 +197,13 @@
|
|
|
219
197
|
"billing_address_id": "integer",
|
|
220
198
|
"notes": "string",
|
|
221
199
|
"created_at": "datetime",
|
|
222
|
-
"
|
|
200
|
+
"modified_at": "datetime"
|
|
223
201
|
},
|
|
224
202
|
"pk": "order_id",
|
|
225
203
|
"unique": ["order_number"],
|
|
226
204
|
"timestamps": {
|
|
227
205
|
"created_at": "created_at",
|
|
228
|
-
"modified_at": "
|
|
206
|
+
"modified_at": "modified_at"
|
|
229
207
|
},
|
|
230
208
|
"parent": null
|
|
231
209
|
},
|
|
@@ -261,13 +239,13 @@
|
|
|
261
239
|
"status": "required|string",
|
|
262
240
|
"paid_at": "datetime",
|
|
263
241
|
"created_at": "datetime",
|
|
264
|
-
"
|
|
242
|
+
"modified_at": "datetime"
|
|
265
243
|
},
|
|
266
244
|
"pk": "payment_id",
|
|
267
245
|
"unique": ["payment_id"],
|
|
268
246
|
"timestamps": {
|
|
269
247
|
"created_at": "created_at",
|
|
270
|
-
"modified_at": "
|
|
248
|
+
"modified_at": "modified_at"
|
|
271
249
|
},
|
|
272
250
|
"parent": "orders"
|
|
273
251
|
},
|
|
@@ -281,13 +259,13 @@
|
|
|
281
259
|
"shipped_at": "datetime",
|
|
282
260
|
"delivered_at": "datetime",
|
|
283
261
|
"created_at": "datetime",
|
|
284
|
-
"
|
|
262
|
+
"modified_at": "datetime"
|
|
285
263
|
},
|
|
286
264
|
"pk": "shipment_id",
|
|
287
265
|
"unique": ["shipment_id"],
|
|
288
266
|
"timestamps": {
|
|
289
267
|
"created_at": "created_at",
|
|
290
|
-
"modified_at": "
|
|
268
|
+
"modified_at": "modified_at"
|
|
291
269
|
},
|
|
292
270
|
"parent": "orders"
|
|
293
271
|
},
|
|
@@ -305,13 +283,13 @@
|
|
|
305
283
|
"expires_at": "datetime",
|
|
306
284
|
"is_active": "boolean",
|
|
307
285
|
"created_at": "datetime",
|
|
308
|
-
"
|
|
286
|
+
"modified_at": "datetime"
|
|
309
287
|
},
|
|
310
288
|
"pk": "coupon_id",
|
|
311
289
|
"unique": ["code"],
|
|
312
290
|
"timestamps": {
|
|
313
291
|
"created_at": "created_at",
|
|
314
|
-
"modified_at": "
|
|
292
|
+
"modified_at": "modified_at"
|
|
315
293
|
},
|
|
316
294
|
"parent": null
|
|
317
295
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Server
|
|
2
|
+
PORT=3000
|
|
3
|
+
API_BASE_PATH=/api
|
|
4
|
+
|
|
5
|
+
# Database
|
|
6
|
+
DB_TYPE=sqlite3
|
|
7
|
+
DB_NAME=./data/data.db
|
|
8
|
+
|
|
9
|
+
# Session
|
|
10
|
+
SESSION_SECRET=your_session_secret
|
|
11
|
+
|
|
12
|
+
# Logging
|
|
13
|
+
APP_NAME=your_app_name
|
|
14
|
+
LOG_LEVEL=info
|
|
15
|
+
LOKI_HOST=http://your-loki-host:3100
|
package/demo/Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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 routes from "./routes/index.js";
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import path from "path";
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const app = express();
|
|
11
|
+
const PORT = process.env.PORT || 3000;
|
|
12
|
+
|
|
13
|
+
// Middleware
|
|
14
|
+
app.use(express.json());
|
|
15
|
+
app.use(express.urlencoded({ extended: true }));
|
|
16
|
+
|
|
17
|
+
// Security (helmet, rate limiting, custom headers)
|
|
18
|
+
applySecurity(app);
|
|
19
|
+
|
|
20
|
+
// Session
|
|
21
|
+
app.use(configureSession());
|
|
22
|
+
|
|
23
|
+
// Logger
|
|
24
|
+
app.use(logger);
|
|
25
|
+
|
|
26
|
+
// Routes
|
|
27
|
+
app.use(process.env.API_BASE_PATH || "/api", routes);
|
|
28
|
+
|
|
29
|
+
// Error handler
|
|
30
|
+
app.use((err, req, res, next) => {
|
|
31
|
+
console.error(err.stack);
|
|
32
|
+
res.status(500).json({ type: "danger", message: "Internal Server Error" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.listen(PORT, () => {
|
|
36
|
+
console.log(`Server running on port ${PORT}`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export default app;
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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;
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
mod.db.connect({
|
|
62
|
+
database: process.env.DB_NAME || "./data/data.db",
|
|
63
|
+
});
|
|
64
|
+
const migrationsDir = path.join(__dirname, "../migrations");
|
|
65
|
+
runMigrations(mod.db, migrationsDir)
|
|
66
|
+
.then(() => process.exit(0))
|
|
67
|
+
.catch(err => { console.error("Migration failed:", err); process.exit(1); });
|
|
68
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of all SaaS module names.
|
|
3
|
+
* This is the single source of truth for valid module identifiers
|
|
4
|
+
* used by the permission system.
|
|
5
|
+
*
|
|
6
|
+
* @type {string[]}
|
|
7
|
+
*/
|
|
8
|
+
export const modules = ["users", "tenants", "roles", "permissions", "webhooks"];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check whether a given name is a registered module.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} name - The module name to validate
|
|
14
|
+
* @returns {boolean} True if the name exists in the modules registry
|
|
15
|
+
*/
|
|
16
|
+
export function isValidModule(name) {
|
|
17
|
+
return modules.includes(name);
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hash a password using scrypt with a random salt.
|
|
5
|
+
* Returns a string in the format "salt:derivedKey" (both hex-encoded).
|
|
6
|
+
*
|
|
7
|
+
* @param {string} password - The plaintext password to hash
|
|
8
|
+
* @returns {Promise<string>} The hashed password string
|
|
9
|
+
*/
|
|
10
|
+
export function hashPassword(password) {
|
|
11
|
+
const salt = crypto.randomBytes(16).toString("hex");
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
|
|
14
|
+
if (err) reject(err);
|
|
15
|
+
resolve(salt + ":" + derivedKey.toString("hex"));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verify a password against a previously hashed value.
|
|
22
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} password - The plaintext password to verify
|
|
25
|
+
* @param {string} hash - The stored hash in "salt:derivedKey" format
|
|
26
|
+
* @returns {Promise<boolean>} True if the password matches, false otherwise
|
|
27
|
+
*/
|
|
28
|
+
export function verifyPassword(password, hash) {
|
|
29
|
+
const [salt, key] = hash.split(":");
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
|
|
32
|
+
if (err) reject(err);
|
|
33
|
+
resolve(crypto.timingSafeEqual(Buffer.from(key, "hex"), derivedKey));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retry delay schedule in seconds.
|
|
5
|
+
* Attempt 0: immediate, 1: 1 min, 2: 5 min, 3: 1 hour, 4: 1 day.
|
|
6
|
+
*
|
|
7
|
+
* @type {number[]}
|
|
8
|
+
*/
|
|
9
|
+
export const RETRY_DELAYS = [0, 60, 300, 3600, 86400];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sign a webhook payload using HMAC-SHA256.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} payload - The payload object to sign
|
|
15
|
+
* @param {string} secret - The tenant's webhook secret
|
|
16
|
+
* @returns {string} Hex-encoded HMAC-SHA256 signature
|
|
17
|
+
*/
|
|
18
|
+
export function signPayload(payload, secret) {
|
|
19
|
+
const body = JSON.stringify(payload);
|
|
20
|
+
return crypto.createHmac("sha256", secret).update(body).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Look up the configured webhook for a tenant.
|
|
25
|
+
* TODO: Replace this stub with actual database lookup.
|
|
26
|
+
*/
|
|
27
|
+
export async function lookupWebhook(tenantId) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Log a webhook delivery event.
|
|
33
|
+
* TODO: Replace this stub with actual database insert into webhook_logs.
|
|
34
|
+
*/
|
|
35
|
+
export async function logWebhookEvent(webhookId, tenantId, eventType, payload, status, responseBody, responseStatusCode) {
|
|
36
|
+
// Stub: replace with actual DB insert
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Delay execution for the specified number of milliseconds.
|
|
41
|
+
*/
|
|
42
|
+
export function delay(ms) {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Send a webhook notification to the configured endpoint for a tenant.
|
|
48
|
+
* Retries delivery up to 5 times with exponential backoff.
|
|
49
|
+
*/
|
|
50
|
+
export async function sendWebhook(tenantId, event, context) {
|
|
51
|
+
const webhook = await lookupWebhook(tenantId);
|
|
52
|
+
if (!webhook) return;
|
|
53
|
+
|
|
54
|
+
const payload = { context, event, timestamp: new Date().toISOString() };
|
|
55
|
+
payload.signature = signPayload(payload, webhook.secret);
|
|
56
|
+
|
|
57
|
+
for (let attempt = 0; attempt < RETRY_DELAYS.length; attempt++) {
|
|
58
|
+
if (attempt > 0) {
|
|
59
|
+
await delay(RETRY_DELAYS[attempt] * 1000);
|
|
60
|
+
console.log(`Webhook retry attempt ${attempt}, delay: ${RETRY_DELAYS[attempt]}s`);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(webhook.url, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json", "X-Webhook-Key": webhook.key },
|
|
66
|
+
body: JSON.stringify(payload),
|
|
67
|
+
});
|
|
68
|
+
await logWebhookEvent(
|
|
69
|
+
webhook.id, tenantId, event.type, payload,
|
|
70
|
+
response.ok ? "success" : "failed",
|
|
71
|
+
await response.text(), response.status
|
|
72
|
+
);
|
|
73
|
+
if (response.ok) return;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
await logWebhookEvent(
|
|
76
|
+
webhook.id, tenantId, event.type, payload,
|
|
77
|
+
"error", err.message, null
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|