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>
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>