feat: add export/import to the postgres backend; import auto-detects backend
CI / format (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 23s
CI / install-and-build (pull_request) Successful in 45s
CI / typecheck-source (pull_request) Successful in 25s
CI / typecheck-tests (pull_request) Successful in 30s
CI / test (pull_request) Successful in 40s
CI / format (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 23s
CI / install-and-build (pull_request) Successful in 45s
CI / typecheck-source (pull_request) Successful in 25s
CI / typecheck-tests (pull_request) Successful in 30s
CI / test (pull_request) Successful in 40s
The export/import plan predated the postgres backend. Bring it up to parity: - PostgresInterface implements Exportable + Importable, mirroring the sqlite backend. Export is a signal-checked async generator (one query per table); import drives a transaction via `WrappedPool.multi`, which rolls back on any error/abort and commits only after the whole stream is consumed cleanly. - The `note` kind is skipped on postgres (its note CRUD is still unimplemented, so a pg database holds none) — a full dump from sqlite imports its user/abode/resident/apikey records and drops notes. - Unlike sqlite (PRAGMA foreign_keys=off), postgres keeps FK enforcement; the FK-safe insertion order keeps a full dump valid, and truly-dangling partial dumps will (correctly) fail. - abode-import now resolves the backend via getDbInterface instead of constructing SqliteInterface directly; isImportable keeps it from ever running over the remote (api) interface, which has no import. - Add isImportable(); make postgres selectClientApikeys' where optional. - Tests: exercise the real PostgresInterface export/import paths against an in-memory fake WrappedPgClient (no pg service in CI) — NDJSON shape, meta.source, filtering, note-skip, insert dispatch, and error/abort rollback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+15
-10
@@ -1,9 +1,7 @@
|
||||
import { createReadStream } from "node:fs";
|
||||
import { getWrappedDb } from "../db/sqlite/impl/index.js";
|
||||
import { SqliteInterface } from "../db/sqlite/SqliteInterface.js";
|
||||
import { parseSqliteUrl } from "../db/sqlite/url.js";
|
||||
import { getDbInterface } from "../db/index.js";
|
||||
import { isExportKind } from "../db/export/filter.js";
|
||||
import type { ExportFilter } from "../db/types/ExportImport.js";
|
||||
import { isImportable, type ExportFilter } from "../db/types/ExportImport.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
@@ -15,14 +13,15 @@ function printUsage(err: boolean | string = false): never {
|
||||
}
|
||||
log("Usage:");
|
||||
log("\tabode-import --help");
|
||||
log("\tabode-import <sqlite-database-url> <input-file|-> [--kinds=...] \\");
|
||||
log("\tabode-import <database-url> <input-file|-> [--kinds=...] \\");
|
||||
log(
|
||||
"\t [--exclude-kinds=...] [--abodes=aid,...] [--users=uid,...]",
|
||||
);
|
||||
log("");
|
||||
log(
|
||||
"The target database must already be migrated (run abode-migrate first).",
|
||||
"The target must be a local backend (sqlite or postgres), already migrated",
|
||||
);
|
||||
log("(run abode-migrate first). Remote (api) targets are not importable.");
|
||||
process.exit(err ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -42,7 +41,7 @@ for (const arg of args) {
|
||||
|
||||
const url = positional[0];
|
||||
const input = positional[1];
|
||||
if (!url) printUsage("missing <sqlite-database-url>");
|
||||
if (!url) printUsage("missing <database-url>");
|
||||
if (!input) printUsage("missing <input-file|->");
|
||||
if (positional.length > 2) printUsage("too many arguments");
|
||||
|
||||
@@ -73,9 +72,15 @@ if (abodes) filter.abodes = abodes;
|
||||
const users = parseList(flags.get("users"));
|
||||
if (users) filter.users = users;
|
||||
|
||||
// Import is sqlite-only: construct the backend directly rather than resolving
|
||||
// generically, so it can never be pointed at a remote (api) target.
|
||||
const db = new SqliteInterface(getWrappedDb(...parseSqliteUrl(url)));
|
||||
// Resolve the backend generically. Import lives on the local backends (sqlite,
|
||||
// postgres); the remote (api) interface has no `import`, so `isImportable`
|
||||
// keeps it from ever running over HTTP.
|
||||
const db = await getDbInterface(url);
|
||||
if (!isImportable(db)) {
|
||||
console.error(`Error: backend '${db.name}' does not support import`);
|
||||
await db.close().catch(() => {});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const source = input === "-" ? process.stdin : createReadStream(input);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user