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.
Files changed (46) hide show
  1. package/README.md +110 -16
  2. package/TODO.md +15 -0
  3. package/dbmr.schema.json +333 -0
  4. package/docker-compose.yml +1 -1
  5. package/package.json +8 -7
  6. package/scripts/demo-create.js +47 -0
  7. package/skill/SKILL.md +464 -0
  8. package/skill/references/cockroachdb.md +49 -0
  9. package/skill/references/dynamodb.md +53 -0
  10. package/skill/references/mongodb.md +56 -0
  11. package/skill/references/mssql.md +55 -0
  12. package/skill/references/oracle.md +52 -0
  13. package/skill/references/postgres.md +50 -0
  14. package/skill/references/redis.md +53 -0
  15. package/skill/references/sqlite3.md +43 -0
  16. package/src/cli/commands/generate.js +95 -31
  17. package/src/cli/commands/help.js +12 -7
  18. package/src/cli/commands/init.js +2 -2
  19. package/src/cli/commands/inspect.js +1 -0
  20. package/src/cli/diff-engine.js +54 -23
  21. package/src/cli/generate-db-manager.js +1573 -0
  22. package/src/cli/generate-docs-route.js +31 -0
  23. package/src/cli/generate-migration.js +356 -0
  24. package/src/cli/generate-model.js +9 -4
  25. package/src/cli/generate-openapi.js +40 -13
  26. package/src/cli/generate-route.js +55 -27
  27. package/src/cli/init/dependencies.js +3 -0
  28. package/src/cli/init/generators.js +37 -31
  29. package/src/cli/init.js +8 -8
  30. package/src/cli/main.js +2 -2
  31. package/src/cockroachdb/db.js +90 -59
  32. package/src/commons/route.js +20 -20
  33. package/src/commons/validator.js +58 -1
  34. package/src/dynamodb/db.js +50 -27
  35. package/src/mongodb/db.js +1 -0
  36. package/src/mssql/db.js +89 -61
  37. package/src/mysql/db.js +1 -0
  38. package/src/oracle/db.js +1 -0
  39. package/src/postgres/db.js +61 -41
  40. package/src/redis/db.js +1 -0
  41. package/src/schema/schema-parser.js +43 -1
  42. package/src/schema/schema-printer.js +7 -0
  43. package/src/schema/schema-validator.js +17 -0
  44. package/src/sqlite3/db.js +12 -0
  45. package/docs/SKILL.md +0 -419
  46. package/src/cli/commands/generate-llm-docs.js +0 -418
@@ -58,8 +58,15 @@ export default route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
58
58
  /**
59
59
  * Generate the routes index file that mounts all routes on an express Router.
60
60
  * Supports parent-child nesting: parent/:pk/child
61
+ *
62
+ * Child routes are placed in subfolders: routes/<parent>/<child>.js
63
+ * Children are only mounted under their parent path (no duplicate top-level route).
64
+ *
65
+ * @param {string[]} tableNames
66
+ * @param {Array<{parent, child, foreignKey}>} relationships
67
+ * @param {{ includeDocs?: boolean }} [options]
61
68
  */
62
- function generateRoutesIndexFile(tableNames, relationships = []) {
69
+ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
63
70
  let imports = `import express from "express";\n\nconst router = express.Router();\n\n`;
64
71
 
65
72
  // Collect child tables that are nested under parents
@@ -68,35 +75,42 @@ function generateRoutesIndexFile(tableNames, relationships = []) {
68
75
  nestedChildren.add(rel.child);
69
76
  }
70
77
 
78
+ // Import top-level routes only (not children)
71
79
  for (const table of tableNames) {
80
+ if (nestedChildren.has(table)) continue;
72
81
  const varName = safeVarName(table);
73
82
  imports += `import ${varName}Route from "./${table}.js";\n`;
74
83
  }
75
- // Import child routes with _child suffix for nested ones
84
+
85
+ // Import child routes from subfolders
76
86
  for (const rel of relationships) {
77
87
  const varName = safeVarName(rel.child);
78
- imports += `import ${varName}ChildRoute from "./${rel.child}_child_of_${rel.parent}.js";\n`;
88
+ imports += `import ${varName}ChildRoute from "./${rel.parent}/${rel.child}.js";\n`;
89
+ }
90
+
91
+ // Import docs route if openapi is generated
92
+ if (options.includeDocs) {
93
+ imports += `import docsRoute from "./docs.js";\n`;
79
94
  }
80
95
 
81
96
  imports += "\n";
82
97
 
83
- // Mount top-level routes (skip tables that are ONLY children)
84
- for (const table of tableNames) {
85
- if (nestedChildren.has(table)) continue;
86
- const varName = safeVarName(table);
87
- imports += `router.use("/${table}", ${varName}Route);\n`;
98
+ // Mount docs route first
99
+ if (options.includeDocs) {
100
+ imports += `router.use("/docs", docsRoute);\n`;
88
101
  }
89
102
 
90
- // Mount nested child routes under parent
103
+ // Mount child routes BEFORE parent routes to prevent path clashing
91
104
  for (const rel of relationships) {
92
105
  const childVar = safeVarName(rel.child);
93
- imports += `router.use("/${rel.parent}/:${rel.fkColumn}/${rel.child}", ${childVar}ChildRoute);\n`;
106
+ imports += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
94
107
  }
95
108
 
96
- // Also mount children as top-level for direct access
97
- for (const rel of relationships) {
98
- const varName = safeVarName(rel.child);
99
- imports += `router.use("/${rel.child}", ${varName}Route);\n`;
109
+ // Mount top-level routes
110
+ for (const table of tableNames) {
111
+ if (nestedChildren.has(table)) continue;
112
+ const varName = safeVarName(table);
113
+ imports += `router.use("/${table}", ${varName}Route);\n`;
100
114
  }
101
115
 
102
116
  imports += "\nexport default router;\n";
@@ -387,7 +401,7 @@ async function main() {
387
401
  const fkColumn = parent.replace(/s$/, "") + "_id";
388
402
  // Only add if both tables exist in our model set
389
403
  if (tableNames.includes(parent) && tableNames.includes(child)) {
390
- relationships.push({ parent, child, fkColumn });
404
+ relationships.push({ parent, child, foreignKey: fkColumn });
391
405
  }
392
406
  }
393
407
  }
@@ -398,23 +412,36 @@ async function main() {
398
412
  fs.mkdirSync(routesDir, { recursive: true });
399
413
  }
400
414
 
415
+ // Collect child tables to skip top-level route files
416
+ const nestedChildren = new Set();
417
+ for (const rel of relationships) {
418
+ nestedChildren.add(rel.child);
419
+ }
420
+
401
421
  for (const table of tableNames) {
422
+ if (nestedChildren.has(table)) continue;
402
423
  const filePath = path.join(routesDir, table + ".js");
403
424
  fs.writeFileSync(filePath, generateRouteFile(table, modelsRelPath));
404
425
  console.log(` Created ${filePath}`);
405
426
  }
406
427
 
407
- // Write child route files for parent-child relationships
428
+ // Write child route files in subfolders: routes/<parent>/<child>.js
408
429
  for (const rel of relationships) {
409
- const fileName = `${rel.child}_child_of_${rel.parent}.js`;
410
- const filePath = path.join(routesDir, fileName);
430
+ const parentDir = path.join(routesDir, rel.parent);
431
+ if (!fs.existsSync(parentDir)) {
432
+ fs.mkdirSync(parentDir, { recursive: true });
433
+ }
434
+ const filePath = path.join(parentDir, `${rel.child}.js`);
435
+ const childModelsRelPath = path
436
+ .relative(parentDir, path.resolve(modelsDir))
437
+ .replace(/\\/g, "/");
411
438
  fs.writeFileSync(
412
439
  filePath,
413
440
  generateChildRouteFile(
414
441
  rel.child,
415
442
  rel.parent,
416
- rel.fkColumn,
417
- modelsRelPath,
443
+ rel.foreignKey,
444
+ childModelsRelPath,
418
445
  ),
419
446
  );
420
447
  console.log(` Created ${filePath}`);
@@ -445,7 +472,7 @@ async function main() {
445
472
  }
446
473
  }
447
474
  if (modelMeta.length > 0) {
448
- const spec = generateOpenAPISpec(modelMeta);
475
+ const spec = generateOpenAPISpec(modelMeta, { relationships });
449
476
  const specPath = path.join(routesDir, "openapi.json");
450
477
  fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
451
478
  console.log(` Created ${specPath}`);
@@ -473,7 +500,7 @@ async function main() {
473
500
  console.log(` Created ${testPath}`);
474
501
  }
475
502
 
476
- // Generate child route test files
503
+ // Generate child route test files in subfolders
477
504
  for (const rel of relationships) {
478
505
  let pk = "id";
479
506
  const modelPath = path.join(modelsDir, rel.child + ".js");
@@ -484,13 +511,14 @@ async function main() {
484
511
  );
485
512
  if (meta && meta.primary_key) pk = meta.primary_key;
486
513
  }
487
- const testPath = path.join(
488
- testsDir,
489
- `${rel.child}_child_of_${rel.parent}.test.js`,
490
- );
514
+ const parentTestDir = path.join(testsDir, rel.parent);
515
+ if (!fs.existsSync(parentTestDir)) {
516
+ fs.mkdirSync(parentTestDir, { recursive: true });
517
+ }
518
+ const testPath = path.join(parentTestDir, `${rel.child}.test.js`);
491
519
  fs.writeFileSync(
492
520
  testPath,
493
- generateChildTestFile(rel.child, rel.parent, rel.fkColumn, pk),
521
+ generateChildTestFile(rel.child, rel.parent, rel.foreignKey, pk),
494
522
  );
495
523
  console.log(` Created ${testPath}`);
496
524
  }
@@ -63,6 +63,9 @@ function collectDependencies(answers) {
63
63
  // Dev dependencies
64
64
  devDependencies["nodemon"] = "latest";
65
65
 
66
+ // Swagger UI for API documentation
67
+ dependencies["swagger-ui-express"] = "latest";
68
+
66
69
  return { dependencies, devDependencies };
67
70
  }
68
71
 
@@ -367,21 +367,21 @@ function generateAppJs(answers) {
367
367
  answers.framework === "ultimate-express" ? "ultimate-express" : "express";
368
368
 
369
369
  // Imports
370
- let imports = `const express = require("${frameworkPkg}");
371
- const { init, db } = require("db-model-router");
372
- const session = require("express-session");`;
370
+ let imports = `import express from "${frameworkPkg}";
371
+ import { init, db } from "db-model-router";
372
+ import session from "express-session";`;
373
373
 
374
374
  if (answers.session === "redis") {
375
- imports += `\nconst RedisStore = require("connect-redis").default;
376
- const { Redis } = require("ioredis");`;
375
+ imports += `\nimport RedisStore from "connect-redis";
376
+ import { Redis } from "ioredis";`;
377
377
  }
378
378
  if (answers.rateLimiting) {
379
- imports += `\nconst rateLimit = require("express-rate-limit");`;
379
+ imports += `\nimport rateLimit from "express-rate-limit";`;
380
380
  }
381
381
  if (answers.helmet) {
382
- imports += `\nconst helmet = require("helmet");`;
382
+ imports += `\nimport helmet from "helmet";`;
383
383
  }
384
- imports += `\nconst logger = require("./middleware/logger");`;
384
+ imports += `\nimport logger from "./middleware/logger.js";`;
385
385
 
386
386
  // Rate limiting block
387
387
  const rateLimitBlock = answers.rateLimiting
@@ -396,9 +396,7 @@ const { Redis } = require("ioredis");`;
396
396
  const helmetBlock = answers.helmet ? `app.use(helmet());` : "";
397
397
 
398
398
  return `${imports}
399
-
400
- // Load environment variables
401
- require("dotenv").config();
399
+ import "dotenv/config";
402
400
 
403
401
  // Initialize database adapter
404
402
  init("${answers.database}");
@@ -428,7 +426,7 @@ app.listen(PORT, () => {
428
426
  console.log(\`Server running on port \${PORT}\`);
429
427
  });
430
428
 
431
- module.exports = app;
429
+ export default app;
432
430
  `;
433
431
  }
434
432
 
@@ -551,14 +549,15 @@ function generateMigrateScript(answers) {
551
549
 
552
550
  if (isNoSql) {
553
551
  return `#!/usr/bin/env node
554
- "use strict";
552
+ import fs from "fs";
553
+ import path from "path";
554
+ import crypto from "crypto";
555
+ import { fileURLToPath } from "url";
556
+ import "dotenv/config";
555
557
 
556
- const fs = require("fs");
557
- const path = require("path");
558
- const crypto = require("crypto");
559
- require("dotenv").config();
558
+ import { init, db } from "db-model-router";
560
559
 
561
- const { init, db } = require("db-model-router");
560
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
562
561
 
563
562
  init("${answers.database}");
564
563
 
@@ -598,7 +597,7 @@ async function migrate() {
598
597
  const content = fs.readFileSync(filePath, "utf8");
599
598
  const checksum = crypto.createHash("md5").update(content).digest("hex");
600
599
 
601
- const migration = require(filePath);
600
+ const migration = await import(filePath);
602
601
  console.log(\` Running migration: \${file}\`);
603
602
  await migration.up(db);
604
603
  await recordMigration(file, checksum);
@@ -622,14 +621,15 @@ migrate().catch(err => {
622
621
  }
623
622
 
624
623
  return `#!/usr/bin/env node
625
- "use strict";
624
+ import fs from "fs";
625
+ import path from "path";
626
+ import crypto from "crypto";
627
+ import { fileURLToPath } from "url";
628
+ import "dotenv/config";
626
629
 
627
- const fs = require("fs");
628
- const path = require("path");
629
- const crypto = require("crypto");
630
- require("dotenv").config();
630
+ import { init, db } from "db-model-router";
631
631
 
632
- const { init, db } = require("db-model-router");
632
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
633
633
 
634
634
  init("${answers.database}");
635
635
 
@@ -700,14 +700,15 @@ function generateAddMigrationScript(answers) {
700
700
  const isNoSql = NOSQL_DATABASES.includes(answers.database);
701
701
  const ext = isNoSql ? "js" : "sql";
702
702
  const template = isNoSql
703
- ? `"use strict";\\n\\nmodule.exports = {\\n async up(db) {\\n // Write your migration here\\n },\\n\\n async down(db) {\\n // Write your rollback here\\n },\\n};\\n`
703
+ ? `export async function up(db) {\\n // Write your migration here\\n}\\n\\nexport async function down(db) {\\n // Write your rollback here\\n}\\n`
704
704
  : `-- Write your migration SQL here\\n`;
705
705
 
706
706
  return `#!/usr/bin/env node
707
- "use strict";
707
+ import fs from "fs";
708
+ import path from "path";
709
+ import { fileURLToPath } from "url";
708
710
 
709
- const fs = require("fs");
710
- const path = require("path");
711
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
711
712
 
712
713
  const migrationsDir = path.join(__dirname, "migrations");
713
714
 
@@ -1422,6 +1423,8 @@ if (isMain) {
1422
1423
  const pkg = await import("db-model-router");
1423
1424
  const mod = pkg.default || pkg;
1424
1425
  mod.init("${answers.database}");
1426
+ mod.db.connect({
1427
+ ${dbConnectArgs(answers.database)}});
1425
1428
  const migrationsDir = path.join(__dirname, "${migrationsRel}");
1426
1429
  runMigrations(mod.db, migrationsDir)
1427
1430
  .then(() => process.exit(0))
@@ -1490,6 +1493,8 @@ if (isMain) {
1490
1493
  const pkg = await import("db-model-router");
1491
1494
  const mod = pkg.default || pkg;
1492
1495
  mod.init("${answers.database}");
1496
+ mod.db.connect({
1497
+ ${dbConnectArgs(answers.database)}});
1493
1498
  const migrationsDir = path.join(__dirname, "${migrationsRel}");
1494
1499
  runMigrations(mod.db, migrationsDir)
1495
1500
  .then(() => process.exit(0))
@@ -1715,7 +1720,7 @@ function generateAppJsV2(answers, outputDir) {
1715
1720
  answers.framework === "ultimate-express" ? "ultimate-express" : "express";
1716
1721
 
1717
1722
  const commonsPrefix = outputDir ? `./${outputDir}/commons` : "./commons";
1718
- const routePrefix = outputDir ? `./${outputDir}/route` : "./route";
1723
+ const routePrefix = outputDir ? `./${outputDir}/routes` : "./routes";
1719
1724
  const middlewarePrefix = outputDir
1720
1725
  ? `./${outputDir}/middleware`
1721
1726
  : "./middleware";
@@ -1726,7 +1731,8 @@ import configureSession from "${commonsPrefix}/session.js";
1726
1731
  import applySecurity from "${commonsPrefix}/security.js";
1727
1732
  import logger from "${middlewarePrefix}/logger.js";
1728
1733
  import route from "${routePrefix}/index.js";
1729
-
1734
+ import { fileURLToPath } from 'node:url';
1735
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1730
1736
  const app = express();
1731
1737
  const PORT = process.env.PORT || 3000;
1732
1738
 
package/src/cli/init.js CHANGED
@@ -95,7 +95,7 @@ function generateFiles(answers, outputDir) {
95
95
  path.join(srcBase, "middleware"),
96
96
  path.join(srcBase, "migrations"),
97
97
  path.join(srcBase, "commons"),
98
- path.join(srcBase, "route"),
98
+ path.join(srcBase, "routes"),
99
99
  ];
100
100
  // SQLite3 needs a data/ folder for the database file
101
101
  if (answers.database === "sqlite3") {
@@ -189,15 +189,15 @@ function generateFiles(answers, outputDir) {
189
189
  if (safeWriteFile(dbPath, generateDbModule(answers)))
190
190
  files.push(path.join(srcBase, "commons/db.js"));
191
191
 
192
- // route/health.js
193
- const healthPath = path.join(srcBase, "route", "health.js");
192
+ // routes/health.js
193
+ const healthPath = path.join(srcBase, "routes", "health.js");
194
194
  if (safeWriteFile(healthPath, generateHealthRoute()))
195
- files.push(path.join(srcBase, "route/health.js"));
195
+ files.push(path.join(srcBase, "routes/health.js"));
196
196
 
197
- // route/index.js
198
- const routeIndexPath = path.join(srcBase, "route", "index.js");
197
+ // routes/index.js
198
+ const routeIndexPath = path.join(srcBase, "routes", "index.js");
199
199
  if (safeWriteFile(routeIndexPath, generateRouteIndexFile()))
200
- files.push(path.join(srcBase, "route/index.js"));
200
+ files.push(path.join(srcBase, "routes/index.js"));
201
201
 
202
202
  // Initial migration (inside outputDir/migrations)
203
203
  const initialMigration = generateInitialMigration(answers);
@@ -365,7 +365,7 @@ Options:
365
365
  --db <name> Alias for --database
366
366
  --session <type> Session store: memory, redis, database
367
367
  --output <dir> Directory for backend source files (e.g. --output backend).
368
- package.json stays in root; index.js, commons/, route/,
368
+ package.json stays in root; index.js, commons/, routes/,
369
369
  middleware/, and migrations/ go inside the output folder.
370
370
  --rateLimiting Enable rate limiting (express-rate-limit)
371
371
  --helmet Enable Helmet security headers
package/src/cli/main.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
4
  const { parseFlags, OutputContext } = require("./flags");
@@ -64,7 +64,7 @@ const COMMAND_FLAGS = {
64
64
  ["--routes", "Generate only route files"],
65
65
  ["--openapi", "Generate only OpenAPI spec"],
66
66
  ["--tests", "Generate only test files"],
67
- ["--llm-docs", "Generate only LLM documentation"],
67
+ ["--db-manager", "Generate DB Manager UI (SQL adapters only)"],
68
68
  ],
69
69
  doctor: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
70
70
  diff: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
@@ -371,22 +371,30 @@ async function insert(table, data, uniqueKeys = []) {
371
371
  }
372
372
  }
373
373
 
374
- // Bulk insert via multi-row VALUES
374
+ // Bulk insert via multi-row VALUES in batches of 1000
375
+ const BATCH_SIZE = 1000;
376
+ const colList = columns.join(",");
375
377
  const client = await pool.connect();
376
378
  try {
377
- let paramIdx = 0;
378
- const allParams = [];
379
- const valuesClauses = array.map((row) => {
380
- const placeholders = columns.map((c) => {
381
- paramIdx++;
382
- allParams.push(sanitizeValue(row[c]));
383
- return "$" + paramIdx;
379
+ await client.query("BEGIN");
380
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
381
+ const batch = array.slice(offset, offset + BATCH_SIZE);
382
+ let paramIdx = 0;
383
+ const allParams = [];
384
+ const valuesClauses = batch.map((row) => {
385
+ const placeholders = columns.map((c) => {
386
+ paramIdx++;
387
+ allParams.push(sanitizeValue(row[c]));
388
+ return "$" + paramIdx;
389
+ });
390
+ return "(" + placeholders.join(",") + ")";
384
391
  });
385
- return "(" + placeholders.join(",") + ")";
386
- });
387
- const sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES ${valuesClauses.join(",")}`;
388
- await client.query(sql, allParams);
392
+ const sql = `INSERT INTO ${table} (${colList}) VALUES ${valuesClauses.join(",")}`;
393
+ await client.query(sql, allParams);
394
+ }
395
+ await client.query("COMMIT");
389
396
  } catch (e) {
397
+ await client.query("ROLLBACK").catch(() => {});
390
398
  throw mapPgError(e);
391
399
  } finally {
392
400
  client.release();
@@ -407,35 +415,40 @@ async function _insertOnConflict(table, array, columns, uniqueKeys, total) {
407
415
  let lastId = 0;
408
416
  const pk = await getPkColumn(table);
409
417
  const conflictCols = uniqueKeys.join(",");
418
+ const colList = columns.join(",");
419
+ const BATCH_SIZE = 1000;
410
420
 
411
- for (const row of array) {
412
- const vals = columns.map((c) => sanitizeValue(row[c]));
413
- const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
414
- let sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES (${placeholders}) ON CONFLICT (${conflictCols}) DO NOTHING`;
415
- if (pk) sql += ` RETURNING ${pk}`;
421
+ const client = await pool.connect();
422
+ try {
423
+ await client.query("BEGIN");
424
+
425
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
426
+ const batch = array.slice(offset, offset + BATCH_SIZE);
427
+ let paramIdx = 0;
428
+ const allParams = [];
429
+ const valuesClauses = batch.map((row) => {
430
+ const placeholders = columns.map((c) => {
431
+ paramIdx++;
432
+ allParams.push(sanitizeValue(row[c]));
433
+ return "$" + paramIdx;
434
+ });
435
+ return "(" + placeholders.join(",") + ")";
436
+ });
437
+ let sql = `INSERT INTO ${table} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols}) DO NOTHING`;
438
+ if (pk) sql += ` RETURNING ${pk}`;
416
439
 
417
- const client = await pool.connect();
418
- try {
419
- const result = await client.query(sql, vals);
420
- if (result.rows && result.rows.length > 0 && pk) {
421
- lastId = result.rows[0][pk] || 0;
422
- } else if (total === 1 && pk) {
423
- // Row already existed, fetch its PK
424
- const whereClauses = uniqueKeys
425
- .map((k, i) => `${k} = $${i + 1}`)
426
- .join(" AND ");
427
- const whereVals = uniqueKeys.map((k) => row[k]);
428
- const fetched = await client.query(
429
- `SELECT ${pk} FROM ${table} WHERE ${whereClauses}`,
430
- whereVals,
431
- );
432
- if (fetched.rows.length > 0) lastId = fetched.rows[0][pk] || 0;
440
+ const result = await client.query(sql, allParams);
441
+ if (pk && result.rows && result.rows.length > 0) {
442
+ lastId = result.rows[result.rows.length - 1][pk] || lastId;
433
443
  }
434
- } catch (e) {
435
- throw mapPgError(e);
436
- } finally {
437
- client.release();
438
444
  }
445
+
446
+ await client.query("COMMIT");
447
+ } catch (e) {
448
+ await client.query("ROLLBACK").catch(() => {});
449
+ throw mapPgError(e);
450
+ } finally {
451
+ client.release();
439
452
  }
440
453
 
441
454
  return {
@@ -471,13 +484,13 @@ async function upsert(table, data, uniqueKeys = []) {
471
484
  const updateCols = columns.filter((c) => c !== keyCol);
472
485
  if (updateCols.length === 0) continue;
473
486
  const setClause = updateCols
474
- .map((c, i) => `${c} = $${i + 1}`)
487
+ .map((c, i) => `${c} = ${i + 1}`)
475
488
  .join(", ");
476
489
  const vals = [
477
490
  ...updateCols.map((c) => sanitizeValue(row[c])),
478
491
  row[keyCol],
479
492
  ];
480
- const sql = `UPDATE ${table} SET ${setClause} WHERE ${keyCol} = $${updateCols.length + 1}`;
493
+ const sql = `UPDATE ${table} SET ${setClause} WHERE ${keyCol} = ${updateCols.length + 1}`;
481
494
  await query(sql, vals);
482
495
  if (row[keyCol]) lastId = row[keyCol];
483
496
  }
@@ -497,34 +510,51 @@ async function upsert(table, data, uniqueKeys = []) {
497
510
  const nonUniqueColumns = columns.filter((c) => !uniqueKeys.includes(c));
498
511
  const pk = await getPkColumn(table);
499
512
  let lastId = 0;
513
+ const BATCH_SIZE = 1000;
514
+
515
+ const client = await pool.connect();
516
+ try {
517
+ await client.query("BEGIN");
500
518
 
501
- for (const row of array) {
502
- const vals = columns.map((c) => sanitizeValue(row[c]));
503
- const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
504
- const conflictCols = uniqueKeys.join(",");
505
519
  const updateSetSql = nonUniqueColumns
506
520
  .map((c) => `${c} = EXCLUDED.${c}`)
507
521
  .join(", ");
522
+ const conflictCols = uniqueKeys.join(",");
523
+ const colList = columns.join(",");
524
+
525
+ for (let offset = 0; offset < total; offset += BATCH_SIZE) {
526
+ const batch = array.slice(offset, offset + BATCH_SIZE);
527
+ let paramIdx = 0;
528
+ const allParams = [];
529
+ const valuesClauses = batch.map((row) => {
530
+ const placeholders = columns.map((c) => {
531
+ paramIdx++;
532
+ allParams.push(sanitizeValue(row[c]));
533
+ return "$" + paramIdx;
534
+ });
535
+ return "(" + placeholders.join(",") + ")";
536
+ });
508
537
 
509
- let sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES (${placeholders}) ON CONFLICT (${conflictCols})`;
510
- if (updateSetSql) {
511
- sql += ` DO UPDATE SET ${updateSetSql}`;
512
- } else {
513
- sql += ` DO NOTHING`;
514
- }
515
- if (pk) sql += ` RETURNING ${pk}`;
538
+ let sql = `INSERT INTO ${table} (${colList}) VALUES ${valuesClauses.join(",")} ON CONFLICT (${conflictCols})`;
539
+ if (updateSetSql) {
540
+ sql += ` DO UPDATE SET ${updateSetSql}`;
541
+ } else {
542
+ sql += ` DO NOTHING`;
543
+ }
544
+ if (pk) sql += ` RETURNING ${pk}`;
516
545
 
517
- const client = await pool.connect();
518
- try {
519
- const result = await client.query(sql, vals);
520
- if (result.rows && result.rows.length > 0 && pk) {
521
- lastId = result.rows[0][pk] || 0;
546
+ const result = await client.query(sql, allParams);
547
+ if (pk && result.rows && result.rows.length > 0) {
548
+ lastId = result.rows[result.rows.length - 1][pk] || lastId;
522
549
  }
523
- } catch (e) {
524
- throw mapPgError(e);
525
- } finally {
526
- client.release();
527
550
  }
551
+
552
+ await client.query("COMMIT");
553
+ } catch (e) {
554
+ await client.query("ROLLBACK").catch(() => {});
555
+ throw mapPgError(e);
556
+ } finally {
557
+ client.release();
528
558
  }
529
559
 
530
560
  const response = {
@@ -554,6 +584,7 @@ module.exports = {
554
584
  query,
555
585
  qcount,
556
586
  remove,
587
+ delete: remove,
557
588
  upsert,
558
589
  change: upsert,
559
590
  insert,
@@ -157,12 +157,10 @@ module.exports = function route(model, override = {}) {
157
157
  })
158
158
  .post("/", (req, res) => {
159
159
  if (!req.body || !Array.isArray(req.body.data)) {
160
- return res
161
- .status(400)
162
- .send({
163
- type: "danger",
164
- message: "Request body must contain a 'data' array",
165
- });
160
+ return res.status(400).send({
161
+ type: "danger",
162
+ message: "Request body must contain a 'data' array",
163
+ });
166
164
  }
167
165
  let payload = payloadOverride(req.body.data, req, override);
168
166
  model
@@ -176,12 +174,10 @@ module.exports = function route(model, override = {}) {
176
174
  })
177
175
  .put("/", (req, res) => {
178
176
  if (!req.body || !Array.isArray(req.body.data)) {
179
- return res
180
- .status(400)
181
- .send({
182
- type: "danger",
183
- message: "Request body must contain a 'data' array",
184
- });
177
+ return res.status(400).send({
178
+ type: "danger",
179
+ message: "Request body must contain a 'data' array",
180
+ });
185
181
  }
186
182
  let payload = payloadOverride(req.body.data, req, override);
187
183
  model
@@ -194,15 +190,19 @@ module.exports = function route(model, override = {}) {
194
190
  });
195
191
  })
196
192
  .delete("/", (req, res) => {
197
- if (!req.body || !Array.isArray(req.body.data)) {
198
- return res
199
- .status(400)
200
- .send({
201
- type: "danger",
202
- message: "Request body must contain a 'data' array",
203
- });
193
+ if (!req.body || Object.keys(req.body).length === 0) {
194
+ return res.status(400).send({
195
+ type: "danger",
196
+ message: "Request body must contain filter criteria",
197
+ });
198
+ }
199
+ // Accept { data: [...] } (legacy array) or plain filter object in body
200
+ let payload;
201
+ if (req.body.data && Array.isArray(req.body.data)) {
202
+ payload = payloadOverride(req.body.data, req, override);
203
+ } else {
204
+ payload = payloadOverride(req.body, req, override);
204
205
  }
205
- let payload = payloadOverride(req.body.data, req, override);
206
206
  model
207
207
  .remove(payload)
208
208
  .then((response) => {