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.
Files changed (92) hide show
  1. package/README.md +110 -16
  2. package/TODO.md +14 -0
  3. package/dbmr.schema.json +333 -0
  4. package/demo/.dockerignore +7 -0
  5. package/demo/.env.example +13 -0
  6. package/demo/Dockerfile +20 -0
  7. package/demo/app.js +37 -0
  8. package/demo/commons/add_migration.js +43 -0
  9. package/demo/commons/db.js +17 -0
  10. package/demo/commons/migrate.js +65 -0
  11. package/demo/commons/security.js +30 -0
  12. package/demo/commons/session.js +13 -0
  13. package/demo/dbmr.schema.json +362 -0
  14. package/demo/docs/llm.md +197 -0
  15. package/demo/llms.txt +70 -0
  16. package/demo/middleware/logger.js +67 -0
  17. package/demo/migrations/20260430155808_create_migrations_table.sql +6 -0
  18. package/demo/migrations/20260430155809_create_tables.sql +207 -0
  19. package/demo/models/addresses.js +22 -0
  20. package/demo/models/cart_items.js +18 -0
  21. package/demo/models/carts.js +16 -0
  22. package/demo/models/categories.js +20 -0
  23. package/demo/models/coupons.js +23 -0
  24. package/demo/models/order_items.js +21 -0
  25. package/demo/models/orders.js +25 -0
  26. package/demo/models/payments.js +21 -0
  27. package/demo/models/product_images.js +18 -0
  28. package/demo/models/product_reviews.js +20 -0
  29. package/demo/models/product_variants.js +20 -0
  30. package/demo/models/products.js +30 -0
  31. package/demo/models/shipments.js +19 -0
  32. package/demo/models/users.js +19 -0
  33. package/demo/models/wishlists.js +15 -0
  34. package/demo/openapi.json +5872 -0
  35. package/demo/package-lock.json +2810 -0
  36. package/demo/package.json +34 -0
  37. package/demo/routes/addresses.js +6 -0
  38. package/demo/routes/carts/cart_items.js +7 -0
  39. package/demo/routes/carts.js +6 -0
  40. package/demo/routes/categories.js +6 -0
  41. package/demo/routes/coupons.js +6 -0
  42. package/demo/routes/docs.js +18 -0
  43. package/demo/routes/health.js +35 -0
  44. package/demo/routes/index.js +39 -0
  45. package/demo/routes/orders/order_items.js +7 -0
  46. package/demo/routes/orders/payments.js +7 -0
  47. package/demo/routes/orders/shipments.js +7 -0
  48. package/demo/routes/orders.js +6 -0
  49. package/demo/routes/products/product_images.js +7 -0
  50. package/demo/routes/products/product_reviews.js +7 -0
  51. package/demo/routes/products/product_variants.js +7 -0
  52. package/demo/routes/products.js +6 -0
  53. package/demo/routes/users.js +6 -0
  54. package/demo/routes/wishlists.js +6 -0
  55. package/docker-compose.yml +1 -1
  56. package/package.json +8 -7
  57. package/scripts/demo-create.js +47 -0
  58. package/skill/SKILL.md +464 -0
  59. package/skill/references/cockroachdb.md +49 -0
  60. package/skill/references/dynamodb.md +53 -0
  61. package/skill/references/mongodb.md +56 -0
  62. package/skill/references/mssql.md +55 -0
  63. package/skill/references/oracle.md +52 -0
  64. package/skill/references/postgres.md +50 -0
  65. package/skill/references/redis.md +53 -0
  66. package/skill/references/sqlite3.md +43 -0
  67. package/src/cli/commands/generate.js +58 -17
  68. package/src/cli/commands/help.js +11 -6
  69. package/src/cli/commands/init.js +2 -2
  70. package/src/cli/commands/inspect.js +1 -0
  71. package/src/cli/diff-engine.js +52 -22
  72. package/src/cli/generate-docs-route.js +31 -0
  73. package/src/cli/generate-migration.js +356 -0
  74. package/src/cli/generate-route.js +52 -24
  75. package/src/cli/init/dependencies.js +3 -0
  76. package/src/cli/init/generators.js +1 -1
  77. package/src/cli/init.js +8 -8
  78. package/src/cockroachdb/db.js +90 -59
  79. package/src/commons/route.js +20 -20
  80. package/src/commons/validator.js +58 -1
  81. package/src/dynamodb/db.js +50 -27
  82. package/src/mongodb/db.js +1 -0
  83. package/src/mssql/db.js +89 -61
  84. package/src/mysql/db.js +1 -0
  85. package/src/oracle/db.js +1 -0
  86. package/src/postgres/db.js +61 -41
  87. package/src/redis/db.js +1 -0
  88. package/src/schema/schema-parser.js +43 -1
  89. package/src/schema/schema-printer.js +7 -0
  90. package/src/schema/schema-validator.js +17 -0
  91. package/src/sqlite3/db.js +1 -0
  92. package/docs/SKILL.md +0 -419
@@ -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
- let paramIdx = 0;
469
- const allParams = [];
470
- const valuesClauses = array.map((row) => {
471
- const placeholders = columns.map((c) => {
472
- paramIdx++;
473
- allParams.push(sanitizeValue(row[c]));
474
- return "$" + paramIdx;
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
- return "(" + placeholders.join(",") + ")";
477
- });
478
- const sql = `INSERT INTO ${escapeId(table)} (${columns.map(escapeId).join(",")}) VALUES ${valuesClauses.join(",")}`;
479
- await client.query(sql, allParams);
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 (const row of array) {
508
- const vals = columns.map((c) => sanitizeValue(row[c]));
509
- const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
510
- let sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES (${placeholders}) ON CONFLICT (${conflictCols}) DO NOTHING`;
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, vals);
514
- if (result.rows && result.rows.length > 0 && pk) {
515
- lastId = result.rows[0][pk] || 0;
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
- for (const row of array) {
603
- const vals = columns.map((c) => sanitizeValue(row[c]));
604
- const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
605
- const conflictCols = uniqueKeys.map(escapeId).join(",");
606
- const updateSetSql = nonUniqueColumns
607
- .map((c) => `${escapeId(c)} = EXCLUDED.${escapeId(c)}`)
608
- .join(", ");
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)} (${columns.map(escapeId).join(",")}) VALUES (${placeholders}) ON CONFLICT (${conflictCols})`;
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, vals);
619
- if (result.rows && result.rows.length > 0 && pk) {
620
- lastId = result.rows[0][pk] || 0;
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
@@ -440,6 +440,7 @@ module.exports = {
440
440
  where,
441
441
  qcount,
442
442
  remove,
443
+ delete: remove,
443
444
  upsert,
444
445
  change: upsert,
445
446
  insert,
@@ -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: raw.relationships ? [...raw.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
 
package/src/sqlite3/db.js CHANGED
@@ -338,6 +338,7 @@ module.exports = {
338
338
  query,
339
339
  qcount,
340
340
  remove,
341
+ delete: remove,
341
342
  upsert,
342
343
  change: upsert,
343
344
  insert,