From d966355f83af54d1106460b8b7f2562277029728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 20 Jul 2026 14:02:08 +0200 Subject: [PATCH] chore: format data integrity fixes --- .../api/admin/backups/[id]/restore/route.ts | 10 +-- src/app/api/import/route.ts | 17 ++--- src/lib/cycle/__tests__/backup.test.ts | 8 +-- src/lib/whoop/sync.ts | 62 ++++++++++--------- .../admin-backups-canonical-roundtrip.test.ts | 57 ++++++++--------- 5 files changed, 67 insertions(+), 87 deletions(-) diff --git a/src/app/api/admin/backups/[id]/restore/route.ts b/src/app/api/admin/backups/[id]/restore/route.ts index 0bb3e75ff..341577473 100644 --- a/src/app/api/admin/backups/[id]/restore/route.ts +++ b/src/app/api/admin/backups/[id]/restore/route.ts @@ -367,8 +367,7 @@ const handler = apiHandler( adminCodexConnectedAt: settings.adminCodexConnectedAt ? new Date(settings.adminCodexConnectedAt) : null, - adminCodexConnectionStatus: - settings.adminCodexConnectionStatus, + adminCodexConnectionStatus: settings.adminCodexConnectionStatus, adminAiInsightsFeedbackSummary: settings.adminAiInsightsFeedbackSummary as never, defaultUserTimezone: settings.defaultUserTimezone, @@ -381,8 +380,7 @@ const handler = apiHandler( settings.assistantCorrelationsEnabled, assistantHealthScoreExplainerEnabled: settings.assistantHealthScoreExplainerEnabled, - moduleAvailabilityJson: - settings.moduleAvailabilityJson as never, + moduleAvailabilityJson: settings.moduleAvailabilityJson as never, documentMaxFileBytes: settings.documentMaxFileBytes, documentQuotaBytes: BigInt(settings.documentQuotaBytes), }; @@ -492,9 +490,7 @@ const handler = apiHandler( active: m.active ?? true, notificationsEnabled: m.notificationsEnabled ?? true, pausedAt: m.pausedAt ? new Date(m.pausedAt) : null, - snoozedUntil: m.snoozedUntil - ? new Date(m.snoozedUntil) - : null, + snoozedUntil: m.snoozedUntil ? new Date(m.snoozedUntil) : null, startsOn: m.startsOn ? new Date(m.startsOn) : null, endsOn: m.endsOn ? new Date(m.endsOn) : null, oneShot: m.oneShot ?? false, diff --git a/src/app/api/import/route.ts b/src/app/api/import/route.ts index 96983102d..fba6e2272 100644 --- a/src/app/api/import/route.ts +++ b/src/app/api/import/route.ts @@ -126,10 +126,8 @@ function stableFallbackMoodInstant( const digest = createHash("sha256") .update(`${fallbackMoodIdentity(entry)}:${occurrence}`) .digest(); - const initialOffset = - digest.readUInt32BE(0) % ENTRY_INSTANT_CLOCK_SKEW_MS; - const offset = - (initialOffset + probe) % ENTRY_INSTANT_CLOCK_SKEW_MS; + const initialOffset = digest.readUInt32BE(0) % ENTRY_INSTANT_CLOCK_SKEW_MS; + const offset = (initialOffset + probe) % ENTRY_INSTANT_CLOCK_SKEW_MS; const dateStart = fallbackMoodDateStart(entry.date); if (!dateStart) throw new Error("Invalid mood date"); return new Date(dateStart.getTime() + offset); @@ -308,17 +306,10 @@ export const POST = apiHandler(async (request: NextRequest) => { fallbackOccurrences.set(identity, occurrence + 1); } - for ( - let probe = 0; - probe < ENTRY_INSTANT_CLOCK_SKEW_MS; - probe++ - ) { + for (let probe = 0; probe < ENTRY_INSTANT_CLOCK_SKEW_MS; probe++) { const loggedAt = e.loggedAt ?? stableFallbackMoodInstant(e, occurrence, probe); - if ( - !e.loggedAt && - reservedFallbackInstants.has(loggedAt.getTime()) - ) { + if (!e.loggedAt && reservedFallbackInstants.has(loggedAt.getTime())) { continue; } try { diff --git a/src/lib/cycle/__tests__/backup.test.ts b/src/lib/cycle/__tests__/backup.test.ts index ee7998e65..170d3c29a 100644 --- a/src/lib/cycle/__tests__/backup.test.ts +++ b/src/lib/cycle/__tests__/backup.test.ts @@ -80,11 +80,9 @@ describe("buildCycleBackupSection disaster-recovery mode", () => { }, }; - const section = await buildCycleBackupSection( - prisma as never, - "user-1", - { purpose: "disaster-recovery" }, - ); + const section = await buildCycleBackupSection(prisma as never, "user-1", { + purpose: "disaster-recovery", + }); expect(prisma.menstrualCycle.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { userId: "user-1" } }), diff --git a/src/lib/whoop/sync.ts b/src/lib/whoop/sync.ts index a058d6cb7..ab11f04c6 100644 --- a/src/lib/whoop/sync.ts +++ b/src/lib/whoop/sync.ts @@ -367,37 +367,39 @@ export async function upsertWhoopMeasurements( chunkStart + WHOOP_MEASUREMENT_TRANSACTION_CHUNK_SIZE, readings.length, ); - const verdicts = await prisma.$transaction( - async (tx) => { - const outcomes = []; - for (let index = chunkStart; index < chunkEnd; index++) { - const reading = readings[index]!; - const verdict = await reconcileExternalMeasurement( - tx, - { - userId, - type: reading.type as MeasurementType, - source: "WHOOP", - value: reading.value, - unit: reading.unit, - measuredAt: reading.measuredAt, - externalId: reading.externalId, - sleepStage: reading.sleepStage ?? null, - }, - { exactExternalMatch: "update" }, - ); - if (verdict.status === "failed") { - throw new MeasurementReconciliationError(verdict); + const verdicts = await prisma + .$transaction( + async (tx) => { + const outcomes = []; + for (let index = chunkStart; index < chunkEnd; index++) { + const reading = readings[index]!; + const verdict = await reconcileExternalMeasurement( + tx, + { + userId, + type: reading.type as MeasurementType, + source: "WHOOP", + value: reading.value, + unit: reading.unit, + measuredAt: reading.measuredAt, + externalId: reading.externalId, + sleepStage: reading.sleepStage ?? null, + }, + { exactExternalMatch: "update" }, + ); + if (verdict.status === "failed") { + throw new MeasurementReconciliationError(verdict); + } + outcomes.push(verdict); } - outcomes.push(verdict); - } - return outcomes; - }, - { maxWait: 10_000, timeout: 60_000 }, - ).catch((err) => { - invalidateTouchedTypes(); - throw err; - }); + return outcomes; + }, + { maxWait: 10_000, timeout: 60_000 }, + ) + .catch((err) => { + invalidateTouchedTypes(); + throw err; + }); const touched: Array<{ type: MeasurementType; measuredAt: Date }> = []; const insertedRows: Array< diff --git a/tests/integration/admin-backups-canonical-roundtrip.test.ts b/tests/integration/admin-backups-canonical-roundtrip.test.ts index d0a24c222..ac4c7ca13 100644 --- a/tests/integration/admin-backups-canonical-roundtrip.test.ts +++ b/tests/integration/admin-backups-canonical-roundtrip.test.ts @@ -280,9 +280,7 @@ describe("canonical disaster-recovery backup round-trip", () => { scheduleType: "CYCLIC", cyclicOnWeeks: 3, cyclicOffWeeks: 1, - doseWindows: [ - { timeOfDay: "08:00", start: "07:30", end: "09:00" }, - ], + doseWindows: [{ timeOfDay: "08:00", start: "07:30", end: "09:00" }], }, }, }, @@ -340,8 +338,7 @@ describe("canonical disaster-recovery backup round-trip", () => { deletedAt: tombstoneDeletedAt, }, }); - const cycleCategory = - await prisma.cycleSymptomCategory.findFirstOrThrow(); + const cycleCategory = await prisma.cycleSymptomCategory.findFirstOrThrow(); const cycleSymptom = await prisma.cycleSymptom.create({ data: { id: "cycle-symptom-dr", @@ -542,7 +539,9 @@ describe("canonical disaster-recovery backup round-trip", () => { ); await prisma.appSettings.deleteMany(); - await prisma.medicationIntakeEvent.deleteMany({ where: { userId: ownerId } }); + await prisma.medicationIntakeEvent.deleteMany({ + where: { userId: ownerId }, + }); await prisma.medication.deleteMany({ where: { userId: ownerId } }); await prisma.cycleDayLog.deleteMany({ where: { userId: ownerId } }); await prisma.menstrualCycle.deleteMany({ where: { userId: ownerId } }); @@ -880,9 +879,7 @@ describe("canonical disaster-recovery backup round-trip", () => { type: "PULSE" as const, value: 60 + (index % 20), unit: "bpm", - measuredAt: new Date( - Date.UTC(2026, 6, 10, 0, 0, 0, index), - ).toISOString(), + measuredAt: new Date(Date.UTC(2026, 6, 10, 0, 0, 0, index)).toISOString(), source: "IMPORT" as const, externalId: `external-${index}`, syncVersion: 3, @@ -907,29 +904,25 @@ describe("canonical disaster-recovery backup round-trip", () => { options?: { maxWait?: number; timeout?: number }, ) => Promise; const transactionSpy = vi.spyOn(prisma, "$transaction"); - transactionSpy.mockImplementation( - (async ( - callback: (tx: Prisma.TransactionClient) => Promise, - options?: { maxWait?: number; timeout?: number }, - ) => - originalTransaction(async (tx) => { - const originalUpsert = tx.measurement.upsert.bind(tx.measurement); - const originalCreateMany = tx.measurement.createMany.bind( - tx.measurement, - ); - vi.spyOn(tx.measurement, "upsert").mockImplementation((args) => { - stableWriteQueries += 1; - return originalUpsert(args); - }); - vi.spyOn(tx.measurement, "createMany").mockImplementation( - (args) => { - stableWriteQueries += 1; - return originalCreateMany(args); - }, - ); - return callback(tx); - }, options)) as never, - ); + transactionSpy.mockImplementation((async ( + callback: (tx: Prisma.TransactionClient) => Promise, + options?: { maxWait?: number; timeout?: number }, + ) => + originalTransaction(async (tx) => { + const originalUpsert = tx.measurement.upsert.bind(tx.measurement); + const originalCreateMany = tx.measurement.createMany.bind( + tx.measurement, + ); + vi.spyOn(tx.measurement, "upsert").mockImplementation((args) => { + stableWriteQueries += 1; + return originalUpsert(args); + }); + vi.spyOn(tx.measurement, "createMany").mockImplementation((args) => { + stableWriteQueries += 1; + return originalCreateMany(args); + }); + return callback(tx); + }, options)) as never); const response = await POST( makeRequest(backup.id) as unknown as Parameters[0],