db-model-router 1.0.3 → 1.0.5
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 +283 -25
- package/TODO.md +14 -0
- package/dbmr.schema.json +333 -0
- package/demo/.dockerignore +7 -0
- package/demo/.env.example +13 -0
- package/demo/Dockerfile +20 -0
- package/demo/app.js +37 -0
- package/demo/commons/add_migration.js +43 -0
- package/demo/commons/db.js +17 -0
- package/demo/commons/migrate.js +65 -0
- package/demo/commons/security.js +30 -0
- package/demo/commons/session.js +13 -0
- package/demo/dbmr.schema.json +362 -0
- package/demo/docs/llm.md +197 -0
- package/demo/llms.txt +70 -0
- package/demo/middleware/logger.js +67 -0
- package/demo/migrations/20260430155808_create_migrations_table.sql +6 -0
- package/demo/migrations/20260430155809_create_tables.sql +207 -0
- package/demo/models/addresses.js +22 -0
- package/demo/models/cart_items.js +18 -0
- package/demo/models/carts.js +16 -0
- package/demo/models/categories.js +20 -0
- package/demo/models/coupons.js +23 -0
- package/demo/models/order_items.js +21 -0
- package/demo/models/orders.js +25 -0
- package/demo/models/payments.js +21 -0
- package/demo/models/product_images.js +18 -0
- package/demo/models/product_reviews.js +20 -0
- package/demo/models/product_variants.js +20 -0
- package/demo/models/products.js +30 -0
- package/demo/models/shipments.js +19 -0
- package/demo/models/users.js +19 -0
- package/demo/models/wishlists.js +15 -0
- package/demo/openapi.json +5872 -0
- package/demo/package-lock.json +2810 -0
- package/demo/package.json +34 -0
- package/demo/routes/addresses.js +6 -0
- package/demo/routes/carts/cart_items.js +7 -0
- package/demo/routes/carts.js +6 -0
- package/demo/routes/categories.js +6 -0
- package/demo/routes/coupons.js +6 -0
- package/demo/routes/docs.js +18 -0
- package/demo/routes/health.js +35 -0
- package/demo/routes/index.js +39 -0
- package/demo/routes/orders/order_items.js +7 -0
- package/demo/routes/orders/payments.js +7 -0
- package/demo/routes/orders/shipments.js +7 -0
- package/demo/routes/orders.js +6 -0
- package/demo/routes/products/product_images.js +7 -0
- package/demo/routes/products/product_reviews.js +7 -0
- package/demo/routes/products/product_variants.js +7 -0
- package/demo/routes/products.js +6 -0
- package/demo/routes/users.js +6 -0
- package/demo/routes/wishlists.js +6 -0
- package/docker-compose.yml +1 -1
- package/package.json +16 -7
- package/scripts/demo-create.js +47 -0
- package/skill/SKILL.md +464 -0
- package/skill/references/cockroachdb.md +49 -0
- package/skill/references/dynamodb.md +53 -0
- package/skill/references/mongodb.md +56 -0
- package/skill/references/mssql.md +55 -0
- package/skill/references/oracle.md +52 -0
- package/skill/references/postgres.md +50 -0
- package/skill/references/redis.md +53 -0
- package/skill/references/sqlite3.md +43 -0
- package/src/cli/commands/generate.js +58 -17
- package/src/cli/commands/help.js +185 -0
- package/src/cli/commands/init.js +42 -14
- package/src/cli/commands/inspect.js +21 -3
- package/src/cli/diff-engine.js +52 -22
- package/src/cli/generate-docs-route.js +31 -0
- package/src/cli/generate-migration.js +356 -0
- package/src/cli/generate-model.js +5 -4
- package/src/cli/generate-route.js +79 -45
- package/src/cli/init/dependencies.js +17 -5
- package/src/cli/init/generators.js +1073 -64
- package/src/cli/init/prompt.js +37 -5
- package/src/cli/init.js +148 -25
- package/src/cli/main.js +90 -10
- package/src/cockroachdb/db.js +90 -59
- package/src/commons/route.js +20 -20
- package/src/commons/validator.js +58 -1
- package/src/dynamodb/db.js +50 -27
- package/src/index.js +2 -0
- package/src/mongodb/db.js +1 -0
- package/src/mssql/db.js +89 -61
- package/src/mysql/db.js +1 -0
- package/src/oracle/db.js +1 -0
- package/src/postgres/db.js +61 -41
- package/src/redis/db.js +1 -0
- package/src/schema/schema-parser.js +43 -1
- package/src/schema/schema-printer.js +8 -5
- package/src/schema/schema-to-meta.js +4 -0
- package/src/schema/schema-validator.js +20 -1
- package/src/sqlite3/db.js +1 -0
- package/docs/SKILL.md +0 -374
package/src/cli/diff-engine.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
generateChildTestFile,
|
|
12
12
|
} = require("./generate-route.js");
|
|
13
13
|
const { generateOpenAPISpec } = require("./generate-openapi.js");
|
|
14
|
+
const { generateDocsRoute } = require("./generate-docs-route.js");
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Simple line-by-line diff between two strings.
|
|
@@ -52,54 +53,63 @@ function buildExpectedFiles(meta, relationships) {
|
|
|
52
53
|
const modelsRelPath = "../models";
|
|
53
54
|
const tableNames = meta.map((m) => m.table).sort();
|
|
54
55
|
|
|
56
|
+
// Collect child tables
|
|
57
|
+
const nestedChildren = new Set();
|
|
58
|
+
for (const rel of relationships) {
|
|
59
|
+
nestedChildren.add(rel.child);
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
// Model files
|
|
56
63
|
for (const m of meta) {
|
|
57
64
|
expected.set(`models/${m.table}.js`, generateModelFile(m));
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
// Route files (
|
|
67
|
+
// Route files (top-level only, skip children)
|
|
61
68
|
for (const m of meta) {
|
|
69
|
+
if (nestedChildren.has(m.table)) continue;
|
|
62
70
|
expected.set(
|
|
63
71
|
`routes/${m.table}.js`,
|
|
64
72
|
generateRouteFile(m.table, modelsRelPath),
|
|
65
73
|
);
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
// Child route files
|
|
76
|
+
// Child route files in subfolders: routes/<parent>/<child>.js
|
|
69
77
|
for (const rel of relationships) {
|
|
70
|
-
const childMeta = meta.find((m) => m.table === rel.child);
|
|
71
|
-
const pk = childMeta ? childMeta.primary_key : "id";
|
|
72
78
|
expected.set(
|
|
73
|
-
`routes/${rel.
|
|
79
|
+
`routes/${rel.parent}/${rel.child}.js`,
|
|
74
80
|
generateChildRouteFile(
|
|
75
81
|
rel.child,
|
|
76
82
|
rel.parent,
|
|
77
83
|
rel.foreignKey,
|
|
78
|
-
|
|
84
|
+
`../../models`,
|
|
79
85
|
),
|
|
80
86
|
);
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
// Routes index file
|
|
89
|
+
// Routes index file (with docs route)
|
|
84
90
|
expected.set(
|
|
85
91
|
"routes/index.js",
|
|
86
|
-
generateRoutesIndexFile(tableNames, relationships),
|
|
92
|
+
generateRoutesIndexFile(tableNames, relationships, { includeDocs: true }),
|
|
87
93
|
);
|
|
88
94
|
|
|
89
|
-
//
|
|
95
|
+
// Docs route (Swagger UI)
|
|
96
|
+
expected.set("routes/docs.js", generateDocsRoute());
|
|
97
|
+
|
|
98
|
+
// Test files (top-level only, skip children)
|
|
90
99
|
for (const m of meta) {
|
|
100
|
+
if (nestedChildren.has(m.table)) continue;
|
|
91
101
|
expected.set(
|
|
92
102
|
`test/${m.table}.test.js`,
|
|
93
103
|
generateTestFile(m.table, m.primary_key),
|
|
94
104
|
);
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
// Child test files
|
|
107
|
+
// Child test files in subfolders: test/<parent>/<child>.test.js
|
|
98
108
|
for (const rel of relationships) {
|
|
99
109
|
const childMeta = meta.find((m) => m.table === rel.child);
|
|
100
110
|
const pk = childMeta ? childMeta.primary_key : "id";
|
|
101
111
|
expected.set(
|
|
102
|
-
`test/${rel.
|
|
112
|
+
`test/${rel.parent}/${rel.child}.test.js`,
|
|
103
113
|
generateChildTestFile(rel.child, rel.parent, rel.foreignKey, pk),
|
|
104
114
|
);
|
|
105
115
|
}
|
|
@@ -115,7 +125,7 @@ function buildExpectedFiles(meta, relationships) {
|
|
|
115
125
|
|
|
116
126
|
/**
|
|
117
127
|
* Scan known artifact directories on disk and return a set of relative paths
|
|
118
|
-
* that exist.
|
|
128
|
+
* that exist. Recursively scans subdirectories.
|
|
119
129
|
*
|
|
120
130
|
* @param {string} baseDir
|
|
121
131
|
* @returns {Set<string>}
|
|
@@ -123,22 +133,42 @@ function buildExpectedFiles(meta, relationships) {
|
|
|
123
133
|
function scanDiskFiles(baseDir) {
|
|
124
134
|
const files = new Set();
|
|
125
135
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
function scanDir(dir, prefix) {
|
|
137
|
+
const fullDir = path.join(baseDir, dir);
|
|
138
|
+
if (!fs.existsSync(fullDir)) return;
|
|
139
|
+
for (const entry of fs.readdirSync(fullDir, { withFileTypes: true })) {
|
|
140
|
+
const relPath = prefix
|
|
141
|
+
? `${prefix}/${entry.name}`
|
|
142
|
+
: `${dir}/${entry.name}`;
|
|
143
|
+
if (entry.isDirectory()) {
|
|
144
|
+
scanDir(path.join(dir, entry.name), relPath);
|
|
145
|
+
} else if (entry.name.endsWith(".js")) {
|
|
146
|
+
files.add(relPath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
scanDir("models");
|
|
152
|
+
scanDir("routes");
|
|
131
153
|
|
|
132
|
-
|
|
154
|
+
// For test dir, only include .test.js files
|
|
155
|
+
function scanTestDir(dir, prefix) {
|
|
133
156
|
const fullDir = path.join(baseDir, dir);
|
|
134
|
-
if (!fs.existsSync(fullDir))
|
|
135
|
-
for (const
|
|
136
|
-
|
|
137
|
-
|
|
157
|
+
if (!fs.existsSync(fullDir)) return;
|
|
158
|
+
for (const entry of fs.readdirSync(fullDir, { withFileTypes: true })) {
|
|
159
|
+
const relPath = prefix
|
|
160
|
+
? `${prefix}/${entry.name}`
|
|
161
|
+
: `${dir}/${entry.name}`;
|
|
162
|
+
if (entry.isDirectory()) {
|
|
163
|
+
scanTestDir(path.join(dir, entry.name), relPath);
|
|
164
|
+
} else if (entry.name.endsWith(".test.js")) {
|
|
165
|
+
files.add(relPath);
|
|
138
166
|
}
|
|
139
167
|
}
|
|
140
168
|
}
|
|
141
169
|
|
|
170
|
+
scanTestDir("test");
|
|
171
|
+
|
|
142
172
|
// Check for openapi.json at root
|
|
143
173
|
const openapiPath = path.join(baseDir, "openapi.json");
|
|
144
174
|
if (fs.existsSync(openapiPath)) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a routes/docs.js file that serves Swagger UI for the OpenAPI spec.
|
|
5
|
+
* Uses swagger-ui-express to mount at /docs.
|
|
6
|
+
*
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
function generateDocsRoute() {
|
|
10
|
+
return `import express from "express";
|
|
11
|
+
import swaggerUi from "swagger-ui-express";
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const spec = JSON.parse(readFileSync(join(__dirname, "../openapi.json"), "utf8"));
|
|
18
|
+
|
|
19
|
+
const router = express.Router();
|
|
20
|
+
|
|
21
|
+
router.use("/", swaggerUi.serve);
|
|
22
|
+
router.get("/", swaggerUi.setup(spec, {
|
|
23
|
+
customSiteTitle: "API Documentation",
|
|
24
|
+
customCss: ".swagger-ui .topbar { display: none }",
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
export default router;
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { generateDocsRoute };
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate database migration SQL/JS from parsed schema tables.
|
|
5
|
+
*
|
|
6
|
+
* Produces CREATE TABLE statements (or equivalent) for each table,
|
|
7
|
+
* with proper column types, constraints, and indexes per adapter.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Column type mapping per adapter
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map a dbmr column rule to a SQL column type for the given adapter.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} rule - e.g. "required|string", "auto_increment", "integer"
|
|
18
|
+
* @param {string} adapter - e.g. "postgres", "mysql", "sqlite3"
|
|
19
|
+
* @returns {{ sqlType: string, nullable: boolean, isAutoIncrement: boolean }}
|
|
20
|
+
*/
|
|
21
|
+
function mapColumnType(rule, adapter) {
|
|
22
|
+
const parts = rule.split("|");
|
|
23
|
+
const isRequired = parts.includes("required");
|
|
24
|
+
const baseType = parts.filter((p) => p !== "required")[0] || "string";
|
|
25
|
+
|
|
26
|
+
let sqlType;
|
|
27
|
+
let isAutoIncrement = false;
|
|
28
|
+
|
|
29
|
+
switch (baseType) {
|
|
30
|
+
case "auto_increment":
|
|
31
|
+
isAutoIncrement = true;
|
|
32
|
+
sqlType = autoIncrementType(adapter);
|
|
33
|
+
break;
|
|
34
|
+
case "string":
|
|
35
|
+
sqlType = stringType(adapter);
|
|
36
|
+
break;
|
|
37
|
+
case "integer":
|
|
38
|
+
sqlType = integerType(adapter);
|
|
39
|
+
break;
|
|
40
|
+
case "numeric":
|
|
41
|
+
sqlType = numericType(adapter);
|
|
42
|
+
break;
|
|
43
|
+
case "boolean":
|
|
44
|
+
sqlType = booleanType(adapter);
|
|
45
|
+
break;
|
|
46
|
+
case "datetime":
|
|
47
|
+
sqlType = datetimeType(adapter);
|
|
48
|
+
break;
|
|
49
|
+
case "object":
|
|
50
|
+
sqlType = jsonType(adapter);
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
sqlType = stringType(adapter);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
sqlType,
|
|
58
|
+
nullable: !isRequired && !isAutoIncrement,
|
|
59
|
+
isAutoIncrement,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function autoIncrementType(adapter) {
|
|
64
|
+
switch (adapter) {
|
|
65
|
+
case "postgres":
|
|
66
|
+
case "cockroachdb":
|
|
67
|
+
return "SERIAL";
|
|
68
|
+
case "mssql":
|
|
69
|
+
return "INT IDENTITY(1,1)";
|
|
70
|
+
case "oracle":
|
|
71
|
+
return "NUMBER GENERATED BY DEFAULT AS IDENTITY";
|
|
72
|
+
case "sqlite3":
|
|
73
|
+
return "INTEGER";
|
|
74
|
+
default:
|
|
75
|
+
// mysql, mariadb
|
|
76
|
+
return "INT AUTO_INCREMENT";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stringType(adapter) {
|
|
81
|
+
if (adapter === "oracle") return "VARCHAR2(255)";
|
|
82
|
+
return "VARCHAR(255)";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function integerType(adapter) {
|
|
86
|
+
if (adapter === "oracle") return "NUMBER(10)";
|
|
87
|
+
return "INTEGER";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function numericType(adapter) {
|
|
91
|
+
if (adapter === "oracle") return "NUMBER(12,2)";
|
|
92
|
+
if (adapter === "mssql") return "DECIMAL(12,2)";
|
|
93
|
+
return "DECIMAL(12,2)";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function booleanType(adapter) {
|
|
97
|
+
if (adapter === "oracle") return "NUMBER(1)";
|
|
98
|
+
if (adapter === "mssql") return "BIT";
|
|
99
|
+
if (adapter === "mysql" || adapter === "mariadb") return "TINYINT(1)";
|
|
100
|
+
return "BOOLEAN";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function datetimeType(adapter) {
|
|
104
|
+
if (adapter === "mssql") return "DATETIME";
|
|
105
|
+
return "TIMESTAMP";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function jsonType(adapter) {
|
|
109
|
+
if (adapter === "postgres" || adapter === "cockroachdb") return "JSONB";
|
|
110
|
+
if (adapter === "oracle") return "CLOB";
|
|
111
|
+
if (adapter === "mssql") return "NVARCHAR(MAX)";
|
|
112
|
+
if (adapter === "sqlite3") return "TEXT";
|
|
113
|
+
return "JSON";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// SQL migration generators
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Generate a CREATE TABLE SQL statement for a single table.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} tableName
|
|
124
|
+
* @param {object} tableDef - parsed table definition from schema
|
|
125
|
+
* @param {string} adapter
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function generateCreateTableSQL(tableName, tableDef, adapter) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
const pk = tableDef.pk;
|
|
131
|
+
|
|
132
|
+
for (const [colName, rule] of Object.entries(tableDef.columns)) {
|
|
133
|
+
const { sqlType, nullable, isAutoIncrement } = mapColumnType(rule, adapter);
|
|
134
|
+
let line = ` ${quoteIdent(colName, adapter)} ${sqlType}`;
|
|
135
|
+
|
|
136
|
+
if (colName === pk) {
|
|
137
|
+
line += " PRIMARY KEY";
|
|
138
|
+
if (isAutoIncrement && adapter === "sqlite3") {
|
|
139
|
+
// SQLite needs AUTOINCREMENT after PRIMARY KEY for INTEGER type
|
|
140
|
+
line = ` ${quoteIdent(colName, adapter)} INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
if (!nullable) line += " NOT NULL";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Default values for timestamps
|
|
147
|
+
if (tableDef.timestamps) {
|
|
148
|
+
if (
|
|
149
|
+
colName === tableDef.timestamps.created_at ||
|
|
150
|
+
colName === tableDef.timestamps.modified_at
|
|
151
|
+
) {
|
|
152
|
+
line += defaultTimestamp(adapter);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Default for boolean softDelete column
|
|
157
|
+
if (tableDef.softDelete && colName === tableDef.softDelete) {
|
|
158
|
+
line += defaultBoolean(adapter);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
lines.push(line);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Unique constraints (excluding PK which is already PRIMARY KEY)
|
|
165
|
+
if (tableDef.unique && tableDef.unique.length > 0) {
|
|
166
|
+
const uniqueCols = tableDef.unique.filter((c) => c !== pk);
|
|
167
|
+
for (const col of uniqueCols) {
|
|
168
|
+
lines.push(` UNIQUE (${quoteIdent(col, adapter)})`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const createPrefix =
|
|
173
|
+
adapter === "oracle" || adapter === "mssql"
|
|
174
|
+
? `CREATE TABLE ${quoteIdent(tableName, adapter)}`
|
|
175
|
+
: `CREATE TABLE IF NOT EXISTS ${quoteIdent(tableName, adapter)}`;
|
|
176
|
+
|
|
177
|
+
return `${createPrefix} (\n${lines.join(",\n")}\n);\n`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Quote an identifier based on adapter.
|
|
182
|
+
*/
|
|
183
|
+
function quoteIdent(name, adapter) {
|
|
184
|
+
// Most adapters are fine without quoting for simple names
|
|
185
|
+
// but we keep it safe for reserved words
|
|
186
|
+
if (adapter === "mssql") return `[${name}]`;
|
|
187
|
+
if (adapter === "oracle") return `"${name.toUpperCase()}"`;
|
|
188
|
+
return name;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Default timestamp expression per adapter.
|
|
193
|
+
*/
|
|
194
|
+
function defaultTimestamp(adapter) {
|
|
195
|
+
switch (adapter) {
|
|
196
|
+
case "mssql":
|
|
197
|
+
return " DEFAULT GETDATE()";
|
|
198
|
+
case "oracle":
|
|
199
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
200
|
+
case "sqlite3":
|
|
201
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
202
|
+
default:
|
|
203
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Default boolean false expression per adapter.
|
|
209
|
+
*/
|
|
210
|
+
function defaultBoolean(adapter) {
|
|
211
|
+
if (adapter === "oracle" || adapter === "mssql") return " DEFAULT 0";
|
|
212
|
+
if (adapter === "mysql" || adapter === "mariadb") return " DEFAULT 0";
|
|
213
|
+
return " DEFAULT FALSE";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// NoSQL migration generators
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generate a MongoDB migration JS file for a single table (collection).
|
|
222
|
+
*/
|
|
223
|
+
function generateMongoDBMigration(tableName, tableDef) {
|
|
224
|
+
const uniqueCols = (tableDef.unique || []).filter((c) => c !== tableDef.pk);
|
|
225
|
+
const indexLines = uniqueCols
|
|
226
|
+
.map(
|
|
227
|
+
(col) =>
|
|
228
|
+
` await db.collection("${tableName}").createIndex({ ${col}: 1 }, { unique: true });`,
|
|
229
|
+
)
|
|
230
|
+
.join("\n");
|
|
231
|
+
|
|
232
|
+
return `"use strict";
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
async up(db) {
|
|
236
|
+
await db.createCollection("${tableName}");
|
|
237
|
+
${indexLines ? indexLines + "\n" : ""} },
|
|
238
|
+
|
|
239
|
+
async down(db) {
|
|
240
|
+
await db.collection("${tableName}").drop();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate a DynamoDB migration JS file for a single table.
|
|
248
|
+
*/
|
|
249
|
+
function generateDynamoDBMigration(tableName, tableDef) {
|
|
250
|
+
const pk = tableDef.pk;
|
|
251
|
+
return `import { CreateTableCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb";
|
|
252
|
+
|
|
253
|
+
export async function up(db) {
|
|
254
|
+
await db.send(new CreateTableCommand({
|
|
255
|
+
TableName: "${tableName}",
|
|
256
|
+
KeySchema: [{ AttributeName: "${pk}", KeyType: "HASH" }],
|
|
257
|
+
AttributeDefinitions: [{ AttributeName: "${pk}", AttributeType: "N" }],
|
|
258
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function down(db) {
|
|
263
|
+
await db.send(new DeleteTableCommand({ TableName: "${tableName}" }));
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Generate a Redis migration JS file (Redis doesn't need table creation,
|
|
270
|
+
* but we create a placeholder for consistency).
|
|
271
|
+
*/
|
|
272
|
+
function generateRedisMigration(tableName) {
|
|
273
|
+
return `"use strict";
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
async up(db) {
|
|
277
|
+
// Redis is schema-less. This migration is a placeholder.
|
|
278
|
+
// Data for "${tableName}" will be stored as hash keys: ${tableName}:<id>
|
|
279
|
+
console.log("Redis: ${tableName} collection ready (schema-less).");
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async down(db) {
|
|
283
|
+
// Warning: this deletes ALL keys matching the pattern
|
|
284
|
+
// In production, use SCAN instead of KEYS
|
|
285
|
+
const keys = await db.keys("${tableName}:*");
|
|
286
|
+
if (keys.length > 0) {
|
|
287
|
+
await db.del(...keys);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Public API
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
const SQL_ADAPTERS = [
|
|
299
|
+
"mysql",
|
|
300
|
+
"mariadb",
|
|
301
|
+
"postgres",
|
|
302
|
+
"sqlite3",
|
|
303
|
+
"mssql",
|
|
304
|
+
"cockroachdb",
|
|
305
|
+
"oracle",
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Generate migration file content for all tables in the schema.
|
|
310
|
+
*
|
|
311
|
+
* For SQL adapters: produces a single .sql file with all CREATE TABLE statements.
|
|
312
|
+
* For NoSQL adapters: produces one .js file per table.
|
|
313
|
+
*
|
|
314
|
+
* @param {object} schema - parsed schema from parseSchema()
|
|
315
|
+
* @returns {Array<{ filename: string, content: string }>}
|
|
316
|
+
*/
|
|
317
|
+
function generateMigrationFiles(schema) {
|
|
318
|
+
const adapter = schema.adapter;
|
|
319
|
+
const tables = schema.tables;
|
|
320
|
+
const tableNames = Object.keys(tables).sort();
|
|
321
|
+
|
|
322
|
+
if (SQL_ADAPTERS.includes(adapter)) {
|
|
323
|
+
// Single SQL file with all CREATE TABLE statements
|
|
324
|
+
const statements = [];
|
|
325
|
+
for (const name of tableNames) {
|
|
326
|
+
statements.push(generateCreateTableSQL(name, tables[name], adapter));
|
|
327
|
+
}
|
|
328
|
+
const content = statements.join("\n");
|
|
329
|
+
return [{ filename: `create_tables.sql`, content }];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// NoSQL: one file per table
|
|
333
|
+
const files = [];
|
|
334
|
+
for (const name of tableNames) {
|
|
335
|
+
let content;
|
|
336
|
+
if (adapter === "mongodb") {
|
|
337
|
+
content = generateMongoDBMigration(name, tables[name]);
|
|
338
|
+
} else if (adapter === "dynamodb") {
|
|
339
|
+
content = generateDynamoDBMigration(name, tables[name]);
|
|
340
|
+
} else if (adapter === "redis") {
|
|
341
|
+
content = generateRedisMigration(name);
|
|
342
|
+
} else {
|
|
343
|
+
// Fallback
|
|
344
|
+
content = `// Migration for ${name}\n`;
|
|
345
|
+
}
|
|
346
|
+
files.push({ filename: `create_${name}.js`, content });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return files;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
generateMigrationFiles,
|
|
354
|
+
generateCreateTableSQL,
|
|
355
|
+
mapColumnType,
|
|
356
|
+
};
|
|
@@ -475,7 +475,7 @@ function mysqlTypeToValidator(t) {
|
|
|
475
475
|
if (/json/.test(t)) return "object";
|
|
476
476
|
if (/text|char|varchar|enum|set/.test(t)) return "string";
|
|
477
477
|
if (/blob|binary/.test(t)) return "string";
|
|
478
|
-
if (/date|time|year/.test(t)) return "
|
|
478
|
+
if (/date|time|year/.test(t)) return "datetime";
|
|
479
479
|
if (/bool/.test(t)) return "integer";
|
|
480
480
|
return "string";
|
|
481
481
|
}
|
|
@@ -487,7 +487,7 @@ function pgTypeToValidator(t) {
|
|
|
487
487
|
if (/json/.test(t)) return "object";
|
|
488
488
|
if (/bool/.test(t)) return "integer";
|
|
489
489
|
if (/char|text|varchar|uuid/.test(t)) return "string";
|
|
490
|
-
if (/date|time|interval/.test(t)) return "
|
|
490
|
+
if (/date|time|interval/.test(t)) return "datetime";
|
|
491
491
|
return "string";
|
|
492
492
|
}
|
|
493
493
|
|
|
@@ -496,6 +496,7 @@ function sqliteTypeToValidator(t) {
|
|
|
496
496
|
if (/int/.test(t)) return "integer";
|
|
497
497
|
if (/real|float|double|numeric|decimal/.test(t)) return "numeric";
|
|
498
498
|
if (/json/.test(t)) return "object";
|
|
499
|
+
if (/date|time/.test(t)) return "datetime";
|
|
499
500
|
if (/blob/.test(t)) return "string";
|
|
500
501
|
return "string";
|
|
501
502
|
}
|
|
@@ -506,7 +507,7 @@ function mssqlTypeToValidator(t) {
|
|
|
506
507
|
if (/decimal|numeric|float|real|money/.test(t)) return "numeric";
|
|
507
508
|
if (/bit/.test(t)) return "integer";
|
|
508
509
|
if (/char|text|varchar|nchar|nvarchar|ntext/.test(t)) return "string";
|
|
509
|
-
if (/date|time|datetime/.test(t)) return "
|
|
510
|
+
if (/date|time|datetime/.test(t)) return "datetime";
|
|
510
511
|
if (/uniqueidentifier/.test(t)) return "string";
|
|
511
512
|
return "string";
|
|
512
513
|
}
|
|
@@ -515,7 +516,7 @@ function oracleTypeToValidator(t) {
|
|
|
515
516
|
if (/NUMBER|INTEGER|FLOAT|BINARY_FLOAT|BINARY_DOUBLE/.test(t))
|
|
516
517
|
return "numeric";
|
|
517
518
|
if (/CLOB|BLOB|RAW|LONG/.test(t)) return "string";
|
|
518
|
-
if (/DATE|TIMESTAMP/.test(t)) return "
|
|
519
|
+
if (/DATE|TIMESTAMP/.test(t)) return "datetime";
|
|
519
520
|
if (/CHAR|VARCHAR|NCHAR|NVARCHAR/.test(t)) return "string";
|
|
520
521
|
return "string";
|
|
521
522
|
}
|