db-model-router 1.0.4 → 1.0.5
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 +14 -0
- package/dbmr.schema.json +333 -0
- package/demo/.dockerignore +7 -0
- package/demo/.env.example +13 -0
- package/demo/Dockerfile +20 -0
- package/demo/app.js +37 -0
- package/demo/commons/add_migration.js +43 -0
- package/demo/commons/db.js +17 -0
- package/demo/commons/migrate.js +65 -0
- package/demo/commons/security.js +30 -0
- package/demo/commons/session.js +13 -0
- package/demo/dbmr.schema.json +362 -0
- package/demo/docs/llm.md +197 -0
- package/demo/llms.txt +70 -0
- package/demo/middleware/logger.js +67 -0
- package/demo/migrations/20260430155808_create_migrations_table.sql +6 -0
- package/demo/migrations/20260430155809_create_tables.sql +207 -0
- package/demo/models/addresses.js +22 -0
- package/demo/models/cart_items.js +18 -0
- package/demo/models/carts.js +16 -0
- package/demo/models/categories.js +20 -0
- package/demo/models/coupons.js +23 -0
- package/demo/models/order_items.js +21 -0
- package/demo/models/orders.js +25 -0
- package/demo/models/payments.js +21 -0
- package/demo/models/product_images.js +18 -0
- package/demo/models/product_reviews.js +20 -0
- package/demo/models/product_variants.js +20 -0
- package/demo/models/products.js +30 -0
- package/demo/models/shipments.js +19 -0
- package/demo/models/users.js +19 -0
- package/demo/models/wishlists.js +15 -0
- package/demo/openapi.json +5872 -0
- package/demo/package-lock.json +2810 -0
- package/demo/package.json +34 -0
- package/demo/routes/addresses.js +6 -0
- package/demo/routes/carts/cart_items.js +7 -0
- package/demo/routes/carts.js +6 -0
- package/demo/routes/categories.js +6 -0
- package/demo/routes/coupons.js +6 -0
- package/demo/routes/docs.js +18 -0
- package/demo/routes/health.js +35 -0
- package/demo/routes/index.js +39 -0
- package/demo/routes/orders/order_items.js +7 -0
- package/demo/routes/orders/payments.js +7 -0
- package/demo/routes/orders/shipments.js +7 -0
- package/demo/routes/orders.js +6 -0
- package/demo/routes/products/product_images.js +7 -0
- package/demo/routes/products/product_reviews.js +7 -0
- package/demo/routes/products/product_variants.js +7 -0
- package/demo/routes/products.js +6 -0
- package/demo/routes/users.js +6 -0
- package/demo/routes/wishlists.js +6 -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 +58 -17
- package/src/cli/commands/help.js +11 -6
- package/src/cli/commands/init.js +2 -2
- package/src/cli/commands/inspect.js +1 -0
- package/src/cli/diff-engine.js +52 -22
- package/src/cli/generate-docs-route.js +31 -0
- package/src/cli/generate-migration.js +356 -0
- package/src/cli/generate-route.js +52 -24
- package/src/cli/init/dependencies.js +3 -0
- package/src/cli/init/generators.js +1 -1
- package/src/cli/init.js +8 -8
- 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 +1 -0
- package/docs/SKILL.md +0 -419
package/src/postgres/db.js
CHANGED
|
@@ -462,22 +462,30 @@ async function insert(table, data, uniqueKeys = []) {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
-
// Bulk insert via multi-row VALUES
|
|
465
|
+
// Bulk insert via multi-row VALUES in batches of 1000
|
|
466
|
+
const BATCH_SIZE = 1000;
|
|
467
|
+
const colList = columns.map(escapeId).join(",");
|
|
466
468
|
const client = await pool.connect();
|
|
467
469
|
try {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
470
|
+
await client.query("BEGIN");
|
|
471
|
+
for (let offset = 0; offset < total; offset += BATCH_SIZE) {
|
|
472
|
+
const batch = array.slice(offset, offset + BATCH_SIZE);
|
|
473
|
+
let paramIdx = 0;
|
|
474
|
+
const allParams = [];
|
|
475
|
+
const valuesClauses = batch.map((row) => {
|
|
476
|
+
const placeholders = columns.map((c) => {
|
|
477
|
+
paramIdx++;
|
|
478
|
+
allParams.push(sanitizeValue(row[c]));
|
|
479
|
+
return "$" + paramIdx;
|
|
480
|
+
});
|
|
481
|
+
return "(" + placeholders.join(",") + ")";
|
|
475
482
|
});
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
await client.query(
|
|
483
|
+
const sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES ${valuesClauses.join(",")}`;
|
|
484
|
+
await client.query(sql, allParams);
|
|
485
|
+
}
|
|
486
|
+
await client.query("COMMIT");
|
|
480
487
|
} catch (e) {
|
|
488
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
481
489
|
throw mapPgError(e);
|
|
482
490
|
} finally {
|
|
483
491
|
client.release();
|
|
@@ -499,31 +507,30 @@ async function _insertOnConflict(table, array, columns, uniqueKeys, total) {
|
|
|
499
507
|
const pk = await getPkColumn(table);
|
|
500
508
|
const conflictCols = uniqueKeys.map(escapeId).join(",");
|
|
501
509
|
const colList = columns.map(escapeId).join(",");
|
|
510
|
+
const BATCH_SIZE = 1000;
|
|
502
511
|
|
|
503
512
|
const client = await pool.connect();
|
|
504
513
|
try {
|
|
505
514
|
await client.query("BEGIN");
|
|
506
515
|
|
|
507
|
-
for (
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
516
|
+
for (let offset = 0; offset < total; offset += BATCH_SIZE) {
|
|
517
|
+
const batch = array.slice(offset, offset + BATCH_SIZE);
|
|
518
|
+
let paramIdx = 0;
|
|
519
|
+
const allParams = [];
|
|
520
|
+
const valuesClauses = batch.map((row) => {
|
|
521
|
+
const placeholders = columns.map((c) => {
|
|
522
|
+
paramIdx++;
|
|
523
|
+
allParams.push(sanitizeValue(row[c]));
|
|
524
|
+
return "$" + paramIdx;
|
|
525
|
+
});
|
|
526
|
+
return "(" + placeholders.join(",") + ")";
|
|
527
|
+
});
|
|
528
|
+
let sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols}) DO NOTHING`;
|
|
511
529
|
if (pk) sql += ` RETURNING ${escapeId(pk)}`;
|
|
512
530
|
|
|
513
|
-
const result = await client.query(sql,
|
|
514
|
-
if (result.rows && result.rows.length > 0
|
|
515
|
-
lastId = result.rows[
|
|
516
|
-
} else if (total === 1 && pk) {
|
|
517
|
-
// Row already existed, fetch its PK
|
|
518
|
-
const whereClauses = uniqueKeys
|
|
519
|
-
.map((k, i) => `${escapeId(k)} = ${i + 1}`)
|
|
520
|
-
.join(" AND ");
|
|
521
|
-
const whereVals = uniqueKeys.map((k) => row[k]);
|
|
522
|
-
const fetched = await client.query(
|
|
523
|
-
`SELECT ${escapeId(pk)} FROM ${escapeId(table)} WHERE ${whereClauses}`,
|
|
524
|
-
whereVals,
|
|
525
|
-
);
|
|
526
|
-
if (fetched.rows.length > 0) lastId = fetched.rows[0][pk] || 0;
|
|
531
|
+
const result = await client.query(sql, allParams);
|
|
532
|
+
if (pk && result.rows && result.rows.length > 0) {
|
|
533
|
+
lastId = result.rows[result.rows.length - 1][pk] || lastId;
|
|
527
534
|
}
|
|
528
535
|
}
|
|
529
536
|
|
|
@@ -594,20 +601,32 @@ async function upsert(table, data, uniqueKeys = []) {
|
|
|
594
601
|
const nonUniqueColumns = columns.filter((c) => !uniqueKeys.includes(c));
|
|
595
602
|
const pk = await getPkColumn(table);
|
|
596
603
|
let lastId = 0;
|
|
604
|
+
const BATCH_SIZE = 1000;
|
|
597
605
|
|
|
598
606
|
const client = await pool.connect();
|
|
599
607
|
try {
|
|
600
608
|
await client.query("BEGIN");
|
|
601
609
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
610
|
+
const updateSetSql = nonUniqueColumns
|
|
611
|
+
.map((c) => `${escapeId(c)} = EXCLUDED.${escapeId(c)}`)
|
|
612
|
+
.join(", ");
|
|
613
|
+
const conflictCols = uniqueKeys.map(escapeId).join(",");
|
|
614
|
+
const colList = columns.map(escapeId).join(",");
|
|
615
|
+
|
|
616
|
+
for (let offset = 0; offset < total; offset += BATCH_SIZE) {
|
|
617
|
+
const batch = array.slice(offset, offset + BATCH_SIZE);
|
|
618
|
+
let paramIdx = 0;
|
|
619
|
+
const allParams = [];
|
|
620
|
+
const valuesClauses = batch.map((row) => {
|
|
621
|
+
const placeholders = columns.map((c) => {
|
|
622
|
+
paramIdx++;
|
|
623
|
+
allParams.push(sanitizeValue(row[c]));
|
|
624
|
+
return "$" + paramIdx;
|
|
625
|
+
});
|
|
626
|
+
return "(" + placeholders.join(",") + ")";
|
|
627
|
+
});
|
|
609
628
|
|
|
610
|
-
let sql = `INSERT INTO ${escapeId(table)} (${
|
|
629
|
+
let sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols})`;
|
|
611
630
|
if (updateSetSql) {
|
|
612
631
|
sql += ` DO UPDATE SET ${updateSetSql}`;
|
|
613
632
|
} else {
|
|
@@ -615,9 +634,9 @@ async function upsert(table, data, uniqueKeys = []) {
|
|
|
615
634
|
}
|
|
616
635
|
if (pk) sql += ` RETURNING ${escapeId(pk)}`;
|
|
617
636
|
|
|
618
|
-
const result = await client.query(sql,
|
|
619
|
-
if (result.rows && result.rows.length > 0
|
|
620
|
-
lastId = result.rows[
|
|
637
|
+
const result = await client.query(sql, allParams);
|
|
638
|
+
if (pk && result.rows && result.rows.length > 0) {
|
|
639
|
+
lastId = result.rows[result.rows.length - 1][pk] || lastId;
|
|
621
640
|
}
|
|
622
641
|
}
|
|
623
642
|
|
|
@@ -656,6 +675,7 @@ module.exports = {
|
|
|
656
675
|
query,
|
|
657
676
|
qcount,
|
|
658
677
|
remove,
|
|
678
|
+
delete: remove,
|
|
659
679
|
upsert,
|
|
660
680
|
change: upsert,
|
|
661
681
|
insert,
|
package/src/redis/db.js
CHANGED
|
@@ -56,6 +56,11 @@ function parseSchema(input) {
|
|
|
56
56
|
const softDelete =
|
|
57
57
|
tableDef.softDelete !== undefined ? tableDef.softDelete : null;
|
|
58
58
|
|
|
59
|
+
const parent =
|
|
60
|
+
tableDef.parent !== undefined && tableDef.parent !== null
|
|
61
|
+
? tableDef.parent
|
|
62
|
+
: null;
|
|
63
|
+
|
|
59
64
|
tables[tableName] = {
|
|
60
65
|
name: tableName,
|
|
61
66
|
columns: { ...tableDef.columns },
|
|
@@ -63,14 +68,51 @@ function parseSchema(input) {
|
|
|
63
68
|
unique,
|
|
64
69
|
softDelete,
|
|
65
70
|
timestamps,
|
|
71
|
+
parent,
|
|
66
72
|
};
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
// Derive relationships from parent fields on tables.
|
|
76
|
+
// If a table has parent set, create a relationship entry.
|
|
77
|
+
// Also merge any explicitly declared relationships.
|
|
78
|
+
const derivedRelationships = [];
|
|
79
|
+
const seenRels = new Set();
|
|
80
|
+
|
|
81
|
+
for (const [tableName, tableDef] of Object.entries(tables)) {
|
|
82
|
+
if (tableDef.parent) {
|
|
83
|
+
const parentTable = tables[tableDef.parent];
|
|
84
|
+
if (parentTable) {
|
|
85
|
+
// FK column is the parent's PK name (e.g. post_id for posts)
|
|
86
|
+
const fkColumn = parentTable.pk;
|
|
87
|
+
const key = `${tableDef.parent}:${tableName}:${fkColumn}`;
|
|
88
|
+
if (!seenRels.has(key)) {
|
|
89
|
+
derivedRelationships.push({
|
|
90
|
+
parent: tableDef.parent,
|
|
91
|
+
child: tableName,
|
|
92
|
+
foreignKey: fkColumn,
|
|
93
|
+
});
|
|
94
|
+
seenRels.add(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Merge explicit relationships (from the relationships array) that aren't already derived
|
|
101
|
+
if (raw.relationships) {
|
|
102
|
+
for (const rel of raw.relationships) {
|
|
103
|
+
const key = `${rel.parent}:${rel.child}:${rel.foreignKey}`;
|
|
104
|
+
if (!seenRels.has(key)) {
|
|
105
|
+
derivedRelationships.push({ ...rel });
|
|
106
|
+
seenRels.add(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
69
111
|
return {
|
|
70
112
|
adapter: raw.adapter,
|
|
71
113
|
framework: raw.framework,
|
|
72
114
|
tables,
|
|
73
|
-
relationships:
|
|
115
|
+
relationships: derivedRelationships,
|
|
74
116
|
options: raw.options ? { ...raw.options } : {},
|
|
75
117
|
};
|
|
76
118
|
}
|
|
@@ -48,6 +48,13 @@ function printSchema(schema) {
|
|
|
48
48
|
tableDef.softDelete = table.softDelete;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Preserve parent if set
|
|
52
|
+
if (table.parent !== null && table.parent !== undefined) {
|
|
53
|
+
tableDef.parent = table.parent;
|
|
54
|
+
} else {
|
|
55
|
+
tableDef.parent = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
// Preserve timestamps if not the default { created_at: null, modified_at: null }
|
|
52
59
|
if (table.timestamps) {
|
|
53
60
|
const hasCustomTimestamps =
|
|
@@ -114,6 +114,8 @@ function validateSchema(raw) {
|
|
|
114
114
|
* Validate all table entries.
|
|
115
115
|
*/
|
|
116
116
|
function validateTables(tables, errors) {
|
|
117
|
+
const tableNames = new Set(Object.keys(tables));
|
|
118
|
+
|
|
117
119
|
for (const [tableName, tableDef] of Object.entries(tables)) {
|
|
118
120
|
const basePath = `tables.${tableName}`;
|
|
119
121
|
|
|
@@ -194,6 +196,21 @@ function validateTables(tables, errors) {
|
|
|
194
196
|
});
|
|
195
197
|
}
|
|
196
198
|
}
|
|
199
|
+
|
|
200
|
+
// Validate parent (route nesting)
|
|
201
|
+
if (tableDef.parent !== undefined && tableDef.parent !== null) {
|
|
202
|
+
if (typeof tableDef.parent !== "string") {
|
|
203
|
+
errors.push({
|
|
204
|
+
path: `${basePath}.parent`,
|
|
205
|
+
message: `parent must be a string or null in table "${tableName}"`,
|
|
206
|
+
});
|
|
207
|
+
} else if (!tableNames.has(tableDef.parent)) {
|
|
208
|
+
errors.push({
|
|
209
|
+
path: `${basePath}.parent`,
|
|
210
|
+
message: `parent "${tableDef.parent}" does not reference an existing table in table "${tableName}"`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
197
214
|
}
|
|
198
215
|
}
|
|
199
216
|
|