db-model-router 1.0.11 → 1.0.13
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 +6 -9
- package/dbmr.postgres.schema.json +269 -0
- package/dbmr.postgres.test.schema.json +705 -0
- package/docs/dbmr-schema-spec.md +26 -38
- package/package.json +3 -2
- package/scripts/demo-create.js +63 -23
- package/skill/SKILL.md +4 -4
- package/src/cli/commands/diff.js +17 -2
- package/src/cli/commands/generate.js +96 -52
- package/src/cli/diff-engine.js +65 -44
- package/src/cli/generate-migration.js +25 -13
- package/src/cli/generate-route.js +11 -4
- package/src/cli/init/generators.js +0 -125
- package/src/cli/init.js +0 -13
- package/src/schema/schema-validator.js +27 -10
|
@@ -131,16 +131,16 @@ function autoIncrementType(adapter) {
|
|
|
131
131
|
switch (adapter) {
|
|
132
132
|
case "postgres":
|
|
133
133
|
case "cockroachdb":
|
|
134
|
-
return "
|
|
134
|
+
return "BIGSERIAL";
|
|
135
135
|
case "mssql":
|
|
136
|
-
return "
|
|
136
|
+
return "BIGINT IDENTITY(1,1)";
|
|
137
137
|
case "oracle":
|
|
138
|
-
return "NUMBER GENERATED BY DEFAULT AS IDENTITY";
|
|
138
|
+
return "NUMBER(19) GENERATED BY DEFAULT AS IDENTITY";
|
|
139
139
|
case "sqlite3":
|
|
140
140
|
return "INTEGER";
|
|
141
141
|
default:
|
|
142
142
|
// mysql, mariadb
|
|
143
|
-
return "
|
|
143
|
+
return "BIGINT AUTO_INCREMENT";
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -354,9 +354,17 @@ function generateCreateTableSQL(tableName, tableDef, adapter) {
|
|
|
354
354
|
|
|
355
355
|
// Unique constraints (excluding PK which is already PRIMARY KEY)
|
|
356
356
|
if (tableDef.unique && tableDef.unique.length > 0) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
357
|
+
// Support both flat arrays (wrapped as one composite group) and array-of-arrays
|
|
358
|
+
const uniqueGroups = Array.isArray(tableDef.unique[0])
|
|
359
|
+
? tableDef.unique
|
|
360
|
+
: [tableDef.unique];
|
|
361
|
+
for (const group of uniqueGroups) {
|
|
362
|
+
const cols = group.filter((c) => c !== pk);
|
|
363
|
+
if (cols.length > 0) {
|
|
364
|
+
lines.push(
|
|
365
|
+
` UNIQUE (${cols.map((c) => quoteIdent(c, adapter)).join(", ")})`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
360
368
|
}
|
|
361
369
|
}
|
|
362
370
|
|
|
@@ -412,12 +420,16 @@ function defaultBoolean(adapter) {
|
|
|
412
420
|
* Generate a MongoDB migration JS file for a single table (collection).
|
|
413
421
|
*/
|
|
414
422
|
function generateMongoDBMigration(tableName, tableDef) {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
423
|
+
const uniqueGroups = Array.isArray((tableDef.unique || [])[0])
|
|
424
|
+
? tableDef.unique || []
|
|
425
|
+
: [tableDef.unique || []];
|
|
426
|
+
const indexLines = uniqueGroups
|
|
427
|
+
.flatMap((group) => {
|
|
428
|
+
const cols = group.filter((c) => c !== tableDef.pk);
|
|
429
|
+
if (cols.length === 0) return [];
|
|
430
|
+
const indexObj = cols.map((c) => `${c}: 1`).join(", ");
|
|
431
|
+
return ` await db.collection("${tableName}").createIndex({ ${indexObj} }, { unique: true });`;
|
|
432
|
+
})
|
|
421
433
|
.join("\n");
|
|
422
434
|
|
|
423
435
|
return `"use strict";
|
|
@@ -42,11 +42,15 @@ export default router;
|
|
|
42
42
|
* Generate a parent route file that includes its own CRUD and mounts child routes.
|
|
43
43
|
* e.g., routes/orders/index.js mounts order_items under /:order_id/items
|
|
44
44
|
*
|
|
45
|
+
* When the parent is itself a child (intermediate node), its own CRUD is scoped
|
|
46
|
+
* by the parentForeignKey so it filters on params from the ancestor router.
|
|
47
|
+
*
|
|
45
48
|
* @param {string} tableName - Parent table name
|
|
46
49
|
* @param {Array<{child, foreignKey}>} children - Child relationships for this parent
|
|
50
|
+
* @param {string} [parentForeignKey] - If set, own CRUD is scoped by this FK from an ancestor
|
|
47
51
|
* @returns {string}
|
|
48
52
|
*/
|
|
49
|
-
function generateParentRouteFile(tableName, children) {
|
|
53
|
+
function generateParentRouteFile(tableName, children, parentForeignKey) {
|
|
50
54
|
const varName = safeVarName(tableName);
|
|
51
55
|
let code = `import dbModelRouter from "db-model-router";
|
|
52
56
|
import express from "express";
|
|
@@ -71,9 +75,12 @@ const { route } = dbModelRouter;
|
|
|
71
75
|
code += `router.use("/:${child.foreignKey}/${child.child}", ${childVar}Route);\n`;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
const scope = parentForeignKey
|
|
79
|
+
? `, { ${parentForeignKey}: "params.${parentForeignKey}" }`
|
|
80
|
+
: "";
|
|
74
81
|
code += `
|
|
75
82
|
// CRUD routes for ${tableName}
|
|
76
|
-
router.use("/", route(${varName}));
|
|
83
|
+
router.use("/", route(${varName}${scope}));
|
|
77
84
|
|
|
78
85
|
export default router;
|
|
79
86
|
`;
|
|
@@ -372,7 +379,7 @@ function columnToFaker(col, rule) {
|
|
|
372
379
|
/**
|
|
373
380
|
* Generate a child route test file that tests the nested parent/:fk/child endpoints.
|
|
374
381
|
*/
|
|
375
|
-
function generateChildTestFile(childTable, parentTable, fkColumn, pk) {
|
|
382
|
+
function generateChildTestFile(childTable, parentTable, fkColumn, pk, modelsRelPath = "../models/") {
|
|
376
383
|
const childVar = safeVarName(childTable);
|
|
377
384
|
return `import assert from "assert";
|
|
378
385
|
import express from "express";
|
|
@@ -381,7 +388,7 @@ import dbModelRouter from "db-model-router";
|
|
|
381
388
|
|
|
382
389
|
const { route } = dbModelRouter;
|
|
383
390
|
|
|
384
|
-
import ${childVar} from "
|
|
391
|
+
import ${childVar} from "${modelsRelPath}${childTable}.js";
|
|
385
392
|
|
|
386
393
|
function createApp() {
|
|
387
394
|
const app = express();
|
|
@@ -900,108 +900,6 @@ const DOCKER_DB_MAP = {
|
|
|
900
900
|
},
|
|
901
901
|
};
|
|
902
902
|
|
|
903
|
-
/**
|
|
904
|
-
* CloudBeaver JDBC driver IDs and URL templates per database.
|
|
905
|
-
*/
|
|
906
|
-
const CLOUDBEAVER_DB_MAP = {
|
|
907
|
-
mysql: {
|
|
908
|
-
provider: "mysql",
|
|
909
|
-
driver: "mysql8",
|
|
910
|
-
urlTemplate: (host, port, dbName) =>
|
|
911
|
-
`jdbc:mysql://${host}:${port}/${dbName}`,
|
|
912
|
-
},
|
|
913
|
-
mariadb: {
|
|
914
|
-
provider: "mysql",
|
|
915
|
-
driver: "mariaDB",
|
|
916
|
-
urlTemplate: (host, port, dbName) =>
|
|
917
|
-
`jdbc:mariadb://${host}:${port}/${dbName}`,
|
|
918
|
-
},
|
|
919
|
-
postgres: {
|
|
920
|
-
provider: "postgresql",
|
|
921
|
-
driver: "postgres-jdbc",
|
|
922
|
-
urlTemplate: (host, port, dbName) =>
|
|
923
|
-
`jdbc:postgresql://${host}:${port}/${dbName}`,
|
|
924
|
-
},
|
|
925
|
-
cockroachdb: {
|
|
926
|
-
provider: "postgresql",
|
|
927
|
-
driver: "postgres-jdbc",
|
|
928
|
-
urlTemplate: (host, port, dbName) =>
|
|
929
|
-
`jdbc:postgresql://${host}:${port}/${dbName}`,
|
|
930
|
-
},
|
|
931
|
-
mssql: {
|
|
932
|
-
provider: "sqlserver",
|
|
933
|
-
driver: "mssql_jdbc_ms_new",
|
|
934
|
-
urlTemplate: (host, port, dbName) =>
|
|
935
|
-
`jdbc:sqlserver://${host}:${port};databaseName=${dbName};trustServerCertificate=true`,
|
|
936
|
-
},
|
|
937
|
-
oracle: {
|
|
938
|
-
provider: "oracle",
|
|
939
|
-
driver: "oracle_thin",
|
|
940
|
-
urlTemplate: (host, port, dbName) =>
|
|
941
|
-
`jdbc:oracle:thin:@${host}:${port}/${dbName}`,
|
|
942
|
-
},
|
|
943
|
-
mongodb: {
|
|
944
|
-
provider: "mongodb",
|
|
945
|
-
driver: "mongodb",
|
|
946
|
-
urlTemplate: (host, port, dbName) => `mongodb://${host}:${port}/${dbName}`,
|
|
947
|
-
},
|
|
948
|
-
};
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* Generate CloudBeaver data-sources.json for auto-connecting to the project database.
|
|
952
|
-
* @param {import('./types').InitAnswers} answers
|
|
953
|
-
* @param {object} secrets
|
|
954
|
-
* @returns {string|null}
|
|
955
|
-
*/
|
|
956
|
-
function generateCloudBeaverDataSources(answers, secrets) {
|
|
957
|
-
const cbDb = CLOUDBEAVER_DB_MAP[answers.database];
|
|
958
|
-
if (!cbDb) return null;
|
|
959
|
-
|
|
960
|
-
const dbConfig = DOCKER_DB_MAP[answers.database];
|
|
961
|
-
if (!dbConfig) return null;
|
|
962
|
-
|
|
963
|
-
const host = answers.database; // service name in docker-compose
|
|
964
|
-
const port = dbConfig.port.split(":")[1];
|
|
965
|
-
const dbName = "my_app";
|
|
966
|
-
|
|
967
|
-
// Determine user/pass based on adapter
|
|
968
|
-
let user = "root";
|
|
969
|
-
let pass = secrets.dbPass;
|
|
970
|
-
if (answers.database === "postgres" || answers.database === "cockroachdb")
|
|
971
|
-
user = "postgres";
|
|
972
|
-
if (answers.database === "mssql") user = "sa";
|
|
973
|
-
if (answers.database === "oracle") user = "system";
|
|
974
|
-
if (answers.database === "mongodb") user = "root";
|
|
975
|
-
|
|
976
|
-
const connId = `${answers.database}-project-db`;
|
|
977
|
-
const url = cbDb.urlTemplate(host, port, dbName);
|
|
978
|
-
|
|
979
|
-
const config = {
|
|
980
|
-
folders: {},
|
|
981
|
-
connections: {
|
|
982
|
-
[connId]: {
|
|
983
|
-
provider: cbDb.provider,
|
|
984
|
-
driver: cbDb.driver,
|
|
985
|
-
name: `${answers.database} - my_app`,
|
|
986
|
-
"save-password": true,
|
|
987
|
-
configuration: {
|
|
988
|
-
host: host,
|
|
989
|
-
port: port,
|
|
990
|
-
database: dbName,
|
|
991
|
-
url: url,
|
|
992
|
-
configurationType: "MANUAL",
|
|
993
|
-
type: "dev",
|
|
994
|
-
auth: "native",
|
|
995
|
-
userName: user,
|
|
996
|
-
userPassword: pass,
|
|
997
|
-
},
|
|
998
|
-
},
|
|
999
|
-
},
|
|
1000
|
-
};
|
|
1001
|
-
|
|
1002
|
-
return JSON.stringify(config, null, 2) + "\n";
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
903
|
/**
|
|
1006
904
|
* Generate docker-compose.yml content.
|
|
1007
905
|
* @param {import('./types').InitAnswers} answers
|
|
@@ -1057,27 +955,6 @@ function generateDockerCompose(answers, secrets) {
|
|
|
1057
955
|
services["redis"] = redisService;
|
|
1058
956
|
}
|
|
1059
957
|
|
|
1060
|
-
// --- CloudBeaver service (for SQL/MongoDB databases) ---
|
|
1061
|
-
const hasCbSupport = !!CLOUDBEAVER_DB_MAP[answers.database];
|
|
1062
|
-
if (hasCbSupport) {
|
|
1063
|
-
services["cloudbeaver"] = {
|
|
1064
|
-
container_name: "cloudbeaver",
|
|
1065
|
-
image: "dbeaver/cloudbeaver:latest",
|
|
1066
|
-
ports: ["8978:8978"],
|
|
1067
|
-
restart: "unless-stopped",
|
|
1068
|
-
environment: {
|
|
1069
|
-
CB_SERVER_NAME: "CloudBeaver",
|
|
1070
|
-
CB_ADMIN_NAME: "cbadmin",
|
|
1071
|
-
CB_ADMIN_PASSWORD: secrets.dbPass,
|
|
1072
|
-
},
|
|
1073
|
-
volumes: [
|
|
1074
|
-
"./data/cloudbeaver:/opt/cloudbeaver/workspace",
|
|
1075
|
-
"./.cloudbeaver/data-sources.json:/opt/cloudbeaver/workspace/GlobalConfiguration/.dbeaver/data-sources.json:ro",
|
|
1076
|
-
],
|
|
1077
|
-
depends_on: [answers.database],
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
958
|
// --- Loki + Grafana (when logger + loki are enabled) ---
|
|
1082
959
|
if (answers.loki) {
|
|
1083
960
|
services["loki"] = {
|
|
@@ -1225,7 +1102,6 @@ function generateGitignore() {
|
|
|
1225
1102
|
.env
|
|
1226
1103
|
*.db
|
|
1227
1104
|
data/
|
|
1228
|
-
.cloudbeaver/
|
|
1229
1105
|
`;
|
|
1230
1106
|
}
|
|
1231
1107
|
|
|
@@ -1713,7 +1589,6 @@ module.exports = {
|
|
|
1713
1589
|
generateDockerignore,
|
|
1714
1590
|
generateGrafanaDatasources,
|
|
1715
1591
|
generateDockerCompose,
|
|
1716
|
-
generateCloudBeaverDataSources,
|
|
1717
1592
|
generateSessionJs,
|
|
1718
1593
|
generateMigrateModule,
|
|
1719
1594
|
generateAddMigrationModule,
|
package/src/cli/init.js
CHANGED
|
@@ -17,7 +17,6 @@ const {
|
|
|
17
17
|
generateDockerignore,
|
|
18
18
|
generateGrafanaDatasources,
|
|
19
19
|
generateDockerCompose,
|
|
20
|
-
generateCloudBeaverDataSources,
|
|
21
20
|
generateSessionJs,
|
|
22
21
|
generateMigrateModule,
|
|
23
22
|
generateAddMigrationModule,
|
|
@@ -124,18 +123,6 @@ function generateFiles(answers, outputDir) {
|
|
|
124
123
|
files.push("docker-compose.yml");
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
// CloudBeaver data-sources.json (auto-connect config)
|
|
128
|
-
const cbDataSources = generateCloudBeaverDataSources(answers, secrets);
|
|
129
|
-
if (cbDataSources !== null) {
|
|
130
|
-
const cbDir = ".cloudbeaver";
|
|
131
|
-
if (!fs.existsSync(cbDir)) {
|
|
132
|
-
fs.mkdirSync(cbDir, { recursive: true });
|
|
133
|
-
}
|
|
134
|
-
const cbPath = path.join(cbDir, "data-sources.json");
|
|
135
|
-
if (safeWriteFile(cbPath, cbDataSources))
|
|
136
|
-
files.push(".cloudbeaver/data-sources.json");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
126
|
// Grafana datasource provisioning (when loki is enabled)
|
|
140
127
|
if (answers.loki) {
|
|
141
128
|
const grafanaDir = ".grafana";
|
|
@@ -165,18 +165,35 @@ function validateTables(tables, errors) {
|
|
|
165
165
|
message: `unique must be an array in table "${tableName}"`,
|
|
166
166
|
});
|
|
167
167
|
} else {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
// Support both flat arrays and array-of-arrays
|
|
169
|
+
const groups = Array.isArray(tableDef.unique[0])
|
|
170
|
+
? tableDef.unique
|
|
171
|
+
: [tableDef.unique];
|
|
172
|
+
for (let g = 0; g < groups.length; g++) {
|
|
173
|
+
const group = groups[g];
|
|
174
|
+
const groupPath = Array.isArray(tableDef.unique[0])
|
|
175
|
+
? `${basePath}.unique[${g}]`
|
|
176
|
+
: `${basePath}.unique`;
|
|
177
|
+
if (!Array.isArray(group)) {
|
|
171
178
|
errors.push({
|
|
172
|
-
path:
|
|
173
|
-
message: `unique
|
|
174
|
-
});
|
|
175
|
-
} else if (entry !== pk && !columnNames.has(entry)) {
|
|
176
|
-
errors.push({
|
|
177
|
-
path: `${basePath}.unique[${i}]`,
|
|
178
|
-
message: `unique entry "${entry}" does not match any column or the primary key "${pk}" in table "${tableName}"`,
|
|
179
|
+
path: groupPath,
|
|
180
|
+
message: `unique constraint must be an array of column names in table "${tableName}"`,
|
|
179
181
|
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
for (let i = 0; i < group.length; i++) {
|
|
185
|
+
const entry = group[i];
|
|
186
|
+
if (typeof entry !== "string") {
|
|
187
|
+
errors.push({
|
|
188
|
+
path: `${groupPath}[${i}]`,
|
|
189
|
+
message: `unique entry must be a string in table "${tableName}"`,
|
|
190
|
+
});
|
|
191
|
+
} else if (entry !== pk && !columnNames.has(entry)) {
|
|
192
|
+
errors.push({
|
|
193
|
+
path: `${groupPath}[${i}]`,
|
|
194
|
+
message: `unique entry "${entry}" does not match any column or the primary key "${pk}" in table "${tableName}"`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
180
197
|
}
|
|
181
198
|
}
|
|
182
199
|
}
|