support bot: select db backend with --db

This commit is contained in:
shum
2026-09-25 12:51:14 +00:00
parent 80182a7237
commit afbb75f887
4 changed files with 56 additions and 108 deletions
+10 -25
View File
@@ -12,30 +12,18 @@ A business-address bot that triages incoming support chats, optionally runs them
```bash
cd apps/simplex-support-bot
npm install # downloads native libs + transitive deps
npm install
npm run build # tsc
```
By default this installs the **SQLite** backend.
The native library is downloaded on first start into the user cache. To download it ahead of time (for example in a Dockerfile), run as the user that runs the bot:
To use **PostgreSQL** instead, drop a `.npmrc` next to `package.json` *before* `npm install`:
```bash
echo 'simplex_backend=postgres' > .npmrc
npm install # now pulls postgres-flavored native libs
npm run build
```sh
npx simplex-chat install --backend sqlite
npx simplex-chat install --backend postgres # Linux x86_64 only
```
`.npmrc` lives next to the package — npm reads it natively, no extra setup.
### Switching backends
`npm install` is a no-op for already-installed deps, so editing `.npmrc` and re-running `npm install` will *not* re-trigger `simplex-chat`'s preinstall. To switch backends, force a clean install:
```bash
rm -rf node_modules
npm install # download-libs.js re-runs and pulls the right native lib
```
Select the database with `--db sqlite` (default) or `--db postgres`.
## Run
@@ -47,11 +35,9 @@ npm start -- --team-group "Support Team"
# PostgreSQL
npm start -- --team-group "Support Team" \
--pg-conn "postgres://user:pass@host/db"
--db postgres --pg-conn "postgres://user:pass@host/db"
```
The bot runs via `npm start` so npm can expose `.npmrc` settings to the process — `detectBackend()` reads `npm_config_simplex_backend` to know which backend was installed.
## Flags
Run `npm start -- --help` for the auto-generated reference. Summary:
@@ -60,6 +46,7 @@ Run `npm start -- --help` for the auto-generated reference. Summary:
|---|---|---|---|---|
| `--team-group` | both | yes | — | team group display name |
| `--state-file` | both | no | `./data/state.json` | path to bot state JSON |
| `--db` | both | no | `sqlite` | `sqlite` or `postgres` |
| `--sqlite-file-prefix` | sqlite | no | `./data/simplex` | DB file prefix (creates `<prefix>_chat.db`, `<prefix>_agent.db`) |
| `--sqlite-key` | sqlite | no | (unencrypted) | SQLCipher encryption key |
| `--pg-conn` | postgres | yes | — | PostgreSQL connection string |
@@ -76,7 +63,7 @@ Run `npm start -- --help` for the auto-generated reference. Summary:
| Var | Purpose |
|---|---|
| `GROK_API_KEY` | xAI API key; enables Grok replies |
| `SIMPLEX_BACKEND` | alternative to `.npmrc` for selecting the install backend (`sqlite` or `postgres`) |
| `SIMPLEX_LIBS_DIR` | directory with a local libsimplex build for the `--db` backend, instead of the downloaded one |
## Local development against unreleased lib changes
@@ -94,8 +81,6 @@ npm link simplex-chat
## Troubleshooting
- **`--pg-conn is required when backend is postgres`** — the postgres backend is installed but you didn't pass a connection string.
- **`--pg-conn is required with --db postgres`** — pass a connection string, or use `--db sqlite`.
- **`libpq5` errors at startup** — install `libpq5` on the host (`apt install libpq5` on Debian/Ubuntu).
- **`ENOENT: no such file or directory, open './data/state.json'`** — the parent directory of `--state-file` must exist; `mkdir -p data` before starting.
- **Wrong backend installed** — check `node_modules/simplex-chat/libs/installed.txt`. Edit `.npmrc`, then `rm -rf node_modules && npm install` to switch (`npm install` alone won't re-run the dep's preinstall).
- **`libpq` connection error** at startup with sqlite-flavored config (or vice versa) — `.npmrc` was changed but libs weren't reinstalled. See "Switching backends" above.
+36 -54
View File
@@ -2403,54 +2403,48 @@ describe("parseConfig Validation", () => {
.toThrow(/--complete-hours must be a non-negative integer, got "abc"/)
})
test("postgres backend without --pg-conn → throws", () => {
const prev = process.env.SIMPLEX_BACKEND
process.env.SIMPLEX_BACKEND = "postgres"
try {
expect(() => parseConfig(baseArgs))
.toThrow(/--pg-conn is required when backend is postgres/)
} finally {
if (prev === undefined) delete process.env.SIMPLEX_BACKEND
else process.env.SIMPLEX_BACKEND = prev
}
test("--db postgres without --pg-conn → throws", () => {
expect(() => parseConfig([...baseArgs, "--db", "postgres"]))
.toThrow(/--pg-conn is required with --db postgres/)
})
test("postgres backend with --pg-conn → db is postgres DbConfig", () => {
const prev = process.env.SIMPLEX_BACKEND
process.env.SIMPLEX_BACKEND = "postgres"
try {
const cfg = parseConfig([...baseArgs, "--pg-conn", "postgres://user:pass@localhost/db"])
expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://user:pass@localhost/db"})
} finally {
if (prev === undefined) delete process.env.SIMPLEX_BACKEND
else process.env.SIMPLEX_BACKEND = prev
}
test("--db postgres with --pg-conn → db is postgres DbConfig", () => {
const cfg = parseConfig([...baseArgs, "--db", "postgres", "--pg-conn", "postgres://user:pass@localhost/db"])
expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://user:pass@localhost/db"})
})
test("postgres backend with --pg-schema → DbConfig carries schemaPrefix", () => {
const prev = process.env.SIMPLEX_BACKEND
process.env.SIMPLEX_BACKEND = "postgres"
try {
const cfg = parseConfig([...baseArgs, "--pg-conn", "postgres://localhost/db", "--pg-schema", "bot"])
expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://localhost/db", schemaPrefix: "bot"})
} finally {
if (prev === undefined) delete process.env.SIMPLEX_BACKEND
else process.env.SIMPLEX_BACKEND = prev
}
test("--db postgres with --pg-schema → DbConfig carries schemaPrefix", () => {
const cfg = parseConfig([...baseArgs, "--db", "postgres", "--pg-conn", "postgres://localhost/db", "--pg-schema", "bot"])
expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://localhost/db", schemaPrefix: "bot"})
})
test("sqlite backend (default) → db is sqlite DbConfig with default filePrefix", () => {
const prevBackend = process.env.SIMPLEX_BACKEND
const prevNpm = process.env.npm_config_simplex_backend
delete process.env.SIMPLEX_BACKEND
delete process.env.npm_config_simplex_backend
try {
const cfg = parseConfig(baseArgs)
expect(cfg.db).toEqual({type: "sqlite", filePrefix: "./data/simplex"})
} finally {
if (prevBackend !== undefined) process.env.SIMPLEX_BACKEND = prevBackend
if (prevNpm !== undefined) process.env.npm_config_simplex_backend = prevNpm
}
test("default --db → sqlite DbConfig with default filePrefix", () => {
const cfg = parseConfig(baseArgs)
expect(cfg.db).toEqual({type: "sqlite", filePrefix: "./data/simplex"})
})
test("--pg-conn with sqlite → throws", () => {
expect(() => parseConfig([...baseArgs, "--pg-conn", "postgres://localhost/db"]))
.toThrow(/--pg-conn and --pg-schema require --db postgres/)
})
test("--pg-schema with sqlite → throws", () => {
expect(() => parseConfig([...baseArgs, "--pg-schema", "bot"]))
.toThrow(/--pg-conn and --pg-schema require --db postgres/)
})
test("--sqlite-key with postgres → throws", () => {
expect(() => parseConfig([...baseArgs, "--db", "postgres", "--pg-conn", "c", "--sqlite-key", "k"]))
.toThrow(/--sqlite-file-prefix and --sqlite-key require --db sqlite/)
})
test("--sqlite-file-prefix with postgres → throws", () => {
expect(() => parseConfig([...baseArgs, "--db", "postgres", "--pg-conn", "c", "--sqlite-file-prefix", "./x"]))
.toThrow(/--sqlite-file-prefix and --sqlite-key require --db sqlite/)
})
test("invalid --db → throws", () => {
expect(() => parseConfig([...baseArgs, "--db", "mysql"])).toThrow(/Allowed choices are sqlite, postgres/)
})
test("sqlite backend with --sqlite-key → DbConfig carries encryptionKey", () => {
@@ -2468,18 +2462,6 @@ describe("parseConfig Validation", () => {
.toThrow(/required option '--team-group/)
})
test("invalid SIMPLEX_BACKEND → throws", () => {
const prev = process.env.SIMPLEX_BACKEND
process.env.SIMPLEX_BACKEND = "mysql"
try {
expect(() => parseConfig(baseArgs))
.toThrow(/Invalid SIMPLEX_BACKEND: "mysql"/)
} finally {
if (prev === undefined) delete process.env.SIMPLEX_BACKEND
else process.env.SIMPLEX_BACKEND = prev
}
})
test("--complete-hours negative → throws", () => {
// parseArgs refuses "-1" as a bare arg (ambiguous with a short flag), so use `=` form
expect(() => parseConfig([...baseArgs, "--complete-hours", "-1"]))
+10 -22
View File
@@ -1,4 +1,4 @@
import {Command} from "commander"
import {Command, Option} from "commander"
import {api} from "simplex-chat"
export interface IdName {
@@ -6,8 +6,6 @@ export interface IdName {
name: string
}
export type Backend = "sqlite" | "postgres"
export interface Config {
stateFile: string // local path to the bot's state JSON
db: api.DbConfig // passed to ChatApi.init / bot.run
@@ -21,18 +19,6 @@ export interface Config {
grokApiKey: string | null
}
// Mirrors packages/simplex-chat-nodejs/src/download-libs.js so runtime detection
// matches what was used at install time. Works whether the user installed via
// SIMPLEX_BACKEND env var, .npmrc (→ npm_config_simplex_backend), or the
// --simplex_backend=postgres CLI flag (also surfaced as npm_config_*).
export function detectBackend(): Backend {
const raw = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || "sqlite").toLowerCase()
if (raw !== "sqlite" && raw !== "postgres") {
throw new Error(`Invalid SIMPLEX_BACKEND: "${raw}". Must be "sqlite" or "postgres".`)
}
return raw
}
export function parseIdName(s: string): IdName {
const i = s.indexOf(":")
if (i < 1) throw new Error(`Invalid ID:name format: "${s}"`)
@@ -57,21 +43,23 @@ function buildCommand(): Command {
.description("business-address triage bot")
.requiredOption("--team-group <name>", "team group display name")
.option("--state-file <path>", "state JSON path", "./data/state.json")
.addOption(new Option("--db <backend>", "database backend").choices(["sqlite", "postgres"]).default("sqlite"))
.option("--sqlite-file-prefix <path>", "SQLite DB file prefix", "./data/simplex")
.option("--sqlite-key <key>", "SQLCipher encryption key (default: unencrypted)")
.option("--pg-conn <conn>", "PostgreSQL connection string (required for postgres)")
.option("--pg-conn <conn>", "PostgreSQL connection string (required with --db postgres)")
.option("--pg-schema <prefix>", "PostgreSQL schema prefix (default: simplex_v1)")
.option("-a, --auto-add-team-members <list>", "comma-separated ID:name pairs (e.g. 1:Alice,2:Bob)")
.option("--timezone <iana>", "IANA timezone for weekend detection", "UTC")
.option("--complete-hours <n>", "auto-complete chats after N hours idle (0 disables)", parseNonNegativeInt("--complete-hours"), 3)
.option("--card-flush-seconds <n>", "debounce card state writes", parseNonNegativeInt("--card-flush-seconds"), 300)
.option("--context-file <path>", "text file with Grok system context (required if GROK_API_KEY set)")
.addHelpText("after", "\nEnvironment:\n GROK_API_KEY xAI API key — enables Grok replies\n SIMPLEX_BACKEND sqlite | postgres — alternative to .npmrc for backend selection\n")
.addHelpText("after", "\nEnvironment:\n GROK_API_KEY xAI API key — enables Grok replies\n SIMPLEX_LIBS_DIR local libsimplex build for the --db backend\n")
}
interface RawOpts {
teamGroup: string
stateFile: string
db: api.DbConfig["type"]
sqliteFilePrefix: string
sqliteKey?: string
pgConn?: string
@@ -93,19 +81,19 @@ export function parseConfig(args: string[]): Config {
throw err
}
const opts = cmd.opts<RawOpts>()
const given = (option: keyof RawOpts) => cmd.getOptionValueSource(option) === "cli"
const grokApiKey = process.env.GROK_API_KEY || null
const backend = detectBackend()
let db: api.DbConfig
if (backend === "sqlite") {
if (opts.db === "sqlite") {
if (given("pgConn") || given("pgSchema")) throw new Error("--pg-conn and --pg-schema require --db postgres")
db = opts.sqliteKey
? {type: "sqlite", filePrefix: opts.sqliteFilePrefix, encryptionKey: opts.sqliteKey}
: {type: "sqlite", filePrefix: opts.sqliteFilePrefix}
} else {
if (!opts.pgConn) {
throw new Error("--pg-conn is required when backend is postgres (PostgreSQL connection string)")
}
if (given("sqliteFilePrefix") || given("sqliteKey")) throw new Error("--sqlite-file-prefix and --sqlite-key require --db sqlite")
if (!opts.pgConn) throw new Error("--pg-conn is required with --db postgres")
db = opts.pgSchema
? {type: "postgres", connectionString: opts.pgConn, schemaPrefix: opts.pgSchema}
: {type: "postgres", connectionString: opts.pgConn}
@@ -5,13 +5,6 @@ export default defineConfig({
test: {
globals: true,
testTimeout: 10000,
// Clear backend signals — .npmrc next to package.json otherwise injects
// npm_config_simplex_backend into every test's env, breaking sqlite-default
// assumptions in parseConfig tests.
env: {
SIMPLEX_BACKEND: "",
npm_config_simplex_backend: "",
},
},
resolve: {
alias: {