db-model-router 1.0.2 → 1.0.4
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 +317 -202
- package/docs/SKILL.md +250 -33
- 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 +12 -6
- 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/help.js +180 -0
- package/src/cli/commands/init.js +181 -0
- package/src/cli/commands/inspect.js +222 -0
- package/src/cli/diff-engine.js +198 -0
- package/src/cli/flags.js +112 -0
- package/src/cli/generate-model.js +5 -4
- package/src/cli/generate-route.js +255 -14
- package/src/cli/init/dependencies.js +92 -0
- package/src/cli/init/generators.js +1791 -0
- package/src/cli/init/prompt.js +191 -0
- package/src/cli/init.js +404 -0
- package/src/cli/main.js +175 -0
- package/src/commons/model.js +5 -6
- package/src/commons/route.js +24 -0
- package/src/index.js +2 -0
- package/src/schema/schema-parser.js +78 -0
- package/src/schema/schema-printer.js +77 -0
- package/src/schema/schema-to-meta.js +78 -0
- package/src/schema/schema-validator.js +255 -0
- package/src/serve.js +5 -3
- package/docs/README.md +0 -208
- package/src/cli/generate-app.js +0 -359
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
const pk = m.primary_key || "id";
|
|
44
|
+
const opt = m.option || {};
|
|
45
|
+
|
|
46
|
+
// Add PK column as auto_increment
|
|
47
|
+
columns[pk] = "auto_increment";
|
|
48
|
+
|
|
49
|
+
// Add timestamp columns as datetime
|
|
50
|
+
if (opt.created_at) {
|
|
51
|
+
columns[opt.created_at] = "datetime";
|
|
52
|
+
}
|
|
53
|
+
if (opt.modified_at) {
|
|
54
|
+
columns[opt.modified_at] = "datetime";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add softDelete column
|
|
58
|
+
if (opt.safeDelete) {
|
|
59
|
+
columns[opt.safeDelete] = "boolean";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add remaining columns from structure
|
|
63
|
+
for (const [col, rule] of Object.entries(m.structure)) {
|
|
64
|
+
columns[col] = rule;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const unique = m.unique && m.unique.length > 0 ? [...m.unique] : [pk];
|
|
68
|
+
|
|
69
|
+
const softDelete = opt.safeDelete || null;
|
|
70
|
+
const timestamps = {
|
|
71
|
+
created_at: opt.created_at || null,
|
|
72
|
+
modified_at: opt.modified_at || null,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
tables[m.table] = {
|
|
76
|
+
name: m.table,
|
|
77
|
+
columns,
|
|
78
|
+
pk,
|
|
79
|
+
unique,
|
|
80
|
+
softDelete,
|
|
81
|
+
timestamps,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
adapter,
|
|
87
|
+
framework: framework || "express",
|
|
88
|
+
tables,
|
|
89
|
+
relationships: [],
|
|
90
|
+
options: {},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Inspect command handler for the unified CLI.
|
|
96
|
+
*
|
|
97
|
+
* Connects to a live database, introspects its structure, converts to
|
|
98
|
+
* ParsedSchema, prints via schema-printer, and writes to file.
|
|
99
|
+
*
|
|
100
|
+
* Supported flags:
|
|
101
|
+
* --type Database adapter type (required)
|
|
102
|
+
* --env Path to .env file for connection params
|
|
103
|
+
* --out Output file path (default: dbmr.schema.json)
|
|
104
|
+
* --tables Comma-separated list of tables to include
|
|
105
|
+
* --json Output schema to stdout as JSON (no file write)
|
|
106
|
+
* --dry-run Output schema to stdout without writing file
|
|
107
|
+
*
|
|
108
|
+
* @param {object} args - Parsed key-value args
|
|
109
|
+
* @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
|
|
110
|
+
* @param {import('../flags').OutputContext} ctx - Output context
|
|
111
|
+
*/
|
|
112
|
+
async function inspect(args, flags, ctx) {
|
|
113
|
+
const adapterType = args.type;
|
|
114
|
+
if (!adapterType || !INTROSPECT_MAP[adapterType]) {
|
|
115
|
+
const supported = Object.keys(INTROSPECT_MAP).join(", ");
|
|
116
|
+
const msg = adapterType
|
|
117
|
+
? `Unsupported --type "${adapterType}". Supported: ${supported}`
|
|
118
|
+
: `Missing required --type flag. Supported: ${supported}`;
|
|
119
|
+
if (flags.json) {
|
|
120
|
+
ctx.result({ error: true, code: "INVALID_TYPE", message: msg });
|
|
121
|
+
} else {
|
|
122
|
+
ctx.log(`Error: ${msg}`);
|
|
123
|
+
}
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Load .env file if --env provided
|
|
129
|
+
if (args.env) {
|
|
130
|
+
require("dotenv").config({ path: path.resolve(args.env) });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Connect to database
|
|
134
|
+
let db;
|
|
135
|
+
try {
|
|
136
|
+
const restRouter = require("../../index.js");
|
|
137
|
+
restRouter.init(adapterType);
|
|
138
|
+
db = restRouter.db;
|
|
139
|
+
|
|
140
|
+
const config = {
|
|
141
|
+
host: process.env.DB_HOST || "localhost",
|
|
142
|
+
port: process.env.DB_PORT,
|
|
143
|
+
database: process.env.DB_NAME,
|
|
144
|
+
user: process.env.DB_USER,
|
|
145
|
+
password: process.env.DB_PASS,
|
|
146
|
+
filename: process.env.DB_NAME,
|
|
147
|
+
server: process.env.DB_HOST || "localhost",
|
|
148
|
+
options: { encrypt: false, trustServerCertificate: true },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
db.connect(config);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const msg = `Database connection failed: ${err.message}`;
|
|
154
|
+
if (flags.json) {
|
|
155
|
+
ctx.result({ error: true, code: "CONNECTION_FAILED", message: msg });
|
|
156
|
+
} else {
|
|
157
|
+
ctx.log(`Error: ${msg}`);
|
|
158
|
+
}
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Introspect
|
|
164
|
+
let models;
|
|
165
|
+
try {
|
|
166
|
+
const introspectFn = INTROSPECT_MAP[adapterType];
|
|
167
|
+
models = await introspectFn(db);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const msg = `Introspection failed: ${err.message}`;
|
|
170
|
+
if (flags.json) {
|
|
171
|
+
ctx.result({ error: true, code: "INTROSPECTION_FAILED", message: msg });
|
|
172
|
+
} else {
|
|
173
|
+
ctx.log(`Error: ${msg}`);
|
|
174
|
+
}
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
// Disconnect
|
|
177
|
+
if (db.disconnect) await db.disconnect();
|
|
178
|
+
else if (db.close) db.close();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Disconnect
|
|
183
|
+
try {
|
|
184
|
+
if (db.disconnect) await db.disconnect();
|
|
185
|
+
else if (db.close) db.close();
|
|
186
|
+
} catch (_) {
|
|
187
|
+
// ignore disconnect errors
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Filter by --tables if provided
|
|
191
|
+
if (args.tables) {
|
|
192
|
+
const allowed = new Set(args.tables.split(",").map((s) => s.trim()));
|
|
193
|
+
models = models.filter((m) => allowed.has(m.table));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Convert ModelMeta[] → ParsedSchema
|
|
197
|
+
const schema = modelMetaToSchema(adapterType, "express", models);
|
|
198
|
+
|
|
199
|
+
// Print via schema-printer
|
|
200
|
+
const output = printSchema(schema);
|
|
201
|
+
|
|
202
|
+
// Determine output path
|
|
203
|
+
const outPath = args.out || "dbmr.schema.json";
|
|
204
|
+
|
|
205
|
+
if (flags.json) {
|
|
206
|
+
// --json: output schema to stdout, no file write
|
|
207
|
+
ctx.result({ schema: JSON.parse(output), writtenTo: null });
|
|
208
|
+
} else if (flags.dryRun) {
|
|
209
|
+
// --dry-run: output schema to stdout, no file write
|
|
210
|
+
ctx.log(output);
|
|
211
|
+
ctx.log(`Would write to: ${outPath}`);
|
|
212
|
+
} else {
|
|
213
|
+
// Write to file
|
|
214
|
+
const resolvedPath = path.resolve(outPath);
|
|
215
|
+
fs.writeFileSync(resolvedPath, output, "utf8");
|
|
216
|
+
ctx.log(`Schema written to ${outPath}`);
|
|
217
|
+
ctx.log(output);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = inspect;
|
|
222
|
+
module.exports.modelMetaToSchema = modelMetaToSchema;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { generateModelFile } = require("./generate-model.js");
|
|
6
|
+
const {
|
|
7
|
+
generateRouteFile,
|
|
8
|
+
generateChildRouteFile,
|
|
9
|
+
generateRoutesIndexFile,
|
|
10
|
+
generateTestFile,
|
|
11
|
+
generateChildTestFile,
|
|
12
|
+
} = require("./generate-route.js");
|
|
13
|
+
const { generateOpenAPISpec } = require("./generate-openapi.js");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Simple line-by-line diff between two strings.
|
|
17
|
+
* Returns a human-readable unified-style diff string.
|
|
18
|
+
*/
|
|
19
|
+
function lineDiff(expected, actual) {
|
|
20
|
+
const expectedLines = expected.split("\n");
|
|
21
|
+
const actualLines = actual.split("\n");
|
|
22
|
+
const lines = [];
|
|
23
|
+
const maxLen = Math.max(expectedLines.length, actualLines.length);
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < maxLen; i++) {
|
|
26
|
+
const exp = i < expectedLines.length ? expectedLines[i] : undefined;
|
|
27
|
+
const act = i < actualLines.length ? actualLines[i] : undefined;
|
|
28
|
+
|
|
29
|
+
if (exp === act) continue;
|
|
30
|
+
if (act !== undefined && exp === undefined) {
|
|
31
|
+
lines.push(`+${i + 1}: ${act}`);
|
|
32
|
+
} else if (exp !== undefined && act === undefined) {
|
|
33
|
+
lines.push(`-${i + 1}: ${exp}`);
|
|
34
|
+
} else {
|
|
35
|
+
lines.push(`-${i + 1}: ${act}`);
|
|
36
|
+
lines.push(`+${i + 1}: ${exp}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a map of relative file path → expected content for all artifacts
|
|
44
|
+
* that the schema would generate.
|
|
45
|
+
*
|
|
46
|
+
* @param {Array<{table, structure, primary_key, unique, option}>} meta
|
|
47
|
+
* @param {Array<{parent, child, foreignKey}>} relationships
|
|
48
|
+
* @returns {Map<string, string>}
|
|
49
|
+
*/
|
|
50
|
+
function buildExpectedFiles(meta, relationships) {
|
|
51
|
+
const expected = new Map();
|
|
52
|
+
const modelsRelPath = "../models";
|
|
53
|
+
const tableNames = meta.map((m) => m.table).sort();
|
|
54
|
+
|
|
55
|
+
// Model files
|
|
56
|
+
for (const m of meta) {
|
|
57
|
+
expected.set(`models/${m.table}.js`, generateModelFile(m));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Route files (one per table)
|
|
61
|
+
for (const m of meta) {
|
|
62
|
+
expected.set(
|
|
63
|
+
`routes/${m.table}.js`,
|
|
64
|
+
generateRouteFile(m.table, modelsRelPath),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Child route files (one per relationship)
|
|
69
|
+
for (const rel of relationships) {
|
|
70
|
+
const childMeta = meta.find((m) => m.table === rel.child);
|
|
71
|
+
const pk = childMeta ? childMeta.primary_key : "id";
|
|
72
|
+
expected.set(
|
|
73
|
+
`routes/${rel.child}_child_of_${rel.parent}.js`,
|
|
74
|
+
generateChildRouteFile(
|
|
75
|
+
rel.child,
|
|
76
|
+
rel.parent,
|
|
77
|
+
rel.foreignKey,
|
|
78
|
+
modelsRelPath,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Routes index file
|
|
84
|
+
expected.set(
|
|
85
|
+
"routes/index.js",
|
|
86
|
+
generateRoutesIndexFile(tableNames, relationships),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Test files (one per table)
|
|
90
|
+
for (const m of meta) {
|
|
91
|
+
expected.set(
|
|
92
|
+
`test/${m.table}.test.js`,
|
|
93
|
+
generateTestFile(m.table, m.primary_key),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Child test files (one per relationship)
|
|
98
|
+
for (const rel of relationships) {
|
|
99
|
+
const childMeta = meta.find((m) => m.table === rel.child);
|
|
100
|
+
const pk = childMeta ? childMeta.primary_key : "id";
|
|
101
|
+
expected.set(
|
|
102
|
+
`test/${rel.child}_child_of_${rel.parent}.test.js`,
|
|
103
|
+
generateChildTestFile(rel.child, rel.parent, rel.foreignKey, pk),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// OpenAPI spec
|
|
108
|
+
expected.set(
|
|
109
|
+
"openapi.json",
|
|
110
|
+
JSON.stringify(generateOpenAPISpec(meta), null, 2) + "\n",
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return expected;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Scan known artifact directories on disk and return a set of relative paths
|
|
118
|
+
* that exist.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} baseDir
|
|
121
|
+
* @returns {Set<string>}
|
|
122
|
+
*/
|
|
123
|
+
function scanDiskFiles(baseDir) {
|
|
124
|
+
const files = new Set();
|
|
125
|
+
|
|
126
|
+
const dirs = [
|
|
127
|
+
{ dir: "models", ext: ".js" },
|
|
128
|
+
{ dir: "routes", ext: ".js" },
|
|
129
|
+
{ dir: "test", ext: ".test.js" },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const { dir, ext } of dirs) {
|
|
133
|
+
const fullDir = path.join(baseDir, dir);
|
|
134
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
135
|
+
for (const file of fs.readdirSync(fullDir)) {
|
|
136
|
+
if (file.endsWith(ext)) {
|
|
137
|
+
files.add(`${dir}/${file}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for openapi.json at root
|
|
143
|
+
const openapiPath = path.join(baseDir, "openapi.json");
|
|
144
|
+
if (fs.existsSync(openapiPath)) {
|
|
145
|
+
files.add("openapi.json");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return files;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Compare expected generated content against actual files on disk.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} baseDir — project root
|
|
155
|
+
* @param {Array<{table, structure, primary_key, unique, option}>} meta — from schema
|
|
156
|
+
* @param {Array<{parent, child, foreignKey}>} relationships
|
|
157
|
+
* @returns {{ added: string[], modified: Array<{file: string, diff: string}>, deleted: string[] }}
|
|
158
|
+
*/
|
|
159
|
+
function computeDiff(baseDir, meta, relationships) {
|
|
160
|
+
const expected = buildExpectedFiles(meta, relationships);
|
|
161
|
+
const diskFiles = scanDiskFiles(baseDir);
|
|
162
|
+
|
|
163
|
+
const added = [];
|
|
164
|
+
const modified = [];
|
|
165
|
+
const deleted = [];
|
|
166
|
+
|
|
167
|
+
// Check expected files against disk
|
|
168
|
+
for (const [relPath, expectedContent] of expected) {
|
|
169
|
+
const fullPath = path.join(baseDir, relPath);
|
|
170
|
+
if (!fs.existsSync(fullPath)) {
|
|
171
|
+
added.push(relPath);
|
|
172
|
+
} else {
|
|
173
|
+
const actualContent = fs.readFileSync(fullPath, "utf8");
|
|
174
|
+
if (actualContent !== expectedContent) {
|
|
175
|
+
modified.push({
|
|
176
|
+
file: relPath,
|
|
177
|
+
diff: lineDiff(expectedContent, actualContent),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// unchanged — not reported
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check disk files not in expected set → deleted
|
|
185
|
+
for (const diskFile of diskFiles) {
|
|
186
|
+
if (!expected.has(diskFile)) {
|
|
187
|
+
deleted.push(diskFile);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
added: added.sort(),
|
|
193
|
+
modified: modified.sort((a, b) => a.file.localeCompare(b.file)),
|
|
194
|
+
deleted: deleted.sort(),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { computeDiff, buildExpectedFiles, lineDiff };
|
package/src/cli/flags.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Universal flag parser and OutputContext for the db-model-router CLI.
|
|
5
|
+
*
|
|
6
|
+
* Parses --yes, --json, --dry-run, --no-install, --help from argv.
|
|
7
|
+
* Extracts the subcommand (first non-flag argument).
|
|
8
|
+
* Collects remaining key-value flags into an args object.
|
|
9
|
+
*
|
|
10
|
+
* @module cli/flags
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse CLI argv into subcommand, flags, and args.
|
|
15
|
+
*
|
|
16
|
+
* @param {string[]} argv - process.argv.slice(2) style array
|
|
17
|
+
* @returns {{ subcommand: string|null, flags: Flags, args: object }}
|
|
18
|
+
*/
|
|
19
|
+
function parseFlags(argv) {
|
|
20
|
+
const flags = {
|
|
21
|
+
yes: false,
|
|
22
|
+
json: false,
|
|
23
|
+
dryRun: false,
|
|
24
|
+
noInstall: false,
|
|
25
|
+
help: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const args = {};
|
|
29
|
+
let subcommand = null;
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const arg = argv[i];
|
|
33
|
+
|
|
34
|
+
if (arg === "--yes") {
|
|
35
|
+
flags.yes = true;
|
|
36
|
+
} else if (arg === "--json") {
|
|
37
|
+
flags.json = true;
|
|
38
|
+
} else if (arg === "--dry-run") {
|
|
39
|
+
flags.dryRun = true;
|
|
40
|
+
} else if (arg === "--no-install") {
|
|
41
|
+
flags.noInstall = true;
|
|
42
|
+
} else if (arg === "--help") {
|
|
43
|
+
flags.help = true;
|
|
44
|
+
} else if (arg.startsWith("--")) {
|
|
45
|
+
// Key-value flag: --from schema.json → { from: "schema.json" }
|
|
46
|
+
const key = arg.slice(2);
|
|
47
|
+
const next = argv[i + 1];
|
|
48
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
49
|
+
args[key] = next;
|
|
50
|
+
i++; // skip the value
|
|
51
|
+
} else {
|
|
52
|
+
args[key] = true;
|
|
53
|
+
}
|
|
54
|
+
} else if (subcommand === null) {
|
|
55
|
+
subcommand = arg;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { subcommand, flags, args };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* OutputContext controls CLI output behavior based on flags.
|
|
64
|
+
*
|
|
65
|
+
* When --json is active:
|
|
66
|
+
* - log() is a no-op (suppresses human-readable output)
|
|
67
|
+
* - result() accumulates data
|
|
68
|
+
* - flush() prints the accumulated JSON to stdout
|
|
69
|
+
*
|
|
70
|
+
* When --json is NOT active:
|
|
71
|
+
* - log() prints to stdout
|
|
72
|
+
* - result() is a no-op
|
|
73
|
+
* - flush() is a no-op
|
|
74
|
+
*/
|
|
75
|
+
class OutputContext {
|
|
76
|
+
constructor(flags) {
|
|
77
|
+
this._json = !!(flags && flags.json);
|
|
78
|
+
this._results = [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Log a human-readable message. No-op when --json is active.
|
|
83
|
+
* @param {string} msg
|
|
84
|
+
*/
|
|
85
|
+
log(msg) {
|
|
86
|
+
if (!this._json) {
|
|
87
|
+
console.log(msg);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Accumulate a result object for JSON output.
|
|
93
|
+
* @param {*} data
|
|
94
|
+
*/
|
|
95
|
+
result(data) {
|
|
96
|
+
this._results.push(data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Flush accumulated JSON results to stdout if --json is active.
|
|
101
|
+
* Prints a single JSON object (or the last result if only one was accumulated).
|
|
102
|
+
*/
|
|
103
|
+
flush() {
|
|
104
|
+
if (this._json && this._results.length > 0) {
|
|
105
|
+
const output =
|
|
106
|
+
this._results.length === 1 ? this._results[0] : this._results;
|
|
107
|
+
console.log(JSON.stringify(output));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { parseFlags, OutputContext };
|
|
@@ -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
|
}
|