@@ -25,6 +25,10 @@ function unauthorizedResponse() {
2525 return jsonResponse ( { error : "Unauthorized" } , 401 ) ;
2626}
2727
28+ function serverErrorResponse ( message = "Internal Server Error" ) {
29+ return jsonResponse ( { error : message } , 500 ) ;
30+ }
31+
2832function isAuthorized ( request , env ) {
2933 const auth = request . headers . get ( "Authorization" ) || "" ;
3034 const expected = `Bearer ${ env . WORKOUT_API_TOKEN } ` ;
@@ -43,22 +47,46 @@ async function readJson(request) {
4347 }
4448}
4549
50+ function assertKvBinding ( env ) {
51+ if ( ! env . WORKOUTS || typeof env . WORKOUTS . get !== "function" || typeof env . WORKOUTS . put !== "function" ) {
52+ throw new Error ( "WORKOUTS KV binding is not configured" ) ;
53+ }
54+ }
55+
56+ function safeParseArray ( raw ) {
57+ if ( ! raw ) {
58+ return [ ] ;
59+ }
60+
61+ try {
62+ const parsed = typeof raw === "string" ? JSON . parse ( raw ) : raw ;
63+ return Array . isArray ( parsed ) ? parsed : [ ] ;
64+ } catch ( error ) {
65+ console . error ( "Failed to parse stored array" , error ) ;
66+ return [ ] ;
67+ }
68+ }
69+
4670async function getWorkouts ( env ) {
47- const workouts = await env . WORKOUTS . get ( WORKOUTS_KEY , "json" ) ;
48- return Array . isArray ( workouts ) ? workouts : [ ] ;
71+ assertKvBinding ( env ) ;
72+ const workouts = await env . WORKOUTS . get ( WORKOUTS_KEY ) ;
73+ return safeParseArray ( workouts ) ;
4974}
5075
5176async function saveWorkouts ( env , workouts ) {
77+ assertKvBinding ( env ) ;
5278 await env . WORKOUTS . put ( WORKOUTS_KEY , JSON . stringify ( workouts ) ) ;
5379}
5480
5581async function getWorkoutLogs ( env , workoutId ) {
56- const logs = await env . WORKOUTS . get ( `${ WORKOUT_LOGS_PREFIX } ${ workoutId } ` , "json" ) ;
57- const parsed = Array . isArray ( logs ) ? logs : [ ] ;
82+ assertKvBinding ( env ) ;
83+ const logs = await env . WORKOUTS . get ( `${ WORKOUT_LOGS_PREFIX } ${ workoutId } ` ) ;
84+ const parsed = safeParseArray ( logs ) ;
5885 return parsed . sort ( ( a , b ) => new Date ( b . finishedAt ) - new Date ( a . finishedAt ) ) ;
5986}
6087
6188async function saveWorkoutLogs ( env , workoutId , logs ) {
89+ assertKvBinding ( env ) ;
6290 const sorted = [ ...logs ] . sort ( ( a , b ) => new Date ( b . finishedAt ) - new Date ( a . finishedAt ) ) ;
6391 await env . WORKOUTS . put ( `${ WORKOUT_LOGS_PREFIX } ${ workoutId } ` , JSON . stringify ( sorted ) ) ;
6492}
@@ -166,129 +194,134 @@ function buildLogEntry(template, payload) {
166194
167195export default {
168196 async fetch ( request , env ) {
169- const url = new URL ( request . url ) ;
170- const pathParts = url . pathname . replace ( / ^ \/ + / , "" ) . split ( "/" ) ;
171-
172- if ( request . method === "OPTIONS" ) {
173- return handleOptions ( ) ;
174- }
175-
176- if ( ! isAuthorized ( request , env ) ) {
177- return unauthorizedResponse ( ) ;
178- }
197+ try {
198+ const url = new URL ( request . url ) ;
199+ const pathParts = url . pathname . replace ( / ^ \/ + / , "" ) . split ( "/" ) ;
179200
180- if ( url . pathname === "/api/workouts" && request . method === "GET" ) {
181- const workouts = await getWorkouts ( env ) ;
182- return jsonResponse ( workouts ) ;
183- }
201+ if ( request . method === "OPTIONS" ) {
202+ return handleOptions ( ) ;
203+ }
184204
185- if ( url . pathname === "/api/workouts" && request . method === "POST" ) {
186- const body = await readJson ( request ) ;
187- if ( body . error ) {
188- return jsonResponse ( { error : body . error } , 400 ) ;
205+ if ( ! isAuthorized ( request , env ) ) {
206+ return unauthorizedResponse ( ) ;
189207 }
190208
191- const name = typeof body . name === "string" ? body . name . trim ( ) : "" ;
192- if ( ! name ) {
193- return jsonResponse ( { error : "Workout name is required" } , 400 ) ;
209+ if ( url . pathname === "/api/workouts" && request . method === "GET" ) {
210+ const workouts = await getWorkouts ( env ) ;
211+ return jsonResponse ( workouts ) ;
194212 }
195213
196- const exerciseBlocksInput = Array . isArray ( body . exerciseBlocks )
197- ? body . exerciseBlocks
198- : [ ] ;
199- const normalizedBlocks = exerciseBlocksInput
200- . map ( normalizeExerciseBlock )
201- . filter ( Boolean ) ;
214+ if ( url . pathname === "/api/workouts" && request . method === "POST" ) {
215+ const body = await readJson ( request ) ;
216+ if ( body . error ) {
217+ return jsonResponse ( { error : body . error } , 400 ) ;
218+ }
202219
203- if ( normalizedBlocks . length === 0 ) {
204- return jsonResponse ( { error : "At least one exercise block is required" } , 400 ) ;
205- }
220+ const name = typeof body . name === "string" ? body . name . trim ( ) : "" ;
221+ if ( ! name ) {
222+ return jsonResponse ( { error : "Workout name is required" } , 400 ) ;
223+ }
206224
207- const workouts = await getWorkouts ( env ) ;
208- const workout = updateTimestamps ( {
209- id : body . id || crypto . randomUUID ( ) ,
210- name,
211- exerciseBlocks : normalizedBlocks ,
212- createdAt : undefined ,
213- updatedAt : undefined ,
214- } ) ;
215-
216- workouts . push ( workout ) ;
217- await saveWorkouts ( env , workouts ) ;
218- return jsonResponse ( workout , 201 ) ;
219- }
225+ const exerciseBlocksInput = Array . isArray ( body . exerciseBlocks )
226+ ? body . exerciseBlocks
227+ : [ ] ;
228+ const normalizedBlocks = exerciseBlocksInput
229+ . map ( normalizeExerciseBlock )
230+ . filter ( Boolean ) ;
231+
232+ if ( normalizedBlocks . length === 0 ) {
233+ return jsonResponse ( { error : "At least one exercise block is required" } , 400 ) ;
234+ }
220235
221- if ( pathParts [ 0 ] === "api" && pathParts [ 1 ] === "workouts" && pathParts [ 2 ] ) {
222- const id = decodeURIComponent ( pathParts [ 2 ] ) ;
223- const workouts = await getWorkouts ( env ) ;
224- const existing = findWorkout ( workouts , id ) ;
236+ const workouts = await getWorkouts ( env ) ;
237+ const workout = updateTimestamps ( {
238+ id : body . id || crypto . randomUUID ( ) ,
239+ name,
240+ exerciseBlocks : normalizedBlocks ,
241+ createdAt : undefined ,
242+ updatedAt : undefined ,
243+ } ) ;
225244
226- if ( ! existing ) {
227- return jsonResponse ( { error : "Workout not found" } , 404 ) ;
245+ workouts . push ( workout ) ;
246+ await saveWorkouts ( env , workouts ) ;
247+ return jsonResponse ( workout , 201 ) ;
228248 }
229249
230- if ( pathParts [ 3 ] === "logs" ) {
231- if ( request . method === "GET" && pathParts [ 4 ] === "latest" ) {
232- const logs = await getWorkoutLogs ( env , id ) ;
233- const latest = logs [ 0 ] || null ;
234- return jsonResponse ( { log : latest } ) ;
250+ if ( pathParts [ 0 ] === "api" && pathParts [ 1 ] === "workouts" && pathParts [ 2 ] ) {
251+ const id = decodeURIComponent ( pathParts [ 2 ] ) ;
252+ const workouts = await getWorkouts ( env ) ;
253+ const existing = findWorkout ( workouts , id ) ;
254+
255+ if ( ! existing ) {
256+ return jsonResponse ( { error : "Workout not found" } , 404 ) ;
257+ }
258+
259+ if ( pathParts [ 3 ] === "logs" ) {
260+ if ( request . method === "GET" && pathParts [ 4 ] === "latest" ) {
261+ const logs = await getWorkoutLogs ( env , id ) ;
262+ const latest = logs [ 0 ] || null ;
263+ return jsonResponse ( { log : latest } ) ;
264+ }
265+
266+ if ( request . method === "GET" ) {
267+ const logs = await getWorkoutLogs ( env , id ) ;
268+ return jsonResponse ( { logs } ) ;
269+ }
270+
271+ if ( request . method === "POST" ) {
272+ const body = await readJson ( request ) ;
273+ if ( body . error ) {
274+ return jsonResponse ( { error : body . error } , 400 ) ;
275+ }
276+
277+ const logEntry = buildLogEntry ( existing , body ) ;
278+ const logs = await getWorkoutLogs ( env , id ) ;
279+ logs . push ( logEntry ) ;
280+ await saveWorkoutLogs ( env , id , logs ) ;
281+ return jsonResponse ( logEntry , 201 ) ;
282+ }
235283 }
236284
237285 if ( request . method === "GET" ) {
238- const logs = await getWorkoutLogs ( env , id ) ;
239- return jsonResponse ( { logs } ) ;
286+ return jsonResponse ( existing ) ;
240287 }
241288
242- if ( request . method === "POST" ) {
289+ if ( request . method === "DELETE" ) {
290+ const updated = workouts . filter ( ( workout ) => workout . id !== id ) ;
291+ await saveWorkouts ( env , updated ) ;
292+ return jsonResponse ( { success : true } ) ;
293+ }
294+
295+ if ( request . method === "PUT" ) {
243296 const body = await readJson ( request ) ;
244297 if ( body . error ) {
245298 return jsonResponse ( { error : body . error } , 400 ) ;
246299 }
247300
248- const logEntry = buildLogEntry ( existing , body ) ;
249- const logs = await getWorkoutLogs ( env , id ) ;
250- logs . push ( logEntry ) ;
251- await saveWorkoutLogs ( env , id , logs ) ;
252- return jsonResponse ( logEntry , 201 ) ;
253- }
254- }
255-
256- if ( request . method === "GET" ) {
257- return jsonResponse ( existing ) ;
258- }
259-
260- if ( request . method === "DELETE" ) {
261- const updated = workouts . filter ( ( workout ) => workout . id !== id ) ;
262- await saveWorkouts ( env , updated ) ;
263- return jsonResponse ( { success : true } ) ;
264- }
265-
266- if ( request . method === "PUT" ) {
267- const body = await readJson ( request ) ;
268- if ( body . error ) {
269- return jsonResponse ( { error : body . error } , 400 ) ;
270- }
301+ const name = typeof body . name === "string" ? body . name . trim ( ) : existing . name ;
302+ const exerciseBlocksInput = Array . isArray ( body . exerciseBlocks )
303+ ? body . exerciseBlocks
304+ : existing . exerciseBlocks ;
305+ const normalizedBlocks = exerciseBlocksInput
306+ . map ( normalizeExerciseBlock )
307+ . filter ( Boolean ) ;
271308
272- const name = typeof body . name === "string" ? body . name . trim ( ) : existing . name ;
273- const exerciseBlocksInput = Array . isArray ( body . exerciseBlocks )
274- ? body . exerciseBlocks
275- : existing . exerciseBlocks ;
276- const normalizedBlocks = exerciseBlocksInput
277- . map ( normalizeExerciseBlock )
278- . filter ( Boolean ) ;
309+ if ( normalizedBlocks . length === 0 ) {
310+ return jsonResponse ( { error : "At least one exercise block is required" } , 400 ) ;
311+ }
279312
280- if ( normalizedBlocks . length === 0 ) {
281- return jsonResponse ( { error : "At least one exercise block is required" } , 400 ) ;
313+ existing . name = name ;
314+ existing . exerciseBlocks = normalizedBlocks ;
315+ updateTimestamps ( existing ) ;
316+ await saveWorkouts ( env , workouts ) ;
317+ return jsonResponse ( existing ) ;
282318 }
283-
284- existing . name = name ;
285- existing . exerciseBlocks = normalizedBlocks ;
286- updateTimestamps ( existing ) ;
287- await saveWorkouts ( env , workouts ) ;
288- return jsonResponse ( existing ) ;
289319 }
290- }
291320
292- return jsonResponse ( { error : "Not found" } , 404 ) ;
321+ return jsonResponse ( { error : "Not found" } , 404 ) ;
322+ } catch ( error ) {
323+ console . error ( "Unhandled error in workouts worker" , error ) ;
324+ return serverErrorResponse ( error . message ) ;
325+ }
293326 } ,
294327} ;
0 commit comments