Files
proof-of-work/proof-of-work/app/api/settings/export-csv/route.ts
T
Keysat dc6a3b1116 v1.0.0:5 — remove caloriesBurned raw-SQL workaround
The three exported helpers in lib/prisma.ts (getCaloriesBurned,
setCaloriesBurned, getCaloriesBurnedBulk) existed because an early
Prisma client generation didn't include the column. Schema and
client have been aligned for several releases — the workaround is
dead weight.

Removed: the helpers from lib/prisma.ts (~30 lines of
$queryRawUnsafe / $executeRawUnsafe).

Updated callers to use plain caloriesBurned field references:
- app/api/workouts/route.ts (GET list + POST create)
- app/api/workouts/[id]/route.ts (GET detail + PATCH update)
- app/api/settings/export-csv/route.ts (CSV export)

All call sites now go through normal type-safe Prisma queries.
Net effect for users: zero. Net effect for the codebase: cleaner
read paths, stronger TS coverage on caloriesBurned, fewer SQL
strings to audit.

No schema changes, no migration. Existing /data is untouched.

v1.0.0:5 promoted to current; :1, :2, :3, :4 in other.
2026-05-09 19:42:45 -05:00

131 lines
3.2 KiB
TypeScript

import { getCurrentUser } from "@/lib/auth";
import { getTimestampFileSuffix } from "@/lib/db-file";
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
function csvCell(value: unknown): string {
if (value === null || value === undefined) return "";
const text = String(value);
if (text.includes(",") || text.includes("\"") || text.includes("\n")) {
return `"${text.replace(/"/g, "\"\"")}"`;
}
return text;
}
/**
* GET /api/settings/export-csv
* Exports workout + set-level rows to CSV for offline backup.
*/
export async function GET() {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const setLogs = await prisma.setLog.findMany({
where: {
workout: {
userId: user.id,
deletedAt: null,
},
},
include: {
exercise: true,
workout: {
select: {
id: true,
date: true,
name: true,
notes: true,
durationMinutes: true,
difficulty: true,
caloriesBurned: true,
},
},
},
orderBy: [
{ workout: { date: "desc" } },
{ setNumber: "asc" },
],
});
const header = [
"workoutId",
"workoutDate",
"workoutName",
"workoutNotes",
"workoutDurationMinutes",
"workoutDifficulty",
"workoutCaloriesBurned",
"exerciseId",
"exerciseName",
"setNumber",
"reps",
"weight",
"weightUnit",
"durationMinutes",
"distance",
"distanceUnit",
"setCalories",
"rpe",
"setNotes",
"customMetricsJson",
];
const lines = [header.join(",")];
for (const set of setLogs) {
const durationMinutes =
typeof set.durationSeconds === "number"
? (set.durationSeconds / 60).toFixed(2)
: "";
const row = [
set.workout.id,
set.workout.date.toISOString(),
set.workout.name ?? "",
set.workout.notes ?? "",
set.workout.durationMinutes ?? "",
set.workout.difficulty ?? "",
set.workout.caloriesBurned ?? "",
set.exerciseId,
set.exercise.name,
set.setNumber,
set.reps ?? "",
set.weight ?? "",
set.weightUnit ?? "",
durationMinutes,
set.distance ?? "",
set.distanceUnit ?? "",
set.calories ?? "",
set.rpe ?? "",
set.notes ?? "",
set.customMetrics ?? "",
];
lines.push(row.map(csvCell).join(","));
}
const csv = lines.join("\n");
const fileName = `proof-of-work-export-${getTimestampFileSuffix()}.csv`;
return new NextResponse(csv, {
status: 200,
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": `attachment; filename="${fileName}"`,
"Cache-Control": "no-store",
},
});
} catch (error) {
console.error("CSV export error:", error);
return NextResponse.json(
{ error: "Failed to export CSV" },
{ status: 500 }
);
}
}