import { createReadStream } from "node:fs"; import { getDbInterface } from "../db/index.js"; import { isExportKind } from "../db/export/filter.js"; import { isImportable, type ExportFilter } from "../db/types/ExportImport.js"; const args = process.argv.slice(2); function printUsage(err: boolean | string = false): never { const log = (err ? console.error : console.log).bind(console); if (typeof err === "string") { log(`Error: ${err}`); log(""); } log("Usage:"); log("\tabode-import --help"); log("\tabode-import [--kinds=...] \\"); log( "\t [--exclude-kinds=...] [--abodes=aid,...] [--users=uid,...]", ); log(""); log( "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); } if (["-h", "--help", "help"].some((x) => args.includes(x))) printUsage(); const positional: string[] = []; const flags = new Map(); for (const arg of args) { if (arg.startsWith("--")) { const eq = arg.indexOf("="); if (eq === -1) printUsage(`invalid flag: ${arg}`); flags.set(arg.slice(2, eq), arg.slice(eq + 1)); } else { positional.push(arg); } } const url = positional[0]; const input = positional[1]; if (!url) printUsage("missing "); if (!input) printUsage("missing "); if (positional.length > 2) printUsage("too many arguments"); const knownFlags = ["kinds", "exclude-kinds", "abodes", "users"]; for (const key of flags.keys()) { if (!knownFlags.includes(key)) printUsage(`unknown flag: --${key}`); } function parseList(value: string | undefined): string[] | undefined { if (value === undefined) return undefined; return value.split(",").filter(Boolean); } function parseKinds(value: string | undefined) { const list = parseList(value); if (!list) return undefined; const bad = list.filter((k) => !isExportKind(k)); if (bad.length) printUsage(`invalid kind(s): ${bad.join(", ")}`); return list.filter(isExportKind); } const filter: ExportFilter = {}; const kinds = parseKinds(flags.get("kinds")); if (kinds) filter.kinds = kinds; const excludeKinds = parseKinds(flags.get("exclude-kinds")); if (excludeKinds) filter.excludeKinds = excludeKinds; const abodes = parseList(flags.get("abodes")); if (abodes) filter.abodes = abodes; const users = parseList(flags.get("users")); if (users) filter.users = users; // 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); const ac = new AbortController(); const onSignal = () => ac.abort(); process.on("SIGINT", onSignal); process.on("SIGTERM", onSignal); try { const result = await db.import(source, { filter, signal: ac.signal }); const total = Object.values(result.counts).reduce((a, b) => a + b, 0); console.log(`Imported ${total} record(s):`); for (const [kind, count] of Object.entries(result.counts)) { console.log(`- ${kind}: ${count}`); } } catch (e) { if (ac.signal.aborted) { console.error("Import aborted; no changes committed"); process.exit(130); } throw e; } finally { process.off("SIGINT", onSignal); process.off("SIGTERM", onSignal); await db.close().catch(() => {}); } process.exit(0);