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:
@@ -1,6 +1,6 @@
|
|||||||
import { getCurrentUser } from "@/lib/auth";
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
import { getTimestampFileSuffix } from "@/lib/db-file";
|
import { getTimestampFileSuffix } from "@/lib/db-file";
|
||||||
import { getCaloriesBurnedBulk, prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -42,6 +42,7 @@ export async function GET() {
|
|||||||
notes: true,
|
notes: true,
|
||||||
durationMinutes: true,
|
durationMinutes: true,
|
||||||
difficulty: 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 = [
|
const header = [
|
||||||
"workoutId",
|
"workoutId",
|
||||||
"workoutDate",
|
"workoutDate",
|
||||||
@@ -92,7 +90,7 @@ export async function GET() {
|
|||||||
set.workout.notes ?? "",
|
set.workout.notes ?? "",
|
||||||
set.workout.durationMinutes ?? "",
|
set.workout.durationMinutes ?? "",
|
||||||
set.workout.difficulty ?? "",
|
set.workout.difficulty ?? "",
|
||||||
caloriesMap[set.workout.id] ?? "",
|
set.workout.caloriesBurned ?? "",
|
||||||
set.exerciseId,
|
set.exerciseId,
|
||||||
set.exercise.name,
|
set.exercise.name,
|
||||||
set.setNumber,
|
set.setNumber,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getCurrentUser } from "@/lib/auth";
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
import { prisma, getCaloriesBurned, setCaloriesBurned } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
// GET: Get workout by ID
|
// GET: Get workout by ID
|
||||||
@@ -36,10 +36,7 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prisma client doesn't know about caloriesBurned — fetch via raw SQL
|
return NextResponse.json(workout);
|
||||||
const caloriesBurned = await getCaloriesBurned(workout.id);
|
|
||||||
|
|
||||||
return NextResponse.json({ ...workout, caloriesBurned });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch workout:", error);
|
console.error("Failed to fetch workout:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -101,11 +98,6 @@ export async function PATCH(
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const validated = updateWorkoutSchema.parse(body);
|
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> = {};
|
const workoutData: Record<string, unknown> = {};
|
||||||
if (validated.name !== undefined) workoutData.name = validated.name;
|
if (validated.name !== undefined) workoutData.name = validated.name;
|
||||||
if (validated.notes !== undefined) workoutData.notes = validated.notes || null;
|
if (validated.notes !== undefined) workoutData.notes = validated.notes || null;
|
||||||
@@ -114,6 +106,8 @@ export async function PATCH(
|
|||||||
workoutData.durationMinutes = validated.durationMinutes;
|
workoutData.durationMinutes = validated.durationMinutes;
|
||||||
if (validated.difficulty !== undefined)
|
if (validated.difficulty !== undefined)
|
||||||
workoutData.difficulty = validated.difficulty;
|
workoutData.difficulty = validated.difficulty;
|
||||||
|
if (validated.caloriesBurned !== undefined)
|
||||||
|
workoutData.caloriesBurned = validated.caloriesBurned;
|
||||||
|
|
||||||
// If sets are provided, do a full replace inside a transaction
|
// If sets are provided, do a full replace inside a transaction
|
||||||
if (validated.sets) {
|
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)
|
return NextResponse.json(result);
|
||||||
if (hasCaloriesUpdate) {
|
|
||||||
await setCaloriesBurned(params.id, caloriesValue ?? null);
|
|
||||||
}
|
|
||||||
const calories = await getCaloriesBurned(params.id);
|
|
||||||
|
|
||||||
return NextResponse.json({ ...result, caloriesBurned: calories });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata-only update
|
// 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({
|
const updated = await prisma.workout.findFirst({
|
||||||
where: { id: params.id, deletedAt: null },
|
where: { id: params.id, deletedAt: null },
|
||||||
include: {
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { getCurrentUser } from "@/lib/auth";
|
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
|
// Schema now supports creating empty workouts (just date) or with sets
|
||||||
const createWorkoutSchema = z.object({
|
const createWorkoutSchema = z.object({
|
||||||
@@ -90,16 +90,8 @@ export async function GET(request: NextRequest) {
|
|||||||
prisma.workout.count({ where }),
|
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({
|
return NextResponse.json({
|
||||||
data: enriched,
|
data: workouts,
|
||||||
meta: {
|
meta: {
|
||||||
total,
|
total,
|
||||||
limit,
|
limit,
|
||||||
@@ -129,20 +121,13 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const workoutDate = validated.date ? new Date(validated.date) : new Date();
|
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 = {
|
const createData: Prisma.WorkoutCreateInput = {
|
||||||
user: { connect: { id: user.id } },
|
user: { connect: { id: user.id } },
|
||||||
name: validated.name || null,
|
name: validated.name || null,
|
||||||
notes: validated.notes,
|
notes: validated.notes,
|
||||||
durationMinutes: validated.durationMinutes,
|
durationMinutes: validated.durationMinutes,
|
||||||
difficulty: validated.difficulty,
|
difficulty: validated.difficulty,
|
||||||
|
caloriesBurned: validated.caloriesBurned,
|
||||||
date: workoutDate,
|
date: workoutDate,
|
||||||
setLogs:
|
setLogs:
|
||||||
validated.sets.length > 0
|
validated.sets.length > 0
|
||||||
@@ -177,12 +162,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const workout = await prisma.workout.create({ data: createData, include: includeOpts });
|
const workout = await prisma.workout.create({ data: createData, include: includeOpts });
|
||||||
|
|
||||||
// Set caloriesBurned via raw SQL
|
return NextResponse.json(workout, { status: 201 });
|
||||||
if (caloriesValue !== undefined) {
|
|
||||||
await setCaloriesBurned(workout.id, caloriesValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ ...workout, caloriesBurned: caloriesValue ?? null }, { status: 201 });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -11,37 +11,3 @@ export const prisma =
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") global.prisma = 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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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_2 } from './v1.0.0.2'
|
||||||
import { v_1_0_0_3 } from './v1.0.0.3'
|
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_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.
|
* Version graph for the `proof-of-work` package.
|
||||||
*
|
*
|
||||||
* v1.0.0:1 — initial release, seeded cutover from the legacy
|
* v1.0.0:1 — initial release, seeded cutover from the legacy
|
||||||
* `workout-log` package.
|
* `workout-log` package.
|
||||||
* v1.0.0:2 — CSP fix (reverted the over-strict nonce-based CSP that
|
* v1.0.0:2 — CSP fix.
|
||||||
* broke first paint in v1.0.0:1).
|
* v1.0.0:3 — post-cutover seed strip.
|
||||||
* v1.0.0:3 — post-cutover seed strip (baked /data snapshot removed
|
* v1.0.0:4 — removes the default admin@local credentials; operator
|
||||||
* from the image now that the cutover is verified done).
|
* must run the StartOS Action to bootstrap the first admin.
|
||||||
* v1.0.0:4 — removes the default admin@local / workout123 credentials
|
* v1.0.0:5 — internal cleanup (removes caloriesBurned raw-SQL
|
||||||
* from fresh installs. Operator must run the StartOS Action
|
* workaround). No user-facing change.
|
||||||
* "Set admin credentials" before login is possible.
|
|
||||||
*
|
*
|
||||||
* StartOS picks `current` as the install target; `other` lists every
|
* 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
|
* node that can upgrade into `current`.
|
||||||
* upgrade to :4 via no-op up migrations; fresh installs land on :4.
|
|
||||||
*/
|
*/
|
||||||
export const versionGraph = VersionGraph.of({
|
export const versionGraph = VersionGraph.of({
|
||||||
current: v_1_0_0_4,
|
current: v_1_0_0_5,
|
||||||
other: [v_1_0_0_1, v_1_0_0_2, v_1_0_0_3],
|
other: [v_1_0_0_1, v_1_0_0_2, v_1_0_0_3, v_1_0_0_4],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user