mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(backup): bring every relation link back from a portable restore
A portable file carries no id for a medication or a lab result, so the restore mints fresh ones and the links that named the old ids were dropped as having no end. Carry the source id as ref on both entries and re-point the links through an old-to-new map on restore.
This commit is contained in:
1 parent
c508ce61e3
commit
1a7050d95d
6 files changed
+297
-5
No files matched your search
@@ -1094,7 +1094,9 @@ export async function buildFullBackupPayload(
|
||||
externalId: m.externalId,
|
||||
updatedAt: m.updatedAt.toISOString(),
|
||||
}
|
||||
: {}),
|
||||
: // No id: a portable restore mints a fresh one. The relation links
|
||||
// still name this drug, so the file says which row they mean.
|
||||
{ ref: m.id }),
|
||||
name: m.name,
|
||||
dose: m.dose,
|
||||
active: m.active,
|
||||
|
||||
@@ -83,6 +83,8 @@ function readForExport(
|
||||
export interface LabResultBackupEntry {
|
||||
/** Present in canonical DR payloads so rows keep a stable identity. */
|
||||
id?: string;
|
||||
/** The source row's id in a portable payload, for the relation links. */
|
||||
ref?: string;
|
||||
panel: string | null;
|
||||
analyte: string;
|
||||
value: number | null;
|
||||
@@ -455,6 +457,9 @@ export async function buildRecordsBackupSection(
|
||||
note: null,
|
||||
}
|
||||
: {
|
||||
// No id: a portable restore mints a fresh one. The relation links
|
||||
// still name this row, so the file says which row they mean.
|
||||
ref: r.id,
|
||||
note: readForExport(
|
||||
r.noteEncrypted,
|
||||
"lab note",
|
||||
|
||||
@@ -244,6 +244,19 @@ function placeable<T>(
|
||||
return kept;
|
||||
}
|
||||
|
||||
/**
|
||||
* The id each medication and lab result had in the file, to the id the
|
||||
* restore gave it. A portable file carries neither row's id (a restore there
|
||||
* mints fresh ones), only a `ref` naming the source row, so a link that names
|
||||
* the source id is re-pointed through this map. An id the map does not hold is
|
||||
* used as it is: a disaster-recovery file keeps its ids, and a hand-edited one
|
||||
* falls through to the placement check below.
|
||||
*/
|
||||
export interface RelationLinkSourceIds {
|
||||
medications: ReadonlyMap<string, string>;
|
||||
labResults: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-create the five link tables.
|
||||
*
|
||||
@@ -261,7 +274,13 @@ export async function restoreRelationLinksData(
|
||||
ownerId: string,
|
||||
payload: RelationLinksBackupSection,
|
||||
skips: RestoreSkipLog,
|
||||
sourceIds: RelationLinkSourceIds = {
|
||||
medications: new Map(),
|
||||
labResults: new Map(),
|
||||
},
|
||||
): Promise<RelationLinksRestoreCleared> {
|
||||
const medication = (id: string) => sourceIds.medications.get(id) ?? id;
|
||||
const labResult = (id: string) => sourceIds.labResults.get(id) ?? id;
|
||||
const owned = { where: { userId: ownerId } };
|
||||
const [c1, c2, c3, c4, c5] = await Promise.all([
|
||||
tx.labResultDocumentLink.deleteMany(owned),
|
||||
@@ -278,7 +297,10 @@ export async function restoreRelationLinksData(
|
||||
});
|
||||
|
||||
const labDocs = placeable(
|
||||
payload.labResultDocumentLinks,
|
||||
payload.labResultDocumentLinks.map((r) => ({
|
||||
...r,
|
||||
labResultId: labResult(r.labResultId),
|
||||
})),
|
||||
[
|
||||
[(r) => r.labResultId, written.labResults],
|
||||
[(r) => r.documentId, written.documents],
|
||||
@@ -297,7 +319,10 @@ export async function restoreRelationLinksData(
|
||||
}
|
||||
|
||||
const encounterMeds = placeable(
|
||||
payload.encounterMedicationLinks,
|
||||
payload.encounterMedicationLinks.map((r) => ({
|
||||
...r,
|
||||
medicationId: medication(r.medicationId),
|
||||
})),
|
||||
[
|
||||
[(r) => r.encounterId, written.encounters],
|
||||
[(r) => r.medicationId, written.medications],
|
||||
@@ -335,7 +360,10 @@ export async function restoreRelationLinksData(
|
||||
}
|
||||
|
||||
const medDocs = placeable(
|
||||
payload.medicationDocumentLinks,
|
||||
payload.medicationDocumentLinks.map((r) => ({
|
||||
...r,
|
||||
medicationId: medication(r.medicationId),
|
||||
})),
|
||||
[
|
||||
[(r) => r.medicationId, written.medications],
|
||||
[(r) => r.documentId, written.documents],
|
||||
@@ -354,7 +382,10 @@ export async function restoreRelationLinksData(
|
||||
}
|
||||
|
||||
const medConditions = placeable(
|
||||
payload.medicationConditionLinks,
|
||||
payload.medicationConditionLinks.map((r) => ({
|
||||
...r,
|
||||
medicationId: medication(r.medicationId),
|
||||
})),
|
||||
[
|
||||
[(r) => r.medicationId, written.medications],
|
||||
[(r) => r.episodeId, written.episodes],
|
||||
|
||||
@@ -1158,6 +1158,12 @@ export async function restoreBackup(
|
||||
|
||||
const medByName = new Map<string, string>();
|
||||
const restoredMedicationIds = new Set<string>();
|
||||
// The id each medication had in the source account, to the id it got
|
||||
// here. A disaster-recovery file keeps the id, so this is the
|
||||
// identity there; a portable file names the source row only through
|
||||
// `ref`, and the relation links (written last) are re-pointed through
|
||||
// this map instead of being dropped as "end is gone".
|
||||
const medicationIdBySourceId = new Map<string, string>();
|
||||
// The efficacy targets cannot ride inside the medication create
|
||||
// beside the schedules and the packs: `biomarkerId` is a foreign key
|
||||
// into a catalogue this transaction has not restored yet. They are
|
||||
@@ -1454,6 +1460,10 @@ export async function restoreBackup(
|
||||
},
|
||||
});
|
||||
restoredMedicationIds.add(created.id);
|
||||
const medicationSourceId = m.id ?? m.ref;
|
||||
if (medicationSourceId) {
|
||||
medicationIdBySourceId.set(medicationSourceId, created.id);
|
||||
}
|
||||
if (!medByName.has(m.name)) medByName.set(m.name, created.id);
|
||||
// v1.39.4 — the clinical category lives in its own table keyed on the
|
||||
// medication id, so it is written after the row exists and inside the
|
||||
@@ -2172,6 +2182,8 @@ export async function restoreBackup(
|
||||
// `committedRecordId` names the lab result it was committed to, and
|
||||
// that reference is resolved against the rows this loop writes.
|
||||
const restoredLabResultIds = new Set<string>();
|
||||
// Same arrangement as `medicationIdBySourceId`, for the lab results.
|
||||
const labResultIdBySourceId = new Map<string, string>();
|
||||
// What each drug was supposed to move, written now that both ends
|
||||
// exist. The drugs came back several hundred lines up and the
|
||||
// analytes only just now, and one of those two had to move for a
|
||||
@@ -2280,6 +2292,9 @@ export async function restoreBackup(
|
||||
},
|
||||
});
|
||||
restoredLabResultIds.add(createdLab.id);
|
||||
const labSourceId = lab.id ?? lab.ref;
|
||||
if (labSourceId)
|
||||
labResultIdBySourceId.set(labSourceId, createdLab.id);
|
||||
}
|
||||
|
||||
reportSection("illness");
|
||||
@@ -2683,6 +2698,10 @@ export async function restoreBackup(
|
||||
ownerId,
|
||||
payload,
|
||||
skips,
|
||||
{
|
||||
medications: medicationIdBySourceId,
|
||||
labResults: labResultIdBySourceId,
|
||||
},
|
||||
);
|
||||
|
||||
// The Coach's threads, turns and document links. AFTER the
|
||||
|
||||
@@ -392,6 +392,10 @@ const customMedicationCategorySchema = z
|
||||
const medicationSchema = z
|
||||
.object({
|
||||
id: z.string().min(1).optional(),
|
||||
// The source row's id in a portable file, which carries no `id`: the
|
||||
// relation links name the medication by it, and the restore re-points
|
||||
// them at the row it writes (`relation-links-backup.ts`).
|
||||
ref: z.string().min(1).optional(),
|
||||
name: z.string().min(1),
|
||||
dose: z.string(),
|
||||
treatmentClass: z.enum(MedicationCategory).optional(),
|
||||
@@ -1126,6 +1130,8 @@ const appSettingsBackupSchema = z
|
||||
const labResultBackupSchema = z
|
||||
.object({
|
||||
id: z.string().min(1).optional(),
|
||||
// The source row's id in a portable file, as on a medication.
|
||||
ref: z.string().min(1).optional(),
|
||||
panel: z.string().nullable().optional(),
|
||||
analyte: z.string().min(1),
|
||||
value: z.number().nullable().optional(),
|
||||
|
||||
Reference in new issue
Block a user