From 205d8f257c338483d65f7285957ab1b43f4fd295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 25 Oct 2026 02:27:40 +0100 Subject: [PATCH] fix(backup): carry the import ledger, and the keys of deleted imports The backup carried neither the import ledger nor the tombstones of deleted documents, so after a restore onto a fresh instance a token importer stored every imported document the person had deleted on purpose again. The document filing section now carries documentImportKeys: the ledger's rows plus the own and alias keys of the documents waiting in Recently deleted, which still do not travel as documents. The restore writes them back, minus any key a restored live document holds, and counts them in the restore summary and the preview. DocumentImportKey moves from the excluded models to the two-ended ones, and the round trip, the column audit and the parity goldens carry a ledger row. --- docs/api/openapi.yaml | 29 +++ .../export/__tests__/per-type-routes.test.ts | 1 + .../__tests__/soft-delete-filter.test.ts | 19 ++ .../export/encrypted/__tests__/route.test.ts | 1 + src/lib/documents/tombstones.ts | 4 +- .../__tests__/full-backup-payload.test.ts | 2 + .../restore-backup-round-trip.test.ts | 1 + src/lib/export/backup-plan.ts | 6 +- src/lib/export/backup-preview.ts | 1 + .../export/document-filing-backup-schema.ts | 23 ++ src/lib/export/document-filing-backup.ts | 146 +++++++++++- src/lib/export/restore-context.ts | 1 + src/lib/validations/backup-summary.ts | 3 + src/lib/validations/backup.ts | 4 + .../export-disaster-recovery.json | 148 ++++++------ .../backup-parity/export-portable.json | 120 +++++----- .../restore-disaster-recovery-over-live.json | 3 + .../restore-disaster-recovery.json | 211 ++++++++++-------- .../backup-parity/restore-portable.json | 153 +++++++------ .../backup-parity/restore-skips.json | 4 + .../backup-parity/restore-version-1.json | 5 + .../backup-parity/seeded-account.json | 146 ++++++------ .../backup-document-import-ledger.test.ts | 208 +++++++++++++++++ tests/integration/backup-fixture.ts | 13 ++ tests/integration/backup-round-trip.test.ts | 2 + 25 files changed, 906 insertions(+), 348 deletions(-) create mode 100644 tests/integration/backup-document-import-ledger.test.ts diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index ec4f71ddb..54946f446 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -44015,6 +44015,34 @@ components: - createdAt - updatedAt additionalProperties: {} + documentImportKeys: + default: [] + type: array + items: + type: object + properties: + sourceSystem: + type: string + enum: + - PAPERLESS + - PAPRA + - OTHER + sourceId: + type: string + minLength: 1 + maxLength: 128 + sourceInstance: + anyOf: + - type: string + maxLength: 512 + - type: "null" + createdAt: + type: string + minLength: 1 + required: + - sourceSystem + - sourceId + additionalProperties: {} nutrientDays: default: [] type: array @@ -46536,6 +46564,7 @@ components: - documents - documentConditionLinks - extractedFacts + - documentImportKeys - nutrientDays - healthProfile - healthProfileFacts diff --git a/src/app/api/export/__tests__/per-type-routes.test.ts b/src/app/api/export/__tests__/per-type-routes.test.ts index 6aa7168b8..f426aaa2e 100644 --- a/src/app/api/export/__tests__/per-type-routes.test.ts +++ b/src/app/api/export/__tests__/per-type-routes.test.ts @@ -75,6 +75,7 @@ vi.mock("@/lib/db", () => ({ // Empty for the same reason as the sections above. documentConditionLink: { findMany: vi.fn().mockResolvedValue([]) }, extractedFact: { findMany: vi.fn().mockResolvedValue([]) }, + documentImportKey: { findMany: vi.fn(async () => []) }, ecgRecording: { findMany: vi.fn().mockResolvedValue([]) }, symptomDefinition: { findMany: vi.fn().mockResolvedValue([]) }, lifeEvent: { findMany: vi.fn().mockResolvedValue([]) }, diff --git a/src/app/api/export/__tests__/soft-delete-filter.test.ts b/src/app/api/export/__tests__/soft-delete-filter.test.ts index 4a245fa0b..ef58cd010 100644 --- a/src/app/api/export/__tests__/soft-delete-filter.test.ts +++ b/src/app/api/export/__tests__/soft-delete-filter.test.ts @@ -76,6 +76,7 @@ vi.mock("@/lib/db", () => ({ // What a document was filed against, and what was read out of it. documentConditionLink: { findMany: vi.fn() }, extractedFact: { findMany: vi.fn() }, + documentImportKey: { findMany: vi.fn(async () => []) }, ecgRecording: { findMany: vi.fn() }, symptomDefinition: { findMany: vi.fn() }, lifeEvent: { findMany: vi.fn() }, @@ -305,6 +306,24 @@ describe("v1.4.41 W-DELETED-2 — soft-delete invisibility", () => { const documentCall = vi.mocked(prisma.inboundDocument.findMany).mock .calls[0][0] as { select?: Record }; expect(documentCall.select).not.toHaveProperty("contentEncrypted"); + // The import ledger reads the source keys of documents waiting in + // Recently deleted, and nothing else of them: no tombstone travels. + for (const [args] of vi.mocked(prisma.inboundDocument.findMany).mock + .calls) { + const call = args as { + where?: { deletedAt?: unknown }; + select?: Record; + }; + if (call.where?.deletedAt === null) continue; + expect(Object.keys(call.select ?? {}).sort()).toEqual([ + "deletedAt", + "sourceAliases", + "sourceId", + "sourceInstance", + "sourceSystem", + "userId", + ]); + } }); it("/api/export (legacy) scopes the measurement read to deletedAt: null", async () => { diff --git a/src/app/api/export/encrypted/__tests__/route.test.ts b/src/app/api/export/encrypted/__tests__/route.test.ts index 367e531b8..71db45a35 100644 --- a/src/app/api/export/encrypted/__tests__/route.test.ts +++ b/src/app/api/export/encrypted/__tests__/route.test.ts @@ -67,6 +67,7 @@ vi.mock("@/lib/db", () => ({ // Empty for the same reason as the sections above. documentConditionLink: { findMany: vi.fn().mockResolvedValue([]) }, extractedFact: { findMany: vi.fn().mockResolvedValue([]) }, + documentImportKey: { findMany: vi.fn(async () => []) }, ecgRecording: { findMany: vi.fn().mockResolvedValue([]) }, symptomDefinition: { findMany: vi.fn().mockResolvedValue([]) }, lifeEvent: { findMany: vi.fn().mockResolvedValue([]) }, diff --git a/src/lib/documents/tombstones.ts b/src/lib/documents/tombstones.ts index 4a26f6f11..7ca5e337d 100644 --- a/src/lib/documents/tombstones.ts +++ b/src/lib/documents/tombstones.ts @@ -39,7 +39,7 @@ interface ImportSourceKey { } /** The columns a row needs for {@link importKeysOf}. */ -const SOURCE_KEY_SELECT = { +export const SOURCE_KEY_SELECT = { userId: true, sourceSystem: true, sourceId: true, @@ -62,7 +62,7 @@ interface KeyedRow { } /** Every import source key a document holds: its own, then its aliases. */ -function importKeysOf(row: KeyedRow): ImportSourceKey[] { +export function importKeysOf(row: KeyedRow): ImportSourceKey[] { return [ ...(row.sourceSystem && row.sourceId ? [ diff --git a/src/lib/export/__tests__/full-backup-payload.test.ts b/src/lib/export/__tests__/full-backup-payload.test.ts index fe9b5f872..96abb9903 100644 --- a/src/lib/export/__tests__/full-backup-payload.test.ts +++ b/src/lib/export/__tests__/full-backup-payload.test.ts @@ -463,6 +463,8 @@ function makePrisma() { // Empty for the same reason as the sections above. documentConditionLink: { findMany: vi.fn().mockResolvedValue([]) }, extractedFact: { findMany: vi.fn().mockResolvedValue([]) }, + documentImportKey: { findMany: vi.fn().mockResolvedValue([]) }, + inboundDocument: { findMany: vi.fn().mockResolvedValue([]) }, // The ECG strips. Empty for the same reason as the sections above: // an account whose watch has never taken one is the ordinary case the // builder must still answer for. diff --git a/src/lib/export/__tests__/restore-backup-round-trip.test.ts b/src/lib/export/__tests__/restore-backup-round-trip.test.ts index 45266aa47..6f06e01ea 100644 --- a/src/lib/export/__tests__/restore-backup-round-trip.test.ts +++ b/src/lib/export/__tests__/restore-backup-round-trip.test.ts @@ -322,6 +322,7 @@ function sourceClient() { // Empty for the same reason as the sections above. documentConditionLink: { findMany: vi.fn().mockResolvedValue([]) }, extractedFact: { findMany: vi.fn().mockResolvedValue([]) }, + documentImportKey: { findMany: vi.fn().mockResolvedValue([]) }, ecgRecording: { findMany: vi.fn().mockResolvedValue([]) }, symptomDefinition: { findMany: vi.fn().mockResolvedValue([]) }, lifeEvent: { findMany: vi.fn().mockResolvedValue([]) }, diff --git a/src/lib/export/backup-plan.ts b/src/lib/export/backup-plan.ts index f0786f13f..3392a7f3b 100644 --- a/src/lib/export/backup-plan.ts +++ b/src/lib/export/backup-plan.ts @@ -159,6 +159,7 @@ export const BACKED_UP_MODELS = [ "InboundDocument", "DocumentConditionLink", "ExtractedFact", + "DocumentImportKey", // Deleted imports' keys: `document-filing-backup.ts`. // ── Visits ──────────────────────────────────────────────────────────────── "Practitioner", @@ -502,6 +503,7 @@ export const TWO_ENDED_MODELS = [ // nulled together with its type when it resolves to nothing. "DocumentConditionLink", "ExtractedFact", + "DocumentImportKey", // What the account earned. The register called the bests "recomputable in // principle", which is true and does not help: nothing recomputes them, and // a recomputation over a restored history would not find the same rows: @@ -768,12 +770,10 @@ export const NOT_IN_BACKUP_MODELS: Readonly> = { "That this host's one-time fold repair has recomputed the account's hourly and daily means, which lets the compaction-tombstone purge delete the tombstones the repair reads. It describes this database, not the file: a restore of a file exported before the repair ran writes back the means and tombstones of that moment, so the restore removes the row and the repair runs again before anything is purged.", BackupPassAttempt: "When this host's backup passes last started and finished this account, kept so a pass the process died under is taken last next time and named on the backups page. It describes this deployment's own runs, so restoring it elsewhere would report an interrupted run that host never had, or hide one it did.", - DocumentImportKey: - "The source keys of imported documents the person deleted and the purge then removed, kept so a re-run of the importer does not store them again. The backup carries only live documents, so the tombstones that hold the same memory before the purge do not travel either; carrying the ledger alone would restore half of one decision. After a restore, a deliberate re-import can bring such a document back, and the person deletes it again.", DocumentAiRun: "A ticket for one background read (a document read with AI, a lab scan), deleted an hour after it finishes. The document and the committed rows travel; the ticket does not, and a restored run would point at a worker job that does not exist on the receiving host.", DocumentSourceAlias: - "Further import source keys that were answered with an already stored document because the bytes matched. The next import run writes the same alias again the moment it meets the same bytes, so a restore loses nothing a re-run does not rebuild; and for a deleted document it restores no less than the tombstones and ledger beside it do, which do not travel either.", + "Further import source keys that were answered with an already stored document because the bytes matched. The next import run writes the same alias again the moment it meets the same bytes, so a restore loses nothing a re-run does not rebuild. A deleted document's aliases travel as import-ledger entries (`DocumentImportKey`), which is what they mean once the document is gone.", ImportJob: "A job record pointing at an uploaded file that the backup does not carry, so restoring it would resurrect a task with nothing to work on.", HealthKitTypeSync: diff --git a/src/lib/export/backup-preview.ts b/src/lib/export/backup-preview.ts index ecc9a9848..84ce9d3ee 100644 --- a/src/lib/export/backup-preview.ts +++ b/src/lib/export/backup-preview.ts @@ -167,6 +167,7 @@ const SUMMARY_COUNTS = [ "documents", "documentConditionLinks", "extractedFacts", + "documentImportKeys", "healthProfile", "healthProfileFactRevisions", "customMetrics", diff --git a/src/lib/export/document-filing-backup-schema.ts b/src/lib/export/document-filing-backup-schema.ts index fc4f87f7b..5b8d042b5 100644 --- a/src/lib/export/document-filing-backup-schema.ts +++ b/src/lib/export/document-filing-backup-schema.ts @@ -12,6 +12,11 @@ import { ExtractedFactType, } from "@/generated/prisma/enums"; import { base64BytesSchema, isoDateTime } from "@/lib/export/backup-schema-kit"; +import { + DOCUMENT_SOURCE_ID_MAX, + DOCUMENT_SOURCE_INSTANCE_MAX, + DOCUMENT_SOURCE_SYSTEMS, +} from "@/lib/validations/inbound-documents"; /** One document filed against one condition episode. */ export const documentConditionLinkBackupSchema = z @@ -54,3 +59,21 @@ export const extractedFactBackupSchema = z updatedAt: isoDateTime, }) .passthrough(); + +/** + * One import source key the person deleted the document of (the import + * ledger). Closed and bounded like the upload's own fields, so a restore + * cannot write a key the upload could never have stored. + */ +export const documentImportKeyBackupSchema = z + .object({ + sourceSystem: z.enum(DOCUMENT_SOURCE_SYSTEMS), + sourceId: z.string().min(1).max(DOCUMENT_SOURCE_ID_MAX), + sourceInstance: z + .string() + .max(DOCUMENT_SOURCE_INSTANCE_MAX) + .nullable() + .optional(), + createdAt: isoDateTime.optional(), + }) + .passthrough(); diff --git a/src/lib/export/document-filing-backup.ts b/src/lib/export/document-filing-backup.ts index 0d1cb9a62..8119eaf10 100644 --- a/src/lib/export/document-filing-backup.ts +++ b/src/lib/export/document-filing-backup.ts @@ -14,6 +14,12 @@ * entries read "a restored vault is unsorted" and "re-derivable only by * re-running the extraction against a provider, at the operator's cost". * + * A third rides beside them: the import ledger (`DocumentImportKey`), the + * source keys of imported documents the person deleted, with the keys of the + * documents still waiting in Recently deleted folded in (those documents do + * not travel). Without it a restore onto a fresh instance let a token importer + * store every deliberately deleted document again. + * * ## The state on a fact is worth more than the fact * * `status`, `needsReview` and the pair `committedRecordId` / @@ -71,6 +77,7 @@ import type { Prisma, PrismaClient } from "@/generated/prisma/client"; import { decryptFromBytes, encryptToBytes } from "@/lib/ai/coach/bytes-codec"; +import { importKeysOf, SOURCE_KEY_SELECT } from "@/lib/documents/tombstones"; import { recordUnknownKeys, type RestoreSkipLog, @@ -112,14 +119,28 @@ export interface ExtractedFactBackupEntry { updatedAt: string; } +/** + * One import source key the person deleted the document of. The ledger's own + * rows (documents the purge removed) and the keys of the documents still + * waiting in Recently deleted, which the file does not carry as documents. + */ +export interface DocumentImportKeyBackupEntry { + sourceSystem: string; + sourceId: string; + sourceInstance: string | null; + createdAt: string; +} + export interface DocumentFilingBackupSection { documentConditionLinks: DocumentConditionLinkBackupEntry[]; extractedFacts: ExtractedFactBackupEntry[]; + documentImportKeys: DocumentImportKeyBackupEntry[]; } export interface DocumentFilingBackupCounts { documentConditionLinks: number; extractedFacts: number; + documentImportKeys: number; } /** @@ -165,8 +186,72 @@ function decryptJsonSoft(bytes: Uint8Array): unknown { } } +/** + * What the person deleted on purpose among their imported documents, as the + * import ledger would hold it once every deletion is purged: the ledger's own + * rows, then the keys (own and alias) of every document still waiting in + * Recently deleted. Those documents do not travel, as no tombstone ever has; + * without their keys a restore onto a fresh instance would let an importer + * store each one again. A key that appears twice is carried once. + */ +async function buildDeletedImportKeys( + prisma: Pick, + userId: string, +): Promise { + const [ledger, tombstones] = await Promise.all([ + prisma.documentImportKey.findMany({ + where: { userId }, + orderBy: [{ createdAt: "asc" }, { id: "asc" }], + select: { + sourceSystem: true, + sourceId: true, + sourceInstance: true, + createdAt: true, + }, + }), + prisma.inboundDocument.findMany({ + where: { + userId, + deletedAt: { not: null }, + OR: [ + { sourceSystem: { not: null }, sourceId: { not: null } }, + { sourceAliases: { some: {} } }, + ], + }, + orderBy: [{ deletedAt: "asc" }, { id: "asc" }], + select: { ...SOURCE_KEY_SELECT, deletedAt: true }, + }), + ]); + const out: DocumentImportKeyBackupEntry[] = []; + const seen = new Set(); + const add = ( + key: Omit, + at: Date, + ) => { + const id = JSON.stringify([ + key.sourceSystem, + key.sourceInstance, + key.sourceId, + ]); + if (seen.has(id)) return; + seen.add(id); + out.push({ ...key, createdAt: at.toISOString() }); + }; + for (const row of ledger) add(row, row.createdAt); + for (const row of tombstones) { + for (const key of importKeysOf(row)) add(key, row.deletedAt!); + } + return out; +} + export async function buildDocumentFilingBackupSection( - prisma: Pick, + prisma: Pick< + PrismaClient, + | "documentConditionLink" + | "extractedFact" + | "documentImportKey" + | "inboundDocument" + >, userId: string, options: DocumentFilingBackupOptions = {}, ): Promise { @@ -177,7 +262,7 @@ export async function buildDocumentFilingBackupSection( // skips tombstoned documents in BOTH purposes, and the episode reader skips // tombstoned episodes in a portable export only — so the filters here mirror // exactly what `records-backup.ts` decided to carry. - const [linkRows, factRows] = await Promise.all([ + const [linkRows, factRows, documentImportKeys] = await Promise.all([ prisma.documentConditionLink.findMany({ where: { userId, @@ -192,6 +277,7 @@ export async function buildDocumentFilingBackupSection( orderBy: [{ createdAt: "asc" }, { id: "asc" }], select: EXTRACTED_FACT_BACKUP_SELECT, }), + buildDeletedImportKeys(prisma, userId), ]); return { @@ -223,6 +309,7 @@ export async function buildDocumentFilingBackupSection( createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), })), + documentImportKeys, }; } @@ -233,6 +320,7 @@ export function countDocumentFilingBackupSection( return { documentConditionLinks: section.documentConditionLinks.length, extractedFacts: section.extractedFacts.length, + documentImportKeys: section.documentImportKeys.length, }; } @@ -240,6 +328,7 @@ export function countDocumentFilingBackupSection( export interface DocumentFilingRestoreCleared { documentConditionLinks: number; extractedFacts: number; + documentImportKeys: number; } type OptionalNullable = { [K in keyof T]?: T[K] | undefined }; @@ -279,6 +368,18 @@ export type RestoredExtractedFact = Pick< export interface DocumentFilingRestoreInput { documentConditionLinks: DocumentConditionLinkBackupEntry[]; extractedFacts: RestoredExtractedFact[]; + documentImportKeys: Array< + Pick & { + sourceInstance?: string | null; + createdAt?: string; + } + >; + /** The restored documents' own keys: a live document's key is not deleted. */ + documents: Array<{ + sourceSystem?: string | null; + sourceId?: string | null; + sourceInstance?: string | null; + }>; } /** @@ -319,10 +420,12 @@ export async function restoreDocumentFilingData( refs: DocumentFilingRestoreRefs, skips: RestoreSkipLog, ): Promise { - const [clearedLinks, clearedFacts] = await Promise.all([ + const [clearedLinks, clearedFacts, clearedKeys] = await Promise.all([ tx.documentConditionLink.deleteMany({ where: { userId: ownerId } }), tx.extractedFact.deleteMany({ where: { userId: ownerId } }), + tx.documentImportKey.deleteMany({ where: { userId: ownerId } }), ]); + await restoreDeletedImportKeys(tx, ownerId, payload); const droppedFilings: string[] = []; const writableLinks = payload.documentConditionLinks.filter((link) => { @@ -416,9 +519,46 @@ export async function restoreDocumentFilingData( return { documentConditionLinks: clearedLinks.count, extractedFacts: clearedFacts.count, + documentImportKeys: clearedKeys.count, }; } +/** + * The import ledger back, as the file carries it, minus any key a restored + * live document holds: the ledger says "the person deleted this", and a + * document in the vault under that key says otherwise. A file written before + * the ledger travelled carries none, and the restore then remembers no + * deletion, as before. + */ +async function restoreDeletedImportKeys( + tx: Prisma.TransactionClient, + ownerId: string, + payload: DocumentFilingRestoreInput, +): Promise { + const keyOf = (k: { + sourceSystem?: string | null; + sourceId?: string | null; + sourceInstance?: string | null; + }) => JSON.stringify([k.sourceSystem, k.sourceInstance ?? null, k.sourceId]); + const live = new Set( + payload.documents + .filter((d) => d.sourceSystem && d.sourceId) + .map((d) => keyOf(d)), + ); + const keys = payload.documentImportKeys.filter((k) => !live.has(keyOf(k))); + if (keys.length === 0) return; + await tx.documentImportKey.createMany({ + data: keys.map((k) => ({ + userId: ownerId, + sourceSystem: k.sourceSystem, + sourceId: k.sourceId, + sourceInstance: k.sourceInstance ?? null, + ...(k.createdAt ? { createdAt: new Date(k.createdAt) } : {}), + })), + skipDuplicates: true, + }); +} + /** * A fact column's stored bytes, whichever end of the contract the file came * from. diff --git a/src/lib/export/restore-context.ts b/src/lib/export/restore-context.ts index 80734d5a9..bb7780cc3 100644 --- a/src/lib/export/restore-context.ts +++ b/src/lib/export/restore-context.ts @@ -72,6 +72,7 @@ export const RESTORE_CLEARED_KEYS = [ "documents", "documentConditionLinks", "extractedFacts", + "documentImportKeys", "healthProfile", "healthProfileFactRevisions", "customMetrics", diff --git a/src/lib/validations/backup-summary.ts b/src/lib/validations/backup-summary.ts index 3b1a19a19..20494c11f 100644 --- a/src/lib/validations/backup-summary.ts +++ b/src/lib/validations/backup-summary.ts @@ -59,6 +59,8 @@ export interface BackupSummary { documentConditionLinks: number; /** Facts staged out of a document, with their review decision. */ extractedFacts: number; + /** Import source keys of documents the person deleted. */ + documentImportKeys: number; /** 1 when the account's durable self-context rides the file, 0 otherwise. */ healthProfile: number; /** Effective-dated structured health-profile revisions. */ @@ -159,6 +161,7 @@ export function summarizeBackup(payload: BackupPayload): BackupSummary { // did for visits. documentConditionLinks: payload.documentConditionLinks.length, extractedFacts: payload.extractedFacts.length, + documentImportKeys: payload.documentImportKeys.length, healthProfile: payload.healthProfile ? 1 : 0, healthProfileFactRevisions: payload.healthProfileFacts.length, customMetrics: payload.customMetrics.length, diff --git a/src/lib/validations/backup.ts b/src/lib/validations/backup.ts index 4cb5ca368..a1b04288d 100644 --- a/src/lib/validations/backup.ts +++ b/src/lib/validations/backup.ts @@ -51,6 +51,7 @@ import { import { documentConditionLinkBackupSchema, extractedFactBackupSchema, + documentImportKeyBackupSchema, } from "@/lib/export/document-filing-backup-schema"; import { documentBackupSchema } from "@/lib/export/documents-backup-schema"; import { ecgRecordingBackupSchema } from "@/lib/export/ecg-backup-schema"; @@ -182,6 +183,9 @@ export const backupPayloadSchema = z .array(documentConditionLinkBackupSchema) .default([]), extractedFacts: z.array(extractedFactBackupSchema).default([]), + // The source keys of imported documents the person deleted. Defaulted: + // a file written before the import ledger travelled carries no key. + documentImportKeys: z.array(documentImportKeyBackupSchema).default([]), nutrientDays: z.array(nutrientDaySchema).default([]), // Durable self-context and user-defined series. Defaulted so files written // before either rode the wire still parse; an account with neither writes diff --git a/tests/integration/__golden__/backup-parity/export-disaster-recovery.json b/tests/integration/__golden__/backup-parity/export-disaster-recovery.json index c741d2b27..a2a4bf6b5 100644 --- a/tests/integration/__golden__/backup-parity/export-disaster-recovery.json +++ b/tests/integration/__golden__/backup-parity/export-disaster-recovery.json @@ -539,7 +539,7 @@ ], "source": "ARCHIVED", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:00.000Z" + "createdAt": "2026-05-06T06:27:00.000Z" }, { "id": "", @@ -554,7 +554,7 @@ ], "source": "ARCHIVED", "supersededByIndex": 2, - "createdAt": "2026-05-06T04:47:02.000Z" + "createdAt": "2026-05-06T06:27:02.000Z" }, { "id": "", @@ -569,14 +569,14 @@ ], "source": "MANUAL", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:01.000Z" + "createdAt": "2026-05-06T06:27:01.000Z" } ], "efficacyTargets": [ { "id": "", - "createdAt": "2026-05-06T06:25:00.000Z", - "updatedAt": "2026-05-06T06:26:00.000Z", + "createdAt": "2026-05-06T08:05:00.000Z", + "updatedAt": "2026-05-06T08:06:00.000Z", "measurementType": "WEIGHT", "biomarkerName": "Ferritin", "primary": false @@ -972,7 +972,7 @@ "name": "Ferr.", "source": "IMPORT_CHOICE", "id": "", - "createdAt": "2026-05-06T11:26:00.000Z" + "createdAt": "2026-05-06T13:06:00.000Z" } ] } @@ -985,7 +985,7 @@ "sourceBiomarkerId": "merged-away-marker", "sourceName": "Ferritin (Serum)", "readingCount": 3, - "mergedAt": "2026-05-06T13:06:00.000Z" + "mergedAt": "2026-05-06T14:46:00.000Z" } ], "illnessEpisodes": [ @@ -1194,8 +1194,8 @@ "sourceSystem": "PAPERLESS", "sourceId": "4711", "sourceInstance": "https://paperless.example.test", - "createdAt": "2026-05-04T01:24:00.000Z", - "updatedAt": "2026-05-04T01:25:00.000Z", + "createdAt": "2026-05-04T01:25:00.000Z", + "updatedAt": "2026-05-04T01:26:00.000Z", "contentEncrypted": "", "contentSha256": "0a738470f0945033965858e9895ee721ce981a25f013514d587d0a7a1946be7a", "contentCodec": "binary2", @@ -1206,7 +1206,8 @@ "summaryState": "PENDING", "lastIndexAttemptAt": "2026-06-30T12:34:56.789Z", "lastIndexOutcome": "rt-InboundDocument-lastIndexOutcome", - "aiReadDeferred": true + "aiReadDeferred": true, + "extractionIncomplete": true } ], "manifest": { @@ -1456,8 +1457,8 @@ ], "onboardingRecord": { "id": "", - "createdAt": "2026-05-06T09:47:00.000Z", - "updatedAt": "2026-05-06T09:48:00.000Z", + "createdAt": "2026-05-06T11:27:00.000Z", + "updatedAt": "2026-05-06T11:28:00.000Z", "needs": { "recordTarget": "me", "areas": [ @@ -1990,6 +1991,14 @@ "updatedAt": "2026-05-05T18:52:00.000Z" } ], + "documentImportKeys": [ + { + "sourceSystem": "PAPRA", + "sourceId": "doc_deleted", + "sourceInstance": "https://papra.example", + "createdAt": "2026-07-03T09:00:00.000Z" + } + ], "personalRecords": [ { "metricType": "WALKING_RUNNING_DISTANCE", @@ -2001,7 +2010,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-5km", - "createdAt": "2026-05-05T20:31:01.000Z" + "createdAt": "2026-05-05T22:11:01.000Z" }, { "metricType": "WALKING_RUNNING_DISTANCE", @@ -2013,7 +2022,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-10km", - "createdAt": "2026-05-05T20:31:02.000Z" + "createdAt": "2026-05-05T22:11:02.000Z" }, { "metricType": "WEIGHT", @@ -2025,19 +2034,19 @@ "sourceMeasurementId": "", "source": "MANUAL", "externalId": null, - "createdAt": "2026-05-05T20:31:00.000Z" + "createdAt": "2026-05-05T22:11:00.000Z" } ], "userAchievements": [ { "achievementId": "retired-badge-from-an-older-release", "unlockedAt": "2025-11-05T08:00:00.000Z", - "createdAt": "2026-05-05T22:04:01.000Z" + "createdAt": "2026-05-05T23:44:01.000Z" }, { "achievementId": "intake-total-10", "unlockedAt": "2026-02-21T10:15:00.000Z", - "createdAt": "2026-05-05T22:04:00.000Z" + "createdAt": "2026-05-05T23:44:00.000Z" } ], "environmentContexts": [ @@ -2083,8 +2092,8 @@ "aqHours": 3, "aqFetchedAt": "2026-06-30T12:34:56.789Z", "fetchedAt": "2026-06-16T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:00.000Z", - "updatedAt": "2026-05-06T00:21:00.000Z" + "createdAt": "2026-05-06T02:00:00.000Z", + "updatedAt": "2026-05-06T02:01:00.000Z" }, { "date": "2026-07-01", @@ -2128,8 +2137,8 @@ "aqHours": null, "aqFetchedAt": null, "fetchedAt": "2026-07-02T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:01.000Z", - "updatedAt": "2026-05-06T00:21:01.000Z" + "createdAt": "2026-05-06T02:00:01.000Z", + "updatedAt": "2026-05-06T02:01:01.000Z" } ], "environmentTravelLocations": [ @@ -2140,8 +2149,8 @@ "lon": null, "label": null, "locationEncrypted": "", - "createdAt": "2026-05-06T01:25:00.000Z", - "updatedAt": "2026-05-06T01:26:00.000Z" + "createdAt": "2026-05-06T03:05:00.000Z", + "updatedAt": "2026-05-06T03:06:00.000Z" } ], "dawarichDays": [ @@ -2156,8 +2165,8 @@ "countryCodeEncrypted": "", "isTravelDay": true, "fetchedAt": "2026-06-16T03:20:00.000Z", - "createdAt": "2026-05-06T03:11:00.000Z", - "updatedAt": "2026-05-06T03:12:00.000Z" + "createdAt": "2026-05-06T04:51:00.000Z", + "updatedAt": "2026-05-06T04:52:00.000Z" } ], "ecgRecordings": [ @@ -2174,8 +2183,8 @@ "averageHeartRate": 88, "rhythmClassification": "IRREGULAR", "measurementId": "", - "createdAt": "2026-05-06T08:13:00.000Z", - "updatedAt": "2026-05-06T08:14:00.000Z" + "createdAt": "2026-05-06T09:53:00.000Z", + "updatedAt": "2026-05-06T09:54:00.000Z" } ], "symptomDefinitions": [ @@ -2271,35 +2280,35 @@ { "labResultId": "", "documentId": "", - "createdAt": "2026-05-06T14:44:00.000Z" + "createdAt": "2026-05-06T16:24:00.000Z" } ], "encounterMedicationLinks": [ { "encounterId": "", "medicationId": "", - "createdAt": "2026-05-06T16:24:00.000Z" + "createdAt": "2026-05-06T18:04:00.000Z" } ], "encounterVaccinationLinks": [ { "encounterId": "", "vaccinationId": "", - "createdAt": "2026-05-06T18:04:00.000Z" + "createdAt": "2026-05-06T19:44:00.000Z" } ], "medicationDocumentLinks": [ { "medicationId": "", "documentId": "", - "createdAt": "2026-05-06T19:44:00.000Z" + "createdAt": "2026-05-06T21:24:00.000Z" } ], "medicationConditionLinks": [ { "medicationId": "", "episodeId": "", - "createdAt": "2026-05-06T21:24:00.000Z" + "createdAt": "2026-05-06T23:04:00.000Z" } ], "nutrientDays": [ @@ -2367,6 +2376,7 @@ "consentReceipts": 2, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "personalRecords": 3, "userAchievements": 2, "environmentContexts": 2, @@ -2430,6 +2440,7 @@ "consentReceipts": 2, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "personalRecords": 3, "userAchievements": 2, "environmentContexts": 2, @@ -2942,8 +2953,8 @@ "measurementType": "WEIGHT", "biomarkerName": "Ferritin", "primary": false, - "createdAt": "2026-05-06T06:25:00.000Z", - "updatedAt": "2026-05-06T06:26:00.000Z" + "createdAt": "2026-05-06T08:05:00.000Z", + "updatedAt": "2026-05-06T08:06:00.000Z" } ], "scheduleRevisions": [ @@ -2960,7 +2971,7 @@ ], "source": "ARCHIVED", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:00.000Z" + "createdAt": "2026-05-06T06:27:00.000Z" }, { "id": "", @@ -2975,7 +2986,7 @@ ], "source": "ARCHIVED", "supersededByIndex": 2, - "createdAt": "2026-05-06T04:47:02.000Z" + "createdAt": "2026-05-06T06:27:02.000Z" }, { "id": "", @@ -2990,7 +3001,7 @@ ], "source": "MANUAL", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:01.000Z" + "createdAt": "2026-05-06T06:27:01.000Z" } ], "courses": [ @@ -3412,7 +3423,7 @@ "id": "", "name": "Ferr.", "source": "IMPORT_CHOICE", - "createdAt": "2026-05-06T11:26:00.000Z" + "createdAt": "2026-05-06T13:06:00.000Z" } ], "createdAt": "2026-05-02T12:30:00.000Z", @@ -3427,7 +3438,7 @@ "sourceBiomarkerId": "merged-away-marker", "sourceName": "Ferritin (Serum)", "readingCount": 3, - "mergedAt": "2026-05-06T13:06:00.000Z" + "mergedAt": "2026-05-06T14:46:00.000Z" } ], "illnessEpisodes": [ @@ -3736,8 +3747,9 @@ "sourceId": "4711", "sourceInstance": "https://paperless.example.test", "aiReadDeferred": true, - "createdAt": "2026-05-04T01:24:00.000Z", - "updatedAt": "2026-05-04T01:25:00.000Z" + "extractionIncomplete": true, + "createdAt": "2026-05-04T01:25:00.000Z", + "updatedAt": "2026-05-04T01:26:00.000Z" } ], "documentConditionLinks": [ @@ -3763,6 +3775,14 @@ "updatedAt": "2026-05-05T18:52:00.000Z" } ], + "documentImportKeys": [ + { + "sourceSystem": "PAPRA", + "sourceId": "doc_deleted", + "sourceInstance": "https://papra.example", + "createdAt": "2026-07-03T09:00:00.000Z" + } + ], "nutrientDays": [ { "day": "2026-07-01", @@ -4064,8 +4084,8 @@ }, "modulesDerivedAt": "2026-07-01T08:00:00.000Z", "completedAt": "2026-07-01T08:00:00.000Z", - "createdAt": "2026-05-06T09:47:00.000Z", - "updatedAt": "2026-05-06T09:48:00.000Z" + "createdAt": "2026-05-06T11:27:00.000Z", + "updatedAt": "2026-05-06T11:28:00.000Z" }, "practitioners": [ { @@ -4226,35 +4246,35 @@ { "labResultId": "", "documentId": "", - "createdAt": "2026-05-06T14:44:00.000Z" + "createdAt": "2026-05-06T16:24:00.000Z" } ], "encounterMedicationLinks": [ { "encounterId": "", "medicationId": "", - "createdAt": "2026-05-06T16:24:00.000Z" + "createdAt": "2026-05-06T18:04:00.000Z" } ], "encounterVaccinationLinks": [ { "encounterId": "", "vaccinationId": "", - "createdAt": "2026-05-06T18:04:00.000Z" + "createdAt": "2026-05-06T19:44:00.000Z" } ], "medicationDocumentLinks": [ { "medicationId": "", "documentId": "", - "createdAt": "2026-05-06T19:44:00.000Z" + "createdAt": "2026-05-06T21:24:00.000Z" } ], "medicationConditionLinks": [ { "medicationId": "", "episodeId": "", - "createdAt": "2026-05-06T21:24:00.000Z" + "createdAt": "2026-05-06T23:04:00.000Z" } ], "coachConversations": [ @@ -4560,7 +4580,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-5km", - "createdAt": "2026-05-05T20:31:01.000Z" + "createdAt": "2026-05-05T22:11:01.000Z" }, { "metricType": "WALKING_RUNNING_DISTANCE", @@ -4572,7 +4592,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-10km", - "createdAt": "2026-05-05T20:31:02.000Z" + "createdAt": "2026-05-05T22:11:02.000Z" }, { "metricType": "WEIGHT", @@ -4584,19 +4604,19 @@ "sourceMeasurementId": "", "source": "MANUAL", "externalId": null, - "createdAt": "2026-05-05T20:31:00.000Z" + "createdAt": "2026-05-05T22:11:00.000Z" } ], "userAchievements": [ { "achievementId": "retired-badge-from-an-older-release", "unlockedAt": "2025-11-05T08:00:00.000Z", - "createdAt": "2026-05-05T22:04:01.000Z" + "createdAt": "2026-05-05T23:44:01.000Z" }, { "achievementId": "intake-total-10", "unlockedAt": "2026-02-21T10:15:00.000Z", - "createdAt": "2026-05-05T22:04:00.000Z" + "createdAt": "2026-05-05T23:44:00.000Z" } ], "environmentContexts": [ @@ -4642,8 +4662,8 @@ "aqHours": 3, "aqFetchedAt": "2026-06-30T12:34:56.789Z", "fetchedAt": "2026-06-16T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:00.000Z", - "updatedAt": "2026-05-06T00:21:00.000Z" + "createdAt": "2026-05-06T02:00:00.000Z", + "updatedAt": "2026-05-06T02:01:00.000Z" }, { "date": "2026-07-01", @@ -4687,8 +4707,8 @@ "aqHours": null, "aqFetchedAt": null, "fetchedAt": "2026-07-02T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:01.000Z", - "updatedAt": "2026-05-06T00:21:01.000Z" + "createdAt": "2026-05-06T02:00:01.000Z", + "updatedAt": "2026-05-06T02:01:01.000Z" } ], "environmentTravelLocations": [ @@ -4699,8 +4719,8 @@ "lon": null, "label": null, "locationEncrypted": "", - "createdAt": "2026-05-06T01:25:00.000Z", - "updatedAt": "2026-05-06T01:26:00.000Z" + "createdAt": "2026-05-06T03:05:00.000Z", + "updatedAt": "2026-05-06T03:06:00.000Z" } ], "dawarichDays": [ @@ -4715,8 +4735,8 @@ "countryCodeEncrypted": "", "isTravelDay": true, "fetchedAt": "2026-06-16T03:20:00.000Z", - "createdAt": "2026-05-06T03:11:00.000Z", - "updatedAt": "2026-05-06T03:12:00.000Z" + "createdAt": "2026-05-06T04:51:00.000Z", + "updatedAt": "2026-05-06T04:52:00.000Z" } ], "ecgRecordings": [ @@ -4733,8 +4753,8 @@ "averageHeartRate": 88, "rhythmClassification": "IRREGULAR", "measurementId": "", - "createdAt": "2026-05-06T08:13:00.000Z", - "updatedAt": "2026-05-06T08:14:00.000Z" + "createdAt": "2026-05-06T09:53:00.000Z", + "updatedAt": "2026-05-06T09:54:00.000Z" } ], "manifest": { @@ -4781,6 +4801,7 @@ "documents": 1, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "healthProfile": 1, "healthProfileFactRevisions": 2, "customMetrics": 2, @@ -4837,6 +4858,7 @@ "documents": 1, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "healthProfile": 1, "healthProfileFactRevisions": 2, "customMetrics": 2, diff --git a/tests/integration/__golden__/backup-parity/export-portable.json b/tests/integration/__golden__/backup-parity/export-portable.json index 436afeeb9..473808185 100644 --- a/tests/integration/__golden__/backup-parity/export-portable.json +++ b/tests/integration/__golden__/backup-parity/export-portable.json @@ -345,7 +345,7 @@ ], "source": "ARCHIVED", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:00.000Z" + "createdAt": "2026-05-06T06:27:00.000Z" }, { "validFrom": "2026-02-01T00:00:00.000Z", @@ -359,7 +359,7 @@ ], "source": "ARCHIVED", "supersededByIndex": 2, - "createdAt": "2026-05-06T04:47:02.000Z" + "createdAt": "2026-05-06T06:27:02.000Z" }, { "validFrom": "2026-03-01T00:00:00.000Z", @@ -373,7 +373,7 @@ ], "source": "MANUAL", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:01.000Z" + "createdAt": "2026-05-06T06:27:01.000Z" } ], "efficacyTargets": [ @@ -576,7 +576,7 @@ "sourceBiomarkerId": "merged-away-marker", "sourceName": "Ferritin (Serum)", "readingCount": 3, - "mergedAt": "2026-05-06T13:06:00.000Z" + "mergedAt": "2026-05-06T14:46:00.000Z" } ], "illnessEpisodes": [ @@ -700,7 +700,7 @@ "sourceSystem": "PAPERLESS", "sourceId": "4711", "sourceInstance": "https://paperless.example.test", - "createdAt": "2026-05-04T01:24:00.000Z", + "createdAt": "2026-05-04T01:25:00.000Z", "summary": "rt-InboundDocument-summaryEncrypted" } ], @@ -1228,6 +1228,14 @@ "updatedAt": "2026-05-05T18:52:00.000Z" } ], + "documentImportKeys": [ + { + "sourceSystem": "PAPRA", + "sourceId": "doc_deleted", + "sourceInstance": "https://papra.example", + "createdAt": "2026-07-03T09:00:00.000Z" + } + ], "personalRecords": [ { "metricType": "WALKING_RUNNING_DISTANCE", @@ -1239,7 +1247,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-5km", - "createdAt": "2026-05-05T20:31:01.000Z" + "createdAt": "2026-05-05T22:11:01.000Z" }, { "metricType": "WALKING_RUNNING_DISTANCE", @@ -1251,7 +1259,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-10km", - "createdAt": "2026-05-05T20:31:02.000Z" + "createdAt": "2026-05-05T22:11:02.000Z" }, { "metricType": "WEIGHT", @@ -1263,19 +1271,19 @@ "sourceMeasurementId": "", "source": "MANUAL", "externalId": null, - "createdAt": "2026-05-05T20:31:00.000Z" + "createdAt": "2026-05-05T22:11:00.000Z" } ], "userAchievements": [ { "achievementId": "retired-badge-from-an-older-release", "unlockedAt": "2025-11-05T08:00:00.000Z", - "createdAt": "2026-05-05T22:04:01.000Z" + "createdAt": "2026-05-05T23:44:01.000Z" }, { "achievementId": "intake-total-10", "unlockedAt": "2026-02-21T10:15:00.000Z", - "createdAt": "2026-05-05T22:04:00.000Z" + "createdAt": "2026-05-05T23:44:00.000Z" } ], "environmentContexts": [ @@ -1320,8 +1328,8 @@ "aqHours": 3, "aqFetchedAt": "2026-06-30T12:34:56.789Z", "fetchedAt": "2026-06-16T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:00.000Z", - "updatedAt": "2026-05-06T00:21:00.000Z" + "createdAt": "2026-05-06T02:00:00.000Z", + "updatedAt": "2026-05-06T02:01:00.000Z" }, { "date": "2026-07-01", @@ -1364,8 +1372,8 @@ "aqHours": null, "aqFetchedAt": null, "fetchedAt": "2026-07-02T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:01.000Z", - "updatedAt": "2026-05-06T00:21:01.000Z" + "createdAt": "2026-05-06T02:00:01.000Z", + "updatedAt": "2026-05-06T02:01:01.000Z" } ], "environmentTravelLocations": [ @@ -1375,8 +1383,8 @@ "lat": 41.4, "lon": 2.2, "label": "Barcelona", - "createdAt": "2026-05-06T01:25:00.000Z", - "updatedAt": "2026-05-06T01:26:00.000Z" + "createdAt": "2026-05-06T03:05:00.000Z", + "updatedAt": "2026-05-06T03:06:00.000Z" } ], "dawarichDays": [ @@ -1390,8 +1398,8 @@ "countryCode": "ES", "isTravelDay": true, "fetchedAt": "2026-06-16T03:20:00.000Z", - "createdAt": "2026-05-06T03:11:00.000Z", - "updatedAt": "2026-05-06T03:12:00.000Z" + "createdAt": "2026-05-06T04:51:00.000Z", + "updatedAt": "2026-05-06T04:52:00.000Z" } ], "ecgRecordings": [ @@ -1416,8 +1424,8 @@ "averageHeartRate": 88, "rhythmClassification": "IRREGULAR", "measurementId": "", - "createdAt": "2026-05-06T08:13:00.000Z", - "updatedAt": "2026-05-06T08:14:00.000Z" + "createdAt": "2026-05-06T09:53:00.000Z", + "updatedAt": "2026-05-06T09:54:00.000Z" } ], "symptomDefinitions": [ @@ -1492,35 +1500,35 @@ { "labResultId": "", "documentId": "", - "createdAt": "2026-05-06T14:44:00.000Z" + "createdAt": "2026-05-06T16:24:00.000Z" } ], "encounterMedicationLinks": [ { "encounterId": "", "medicationId": "", - "createdAt": "2026-05-06T16:24:00.000Z" + "createdAt": "2026-05-06T18:04:00.000Z" } ], "encounterVaccinationLinks": [ { "encounterId": "", "vaccinationId": "", - "createdAt": "2026-05-06T18:04:00.000Z" + "createdAt": "2026-05-06T19:44:00.000Z" } ], "medicationDocumentLinks": [ { "medicationId": "", "documentId": "", - "createdAt": "2026-05-06T19:44:00.000Z" + "createdAt": "2026-05-06T21:24:00.000Z" } ], "medicationConditionLinks": [ { "medicationId": "", "episodeId": "", - "createdAt": "2026-05-06T21:24:00.000Z" + "createdAt": "2026-05-06T23:04:00.000Z" } ], "nutrientDays": [ @@ -1587,6 +1595,7 @@ "consentReceipts": 0, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "personalRecords": 3, "userAchievements": 2, "environmentContexts": 2, @@ -1650,6 +1659,7 @@ "consentReceipts": 0, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "personalRecords": 3, "userAchievements": 2, "environmentContexts": 2, @@ -1989,7 +1999,7 @@ ], "source": "ARCHIVED", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:00.000Z" + "createdAt": "2026-05-06T06:27:00.000Z" }, { "validFrom": "2026-02-01T00:00:00.000Z", @@ -2003,7 +2013,7 @@ ], "source": "ARCHIVED", "supersededByIndex": 2, - "createdAt": "2026-05-06T04:47:02.000Z" + "createdAt": "2026-05-06T06:27:02.000Z" }, { "validFrom": "2026-03-01T00:00:00.000Z", @@ -2017,7 +2027,7 @@ ], "source": "MANUAL", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:01.000Z" + "createdAt": "2026-05-06T06:27:01.000Z" } ], "courses": [ @@ -2238,7 +2248,7 @@ "sourceBiomarkerId": "merged-away-marker", "sourceName": "Ferritin (Serum)", "readingCount": 3, - "mergedAt": "2026-05-06T13:06:00.000Z" + "mergedAt": "2026-05-06T14:46:00.000Z" } ], "illnessEpisodes": [ @@ -2431,7 +2441,7 @@ "sourceId": "4711", "sourceInstance": "https://paperless.example.test", "summary": "rt-InboundDocument-summaryEncrypted", - "createdAt": "2026-05-04T01:24:00.000Z" + "createdAt": "2026-05-04T01:25:00.000Z" } ], "documentConditionLinks": [ @@ -2473,6 +2483,14 @@ "updatedAt": "2026-05-05T18:52:00.000Z" } ], + "documentImportKeys": [ + { + "sourceSystem": "PAPRA", + "sourceId": "doc_deleted", + "sourceInstance": "https://papra.example", + "createdAt": "2026-07-03T09:00:00.000Z" + } + ], "nutrientDays": [ { "day": "2026-07-01", @@ -2790,35 +2808,35 @@ { "labResultId": "", "documentId": "", - "createdAt": "2026-05-06T14:44:00.000Z" + "createdAt": "2026-05-06T16:24:00.000Z" } ], "encounterMedicationLinks": [ { "encounterId": "", "medicationId": "", - "createdAt": "2026-05-06T16:24:00.000Z" + "createdAt": "2026-05-06T18:04:00.000Z" } ], "encounterVaccinationLinks": [ { "encounterId": "", "vaccinationId": "", - "createdAt": "2026-05-06T18:04:00.000Z" + "createdAt": "2026-05-06T19:44:00.000Z" } ], "medicationDocumentLinks": [ { "medicationId": "", "documentId": "", - "createdAt": "2026-05-06T19:44:00.000Z" + "createdAt": "2026-05-06T21:24:00.000Z" } ], "medicationConditionLinks": [ { "medicationId": "", "episodeId": "", - "createdAt": "2026-05-06T21:24:00.000Z" + "createdAt": "2026-05-06T23:04:00.000Z" } ], "coachConversations": [ @@ -2996,7 +3014,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-5km", - "createdAt": "2026-05-05T20:31:01.000Z" + "createdAt": "2026-05-05T22:11:01.000Z" }, { "metricType": "WALKING_RUNNING_DISTANCE", @@ -3008,7 +3026,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-10km", - "createdAt": "2026-05-05T20:31:02.000Z" + "createdAt": "2026-05-05T22:11:02.000Z" }, { "metricType": "WEIGHT", @@ -3020,19 +3038,19 @@ "sourceMeasurementId": "", "source": "MANUAL", "externalId": null, - "createdAt": "2026-05-05T20:31:00.000Z" + "createdAt": "2026-05-05T22:11:00.000Z" } ], "userAchievements": [ { "achievementId": "retired-badge-from-an-older-release", "unlockedAt": "2025-11-05T08:00:00.000Z", - "createdAt": "2026-05-05T22:04:01.000Z" + "createdAt": "2026-05-05T23:44:01.000Z" }, { "achievementId": "intake-total-10", "unlockedAt": "2026-02-21T10:15:00.000Z", - "createdAt": "2026-05-05T22:04:00.000Z" + "createdAt": "2026-05-05T23:44:00.000Z" } ], "environmentContexts": [ @@ -3077,8 +3095,8 @@ "aqHours": 3, "aqFetchedAt": "2026-06-30T12:34:56.789Z", "fetchedAt": "2026-06-16T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:00.000Z", - "updatedAt": "2026-05-06T00:21:00.000Z" + "createdAt": "2026-05-06T02:00:00.000Z", + "updatedAt": "2026-05-06T02:01:00.000Z" }, { "date": "2026-07-01", @@ -3121,8 +3139,8 @@ "aqHours": null, "aqFetchedAt": null, "fetchedAt": "2026-07-02T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:01.000Z", - "updatedAt": "2026-05-06T00:21:01.000Z" + "createdAt": "2026-05-06T02:00:01.000Z", + "updatedAt": "2026-05-06T02:01:01.000Z" } ], "environmentTravelLocations": [ @@ -3132,8 +3150,8 @@ "lat": 41.4, "lon": 2.2, "label": "Barcelona", - "createdAt": "2026-05-06T01:25:00.000Z", - "updatedAt": "2026-05-06T01:26:00.000Z" + "createdAt": "2026-05-06T03:05:00.000Z", + "updatedAt": "2026-05-06T03:06:00.000Z" } ], "dawarichDays": [ @@ -3147,8 +3165,8 @@ "countryCode": "ES", "isTravelDay": true, "fetchedAt": "2026-06-16T03:20:00.000Z", - "createdAt": "2026-05-06T03:11:00.000Z", - "updatedAt": "2026-05-06T03:12:00.000Z" + "createdAt": "2026-05-06T04:51:00.000Z", + "updatedAt": "2026-05-06T04:52:00.000Z" } ], "ecgRecordings": [ @@ -3173,8 +3191,8 @@ "averageHeartRate": 88, "rhythmClassification": "IRREGULAR", "measurementId": "", - "createdAt": "2026-05-06T08:13:00.000Z", - "updatedAt": "2026-05-06T08:14:00.000Z" + "createdAt": "2026-05-06T09:53:00.000Z", + "updatedAt": "2026-05-06T09:54:00.000Z" } ], "manifest": { @@ -3221,6 +3239,7 @@ "documents": 1, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "healthProfile": 1, "healthProfileFactRevisions": 0, "customMetrics": 1, @@ -3277,6 +3296,7 @@ "documents": 1, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "healthProfile": 1, "healthProfileFactRevisions": 0, "customMetrics": 1, diff --git a/tests/integration/__golden__/backup-parity/restore-disaster-recovery-over-live.json b/tests/integration/__golden__/backup-parity/restore-disaster-recovery-over-live.json index 77fa33c88..43417f7d6 100644 --- a/tests/integration/__golden__/backup-parity/restore-disaster-recovery-over-live.json +++ b/tests/integration/__golden__/backup-parity/restore-disaster-recovery-over-live.json @@ -27,6 +27,7 @@ "documents": 1, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "healthProfile": 1, "healthProfileFactRevisions": 2, "customMetrics": 2, @@ -83,6 +84,7 @@ "documents": 1, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 1, "healthProfile": 1, "healthProfileFactRevisions": 2, "customMetrics": 2, @@ -299,6 +301,7 @@ "documents": 1, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 1, "healthProfile": 1, "healthProfileFactRevisions": 2, "customMetrics": 2, diff --git a/tests/integration/__golden__/backup-parity/restore-disaster-recovery.json b/tests/integration/__golden__/backup-parity/restore-disaster-recovery.json index a8d078b9b..66db5ca0f 100644 --- a/tests/integration/__golden__/backup-parity/restore-disaster-recovery.json +++ b/tests/integration/__golden__/backup-parity/restore-disaster-recovery.json @@ -27,6 +27,7 @@ "documents": 1, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "healthProfile": 1, "healthProfileFactRevisions": 2, "customMetrics": 2, @@ -83,6 +84,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -299,6 +301,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -1641,8 +1644,9 @@ "sourceId": "4711", "sourceInstance": "https://paperless.example.test", "aiReadDeferred": true, - "createdAt": "2026-05-04T01:24:00.000Z", - "updatedAt": "2026-05-04T01:25:00.000Z", + "extractionIncomplete": true, + "createdAt": "2026-05-04T01:25:00.000Z", + "updatedAt": "2026-05-04T01:26:00.000Z", "deletedAt": null } ], @@ -2245,10 +2249,20 @@ "updatedAt": "2026-05-05T18:52:00.000Z" } ], - "PersonalRecord": [ + "DocumentImportKey": [ { "id": "", "userId": "round-trip-owner", + "sourceSystem": "PAPRA", + "sourceId": "doc_deleted", + "sourceInstance": "https://papra.example", + "createdAt": "2026-07-03T09:00:00.000Z" + } + ], + "PersonalRecord": [ + { + "id": "", + "userId": "round-trip-owner", "metricType": "WALKING_RUNNING_DISTANCE", "metricSlot": "running_10km_time", "direction": "MIN", @@ -2258,10 +2272,10 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-10km", - "createdAt": "2026-05-05T20:31:02.000Z" + "createdAt": "2026-05-05T22:11:02.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "metricType": "WALKING_RUNNING_DISTANCE", "metricSlot": "running_5km_time", @@ -2272,10 +2286,10 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-5km", - "createdAt": "2026-05-05T20:31:01.000Z" + "createdAt": "2026-05-05T22:11:01.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "metricType": "WEIGHT", "metricSlot": null, @@ -2286,28 +2300,28 @@ "sourceMeasurementId": "", "source": "MANUAL", "externalId": null, - "createdAt": "2026-05-05T20:31:00.000Z" + "createdAt": "2026-05-05T22:11:00.000Z" } ], "UserAchievement": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "achievementId": "intake-total-10", "unlockedAt": "2026-02-21T10:15:00.000Z", - "createdAt": "2026-05-05T22:04:00.000Z" + "createdAt": "2026-05-05T23:44:00.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "achievementId": "retired-badge-from-an-older-release", "unlockedAt": "2025-11-05T08:00:00.000Z", - "createdAt": "2026-05-05T22:04:01.000Z" + "createdAt": "2026-05-05T23:44:01.000Z" } ], "EnvironmentContext": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "date": "2026-06-15", "locationEncrypted": "", @@ -2347,11 +2361,11 @@ "aqHours": 3, "aqFetchedAt": "2026-06-30T12:34:56.789Z", "fetchedAt": "2026-06-16T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:00.000Z", - "updatedAt": "2026-05-06T00:21:00.000Z" + "createdAt": "2026-05-06T02:00:00.000Z", + "updatedAt": "2026-05-06T02:01:00.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "date": "2026-07-01", "locationEncrypted": "", @@ -2391,24 +2405,24 @@ "aqHours": null, "aqFetchedAt": null, "fetchedAt": "2026-07-02T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:01.000Z", - "updatedAt": "2026-05-06T00:21:01.000Z" + "createdAt": "2026-05-06T02:00:01.000Z", + "updatedAt": "2026-05-06T02:01:01.000Z" } ], "EnvironmentTravelLocation": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "startDate": "2026-06-10", "endDate": "2026-06-20", "locationEncrypted": "", - "createdAt": "2026-05-06T01:25:00.000Z", - "updatedAt": "2026-05-06T01:26:00.000Z" + "createdAt": "2026-05-06T03:05:00.000Z", + "updatedAt": "2026-05-06T03:06:00.000Z" } ], "DawarichDay": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "date": "2026-06-15", "totalDistanceM": 48300, @@ -2419,13 +2433,13 @@ "countryCodeEncrypted": "", "isTravelDay": true, "fetchedAt": "2026-06-16T03:20:00.000Z", - "createdAt": "2026-05-06T03:11:00.000Z", - "updatedAt": "2026-05-06T03:12:00.000Z" + "createdAt": "2026-05-06T04:51:00.000Z", + "updatedAt": "2026-05-06T04:52:00.000Z" } ], "MedicationScheduleRevision": [ { - "id": "", + "id": "", "medicationId": "", "validFrom": "2026-01-01T00:00:00.000Z", "validUntil": "2026-03-01T00:00:00.000Z", @@ -2438,10 +2452,10 @@ ], "source": "ARCHIVED", "supersededByRevisionId": null, - "createdAt": "2026-05-06T04:47:00.000Z" + "createdAt": "2026-05-06T06:27:00.000Z" }, { - "id": "", + "id": "", "medicationId": "", "validFrom": "2026-02-01T00:00:00.000Z", "validUntil": "2026-03-01T00:00:00.000Z", @@ -2453,11 +2467,11 @@ } ], "source": "ARCHIVED", - "supersededByRevisionId": "", - "createdAt": "2026-05-06T04:47:02.000Z" + "supersededByRevisionId": "", + "createdAt": "2026-05-06T06:27:02.000Z" }, { - "id": "", + "id": "", "medicationId": "", "validFrom": "2026-03-01T00:00:00.000Z", "validUntil": "2026-05-01T00:00:00.000Z", @@ -2470,23 +2484,23 @@ ], "source": "MANUAL", "supersededByRevisionId": null, - "createdAt": "2026-05-06T04:47:01.000Z" + "createdAt": "2026-05-06T06:27:01.000Z" } ], "MedicationEfficacyTarget": [ { - "id": "", + "id": "", "medicationId": "", "measurementType": "WEIGHT", "biomarkerId": "", "primary": false, - "createdAt": "2026-05-06T06:25:00.000Z", - "updatedAt": "2026-05-06T06:26:00.000Z" + "createdAt": "2026-05-06T08:05:00.000Z", + "updatedAt": "2026-05-06T08:06:00.000Z" } ], "EcgRecording": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "source": "MANUAL", "externalRecordingId": "round-trip-signal-1", @@ -2499,13 +2513,13 @@ "averageHeartRate": 88, "rhythmClassification": "IRREGULAR", "measurementId": "", - "createdAt": "2026-05-06T08:13:00.000Z", - "updatedAt": "2026-05-06T08:14:00.000Z" + "createdAt": "2026-05-06T09:53:00.000Z", + "updatedAt": "2026-05-06T09:54:00.000Z" } ], "OnboardingRecord": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "needsJson": { "areas": [ @@ -2568,80 +2582,80 @@ }, "modulesDerivedAt": "2026-07-01T08:00:00.000Z", "completedAt": "2026-07-01T08:00:00.000Z", - "createdAt": "2026-05-06T09:47:00.000Z", - "updatedAt": "2026-05-06T09:48:00.000Z" + "createdAt": "2026-05-06T11:27:00.000Z", + "updatedAt": "2026-05-06T11:28:00.000Z" } ], "BiomarkerAlias": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "biomarkerId": "", "name": "Ferr.", "nameFolded": "ferr.", "source": "IMPORT_CHOICE", - "createdAt": "2026-05-06T11:26:00.000Z" + "createdAt": "2026-05-06T13:06:00.000Z" } ], "BiomarkerMerge": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "targetBiomarkerId": "", "sourceBiomarkerId": "merged-away-marker", "sourceName": "Ferritin (Serum)", "readingCount": 3, - "mergedAt": "2026-05-06T13:06:00.000Z" + "mergedAt": "2026-05-06T14:46:00.000Z" } ], "LabResultDocumentLink": [ { - "id": "", + "id": "", "labResultId": "", "documentId": "", "userId": "round-trip-owner", - "createdAt": "2026-05-06T14:44:00.000Z" - } - ], - "EncounterMedicationLink": [ - { - "id": "", - "encounterId": "", - "medicationId": "", - "userId": "round-trip-owner", "createdAt": "2026-05-06T16:24:00.000Z" } ], - "EncounterVaccinationLink": [ + "EncounterMedicationLink": [ { "id": "", "encounterId": "", - "vaccinationId": "", + "medicationId": "", "userId": "round-trip-owner", "createdAt": "2026-05-06T18:04:00.000Z" } ], - "MedicationDocumentLink": [ + "EncounterVaccinationLink": [ { "id": "", - "medicationId": "", - "documentId": "", + "encounterId": "", + "vaccinationId": "", "userId": "round-trip-owner", "createdAt": "2026-05-06T19:44:00.000Z" } ], - "MedicationConditionLink": [ + "MedicationDocumentLink": [ { "id": "", "medicationId": "", - "episodeId": "", + "documentId": "", "userId": "round-trip-owner", "createdAt": "2026-05-06T21:24:00.000Z" } ], - "AccountGrant": [ + "MedicationConditionLink": [ { "id": "", + "medicationId": "", + "episodeId": "", + "userId": "round-trip-owner", + "createdAt": "2026-05-06T23:04:00.000Z" + } + ], + "AccountGrant": [ + { + "id": "", "grantorId": "round-trip-owner", "granteeId": "parity-delegate", "access": "READ", @@ -3444,7 +3458,7 @@ ], "source": "ARCHIVED", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:00.000Z" + "createdAt": "2026-05-06T06:27:00.000Z" }, { "id": "", @@ -3459,7 +3473,7 @@ ], "source": "ARCHIVED", "supersededByIndex": 2, - "createdAt": "2026-05-06T04:47:02.000Z" + "createdAt": "2026-05-06T06:27:02.000Z" }, { "id": "", @@ -3474,14 +3488,14 @@ ], "source": "MANUAL", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:01.000Z" + "createdAt": "2026-05-06T06:27:01.000Z" } ], "efficacyTargets": [ { "id": "", - "createdAt": "2026-05-06T06:25:00.000Z", - "updatedAt": "2026-05-06T06:26:00.000Z", + "createdAt": "2026-05-06T08:05:00.000Z", + "updatedAt": "2026-05-06T08:06:00.000Z", "measurementType": "WEIGHT", "biomarkerName": "Ferritin", "primary": false @@ -3877,7 +3891,7 @@ "name": "Ferr.", "source": "IMPORT_CHOICE", "id": "", - "createdAt": "2026-05-06T11:26:00.000Z" + "createdAt": "2026-05-06T13:06:00.000Z" } ] } @@ -3890,7 +3904,7 @@ "sourceBiomarkerId": "merged-away-marker", "sourceName": "Ferritin (Serum)", "readingCount": 3, - "mergedAt": "2026-05-06T13:06:00.000Z" + "mergedAt": "2026-05-06T14:46:00.000Z" } ], "illnessEpisodes": [ @@ -4099,8 +4113,8 @@ "sourceSystem": "PAPERLESS", "sourceId": "4711", "sourceInstance": "https://paperless.example.test", - "createdAt": "2026-05-04T01:24:00.000Z", - "updatedAt": "2026-05-04T01:25:00.000Z", + "createdAt": "2026-05-04T01:25:00.000Z", + "updatedAt": "2026-05-04T01:26:00.000Z", "contentEncrypted": "", "contentSha256": "0a738470f0945033965858e9895ee721ce981a25f013514d587d0a7a1946be7a", "contentCodec": "binary2", @@ -4111,7 +4125,8 @@ "summaryState": "PENDING", "lastIndexAttemptAt": "2026-06-30T12:34:56.789Z", "lastIndexOutcome": "rt-InboundDocument-lastIndexOutcome", - "aiReadDeferred": true + "aiReadDeferred": true, + "extractionIncomplete": true } ], "manifest": { @@ -4361,8 +4376,8 @@ ], "onboardingRecord": { "id": "", - "createdAt": "2026-05-06T09:47:00.000Z", - "updatedAt": "2026-05-06T09:48:00.000Z", + "createdAt": "2026-05-06T11:27:00.000Z", + "updatedAt": "2026-05-06T11:28:00.000Z", "needs": { "recordTarget": "me", "areas": [ @@ -4895,6 +4910,14 @@ "updatedAt": "2026-05-05T18:52:00.000Z" } ], + "documentImportKeys": [ + { + "sourceSystem": "PAPRA", + "sourceId": "doc_deleted", + "sourceInstance": "https://papra.example", + "createdAt": "2026-07-03T09:00:00.000Z" + } + ], "personalRecords": [ { "metricType": "WALKING_RUNNING_DISTANCE", @@ -4906,7 +4929,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-5km", - "createdAt": "2026-05-05T20:31:01.000Z" + "createdAt": "2026-05-05T22:11:01.000Z" }, { "metricType": "WALKING_RUNNING_DISTANCE", @@ -4918,7 +4941,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-10km", - "createdAt": "2026-05-05T20:31:02.000Z" + "createdAt": "2026-05-05T22:11:02.000Z" }, { "metricType": "WEIGHT", @@ -4930,19 +4953,19 @@ "sourceMeasurementId": "", "source": "MANUAL", "externalId": null, - "createdAt": "2026-05-05T20:31:00.000Z" + "createdAt": "2026-05-05T22:11:00.000Z" } ], "userAchievements": [ { "achievementId": "retired-badge-from-an-older-release", "unlockedAt": "2025-11-05T08:00:00.000Z", - "createdAt": "2026-05-05T22:04:01.000Z" + "createdAt": "2026-05-05T23:44:01.000Z" }, { "achievementId": "intake-total-10", "unlockedAt": "2026-02-21T10:15:00.000Z", - "createdAt": "2026-05-05T22:04:00.000Z" + "createdAt": "2026-05-05T23:44:00.000Z" } ], "environmentContexts": [ @@ -4988,8 +5011,8 @@ "aqHours": 3, "aqFetchedAt": "2026-06-30T12:34:56.789Z", "fetchedAt": "2026-06-16T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:00.000Z", - "updatedAt": "2026-05-06T00:21:00.000Z" + "createdAt": "2026-05-06T02:00:00.000Z", + "updatedAt": "2026-05-06T02:01:00.000Z" }, { "date": "2026-07-01", @@ -5033,8 +5056,8 @@ "aqHours": null, "aqFetchedAt": null, "fetchedAt": "2026-07-02T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:01.000Z", - "updatedAt": "2026-05-06T00:21:01.000Z" + "createdAt": "2026-05-06T02:00:01.000Z", + "updatedAt": "2026-05-06T02:01:01.000Z" } ], "environmentTravelLocations": [ @@ -5045,8 +5068,8 @@ "lon": null, "label": null, "locationEncrypted": "", - "createdAt": "2026-05-06T01:25:00.000Z", - "updatedAt": "2026-05-06T01:26:00.000Z" + "createdAt": "2026-05-06T03:05:00.000Z", + "updatedAt": "2026-05-06T03:06:00.000Z" } ], "dawarichDays": [ @@ -5061,8 +5084,8 @@ "countryCodeEncrypted": "", "isTravelDay": true, "fetchedAt": "2026-06-16T03:20:00.000Z", - "createdAt": "2026-05-06T03:11:00.000Z", - "updatedAt": "2026-05-06T03:12:00.000Z" + "createdAt": "2026-05-06T04:51:00.000Z", + "updatedAt": "2026-05-06T04:52:00.000Z" } ], "ecgRecordings": [ @@ -5079,8 +5102,8 @@ "averageHeartRate": 88, "rhythmClassification": "IRREGULAR", "measurementId": "", - "createdAt": "2026-05-06T08:13:00.000Z", - "updatedAt": "2026-05-06T08:14:00.000Z" + "createdAt": "2026-05-06T09:53:00.000Z", + "updatedAt": "2026-05-06T09:54:00.000Z" } ], "symptomDefinitions": [ @@ -5176,35 +5199,35 @@ { "labResultId": "", "documentId": "", - "createdAt": "2026-05-06T14:44:00.000Z" + "createdAt": "2026-05-06T16:24:00.000Z" } ], "encounterMedicationLinks": [ { "encounterId": "", "medicationId": "", - "createdAt": "2026-05-06T16:24:00.000Z" + "createdAt": "2026-05-06T18:04:00.000Z" } ], "encounterVaccinationLinks": [ { "encounterId": "", "vaccinationId": "", - "createdAt": "2026-05-06T18:04:00.000Z" + "createdAt": "2026-05-06T19:44:00.000Z" } ], "medicationDocumentLinks": [ { "medicationId": "", "documentId": "", - "createdAt": "2026-05-06T19:44:00.000Z" + "createdAt": "2026-05-06T21:24:00.000Z" } ], "medicationConditionLinks": [ { "medicationId": "", "episodeId": "", - "createdAt": "2026-05-06T21:24:00.000Z" + "createdAt": "2026-05-06T23:04:00.000Z" } ], "nutrientDays": [ diff --git a/tests/integration/__golden__/backup-parity/restore-portable.json b/tests/integration/__golden__/backup-parity/restore-portable.json index 68ae26e99..8b313cb53 100644 --- a/tests/integration/__golden__/backup-parity/restore-portable.json +++ b/tests/integration/__golden__/backup-parity/restore-portable.json @@ -27,6 +27,7 @@ "documents": 0, "documentConditionLinks": 1, "extractedFacts": 1, + "documentImportKeys": 1, "healthProfile": 1, "healthProfileFactRevisions": 0, "customMetrics": 1, @@ -129,6 +130,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -345,6 +347,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -1756,10 +1759,20 @@ "ConsentReceipt": [], "DocumentConditionLink": [], "ExtractedFact": [], - "PersonalRecord": [ + "DocumentImportKey": [ { "id": "", "userId": "round-trip-owner", + "sourceSystem": "PAPRA", + "sourceId": "doc_deleted", + "sourceInstance": "https://papra.example", + "createdAt": "2026-07-03T09:00:00.000Z" + } + ], + "PersonalRecord": [ + { + "id": "", + "userId": "round-trip-owner", "metricType": "WALKING_RUNNING_DISTANCE", "metricSlot": "running_10km_time", "direction": "MIN", @@ -1769,10 +1782,10 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-10km", - "createdAt": "2026-05-05T20:31:02.000Z" + "createdAt": "2026-05-05T22:11:02.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "metricType": "WALKING_RUNNING_DISTANCE", "metricSlot": "running_5km_time", @@ -1783,10 +1796,10 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-5km", - "createdAt": "2026-05-05T20:31:01.000Z" + "createdAt": "2026-05-05T22:11:01.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "metricType": "WEIGHT", "metricSlot": null, @@ -1797,28 +1810,28 @@ "sourceMeasurementId": "", "source": "MANUAL", "externalId": null, - "createdAt": "2026-05-05T20:31:00.000Z" + "createdAt": "2026-05-05T22:11:00.000Z" } ], "UserAchievement": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "achievementId": "intake-total-10", "unlockedAt": "2026-02-21T10:15:00.000Z", - "createdAt": "2026-05-05T22:04:00.000Z" + "createdAt": "2026-05-05T23:44:00.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "achievementId": "retired-badge-from-an-older-release", "unlockedAt": "2025-11-05T08:00:00.000Z", - "createdAt": "2026-05-05T22:04:01.000Z" + "createdAt": "2026-05-05T23:44:01.000Z" } ], "EnvironmentContext": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "date": "2026-06-15", "locationEncrypted": "", @@ -1858,11 +1871,11 @@ "aqHours": 3, "aqFetchedAt": "2026-06-30T12:34:56.789Z", "fetchedAt": "2026-06-16T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:00.000Z", - "updatedAt": "2026-05-06T00:21:00.000Z" + "createdAt": "2026-05-06T02:00:00.000Z", + "updatedAt": "2026-05-06T02:01:00.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "date": "2026-07-01", "locationEncrypted": "", @@ -1902,24 +1915,24 @@ "aqHours": null, "aqFetchedAt": null, "fetchedAt": "2026-07-02T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:01.000Z", - "updatedAt": "2026-05-06T00:21:01.000Z" + "createdAt": "2026-05-06T02:00:01.000Z", + "updatedAt": "2026-05-06T02:01:01.000Z" } ], "EnvironmentTravelLocation": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "startDate": "2026-06-10", "endDate": "2026-06-20", "locationEncrypted": "", - "createdAt": "2026-05-06T01:25:00.000Z", - "updatedAt": "2026-05-06T01:26:00.000Z" + "createdAt": "2026-05-06T03:05:00.000Z", + "updatedAt": "2026-05-06T03:06:00.000Z" } ], "DawarichDay": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "date": "2026-06-15", "totalDistanceM": 48300, @@ -1930,13 +1943,13 @@ "countryCodeEncrypted": "", "isTravelDay": true, "fetchedAt": "2026-06-16T03:20:00.000Z", - "createdAt": "2026-05-06T03:11:00.000Z", - "updatedAt": "2026-05-06T03:12:00.000Z" + "createdAt": "2026-05-06T04:51:00.000Z", + "updatedAt": "2026-05-06T04:52:00.000Z" } ], "MedicationScheduleRevision": [ { - "id": "", + "id": "", "medicationId": "", "validFrom": "2026-01-01T00:00:00.000Z", "validUntil": "2026-03-01T00:00:00.000Z", @@ -1949,10 +1962,10 @@ ], "source": "ARCHIVED", "supersededByRevisionId": null, - "createdAt": "2026-05-06T04:47:00.000Z" + "createdAt": "2026-05-06T06:27:00.000Z" }, { - "id": "", + "id": "", "medicationId": "", "validFrom": "2026-02-01T00:00:00.000Z", "validUntil": "2026-03-01T00:00:00.000Z", @@ -1964,11 +1977,11 @@ } ], "source": "ARCHIVED", - "supersededByRevisionId": "", - "createdAt": "2026-05-06T04:47:02.000Z" + "supersededByRevisionId": "", + "createdAt": "2026-05-06T06:27:02.000Z" }, { - "id": "", + "id": "", "medicationId": "", "validFrom": "2026-03-01T00:00:00.000Z", "validUntil": "2026-05-01T00:00:00.000Z", @@ -1981,12 +1994,12 @@ ], "source": "MANUAL", "supersededByRevisionId": null, - "createdAt": "2026-05-06T04:47:01.000Z" + "createdAt": "2026-05-06T06:27:01.000Z" } ], "MedicationEfficacyTarget": [ { - "id": "", + "id": "", "medicationId": "", "measurementType": "WEIGHT", "biomarkerId": "", @@ -1997,7 +2010,7 @@ ], "EcgRecording": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "source": "MANUAL", "externalRecordingId": "round-trip-signal-1", @@ -2010,13 +2023,13 @@ "averageHeartRate": 88, "rhythmClassification": "IRREGULAR", "measurementId": "", - "createdAt": "2026-05-06T08:13:00.000Z", - "updatedAt": "2026-05-06T08:14:00.000Z" + "createdAt": "2026-05-06T09:53:00.000Z", + "updatedAt": "2026-05-06T09:54:00.000Z" } ], "OnboardingRecord": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "needsJson": { "areas": [ @@ -2085,7 +2098,7 @@ ], "BiomarkerAlias": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "biomarkerId": "", "name": "Ferr.", @@ -2096,47 +2109,47 @@ ], "BiomarkerMerge": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "targetBiomarkerId": "", "sourceBiomarkerId": "merged-away-marker", "sourceName": "Ferritin (Serum)", "readingCount": 3, - "mergedAt": "2026-05-06T13:06:00.000Z" + "mergedAt": "2026-05-06T14:46:00.000Z" } ], "LabResultDocumentLink": [], "EncounterMedicationLink": [ { - "id": "", + "id": "", "encounterId": "", "medicationId": "", "userId": "round-trip-owner", - "createdAt": "2026-05-06T16:24:00.000Z" + "createdAt": "2026-05-06T18:04:00.000Z" } ], "EncounterVaccinationLink": [ { - "id": "", + "id": "", "encounterId": "", "vaccinationId": "", "userId": "round-trip-owner", - "createdAt": "2026-05-06T18:04:00.000Z" + "createdAt": "2026-05-06T19:44:00.000Z" } ], "MedicationDocumentLink": [], "MedicationConditionLink": [ { - "id": "", + "id": "", "medicationId": "", "episodeId": "", "userId": "round-trip-owner", - "createdAt": "2026-05-06T21:24:00.000Z" + "createdAt": "2026-05-06T23:04:00.000Z" } ], "AccountGrant": [ { - "id": "", + "id": "", "grantorId": "round-trip-owner", "granteeId": "parity-delegate", "access": "READ", @@ -2745,7 +2758,7 @@ ], "source": "ARCHIVED", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:00.000Z" + "createdAt": "2026-05-06T06:27:00.000Z" }, { "validFrom": "2026-02-01T00:00:00.000Z", @@ -2759,7 +2772,7 @@ ], "source": "ARCHIVED", "supersededByIndex": 2, - "createdAt": "2026-05-06T04:47:02.000Z" + "createdAt": "2026-05-06T06:27:02.000Z" }, { "validFrom": "2026-03-01T00:00:00.000Z", @@ -2773,7 +2786,7 @@ ], "source": "MANUAL", "supersededByIndex": null, - "createdAt": "2026-05-06T04:47:01.000Z" + "createdAt": "2026-05-06T06:27:01.000Z" } ], "efficacyTargets": [ @@ -2976,7 +2989,7 @@ "sourceBiomarkerId": "merged-away-marker", "sourceName": "Ferritin (Serum)", "readingCount": 3, - "mergedAt": "2026-05-06T13:06:00.000Z" + "mergedAt": "2026-05-06T14:46:00.000Z" } ], "illnessEpisodes": [ @@ -3551,6 +3564,14 @@ "consentReceipts": [], "documentConditionLinks": [], "extractedFacts": [], + "documentImportKeys": [ + { + "sourceSystem": "PAPRA", + "sourceId": "doc_deleted", + "sourceInstance": "https://papra.example", + "createdAt": "2026-07-03T09:00:00.000Z" + } + ], "personalRecords": [ { "metricType": "WALKING_RUNNING_DISTANCE", @@ -3562,7 +3583,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-5km", - "createdAt": "2026-05-05T20:31:01.000Z" + "createdAt": "2026-05-05T22:11:01.000Z" }, { "metricType": "WALKING_RUNNING_DISTANCE", @@ -3574,7 +3595,7 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-10km", - "createdAt": "2026-05-05T20:31:02.000Z" + "createdAt": "2026-05-05T22:11:02.000Z" }, { "metricType": "WEIGHT", @@ -3586,19 +3607,19 @@ "sourceMeasurementId": "", "source": "MANUAL", "externalId": null, - "createdAt": "2026-05-05T20:31:00.000Z" + "createdAt": "2026-05-05T22:11:00.000Z" } ], "userAchievements": [ { "achievementId": "retired-badge-from-an-older-release", "unlockedAt": "2025-11-05T08:00:00.000Z", - "createdAt": "2026-05-05T22:04:01.000Z" + "createdAt": "2026-05-05T23:44:01.000Z" }, { "achievementId": "intake-total-10", "unlockedAt": "2026-02-21T10:15:00.000Z", - "createdAt": "2026-05-05T22:04:00.000Z" + "createdAt": "2026-05-05T23:44:00.000Z" } ], "environmentContexts": [ @@ -3643,8 +3664,8 @@ "aqHours": 3, "aqFetchedAt": "2026-06-30T12:34:56.789Z", "fetchedAt": "2026-06-16T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:00.000Z", - "updatedAt": "2026-05-06T00:21:00.000Z" + "createdAt": "2026-05-06T02:00:00.000Z", + "updatedAt": "2026-05-06T02:01:00.000Z" }, { "date": "2026-07-01", @@ -3687,8 +3708,8 @@ "aqHours": null, "aqFetchedAt": null, "fetchedAt": "2026-07-02T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:01.000Z", - "updatedAt": "2026-05-06T00:21:01.000Z" + "createdAt": "2026-05-06T02:00:01.000Z", + "updatedAt": "2026-05-06T02:01:01.000Z" } ], "environmentTravelLocations": [ @@ -3698,8 +3719,8 @@ "lat": 41.4, "lon": 2.2, "label": "Barcelona", - "createdAt": "2026-05-06T01:25:00.000Z", - "updatedAt": "2026-05-06T01:26:00.000Z" + "createdAt": "2026-05-06T03:05:00.000Z", + "updatedAt": "2026-05-06T03:06:00.000Z" } ], "dawarichDays": [ @@ -3713,8 +3734,8 @@ "countryCode": "ES", "isTravelDay": true, "fetchedAt": "2026-06-16T03:20:00.000Z", - "createdAt": "2026-05-06T03:11:00.000Z", - "updatedAt": "2026-05-06T03:12:00.000Z" + "createdAt": "2026-05-06T04:51:00.000Z", + "updatedAt": "2026-05-06T04:52:00.000Z" } ], "ecgRecordings": [ @@ -3739,8 +3760,8 @@ "averageHeartRate": 88, "rhythmClassification": "IRREGULAR", "measurementId": "", - "createdAt": "2026-05-06T08:13:00.000Z", - "updatedAt": "2026-05-06T08:14:00.000Z" + "createdAt": "2026-05-06T09:53:00.000Z", + "updatedAt": "2026-05-06T09:54:00.000Z" } ], "symptomDefinitions": [ @@ -3816,14 +3837,14 @@ { "encounterId": "", "medicationId": "", - "createdAt": "2026-05-06T16:24:00.000Z" + "createdAt": "2026-05-06T18:04:00.000Z" } ], "encounterVaccinationLinks": [ { "encounterId": "", "vaccinationId": "", - "createdAt": "2026-05-06T18:04:00.000Z" + "createdAt": "2026-05-06T19:44:00.000Z" } ], "medicationDocumentLinks": [], @@ -3831,7 +3852,7 @@ { "medicationId": "", "episodeId": "", - "createdAt": "2026-05-06T21:24:00.000Z" + "createdAt": "2026-05-06T23:04:00.000Z" } ], "nutrientDays": [ diff --git a/tests/integration/__golden__/backup-parity/restore-skips.json b/tests/integration/__golden__/backup-parity/restore-skips.json index 1dec697bd..20f298931 100644 --- a/tests/integration/__golden__/backup-parity/restore-skips.json +++ b/tests/integration/__golden__/backup-parity/restore-skips.json @@ -27,6 +27,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -144,6 +145,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -360,6 +362,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -737,6 +740,7 @@ "ConsentReceipt": [], "DocumentConditionLink": [], "ExtractedFact": [], + "DocumentImportKey": [], "PersonalRecord": [], "UserAchievement": [], "EnvironmentContext": [], diff --git a/tests/integration/__golden__/backup-parity/restore-version-1.json b/tests/integration/__golden__/backup-parity/restore-version-1.json index 68895abe8..cc4f8dc5a 100644 --- a/tests/integration/__golden__/backup-parity/restore-version-1.json +++ b/tests/integration/__golden__/backup-parity/restore-version-1.json @@ -27,6 +27,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -83,6 +84,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -299,6 +301,7 @@ "documents": 0, "documentConditionLinks": 0, "extractedFacts": 0, + "documentImportKeys": 0, "healthProfile": 0, "healthProfileFactRevisions": 0, "customMetrics": 0, @@ -912,6 +915,7 @@ "ConsentReceipt": [], "DocumentConditionLink": [], "ExtractedFact": [], + "DocumentImportKey": [], "PersonalRecord": [], "UserAchievement": [], "EnvironmentContext": [], @@ -1800,6 +1804,7 @@ "consentReceipts": [], "documentConditionLinks": [], "extractedFacts": [], + "documentImportKeys": [], "personalRecords": [], "userAchievements": [], "environmentContexts": [], diff --git a/tests/integration/__golden__/backup-parity/seeded-account.json b/tests/integration/__golden__/backup-parity/seeded-account.json index cd3618a3f..065d219de 100644 --- a/tests/integration/__golden__/backup-parity/seeded-account.json +++ b/tests/integration/__golden__/backup-parity/seeded-account.json @@ -1239,8 +1239,9 @@ "sourceId": "4711", "sourceInstance": "https://paperless.example.test", "aiReadDeferred": true, - "createdAt": "2026-05-04T01:24:00.000Z", - "updatedAt": "2026-05-04T01:25:00.000Z", + "extractionIncomplete": true, + "createdAt": "2026-05-04T01:25:00.000Z", + "updatedAt": "2026-05-04T01:26:00.000Z", "deletedAt": null }, { @@ -1268,8 +1269,9 @@ "sourceId": null, "sourceInstance": null, "aiReadDeferred": false, - "createdAt": "2026-05-04T01:24:01.000Z", - "updatedAt": "2026-05-04T01:25:01.000Z", + "extractionIncomplete": false, + "createdAt": "2026-05-04T01:25:01.000Z", + "updatedAt": "2026-05-04T01:26:01.000Z", "deletedAt": "" } ], @@ -1872,10 +1874,20 @@ "updatedAt": "2026-05-05T18:52:00.000Z" } ], - "PersonalRecord": [ + "DocumentImportKey": [ { "id": "", "userId": "round-trip-owner", + "sourceSystem": "PAPRA", + "sourceId": "doc_deleted", + "sourceInstance": "https://papra.example", + "createdAt": "2026-07-03T09:00:00.000Z" + } + ], + "PersonalRecord": [ + { + "id": "", + "userId": "round-trip-owner", "metricType": "WALKING_RUNNING_DISTANCE", "metricSlot": "running_10km_time", "direction": "MIN", @@ -1885,10 +1897,10 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-10km", - "createdAt": "2026-05-05T20:31:02.000Z" + "createdAt": "2026-05-05T22:11:02.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "metricType": "WALKING_RUNNING_DISTANCE", "metricSlot": "running_5km_time", @@ -1899,10 +1911,10 @@ "sourceMeasurementId": null, "source": "APPLE_HEALTH", "externalId": "workout-pr-5km", - "createdAt": "2026-05-05T20:31:01.000Z" + "createdAt": "2026-05-05T22:11:01.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "metricType": "WEIGHT", "metricSlot": null, @@ -1913,28 +1925,28 @@ "sourceMeasurementId": "", "source": "MANUAL", "externalId": null, - "createdAt": "2026-05-05T20:31:00.000Z" + "createdAt": "2026-05-05T22:11:00.000Z" } ], "UserAchievement": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "achievementId": "intake-total-10", "unlockedAt": "2026-02-21T10:15:00.000Z", - "createdAt": "2026-05-05T22:04:00.000Z" + "createdAt": "2026-05-05T23:44:00.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "achievementId": "retired-badge-from-an-older-release", "unlockedAt": "2025-11-05T08:00:00.000Z", - "createdAt": "2026-05-05T22:04:01.000Z" + "createdAt": "2026-05-05T23:44:01.000Z" } ], "EnvironmentContext": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "date": "2026-06-15", "locationEncrypted": "", @@ -1974,11 +1986,11 @@ "aqHours": 3, "aqFetchedAt": "2026-06-30T12:34:56.789Z", "fetchedAt": "2026-06-16T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:00.000Z", - "updatedAt": "2026-05-06T00:21:00.000Z" + "createdAt": "2026-05-06T02:00:00.000Z", + "updatedAt": "2026-05-06T02:01:00.000Z" }, { - "id": "", + "id": "", "userId": "round-trip-owner", "date": "2026-07-01", "locationEncrypted": "", @@ -2018,24 +2030,24 @@ "aqHours": null, "aqFetchedAt": null, "fetchedAt": "2026-07-02T03:15:00.000Z", - "createdAt": "2026-05-06T00:20:01.000Z", - "updatedAt": "2026-05-06T00:21:01.000Z" + "createdAt": "2026-05-06T02:00:01.000Z", + "updatedAt": "2026-05-06T02:01:01.000Z" } ], "EnvironmentTravelLocation": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "startDate": "2026-06-10", "endDate": "2026-06-20", "locationEncrypted": "", - "createdAt": "2026-05-06T01:25:00.000Z", - "updatedAt": "2026-05-06T01:26:00.000Z" + "createdAt": "2026-05-06T03:05:00.000Z", + "updatedAt": "2026-05-06T03:06:00.000Z" } ], "DawarichDay": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "date": "2026-06-15", "totalDistanceM": 48300, @@ -2046,13 +2058,13 @@ "countryCodeEncrypted": "", "isTravelDay": true, "fetchedAt": "2026-06-16T03:20:00.000Z", - "createdAt": "2026-05-06T03:11:00.000Z", - "updatedAt": "2026-05-06T03:12:00.000Z" + "createdAt": "2026-05-06T04:51:00.000Z", + "updatedAt": "2026-05-06T04:52:00.000Z" } ], "MedicationScheduleRevision": [ { - "id": "", + "id": "", "medicationId": "", "validFrom": "2026-01-01T00:00:00.000Z", "validUntil": "2026-03-01T00:00:00.000Z", @@ -2065,10 +2077,10 @@ ], "source": "ARCHIVED", "supersededByRevisionId": null, - "createdAt": "2026-05-06T04:47:00.000Z" + "createdAt": "2026-05-06T06:27:00.000Z" }, { - "id": "", + "id": "", "medicationId": "", "validFrom": "2026-02-01T00:00:00.000Z", "validUntil": "2026-03-01T00:00:00.000Z", @@ -2080,11 +2092,11 @@ } ], "source": "ARCHIVED", - "supersededByRevisionId": "", - "createdAt": "2026-05-06T04:47:02.000Z" + "supersededByRevisionId": "", + "createdAt": "2026-05-06T06:27:02.000Z" }, { - "id": "", + "id": "", "medicationId": "", "validFrom": "2026-03-01T00:00:00.000Z", "validUntil": "2026-05-01T00:00:00.000Z", @@ -2097,23 +2109,23 @@ ], "source": "MANUAL", "supersededByRevisionId": null, - "createdAt": "2026-05-06T04:47:01.000Z" + "createdAt": "2026-05-06T06:27:01.000Z" } ], "MedicationEfficacyTarget": [ { - "id": "", + "id": "", "medicationId": "", "measurementType": "WEIGHT", "biomarkerId": "", "primary": false, - "createdAt": "2026-05-06T06:25:00.000Z", - "updatedAt": "2026-05-06T06:26:00.000Z" + "createdAt": "2026-05-06T08:05:00.000Z", + "updatedAt": "2026-05-06T08:06:00.000Z" } ], "EcgRecording": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "source": "MANUAL", "externalRecordingId": "round-trip-signal-1", @@ -2126,13 +2138,13 @@ "averageHeartRate": 88, "rhythmClassification": "IRREGULAR", "measurementId": "", - "createdAt": "2026-05-06T08:13:00.000Z", - "updatedAt": "2026-05-06T08:14:00.000Z" + "createdAt": "2026-05-06T09:53:00.000Z", + "updatedAt": "2026-05-06T09:54:00.000Z" } ], "OnboardingRecord": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "needsJson": { "areas": [ @@ -2171,80 +2183,80 @@ }, "modulesDerivedAt": "2026-07-01T08:00:00.000Z", "completedAt": "2026-07-01T08:00:00.000Z", - "createdAt": "2026-05-06T09:47:00.000Z", - "updatedAt": "2026-05-06T09:48:00.000Z" + "createdAt": "2026-05-06T11:27:00.000Z", + "updatedAt": "2026-05-06T11:28:00.000Z" } ], "BiomarkerAlias": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "biomarkerId": "", "name": "Ferr.", "nameFolded": "ferr.", "source": "IMPORT_CHOICE", - "createdAt": "2026-05-06T11:26:00.000Z" + "createdAt": "2026-05-06T13:06:00.000Z" } ], "BiomarkerMerge": [ { - "id": "", + "id": "", "userId": "round-trip-owner", "targetBiomarkerId": "", "sourceBiomarkerId": "merged-away-marker", "sourceName": "Ferritin (Serum)", "readingCount": 3, - "mergedAt": "2026-05-06T13:06:00.000Z" + "mergedAt": "2026-05-06T14:46:00.000Z" } ], "LabResultDocumentLink": [ { - "id": "", + "id": "", "labResultId": "", "documentId": "", "userId": "round-trip-owner", - "createdAt": "2026-05-06T14:44:00.000Z" - } - ], - "EncounterMedicationLink": [ - { - "id": "", - "encounterId": "", - "medicationId": "", - "userId": "round-trip-owner", "createdAt": "2026-05-06T16:24:00.000Z" } ], - "EncounterVaccinationLink": [ + "EncounterMedicationLink": [ { "id": "", "encounterId": "", - "vaccinationId": "", + "medicationId": "", "userId": "round-trip-owner", "createdAt": "2026-05-06T18:04:00.000Z" } ], - "MedicationDocumentLink": [ + "EncounterVaccinationLink": [ { "id": "", - "medicationId": "", - "documentId": "", + "encounterId": "", + "vaccinationId": "", "userId": "round-trip-owner", "createdAt": "2026-05-06T19:44:00.000Z" } ], - "MedicationConditionLink": [ + "MedicationDocumentLink": [ { "id": "", "medicationId": "", - "episodeId": "", + "documentId": "", "userId": "round-trip-owner", "createdAt": "2026-05-06T21:24:00.000Z" } ], - "AccountGrant": [ + "MedicationConditionLink": [ { "id": "", + "medicationId": "", + "episodeId": "", + "userId": "round-trip-owner", + "createdAt": "2026-05-06T23:04:00.000Z" + } + ], + "AccountGrant": [ + { + "id": "", "grantorId": "round-trip-owner", "granteeId": "parity-delegate", "access": "WRITE", @@ -2253,13 +2265,13 @@ "measurements" ] }, - "invitedAt": "2026-05-07T02:25:00.000Z", + "invitedAt": "2026-05-07T04:05:00.000Z", "acceptedAt": "2026-07-02T09:00:00.000Z", "revokedAt": null, "revokedBy": null, "expiresAt": null, "lastUsedAt": null, - "createdAt": "2026-05-07T02:31:00.000Z" + "createdAt": "2026-05-07T04:11:00.000Z" } ], "User": { @@ -2271,7 +2283,7 @@ "oidcIssuer": "rt-User-oidcIssuer", "oidcSub": "rt-User-oidcSub", "createdAt": "2026-06-30T12:34:56.789Z", - "updatedAt": "2026-05-06T23:08:00.000Z", + "updatedAt": "2026-05-07T00:48:00.000Z", "managedProfileAt": "2026-06-30T12:34:56.789Z", "heightCm": 2.5, "dateOfBirth": "2026-06-30T12:34:56.789Z", diff --git a/tests/integration/backup-document-import-ledger.test.ts b/tests/integration/backup-document-import-ledger.test.ts new file mode 100644 index 000000000..fa207d3c8 --- /dev/null +++ b/tests/integration/backup-document-import-ledger.test.ts @@ -0,0 +1,208 @@ +/** + * The memory that the person deleted an imported document, through a backup + * and a restore onto an instance that has never seen the account. + * + * Deleting an imported document leaves its source keys behind: on the + * tombstone while it waits in Recently deleted, then in the import ledger + * (`DocumentImportKey`) once the purge removes it. An importer re-sending + * such a key is answered "deleted" and nothing is stored. The backup carried + * neither, so after a restore onto a fresh instance a token importer brought + * back every document the person had deleted on purpose. + * + * The file now carries the ledger, and the keys of the documents waiting in + * Recently deleted as ledger entries (the documents themselves stay out, as + * they always have): restored, both are answered "deleted", while a live + * document's key stays the live document's. + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +process.env.ENCRYPTION_KEY = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +import { encrypt } from "@/lib/crypto"; +import { encryptDocumentContent } from "@/lib/documents/store"; +import { buildFullBackupPayload } from "@/lib/export/full-backup-payload"; +import { parseBackupPayload } from "@/lib/validations/backup"; +import { POST as RESTORE } from "./restore-job-driver"; + +import { cookieJar, headerJar } from "./mock-next-headers"; +import { getPrismaClient, truncateAllTables } from "./setup"; + +vi.mock("next/headers", async () => { + const { cookieJar, headerJar } = await import("./mock-next-headers"); + return { + headers: vi.fn(async () => ({ + get: (name: string) => headerJar.get(name.toLowerCase()) ?? null, + })), + cookies: vi.fn(async () => ({ + get: (name: string) => { + const value = cookieJar.get(name); + return value ? { name, value } : undefined; + }, + set: (name: string, value: string) => cookieJar.set(name, value), + delete: (name: string) => cookieJar.delete(name), + })), + }; +}); + +vi.mock("@/lib/db-compat", () => ({ + ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("@/lib/cache/invalidate", async (importOriginal) => { + const actual = + await importOriginal(); + return { ...actual, invalidateUserData: vi.fn() }; +}); + +const INSTANCE = "https://paperless.example"; +const pdf = (tag: string) => + Buffer.from(`%PDF-1.7\n% ${tag}\n1 0 obj\n<<>>\nendobj\n%%EOF\n`); + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); + cookieJar.clear(); + headerJar.clear(); +}); + +async function seedDocument( + userId: string, + sourceId: string, + deletedAt: Date | null, +) { + const bytes = pdf(sourceId); + const { content, codec } = encryptDocumentContent(bytes); + return getPrismaClient().inboundDocument.create({ + data: { + userId, + mimeType: "application/pdf", + byteSize: bytes.byteLength, + contentEncrypted: content, + contentCodec: codec, + sourceSystem: "PAPERLESS", + sourceId, + sourceInstance: INSTANCE, + deletedAt, + }, + }); +} + +async function signIn(userId: string) { + const session = await getPrismaClient().session.create({ + data: { userId, expiresAt: new Date(Date.now() + 600_000) }, + }); + cookieJar.set("healthlog_session", session.id); +} + +async function importAgain(sourceId: string) { + const { POST } = await import("@/app/api/documents/inbound/route"); + const bytes = pdf(sourceId); + const payload = new Uint8Array(bytes.byteLength); + payload.set(bytes); + const form = new FormData(); + form.append("file", new Blob([payload]), "letter.pdf"); + form.append("sourceSystem", "PAPERLESS"); + form.append("sourceId", sourceId); + form.append("sourceInstance", INSTANCE); + const res = await (POST as unknown as (r: Request) => Promise)( + new Request("http://localhost/api/documents/inbound", { + method: "POST", + body: form, + }), + ); + return { status: res.status, body: await res.json() }; +} + +describe("a restore onto a fresh instance", () => { + it("still answers a deliberately deleted import as deleted", async () => { + const prisma = getPrismaClient(); + const owner = await prisma.user.create({ + data: { + username: "ledger-owner", + email: "ledger-owner@example.test", + role: "ADMIN", + modulePreferencesJson: { inboundDocuments: true }, + }, + }); + // Waiting in Recently deleted, with a second key remembered as an alias. + const waiting = await seedDocument(owner.id, "412", new Date()); + await prisma.documentSourceAlias.create({ + data: { + userId: owner.id, + documentId: waiting.id, + sourceSystem: "PAPERLESS", + sourceId: "413", + sourceInstance: INSTANCE, + }, + }); + // Purged already: only the ledger remembers it. + await prisma.documentImportKey.create({ + data: { + userId: owner.id, + sourceSystem: "PAPERLESS", + sourceId: "500", + sourceInstance: INSTANCE, + }, + }); + // Live. + await seedDocument(owner.id, "600", null); + + const { payload: built } = await buildFullBackupPayload(prisma, owner.id, { + purpose: "disaster-recovery", + }); + const parsed = parseBackupPayload(JSON.stringify(built)); + + // The fresh instance: the account as the file brings it, nothing else. + await prisma.documentImportKey.deleteMany({ where: { userId: owner.id } }); + await prisma.inboundDocument.deleteMany({ where: { userId: owner.id } }); + const backup = await prisma.dataBackup.create({ + data: { + userId: owner.id, + type: "IMPORT_LEDGER_ROUND_TRIP", + data: encrypt(JSON.stringify(parsed)), + }, + }); + await signIn(owner.id); + const response = await RESTORE( + new Request(`http://localhost/api/admin/backups/${backup.id}/restore`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ confirm: "RESTORE" }), + }) as unknown as Parameters[0], + { params: Promise.resolve({ id: backup.id }) }, + ); + expect(response.status).toBe(200); + + expect( + ( + await prisma.documentImportKey.findMany({ + where: { userId: owner.id }, + select: { sourceSystem: true, sourceId: true, sourceInstance: true }, + orderBy: { sourceId: "asc" }, + }) + ).map((k) => [k.sourceSystem, k.sourceId, k.sourceInstance]), + ).toEqual([ + ["PAPERLESS", "412", INSTANCE], + ["PAPERLESS", "413", INSTANCE], + ["PAPERLESS", "500", INSTANCE], + ]); + + await signIn(owner.id); + for (const sourceId of ["412", "413", "500"]) { + const again = await importAgain(sourceId); + expect(again.status, sourceId).toBe(200); + expect(again.body.meta, sourceId).toEqual({ + duplicate: true, + deleted: true, + }); + } + // The live document's key is still the live document's. + const live = await importAgain("600"); + expect(live.status).toBe(200); + expect(live.body.meta).toMatchObject({ duplicate: true }); + expect(live.body.meta.deleted).toBeUndefined(); + expect( + await prisma.inboundDocument.count({ where: { userId: owner.id } }), + ).toBe(1); + }); +}); diff --git a/tests/integration/backup-fixture.ts b/tests/integration/backup-fixture.ts index 8b271145c..d28b6f67f 100644 --- a/tests/integration/backup-fixture.ts +++ b/tests/integration/backup-fixture.ts @@ -847,6 +847,19 @@ export async function seedEveryTwoEndedModel( }, }); + // An imported page the person deleted and the purge then removed: only the + // ledger remembers it, and a restore that dropped the row would let the + // importer store it again. + await prisma.documentImportKey.create({ + data: { + userId: OWNER_ID, + sourceSystem: "PAPRA", + sourceId: "doc_deleted", + sourceInstance: "https://papra.example", + createdAt: AT("2026-07-03T09:00:00.000Z"), + }, + }); + // The page filed under the SECOND condition, not the first. A restore that // kept the count and lost the pairing would leave the vault sorted wrongly // rather than visibly unsorted, which is harder to notice and worse. diff --git a/tests/integration/backup-round-trip.test.ts b/tests/integration/backup-round-trip.test.ts index 5b7c100da..0698b1bb1 100644 --- a/tests/integration/backup-round-trip.test.ts +++ b/tests/integration/backup-round-trip.test.ts @@ -286,6 +286,8 @@ const COUNT_BACK: Record< DocumentConditionLink: (p, userId) => p.documentConditionLink.count({ where: { userId } }), ExtractedFact: (p, userId) => p.extractedFact.count({ where: { userId } }), + DocumentImportKey: (p, userId) => + p.documentImportKey.count({ where: { userId } }), PersonalRecord: (p, userId) => p.personalRecord.count({ where: { userId } }), UserAchievement: (p, userId) => p.userAchievement.count({ where: { userId } }),