db-model-router 1.0.3 → 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 (97) hide show
  1. package/README.md +283 -25
  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 +16 -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 +185 -0
  69. package/src/cli/commands/init.js +42 -14
  70. package/src/cli/commands/inspect.js +21 -3
  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-model.js +5 -4
  75. package/src/cli/generate-route.js +79 -45
  76. package/src/cli/init/dependencies.js +17 -5
  77. package/src/cli/init/generators.js +1073 -64
  78. package/src/cli/init/prompt.js +37 -5
  79. package/src/cli/init.js +148 -25
  80. package/src/cli/main.js +90 -10
  81. package/src/cockroachdb/db.js +90 -59
  82. package/src/commons/route.js +20 -20
  83. package/src/commons/validator.js +58 -1
  84. package/src/dynamodb/db.js +50 -27
  85. package/src/index.js +2 -0
  86. package/src/mongodb/db.js +1 -0
  87. package/src/mssql/db.js +89 -61
  88. package/src/mysql/db.js +1 -0
  89. package/src/oracle/db.js +1 -0
  90. package/src/postgres/db.js +61 -41
  91. package/src/redis/db.js +1 -0
  92. package/src/schema/schema-parser.js +43 -1
  93. package/src/schema/schema-printer.js +8 -5
  94. package/src/schema/schema-to-meta.js +4 -0
  95. package/src/schema/schema-validator.js +20 -1
  96. package/src/sqlite3/db.js +1 -0
  97. package/docs/SKILL.md +0 -374
@@ -5,6 +5,7 @@ const inquirer = require("inquirer");
5
5
  const VALID_FRAMEWORKS = ["ultimate-express", "express"];
6
6
  const VALID_DATABASES = [
7
7
  "mysql",
8
+ "mariadb",
8
9
  "postgres",
9
10
  "sqlite3",
10
11
  "mongodb",
@@ -56,12 +57,17 @@ function parseInitArgs(argv) {
56
57
  }
57
58
 
58
59
  // Boolean flags
59
- for (const flag of ["rateLimiting", "helmet", "logger"]) {
60
+ for (const flag of ["rateLimiting", "helmet", "logger", "loki"]) {
60
61
  if (args[flag] !== undefined) {
61
62
  partial[flag] = parseBool(args[flag]);
62
63
  }
63
64
  }
64
65
 
66
+ // Output directory
67
+ if (args.output !== undefined && args.output !== "true") {
68
+ partial.output = args.output;
69
+ }
70
+
65
71
  return partial;
66
72
  }
67
73
 
@@ -114,12 +120,22 @@ async function promptUser(prefilledAnswers) {
114
120
  });
115
121
  }
116
122
 
123
+ if (prefilled.output === undefined) {
124
+ questions.push({
125
+ type: "input",
126
+ name: "output",
127
+ message:
128
+ "Output directory for backend source files (leave empty for root):",
129
+ default: "",
130
+ });
131
+ }
132
+
117
133
  if (prefilled.rateLimiting === undefined) {
118
134
  questions.push({
119
135
  type: "confirm",
120
136
  name: "rateLimiting",
121
137
  message: "Enable rate limiting?",
122
- default: false,
138
+ default: true,
123
139
  });
124
140
  }
125
141
 
@@ -128,7 +144,7 @@ async function promptUser(prefilledAnswers) {
128
144
  type: "confirm",
129
145
  name: "helmet",
130
146
  message: "Enable Helmet security headers?",
131
- default: false,
147
+ default: true,
132
148
  });
133
149
  }
134
150
 
@@ -136,8 +152,8 @@ async function promptUser(prefilledAnswers) {
136
152
  questions.push({
137
153
  type: "confirm",
138
154
  name: "logger",
139
- message: "Enable request/response logger?",
140
- default: false,
155
+ message: "Enable request/response logger (Winston)?",
156
+ default: true,
141
157
  });
142
158
  }
143
159
 
@@ -147,6 +163,22 @@ async function promptUser(prefilledAnswers) {
147
163
  }
148
164
 
149
165
  const prompted = await inquirer.prompt(questions);
166
+
167
+ // Follow-up: if logger is enabled, ask about Loki
168
+ if (prompted.logger && prefilled.loki === undefined) {
169
+ const lokiAnswer = await inquirer.prompt([
170
+ {
171
+ type: "confirm",
172
+ name: "loki",
173
+ message: "Send logs to Grafana Loki?",
174
+ default: false,
175
+ },
176
+ ]);
177
+ prompted.loki = lokiAnswer.loki;
178
+ } else if (!prompted.logger && prefilled.logger === undefined) {
179
+ prompted.loki = false;
180
+ }
181
+
150
182
  return Object.assign({}, prefilled, prompted);
151
183
  }
152
184
 
package/src/cli/init.js CHANGED
@@ -7,14 +7,26 @@ const { execSync } = require("child_process");
7
7
 
8
8
  const {
9
9
  generateAppJs,
10
+ generateAppJsV2,
10
11
  generateEnvFile,
11
12
  generateEnvExample,
12
13
  generateLoggerMiddleware,
13
- generateMigrateScript,
14
- generateAddMigrationScript,
15
14
  generateInitialMigration,
16
15
  generateSessionMigration,
17
16
  generateGitignore,
17
+ generateDockerfile,
18
+ generateDockerignore,
19
+ generateGrafanaDatasources,
20
+ generateDockerCompose,
21
+ generateCloudBeaverDataSources,
22
+ generateSessionJs,
23
+ generateMigrateModule,
24
+ generateAddMigrationModule,
25
+ generateSecurityJs,
26
+ generateHealthRoute,
27
+ generateRouteIndexFile,
28
+ generateDbModule,
29
+ randomPassword,
18
30
  } = require("./init/generators");
19
31
 
20
32
  const { collectDependencies, getScripts } = require("./init/dependencies");
@@ -61,40 +73,139 @@ function safeWriteFile(filePath, content) {
61
73
  * Creates directories and writes files. Skips files that already exist.
62
74
  * Returns the list of generated filenames for the summary.
63
75
  * @param {import('./init/types').InitAnswers} answers
76
+ * @param {string} [outputDir] - relative directory for source files (e.g. "backend")
64
77
  * @returns {{ files: string[], migrationFiles: string[] }}
65
78
  */
66
- function generateFiles(answers) {
79
+ function generateFiles(answers, outputDir) {
67
80
  const files = [];
68
81
  const migrationFiles = [];
69
82
 
83
+ // Resolve source directory (outputDir-relative or cwd)
84
+ const srcBase = outputDir || ".";
85
+
86
+ // Generate random secrets (shared between .env and docker-compose)
87
+ const secrets = {
88
+ dbPass: randomPassword(),
89
+ redisPass: answers.session === "redis" ? randomPassword() : "",
90
+ sessionSecret: randomPassword(32),
91
+ };
92
+
70
93
  // Create directories
71
- if (!fs.existsSync("middleware")) {
72
- fs.mkdirSync("middleware", { recursive: true });
94
+ const dirs = [
95
+ path.join(srcBase, "middleware"),
96
+ path.join(srcBase, "migrations"),
97
+ path.join(srcBase, "commons"),
98
+ path.join(srcBase, "routes"),
99
+ ];
100
+ // SQLite3 needs a data/ folder for the database file
101
+ if (answers.database === "sqlite3") {
102
+ dirs.push("data");
73
103
  }
74
- if (!fs.existsSync("migrations")) {
75
- fs.mkdirSync("migrations", { recursive: true });
104
+ for (const dir of dirs) {
105
+ if (!fs.existsSync(dir)) {
106
+ fs.mkdirSync(dir, { recursive: true });
107
+ }
76
108
  }
77
109
 
78
- // Write files (skip if they already exist)
79
- if (safeWriteFile("app.js", generateAppJs(answers))) files.push("app.js");
80
- if (safeWriteFile(".env", generateEnvFile(answers))) files.push(".env");
110
+ // Root-level files (always in cwd, not in outputDir)
111
+ // app.js uses the v2 generator that links commons/route modules
112
+ if (safeWriteFile("app.js", generateAppJsV2(answers, outputDir || "")))
113
+ files.push("app.js");
114
+ if (safeWriteFile(".env", generateEnvFile(answers, secrets)))
115
+ files.push(".env");
81
116
  if (safeWriteFile(".env.example", generateEnvExample(answers)))
82
117
  files.push(".env.example");
83
-
84
- const loggerPath = path.join("middleware", "logger.js");
85
- if (safeWriteFile(loggerPath, generateLoggerMiddleware(answers)))
86
- files.push("middleware/logger.js");
87
-
88
- if (safeWriteFile("migrate.js", generateMigrateScript(answers)))
89
- files.push("migrate.js");
90
- if (safeWriteFile("add_migration.js", generateAddMigrationScript(answers)))
91
- files.push("add_migration.js");
92
118
  if (safeWriteFile(".gitignore", generateGitignore()))
93
119
  files.push(".gitignore");
94
120
 
95
- // Initial migration
121
+ // docker-compose.yml (if the database needs Docker)
122
+ const dockerCompose = generateDockerCompose(answers, secrets);
123
+ if (dockerCompose !== null) {
124
+ if (safeWriteFile("docker-compose.yml", dockerCompose))
125
+ files.push("docker-compose.yml");
126
+ }
127
+
128
+ // CloudBeaver data-sources.json (auto-connect config)
129
+ const cbDataSources = generateCloudBeaverDataSources(answers, secrets);
130
+ if (cbDataSources !== null) {
131
+ const cbDir = ".cloudbeaver";
132
+ if (!fs.existsSync(cbDir)) {
133
+ fs.mkdirSync(cbDir, { recursive: true });
134
+ }
135
+ const cbPath = path.join(cbDir, "data-sources.json");
136
+ if (safeWriteFile(cbPath, cbDataSources))
137
+ files.push(".cloudbeaver/data-sources.json");
138
+ }
139
+
140
+ // Grafana datasource provisioning (when loki is enabled)
141
+ if (answers.loki) {
142
+ const grafanaDir = ".grafana";
143
+ if (!fs.existsSync(grafanaDir)) {
144
+ fs.mkdirSync(grafanaDir, { recursive: true });
145
+ }
146
+ const grafanaPath = path.join(grafanaDir, "datasources.yml");
147
+ if (safeWriteFile(grafanaPath, generateGrafanaDatasources()))
148
+ files.push(".grafana/datasources.yml");
149
+ }
150
+
151
+ // Dockerfile and .dockerignore
152
+ if (safeWriteFile("Dockerfile", generateDockerfile(answers, outputDir)))
153
+ files.push("Dockerfile");
154
+ if (safeWriteFile(".dockerignore", generateDockerignore()))
155
+ files.push(".dockerignore");
156
+
157
+ // Source files inside outputDir (or cwd if no outputDir)
158
+ const loggerPath = path.join(srcBase, "middleware", "logger.js");
159
+ if (safeWriteFile(loggerPath, generateLoggerMiddleware(answers)))
160
+ files.push(path.join(srcBase, "middleware/logger.js"));
161
+
162
+ // commons/session.js
163
+ const sessionPath = path.join(srcBase, "commons", "session.js");
164
+ if (safeWriteFile(sessionPath, generateSessionJs(answers)))
165
+ files.push(path.join(srcBase, "commons/session.js"));
166
+
167
+ // commons/migrate.js
168
+ const migratePath = path.join(srcBase, "commons", "migrate.js");
169
+ if (safeWriteFile(migratePath, generateMigrateModule(answers, outputDir)))
170
+ files.push(path.join(srcBase, "commons/migrate.js"));
171
+
172
+ // commons/add_migration.js
173
+ const addMigrationPath = path.join(srcBase, "commons", "add_migration.js");
174
+ if (
175
+ safeWriteFile(
176
+ addMigrationPath,
177
+ generateAddMigrationModule(answers, outputDir),
178
+ )
179
+ )
180
+ files.push(path.join(srcBase, "commons/add_migration.js"));
181
+
182
+ // commons/security.js
183
+ const securityPath = path.join(srcBase, "commons", "security.js");
184
+ if (safeWriteFile(securityPath, generateSecurityJs(answers)))
185
+ files.push(path.join(srcBase, "commons/security.js"));
186
+
187
+ // commons/db.js
188
+ const dbPath = path.join(srcBase, "commons", "db.js");
189
+ if (safeWriteFile(dbPath, generateDbModule(answers)))
190
+ files.push(path.join(srcBase, "commons/db.js"));
191
+
192
+ // routes/health.js
193
+ const healthPath = path.join(srcBase, "routes", "health.js");
194
+ if (safeWriteFile(healthPath, generateHealthRoute()))
195
+ files.push(path.join(srcBase, "routes/health.js"));
196
+
197
+ // routes/index.js
198
+ const routeIndexPath = path.join(srcBase, "routes", "index.js");
199
+ if (safeWriteFile(routeIndexPath, generateRouteIndexFile()))
200
+ files.push(path.join(srcBase, "routes/index.js"));
201
+
202
+ // Initial migration (inside outputDir/migrations)
96
203
  const initialMigration = generateInitialMigration(answers);
97
- const initialPath = path.join("migrations", initialMigration.filename);
204
+ const initialPath = path.join(
205
+ srcBase,
206
+ "migrations",
207
+ initialMigration.filename,
208
+ );
98
209
  if (safeWriteFile(initialPath, initialMigration.content)) {
99
210
  migrationFiles.push(initialMigration.filename);
100
211
  }
@@ -102,8 +213,12 @@ function generateFiles(answers) {
102
213
  // Conditional session migration
103
214
  const sessionMigration = generateSessionMigration(answers);
104
215
  if (sessionMigration !== null) {
105
- const sessionPath = path.join("migrations", sessionMigration.filename);
106
- if (safeWriteFile(sessionPath, sessionMigration.content)) {
216
+ const sessionMigPath = path.join(
217
+ srcBase,
218
+ "migrations",
219
+ sessionMigration.filename,
220
+ );
221
+ if (safeWriteFile(sessionMigPath, sessionMigration.content)) {
107
222
  migrationFiles.push(sessionMigration.filename);
108
223
  }
109
224
  }
@@ -114,8 +229,9 @@ function generateFiles(answers) {
114
229
  /**
115
230
  * Update package.json with scripts and dependencies from the answers.
116
231
  * @param {import('./init/types').InitAnswers} answers
232
+ * @param {string} [outputDir] - relative output directory for source files
117
233
  */
118
- function updatePackageJson(answers) {
234
+ function updatePackageJson(answers, outputDir) {
119
235
  let raw;
120
236
  try {
121
237
  raw = fs.readFileSync("package.json", "utf8");
@@ -135,8 +251,9 @@ function updatePackageJson(answers) {
135
251
  }
136
252
 
137
253
  const { dependencies, devDependencies } = collectDependencies(answers);
138
- const scripts = getScripts();
254
+ const scripts = getScripts(outputDir);
139
255
 
256
+ pkg.type = "module";
140
257
  pkg.scripts = Object.assign({}, pkg.scripts || {}, scripts);
141
258
  pkg.dependencies = Object.assign({}, pkg.dependencies || {}, dependencies);
142
259
  pkg.devDependencies = Object.assign(
@@ -247,6 +364,9 @@ Options:
247
364
  cockroachdb, oracle, redis, dynamodb
248
365
  --db <name> Alias for --database
249
366
  --session <type> Session store: memory, redis, database
367
+ --output <dir> Directory for backend source files (e.g. --output backend).
368
+ package.json stays in root; index.js, commons/, routes/,
369
+ middleware/, and migrations/ go inside the output folder.
250
370
  --rateLimiting Enable rate limiting (express-rate-limit)
251
371
  --helmet Enable Helmet security headers
252
372
  --logger Enable request/response logger (express-mung)
@@ -256,6 +376,9 @@ Examples:
256
376
  # Fully non-interactive (LLM-friendly)
257
377
  db-model-router-init --framework express --database postgres --session redis --rateLimiting --helmet --logger
258
378
 
379
+ # With output directory
380
+ db-model-router-init --framework express --database postgres --output backend --yes
381
+
259
382
  # Partial — only prompts for missing values
260
383
  db-model-router-init --database mysql --session memory
261
384
 
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");
@@ -8,6 +8,7 @@ const inspectCmd = require("./commands/inspect");
8
8
  const generateCmd = require("./commands/generate");
9
9
  const doctorCmd = require("./commands/doctor");
10
10
  const diffCmd = require("./commands/diff");
11
+ const helpCmd = require("./commands/help");
11
12
 
12
13
  /**
13
14
  * Map of subcommand names to their handler functions.
@@ -18,6 +19,7 @@ const COMMANDS = {
18
19
  generate: generateCmd,
19
20
  doctor: doctorCmd,
20
21
  diff: diffCmd,
22
+ help: helpCmd,
21
23
  };
22
24
 
23
25
  /**
@@ -29,23 +31,74 @@ const COMMAND_DESCRIPTIONS = {
29
31
  generate: "Generate models, routes, tests, and OpenAPI spec from a schema",
30
32
  doctor: "Validate schema, check dependencies, and verify file sync",
31
33
  diff: "Preview changes between current files and what the schema would produce",
34
+ help: "Show help for a command",
32
35
  };
33
36
 
34
37
  /**
35
- * Print help message listing all available subcommands.
38
+ * Per-command flag summaries shown in the general help overview.
39
+ */
40
+ const COMMAND_FLAGS = {
41
+ init: [
42
+ ["--from <path>", "Read config from a schema file"],
43
+ ["--framework <name>", "express, ultimate-express"],
44
+ [
45
+ "--database <name>",
46
+ "mysql, mariadb, postgres, sqlite3, mongodb, mssql, cockroachdb, oracle, redis, dynamodb",
47
+ ],
48
+ ["--db <name>", "Alias for --database"],
49
+ ["--session <type>", "memory, redis, database"],
50
+ ["--output <dir>", "Directory for backend source files"],
51
+ ["--rateLimiting", "Enable rate limiting (default: yes)"],
52
+ ["--helmet", "Enable Helmet security headers (default: yes)"],
53
+ ["--logger", "Enable Winston + Loki logger (default: yes)"],
54
+ ],
55
+ inspect: [
56
+ ["--type <adapter>", "Database adapter (required)"],
57
+ ["--env <path>", "Path to .env file"],
58
+ ["--out <path>", "Output file (default: dbmr.schema.json)"],
59
+ ["--tables <list>", "Comma-separated table filter"],
60
+ ],
61
+ generate: [
62
+ ["--from <path>", "Schema file (default: dbmr.schema.json)"],
63
+ ["--models", "Generate only model files"],
64
+ ["--routes", "Generate only route files"],
65
+ ["--openapi", "Generate only OpenAPI spec"],
66
+ ["--tests", "Generate only test files"],
67
+ ["--llm-docs", "Generate only LLM documentation"],
68
+ ],
69
+ doctor: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
70
+ diff: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
71
+ };
72
+
73
+ /**
74
+ * Print help message listing all available subcommands with their flags.
36
75
  */
37
76
  function printHelp() {
38
77
  console.log("Usage: db-model-router <command> [options]\n");
39
- console.log("Commands:");
78
+
79
+ console.log("Commands:\n");
40
80
  for (const [name, desc] of Object.entries(COMMAND_DESCRIPTIONS)) {
81
+ if (name === "help") {
82
+ console.log(` ${name.padEnd(12)} ${desc}`);
83
+ continue;
84
+ }
41
85
  console.log(` ${name.padEnd(12)} ${desc}`);
86
+ const flags = COMMAND_FLAGS[name];
87
+ if (flags) {
88
+ for (const [flag, info] of flags) {
89
+ console.log(` ${flag.padEnd(22)} ${info}`);
90
+ }
91
+ console.log("");
92
+ }
42
93
  }
43
- console.log("\nGlobal flags:");
44
- console.log(" --yes Accept all defaults without prompting");
45
- console.log(" --json Output machine-readable JSON");
46
- console.log(" --dry-run Preview actions without side effects");
47
- console.log(" --no-install Skip npm install step");
48
- console.log(" --help Show help for a command");
94
+
95
+ console.log("Global flags (all commands):");
96
+ console.log(" --yes Accept all defaults without prompting");
97
+ console.log(" --json Output machine-readable JSON");
98
+ console.log(" --dry-run Preview actions without side effects");
99
+ console.log(" --no-install Skip npm install step");
100
+ console.log(" --help Show help for a command");
101
+ console.log('\nRun "db-model-router help <command>" for detailed usage.');
49
102
  }
50
103
 
51
104
  /**
@@ -65,11 +118,38 @@ function printUnknown(cmd) {
65
118
  async function main(argv) {
66
119
  const { subcommand, flags, args } = parseFlags(argv);
67
120
 
68
- if (!subcommand || flags.help) {
121
+ if (!subcommand) {
69
122
  printHelp();
70
123
  return;
71
124
  }
72
125
 
126
+ // `help <command>` — extract the topic from the second positional arg
127
+ if (subcommand === "help") {
128
+ // Re-parse to grab the second positional word as the help topic.
129
+ // parseFlags puts the first positional into subcommand; the second
130
+ // positional ends up as a key in args (if it looks like a flag value)
131
+ // or is lost. So we grab it directly from argv.
132
+ const topic =
133
+ argv.find((a, i) => i > 0 && !a.startsWith("-") && argv[0] === "help") ||
134
+ argv.find(
135
+ (a, i) => i > 0 && !a.startsWith("-") && argv.indexOf("help") < i,
136
+ );
137
+ args._command = topic || null;
138
+ const ctx = new OutputContext(flags);
139
+ await helpCmd(args, flags, ctx, { printHelp });
140
+ ctx.flush();
141
+ return;
142
+ }
143
+
144
+ // `<command> --help` — show detailed help for that command
145
+ if (flags.help) {
146
+ args._command = subcommand;
147
+ const ctx = new OutputContext(flags);
148
+ await helpCmd(args, flags, ctx, { printHelp });
149
+ ctx.flush();
150
+ return;
151
+ }
152
+
73
153
  if (!COMMANDS[subcommand]) {
74
154
  printUnknown(subcommand);
75
155
  process.exitCode = 1;
@@ -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,