db-model-router 1.0.5 → 1.0.7
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 -14
- 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 +23 -45
- package/demo/.env.example +1 -0
- package/demo/app.js +3 -1
- package/demo/commons/db.js +11 -0
- package/demo/commons/migrate.js +3 -0
- package/demo/commons/modules.js +18 -0
- package/demo/commons/password.js +36 -0
- package/demo/commons/webhook.js +81 -0
- package/demo/dbmr.schema.json +22 -46
- package/demo/middleware/authenticate.js +14 -0
- package/demo/middleware/hasPermission.js +30 -0
- package/demo/middleware/tenantIsolation.js +17 -0
- package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
- package/demo/migrations/{20260430155809_create_tables.sql → 20260509170349_create_tables.sql} +11 -25
- package/demo/models/addresses.js +5 -3
- package/demo/models/cart_items.js +5 -3
- package/demo/models/carts.js +5 -3
- package/demo/models/categories.js +5 -3
- package/demo/models/coupons.js +5 -3
- package/demo/models/index.js +43 -0
- package/demo/models/order_items.js +4 -2
- package/demo/models/orders.js +5 -3
- package/demo/models/payments.js +5 -3
- package/demo/models/product_images.js +4 -2
- package/demo/models/product_reviews.js +5 -3
- package/demo/models/product_variants.js +5 -3
- package/demo/models/products.js +5 -3
- package/demo/models/role_permissions.js +17 -0
- package/demo/models/roles.js +17 -0
- package/demo/models/shipments.js +5 -3
- package/demo/models/tenants.js +18 -0
- package/demo/models/users.js +12 -8
- package/demo/models/webhook_logs.js +22 -0
- package/demo/models/webhooks.js +19 -0
- package/demo/models/wishlists.js +4 -2
- package/demo/openapi.json +1744 -616
- package/demo/package-lock.json +24 -24
- package/demo/package.json +9 -0
- package/demo/routes/{addresses.js → addresses/index.js} +1 -1
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/{cart_items.js → cart_items/index.js} +1 -1
- package/demo/routes/{carts.js → carts/index.js} +1 -1
- package/demo/routes/{categories.js → categories/index.js} +1 -1
- package/demo/routes/{coupons.js → coupons/index.js} +1 -1
- package/demo/routes/index.js +39 -24
- package/demo/routes/{orders.js → orders/index.js} +1 -1
- package/demo/routes/orders/{order_items.js → order_items/index.js} +1 -1
- package/demo/routes/orders/{payments.js → payments/index.js} +1 -1
- package/demo/routes/orders/{shipments.js → shipments/index.js} +1 -1
- package/demo/routes/{products.js → products/index.js} +1 -1
- package/demo/routes/products/{product_images.js → product_images/index.js} +1 -1
- package/demo/routes/products/{product_reviews.js → product_reviews/index.js} +1 -1
- package/demo/routes/products/{product_variants.js → product_variants/index.js} +1 -1
- 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.js → wishlists/index.js} +1 -1
- 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 +112 -43
- package/src/cli/commands/help.js +0 -1
- package/src/cli/diff-engine.js +2 -1
- package/src/cli/generate-model.js +9 -4
- package/src/cli/generate-openapi.js +40 -13
- package/src/cli/generate-route.js +61 -22
- package/src/cli/generate-saas-structure.js +122 -0
- package/src/cli/init/generators.js +42 -30
- package/src/cli/init.js +8 -0
- package/src/cli/main.js +8 -1
- package/src/cli/saas/generate-saas-middleware.js +108 -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-utils.js +176 -0
- package/src/commons/kafka.js +139 -0
- package/src/commons/model.js +29 -9
- 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/sqlite3/db.js +11 -0
- package/demo/docs/llm.md +0 -197
- package/demo/llms.txt +0 -70
- package/demo/routes/users.js +0 -6
- package/src/cli/commands/generate-llm-docs.js +0 -418
- /package/demo/migrations/{20260430155808_create_migrations_table.sql → 20260509170349_create_migrations_table.sql} +0 -0
package/dbmr.schema.json
CHANGED
|
@@ -2,35 +2,13 @@
|
|
|
2
2
|
"adapter": "sqlite3",
|
|
3
3
|
"framework": "express",
|
|
4
4
|
"options": {
|
|
5
|
-
"session": "
|
|
5
|
+
"session": "memory",
|
|
6
6
|
"rateLimiting": true,
|
|
7
7
|
"helmet": true,
|
|
8
8
|
"logger": true,
|
|
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
|
},
|
package/demo/.env.example
CHANGED
package/demo/app.js
CHANGED
|
@@ -4,7 +4,9 @@ import configureSession from "./commons/session.js";
|
|
|
4
4
|
import applySecurity from "./commons/security.js";
|
|
5
5
|
import logger from "./middleware/logger.js";
|
|
6
6
|
import route from "./routes/index.js";
|
|
7
|
-
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import path from "path";
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
10
|
const app = express();
|
|
9
11
|
const PORT = process.env.PORT || 3000;
|
|
10
12
|
|
package/demo/commons/db.js
CHANGED
|
@@ -9,6 +9,17 @@ dbModelRouter.db.connect({
|
|
|
9
9
|
database: process.env.DB_NAME || "./data/data.db",
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
// Initialize Kafka if KAFKA_BROKER is configured
|
|
13
|
+
if (process.env.KAFKA_BROKER) {
|
|
14
|
+
dbModelRouter.kafka.init().then((connected) => {
|
|
15
|
+
if (connected) {
|
|
16
|
+
console.log("[kafka] Producer connected to", process.env.KAFKA_BROKER);
|
|
17
|
+
} else {
|
|
18
|
+
console.warn("[kafka] Failed to connect to Kafka broker");
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
// Make db available globally across the application
|
|
13
24
|
const db = dbModelRouter.db;
|
|
14
25
|
global.db = db;
|
package/demo/commons/migrate.js
CHANGED
|
@@ -58,6 +58,9 @@ if (isMain) {
|
|
|
58
58
|
const pkg = await import("db-model-router");
|
|
59
59
|
const mod = pkg.default || pkg;
|
|
60
60
|
mod.init("sqlite3");
|
|
61
|
+
mod.db.connect({
|
|
62
|
+
database: process.env.DB_NAME || "./data/data.db",
|
|
63
|
+
});
|
|
61
64
|
const migrationsDir = path.join(__dirname, "../migrations");
|
|
62
65
|
runMigrations(mod.db, migrationsDir)
|
|
63
66
|
.then(() => process.exit(0))
|
|
@@ -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,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
|
+
}
|
package/demo/dbmr.schema.json
CHANGED
|
@@ -7,30 +7,6 @@
|
|
|
7
7
|
"logger": true
|
|
8
8
|
},
|
|
9
9
|
"tables": {
|
|
10
|
-
"users": {
|
|
11
|
-
"columns": {
|
|
12
|
-
"user_id": "auto_increment",
|
|
13
|
-
"name": "required|string",
|
|
14
|
-
"email": "required|string",
|
|
15
|
-
"password_hash": "required|string",
|
|
16
|
-
"phone": "string",
|
|
17
|
-
"avatar_url": "string",
|
|
18
|
-
"role": "required|string",
|
|
19
|
-
"is_deleted": "boolean",
|
|
20
|
-
"created_at": "datetime",
|
|
21
|
-
"updated_at": "datetime"
|
|
22
|
-
},
|
|
23
|
-
"pk": "user_id",
|
|
24
|
-
"unique": [
|
|
25
|
-
"email"
|
|
26
|
-
],
|
|
27
|
-
"softDelete": "is_deleted",
|
|
28
|
-
"timestamps": {
|
|
29
|
-
"created_at": "created_at",
|
|
30
|
-
"modified_at": "updated_at"
|
|
31
|
-
},
|
|
32
|
-
"parent": null
|
|
33
|
-
},
|
|
34
10
|
"addresses": {
|
|
35
11
|
"columns": {
|
|
36
12
|
"address_id": "auto_increment",
|
|
@@ -44,7 +20,7 @@
|
|
|
44
20
|
"country": "required|string",
|
|
45
21
|
"is_default": "boolean",
|
|
46
22
|
"created_at": "datetime",
|
|
47
|
-
"
|
|
23
|
+
"modified_at": "datetime"
|
|
48
24
|
},
|
|
49
25
|
"pk": "address_id",
|
|
50
26
|
"unique": [
|
|
@@ -52,7 +28,7 @@
|
|
|
52
28
|
],
|
|
53
29
|
"timestamps": {
|
|
54
30
|
"created_at": "created_at",
|
|
55
|
-
"modified_at": "
|
|
31
|
+
"modified_at": "modified_at"
|
|
56
32
|
},
|
|
57
33
|
"parent": null
|
|
58
34
|
},
|
|
@@ -67,7 +43,7 @@
|
|
|
67
43
|
"sort_order": "integer",
|
|
68
44
|
"is_active": "boolean",
|
|
69
45
|
"created_at": "datetime",
|
|
70
|
-
"
|
|
46
|
+
"modified_at": "datetime"
|
|
71
47
|
},
|
|
72
48
|
"pk": "category_id",
|
|
73
49
|
"unique": [
|
|
@@ -75,7 +51,7 @@
|
|
|
75
51
|
],
|
|
76
52
|
"timestamps": {
|
|
77
53
|
"created_at": "created_at",
|
|
78
|
-
"modified_at": "
|
|
54
|
+
"modified_at": "modified_at"
|
|
79
55
|
},
|
|
80
56
|
"parent": null
|
|
81
57
|
},
|
|
@@ -101,7 +77,7 @@
|
|
|
101
77
|
"is_deleted": "boolean",
|
|
102
78
|
"meta": "object",
|
|
103
79
|
"created_at": "datetime",
|
|
104
|
-
"
|
|
80
|
+
"modified_at": "datetime"
|
|
105
81
|
},
|
|
106
82
|
"pk": "product_id",
|
|
107
83
|
"unique": [
|
|
@@ -111,7 +87,7 @@
|
|
|
111
87
|
"softDelete": "is_deleted",
|
|
112
88
|
"timestamps": {
|
|
113
89
|
"created_at": "created_at",
|
|
114
|
-
"modified_at": "
|
|
90
|
+
"modified_at": "modified_at"
|
|
115
91
|
},
|
|
116
92
|
"parent": null
|
|
117
93
|
},
|
|
@@ -145,7 +121,7 @@
|
|
|
145
121
|
"attributes": "object",
|
|
146
122
|
"is_active": "boolean",
|
|
147
123
|
"created_at": "datetime",
|
|
148
|
-
"
|
|
124
|
+
"modified_at": "datetime"
|
|
149
125
|
},
|
|
150
126
|
"pk": "variant_id",
|
|
151
127
|
"unique": [
|
|
@@ -153,7 +129,7 @@
|
|
|
153
129
|
],
|
|
154
130
|
"timestamps": {
|
|
155
131
|
"created_at": "created_at",
|
|
156
|
-
"modified_at": "
|
|
132
|
+
"modified_at": "modified_at"
|
|
157
133
|
},
|
|
158
134
|
"parent": "products"
|
|
159
135
|
},
|
|
@@ -168,7 +144,7 @@
|
|
|
168
144
|
"is_verified": "boolean",
|
|
169
145
|
"is_approved": "boolean",
|
|
170
146
|
"created_at": "datetime",
|
|
171
|
-
"
|
|
147
|
+
"modified_at": "datetime"
|
|
172
148
|
},
|
|
173
149
|
"pk": "review_id",
|
|
174
150
|
"unique": [
|
|
@@ -176,7 +152,7 @@
|
|
|
176
152
|
],
|
|
177
153
|
"timestamps": {
|
|
178
154
|
"created_at": "created_at",
|
|
179
|
-
"modified_at": "
|
|
155
|
+
"modified_at": "modified_at"
|
|
180
156
|
},
|
|
181
157
|
"parent": "products"
|
|
182
158
|
},
|
|
@@ -187,7 +163,7 @@
|
|
|
187
163
|
"session_id": "string",
|
|
188
164
|
"currency": "required|string",
|
|
189
165
|
"created_at": "datetime",
|
|
190
|
-
"
|
|
166
|
+
"modified_at": "datetime"
|
|
191
167
|
},
|
|
192
168
|
"pk": "cart_id",
|
|
193
169
|
"unique": [
|
|
@@ -195,7 +171,7 @@
|
|
|
195
171
|
],
|
|
196
172
|
"timestamps": {
|
|
197
173
|
"created_at": "created_at",
|
|
198
|
-
"modified_at": "
|
|
174
|
+
"modified_at": "modified_at"
|
|
199
175
|
},
|
|
200
176
|
"parent": null
|
|
201
177
|
},
|
|
@@ -208,7 +184,7 @@
|
|
|
208
184
|
"quantity": "required|integer",
|
|
209
185
|
"unit_price": "required|numeric",
|
|
210
186
|
"created_at": "datetime",
|
|
211
|
-
"
|
|
187
|
+
"modified_at": "datetime"
|
|
212
188
|
},
|
|
213
189
|
"pk": "cart_item_id",
|
|
214
190
|
"unique": [
|
|
@@ -216,7 +192,7 @@
|
|
|
216
192
|
],
|
|
217
193
|
"timestamps": {
|
|
218
194
|
"created_at": "created_at",
|
|
219
|
-
"modified_at": "
|
|
195
|
+
"modified_at": "modified_at"
|
|
220
196
|
},
|
|
221
197
|
"parent": "carts"
|
|
222
198
|
},
|
|
@@ -236,7 +212,7 @@
|
|
|
236
212
|
"billing_address_id": "integer",
|
|
237
213
|
"notes": "string",
|
|
238
214
|
"created_at": "datetime",
|
|
239
|
-
"
|
|
215
|
+
"modified_at": "datetime"
|
|
240
216
|
},
|
|
241
217
|
"pk": "order_id",
|
|
242
218
|
"unique": [
|
|
@@ -244,7 +220,7 @@
|
|
|
244
220
|
],
|
|
245
221
|
"timestamps": {
|
|
246
222
|
"created_at": "created_at",
|
|
247
|
-
"modified_at": "
|
|
223
|
+
"modified_at": "modified_at"
|
|
248
224
|
},
|
|
249
225
|
"parent": null
|
|
250
226
|
},
|
|
@@ -282,7 +258,7 @@
|
|
|
282
258
|
"status": "required|string",
|
|
283
259
|
"paid_at": "datetime",
|
|
284
260
|
"created_at": "datetime",
|
|
285
|
-
"
|
|
261
|
+
"modified_at": "datetime"
|
|
286
262
|
},
|
|
287
263
|
"pk": "payment_id",
|
|
288
264
|
"unique": [
|
|
@@ -290,7 +266,7 @@
|
|
|
290
266
|
],
|
|
291
267
|
"timestamps": {
|
|
292
268
|
"created_at": "created_at",
|
|
293
|
-
"modified_at": "
|
|
269
|
+
"modified_at": "modified_at"
|
|
294
270
|
},
|
|
295
271
|
"parent": "orders"
|
|
296
272
|
},
|
|
@@ -304,7 +280,7 @@
|
|
|
304
280
|
"shipped_at": "datetime",
|
|
305
281
|
"delivered_at": "datetime",
|
|
306
282
|
"created_at": "datetime",
|
|
307
|
-
"
|
|
283
|
+
"modified_at": "datetime"
|
|
308
284
|
},
|
|
309
285
|
"pk": "shipment_id",
|
|
310
286
|
"unique": [
|
|
@@ -312,7 +288,7 @@
|
|
|
312
288
|
],
|
|
313
289
|
"timestamps": {
|
|
314
290
|
"created_at": "created_at",
|
|
315
|
-
"modified_at": "
|
|
291
|
+
"modified_at": "modified_at"
|
|
316
292
|
},
|
|
317
293
|
"parent": "orders"
|
|
318
294
|
},
|
|
@@ -330,7 +306,7 @@
|
|
|
330
306
|
"expires_at": "datetime",
|
|
331
307
|
"is_active": "boolean",
|
|
332
308
|
"created_at": "datetime",
|
|
333
|
-
"
|
|
309
|
+
"modified_at": "datetime"
|
|
334
310
|
},
|
|
335
311
|
"pk": "coupon_id",
|
|
336
312
|
"unique": [
|
|
@@ -338,7 +314,7 @@
|
|
|
338
314
|
],
|
|
339
315
|
"timestamps": {
|
|
340
316
|
"created_at": "created_at",
|
|
341
|
-
"modified_at": "
|
|
317
|
+
"modified_at": "modified_at"
|
|
342
318
|
},
|
|
343
319
|
"parent": null
|
|
344
320
|
},
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication middleware.
|
|
3
|
+
*
|
|
4
|
+
* Validates that the request has an active session with a user object.
|
|
5
|
+
* Responds with 401 Unauthorized if no valid session exists.
|
|
6
|
+
*/
|
|
7
|
+
function authenticate(req, res, next) {
|
|
8
|
+
if (!req.session || !req.session.user) {
|
|
9
|
+
return res.status(401).json({ message: "Unauthorized" });
|
|
10
|
+
}
|
|
11
|
+
next();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default authenticate;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isValidModule } from "#commons/modules.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Permission validation middleware factory.
|
|
5
|
+
*
|
|
6
|
+
* Returns a middleware function that checks whether the authenticated user
|
|
7
|
+
* has the required permission for the specified module and action.
|
|
8
|
+
* A permission entry with action "global" grants access to any action
|
|
9
|
+
* on that module.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} module - The module name to check permission for
|
|
12
|
+
* @param {string} action - The required action
|
|
13
|
+
* @returns {function} Express middleware function
|
|
14
|
+
*/
|
|
15
|
+
function hasPermission(module, action) {
|
|
16
|
+
return (req, res, next) => {
|
|
17
|
+
if (!isValidModule(module)) {
|
|
18
|
+
return res.status(403).json({ message: "Invalid module" });
|
|
19
|
+
}
|
|
20
|
+
const match = req.session.permission.find(
|
|
21
|
+
(p) => p.module === module && (p.action === action || p.action === "global")
|
|
22
|
+
);
|
|
23
|
+
if (!match) {
|
|
24
|
+
return res.status(403).json({ message: "Forbidden" });
|
|
25
|
+
}
|
|
26
|
+
next();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default hasPermission;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant isolation middleware.
|
|
3
|
+
*
|
|
4
|
+
* Restricts data access to the user's own tenant unless the user
|
|
5
|
+
* has a global-scoped permission. Injects tenant_id into query
|
|
6
|
+
* and body parameters for non-global users.
|
|
7
|
+
*/
|
|
8
|
+
function tenantIsolation(req, res, next) {
|
|
9
|
+
const hasGlobal = req.session.permission.some((p) => p.scope === "global");
|
|
10
|
+
if (!hasGlobal) {
|
|
11
|
+
req.query.tenant_id = req.session.user.tenant_id;
|
|
12
|
+
req.body.tenant_id = req.session.user.tenant_id;
|
|
13
|
+
}
|
|
14
|
+
next();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default tenantIsolation;
|