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/init/prompt.js
CHANGED
|
@@ -5,6 +5,7 @@ const inquirer = require("inquirer");
|
|
|
5
5
|
const VALID_FRAMEWORKS = ["ultimate-express", "express"];
|
|
6
6
|
const VALID_DATABASES = [
|
|
7
7
|
"mysql",
|
|
8
|
+
"mariadb",
|
|
8
9
|
"postgres",
|
|
9
10
|
"sqlite3",
|
|
10
11
|
"mongodb",
|
|
@@ -56,12 +57,17 @@ function parseInitArgs(argv) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// Boolean flags
|
|
59
|
-
for (const flag of ["rateLimiting", "helmet", "logger"]) {
|
|
60
|
+
for (const flag of ["rateLimiting", "helmet", "logger", "loki"]) {
|
|
60
61
|
if (args[flag] !== undefined) {
|
|
61
62
|
partial[flag] = parseBool(args[flag]);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// Output directory
|
|
67
|
+
if (args.output !== undefined && args.output !== "true") {
|
|
68
|
+
partial.output = args.output;
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
return partial;
|
|
66
72
|
}
|
|
67
73
|
|
|
@@ -114,12 +120,22 @@ async function promptUser(prefilledAnswers) {
|
|
|
114
120
|
});
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
if (prefilled.output === undefined) {
|
|
124
|
+
questions.push({
|
|
125
|
+
type: "input",
|
|
126
|
+
name: "output",
|
|
127
|
+
message:
|
|
128
|
+
"Output directory for backend source files (leave empty for root):",
|
|
129
|
+
default: "",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
117
133
|
if (prefilled.rateLimiting === undefined) {
|
|
118
134
|
questions.push({
|
|
119
135
|
type: "confirm",
|
|
120
136
|
name: "rateLimiting",
|
|
121
137
|
message: "Enable rate limiting?",
|
|
122
|
-
default:
|
|
138
|
+
default: true,
|
|
123
139
|
});
|
|
124
140
|
}
|
|
125
141
|
|
|
@@ -128,7 +144,7 @@ async function promptUser(prefilledAnswers) {
|
|
|
128
144
|
type: "confirm",
|
|
129
145
|
name: "helmet",
|
|
130
146
|
message: "Enable Helmet security headers?",
|
|
131
|
-
default:
|
|
147
|
+
default: true,
|
|
132
148
|
});
|
|
133
149
|
}
|
|
134
150
|
|
|
@@ -136,8 +152,8 @@ async function promptUser(prefilledAnswers) {
|
|
|
136
152
|
questions.push({
|
|
137
153
|
type: "confirm",
|
|
138
154
|
name: "logger",
|
|
139
|
-
message: "Enable request/response logger?",
|
|
140
|
-
default:
|
|
155
|
+
message: "Enable request/response logger (Winston)?",
|
|
156
|
+
default: true,
|
|
141
157
|
});
|
|
142
158
|
}
|
|
143
159
|
|
|
@@ -147,6 +163,22 @@ async function promptUser(prefilledAnswers) {
|
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
const prompted = await inquirer.prompt(questions);
|
|
166
|
+
|
|
167
|
+
// Follow-up: if logger is enabled, ask about Loki
|
|
168
|
+
if (prompted.logger && prefilled.loki === undefined) {
|
|
169
|
+
const lokiAnswer = await inquirer.prompt([
|
|
170
|
+
{
|
|
171
|
+
type: "confirm",
|
|
172
|
+
name: "loki",
|
|
173
|
+
message: "Send logs to Grafana Loki?",
|
|
174
|
+
default: false,
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
prompted.loki = lokiAnswer.loki;
|
|
178
|
+
} else if (!prompted.logger && prefilled.logger === undefined) {
|
|
179
|
+
prompted.loki = false;
|
|
180
|
+
}
|
|
181
|
+
|
|
150
182
|
return Object.assign({}, prefilled, prompted);
|
|
151
183
|
}
|
|
152
184
|
|
package/src/cli/init.js
CHANGED
|
@@ -7,14 +7,26 @@ const { execSync } = require("child_process");
|
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
9
|
generateAppJs,
|
|
10
|
+
generateAppJsV2,
|
|
10
11
|
generateEnvFile,
|
|
11
12
|
generateEnvExample,
|
|
12
13
|
generateLoggerMiddleware,
|
|
13
|
-
generateMigrateScript,
|
|
14
|
-
generateAddMigrationScript,
|
|
15
14
|
generateInitialMigration,
|
|
16
15
|
generateSessionMigration,
|
|
17
16
|
generateGitignore,
|
|
17
|
+
generateDockerfile,
|
|
18
|
+
generateDockerignore,
|
|
19
|
+
generateGrafanaDatasources,
|
|
20
|
+
generateDockerCompose,
|
|
21
|
+
generateCloudBeaverDataSources,
|
|
22
|
+
generateSessionJs,
|
|
23
|
+
generateMigrateModule,
|
|
24
|
+
generateAddMigrationModule,
|
|
25
|
+
generateSecurityJs,
|
|
26
|
+
generateHealthRoute,
|
|
27
|
+
generateRouteIndexFile,
|
|
28
|
+
generateDbModule,
|
|
29
|
+
randomPassword,
|
|
18
30
|
} = require("./init/generators");
|
|
19
31
|
|
|
20
32
|
const { collectDependencies, getScripts } = require("./init/dependencies");
|
|
@@ -61,40 +73,139 @@ function safeWriteFile(filePath, content) {
|
|
|
61
73
|
* Creates directories and writes files. Skips files that already exist.
|
|
62
74
|
* Returns the list of generated filenames for the summary.
|
|
63
75
|
* @param {import('./init/types').InitAnswers} answers
|
|
76
|
+
* @param {string} [outputDir] - relative directory for source files (e.g. "backend")
|
|
64
77
|
* @returns {{ files: string[], migrationFiles: string[] }}
|
|
65
78
|
*/
|
|
66
|
-
function generateFiles(answers) {
|
|
79
|
+
function generateFiles(answers, outputDir) {
|
|
67
80
|
const files = [];
|
|
68
81
|
const migrationFiles = [];
|
|
69
82
|
|
|
83
|
+
// Resolve source directory (outputDir-relative or cwd)
|
|
84
|
+
const srcBase = outputDir || ".";
|
|
85
|
+
|
|
86
|
+
// Generate random secrets (shared between .env and docker-compose)
|
|
87
|
+
const secrets = {
|
|
88
|
+
dbPass: randomPassword(),
|
|
89
|
+
redisPass: answers.session === "redis" ? randomPassword() : "",
|
|
90
|
+
sessionSecret: randomPassword(32),
|
|
91
|
+
};
|
|
92
|
+
|
|
70
93
|
// Create directories
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
const dirs = [
|
|
95
|
+
path.join(srcBase, "middleware"),
|
|
96
|
+
path.join(srcBase, "migrations"),
|
|
97
|
+
path.join(srcBase, "commons"),
|
|
98
|
+
path.join(srcBase, "routes"),
|
|
99
|
+
];
|
|
100
|
+
// SQLite3 needs a data/ folder for the database file
|
|
101
|
+
if (answers.database === "sqlite3") {
|
|
102
|
+
dirs.push("data");
|
|
73
103
|
}
|
|
74
|
-
|
|
75
|
-
fs.
|
|
104
|
+
for (const dir of dirs) {
|
|
105
|
+
if (!fs.existsSync(dir)) {
|
|
106
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
107
|
+
}
|
|
76
108
|
}
|
|
77
109
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
if (safeWriteFile(".
|
|
110
|
+
// Root-level files (always in cwd, not in outputDir)
|
|
111
|
+
// app.js uses the v2 generator that links commons/route modules
|
|
112
|
+
if (safeWriteFile("app.js", generateAppJsV2(answers, outputDir || "")))
|
|
113
|
+
files.push("app.js");
|
|
114
|
+
if (safeWriteFile(".env", generateEnvFile(answers, secrets)))
|
|
115
|
+
files.push(".env");
|
|
81
116
|
if (safeWriteFile(".env.example", generateEnvExample(answers)))
|
|
82
117
|
files.push(".env.example");
|
|
83
|
-
|
|
84
|
-
const loggerPath = path.join("middleware", "logger.js");
|
|
85
|
-
if (safeWriteFile(loggerPath, generateLoggerMiddleware(answers)))
|
|
86
|
-
files.push("middleware/logger.js");
|
|
87
|
-
|
|
88
|
-
if (safeWriteFile("migrate.js", generateMigrateScript(answers)))
|
|
89
|
-
files.push("migrate.js");
|
|
90
|
-
if (safeWriteFile("add_migration.js", generateAddMigrationScript(answers)))
|
|
91
|
-
files.push("add_migration.js");
|
|
92
118
|
if (safeWriteFile(".gitignore", generateGitignore()))
|
|
93
119
|
files.push(".gitignore");
|
|
94
120
|
|
|
95
|
-
//
|
|
121
|
+
// docker-compose.yml (if the database needs Docker)
|
|
122
|
+
const dockerCompose = generateDockerCompose(answers, secrets);
|
|
123
|
+
if (dockerCompose !== null) {
|
|
124
|
+
if (safeWriteFile("docker-compose.yml", dockerCompose))
|
|
125
|
+
files.push("docker-compose.yml");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// CloudBeaver data-sources.json (auto-connect config)
|
|
129
|
+
const cbDataSources = generateCloudBeaverDataSources(answers, secrets);
|
|
130
|
+
if (cbDataSources !== null) {
|
|
131
|
+
const cbDir = ".cloudbeaver";
|
|
132
|
+
if (!fs.existsSync(cbDir)) {
|
|
133
|
+
fs.mkdirSync(cbDir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
const cbPath = path.join(cbDir, "data-sources.json");
|
|
136
|
+
if (safeWriteFile(cbPath, cbDataSources))
|
|
137
|
+
files.push(".cloudbeaver/data-sources.json");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Grafana datasource provisioning (when loki is enabled)
|
|
141
|
+
if (answers.loki) {
|
|
142
|
+
const grafanaDir = ".grafana";
|
|
143
|
+
if (!fs.existsSync(grafanaDir)) {
|
|
144
|
+
fs.mkdirSync(grafanaDir, { recursive: true });
|
|
145
|
+
}
|
|
146
|
+
const grafanaPath = path.join(grafanaDir, "datasources.yml");
|
|
147
|
+
if (safeWriteFile(grafanaPath, generateGrafanaDatasources()))
|
|
148
|
+
files.push(".grafana/datasources.yml");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Dockerfile and .dockerignore
|
|
152
|
+
if (safeWriteFile("Dockerfile", generateDockerfile(answers, outputDir)))
|
|
153
|
+
files.push("Dockerfile");
|
|
154
|
+
if (safeWriteFile(".dockerignore", generateDockerignore()))
|
|
155
|
+
files.push(".dockerignore");
|
|
156
|
+
|
|
157
|
+
// Source files inside outputDir (or cwd if no outputDir)
|
|
158
|
+
const loggerPath = path.join(srcBase, "middleware", "logger.js");
|
|
159
|
+
if (safeWriteFile(loggerPath, generateLoggerMiddleware(answers)))
|
|
160
|
+
files.push(path.join(srcBase, "middleware/logger.js"));
|
|
161
|
+
|
|
162
|
+
// commons/session.js
|
|
163
|
+
const sessionPath = path.join(srcBase, "commons", "session.js");
|
|
164
|
+
if (safeWriteFile(sessionPath, generateSessionJs(answers)))
|
|
165
|
+
files.push(path.join(srcBase, "commons/session.js"));
|
|
166
|
+
|
|
167
|
+
// commons/migrate.js
|
|
168
|
+
const migratePath = path.join(srcBase, "commons", "migrate.js");
|
|
169
|
+
if (safeWriteFile(migratePath, generateMigrateModule(answers, outputDir)))
|
|
170
|
+
files.push(path.join(srcBase, "commons/migrate.js"));
|
|
171
|
+
|
|
172
|
+
// commons/add_migration.js
|
|
173
|
+
const addMigrationPath = path.join(srcBase, "commons", "add_migration.js");
|
|
174
|
+
if (
|
|
175
|
+
safeWriteFile(
|
|
176
|
+
addMigrationPath,
|
|
177
|
+
generateAddMigrationModule(answers, outputDir),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
files.push(path.join(srcBase, "commons/add_migration.js"));
|
|
181
|
+
|
|
182
|
+
// commons/security.js
|
|
183
|
+
const securityPath = path.join(srcBase, "commons", "security.js");
|
|
184
|
+
if (safeWriteFile(securityPath, generateSecurityJs(answers)))
|
|
185
|
+
files.push(path.join(srcBase, "commons/security.js"));
|
|
186
|
+
|
|
187
|
+
// commons/db.js
|
|
188
|
+
const dbPath = path.join(srcBase, "commons", "db.js");
|
|
189
|
+
if (safeWriteFile(dbPath, generateDbModule(answers)))
|
|
190
|
+
files.push(path.join(srcBase, "commons/db.js"));
|
|
191
|
+
|
|
192
|
+
// routes/health.js
|
|
193
|
+
const healthPath = path.join(srcBase, "routes", "health.js");
|
|
194
|
+
if (safeWriteFile(healthPath, generateHealthRoute()))
|
|
195
|
+
files.push(path.join(srcBase, "routes/health.js"));
|
|
196
|
+
|
|
197
|
+
// routes/index.js
|
|
198
|
+
const routeIndexPath = path.join(srcBase, "routes", "index.js");
|
|
199
|
+
if (safeWriteFile(routeIndexPath, generateRouteIndexFile()))
|
|
200
|
+
files.push(path.join(srcBase, "routes/index.js"));
|
|
201
|
+
|
|
202
|
+
// Initial migration (inside outputDir/migrations)
|
|
96
203
|
const initialMigration = generateInitialMigration(answers);
|
|
97
|
-
const initialPath = path.join(
|
|
204
|
+
const initialPath = path.join(
|
|
205
|
+
srcBase,
|
|
206
|
+
"migrations",
|
|
207
|
+
initialMigration.filename,
|
|
208
|
+
);
|
|
98
209
|
if (safeWriteFile(initialPath, initialMigration.content)) {
|
|
99
210
|
migrationFiles.push(initialMigration.filename);
|
|
100
211
|
}
|
|
@@ -102,8 +213,12 @@ function generateFiles(answers) {
|
|
|
102
213
|
// Conditional session migration
|
|
103
214
|
const sessionMigration = generateSessionMigration(answers);
|
|
104
215
|
if (sessionMigration !== null) {
|
|
105
|
-
const
|
|
106
|
-
|
|
216
|
+
const sessionMigPath = path.join(
|
|
217
|
+
srcBase,
|
|
218
|
+
"migrations",
|
|
219
|
+
sessionMigration.filename,
|
|
220
|
+
);
|
|
221
|
+
if (safeWriteFile(sessionMigPath, sessionMigration.content)) {
|
|
107
222
|
migrationFiles.push(sessionMigration.filename);
|
|
108
223
|
}
|
|
109
224
|
}
|
|
@@ -114,8 +229,9 @@ function generateFiles(answers) {
|
|
|
114
229
|
/**
|
|
115
230
|
* Update package.json with scripts and dependencies from the answers.
|
|
116
231
|
* @param {import('./init/types').InitAnswers} answers
|
|
232
|
+
* @param {string} [outputDir] - relative output directory for source files
|
|
117
233
|
*/
|
|
118
|
-
function updatePackageJson(answers) {
|
|
234
|
+
function updatePackageJson(answers, outputDir) {
|
|
119
235
|
let raw;
|
|
120
236
|
try {
|
|
121
237
|
raw = fs.readFileSync("package.json", "utf8");
|
|
@@ -135,8 +251,9 @@ function updatePackageJson(answers) {
|
|
|
135
251
|
}
|
|
136
252
|
|
|
137
253
|
const { dependencies, devDependencies } = collectDependencies(answers);
|
|
138
|
-
const scripts = getScripts();
|
|
254
|
+
const scripts = getScripts(outputDir);
|
|
139
255
|
|
|
256
|
+
pkg.type = "module";
|
|
140
257
|
pkg.scripts = Object.assign({}, pkg.scripts || {}, scripts);
|
|
141
258
|
pkg.dependencies = Object.assign({}, pkg.dependencies || {}, dependencies);
|
|
142
259
|
pkg.devDependencies = Object.assign(
|
|
@@ -247,6 +364,9 @@ Options:
|
|
|
247
364
|
cockroachdb, oracle, redis, dynamodb
|
|
248
365
|
--db <name> Alias for --database
|
|
249
366
|
--session <type> Session store: memory, redis, database
|
|
367
|
+
--output <dir> Directory for backend source files (e.g. --output backend).
|
|
368
|
+
package.json stays in root; index.js, commons/, routes/,
|
|
369
|
+
middleware/, and migrations/ go inside the output folder.
|
|
250
370
|
--rateLimiting Enable rate limiting (express-rate-limit)
|
|
251
371
|
--helmet Enable Helmet security headers
|
|
252
372
|
--logger Enable request/response logger (express-mung)
|
|
@@ -256,6 +376,9 @@ Examples:
|
|
|
256
376
|
# Fully non-interactive (LLM-friendly)
|
|
257
377
|
db-model-router-init --framework express --database postgres --session redis --rateLimiting --helmet --logger
|
|
258
378
|
|
|
379
|
+
# With output directory
|
|
380
|
+
db-model-router-init --framework express --database postgres --output backend --yes
|
|
381
|
+
|
|
259
382
|
# Partial — only prompts for missing values
|
|
260
383
|
db-model-router-init --database mysql --session memory
|
|
261
384
|
|
package/src/cli/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { parseFlags, OutputContext } = require("./flags");
|
|
@@ -8,6 +8,7 @@ const inspectCmd = require("./commands/inspect");
|
|
|
8
8
|
const generateCmd = require("./commands/generate");
|
|
9
9
|
const doctorCmd = require("./commands/doctor");
|
|
10
10
|
const diffCmd = require("./commands/diff");
|
|
11
|
+
const helpCmd = require("./commands/help");
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Map of subcommand names to their handler functions.
|
|
@@ -18,6 +19,7 @@ const COMMANDS = {
|
|
|
18
19
|
generate: generateCmd,
|
|
19
20
|
doctor: doctorCmd,
|
|
20
21
|
diff: diffCmd,
|
|
22
|
+
help: helpCmd,
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -29,23 +31,74 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
29
31
|
generate: "Generate models, routes, tests, and OpenAPI spec from a schema",
|
|
30
32
|
doctor: "Validate schema, check dependencies, and verify file sync",
|
|
31
33
|
diff: "Preview changes between current files and what the schema would produce",
|
|
34
|
+
help: "Show help for a command",
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
*
|
|
38
|
+
* Per-command flag summaries shown in the general help overview.
|
|
39
|
+
*/
|
|
40
|
+
const COMMAND_FLAGS = {
|
|
41
|
+
init: [
|
|
42
|
+
["--from <path>", "Read config from a schema file"],
|
|
43
|
+
["--framework <name>", "express, ultimate-express"],
|
|
44
|
+
[
|
|
45
|
+
"--database <name>",
|
|
46
|
+
"mysql, mariadb, postgres, sqlite3, mongodb, mssql, cockroachdb, oracle, redis, dynamodb",
|
|
47
|
+
],
|
|
48
|
+
["--db <name>", "Alias for --database"],
|
|
49
|
+
["--session <type>", "memory, redis, database"],
|
|
50
|
+
["--output <dir>", "Directory for backend source files"],
|
|
51
|
+
["--rateLimiting", "Enable rate limiting (default: yes)"],
|
|
52
|
+
["--helmet", "Enable Helmet security headers (default: yes)"],
|
|
53
|
+
["--logger", "Enable Winston + Loki logger (default: yes)"],
|
|
54
|
+
],
|
|
55
|
+
inspect: [
|
|
56
|
+
["--type <adapter>", "Database adapter (required)"],
|
|
57
|
+
["--env <path>", "Path to .env file"],
|
|
58
|
+
["--out <path>", "Output file (default: dbmr.schema.json)"],
|
|
59
|
+
["--tables <list>", "Comma-separated table filter"],
|
|
60
|
+
],
|
|
61
|
+
generate: [
|
|
62
|
+
["--from <path>", "Schema file (default: dbmr.schema.json)"],
|
|
63
|
+
["--models", "Generate only model files"],
|
|
64
|
+
["--routes", "Generate only route files"],
|
|
65
|
+
["--openapi", "Generate only OpenAPI spec"],
|
|
66
|
+
["--tests", "Generate only test files"],
|
|
67
|
+
["--llm-docs", "Generate only LLM documentation"],
|
|
68
|
+
],
|
|
69
|
+
doctor: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
|
|
70
|
+
diff: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Print help message listing all available subcommands with their flags.
|
|
36
75
|
*/
|
|
37
76
|
function printHelp() {
|
|
38
77
|
console.log("Usage: db-model-router <command> [options]\n");
|
|
39
|
-
|
|
78
|
+
|
|
79
|
+
console.log("Commands:\n");
|
|
40
80
|
for (const [name, desc] of Object.entries(COMMAND_DESCRIPTIONS)) {
|
|
81
|
+
if (name === "help") {
|
|
82
|
+
console.log(` ${name.padEnd(12)} ${desc}`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
41
85
|
console.log(` ${name.padEnd(12)} ${desc}`);
|
|
86
|
+
const flags = COMMAND_FLAGS[name];
|
|
87
|
+
if (flags) {
|
|
88
|
+
for (const [flag, info] of flags) {
|
|
89
|
+
console.log(` ${flag.padEnd(22)} ${info}`);
|
|
90
|
+
}
|
|
91
|
+
console.log("");
|
|
92
|
+
}
|
|
42
93
|
}
|
|
43
|
-
|
|
44
|
-
console.log("
|
|
45
|
-
console.log(" --
|
|
46
|
-
console.log(" --
|
|
47
|
-
console.log(" --
|
|
48
|
-
console.log(" --
|
|
94
|
+
|
|
95
|
+
console.log("Global flags (all commands):");
|
|
96
|
+
console.log(" --yes Accept all defaults without prompting");
|
|
97
|
+
console.log(" --json Output machine-readable JSON");
|
|
98
|
+
console.log(" --dry-run Preview actions without side effects");
|
|
99
|
+
console.log(" --no-install Skip npm install step");
|
|
100
|
+
console.log(" --help Show help for a command");
|
|
101
|
+
console.log('\nRun "db-model-router help <command>" for detailed usage.');
|
|
49
102
|
}
|
|
50
103
|
|
|
51
104
|
/**
|
|
@@ -65,11 +118,38 @@ function printUnknown(cmd) {
|
|
|
65
118
|
async function main(argv) {
|
|
66
119
|
const { subcommand, flags, args } = parseFlags(argv);
|
|
67
120
|
|
|
68
|
-
if (!subcommand
|
|
121
|
+
if (!subcommand) {
|
|
69
122
|
printHelp();
|
|
70
123
|
return;
|
|
71
124
|
}
|
|
72
125
|
|
|
126
|
+
// `help <command>` — extract the topic from the second positional arg
|
|
127
|
+
if (subcommand === "help") {
|
|
128
|
+
// Re-parse to grab the second positional word as the help topic.
|
|
129
|
+
// parseFlags puts the first positional into subcommand; the second
|
|
130
|
+
// positional ends up as a key in args (if it looks like a flag value)
|
|
131
|
+
// or is lost. So we grab it directly from argv.
|
|
132
|
+
const topic =
|
|
133
|
+
argv.find((a, i) => i > 0 && !a.startsWith("-") && argv[0] === "help") ||
|
|
134
|
+
argv.find(
|
|
135
|
+
(a, i) => i > 0 && !a.startsWith("-") && argv.indexOf("help") < i,
|
|
136
|
+
);
|
|
137
|
+
args._command = topic || null;
|
|
138
|
+
const ctx = new OutputContext(flags);
|
|
139
|
+
await helpCmd(args, flags, ctx, { printHelp });
|
|
140
|
+
ctx.flush();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// `<command> --help` — show detailed help for that command
|
|
145
|
+
if (flags.help) {
|
|
146
|
+
args._command = subcommand;
|
|
147
|
+
const ctx = new OutputContext(flags);
|
|
148
|
+
await helpCmd(args, flags, ctx, { printHelp });
|
|
149
|
+
ctx.flush();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
73
153
|
if (!COMMANDS[subcommand]) {
|
|
74
154
|
printUnknown(subcommand);
|
|
75
155
|
process.exitCode = 1;
|
package/src/cockroachdb/db.js
CHANGED
|
@@ -371,22 +371,30 @@ async function insert(table, data, uniqueKeys = []) {
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
// Bulk insert via multi-row VALUES
|
|
374
|
+
// Bulk insert via multi-row VALUES in batches of 1000
|
|
375
|
+
const BATCH_SIZE = 1000;
|
|
376
|
+
const colList = columns.join(",");
|
|
375
377
|
const client = await pool.connect();
|
|
376
378
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
379
|
+
await client.query("BEGIN");
|
|
380
|
+
for (let offset = 0; offset < total; offset += BATCH_SIZE) {
|
|
381
|
+
const batch = array.slice(offset, offset + BATCH_SIZE);
|
|
382
|
+
let paramIdx = 0;
|
|
383
|
+
const allParams = [];
|
|
384
|
+
const valuesClauses = batch.map((row) => {
|
|
385
|
+
const placeholders = columns.map((c) => {
|
|
386
|
+
paramIdx++;
|
|
387
|
+
allParams.push(sanitizeValue(row[c]));
|
|
388
|
+
return "$" + paramIdx;
|
|
389
|
+
});
|
|
390
|
+
return "(" + placeholders.join(",") + ")";
|
|
384
391
|
});
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
await client.query(
|
|
392
|
+
const sql = `INSERT INTO ${table} (${colList}) VALUES ${valuesClauses.join(",")}`;
|
|
393
|
+
await client.query(sql, allParams);
|
|
394
|
+
}
|
|
395
|
+
await client.query("COMMIT");
|
|
389
396
|
} catch (e) {
|
|
397
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
390
398
|
throw mapPgError(e);
|
|
391
399
|
} finally {
|
|
392
400
|
client.release();
|
|
@@ -407,35 +415,40 @@ async function _insertOnConflict(table, array, columns, uniqueKeys, total) {
|
|
|
407
415
|
let lastId = 0;
|
|
408
416
|
const pk = await getPkColumn(table);
|
|
409
417
|
const conflictCols = uniqueKeys.join(",");
|
|
418
|
+
const colList = columns.join(",");
|
|
419
|
+
const BATCH_SIZE = 1000;
|
|
410
420
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
421
|
+
const client = await pool.connect();
|
|
422
|
+
try {
|
|
423
|
+
await client.query("BEGIN");
|
|
424
|
+
|
|
425
|
+
for (let offset = 0; offset < total; offset += BATCH_SIZE) {
|
|
426
|
+
const batch = array.slice(offset, offset + BATCH_SIZE);
|
|
427
|
+
let paramIdx = 0;
|
|
428
|
+
const allParams = [];
|
|
429
|
+
const valuesClauses = batch.map((row) => {
|
|
430
|
+
const placeholders = columns.map((c) => {
|
|
431
|
+
paramIdx++;
|
|
432
|
+
allParams.push(sanitizeValue(row[c]));
|
|
433
|
+
return "$" + paramIdx;
|
|
434
|
+
});
|
|
435
|
+
return "(" + placeholders.join(",") + ")";
|
|
436
|
+
});
|
|
437
|
+
let sql = `INSERT INTO ${table} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols}) DO NOTHING`;
|
|
438
|
+
if (pk) sql += ` RETURNING ${pk}`;
|
|
416
439
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (result.rows && result.rows.length > 0 && pk) {
|
|
421
|
-
lastId = result.rows[0][pk] || 0;
|
|
422
|
-
} else if (total === 1 && pk) {
|
|
423
|
-
// Row already existed, fetch its PK
|
|
424
|
-
const whereClauses = uniqueKeys
|
|
425
|
-
.map((k, i) => `${k} = $${i + 1}`)
|
|
426
|
-
.join(" AND ");
|
|
427
|
-
const whereVals = uniqueKeys.map((k) => row[k]);
|
|
428
|
-
const fetched = await client.query(
|
|
429
|
-
`SELECT ${pk} FROM ${table} WHERE ${whereClauses}`,
|
|
430
|
-
whereVals,
|
|
431
|
-
);
|
|
432
|
-
if (fetched.rows.length > 0) lastId = fetched.rows[0][pk] || 0;
|
|
440
|
+
const result = await client.query(sql, allParams);
|
|
441
|
+
if (pk && result.rows && result.rows.length > 0) {
|
|
442
|
+
lastId = result.rows[result.rows.length - 1][pk] || lastId;
|
|
433
443
|
}
|
|
434
|
-
} catch (e) {
|
|
435
|
-
throw mapPgError(e);
|
|
436
|
-
} finally {
|
|
437
|
-
client.release();
|
|
438
444
|
}
|
|
445
|
+
|
|
446
|
+
await client.query("COMMIT");
|
|
447
|
+
} catch (e) {
|
|
448
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
449
|
+
throw mapPgError(e);
|
|
450
|
+
} finally {
|
|
451
|
+
client.release();
|
|
439
452
|
}
|
|
440
453
|
|
|
441
454
|
return {
|
|
@@ -471,13 +484,13 @@ async function upsert(table, data, uniqueKeys = []) {
|
|
|
471
484
|
const updateCols = columns.filter((c) => c !== keyCol);
|
|
472
485
|
if (updateCols.length === 0) continue;
|
|
473
486
|
const setClause = updateCols
|
|
474
|
-
.map((c, i) => `${c} =
|
|
487
|
+
.map((c, i) => `${c} = ${i + 1}`)
|
|
475
488
|
.join(", ");
|
|
476
489
|
const vals = [
|
|
477
490
|
...updateCols.map((c) => sanitizeValue(row[c])),
|
|
478
491
|
row[keyCol],
|
|
479
492
|
];
|
|
480
|
-
const sql = `UPDATE ${table} SET ${setClause} WHERE ${keyCol} =
|
|
493
|
+
const sql = `UPDATE ${table} SET ${setClause} WHERE ${keyCol} = ${updateCols.length + 1}`;
|
|
481
494
|
await query(sql, vals);
|
|
482
495
|
if (row[keyCol]) lastId = row[keyCol];
|
|
483
496
|
}
|
|
@@ -497,34 +510,51 @@ async function upsert(table, data, uniqueKeys = []) {
|
|
|
497
510
|
const nonUniqueColumns = columns.filter((c) => !uniqueKeys.includes(c));
|
|
498
511
|
const pk = await getPkColumn(table);
|
|
499
512
|
let lastId = 0;
|
|
513
|
+
const BATCH_SIZE = 1000;
|
|
514
|
+
|
|
515
|
+
const client = await pool.connect();
|
|
516
|
+
try {
|
|
517
|
+
await client.query("BEGIN");
|
|
500
518
|
|
|
501
|
-
for (const row of array) {
|
|
502
|
-
const vals = columns.map((c) => sanitizeValue(row[c]));
|
|
503
|
-
const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
|
|
504
|
-
const conflictCols = uniqueKeys.join(",");
|
|
505
519
|
const updateSetSql = nonUniqueColumns
|
|
506
520
|
.map((c) => `${c} = EXCLUDED.${c}`)
|
|
507
521
|
.join(", ");
|
|
522
|
+
const conflictCols = uniqueKeys.join(",");
|
|
523
|
+
const colList = columns.join(",");
|
|
524
|
+
|
|
525
|
+
for (let offset = 0; offset < total; offset += BATCH_SIZE) {
|
|
526
|
+
const batch = array.slice(offset, offset + BATCH_SIZE);
|
|
527
|
+
let paramIdx = 0;
|
|
528
|
+
const allParams = [];
|
|
529
|
+
const valuesClauses = batch.map((row) => {
|
|
530
|
+
const placeholders = columns.map((c) => {
|
|
531
|
+
paramIdx++;
|
|
532
|
+
allParams.push(sanitizeValue(row[c]));
|
|
533
|
+
return "$" + paramIdx;
|
|
534
|
+
});
|
|
535
|
+
return "(" + placeholders.join(",") + ")";
|
|
536
|
+
});
|
|
508
537
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
538
|
+
let sql = `INSERT INTO ${table} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols})`;
|
|
539
|
+
if (updateSetSql) {
|
|
540
|
+
sql += ` DO UPDATE SET ${updateSetSql}`;
|
|
541
|
+
} else {
|
|
542
|
+
sql += ` DO NOTHING`;
|
|
543
|
+
}
|
|
544
|
+
if (pk) sql += ` RETURNING ${pk}`;
|
|
516
545
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (result.rows && result.rows.length > 0 && pk) {
|
|
521
|
-
lastId = result.rows[0][pk] || 0;
|
|
546
|
+
const result = await client.query(sql, allParams);
|
|
547
|
+
if (pk && result.rows && result.rows.length > 0) {
|
|
548
|
+
lastId = result.rows[result.rows.length - 1][pk] || lastId;
|
|
522
549
|
}
|
|
523
|
-
} catch (e) {
|
|
524
|
-
throw mapPgError(e);
|
|
525
|
-
} finally {
|
|
526
|
-
client.release();
|
|
527
550
|
}
|
|
551
|
+
|
|
552
|
+
await client.query("COMMIT");
|
|
553
|
+
} catch (e) {
|
|
554
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
555
|
+
throw mapPgError(e);
|
|
556
|
+
} finally {
|
|
557
|
+
client.release();
|
|
528
558
|
}
|
|
529
559
|
|
|
530
560
|
const response = {
|
|
@@ -554,6 +584,7 @@ module.exports = {
|
|
|
554
584
|
query,
|
|
555
585
|
qcount,
|
|
556
586
|
remove,
|
|
587
|
+
delete: remove,
|
|
557
588
|
upsert,
|
|
558
589
|
change: upsert,
|
|
559
590
|
insert,
|