db-model-router 1.0.9 → 1.0.11
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/.codegraph/config.json +143 -0
- package/CLAUDE.md +150 -0
- package/dbmr.schema.json +3 -0
- package/docs/dbmr-schema-spec.md +597 -393
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/skill/references/dbmr-schema-spec.md +597 -0
- package/src/cli/commands/generate.js +6 -6
- package/src/cli/diff-engine.js +4 -4
- package/src/cli/generate-migration.js +4 -0
- package/src/cli/generate-route.js +3 -3
- package/src/schema/schema-validator.js +1 -1
- package/.env +0 -7
- package/db-manager/.dbmanager.sqlite +0 -0
- package/db-manager/.dbmanager.sqlite-shm +0 -0
- package/db-manager/.dbmanager.sqlite-wal +0 -0
- package/db-manager/demo/cockroachdb.env +0 -6
- package/db-manager/demo/demo.sqlite +0 -0
- package/db-manager/demo/dynamodb.env +0 -7
- package/db-manager/demo/mongodb.env +0 -4
- package/db-manager/demo/mssql.env +0 -6
- package/db-manager/demo/mysql.env +0 -6
- package/db-manager/demo/oracle.env +0 -6
- package/db-manager/demo/postgres.env +0 -6
- package/db-manager/demo/redis.env +0 -4
- package/db-manager/demo/seeds/cockroachdb.sql +0 -32
- package/db-manager/demo/seeds/mssql.sql +0 -32
- package/db-manager/demo/seeds/mysql.sql +0 -32
- package/db-manager/demo/seeds/oracle.sql +0 -43
- package/db-manager/demo/seeds/postgres.sql +0 -32
- package/db-manager/demo/seeds/sqlite3.sql +0 -32
- package/db-manager/demo/sqlite3.env +0 -2
- package/demo/.dockerignore +0 -7
- package/demo/.env.example +0 -15
- package/demo/Dockerfile +0 -20
- package/demo/app.js +0 -39
- package/demo/commons/add_migration.js +0 -43
- package/demo/commons/db.js +0 -17
- package/demo/commons/migrate.js +0 -68
- package/demo/commons/modules.js +0 -18
- package/demo/commons/password.js +0 -36
- package/demo/commons/security.js +0 -30
- package/demo/commons/session.js +0 -13
- package/demo/commons/webhook.js +0 -81
- package/demo/dbmr.schema.json +0 -338
- package/demo/middleware/authenticate.js +0 -14
- package/demo/middleware/hasPermission.js +0 -30
- package/demo/middleware/logger.js +0 -67
- package/demo/middleware/tenantIsolation.js +0 -19
- package/demo/migrations/20260510193736_create_migrations_table.sql +0 -6
- package/demo/migrations/20260510193737_create_saas_tables.sql +0 -69
- package/demo/migrations/20260510193737_create_tables.sql +0 -193
- package/demo/models/addresses.js +0 -24
- package/demo/models/cart_items.js +0 -20
- package/demo/models/carts.js +0 -18
- package/demo/models/categories.js +0 -22
- package/demo/models/coupons.js +0 -25
- package/demo/models/index.js +0 -43
- package/demo/models/order_items.js +0 -23
- package/demo/models/orders.js +0 -27
- package/demo/models/payments.js +0 -23
- package/demo/models/product_images.js +0 -20
- package/demo/models/product_reviews.js +0 -22
- package/demo/models/product_variants.js +0 -22
- package/demo/models/products.js +0 -32
- package/demo/models/role_permissions.js +0 -17
- package/demo/models/roles.js +0 -17
- package/demo/models/shipments.js +0 -21
- package/demo/models/tenants.js +0 -18
- package/demo/models/users.js +0 -23
- package/demo/models/webhook_logs.js +0 -22
- package/demo/models/webhooks.js +0 -19
- package/demo/models/wishlists.js +0 -17
- package/demo/openapi.json +0 -7000
- package/demo/package-lock.json +0 -3972
- package/demo/package.json +0 -46
- package/demo/routes/addresses/index.js +0 -10
- package/demo/routes/auth/index.js +0 -55
- package/demo/routes/carts/cart_items/index.js +0 -11
- package/demo/routes/carts/index.js +0 -14
- package/demo/routes/categories/index.js +0 -10
- package/demo/routes/coupons/index.js +0 -10
- package/demo/routes/docs.js +0 -18
- package/demo/routes/health.js +0 -35
- package/demo/routes/index.js +0 -40
- package/demo/routes/orders/index.js +0 -18
- package/demo/routes/orders/order_items/index.js +0 -11
- package/demo/routes/orders/payments/index.js +0 -11
- package/demo/routes/orders/shipments/index.js +0 -11
- package/demo/routes/products/index.js +0 -18
- package/demo/routes/products/product_images/index.js +0 -11
- package/demo/routes/products/product_reviews/index.js +0 -11
- package/demo/routes/products/product_variants/index.js +0 -11
- package/demo/routes/roles/index.js +0 -75
- package/demo/routes/roles/permissions/index.js +0 -47
- package/demo/routes/tenants/index.js +0 -45
- package/demo/routes/users/index.js +0 -45
- package/demo/routes/wishlists/index.js +0 -10
- package/demo/seeds/saas-seed.js +0 -329
|
@@ -317,6 +317,10 @@ function generateCreateTableSQL(tableName, tableDef, adapter) {
|
|
|
317
317
|
const pk = tableDef.pk;
|
|
318
318
|
|
|
319
319
|
for (const [colName, rule] of Object.entries(tableDef.columns)) {
|
|
320
|
+
// Skip dot-notation keys — they are object sub-key validation rules,
|
|
321
|
+
// not standalone database columns. Only the parent object column is created.
|
|
322
|
+
if (colName.includes(".")) continue;
|
|
323
|
+
|
|
320
324
|
const { sqlType, nullable, isAutoIncrement } = mapColumnType(rule, adapter);
|
|
321
325
|
let line = ` ${quoteIdent(colName, adapter)} ${sqlType}`;
|
|
322
326
|
|
|
@@ -40,7 +40,7 @@ export default router;
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Generate a parent route file that includes its own CRUD and mounts child routes.
|
|
43
|
-
* e.g., routes/
|
|
43
|
+
* e.g., routes/orders/index.js mounts order_items under /:order_id/items
|
|
44
44
|
*
|
|
45
45
|
* @param {string} tableName - Parent table name
|
|
46
46
|
* @param {Array<{child, foreignKey}>} children - Child relationships for this parent
|
|
@@ -56,7 +56,7 @@ import { ${varName} } from "#models";
|
|
|
56
56
|
// Import child routes
|
|
57
57
|
for (const child of children) {
|
|
58
58
|
const childVar = safeVarName(child.child);
|
|
59
|
-
code += `import ${childVar}Route from "./${
|
|
59
|
+
code += `import ${childVar}Route from "./${child.child}/index.js";\n`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
code += `
|
|
@@ -129,7 +129,7 @@ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
|
|
|
129
129
|
for (const table of tableNames) {
|
|
130
130
|
if (nestedChildren.has(table)) continue;
|
|
131
131
|
const varName = safeVarName(table);
|
|
132
|
-
imports += `import ${varName}Route from "./${table}.js";\n`;
|
|
132
|
+
imports += `import ${varName}Route from "./${table}/index.js";\n`;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// Import docs route if openapi is generated
|
|
@@ -16,7 +16,7 @@ const VALID_ADAPTERS = new Set([
|
|
|
16
16
|
const VALID_FRAMEWORKS = new Set(["express", "ultimate-express"]);
|
|
17
17
|
|
|
18
18
|
const COLUMN_RULE_RE =
|
|
19
|
-
/^(required\|)?(string|integer|numeric|boolean|object|datetime|auto_increment)
|
|
19
|
+
/^(required\|)?(string|integer|numeric|boolean|object|datetime|auto_increment)(:[^|]+)?(\|[^|]+)*$/;
|
|
20
20
|
|
|
21
21
|
class SchemaValidationError extends Error {
|
|
22
22
|
constructor(errors) {
|
package/.env
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
-- CockroachDB seed data for DB Manager demo
|
|
2
|
-
-- Creates users and products tables with sample data
|
|
3
|
-
|
|
4
|
-
DROP TABLE IF EXISTS users;
|
|
5
|
-
DROP TABLE IF EXISTS products;
|
|
6
|
-
|
|
7
|
-
CREATE TABLE users (
|
|
8
|
-
id SERIAL PRIMARY KEY,
|
|
9
|
-
name VARCHAR(255) NOT NULL,
|
|
10
|
-
email VARCHAR(255) NOT NULL,
|
|
11
|
-
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
CREATE TABLE products (
|
|
15
|
-
id SERIAL PRIMARY KEY,
|
|
16
|
-
name VARCHAR(255) NOT NULL,
|
|
17
|
-
price NUMERIC(10,2) NOT NULL DEFAULT 0,
|
|
18
|
-
stock INTEGER NOT NULL DEFAULT 0,
|
|
19
|
-
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
|
|
23
|
-
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
|
|
24
|
-
INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com');
|
|
25
|
-
INSERT INTO users (name, email) VALUES ('Diana', 'diana@example.com');
|
|
26
|
-
INSERT INTO users (name, email) VALUES ('Eve', 'eve@example.com');
|
|
27
|
-
|
|
28
|
-
INSERT INTO products (name, price, stock) VALUES ('Widget', 9.99, 100);
|
|
29
|
-
INSERT INTO products (name, price, stock) VALUES ('Gadget', 24.99, 50);
|
|
30
|
-
INSERT INTO products (name, price, stock) VALUES ('Doohickey', 4.99, 200);
|
|
31
|
-
INSERT INTO products (name, price, stock) VALUES ('Thingamajig', 49.99, 25);
|
|
32
|
-
INSERT INTO products (name, price, stock) VALUES ('Whatchamacallit', 14.99, 75);
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
-- MSSQL seed data for DB Manager demo
|
|
2
|
-
-- Creates users and products tables with sample data
|
|
3
|
-
|
|
4
|
-
IF OBJECT_ID('dbo.users', 'U') IS NOT NULL DROP TABLE dbo.users;
|
|
5
|
-
IF OBJECT_ID('dbo.products', 'U') IS NOT NULL DROP TABLE dbo.products;
|
|
6
|
-
|
|
7
|
-
CREATE TABLE users (
|
|
8
|
-
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
9
|
-
name NVARCHAR(255) NOT NULL,
|
|
10
|
-
email NVARCHAR(255) NOT NULL,
|
|
11
|
-
created_at DATETIME2 NOT NULL DEFAULT GETDATE()
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
CREATE TABLE products (
|
|
15
|
-
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
16
|
-
name NVARCHAR(255) NOT NULL,
|
|
17
|
-
price DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
18
|
-
stock INT NOT NULL DEFAULT 0,
|
|
19
|
-
created_at DATETIME2 NOT NULL DEFAULT GETDATE()
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
|
|
23
|
-
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
|
|
24
|
-
INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com');
|
|
25
|
-
INSERT INTO users (name, email) VALUES ('Diana', 'diana@example.com');
|
|
26
|
-
INSERT INTO users (name, email) VALUES ('Eve', 'eve@example.com');
|
|
27
|
-
|
|
28
|
-
INSERT INTO products (name, price, stock) VALUES ('Widget', 9.99, 100);
|
|
29
|
-
INSERT INTO products (name, price, stock) VALUES ('Gadget', 24.99, 50);
|
|
30
|
-
INSERT INTO products (name, price, stock) VALUES ('Doohickey', 4.99, 200);
|
|
31
|
-
INSERT INTO products (name, price, stock) VALUES ('Thingamajig', 49.99, 25);
|
|
32
|
-
INSERT INTO products (name, price, stock) VALUES ('Whatchamacallit', 14.99, 75);
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
-- MySQL seed data for DB Manager demo
|
|
2
|
-
-- Creates users and products tables with sample data
|
|
3
|
-
|
|
4
|
-
DROP TABLE IF EXISTS users;
|
|
5
|
-
DROP TABLE IF EXISTS products;
|
|
6
|
-
|
|
7
|
-
CREATE TABLE users (
|
|
8
|
-
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
9
|
-
name VARCHAR(255) NOT NULL,
|
|
10
|
-
email VARCHAR(255) NOT NULL,
|
|
11
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
CREATE TABLE products (
|
|
15
|
-
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
16
|
-
name VARCHAR(255) NOT NULL,
|
|
17
|
-
price DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
18
|
-
stock INT NOT NULL DEFAULT 0,
|
|
19
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
|
|
23
|
-
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
|
|
24
|
-
INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com');
|
|
25
|
-
INSERT INTO users (name, email) VALUES ('Diana', 'diana@example.com');
|
|
26
|
-
INSERT INTO users (name, email) VALUES ('Eve', 'eve@example.com');
|
|
27
|
-
|
|
28
|
-
INSERT INTO products (name, price, stock) VALUES ('Widget', 9.99, 100);
|
|
29
|
-
INSERT INTO products (name, price, stock) VALUES ('Gadget', 24.99, 50);
|
|
30
|
-
INSERT INTO products (name, price, stock) VALUES ('Doohickey', 4.99, 200);
|
|
31
|
-
INSERT INTO products (name, price, stock) VALUES ('Thingamajig', 49.99, 25);
|
|
32
|
-
INSERT INTO products (name, price, stock) VALUES ('Whatchamacallit', 14.99, 75);
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
-- Oracle seed data for DB Manager demo
|
|
2
|
-
-- Creates users and products tables with sample data
|
|
3
|
-
|
|
4
|
-
BEGIN
|
|
5
|
-
EXECUTE IMMEDIATE 'DROP TABLE users CASCADE CONSTRAINTS';
|
|
6
|
-
EXCEPTION
|
|
7
|
-
WHEN OTHERS THEN NULL;
|
|
8
|
-
END;
|
|
9
|
-
/
|
|
10
|
-
|
|
11
|
-
BEGIN
|
|
12
|
-
EXECUTE IMMEDIATE 'DROP TABLE products CASCADE CONSTRAINTS';
|
|
13
|
-
EXCEPTION
|
|
14
|
-
WHEN OTHERS THEN NULL;
|
|
15
|
-
END;
|
|
16
|
-
/
|
|
17
|
-
|
|
18
|
-
CREATE TABLE users (
|
|
19
|
-
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
20
|
-
name VARCHAR2(255) NOT NULL,
|
|
21
|
-
email VARCHAR2(255) NOT NULL,
|
|
22
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
CREATE TABLE products (
|
|
26
|
-
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
27
|
-
name VARCHAR2(255) NOT NULL,
|
|
28
|
-
price NUMBER(10,2) DEFAULT 0 NOT NULL,
|
|
29
|
-
stock NUMBER DEFAULT 0 NOT NULL,
|
|
30
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
|
|
34
|
-
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
|
|
35
|
-
INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com');
|
|
36
|
-
INSERT INTO users (name, email) VALUES ('Diana', 'diana@example.com');
|
|
37
|
-
INSERT INTO users (name, email) VALUES ('Eve', 'eve@example.com');
|
|
38
|
-
|
|
39
|
-
INSERT INTO products (name, price, stock) VALUES ('Widget', 9.99, 100);
|
|
40
|
-
INSERT INTO products (name, price, stock) VALUES ('Gadget', 24.99, 50);
|
|
41
|
-
INSERT INTO products (name, price, stock) VALUES ('Doohickey', 4.99, 200);
|
|
42
|
-
INSERT INTO products (name, price, stock) VALUES ('Thingamajig', 49.99, 25);
|
|
43
|
-
INSERT INTO products (name, price, stock) VALUES ('Whatchamacallit', 14.99, 75);
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
-- PostgreSQL seed data for DB Manager demo
|
|
2
|
-
-- Creates users and products tables with sample data
|
|
3
|
-
|
|
4
|
-
DROP TABLE IF EXISTS users;
|
|
5
|
-
DROP TABLE IF EXISTS products;
|
|
6
|
-
|
|
7
|
-
CREATE TABLE users (
|
|
8
|
-
id SERIAL PRIMARY KEY,
|
|
9
|
-
name VARCHAR(255) NOT NULL,
|
|
10
|
-
email VARCHAR(255) NOT NULL,
|
|
11
|
-
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
CREATE TABLE products (
|
|
15
|
-
id SERIAL PRIMARY KEY,
|
|
16
|
-
name VARCHAR(255) NOT NULL,
|
|
17
|
-
price NUMERIC(10,2) NOT NULL DEFAULT 0,
|
|
18
|
-
stock INTEGER NOT NULL DEFAULT 0,
|
|
19
|
-
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
|
|
23
|
-
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
|
|
24
|
-
INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com');
|
|
25
|
-
INSERT INTO users (name, email) VALUES ('Diana', 'diana@example.com');
|
|
26
|
-
INSERT INTO users (name, email) VALUES ('Eve', 'eve@example.com');
|
|
27
|
-
|
|
28
|
-
INSERT INTO products (name, price, stock) VALUES ('Widget', 9.99, 100);
|
|
29
|
-
INSERT INTO products (name, price, stock) VALUES ('Gadget', 24.99, 50);
|
|
30
|
-
INSERT INTO products (name, price, stock) VALUES ('Doohickey', 4.99, 200);
|
|
31
|
-
INSERT INTO products (name, price, stock) VALUES ('Thingamajig', 49.99, 25);
|
|
32
|
-
INSERT INTO products (name, price, stock) VALUES ('Whatchamacallit', 14.99, 75);
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
-- SQLite3 seed data for DB Manager demo
|
|
2
|
-
-- Creates users and products tables with sample data
|
|
3
|
-
|
|
4
|
-
DROP TABLE IF EXISTS users;
|
|
5
|
-
DROP TABLE IF EXISTS products;
|
|
6
|
-
|
|
7
|
-
CREATE TABLE users (
|
|
8
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9
|
-
name TEXT NOT NULL,
|
|
10
|
-
email TEXT NOT NULL,
|
|
11
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
CREATE TABLE products (
|
|
15
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
-
name TEXT NOT NULL,
|
|
17
|
-
price REAL NOT NULL DEFAULT 0,
|
|
18
|
-
stock INTEGER NOT NULL DEFAULT 0,
|
|
19
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
|
|
23
|
-
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
|
|
24
|
-
INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com');
|
|
25
|
-
INSERT INTO users (name, email) VALUES ('Diana', 'diana@example.com');
|
|
26
|
-
INSERT INTO users (name, email) VALUES ('Eve', 'eve@example.com');
|
|
27
|
-
|
|
28
|
-
INSERT INTO products (name, price, stock) VALUES ('Widget', 9.99, 100);
|
|
29
|
-
INSERT INTO products (name, price, stock) VALUES ('Gadget', 24.99, 50);
|
|
30
|
-
INSERT INTO products (name, price, stock) VALUES ('Doohickey', 4.99, 200);
|
|
31
|
-
INSERT INTO products (name, price, stock) VALUES ('Thingamajig', 49.99, 25);
|
|
32
|
-
INSERT INTO products (name, price, stock) VALUES ('Whatchamacallit', 14.99, 75);
|
package/demo/.dockerignore
DELETED
package/demo/.env.example
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
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,39 +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 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;
|
|
@@ -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,68 +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
|
-
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
|
-
}
|
package/demo/commons/modules.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|
package/demo/commons/password.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
}
|
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
|
-
}
|