db-model-router 1.0.0 → 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 +154 -203
- package/docker-compose.yml +13 -9
- package/docs/SKILL.md +196 -24
- 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 +7 -12
- 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-model.js +4 -4
- package/src/cli/generate-openapi.js +5 -1
- package/src/cli/generate-route.js +242 -7
- 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 +7 -10
- package/docs/README.md +0 -208
- package/src/cli/generate-app.js +0 -359
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { parseSchema } = require("../../schema/schema-parser");
|
|
6
|
+
const { schemaToModelMeta } = require("../../schema/schema-to-meta");
|
|
7
|
+
const { generateModelFile } = require("../generate-model");
|
|
8
|
+
const {
|
|
9
|
+
generateRouteFile,
|
|
10
|
+
generateChildRouteFile,
|
|
11
|
+
generateRoutesIndexFile,
|
|
12
|
+
generateTestFile,
|
|
13
|
+
generateChildTestFile,
|
|
14
|
+
} = require("../generate-route");
|
|
15
|
+
const { generateOpenAPISpec } = require("../generate-openapi");
|
|
16
|
+
const { generateLlmsTxt, generateLlmMd } = require("./generate-llm-docs");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate command handler for the unified CLI.
|
|
20
|
+
*
|
|
21
|
+
* Reads a schema file, converts to ModelMeta[], and generates
|
|
22
|
+
* models, routes, tests, and OpenAPI spec files.
|
|
23
|
+
*
|
|
24
|
+
* Supported flags:
|
|
25
|
+
* --from Path to schema file (default: dbmr.schema.json)
|
|
26
|
+
* --models Generate only model files
|
|
27
|
+
* --routes Generate only route files (including child routes and index)
|
|
28
|
+
* --openapi Generate only OpenAPI spec
|
|
29
|
+
* --tests Generate only test files
|
|
30
|
+
* --dry-run Report planned files without writing
|
|
31
|
+
* --json Output JSON result via ctx
|
|
32
|
+
*
|
|
33
|
+
* When no artifact flags are provided, all artifact types are generated.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} args - Parsed key-value args
|
|
36
|
+
* @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
|
|
37
|
+
* @param {import('../flags').OutputContext} ctx - Output context
|
|
38
|
+
*/
|
|
39
|
+
async function generate(args, flags, ctx) {
|
|
40
|
+
const schemaPath = path.resolve(args.from || "dbmr.schema.json");
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(schemaPath)) {
|
|
43
|
+
const msg = `Schema file not found: ${args.from || "dbmr.schema.json"}`;
|
|
44
|
+
if (flags.json) {
|
|
45
|
+
ctx.result({ error: true, code: "SCHEMA_NOT_FOUND", message: msg });
|
|
46
|
+
} else {
|
|
47
|
+
ctx.log(`Error: ${msg}`);
|
|
48
|
+
}
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let schema;
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(schemaPath, "utf8");
|
|
56
|
+
schema = parseSchema(raw);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const msg = `Schema parse error: ${err.message}`;
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
ctx.result({
|
|
61
|
+
error: true,
|
|
62
|
+
code: "SCHEMA_VALIDATION",
|
|
63
|
+
message: msg,
|
|
64
|
+
errors: err.errors || [],
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
ctx.log(`Error: ${msg}`);
|
|
68
|
+
}
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const meta = schemaToModelMeta(schema);
|
|
74
|
+
const relationships = schema.relationships || [];
|
|
75
|
+
const tableNames = meta.map((m) => m.table).sort();
|
|
76
|
+
|
|
77
|
+
// Determine which artifact types to generate
|
|
78
|
+
const hasArtifactFlag =
|
|
79
|
+
args.models === true ||
|
|
80
|
+
args.routes === true ||
|
|
81
|
+
args.openapi === true ||
|
|
82
|
+
args.tests === true ||
|
|
83
|
+
args["llm-docs"] === true;
|
|
84
|
+
|
|
85
|
+
const genModels = !hasArtifactFlag || args.models === true;
|
|
86
|
+
const genRoutes = !hasArtifactFlag || args.routes === true;
|
|
87
|
+
const genOpenapi = !hasArtifactFlag || args.openapi === true;
|
|
88
|
+
const genTests = !hasArtifactFlag || args.tests === true;
|
|
89
|
+
const genLlmDocs = !hasArtifactFlag || args["llm-docs"] === true;
|
|
90
|
+
|
|
91
|
+
const modelsRelPath = "../models";
|
|
92
|
+
const baseDir = process.cwd();
|
|
93
|
+
|
|
94
|
+
// Collect all planned files: { relPath, content }
|
|
95
|
+
const planned = [];
|
|
96
|
+
|
|
97
|
+
// --- Model files ---
|
|
98
|
+
if (genModels) {
|
|
99
|
+
for (const m of meta) {
|
|
100
|
+
planned.push({
|
|
101
|
+
relPath: `models/${m.table}.js`,
|
|
102
|
+
content: generateModelFile(m),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Route files ---
|
|
108
|
+
if (genRoutes) {
|
|
109
|
+
// One route per table
|
|
110
|
+
for (const m of meta) {
|
|
111
|
+
planned.push({
|
|
112
|
+
relPath: `routes/${m.table}.js`,
|
|
113
|
+
content: generateRouteFile(m.table, modelsRelPath),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Child route files (one per relationship)
|
|
118
|
+
for (const rel of relationships) {
|
|
119
|
+
planned.push({
|
|
120
|
+
relPath: `routes/${rel.child}_child_of_${rel.parent}.js`,
|
|
121
|
+
content: generateChildRouteFile(
|
|
122
|
+
rel.child,
|
|
123
|
+
rel.parent,
|
|
124
|
+
rel.foreignKey,
|
|
125
|
+
modelsRelPath,
|
|
126
|
+
),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Routes index file
|
|
131
|
+
planned.push({
|
|
132
|
+
relPath: "routes/index.js",
|
|
133
|
+
content: generateRoutesIndexFile(tableNames, relationships),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- OpenAPI spec ---
|
|
138
|
+
if (genOpenapi) {
|
|
139
|
+
planned.push({
|
|
140
|
+
relPath: "openapi.json",
|
|
141
|
+
content: JSON.stringify(generateOpenAPISpec(meta), null, 2) + "\n",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Test files ---
|
|
146
|
+
if (genTests) {
|
|
147
|
+
for (const m of meta) {
|
|
148
|
+
planned.push({
|
|
149
|
+
relPath: `test/${m.table}.test.js`,
|
|
150
|
+
content: generateTestFile(m.table, m.primary_key),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Child test files (one per relationship)
|
|
155
|
+
for (const rel of relationships) {
|
|
156
|
+
const childMeta = meta.find((m) => m.table === rel.child);
|
|
157
|
+
const pk = childMeta ? childMeta.primary_key : "id";
|
|
158
|
+
planned.push({
|
|
159
|
+
relPath: `test/${rel.child}_child_of_${rel.parent}.test.js`,
|
|
160
|
+
content: generateChildTestFile(
|
|
161
|
+
rel.child,
|
|
162
|
+
rel.parent,
|
|
163
|
+
rel.foreignKey,
|
|
164
|
+
pk,
|
|
165
|
+
),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- LLM docs ---
|
|
171
|
+
if (genLlmDocs) {
|
|
172
|
+
planned.push({
|
|
173
|
+
relPath: "llms.txt",
|
|
174
|
+
content: generateLlmsTxt(),
|
|
175
|
+
});
|
|
176
|
+
planned.push({
|
|
177
|
+
relPath: "docs/llm.md",
|
|
178
|
+
content: generateLlmMd(),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Process planned files ---
|
|
183
|
+
const results = [];
|
|
184
|
+
|
|
185
|
+
for (const { relPath, content } of planned) {
|
|
186
|
+
const fullPath = path.join(baseDir, relPath);
|
|
187
|
+
|
|
188
|
+
if (flags.dryRun) {
|
|
189
|
+
results.push({ path: relPath, status: "planned" });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check if file exists and content matches (skip-unchanged)
|
|
194
|
+
if (fs.existsSync(fullPath)) {
|
|
195
|
+
const existing = fs.readFileSync(fullPath, "utf8");
|
|
196
|
+
if (existing === content) {
|
|
197
|
+
results.push({ path: relPath, status: "unchanged" });
|
|
198
|
+
ctx.log(` unchanged ${relPath}`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// File exists but content differs — overwrite
|
|
202
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
203
|
+
fs.writeFileSync(fullPath, content, "utf8");
|
|
204
|
+
results.push({ path: relPath, status: "overwritten" });
|
|
205
|
+
ctx.log(` overwritten ${relPath}`);
|
|
206
|
+
} else {
|
|
207
|
+
// File does not exist — create
|
|
208
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
209
|
+
fs.writeFileSync(fullPath, content, "utf8");
|
|
210
|
+
results.push({ path: relPath, status: "created" });
|
|
211
|
+
ctx.log(` created ${relPath}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Output ---
|
|
216
|
+
if (flags.dryRun) {
|
|
217
|
+
if (flags.json) {
|
|
218
|
+
ctx.result({ files: results });
|
|
219
|
+
} else {
|
|
220
|
+
ctx.log("Dry run — the following files would be generated:");
|
|
221
|
+
for (const r of results) {
|
|
222
|
+
ctx.log(` ${r.path}`);
|
|
223
|
+
}
|
|
224
|
+
ctx.log(`\n${results.length} file(s) planned.`);
|
|
225
|
+
}
|
|
226
|
+
} else if (flags.json) {
|
|
227
|
+
ctx.result({ files: results });
|
|
228
|
+
} else {
|
|
229
|
+
const created = results.filter((r) => r.status === "created").length;
|
|
230
|
+
const overwritten = results.filter(
|
|
231
|
+
(r) => r.status === "overwritten",
|
|
232
|
+
).length;
|
|
233
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
234
|
+
ctx.log(
|
|
235
|
+
`\nDone. ${created} created, ${overwritten} overwritten, ${unchanged} unchanged.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = generate;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { parseSchema } = require("../../schema/schema-parser");
|
|
6
|
+
const {
|
|
7
|
+
generateFiles,
|
|
8
|
+
updatePackageJson,
|
|
9
|
+
runInstall,
|
|
10
|
+
printSummary,
|
|
11
|
+
ensurePackageJson,
|
|
12
|
+
} = require("../init");
|
|
13
|
+
const { promptUser } = require("../init/prompt");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default answers used when --yes is provided and no schema is available.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_ANSWERS = {
|
|
19
|
+
framework: "express",
|
|
20
|
+
database: "postgres",
|
|
21
|
+
session: "memory",
|
|
22
|
+
rateLimiting: false,
|
|
23
|
+
helmet: false,
|
|
24
|
+
logger: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Init command handler for the unified CLI.
|
|
29
|
+
*
|
|
30
|
+
* Scaffolds a new project from a schema file or interactively.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} args - Parsed positional/key-value args (e.g. { from, framework, database })
|
|
33
|
+
* @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
|
|
34
|
+
* @param {import('../flags').OutputContext} ctx - Output context for --json support
|
|
35
|
+
*/
|
|
36
|
+
async function init(args, flags, ctx) {
|
|
37
|
+
let answers;
|
|
38
|
+
|
|
39
|
+
if (args.from) {
|
|
40
|
+
// --from points to a schema file: read adapter/framework from it
|
|
41
|
+
const schemaPath = path.resolve(args.from);
|
|
42
|
+
if (!fs.existsSync(schemaPath)) {
|
|
43
|
+
throw new Error(`Schema file not found: ${args.from}`);
|
|
44
|
+
}
|
|
45
|
+
const raw = fs.readFileSync(schemaPath, "utf8");
|
|
46
|
+
const schema = parseSchema(raw);
|
|
47
|
+
|
|
48
|
+
answers = {
|
|
49
|
+
framework: schema.framework,
|
|
50
|
+
database: schema.adapter,
|
|
51
|
+
session: (schema.options && schema.options.session) || "memory",
|
|
52
|
+
rateLimiting: !!(schema.options && schema.options.rateLimiting),
|
|
53
|
+
helmet: !!(schema.options && schema.options.helmet),
|
|
54
|
+
logger: !!(schema.options && schema.options.logger),
|
|
55
|
+
};
|
|
56
|
+
} else if (flags.yes) {
|
|
57
|
+
// --yes with no schema: use defaults, but allow CLI overrides
|
|
58
|
+
answers = Object.assign({}, DEFAULT_ANSWERS);
|
|
59
|
+
if (args.framework) answers.framework = args.framework;
|
|
60
|
+
if (args.database) answers.database = args.database;
|
|
61
|
+
} else {
|
|
62
|
+
// Interactive: build prefilled from CLI args, prompt for the rest
|
|
63
|
+
const prefilled = {};
|
|
64
|
+
if (args.framework) prefilled.framework = args.framework;
|
|
65
|
+
if (args.database) prefilled.database = args.database;
|
|
66
|
+
answers = await promptUser(prefilled);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --dry-run: report planned files without writing
|
|
70
|
+
if (flags.dryRun) {
|
|
71
|
+
const planned = planFiles(answers);
|
|
72
|
+
if (flags.json) {
|
|
73
|
+
ctx.result({
|
|
74
|
+
files: planned,
|
|
75
|
+
dependencies: { installed: false },
|
|
76
|
+
actions: ["dry-run"],
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
ctx.log("Dry run — the following files would be created:");
|
|
80
|
+
for (const f of planned) {
|
|
81
|
+
ctx.log(` ${f}`);
|
|
82
|
+
}
|
|
83
|
+
ctx.log("\nNo files were written.");
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Ensure package.json exists
|
|
89
|
+
ensurePackageJson();
|
|
90
|
+
|
|
91
|
+
// Generate project files
|
|
92
|
+
const generated = generateFiles(answers);
|
|
93
|
+
|
|
94
|
+
// Update package.json with deps and scripts
|
|
95
|
+
updatePackageJson(answers);
|
|
96
|
+
|
|
97
|
+
// npm install (unless --no-install)
|
|
98
|
+
const installed = !flags.noInstall;
|
|
99
|
+
if (installed) {
|
|
100
|
+
runInstall();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Output
|
|
104
|
+
const allFiles = [
|
|
105
|
+
...generated.files,
|
|
106
|
+
...generated.migrationFiles.map((m) => `migrations/${m}`),
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
if (flags.json) {
|
|
110
|
+
ctx.result({
|
|
111
|
+
files: allFiles,
|
|
112
|
+
dependencies: { installed },
|
|
113
|
+
actions: installed ? ["scaffolded", "installed"] : ["scaffolded"],
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
printSummary(generated);
|
|
117
|
+
if (!installed) {
|
|
118
|
+
ctx.log(
|
|
119
|
+
"\nSkipped npm install (--no-install). Run `npm install` manually.",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Compute the list of files that would be created (for --dry-run).
|
|
127
|
+
* This mirrors the file list from generateFiles() without writing anything.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} answers
|
|
130
|
+
* @returns {string[]}
|
|
131
|
+
*/
|
|
132
|
+
function planFiles(answers) {
|
|
133
|
+
const { isSql } = require("../init/generators");
|
|
134
|
+
const files = [
|
|
135
|
+
"app.js",
|
|
136
|
+
".env",
|
|
137
|
+
".env.example",
|
|
138
|
+
"middleware/logger.js",
|
|
139
|
+
"migrate.js",
|
|
140
|
+
"add_migration.js",
|
|
141
|
+
".gitignore",
|
|
142
|
+
"migrations/<timestamp>_create_migrations_table" +
|
|
143
|
+
(isSql(answers.database) ? ".sql" : ".js"),
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
if (answers.session === "database" && isSql(answers.database)) {
|
|
147
|
+
files.push("migrations/<timestamp>_create_sessions_table.sql");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return files;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = init;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { printSchema } = require("../../schema/schema-printer");
|
|
6
|
+
const {
|
|
7
|
+
introspectMySQL,
|
|
8
|
+
introspectPostgres,
|
|
9
|
+
introspectSQLite3,
|
|
10
|
+
introspectMSSQL,
|
|
11
|
+
introspectOracle,
|
|
12
|
+
introspectCockroachDB,
|
|
13
|
+
} = require("../generate-model");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Map of adapter names to their introspection functions.
|
|
17
|
+
* Each value is an async function(db) => ModelMeta[].
|
|
18
|
+
*/
|
|
19
|
+
const INTROSPECT_MAP = {
|
|
20
|
+
mysql: introspectMySQL,
|
|
21
|
+
postgres: introspectPostgres,
|
|
22
|
+
sqlite3: introspectSQLite3,
|
|
23
|
+
mssql: introspectMSSQL,
|
|
24
|
+
oracle: introspectOracle,
|
|
25
|
+
cockroachdb: introspectCockroachDB,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert a ModelMeta array (from introspection) into a ParsedSchema object.
|
|
30
|
+
* This is the reverse of schemaToModelMeta.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} adapter - The database adapter name
|
|
33
|
+
* @param {string} framework - The framework name (default: "express")
|
|
34
|
+
* @param {Array<{table, structure, primary_key, unique, option}>} models
|
|
35
|
+
* @returns {object} ParsedSchema
|
|
36
|
+
*/
|
|
37
|
+
function modelMetaToSchema(adapter, framework, models) {
|
|
38
|
+
const tables = {};
|
|
39
|
+
|
|
40
|
+
for (const m of models) {
|
|
41
|
+
const columns = {};
|
|
42
|
+
|
|
43
|
+
// Re-add columns from structure
|
|
44
|
+
for (const [col, rule] of Object.entries(m.structure)) {
|
|
45
|
+
columns[col] = rule;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const pk = m.primary_key || "id";
|
|
49
|
+
const unique = m.unique && m.unique.length > 0 ? [...m.unique] : [pk];
|
|
50
|
+
|
|
51
|
+
const opt = m.option || {};
|
|
52
|
+
const softDelete = opt.safeDelete || null;
|
|
53
|
+
const timestamps = {
|
|
54
|
+
created_at: opt.created_at || null,
|
|
55
|
+
modified_at: opt.modified_at || null,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
tables[m.table] = {
|
|
59
|
+
name: m.table,
|
|
60
|
+
columns,
|
|
61
|
+
pk,
|
|
62
|
+
unique,
|
|
63
|
+
softDelete,
|
|
64
|
+
timestamps,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
adapter,
|
|
70
|
+
framework: framework || "express",
|
|
71
|
+
tables,
|
|
72
|
+
relationships: [],
|
|
73
|
+
options: {},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Inspect command handler for the unified CLI.
|
|
79
|
+
*
|
|
80
|
+
* Connects to a live database, introspects its structure, converts to
|
|
81
|
+
* ParsedSchema, prints via schema-printer, and writes to file.
|
|
82
|
+
*
|
|
83
|
+
* Supported flags:
|
|
84
|
+
* --type Database adapter type (required)
|
|
85
|
+
* --env Path to .env file for connection params
|
|
86
|
+
* --out Output file path (default: dbmr.schema.json)
|
|
87
|
+
* --tables Comma-separated list of tables to include
|
|
88
|
+
* --json Output schema to stdout as JSON (no file write)
|
|
89
|
+
* --dry-run Output schema to stdout without writing file
|
|
90
|
+
*
|
|
91
|
+
* @param {object} args - Parsed key-value args
|
|
92
|
+
* @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
|
|
93
|
+
* @param {import('../flags').OutputContext} ctx - Output context
|
|
94
|
+
*/
|
|
95
|
+
async function inspect(args, flags, ctx) {
|
|
96
|
+
const adapterType = args.type;
|
|
97
|
+
if (!adapterType || !INTROSPECT_MAP[adapterType]) {
|
|
98
|
+
const supported = Object.keys(INTROSPECT_MAP).join(", ");
|
|
99
|
+
const msg = adapterType
|
|
100
|
+
? `Unsupported --type "${adapterType}". Supported: ${supported}`
|
|
101
|
+
: `Missing required --type flag. Supported: ${supported}`;
|
|
102
|
+
if (flags.json) {
|
|
103
|
+
ctx.result({ error: true, code: "INVALID_TYPE", message: msg });
|
|
104
|
+
} else {
|
|
105
|
+
ctx.log(`Error: ${msg}`);
|
|
106
|
+
}
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Load .env file if --env provided
|
|
112
|
+
if (args.env) {
|
|
113
|
+
require("dotenv").config({ path: path.resolve(args.env) });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Connect to database
|
|
117
|
+
let db;
|
|
118
|
+
try {
|
|
119
|
+
const restRouter = require("../../index.js");
|
|
120
|
+
restRouter.init(adapterType);
|
|
121
|
+
db = restRouter.db;
|
|
122
|
+
|
|
123
|
+
const config = {
|
|
124
|
+
host: process.env.DB_HOST || "localhost",
|
|
125
|
+
port: process.env.DB_PORT,
|
|
126
|
+
database: process.env.DB_NAME,
|
|
127
|
+
user: process.env.DB_USER,
|
|
128
|
+
password: process.env.DB_PASS,
|
|
129
|
+
filename: process.env.DB_NAME,
|
|
130
|
+
server: process.env.DB_HOST || "localhost",
|
|
131
|
+
options: { encrypt: false, trustServerCertificate: true },
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
db.connect(config);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
const msg = `Database connection failed: ${err.message}`;
|
|
137
|
+
if (flags.json) {
|
|
138
|
+
ctx.result({ error: true, code: "CONNECTION_FAILED", message: msg });
|
|
139
|
+
} else {
|
|
140
|
+
ctx.log(`Error: ${msg}`);
|
|
141
|
+
}
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Introspect
|
|
147
|
+
let models;
|
|
148
|
+
try {
|
|
149
|
+
const introspectFn = INTROSPECT_MAP[adapterType];
|
|
150
|
+
models = await introspectFn(db);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const msg = `Introspection failed: ${err.message}`;
|
|
153
|
+
if (flags.json) {
|
|
154
|
+
ctx.result({ error: true, code: "INTROSPECTION_FAILED", message: msg });
|
|
155
|
+
} else {
|
|
156
|
+
ctx.log(`Error: ${msg}`);
|
|
157
|
+
}
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
// Disconnect
|
|
160
|
+
if (db.disconnect) await db.disconnect();
|
|
161
|
+
else if (db.close) db.close();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Disconnect
|
|
166
|
+
try {
|
|
167
|
+
if (db.disconnect) await db.disconnect();
|
|
168
|
+
else if (db.close) db.close();
|
|
169
|
+
} catch (_) {
|
|
170
|
+
// ignore disconnect errors
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Filter by --tables if provided
|
|
174
|
+
if (args.tables) {
|
|
175
|
+
const allowed = new Set(args.tables.split(",").map((s) => s.trim()));
|
|
176
|
+
models = models.filter((m) => allowed.has(m.table));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Convert ModelMeta[] → ParsedSchema
|
|
180
|
+
const schema = modelMetaToSchema(adapterType, "express", models);
|
|
181
|
+
|
|
182
|
+
// Print via schema-printer
|
|
183
|
+
const output = printSchema(schema);
|
|
184
|
+
|
|
185
|
+
// Determine output path
|
|
186
|
+
const outPath = args.out || "dbmr.schema.json";
|
|
187
|
+
|
|
188
|
+
if (flags.json) {
|
|
189
|
+
// --json: output schema to stdout, no file write
|
|
190
|
+
ctx.result({ schema: JSON.parse(output), writtenTo: null });
|
|
191
|
+
} else if (flags.dryRun) {
|
|
192
|
+
// --dry-run: output schema to stdout, no file write
|
|
193
|
+
ctx.log(output);
|
|
194
|
+
ctx.log(`Would write to: ${outPath}`);
|
|
195
|
+
} else {
|
|
196
|
+
// Write to file
|
|
197
|
+
const resolvedPath = path.resolve(outPath);
|
|
198
|
+
fs.writeFileSync(resolvedPath, output, "utf8");
|
|
199
|
+
ctx.log(`Schema written to ${outPath}`);
|
|
200
|
+
ctx.log(output);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = inspect;
|
|
205
|
+
module.exports.modelMetaToSchema = modelMetaToSchema;
|