Files
abode/src/bin/abode-inspect.ts
T
codingetandClaude ccb970f200
CI / format (pull_request) Successful in 28s
CI / lint (pull_request) Successful in 28s
CI / install-and-build (pull_request) Successful in 53s
CI / typecheck-tests (pull_request) Successful in 28s
CI / typecheck-source (pull_request) Successful in 32s
CI / test (pull_request) Successful in 43s
feat: add export/import streaming to the pluggable backends
Add backend-agnostic data export/import over an NDJSON wire format, plus an
inspect utility, exposed via three new CLIs and an HTTP export endpoint.

- ExportImport types + filter helpers (kind/record scoping, hard-intersection
  of filters) in src/db/{types/ExportImport,export/filter}.ts
- SqliteInterface implements Exportable + Importable: signal-checked async
  generator export (one query per table, per-record yield, trailing error
  sentinel on mid-stream failure) and a manually-driven import transaction
  that rolls back on any error/abort and never commits partial data
- ApiInterface implements Exportable via its own fetch({signal})
- computeForcedExportFilter enforces non-global-admin scope (resided-in abodes
  + co-resident users, intersected with apikey restrict_*); GET /export
  intersects it with the caller's filter and wires an AbortController to the
  response socket
- inspectExportStream reports kinds/counts from any stream without a db
- abode-export / abode-import / abode-inspect CLIs (import is sqlite-only)
- Secrets are not exported: imported users default to '#unset' passwords and
  apikeys are re-minted a token (ClientApikey view round-trips exactly)
- test/tools/export-import.test.ts: round-trip, filter narrowing, forced-scope,
  export/import cancellation, in-process Koa endpoint, inspect

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 22:29:03 +00:00

69 lines
2.2 KiB
TypeScript

import { createReadStream } from "node:fs";
import { inspectExportStream } from "../db/export/inspect.js";
import { isExportKind } from "../db/export/filter.js";
import type { ExportKind } 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-inspect --help");
log("\tabode-inspect <input-file|-> [--stop-after=note,...]");
process.exit(err ? 1 : 0);
}
if (["-h", "--help", "help"].some((x) => args.includes(x))) printUsage();
let input: string | undefined;
const flags = new Map<string, string>();
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 if (input === undefined) {
input = arg;
} else {
printUsage("too many arguments");
}
}
if (!input) printUsage("missing <input-file|->");
for (const key of flags.keys()) {
if (key !== "stop-after") printUsage(`unknown flag: --${key}`);
}
let stopAfterKinds: ExportKind[] | undefined;
const rawStop = flags.get("stop-after");
if (rawStop !== undefined) {
const list = rawStop.split(",").filter(Boolean);
const bad = list.filter((k) => !isExportKind(k));
if (bad.length) printUsage(`invalid kind(s): ${bad.join(", ")}`);
stopAfterKinds = list.filter(isExportKind);
}
const source = input === "-" ? process.stdin : createReadStream(input);
const { counts, meta } = await inspectExportStream(source, { stopAfterKinds });
if (meta) {
console.log("Meta:");
console.log(`- format version: ${meta.v ?? "?"}`);
console.log(`- exported at: ${meta.exportedAt ?? "?"}`);
console.log(`- source backend: ${meta.source ?? "?"}`);
if (meta.filter && Object.keys(meta.filter).length) {
console.log(`- effective filter: ${JSON.stringify(meta.filter)}`);
}
}
console.log("Counts:");
const entries = Object.entries(counts);
if (!entries.length) console.log("(none)");
for (const [kind, count] of entries) console.log(`- ${kind}: ${count}`);
process.exit(0);