db-model-router 1.0.0
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/.env +7 -0
- package/LICENSE +201 -0
- package/README.md +505 -0
- package/docker-compose.yml +141 -0
- package/docs/README.md +208 -0
- package/docs/SKILL.md +202 -0
- package/docs/adapters/cockroachdb.md +49 -0
- package/docs/adapters/dynamodb.md +53 -0
- package/docs/adapters/mongodb.md +56 -0
- package/docs/adapters/mssql.md +55 -0
- package/docs/adapters/oracle.md +52 -0
- package/docs/adapters/postgres.md +50 -0
- package/docs/adapters/redis.md +53 -0
- package/docs/adapters/sqlite3.md +43 -0
- package/package.json +109 -0
- package/src/cli/generate-app.js +359 -0
- package/src/cli/generate-model.js +760 -0
- package/src/cli/generate-openapi.js +237 -0
- package/src/cli/generate-route.js +346 -0
- package/src/cockroachdb/db.js +563 -0
- package/src/commons/function.js +165 -0
- package/src/commons/model.js +444 -0
- package/src/commons/route.js +214 -0
- package/src/commons/validator.js +172 -0
- package/src/dynamodb/db.js +552 -0
- package/src/index.js +57 -0
- package/src/mongodb/db.js +381 -0
- package/src/mssql/db.js +461 -0
- package/src/mysql/db.js +527 -0
- package/src/oracle/db.js +855 -0
- package/src/oracle/sql_translator.js +406 -0
- package/src/postgres/db.js +666 -0
- package/src/postgres/ddl_translator.js +69 -0
- package/src/postgres/sql_translator.js +396 -0
- package/src/redis/db.js +448 -0
- package/src/serve.js +90 -0
- package/src/sqlite3/db.js +346 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate OpenAPI 3.0 spec from introspected model metadata.
|
|
3
|
+
*/
|
|
4
|
+
function generateOpenAPISpec(models, options = {}) {
|
|
5
|
+
const basePath = options.basePath || "/api";
|
|
6
|
+
const title = options.title || "REST Router API";
|
|
7
|
+
const version = options.version || "1.0.0";
|
|
8
|
+
|
|
9
|
+
const spec = {
|
|
10
|
+
openapi: "3.0.3",
|
|
11
|
+
info: { title, version, description: "Auto-generated by rest-router CLI" },
|
|
12
|
+
paths: {},
|
|
13
|
+
components: { schemas: {} },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
for (const m of models) {
|
|
17
|
+
const tag = m.table;
|
|
18
|
+
const pk = m.primary_key;
|
|
19
|
+
const schemaName = capitalize(m.table);
|
|
20
|
+
|
|
21
|
+
// Build schema from modelStructure
|
|
22
|
+
const properties = {};
|
|
23
|
+
const required = [];
|
|
24
|
+
// Add PK
|
|
25
|
+
properties[pk] = { type: "integer", description: "Primary key" };
|
|
26
|
+
for (const [col, rule] of Object.entries(m.structure)) {
|
|
27
|
+
const parsed = parseRule(rule);
|
|
28
|
+
properties[col] = { type: parsed.type };
|
|
29
|
+
if (parsed.required) required.push(col);
|
|
30
|
+
}
|
|
31
|
+
spec.components.schemas[schemaName] = {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties,
|
|
34
|
+
...(required.length > 0 ? { required } : {}),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ref = { $ref: `#/components/schemas/${schemaName}` };
|
|
38
|
+
const prefix = `${basePath}/${m.table}`;
|
|
39
|
+
|
|
40
|
+
// GET / — list
|
|
41
|
+
spec.paths[`${prefix}/`] = {
|
|
42
|
+
get: {
|
|
43
|
+
tags: [tag],
|
|
44
|
+
summary: `List ${m.table}`,
|
|
45
|
+
parameters: [
|
|
46
|
+
{
|
|
47
|
+
name: "page",
|
|
48
|
+
in: "query",
|
|
49
|
+
schema: { type: "integer", default: 0 },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "size",
|
|
53
|
+
in: "query",
|
|
54
|
+
schema: { type: "integer", default: 30 },
|
|
55
|
+
},
|
|
56
|
+
{ name: "sort", in: "query", schema: { type: "string" } },
|
|
57
|
+
{
|
|
58
|
+
name: "select_columns",
|
|
59
|
+
in: "query",
|
|
60
|
+
schema: { type: "string" },
|
|
61
|
+
description: "Comma-separated column names",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "output_content_type",
|
|
65
|
+
in: "query",
|
|
66
|
+
schema: { type: "string", enum: ["json", "csv", "xml"] },
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
responses: {
|
|
70
|
+
200: {
|
|
71
|
+
description: "Success",
|
|
72
|
+
content: {
|
|
73
|
+
"application/json": {
|
|
74
|
+
schema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
data: { type: "array", items: ref },
|
|
78
|
+
count: { type: "integer" },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
post: {
|
|
87
|
+
tags: [tag],
|
|
88
|
+
summary: `Bulk insert ${m.table}`,
|
|
89
|
+
requestBody: {
|
|
90
|
+
content: {
|
|
91
|
+
"application/json": {
|
|
92
|
+
schema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: { data: { type: "array", items: ref } },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
responses: { 200: { description: "Success" } },
|
|
100
|
+
},
|
|
101
|
+
put: {
|
|
102
|
+
tags: [tag],
|
|
103
|
+
summary: `Bulk update ${m.table}`,
|
|
104
|
+
requestBody: {
|
|
105
|
+
content: {
|
|
106
|
+
"application/json": {
|
|
107
|
+
schema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: { data: { type: "array", items: ref } },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
responses: { 200: { description: "Success" } },
|
|
115
|
+
},
|
|
116
|
+
delete: {
|
|
117
|
+
tags: [tag],
|
|
118
|
+
summary: `Bulk delete ${m.table}`,
|
|
119
|
+
requestBody: {
|
|
120
|
+
content: {
|
|
121
|
+
"application/json": {
|
|
122
|
+
schema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
data: { type: "array", items: { type: "object" } },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
responses: { 200: { description: "Success" } },
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// GET /:id, POST /:id, PUT /:id, PATCH /:id, DELETE /:id
|
|
136
|
+
spec.paths[`${prefix}/{${pk}}`] = {
|
|
137
|
+
get: {
|
|
138
|
+
tags: [tag],
|
|
139
|
+
summary: `Get ${m.table} by ${pk}`,
|
|
140
|
+
parameters: [
|
|
141
|
+
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
142
|
+
{ name: "select_columns", in: "query", schema: { type: "string" } },
|
|
143
|
+
{
|
|
144
|
+
name: "output_content_type",
|
|
145
|
+
in: "query",
|
|
146
|
+
schema: { type: "string", enum: ["json", "csv", "xml"] },
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
responses: {
|
|
150
|
+
200: {
|
|
151
|
+
description: "Success",
|
|
152
|
+
content: { "application/json": { schema: ref } },
|
|
153
|
+
},
|
|
154
|
+
404: { description: "Not Found" },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
post: {
|
|
158
|
+
tags: [tag],
|
|
159
|
+
summary: `Insert a ${m.table}`,
|
|
160
|
+
parameters: [
|
|
161
|
+
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
162
|
+
],
|
|
163
|
+
requestBody: { content: { "application/json": { schema: ref } } },
|
|
164
|
+
responses: {
|
|
165
|
+
200: {
|
|
166
|
+
description: "Created",
|
|
167
|
+
content: { "application/json": { schema: ref } },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
put: {
|
|
172
|
+
tags: [tag],
|
|
173
|
+
summary: `Update a ${m.table}`,
|
|
174
|
+
parameters: [
|
|
175
|
+
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
176
|
+
],
|
|
177
|
+
requestBody: { content: { "application/json": { schema: ref } } },
|
|
178
|
+
responses: {
|
|
179
|
+
200: {
|
|
180
|
+
description: "Updated",
|
|
181
|
+
content: { "application/json": { schema: ref } },
|
|
182
|
+
},
|
|
183
|
+
404: { description: "Not Found" },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
patch: {
|
|
187
|
+
tags: [tag],
|
|
188
|
+
summary: `Partial update a ${m.table}`,
|
|
189
|
+
parameters: [
|
|
190
|
+
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
191
|
+
],
|
|
192
|
+
requestBody: {
|
|
193
|
+
content: { "application/json": { schema: { type: "object" } } },
|
|
194
|
+
},
|
|
195
|
+
responses: {
|
|
196
|
+
200: {
|
|
197
|
+
description: "Updated",
|
|
198
|
+
content: { "application/json": { schema: ref } },
|
|
199
|
+
},
|
|
200
|
+
404: { description: "Not Found" },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
delete: {
|
|
204
|
+
tags: [tag],
|
|
205
|
+
summary: `Delete a ${m.table}`,
|
|
206
|
+
parameters: [
|
|
207
|
+
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
208
|
+
],
|
|
209
|
+
responses: {
|
|
210
|
+
200: { description: "Deleted" },
|
|
211
|
+
404: { description: "Not Found" },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return spec;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parseRule(rule) {
|
|
221
|
+
const parts = rule.split("|");
|
|
222
|
+
const isRequired = parts.includes("required");
|
|
223
|
+
let type = "string";
|
|
224
|
+
for (const p of parts) {
|
|
225
|
+
if (p === "integer") type = "integer";
|
|
226
|
+
else if (p === "numeric") type = "number";
|
|
227
|
+
else if (p === "object") type = "object";
|
|
228
|
+
else if (p === "string") type = "string";
|
|
229
|
+
}
|
|
230
|
+
return { type, required: isRequired };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function capitalize(str) {
|
|
234
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { generateOpenAPISpec };
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
const DB_TYPE_MAP = {
|
|
7
|
+
mysql: "mysql",
|
|
8
|
+
postgres: "postgres",
|
|
9
|
+
postgresql: "postgres",
|
|
10
|
+
sqlite3: "sqlite3",
|
|
11
|
+
mssql: "mssql",
|
|
12
|
+
oracle: "oracle",
|
|
13
|
+
cockroachdb: "cockroachdb",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SUPPORTED_TYPES = Object.keys(DB_TYPE_MAP);
|
|
17
|
+
|
|
18
|
+
function safeVarName(name) {
|
|
19
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) return name;
|
|
20
|
+
return name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a route file for a single model.
|
|
25
|
+
*/
|
|
26
|
+
function generateRouteFile(tableName, modelsRelPath) {
|
|
27
|
+
const varName = safeVarName(tableName);
|
|
28
|
+
return `const { route } = require("db-model-router");
|
|
29
|
+
const ${varName} = require("${modelsRelPath}/${tableName}");
|
|
30
|
+
|
|
31
|
+
module.exports = route(${varName});
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate a child route file that scopes queries by parent FK.
|
|
37
|
+
* e.g., posts/:post_id/comments — filters comments where post_id = :post_id
|
|
38
|
+
*/
|
|
39
|
+
function generateChildRouteFile(
|
|
40
|
+
childTable,
|
|
41
|
+
parentTable,
|
|
42
|
+
fkColumn,
|
|
43
|
+
modelsRelPath,
|
|
44
|
+
) {
|
|
45
|
+
const varName = safeVarName(childTable);
|
|
46
|
+
return `const { route } = require("db-model-router");
|
|
47
|
+
const ${varName} = require("${modelsRelPath}/${childTable}");
|
|
48
|
+
|
|
49
|
+
// Child route: scoped by parent ${parentTable} via ${fkColumn}
|
|
50
|
+
module.exports = route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate the routes index file that mounts all routes on an express Router.
|
|
56
|
+
* Supports parent-child nesting: parent/:pk/child
|
|
57
|
+
*/
|
|
58
|
+
function generateRoutesIndexFile(tableNames, relationships = []) {
|
|
59
|
+
let imports = `let express;\ntry { express = require("ultimate-express"); } catch (_) { express = require("express"); }\nconst router = express.Router();\n\n`;
|
|
60
|
+
|
|
61
|
+
// Collect child tables that are nested under parents
|
|
62
|
+
const nestedChildren = new Set();
|
|
63
|
+
for (const rel of relationships) {
|
|
64
|
+
nestedChildren.add(rel.child);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const table of tableNames) {
|
|
68
|
+
const varName = safeVarName(table);
|
|
69
|
+
imports += `const ${varName}Route = require("./${table}");\n`;
|
|
70
|
+
}
|
|
71
|
+
// Import child routes with _child suffix for nested ones
|
|
72
|
+
for (const rel of relationships) {
|
|
73
|
+
const varName = safeVarName(rel.child);
|
|
74
|
+
imports += `const ${varName}ChildRoute = require("./${rel.child}_child_of_${rel.parent}");\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
imports += "\n";
|
|
78
|
+
|
|
79
|
+
// Mount top-level routes (skip tables that are ONLY children)
|
|
80
|
+
for (const table of tableNames) {
|
|
81
|
+
if (nestedChildren.has(table)) continue;
|
|
82
|
+
const varName = safeVarName(table);
|
|
83
|
+
imports += `router.use("/${table}", ${varName}Route);\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Mount nested child routes under parent
|
|
87
|
+
for (const rel of relationships) {
|
|
88
|
+
const parentVar = safeVarName(rel.parent);
|
|
89
|
+
const childVar = safeVarName(rel.child);
|
|
90
|
+
// Find parent PK from model file name convention — use fkColumn without _id suffix as parent pk param
|
|
91
|
+
imports += `router.use("/${rel.parent}/:${rel.fkColumn}/${rel.child}", ${childVar}ChildRoute);\n`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Also mount children as top-level for direct access
|
|
95
|
+
for (const rel of relationships) {
|
|
96
|
+
const varName = safeVarName(rel.child);
|
|
97
|
+
imports += `router.use("/${rel.child}", ${varName}Route);\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
imports += "\nmodule.exports = router;\n";
|
|
101
|
+
return imports;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate the routes index file (simple version, no relationships).
|
|
106
|
+
*/
|
|
107
|
+
function generateSimpleRoutesIndexFile(tableNames) {
|
|
108
|
+
return generateRoutesIndexFile(tableNames, []);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read model directory to discover table names from generated model files.
|
|
113
|
+
* Looks for .js files that are not index.js.
|
|
114
|
+
*/
|
|
115
|
+
function discoverModels(modelsDir) {
|
|
116
|
+
if (!fs.existsSync(modelsDir)) return [];
|
|
117
|
+
const files = fs
|
|
118
|
+
.readdirSync(modelsDir)
|
|
119
|
+
.filter((f) => f.endsWith(".js") && f !== "index.js");
|
|
120
|
+
return files.map((f) => f.replace(/\.js$/, ""));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Main CLI ---
|
|
124
|
+
|
|
125
|
+
async function main() {
|
|
126
|
+
const args = parseArgs(process.argv.slice(2));
|
|
127
|
+
|
|
128
|
+
if (args.help) {
|
|
129
|
+
printUsage();
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const modelsDir = path.resolve(args.models || "./models");
|
|
134
|
+
const routesDir = path.resolve(args.output || "./routes");
|
|
135
|
+
|
|
136
|
+
// Check if models exist; if not, generate them first
|
|
137
|
+
let tableNames = discoverModels(modelsDir);
|
|
138
|
+
|
|
139
|
+
if (tableNames.length === 0) {
|
|
140
|
+
console.log("No models found. Generating models first...\n");
|
|
141
|
+
|
|
142
|
+
const dbType = DB_TYPE_MAP[(args.type || "").toLowerCase()];
|
|
143
|
+
if (!dbType) {
|
|
144
|
+
console.error(
|
|
145
|
+
`Error: No models found in "${modelsDir}" and no --type provided to generate them.\n` +
|
|
146
|
+
`Either generate models first with rest-router-generate-model, or provide --type to auto-generate.`,
|
|
147
|
+
);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Build the generate-model command args and run it
|
|
152
|
+
const generateArgs = ["--type", dbType, "--output", modelsDir];
|
|
153
|
+
if (args.host) generateArgs.push("--host", args.host);
|
|
154
|
+
if (args.port) generateArgs.push("--port", args.port);
|
|
155
|
+
if (args.database) generateArgs.push("--database", args.database);
|
|
156
|
+
if (args.user) generateArgs.push("--user", args.user);
|
|
157
|
+
if (args.password) generateArgs.push("--password", args.password);
|
|
158
|
+
if (args.schema) generateArgs.push("--schema", args.schema);
|
|
159
|
+
if (args.env) generateArgs.push("--env", args.env);
|
|
160
|
+
if (args.tables) generateArgs.push("--tables", args.tables);
|
|
161
|
+
|
|
162
|
+
const { execFileSync } = require("child_process");
|
|
163
|
+
try {
|
|
164
|
+
const generateScript = path.join(__dirname, "generate-model.js");
|
|
165
|
+
execFileSync(process.execPath, [generateScript, ...generateArgs], {
|
|
166
|
+
stdio: "inherit",
|
|
167
|
+
});
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error("Model generation failed.");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
tableNames = discoverModels(modelsDir);
|
|
174
|
+
if (tableNames.length === 0) {
|
|
175
|
+
console.error("No models were generated. Cannot create routes.");
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
console.log(""); // blank line after model generation output
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Calculate relative path from routes dir to models dir
|
|
182
|
+
const modelsRelPath = path.relative(routesDir, modelsDir).replace(/\\/g, "/");
|
|
183
|
+
|
|
184
|
+
// Parse --tables for parent.child relationships
|
|
185
|
+
const relationships = [];
|
|
186
|
+
if (args.tables) {
|
|
187
|
+
const tableSpecs = args.tables.split(",").map((s) => s.trim());
|
|
188
|
+
for (const spec of tableSpecs) {
|
|
189
|
+
if (spec.includes(".")) {
|
|
190
|
+
const parts = spec.split(".");
|
|
191
|
+
const parent = parts[0];
|
|
192
|
+
const child = parts[1];
|
|
193
|
+
// Guess FK column: parent_id or parent's PK name
|
|
194
|
+
// Convention: child table has a column named <parent>_id or <parent_singular>_id
|
|
195
|
+
const fkColumn = parent.replace(/s$/, "") + "_id";
|
|
196
|
+
// Only add if both tables exist in our model set
|
|
197
|
+
if (tableNames.includes(parent) && tableNames.includes(child)) {
|
|
198
|
+
relationships.push({ parent, child, fkColumn });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Write route files
|
|
205
|
+
if (!fs.existsSync(routesDir)) {
|
|
206
|
+
fs.mkdirSync(routesDir, { recursive: true });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const table of tableNames) {
|
|
210
|
+
const filePath = path.join(routesDir, table + ".js");
|
|
211
|
+
fs.writeFileSync(filePath, generateRouteFile(table, modelsRelPath));
|
|
212
|
+
console.log(` Created ${filePath}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Write child route files for parent-child relationships
|
|
216
|
+
for (const rel of relationships) {
|
|
217
|
+
const fileName = `${rel.child}_child_of_${rel.parent}.js`;
|
|
218
|
+
const filePath = path.join(routesDir, fileName);
|
|
219
|
+
fs.writeFileSync(
|
|
220
|
+
filePath,
|
|
221
|
+
generateChildRouteFile(
|
|
222
|
+
rel.child,
|
|
223
|
+
rel.parent,
|
|
224
|
+
rel.fkColumn,
|
|
225
|
+
modelsRelPath,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
console.log(` Created ${filePath}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const indexPath = path.join(routesDir, "index.js");
|
|
232
|
+
fs.writeFileSync(
|
|
233
|
+
indexPath,
|
|
234
|
+
generateRoutesIndexFile(tableNames, relationships),
|
|
235
|
+
);
|
|
236
|
+
console.log(` Created ${indexPath}`);
|
|
237
|
+
|
|
238
|
+
console.log(`\nGenerated ${tableNames.length} route(s) in ${routesDir}`);
|
|
239
|
+
|
|
240
|
+
// Generate OpenAPI spec if model metadata is available
|
|
241
|
+
try {
|
|
242
|
+
const { introspectSQLite3 } = require("./generate-model.js");
|
|
243
|
+
// Try to read model files to extract metadata for OpenAPI
|
|
244
|
+
const { generateOpenAPISpec } = require("./generate-openapi.js");
|
|
245
|
+
const modelMeta = [];
|
|
246
|
+
for (const table of tableNames) {
|
|
247
|
+
const modelPath = path.join(modelsDir, table + ".js");
|
|
248
|
+
if (fs.existsSync(modelPath)) {
|
|
249
|
+
const content = fs.readFileSync(modelPath, "utf8");
|
|
250
|
+
// Extract structure, pk, unique from generated model file
|
|
251
|
+
const meta = parseModelFile(content, table);
|
|
252
|
+
if (meta) modelMeta.push(meta);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (modelMeta.length > 0) {
|
|
256
|
+
const spec = generateOpenAPISpec(modelMeta);
|
|
257
|
+
const specPath = path.join(routesDir, "openapi.json");
|
|
258
|
+
fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
|
|
259
|
+
console.log(` Created ${specPath}`);
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
// OpenAPI generation is optional, don't fail
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
process.exit(0);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Parse a generated model file to extract metadata for OpenAPI generation.
|
|
270
|
+
*/
|
|
271
|
+
function parseModelFile(content, tableName) {
|
|
272
|
+
try {
|
|
273
|
+
// Extract structure JSON
|
|
274
|
+
const structMatch = content.match(/model\(\s*\n?\s*db,\s*\n?\s*"[^"]+",\s*\n?\s*(\{[\s\S]*?\}),/);
|
|
275
|
+
if (!structMatch) return null;
|
|
276
|
+
const structure = JSON.parse(structMatch[1]);
|
|
277
|
+
// Extract primary key
|
|
278
|
+
const pkMatch = content.match(/"([^"]+)",\s*\n?\s*\[/);
|
|
279
|
+
const primary_key = pkMatch ? pkMatch[1] : "id";
|
|
280
|
+
return { table: tableName, structure, primary_key };
|
|
281
|
+
} catch (e) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseArgs(argv) {
|
|
286
|
+
const args = {};
|
|
287
|
+
for (let i = 0; i < argv.length; i++) {
|
|
288
|
+
const arg = argv[i];
|
|
289
|
+
if (arg.startsWith("--")) {
|
|
290
|
+
const key = arg.slice(2);
|
|
291
|
+
const next = argv[i + 1];
|
|
292
|
+
if (next && !next.startsWith("--")) {
|
|
293
|
+
args[key] = next;
|
|
294
|
+
i++;
|
|
295
|
+
} else {
|
|
296
|
+
args[key] = true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return args;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function printUsage() {
|
|
304
|
+
console.log(`
|
|
305
|
+
Usage: rest-router-generate-route [options]
|
|
306
|
+
|
|
307
|
+
Options:
|
|
308
|
+
--models Path to models directory (default: ./models)
|
|
309
|
+
--output Output directory for routes (default: ./routes)
|
|
310
|
+
--type Database type — used to auto-generate models if missing
|
|
311
|
+
(${SUPPORTED_TYPES.join(", ")})
|
|
312
|
+
--host Database host (passed to model generation)
|
|
313
|
+
--port Database port (passed to model generation)
|
|
314
|
+
--database Database name or file path (passed to model generation)
|
|
315
|
+
--user Database user (passed to model generation)
|
|
316
|
+
--password Database password (passed to model generation)
|
|
317
|
+
--schema Schema name, postgres only (passed to model generation)
|
|
318
|
+
--env Path to .env file (passed to model generation)
|
|
319
|
+
--help Show this help message
|
|
320
|
+
|
|
321
|
+
Examples:
|
|
322
|
+
# Generate routes from existing models
|
|
323
|
+
rest-router-generate-route --models ./models --output ./routes
|
|
324
|
+
|
|
325
|
+
# Auto-generate models + routes in one step
|
|
326
|
+
rest-router-generate-route --type mysql --env .env --models ./models --output ./routes
|
|
327
|
+
|
|
328
|
+
# SQLite3 example
|
|
329
|
+
rest-router-generate-route --type sqlite3 --database ./myapp.db
|
|
330
|
+
`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (require.main === module) {
|
|
334
|
+
main().catch((err) => {
|
|
335
|
+
console.error("Error:", err.message);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
module.exports = {
|
|
341
|
+
generateRouteFile,
|
|
342
|
+
generateChildRouteFile,
|
|
343
|
+
generateRoutesIndexFile,
|
|
344
|
+
discoverModels,
|
|
345
|
+
safeVarName,
|
|
346
|
+
};
|