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
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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.
|
|
106
|
+
imports += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
//
|
|
97
|
-
for (const
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
428
|
+
// Write child route files in subfolders: routes/<parent>/<child>.js
|
|
408
429
|
for (const rel of relationships) {
|
|
409
|
-
const
|
|
410
|
-
|
|
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.
|
|
417
|
-
|
|
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
|
|
488
|
-
|
|
489
|
-
|
|
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.
|
|
521
|
+
generateChildTestFile(rel.child, rel.parent, rel.foreignKey, pk),
|
|
494
522
|
);
|
|
495
523
|
console.log(` Created ${testPath}`);
|
|
496
524
|
}
|
|
@@ -367,21 +367,21 @@ function generateAppJs(answers) {
|
|
|
367
367
|
answers.framework === "ultimate-express" ? "ultimate-express" : "express";
|
|
368
368
|
|
|
369
369
|
// Imports
|
|
370
|
-
let imports = `
|
|
371
|
-
|
|
372
|
-
|
|
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 += `\
|
|
376
|
-
|
|
375
|
+
imports += `\nimport RedisStore from "connect-redis";
|
|
376
|
+
import { Redis } from "ioredis";`;
|
|
377
377
|
}
|
|
378
378
|
if (answers.rateLimiting) {
|
|
379
|
-
imports += `\
|
|
379
|
+
imports += `\nimport rateLimit from "express-rate-limit";`;
|
|
380
380
|
}
|
|
381
381
|
if (answers.helmet) {
|
|
382
|
-
imports += `\
|
|
382
|
+
imports += `\nimport helmet from "helmet";`;
|
|
383
383
|
}
|
|
384
|
-
imports += `\
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
? `
|
|
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
|
-
|
|
707
|
+
import fs from "fs";
|
|
708
|
+
import path from "path";
|
|
709
|
+
import { fileURLToPath } from "url";
|
|
708
710
|
|
|
709
|
-
const
|
|
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}/
|
|
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, "
|
|
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
|
-
//
|
|
193
|
-
const healthPath = path.join(srcBase, "
|
|
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, "
|
|
195
|
+
files.push(path.join(srcBase, "routes/health.js"));
|
|
196
196
|
|
|
197
|
-
//
|
|
198
|
-
const routeIndexPath = path.join(srcBase, "
|
|
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, "
|
|
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/,
|
|
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
|
-
["--
|
|
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)"]],
|
package/src/cockroachdb/db.js
CHANGED
|
@@ -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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
await client.query(
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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} =
|
|
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} =
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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,
|
package/src/commons/route.js
CHANGED
|
@@ -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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 ||
|
|
198
|
-
return res
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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) => {
|