db-model-router 1.0.4 → 1.0.6
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 +110 -16
- package/TODO.md +15 -0
- package/dbmr.schema.json +333 -0
- package/docker-compose.yml +1 -1
- package/package.json +8 -7
- package/scripts/demo-create.js +47 -0
- package/skill/SKILL.md +464 -0
- package/skill/references/cockroachdb.md +49 -0
- package/skill/references/dynamodb.md +53 -0
- package/skill/references/mongodb.md +56 -0
- package/skill/references/mssql.md +55 -0
- package/skill/references/oracle.md +52 -0
- package/skill/references/postgres.md +50 -0
- package/skill/references/redis.md +53 -0
- package/skill/references/sqlite3.md +43 -0
- package/src/cli/commands/generate.js +95 -31
- package/src/cli/commands/help.js +12 -7
- package/src/cli/commands/init.js +2 -2
- package/src/cli/commands/inspect.js +1 -0
- package/src/cli/diff-engine.js +54 -23
- package/src/cli/generate-db-manager.js +1573 -0
- package/src/cli/generate-docs-route.js +31 -0
- package/src/cli/generate-migration.js +356 -0
- package/src/cli/generate-model.js +9 -4
- package/src/cli/generate-openapi.js +40 -13
- package/src/cli/generate-route.js +55 -27
- package/src/cli/init/dependencies.js +3 -0
- package/src/cli/init/generators.js +37 -31
- package/src/cli/init.js +8 -8
- package/src/cli/main.js +2 -2
- package/src/cockroachdb/db.js +90 -59
- package/src/commons/route.js +20 -20
- package/src/commons/validator.js +58 -1
- package/src/dynamodb/db.js +50 -27
- package/src/mongodb/db.js +1 -0
- package/src/mssql/db.js +89 -61
- package/src/mysql/db.js +1 -0
- package/src/oracle/db.js +1 -0
- package/src/postgres/db.js +61 -41
- package/src/redis/db.js +1 -0
- package/src/schema/schema-parser.js +43 -1
- package/src/schema/schema-printer.js +7 -0
- package/src/schema/schema-validator.js +17 -0
- package/src/sqlite3/db.js +12 -0
- package/docs/SKILL.md +0 -419
- package/src/cli/commands/generate-llm-docs.js +0 -418
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a routes/docs.js file that serves Swagger UI for the OpenAPI spec.
|
|
5
|
+
* Uses swagger-ui-express to mount at /docs.
|
|
6
|
+
*
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
function generateDocsRoute() {
|
|
10
|
+
return `import express from "express";
|
|
11
|
+
import swaggerUi from "swagger-ui-express";
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const spec = JSON.parse(readFileSync(join(__dirname, "../openapi.json"), "utf8"));
|
|
18
|
+
|
|
19
|
+
const router = express.Router();
|
|
20
|
+
|
|
21
|
+
router.use("/", swaggerUi.serve);
|
|
22
|
+
router.get("/", swaggerUi.setup(spec, {
|
|
23
|
+
customSiteTitle: "API Documentation",
|
|
24
|
+
customCss: ".swagger-ui .topbar { display: none }",
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
export default router;
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { generateDocsRoute };
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate database migration SQL/JS from parsed schema tables.
|
|
5
|
+
*
|
|
6
|
+
* Produces CREATE TABLE statements (or equivalent) for each table,
|
|
7
|
+
* with proper column types, constraints, and indexes per adapter.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Column type mapping per adapter
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map a dbmr column rule to a SQL column type for the given adapter.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} rule - e.g. "required|string", "auto_increment", "integer"
|
|
18
|
+
* @param {string} adapter - e.g. "postgres", "mysql", "sqlite3"
|
|
19
|
+
* @returns {{ sqlType: string, nullable: boolean, isAutoIncrement: boolean }}
|
|
20
|
+
*/
|
|
21
|
+
function mapColumnType(rule, adapter) {
|
|
22
|
+
const parts = rule.split("|");
|
|
23
|
+
const isRequired = parts.includes("required");
|
|
24
|
+
const baseType = parts.filter((p) => p !== "required")[0] || "string";
|
|
25
|
+
|
|
26
|
+
let sqlType;
|
|
27
|
+
let isAutoIncrement = false;
|
|
28
|
+
|
|
29
|
+
switch (baseType) {
|
|
30
|
+
case "auto_increment":
|
|
31
|
+
isAutoIncrement = true;
|
|
32
|
+
sqlType = autoIncrementType(adapter);
|
|
33
|
+
break;
|
|
34
|
+
case "string":
|
|
35
|
+
sqlType = stringType(adapter);
|
|
36
|
+
break;
|
|
37
|
+
case "integer":
|
|
38
|
+
sqlType = integerType(adapter);
|
|
39
|
+
break;
|
|
40
|
+
case "numeric":
|
|
41
|
+
sqlType = numericType(adapter);
|
|
42
|
+
break;
|
|
43
|
+
case "boolean":
|
|
44
|
+
sqlType = booleanType(adapter);
|
|
45
|
+
break;
|
|
46
|
+
case "datetime":
|
|
47
|
+
sqlType = datetimeType(adapter);
|
|
48
|
+
break;
|
|
49
|
+
case "object":
|
|
50
|
+
sqlType = jsonType(adapter);
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
sqlType = stringType(adapter);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
sqlType,
|
|
58
|
+
nullable: !isRequired && !isAutoIncrement,
|
|
59
|
+
isAutoIncrement,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function autoIncrementType(adapter) {
|
|
64
|
+
switch (adapter) {
|
|
65
|
+
case "postgres":
|
|
66
|
+
case "cockroachdb":
|
|
67
|
+
return "SERIAL";
|
|
68
|
+
case "mssql":
|
|
69
|
+
return "INT IDENTITY(1,1)";
|
|
70
|
+
case "oracle":
|
|
71
|
+
return "NUMBER GENERATED BY DEFAULT AS IDENTITY";
|
|
72
|
+
case "sqlite3":
|
|
73
|
+
return "INTEGER";
|
|
74
|
+
default:
|
|
75
|
+
// mysql, mariadb
|
|
76
|
+
return "INT AUTO_INCREMENT";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stringType(adapter) {
|
|
81
|
+
if (adapter === "oracle") return "VARCHAR2(255)";
|
|
82
|
+
return "VARCHAR(255)";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function integerType(adapter) {
|
|
86
|
+
if (adapter === "oracle") return "NUMBER(10)";
|
|
87
|
+
return "INTEGER";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function numericType(adapter) {
|
|
91
|
+
if (adapter === "oracle") return "NUMBER(12,2)";
|
|
92
|
+
if (adapter === "mssql") return "DECIMAL(12,2)";
|
|
93
|
+
return "DECIMAL(12,2)";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function booleanType(adapter) {
|
|
97
|
+
if (adapter === "oracle") return "NUMBER(1)";
|
|
98
|
+
if (adapter === "mssql") return "BIT";
|
|
99
|
+
if (adapter === "mysql" || adapter === "mariadb") return "TINYINT(1)";
|
|
100
|
+
return "BOOLEAN";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function datetimeType(adapter) {
|
|
104
|
+
if (adapter === "mssql") return "DATETIME";
|
|
105
|
+
return "TIMESTAMP";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function jsonType(adapter) {
|
|
109
|
+
if (adapter === "postgres" || adapter === "cockroachdb") return "JSONB";
|
|
110
|
+
if (adapter === "oracle") return "CLOB";
|
|
111
|
+
if (adapter === "mssql") return "NVARCHAR(MAX)";
|
|
112
|
+
if (adapter === "sqlite3") return "TEXT";
|
|
113
|
+
return "JSON";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// SQL migration generators
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Generate a CREATE TABLE SQL statement for a single table.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} tableName
|
|
124
|
+
* @param {object} tableDef - parsed table definition from schema
|
|
125
|
+
* @param {string} adapter
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function generateCreateTableSQL(tableName, tableDef, adapter) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
const pk = tableDef.pk;
|
|
131
|
+
|
|
132
|
+
for (const [colName, rule] of Object.entries(tableDef.columns)) {
|
|
133
|
+
const { sqlType, nullable, isAutoIncrement } = mapColumnType(rule, adapter);
|
|
134
|
+
let line = ` ${quoteIdent(colName, adapter)} ${sqlType}`;
|
|
135
|
+
|
|
136
|
+
if (colName === pk) {
|
|
137
|
+
line += " PRIMARY KEY";
|
|
138
|
+
if (isAutoIncrement && adapter === "sqlite3") {
|
|
139
|
+
// SQLite needs AUTOINCREMENT after PRIMARY KEY for INTEGER type
|
|
140
|
+
line = ` ${quoteIdent(colName, adapter)} INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
if (!nullable) line += " NOT NULL";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Default values for timestamps
|
|
147
|
+
if (tableDef.timestamps) {
|
|
148
|
+
if (
|
|
149
|
+
colName === tableDef.timestamps.created_at ||
|
|
150
|
+
colName === tableDef.timestamps.modified_at
|
|
151
|
+
) {
|
|
152
|
+
line += defaultTimestamp(adapter);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Default for boolean softDelete column
|
|
157
|
+
if (tableDef.softDelete && colName === tableDef.softDelete) {
|
|
158
|
+
line += defaultBoolean(adapter);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
lines.push(line);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Unique constraints (excluding PK which is already PRIMARY KEY)
|
|
165
|
+
if (tableDef.unique && tableDef.unique.length > 0) {
|
|
166
|
+
const uniqueCols = tableDef.unique.filter((c) => c !== pk);
|
|
167
|
+
for (const col of uniqueCols) {
|
|
168
|
+
lines.push(` UNIQUE (${quoteIdent(col, adapter)})`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const createPrefix =
|
|
173
|
+
adapter === "oracle" || adapter === "mssql"
|
|
174
|
+
? `CREATE TABLE ${quoteIdent(tableName, adapter)}`
|
|
175
|
+
: `CREATE TABLE IF NOT EXISTS ${quoteIdent(tableName, adapter)}`;
|
|
176
|
+
|
|
177
|
+
return `${createPrefix} (\n${lines.join(",\n")}\n);\n`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Quote an identifier based on adapter.
|
|
182
|
+
*/
|
|
183
|
+
function quoteIdent(name, adapter) {
|
|
184
|
+
// Most adapters are fine without quoting for simple names
|
|
185
|
+
// but we keep it safe for reserved words
|
|
186
|
+
if (adapter === "mssql") return `[${name}]`;
|
|
187
|
+
if (adapter === "oracle") return `"${name.toUpperCase()}"`;
|
|
188
|
+
return name;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Default timestamp expression per adapter.
|
|
193
|
+
*/
|
|
194
|
+
function defaultTimestamp(adapter) {
|
|
195
|
+
switch (adapter) {
|
|
196
|
+
case "mssql":
|
|
197
|
+
return " DEFAULT GETDATE()";
|
|
198
|
+
case "oracle":
|
|
199
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
200
|
+
case "sqlite3":
|
|
201
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
202
|
+
default:
|
|
203
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Default boolean false expression per adapter.
|
|
209
|
+
*/
|
|
210
|
+
function defaultBoolean(adapter) {
|
|
211
|
+
if (adapter === "oracle" || adapter === "mssql") return " DEFAULT 0";
|
|
212
|
+
if (adapter === "mysql" || adapter === "mariadb") return " DEFAULT 0";
|
|
213
|
+
return " DEFAULT FALSE";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// NoSQL migration generators
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generate a MongoDB migration JS file for a single table (collection).
|
|
222
|
+
*/
|
|
223
|
+
function generateMongoDBMigration(tableName, tableDef) {
|
|
224
|
+
const uniqueCols = (tableDef.unique || []).filter((c) => c !== tableDef.pk);
|
|
225
|
+
const indexLines = uniqueCols
|
|
226
|
+
.map(
|
|
227
|
+
(col) =>
|
|
228
|
+
` await db.collection("${tableName}").createIndex({ ${col}: 1 }, { unique: true });`,
|
|
229
|
+
)
|
|
230
|
+
.join("\n");
|
|
231
|
+
|
|
232
|
+
return `"use strict";
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
async up(db) {
|
|
236
|
+
await db.createCollection("${tableName}");
|
|
237
|
+
${indexLines ? indexLines + "\n" : ""} },
|
|
238
|
+
|
|
239
|
+
async down(db) {
|
|
240
|
+
await db.collection("${tableName}").drop();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate a DynamoDB migration JS file for a single table.
|
|
248
|
+
*/
|
|
249
|
+
function generateDynamoDBMigration(tableName, tableDef) {
|
|
250
|
+
const pk = tableDef.pk;
|
|
251
|
+
return `import { CreateTableCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb";
|
|
252
|
+
|
|
253
|
+
export async function up(db) {
|
|
254
|
+
await db.send(new CreateTableCommand({
|
|
255
|
+
TableName: "${tableName}",
|
|
256
|
+
KeySchema: [{ AttributeName: "${pk}", KeyType: "HASH" }],
|
|
257
|
+
AttributeDefinitions: [{ AttributeName: "${pk}", AttributeType: "N" }],
|
|
258
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function down(db) {
|
|
263
|
+
await db.send(new DeleteTableCommand({ TableName: "${tableName}" }));
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Generate a Redis migration JS file (Redis doesn't need table creation,
|
|
270
|
+
* but we create a placeholder for consistency).
|
|
271
|
+
*/
|
|
272
|
+
function generateRedisMigration(tableName) {
|
|
273
|
+
return `"use strict";
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
async up(db) {
|
|
277
|
+
// Redis is schema-less. This migration is a placeholder.
|
|
278
|
+
// Data for "${tableName}" will be stored as hash keys: ${tableName}:<id>
|
|
279
|
+
console.log("Redis: ${tableName} collection ready (schema-less).");
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async down(db) {
|
|
283
|
+
// Warning: this deletes ALL keys matching the pattern
|
|
284
|
+
// In production, use SCAN instead of KEYS
|
|
285
|
+
const keys = await db.keys("${tableName}:*");
|
|
286
|
+
if (keys.length > 0) {
|
|
287
|
+
await db.del(...keys);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Public API
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
const SQL_ADAPTERS = [
|
|
299
|
+
"mysql",
|
|
300
|
+
"mariadb",
|
|
301
|
+
"postgres",
|
|
302
|
+
"sqlite3",
|
|
303
|
+
"mssql",
|
|
304
|
+
"cockroachdb",
|
|
305
|
+
"oracle",
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Generate migration file content for all tables in the schema.
|
|
310
|
+
*
|
|
311
|
+
* For SQL adapters: produces a single .sql file with all CREATE TABLE statements.
|
|
312
|
+
* For NoSQL adapters: produces one .js file per table.
|
|
313
|
+
*
|
|
314
|
+
* @param {object} schema - parsed schema from parseSchema()
|
|
315
|
+
* @returns {Array<{ filename: string, content: string }>}
|
|
316
|
+
*/
|
|
317
|
+
function generateMigrationFiles(schema) {
|
|
318
|
+
const adapter = schema.adapter;
|
|
319
|
+
const tables = schema.tables;
|
|
320
|
+
const tableNames = Object.keys(tables).sort();
|
|
321
|
+
|
|
322
|
+
if (SQL_ADAPTERS.includes(adapter)) {
|
|
323
|
+
// Single SQL file with all CREATE TABLE statements
|
|
324
|
+
const statements = [];
|
|
325
|
+
for (const name of tableNames) {
|
|
326
|
+
statements.push(generateCreateTableSQL(name, tables[name], adapter));
|
|
327
|
+
}
|
|
328
|
+
const content = statements.join("\n");
|
|
329
|
+
return [{ filename: `create_tables.sql`, content }];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// NoSQL: one file per table
|
|
333
|
+
const files = [];
|
|
334
|
+
for (const name of tableNames) {
|
|
335
|
+
let content;
|
|
336
|
+
if (adapter === "mongodb") {
|
|
337
|
+
content = generateMongoDBMigration(name, tables[name]);
|
|
338
|
+
} else if (adapter === "dynamodb") {
|
|
339
|
+
content = generateDynamoDBMigration(name, tables[name]);
|
|
340
|
+
} else if (adapter === "redis") {
|
|
341
|
+
content = generateRedisMigration(name);
|
|
342
|
+
} else {
|
|
343
|
+
// Fallback
|
|
344
|
+
content = `// Migration for ${name}\n`;
|
|
345
|
+
}
|
|
346
|
+
files.push({ filename: `create_${name}.js`, content });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return files;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
generateMigrationFiles,
|
|
354
|
+
generateCreateTableSQL,
|
|
355
|
+
mapColumnType,
|
|
356
|
+
};
|
|
@@ -544,7 +544,9 @@ function generateModelFile(m) {
|
|
|
544
544
|
if (opt.modified_at) parts.push(`modified_at: "${opt.modified_at}"`);
|
|
545
545
|
optionStr = `\n { ${parts.join(", ")} },`;
|
|
546
546
|
}
|
|
547
|
-
return `
|
|
547
|
+
return `import dbModelRouter from "db-model-router";
|
|
548
|
+
|
|
549
|
+
const { db, model } = dbModelRouter;
|
|
548
550
|
|
|
549
551
|
const ${varName} = model(
|
|
550
552
|
db,
|
|
@@ -554,7 +556,7 @@ const ${varName} = model(
|
|
|
554
556
|
${uniqueStr},${optionStr}
|
|
555
557
|
);
|
|
556
558
|
|
|
557
|
-
|
|
559
|
+
export default ${varName};
|
|
558
560
|
`;
|
|
559
561
|
}
|
|
560
562
|
|
|
@@ -563,11 +565,14 @@ function generateIndexFile(models) {
|
|
|
563
565
|
let exports = "";
|
|
564
566
|
for (const m of models) {
|
|
565
567
|
const varName = safeVarName(m.table);
|
|
566
|
-
imports += `
|
|
568
|
+
imports += `import ${varName} from "./${m.table}.js";\n`;
|
|
567
569
|
exports += ` ${varName},\n`;
|
|
568
570
|
}
|
|
569
571
|
return `${imports}
|
|
570
|
-
|
|
572
|
+
export {
|
|
573
|
+
${exports}};
|
|
574
|
+
|
|
575
|
+
export default {
|
|
571
576
|
${exports}};
|
|
572
577
|
`;
|
|
573
578
|
}
|
|
@@ -5,6 +5,13 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
5
5
|
const basePath = options.basePath || "/api";
|
|
6
6
|
const title = options.title || "REST Router API";
|
|
7
7
|
const version = options.version || "1.0.0";
|
|
8
|
+
const relationships = options.relationships || [];
|
|
9
|
+
|
|
10
|
+
// Build a lookup: child table -> { parent, foreignKey }
|
|
11
|
+
const childMap = {};
|
|
12
|
+
for (const rel of relationships) {
|
|
13
|
+
childMap[rel.child] = { parent: rel.parent, foreignKey: rel.foreignKey };
|
|
14
|
+
}
|
|
8
15
|
|
|
9
16
|
const spec = {
|
|
10
17
|
openapi: "3.0.3",
|
|
@@ -39,14 +46,34 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
const ref = { $ref: `#/components/schemas/${schemaName}` };
|
|
42
|
-
|
|
49
|
+
|
|
50
|
+
// Determine path prefix: nested under parent if this is a child table
|
|
51
|
+
let prefix;
|
|
52
|
+
const isChild = !!childMap[m.table];
|
|
53
|
+
let fkParam = null;
|
|
54
|
+
if (isChild) {
|
|
55
|
+
const { parent, foreignKey } = childMap[m.table];
|
|
56
|
+
prefix = `${basePath}/${parent}/{${foreignKey}}/${m.table}`;
|
|
57
|
+
fkParam = {
|
|
58
|
+
name: foreignKey,
|
|
59
|
+
in: "path",
|
|
60
|
+
required: true,
|
|
61
|
+
schema: { type: "string" },
|
|
62
|
+
description: `${capitalize(parent)} foreign key`,
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
prefix = `${basePath}/${m.table}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper: prepend FK path param for child routes
|
|
69
|
+
const withFk = (params) => (fkParam ? [fkParam, ...params] : params);
|
|
43
70
|
|
|
44
71
|
// GET / — list
|
|
45
72
|
spec.paths[`${prefix}/`] = {
|
|
46
73
|
get: {
|
|
47
74
|
tags: [tag],
|
|
48
75
|
summary: `List ${m.table}`,
|
|
49
|
-
parameters: [
|
|
76
|
+
parameters: withFk([
|
|
50
77
|
{
|
|
51
78
|
name: "page",
|
|
52
79
|
in: "query",
|
|
@@ -69,7 +96,7 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
69
96
|
in: "query",
|
|
70
97
|
schema: { type: "string", enum: ["json", "csv", "xml"] },
|
|
71
98
|
},
|
|
72
|
-
],
|
|
99
|
+
]),
|
|
73
100
|
responses: {
|
|
74
101
|
200: {
|
|
75
102
|
description: "Success",
|
|
@@ -141,7 +168,7 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
141
168
|
get: {
|
|
142
169
|
tags: [tag],
|
|
143
170
|
summary: `Get ${m.table} by ${pk}`,
|
|
144
|
-
parameters: [
|
|
171
|
+
parameters: withFk([
|
|
145
172
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
146
173
|
{ name: "select_columns", in: "query", schema: { type: "string" } },
|
|
147
174
|
{
|
|
@@ -149,7 +176,7 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
149
176
|
in: "query",
|
|
150
177
|
schema: { type: "string", enum: ["json", "csv", "xml"] },
|
|
151
178
|
},
|
|
152
|
-
],
|
|
179
|
+
]),
|
|
153
180
|
responses: {
|
|
154
181
|
200: {
|
|
155
182
|
description: "Success",
|
|
@@ -161,9 +188,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
161
188
|
post: {
|
|
162
189
|
tags: [tag],
|
|
163
190
|
summary: `Insert a ${m.table}`,
|
|
164
|
-
parameters: [
|
|
191
|
+
parameters: withFk([
|
|
165
192
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
166
|
-
],
|
|
193
|
+
]),
|
|
167
194
|
requestBody: { content: { "application/json": { schema: ref } } },
|
|
168
195
|
responses: {
|
|
169
196
|
200: {
|
|
@@ -175,9 +202,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
175
202
|
put: {
|
|
176
203
|
tags: [tag],
|
|
177
204
|
summary: `Update a ${m.table}`,
|
|
178
|
-
parameters: [
|
|
205
|
+
parameters: withFk([
|
|
179
206
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
180
|
-
],
|
|
207
|
+
]),
|
|
181
208
|
requestBody: { content: { "application/json": { schema: ref } } },
|
|
182
209
|
responses: {
|
|
183
210
|
200: {
|
|
@@ -190,9 +217,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
190
217
|
patch: {
|
|
191
218
|
tags: [tag],
|
|
192
219
|
summary: `Partial update a ${m.table}`,
|
|
193
|
-
parameters: [
|
|
220
|
+
parameters: withFk([
|
|
194
221
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
195
|
-
],
|
|
222
|
+
]),
|
|
196
223
|
requestBody: {
|
|
197
224
|
content: { "application/json": { schema: { type: "object" } } },
|
|
198
225
|
},
|
|
@@ -207,9 +234,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
207
234
|
delete: {
|
|
208
235
|
tags: [tag],
|
|
209
236
|
summary: `Delete a ${m.table}`,
|
|
210
|
-
parameters: [
|
|
237
|
+
parameters: withFk([
|
|
211
238
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
212
|
-
],
|
|
239
|
+
]),
|
|
213
240
|
responses: {
|
|
214
241
|
200: { description: "Deleted" },
|
|
215
242
|
404: { description: "Not Found" },
|