mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Squashed merge of the v1.4.39 wave work from develop. The release extends the v1.4.38 fast-path posture to two more endpoints that still walked the source table on every cold mount and folds a cumulative `sum_value` column into the existing rollup tier so step / flight / distance / daylight / active-energy sparklines no longer re-derive their daily totals in Node. Raw measurements / mood_entries / medication_intake_events tables are byte-unchanged. Three new tiers (mood_entry_rollups, medication_compliance_rollups, measurement_rollups.sum_value) are derived caches that self-heal via boot-time backfill on first reach. Migrations 0070, 0071, 0072 are additive only. The sum_value column add is catalog-only DDL on PostgreSQL 11+ — no table rewrite, writes resume in milliseconds. No environment-variable change. No API contract break. No iOS contract change. Expected wins (live perf-verify rides the post-deploy window): - /api/mood/analytics cold 12.7 s -> ~200 ms - /api/medications/intake?scope=compliance cold 3.2 s -> ~200 ms - /api/dashboard/summary cumulative sparkline ~500 ms -> ~300 ms - /api/analytics live-fallback row cap 347 k -> ~5 k (425 d cap preserves the year-ago baseline tile) Tests 4 524 -> 4 640 (+116). Typecheck clean. Lint clean. See CHANGELOG.md for the full v1.4.39 entry.
120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
import { auditLog } from "@/lib/auth/audit";
|
|
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
|
|
import { NextRequest } from "next/server";
|
|
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { annotate } from "@/lib/logging/context";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
/**
|
|
* Delete all user-owned health/integration data while keeping the account.
|
|
*/
|
|
export const DELETE = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireAuth();
|
|
|
|
let confirm = "";
|
|
try {
|
|
const body = await request.json();
|
|
confirm = typeof body?.confirm === "string" ? body.confirm : "";
|
|
} catch {
|
|
return apiError("Invalid request", 422);
|
|
}
|
|
|
|
if (confirm !== "DELETE") {
|
|
return apiError("Confirmation missing", 422);
|
|
}
|
|
|
|
const userId = user.id;
|
|
|
|
const result = await prisma.$transaction(async (tx) => {
|
|
const measurements = await tx.measurement.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const intakeEvents = await tx.medicationIntakeEvent.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const medications = await tx.medication.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const moodEntries = await tx.moodEntry.deleteMany({
|
|
where: { userId },
|
|
});
|
|
// v1.4.39 W-MOOD — wipe the persisted rollup partition for this
|
|
// user so the next analytics read returns an empty envelope.
|
|
await tx.moodEntryRollup.deleteMany({ where: { userId } });
|
|
const apiTokens = await tx.apiToken.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const withingsConnections = await tx.withingsConnection.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const notificationChannels = await tx.notificationChannel.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const pushSubscriptions = await tx.pushSubscription.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const dataBackups = await tx.dataBackup.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const achievements = await tx.userAchievement.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const telegramDeletions = await tx.telegramScheduledDeletion.deleteMany({
|
|
where: { userId },
|
|
});
|
|
const auditLogs = await tx.auditLog.deleteMany({
|
|
where: { userId },
|
|
});
|
|
|
|
await tx.user.update({
|
|
where: { id: userId },
|
|
data: {
|
|
heightCm: null,
|
|
dateOfBirth: null,
|
|
gender: null,
|
|
insightsPrivacyMode: "aggregated",
|
|
insightsCachedAt: null,
|
|
insightsCachedText: null,
|
|
telegramBotToken: null,
|
|
telegramChatId: null,
|
|
telegramEnabled: false,
|
|
withingsClientIdEncrypted: null,
|
|
withingsClientSecretEncrypted: null,
|
|
moodLogUrlEncrypted: null,
|
|
moodLogApiKeyEncrypted: null,
|
|
moodLogEnabled: false,
|
|
moodLogLastSyncedAt: null,
|
|
moodLogWebhookSecret: null,
|
|
onboardingCompletedAt: null,
|
|
},
|
|
});
|
|
|
|
return {
|
|
measurements: measurements.count,
|
|
intakeEvents: intakeEvents.count,
|
|
medications: medications.count,
|
|
moodEntries: moodEntries.count,
|
|
apiTokens: apiTokens.count,
|
|
withingsConnections: withingsConnections.count,
|
|
notificationChannels: notificationChannels.count,
|
|
pushSubscriptions: pushSubscriptions.count,
|
|
dataBackups: dataBackups.count,
|
|
achievements: achievements.count,
|
|
telegramDeletions: telegramDeletions.count,
|
|
auditLogs: auditLogs.count,
|
|
};
|
|
});
|
|
|
|
await auditLog("user.data.clear", {
|
|
userId,
|
|
ipAddress: getClientIp(request),
|
|
details: result,
|
|
});
|
|
|
|
annotate({ action: { name: "settings.data.clear" }, meta: result });
|
|
|
|
return apiSuccess({ cleared: true, ...result });
|
|
});
|