feat: add export/import streaming to the pluggable backends
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
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
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>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user