db-model-router 1.0.2 → 1.0.3
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 +153 -202
- package/docs/SKILL.md +194 -22
- package/docs/adapters/cockroachdb.md +1 -1
- package/docs/adapters/dynamodb.md +1 -1
- package/docs/adapters/mongodb.md +1 -1
- package/docs/adapters/mssql.md +1 -1
- package/docs/adapters/oracle.md +1 -1
- package/docs/adapters/postgres.md +1 -1
- package/docs/adapters/redis.md +1 -1
- package/docs/adapters/sqlite3.md +1 -1
- package/package.json +3 -5
- package/src/cli/commands/diff.js +114 -0
- package/src/cli/commands/doctor.js +181 -0
- package/src/cli/commands/generate-llm-docs.js +418 -0
- package/src/cli/commands/generate.js +240 -0
- package/src/cli/commands/init.js +153 -0
- package/src/cli/commands/inspect.js +205 -0
- package/src/cli/diff-engine.js +198 -0
- package/src/cli/flags.js +112 -0
- package/src/cli/generate-route.js +237 -2
- package/src/cli/init/dependencies.js +83 -0
- package/src/cli/init/generators.js +782 -0
- package/src/cli/init/prompt.js +159 -0
- package/src/cli/init.js +281 -0
- package/src/cli/main.js +95 -0
- package/src/commons/model.js +5 -6
- package/src/commons/route.js +24 -0
- package/src/schema/schema-parser.js +78 -0
- package/src/schema/schema-printer.js +81 -0
- package/src/schema/schema-to-meta.js +74 -0
- package/src/schema/schema-validator.js +253 -0
- package/src/serve.js +5 -3
- package/docs/README.md +0 -208
- package/src/cli/generate-app.js +0 -359
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const inquirer = require("inquirer");
|
|
4
|
+
|
|
5
|
+
const VALID_FRAMEWORKS = ["ultimate-express", "express"];
|
|
6
|
+
const VALID_DATABASES = [
|
|
7
|
+
"mysql",
|
|
8
|
+
"postgres",
|
|
9
|
+
"sqlite3",
|
|
10
|
+
"mongodb",
|
|
11
|
+
"mssql",
|
|
12
|
+
"cockroachdb",
|
|
13
|
+
"oracle",
|
|
14
|
+
"redis",
|
|
15
|
+
"dynamodb",
|
|
16
|
+
];
|
|
17
|
+
const VALID_SESSIONS = ["memory", "redis", "database"];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse CLI arguments into a partial answers object.
|
|
21
|
+
* Supports: --framework, --database, --session, --rateLimiting, --helmet, --logger
|
|
22
|
+
* Boolean flags accept: true/false, yes/no, 1/0, or just --flag (implies true)
|
|
23
|
+
* @param {string[]} argv
|
|
24
|
+
* @returns {Partial<import('./types').InitAnswers>}
|
|
25
|
+
*/
|
|
26
|
+
function parseInitArgs(argv) {
|
|
27
|
+
const args = {};
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const arg = argv[i];
|
|
30
|
+
if (arg.startsWith("--")) {
|
|
31
|
+
const key = arg.slice(2);
|
|
32
|
+
const next = argv[i + 1];
|
|
33
|
+
if (next && !next.startsWith("--")) {
|
|
34
|
+
args[key] = next;
|
|
35
|
+
i++;
|
|
36
|
+
} else {
|
|
37
|
+
args[key] = "true";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const partial = {};
|
|
43
|
+
|
|
44
|
+
if (args.framework && VALID_FRAMEWORKS.includes(args.framework)) {
|
|
45
|
+
partial.framework = args.framework;
|
|
46
|
+
}
|
|
47
|
+
if (args.database && VALID_DATABASES.includes(args.database)) {
|
|
48
|
+
partial.database = args.database;
|
|
49
|
+
}
|
|
50
|
+
// Accept --db as alias for --database
|
|
51
|
+
if (!partial.database && args.db && VALID_DATABASES.includes(args.db)) {
|
|
52
|
+
partial.database = args.db;
|
|
53
|
+
}
|
|
54
|
+
if (args.session && VALID_SESSIONS.includes(args.session)) {
|
|
55
|
+
partial.session = args.session;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Boolean flags
|
|
59
|
+
for (const flag of ["rateLimiting", "helmet", "logger"]) {
|
|
60
|
+
if (args[flag] !== undefined) {
|
|
61
|
+
partial[flag] = parseBool(args[flag]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return partial;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse a string as a boolean.
|
|
70
|
+
* @param {string} val
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
function parseBool(val) {
|
|
74
|
+
return ["true", "yes", "1"].includes(String(val).toLowerCase());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run the interactive prompt flow to collect user preferences.
|
|
79
|
+
* Any values already present in `prefilledAnswers` are skipped (not prompted).
|
|
80
|
+
* When all 6 values are provided, no prompts are shown at all.
|
|
81
|
+
* @param {Partial<import('./types').InitAnswers>} [prefilledAnswers={}]
|
|
82
|
+
* @returns {Promise<import('./types').InitAnswers>}
|
|
83
|
+
*/
|
|
84
|
+
async function promptUser(prefilledAnswers) {
|
|
85
|
+
const prefilled = prefilledAnswers || {};
|
|
86
|
+
|
|
87
|
+
const questions = [];
|
|
88
|
+
|
|
89
|
+
if (prefilled.framework === undefined) {
|
|
90
|
+
questions.push({
|
|
91
|
+
type: "list",
|
|
92
|
+
name: "framework",
|
|
93
|
+
message: "Select your Express framework:",
|
|
94
|
+
choices: VALID_FRAMEWORKS,
|
|
95
|
+
default: "ultimate-express",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (prefilled.database === undefined) {
|
|
100
|
+
questions.push({
|
|
101
|
+
type: "list",
|
|
102
|
+
name: "database",
|
|
103
|
+
message: "Select your database:",
|
|
104
|
+
choices: VALID_DATABASES,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (prefilled.session === undefined) {
|
|
109
|
+
questions.push({
|
|
110
|
+
type: "list",
|
|
111
|
+
name: "session",
|
|
112
|
+
message: "Select your session store:",
|
|
113
|
+
choices: VALID_SESSIONS,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (prefilled.rateLimiting === undefined) {
|
|
118
|
+
questions.push({
|
|
119
|
+
type: "confirm",
|
|
120
|
+
name: "rateLimiting",
|
|
121
|
+
message: "Enable rate limiting?",
|
|
122
|
+
default: false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (prefilled.helmet === undefined) {
|
|
127
|
+
questions.push({
|
|
128
|
+
type: "confirm",
|
|
129
|
+
name: "helmet",
|
|
130
|
+
message: "Enable Helmet security headers?",
|
|
131
|
+
default: false,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (prefilled.logger === undefined) {
|
|
136
|
+
questions.push({
|
|
137
|
+
type: "confirm",
|
|
138
|
+
name: "logger",
|
|
139
|
+
message: "Enable request/response logger?",
|
|
140
|
+
default: false,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// If all values are prefilled, skip prompts entirely
|
|
145
|
+
if (questions.length === 0) {
|
|
146
|
+
return /** @type {import('./types').InitAnswers} */ (prefilled);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const prompted = await inquirer.prompt(questions);
|
|
150
|
+
return Object.assign({}, prefilled, prompted);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
promptUser,
|
|
155
|
+
parseInitArgs,
|
|
156
|
+
VALID_FRAMEWORKS,
|
|
157
|
+
VALID_DATABASES,
|
|
158
|
+
VALID_SESSIONS,
|
|
159
|
+
};
|
package/src/cli/init.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execSync } = require("child_process");
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
generateAppJs,
|
|
10
|
+
generateEnvFile,
|
|
11
|
+
generateEnvExample,
|
|
12
|
+
generateLoggerMiddleware,
|
|
13
|
+
generateMigrateScript,
|
|
14
|
+
generateAddMigrationScript,
|
|
15
|
+
generateInitialMigration,
|
|
16
|
+
generateSessionMigration,
|
|
17
|
+
generateGitignore,
|
|
18
|
+
} = require("./init/generators");
|
|
19
|
+
|
|
20
|
+
const { collectDependencies, getScripts } = require("./init/dependencies");
|
|
21
|
+
const { promptUser, parseInitArgs } = require("./init/prompt");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure a package.json exists in the current directory.
|
|
25
|
+
* Uses `npm init -y` for non-interactive creation. Exits with code 1 on failure.
|
|
26
|
+
*/
|
|
27
|
+
function ensurePackageJson() {
|
|
28
|
+
if (fs.existsSync("package.json")) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
execSync("npm init -y", { stdio: "inherit" });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error("Error: npm init failed or was aborted.");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (!fs.existsSync("package.json")) {
|
|
38
|
+
console.error("Error: package.json was not created. Aborting.");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Write a file, but warn (and skip) if it already exists.
|
|
45
|
+
* @param {string} filePath
|
|
46
|
+
* @param {string} content
|
|
47
|
+
* @returns {boolean} true if written, false if skipped
|
|
48
|
+
*/
|
|
49
|
+
function safeWriteFile(filePath, content) {
|
|
50
|
+
if (fs.existsSync(filePath)) {
|
|
51
|
+
console.log(` Skipped ${filePath} (already exists)`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
fs.writeFileSync(filePath, content);
|
|
55
|
+
console.log(` Created ${filePath}`);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate all project files based on user answers.
|
|
61
|
+
* Creates directories and writes files. Skips files that already exist.
|
|
62
|
+
* Returns the list of generated filenames for the summary.
|
|
63
|
+
* @param {import('./init/types').InitAnswers} answers
|
|
64
|
+
* @returns {{ files: string[], migrationFiles: string[] }}
|
|
65
|
+
*/
|
|
66
|
+
function generateFiles(answers) {
|
|
67
|
+
const files = [];
|
|
68
|
+
const migrationFiles = [];
|
|
69
|
+
|
|
70
|
+
// Create directories
|
|
71
|
+
if (!fs.existsSync("middleware")) {
|
|
72
|
+
fs.mkdirSync("middleware", { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
if (!fs.existsSync("migrations")) {
|
|
75
|
+
fs.mkdirSync("migrations", { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Write files (skip if they already exist)
|
|
79
|
+
if (safeWriteFile("app.js", generateAppJs(answers))) files.push("app.js");
|
|
80
|
+
if (safeWriteFile(".env", generateEnvFile(answers))) files.push(".env");
|
|
81
|
+
if (safeWriteFile(".env.example", generateEnvExample(answers)))
|
|
82
|
+
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
|
+
if (safeWriteFile(".gitignore", generateGitignore()))
|
|
93
|
+
files.push(".gitignore");
|
|
94
|
+
|
|
95
|
+
// Initial migration
|
|
96
|
+
const initialMigration = generateInitialMigration(answers);
|
|
97
|
+
const initialPath = path.join("migrations", initialMigration.filename);
|
|
98
|
+
if (safeWriteFile(initialPath, initialMigration.content)) {
|
|
99
|
+
migrationFiles.push(initialMigration.filename);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Conditional session migration
|
|
103
|
+
const sessionMigration = generateSessionMigration(answers);
|
|
104
|
+
if (sessionMigration !== null) {
|
|
105
|
+
const sessionPath = path.join("migrations", sessionMigration.filename);
|
|
106
|
+
if (safeWriteFile(sessionPath, sessionMigration.content)) {
|
|
107
|
+
migrationFiles.push(sessionMigration.filename);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { files, migrationFiles };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Update package.json with scripts and dependencies from the answers.
|
|
116
|
+
* @param {import('./init/types').InitAnswers} answers
|
|
117
|
+
*/
|
|
118
|
+
function updatePackageJson(answers) {
|
|
119
|
+
let raw;
|
|
120
|
+
try {
|
|
121
|
+
raw = fs.readFileSync("package.json", "utf8");
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error("Error: Could not read package.json.");
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let pkg;
|
|
128
|
+
try {
|
|
129
|
+
pkg = JSON.parse(raw);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(
|
|
132
|
+
"Error: package.json contains invalid JSON. Please fix it manually and re-run.",
|
|
133
|
+
);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { dependencies, devDependencies } = collectDependencies(answers);
|
|
138
|
+
const scripts = getScripts();
|
|
139
|
+
|
|
140
|
+
pkg.scripts = Object.assign({}, pkg.scripts || {}, scripts);
|
|
141
|
+
pkg.dependencies = Object.assign({}, pkg.dependencies || {}, dependencies);
|
|
142
|
+
pkg.devDependencies = Object.assign(
|
|
143
|
+
{},
|
|
144
|
+
pkg.devDependencies || {},
|
|
145
|
+
devDependencies,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
|
|
149
|
+
console.log(" Updated package.json");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Run npm install. On failure, print manual install instructions and exit with code 1.
|
|
154
|
+
*/
|
|
155
|
+
function runInstall() {
|
|
156
|
+
console.log("\nInstalling dependencies...\n");
|
|
157
|
+
try {
|
|
158
|
+
execSync("npm install", { stdio: "inherit" });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(
|
|
161
|
+
"\nError: npm install failed. Please run the following command manually:",
|
|
162
|
+
);
|
|
163
|
+
console.error(" npm install");
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Print a summary of generated files and next-step instructions.
|
|
170
|
+
* Uses the file lists captured during generation (no re-calling generators).
|
|
171
|
+
* @param {{ files: string[], migrationFiles: string[] }} generated
|
|
172
|
+
*/
|
|
173
|
+
function printSummary(generated) {
|
|
174
|
+
console.log("\n✔ Project scaffolded successfully!\n");
|
|
175
|
+
console.log("Generated files:");
|
|
176
|
+
|
|
177
|
+
for (const f of generated.files) {
|
|
178
|
+
console.log(` ${f}`);
|
|
179
|
+
}
|
|
180
|
+
if (generated.migrationFiles.length > 0) {
|
|
181
|
+
console.log(" migrations/");
|
|
182
|
+
for (const m of generated.migrationFiles) {
|
|
183
|
+
console.log(` └── ${m}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log("\nNext steps:");
|
|
188
|
+
console.log(" 1. Edit .env with your database credentials");
|
|
189
|
+
console.log(" 2. Run: npm run dev");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Main orchestrator.
|
|
194
|
+
*/
|
|
195
|
+
async function main() {
|
|
196
|
+
const cliArgs = parseInitArgs(process.argv.slice(2));
|
|
197
|
+
|
|
198
|
+
// If --help flag, print usage and exit
|
|
199
|
+
if (process.argv.includes("--help")) {
|
|
200
|
+
printUsage();
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
ensurePackageJson();
|
|
205
|
+
|
|
206
|
+
let answers;
|
|
207
|
+
try {
|
|
208
|
+
answers = await promptUser(cliArgs);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
// Handle Ctrl+C (inquirer throws on user cancel)
|
|
211
|
+
console.log("\nAborted.");
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let generated;
|
|
216
|
+
try {
|
|
217
|
+
generated = generateFiles(answers);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err.code === "EACCES" || err.code === "EPERM") {
|
|
220
|
+
console.error(
|
|
221
|
+
`Error: Permission denied writing files. Check directory permissions.\n ${err.message}`,
|
|
222
|
+
);
|
|
223
|
+
} else {
|
|
224
|
+
console.error(`Error: Failed to generate files.\n ${err.message}`);
|
|
225
|
+
}
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
updatePackageJson(answers);
|
|
230
|
+
runInstall();
|
|
231
|
+
printSummary(generated);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Print CLI usage information.
|
|
236
|
+
*/
|
|
237
|
+
function printUsage() {
|
|
238
|
+
console.log(`
|
|
239
|
+
Usage: db-model-router-init [options]
|
|
240
|
+
|
|
241
|
+
Scaffolds a complete Express-based REST API project. When all options are
|
|
242
|
+
provided, runs non-interactively (no prompts). Missing options are prompted.
|
|
243
|
+
|
|
244
|
+
Options:
|
|
245
|
+
--framework <name> Express framework: ultimate-express, express
|
|
246
|
+
--database <name> Database: mysql, postgres, sqlite3, mongodb, mssql,
|
|
247
|
+
cockroachdb, oracle, redis, dynamodb
|
|
248
|
+
--db <name> Alias for --database
|
|
249
|
+
--session <type> Session store: memory, redis, database
|
|
250
|
+
--rateLimiting Enable rate limiting (express-rate-limit)
|
|
251
|
+
--helmet Enable Helmet security headers
|
|
252
|
+
--logger Enable request/response logger (express-mung)
|
|
253
|
+
--help Show this help message
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
# Fully non-interactive (LLM-friendly)
|
|
257
|
+
db-model-router-init --framework express --database postgres --session redis --rateLimiting --helmet --logger
|
|
258
|
+
|
|
259
|
+
# Partial — only prompts for missing values
|
|
260
|
+
db-model-router-init --database mysql --session memory
|
|
261
|
+
|
|
262
|
+
# Interactive (no flags)
|
|
263
|
+
db-model-router-init
|
|
264
|
+
`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (require.main === module) {
|
|
268
|
+
main().catch((err) => {
|
|
269
|
+
console.error("Error:", err.message);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
ensurePackageJson,
|
|
276
|
+
generateFiles,
|
|
277
|
+
updatePackageJson,
|
|
278
|
+
runInstall,
|
|
279
|
+
printSummary,
|
|
280
|
+
main,
|
|
281
|
+
};
|
package/src/cli/main.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { parseFlags, OutputContext } = require("./flags");
|
|
5
|
+
|
|
6
|
+
const initCmd = require("./commands/init");
|
|
7
|
+
const inspectCmd = require("./commands/inspect");
|
|
8
|
+
const generateCmd = require("./commands/generate");
|
|
9
|
+
const doctorCmd = require("./commands/doctor");
|
|
10
|
+
const diffCmd = require("./commands/diff");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Map of subcommand names to their handler functions.
|
|
14
|
+
*/
|
|
15
|
+
const COMMANDS = {
|
|
16
|
+
init: initCmd,
|
|
17
|
+
inspect: inspectCmd,
|
|
18
|
+
generate: generateCmd,
|
|
19
|
+
doctor: doctorCmd,
|
|
20
|
+
diff: diffCmd,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Descriptions for each subcommand, used in help output.
|
|
25
|
+
*/
|
|
26
|
+
const COMMAND_DESCRIPTIONS = {
|
|
27
|
+
init: "Scaffold a new project from a schema file or interactively",
|
|
28
|
+
inspect: "Introspect a live database and produce a schema file",
|
|
29
|
+
generate: "Generate models, routes, tests, and OpenAPI spec from a schema",
|
|
30
|
+
doctor: "Validate schema, check dependencies, and verify file sync",
|
|
31
|
+
diff: "Preview changes between current files and what the schema would produce",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Print help message listing all available subcommands.
|
|
36
|
+
*/
|
|
37
|
+
function printHelp() {
|
|
38
|
+
console.log("Usage: db-model-router <command> [options]\n");
|
|
39
|
+
console.log("Commands:");
|
|
40
|
+
for (const [name, desc] of Object.entries(COMMAND_DESCRIPTIONS)) {
|
|
41
|
+
console.log(` ${name.padEnd(12)} ${desc}`);
|
|
42
|
+
}
|
|
43
|
+
console.log("\nGlobal flags:");
|
|
44
|
+
console.log(" --yes Accept all defaults without prompting");
|
|
45
|
+
console.log(" --json Output machine-readable JSON");
|
|
46
|
+
console.log(" --dry-run Preview actions without side effects");
|
|
47
|
+
console.log(" --no-install Skip npm install step");
|
|
48
|
+
console.log(" --help Show help for a command");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Print error for unknown subcommand and list valid subcommands.
|
|
53
|
+
* @param {string} cmd - The unknown subcommand
|
|
54
|
+
*/
|
|
55
|
+
function printUnknown(cmd) {
|
|
56
|
+
const valid = Object.keys(COMMANDS).join(", ");
|
|
57
|
+
console.error(`Unknown command: ${cmd}`);
|
|
58
|
+
console.error(`Valid commands: ${valid}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Main CLI entry point.
|
|
63
|
+
* @param {string[]} argv - process.argv.slice(2) style array
|
|
64
|
+
*/
|
|
65
|
+
async function main(argv) {
|
|
66
|
+
const { subcommand, flags, args } = parseFlags(argv);
|
|
67
|
+
|
|
68
|
+
if (!subcommand || flags.help) {
|
|
69
|
+
printHelp();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!COMMANDS[subcommand]) {
|
|
74
|
+
printUnknown(subcommand);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ctx = new OutputContext(flags);
|
|
80
|
+
await COMMANDS[subcommand](args, flags, ctx);
|
|
81
|
+
ctx.flush();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// When run directly as a script
|
|
85
|
+
if (require.main === module) {
|
|
86
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
87
|
+
console.error(err.message || err);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = main;
|
|
93
|
+
module.exports.COMMANDS = COMMANDS;
|
|
94
|
+
module.exports.printHelp = printHelp;
|
|
95
|
+
module.exports.printUnknown = printUnknown;
|
package/src/commons/model.js
CHANGED
|
@@ -280,6 +280,7 @@ module.exports = function model(
|
|
|
280
280
|
data,
|
|
281
281
|
getPayloadValidator("CREATE", modelStructure, primary_key, false),
|
|
282
282
|
);
|
|
283
|
+
const originalData = { ...data };
|
|
283
284
|
data = RemoveUnknownData(modelStructure, [data]);
|
|
284
285
|
data = jsonStringify(data);
|
|
285
286
|
updateResult = await db.upsert(table, data, unique);
|
|
@@ -288,12 +289,10 @@ module.exports = function model(
|
|
|
288
289
|
[[primary_key, "=", updateResult.id]],
|
|
289
290
|
]);
|
|
290
291
|
return getResult.count > 0 ? getResult["data"][0] : null;
|
|
291
|
-
} else if (
|
|
292
|
-
const result = await db.get(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
option.safeDelete,
|
|
296
|
-
);
|
|
292
|
+
} else if (originalData.hasOwnProperty(primary_key)) {
|
|
293
|
+
const result = await db.get(table, [
|
|
294
|
+
[[primary_key, "=", originalData[primary_key]]],
|
|
295
|
+
]);
|
|
297
296
|
if (result.count > 0) return result["data"][0];
|
|
298
297
|
else return null;
|
|
299
298
|
}
|
package/src/commons/route.js
CHANGED
|
@@ -156,6 +156,14 @@ module.exports = function route(model, override = {}) {
|
|
|
156
156
|
});
|
|
157
157
|
})
|
|
158
158
|
.post("/", (req, res) => {
|
|
159
|
+
if (!req.body || !Array.isArray(req.body.data)) {
|
|
160
|
+
return res
|
|
161
|
+
.status(400)
|
|
162
|
+
.send({
|
|
163
|
+
type: "danger",
|
|
164
|
+
message: "Request body must contain a 'data' array",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
159
167
|
let payload = payloadOverride(req.body.data, req, override);
|
|
160
168
|
model
|
|
161
169
|
.insert({ data: payload })
|
|
@@ -167,6 +175,14 @@ module.exports = function route(model, override = {}) {
|
|
|
167
175
|
});
|
|
168
176
|
})
|
|
169
177
|
.put("/", (req, res) => {
|
|
178
|
+
if (!req.body || !Array.isArray(req.body.data)) {
|
|
179
|
+
return res
|
|
180
|
+
.status(400)
|
|
181
|
+
.send({
|
|
182
|
+
type: "danger",
|
|
183
|
+
message: "Request body must contain a 'data' array",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
170
186
|
let payload = payloadOverride(req.body.data, req, override);
|
|
171
187
|
model
|
|
172
188
|
.update({ data: payload })
|
|
@@ -178,6 +194,14 @@ module.exports = function route(model, override = {}) {
|
|
|
178
194
|
});
|
|
179
195
|
})
|
|
180
196
|
.delete("/", (req, res) => {
|
|
197
|
+
if (!req.body || !Array.isArray(req.body.data)) {
|
|
198
|
+
return res
|
|
199
|
+
.status(400)
|
|
200
|
+
.send({
|
|
201
|
+
type: "danger",
|
|
202
|
+
message: "Request body must contain a 'data' array",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
181
205
|
let payload = payloadOverride(req.body.data, req, override);
|
|
182
206
|
model
|
|
183
207
|
.remove(payload)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { validateSchema, SchemaValidationError } = require("./schema-validator");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse a schema from a JSON string or plain object.
|
|
7
|
+
*
|
|
8
|
+
* - If `input` is a string, JSON.parse it (wrapping parse errors).
|
|
9
|
+
* - Validate via validateSchema(); throw SchemaValidationError if invalid.
|
|
10
|
+
* - Normalize each table with defaults for pk, unique, timestamps, softDelete.
|
|
11
|
+
* - Return the internal { adapter, framework, tables, relationships, options } representation.
|
|
12
|
+
*
|
|
13
|
+
* @param {string|object} input — raw JSON string or parsed object
|
|
14
|
+
* @returns {{ adapter: string, framework: string, tables: object, relationships: Array, options: object }}
|
|
15
|
+
* @throws {SchemaValidationError}
|
|
16
|
+
*/
|
|
17
|
+
function parseSchema(input) {
|
|
18
|
+
let raw = input;
|
|
19
|
+
|
|
20
|
+
// If string, attempt JSON.parse; wrap errors
|
|
21
|
+
if (typeof raw === "string") {
|
|
22
|
+
try {
|
|
23
|
+
raw = JSON.parse(raw);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new SchemaValidationError([
|
|
26
|
+
{
|
|
27
|
+
path: "",
|
|
28
|
+
message: `Invalid JSON: ${err.message}`,
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate
|
|
35
|
+
const result = validateSchema(raw);
|
|
36
|
+
if (!result.valid) {
|
|
37
|
+
throw new SchemaValidationError(result.errors);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Normalize tables
|
|
41
|
+
const tables = {};
|
|
42
|
+
for (const [tableName, tableDef] of Object.entries(raw.tables)) {
|
|
43
|
+
const pk = tableDef.pk || "id";
|
|
44
|
+
const unique = tableDef.unique !== undefined ? [...tableDef.unique] : [pk];
|
|
45
|
+
const timestamps =
|
|
46
|
+
tableDef.timestamps !== undefined
|
|
47
|
+
? { ...tableDef.timestamps }
|
|
48
|
+
: { created_at: null, modified_at: null };
|
|
49
|
+
// Ensure timestamps always has both keys
|
|
50
|
+
if (!("created_at" in timestamps)) {
|
|
51
|
+
timestamps.created_at = null;
|
|
52
|
+
}
|
|
53
|
+
if (!("modified_at" in timestamps)) {
|
|
54
|
+
timestamps.modified_at = null;
|
|
55
|
+
}
|
|
56
|
+
const softDelete =
|
|
57
|
+
tableDef.softDelete !== undefined ? tableDef.softDelete : null;
|
|
58
|
+
|
|
59
|
+
tables[tableName] = {
|
|
60
|
+
name: tableName,
|
|
61
|
+
columns: { ...tableDef.columns },
|
|
62
|
+
pk,
|
|
63
|
+
unique,
|
|
64
|
+
softDelete,
|
|
65
|
+
timestamps,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
adapter: raw.adapter,
|
|
71
|
+
framework: raw.framework,
|
|
72
|
+
tables,
|
|
73
|
+
relationships: raw.relationships ? [...raw.relationships] : [],
|
|
74
|
+
options: raw.options ? { ...raw.options } : {},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { parseSchema };
|