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.
This commit is contained in:
Keysat
2026-05-09 19:42:45 -05:00
parent 5f7b3b6b7a
commit dc6a3b1116
6 changed files with 54 additions and 98 deletions
@@ -1,6 +1,6 @@
import { getCurrentUser } from "@/lib/auth";
import { getTimestampFileSuffix } from "@/lib/db-file";
import { getCaloriesBurnedBulk, prisma } from "@/lib/prisma";
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
@@ -42,6 +42,7 @@ export async function GET() {
notes: true,
durationMinutes: true,
difficulty: true,
caloriesBurned: true,
},
},
},
@@ -51,9 +52,6 @@ export async function GET() {
],
});
const workoutIds = Array.from(new Set(setLogs.map((s) => s.workout.id)));
const caloriesMap = await getCaloriesBurnedBulk(workoutIds);
const header = [
"workoutId",
"workoutDate",
@@ -92,7 +90,7 @@ export async function GET() {
set.workout.notes ?? "",
set.workout.durationMinutes ?? "",
set.workout.difficulty ?? "",
caloriesMap[set.workout.id] ?? "",
set.workout.caloriesBurned ?? "",
set.exerciseId,
set.exercise.name,
set.setNumber,
+6 -24
View File
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { prisma, getCaloriesBurned, setCaloriesBurned } from "@/lib/prisma";
import { prisma } from "@/lib/prisma";
import { z } from "zod";
// GET: Get workout by ID
@@ -36,10 +36,7 @@ export async function GET(
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
}
// Prisma client doesn't know about caloriesBurned — fetch via raw SQL
const caloriesBurned = await getCaloriesBurned(workout.id);
return NextResponse.json({ ...workout, caloriesBurned });
return NextResponse.json(workout);
} catch (error) {
console.error("Failed to fetch workout:", error);
return NextResponse.json(
@@ -101,11 +98,6 @@ export async function PATCH(
const body = await request.json();
const validated = updateWorkoutSchema.parse(body);
// Extract caloriesBurned separately — handled via raw SQL
const caloriesValue = validated.caloriesBurned;
const hasCaloriesUpdate = validated.caloriesBurned !== undefined;
// Build the Prisma-compatible workout update data (no caloriesBurned)
const workoutData: Record<string, unknown> = {};
if (validated.name !== undefined) workoutData.name = validated.name;
if (validated.notes !== undefined) workoutData.notes = validated.notes || null;
@@ -114,6 +106,8 @@ export async function PATCH(
workoutData.durationMinutes = validated.durationMinutes;
if (validated.difficulty !== undefined)
workoutData.difficulty = validated.difficulty;
if (validated.caloriesBurned !== undefined)
workoutData.caloriesBurned = validated.caloriesBurned;
// If sets are provided, do a full replace inside a transaction
if (validated.sets) {
@@ -164,13 +158,7 @@ export async function PATCH(
});
});
// Update caloriesBurned via raw SQL (outside transaction since Prisma doesn't know this column)
if (hasCaloriesUpdate) {
await setCaloriesBurned(params.id, caloriesValue ?? null);
}
const calories = await getCaloriesBurned(params.id);
return NextResponse.json({ ...result, caloriesBurned: calories });
return NextResponse.json(result);
}
// Metadata-only update
@@ -181,11 +169,6 @@ export async function PATCH(
});
}
// Update caloriesBurned via raw SQL
if (hasCaloriesUpdate) {
await setCaloriesBurned(params.id, caloriesValue ?? null);
}
const updated = await prisma.workout.findFirst({
where: { id: params.id, deletedAt: null },
include: {
@@ -195,9 +178,8 @@ export async function PATCH(
},
},
});
const calories = await getCaloriesBurned(params.id);
return NextResponse.json({ ...updated, caloriesBurned: calories });
return NextResponse.json(updated);
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
+4 -24
View File
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { Prisma } from "@prisma/client";
import { getCurrentUser } from "@/lib/auth";
import { prisma, setCaloriesBurned, getCaloriesBurnedBulk } from "@/lib/prisma";
import { prisma } from "@/lib/prisma";
// Schema now supports creating empty workouts (just date) or with sets
const createWorkoutSchema = z.object({
@@ -90,16 +90,8 @@ export async function GET(request: NextRequest) {
prisma.workout.count({ where }),
]);
// Supplement with caloriesBurned from raw SQL
const ids = workouts.map((w) => w.id);
const caloriesMap = await getCaloriesBurnedBulk(ids);
const enriched = workouts.map((w) => ({
...w,
caloriesBurned: caloriesMap[w.id] ?? null,
}));
return NextResponse.json({
data: enriched,
data: workouts,
meta: {
total,
limit,
@@ -129,20 +121,13 @@ export async function POST(request: NextRequest) {
const workoutDate = validated.date ? new Date(validated.date) : new Date();
// Extract caloriesBurned — handled via raw SQL after creation
const caloriesValue = validated.caloriesBurned;
// Note: caloriesBurned was historically handled via raw SQL because
// older Prisma client generations didn't include the column. Schema
// and client are now aligned, so it's a normal field — but we keep
// the post-create raw-SQL setter call below to avoid touching the
// existing call site in case it's relied upon elsewhere.
const createData: Prisma.WorkoutCreateInput = {
user: { connect: { id: user.id } },
name: validated.name || null,
notes: validated.notes,
durationMinutes: validated.durationMinutes,
difficulty: validated.difficulty,
caloriesBurned: validated.caloriesBurned,
date: workoutDate,
setLogs:
validated.sets.length > 0
@@ -177,12 +162,7 @@ export async function POST(request: NextRequest) {
const workout = await prisma.workout.create({ data: createData, include: includeOpts });
// Set caloriesBurned via raw SQL
if (caloriesValue !== undefined) {
await setCaloriesBurned(workout.id, caloriesValue);
}
return NextResponse.json({ ...workout, caloriesBurned: caloriesValue ?? null }, { status: 201 });
return NextResponse.json(workout, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
-34
View File
@@ -11,37 +11,3 @@ export const prisma =
});
if (process.env.NODE_ENV !== "production") global.prisma = prisma;
/**
* caloriesBurned is in the DB schema but NOT in the generated Prisma client.
* These helpers use raw SQL to read/write it until Prisma client can be regenerated.
*/
export async function getCaloriesBurned(workoutId: string): Promise<number | null> {
const rows = await prisma.$queryRawUnsafe<Array<{ caloriesBurned: number | null }>>(
`SELECT caloriesBurned FROM Workout WHERE id = ?`,
workoutId
);
return rows[0]?.caloriesBurned ?? null;
}
export async function setCaloriesBurned(workoutId: string, calories: number | null): Promise<void> {
await prisma.$executeRawUnsafe(
`UPDATE Workout SET caloriesBurned = ? WHERE id = ?`,
calories,
workoutId
);
}
export async function getCaloriesBurnedBulk(workoutIds: string[]): Promise<Record<string, number | null>> {
if (workoutIds.length === 0) return {};
const placeholders = workoutIds.map(() => "?").join(",");
const rows = await prisma.$queryRawUnsafe<Array<{ id: string; caloriesBurned: number | null }>>(
`SELECT id, caloriesBurned FROM Workout WHERE id IN (${placeholders})`,
...workoutIds
);
const map: Record<string, number | null> = {};
for (const r of rows) {
map[r.id] = r.caloriesBurned;
}
return map;
}
+10 -11
View File
@@ -3,25 +3,24 @@ import { v_1_0_0_1 } from './v1.0.0.1'
import { v_1_0_0_2 } from './v1.0.0.2'
import { v_1_0_0_3 } from './v1.0.0.3'
import { v_1_0_0_4 } from './v1.0.0.4'
import { v_1_0_0_5 } from './v1.0.0.5'
/**
* Version graph for the `proof-of-work` package.
*
* v1.0.0:1 — initial release, seeded cutover from the legacy
* `workout-log` package.
* v1.0.0:2 — CSP fix (reverted the over-strict nonce-based CSP that
* broke first paint in v1.0.0:1).
* v1.0.0:3post-cutover seed strip (baked /data snapshot removed
* from the image now that the cutover is verified done).
* v1.0.0:4removes the default admin@local / workout123 credentials
* from fresh installs. Operator must run the StartOS Action
* "Set admin credentials" before login is possible.
* v1.0.0:2 — CSP fix.
* v1.0.0:3 — post-cutover seed strip.
* v1.0.0:4removes the default admin@local credentials; operator
* must run the StartOS Action to bootstrap the first admin.
* v1.0.0:5internal cleanup (removes caloriesBurned raw-SQL
* workaround). No user-facing change.
*
* StartOS picks `current` as the install target; `other` lists every
* node that can upgrade into `current`. Hosts on v1.0.0:1, :2, or :3
* upgrade to :4 via no-op up migrations; fresh installs land on :4.
* node that can upgrade into `current`.
*/
export const versionGraph = VersionGraph.of({
current: v_1_0_0_4,
other: [v_1_0_0_1, v_1_0_0_2, v_1_0_0_3],
current: v_1_0_0_5,
other: [v_1_0_0_1, v_1_0_0_2, v_1_0_0_3, v_1_0_0_4],
})
+31
View File
@@ -0,0 +1,31 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
/**
* v1.0.0:5 — internal cleanup, no user-facing change.
*
* Removes the `caloriesBurned` raw-SQL workaround from lib/prisma.ts.
* That workaround was a vestige of an early Prisma client generation
* that didn't include the column; schema and client have been aligned
* for several releases. The three exported helpers
* (getCaloriesBurned, setCaloriesBurned, getCaloriesBurnedBulk) and
* every caller now use normal type-safe Prisma queries.
*
* Net effect for users: zero. Net effect for the codebase: ~30 lines
* of $queryRawUnsafe / $executeRawUnsafe deleted, three call sites
* (workouts list, workout detail GET/PATCH, settings/export-csv)
* simplified to plain `caloriesBurned` field references with full
* TS type checking.
*
* No schema changes, no migration, no config changes.
*/
export const v_1_0_0_5 = VersionInfo.of({
version: '1.0.0:5',
releaseNotes: {
en_US:
'Internal cleanup: removes the legacy caloriesBurned raw-SQL workaround from lib/prisma.ts and switches every caller to type-safe Prisma queries. No user-facing changes; no migration; existing /data is untouched.',
},
migrations: {
up: async () => {},
down: IMPOSSIBLE,
},
})