From 14bd0407f9087ac8787e3ecd460b220c25bda5bd Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Thu, 15 Jan 2026 15:16:07 +0000 Subject: [PATCH] Fix PocketBase date format - use YYYY-MM-DD instead of ISO PocketBase filters don't accept ISO format with T separator (causes 400). Changed both garmin-sync storage and today route query to use simple YYYY-MM-DD format, matching the working /api/history pattern. TDD approach: wrote failing tests first, then implemented the fix. Co-Authored-By: Claude Opus 4.5 --- src/app/api/cron/garmin-sync/route.test.ts | 11 ++++----- src/app/api/cron/garmin-sync/route.ts | 12 ++++------ src/app/api/today/route.test.ts | 28 +++++++++++++++++++++- src/app/api/today/route.ts | 22 ++++++----------- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/app/api/cron/garmin-sync/route.test.ts b/src/app/api/cron/garmin-sync/route.test.ts index 7d62cdf..39b73dd 100644 --- a/src/app/api/cron/garmin-sync/route.test.ts +++ b/src/app/api/cron/garmin-sync/route.test.ts @@ -414,18 +414,17 @@ describe("POST /api/cron/garmin-sync", () => { ); }); - it("sets date to today's ISO date string", async () => { + it("sets date to YYYY-MM-DD format string", async () => { mockUsers = [createMockUser()]; - // Full ISO format for consistent date comparison in queries - const todayDate = new Date(); - todayDate.setUTCHours(0, 0, 0, 0); - const todayISO = todayDate.toISOString(); + // Simple YYYY-MM-DD format for PocketBase date field compatibility + // PocketBase filters don't accept ISO format with T separator + const today = new Date().toISOString().split("T")[0]; await POST(createMockRequest(`Bearer ${validSecret}`)); expect(mockPbCreate).toHaveBeenCalledWith( expect.objectContaining({ - date: todayISO, + date: today, }), ); }); diff --git a/src/app/api/cron/garmin-sync/route.ts b/src/app/api/cron/garmin-sync/route.ts index adbaf74..59ee4e4 100644 --- a/src/app/api/cron/garmin-sync/route.ts +++ b/src/app/api/cron/garmin-sync/route.ts @@ -91,13 +91,9 @@ export async function POST(request: Request) { (u) => u.garminConnected && u.garminTokenExpiresAt && u.lastPeriodDate, ); - // YYYY-MM-DD format for Garmin API calls + // YYYY-MM-DD format for both Garmin API calls and PocketBase storage + // PocketBase date filters don't accept ISO format with T separator const today = new Date().toISOString().split("T")[0]; - // Full ISO format for PocketBase date field storage - // This ensures consistent date comparison in queries - const todayDate = new Date(); - todayDate.setUTCHours(0, 0, 0, 0); - const todayISO = todayDate.toISOString(); for (const user of users) { const userSyncStartTime = Date.now(); @@ -214,10 +210,10 @@ export async function POST(request: Request) { // Store default value 100 for body battery when Garmin returns null. // This prevents PocketBase's number field null-to-0 coercion from // causing the dashboard to display 0 instead of a meaningful value. - // Use full ISO date format for consistent date comparison in queries + // Use YYYY-MM-DD format for PocketBase date field compatibility await pb.collection("dailyLogs").create({ user: user.id, - date: todayISO, + date: today, cycleDay, phase, bodyBatteryCurrent: bodyBattery.current ?? 100, diff --git a/src/app/api/today/route.test.ts b/src/app/api/today/route.test.ts index 061ea54..1bdca77 100644 --- a/src/app/api/today/route.test.ts +++ b/src/app/api/today/route.test.ts @@ -12,6 +12,9 @@ let currentMockUser: User | null = null; // Module-level variable to control mock daily log in tests let currentMockDailyLog: DailyLog | null = null; +// Track the filter string passed to getFirstListItem +let lastDailyLogFilter: string | null = null; + // Create mock PocketBase client const mockPb = { collection: vi.fn((collectionName: string) => ({ @@ -30,7 +33,11 @@ const mockPb = { } throw new Error("Record not found"); }), - getFirstListItem: vi.fn(async () => { + getFirstListItem: vi.fn(async (filter: string) => { + // Capture the filter for testing + if (collectionName === "dailyLogs") { + lastDailyLogFilter = filter; + } if (!currentMockDailyLog) { const error = new Error("No DailyLog found"); (error as { status?: number }).status = 404; @@ -100,6 +107,7 @@ describe("GET /api/today", () => { vi.clearAllMocks(); currentMockUser = null; currentMockDailyLog = null; + lastDailyLogFilter = null; // Mock current date to 2025-01-10 for predictable testing vi.useFakeTimers(); vi.setSystemTime(new Date("2025-01-10T12:00:00Z")); @@ -508,6 +516,24 @@ describe("GET /api/today", () => { }); }); + describe("dailyLog query", () => { + it("queries dailyLogs with YYYY-MM-DD date format using contains operator", async () => { + // PocketBase filters don't accept ISO format with T separator + // Must use simple YYYY-MM-DD with ~ contains operator + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog(); + + await GET(mockRequest); + + // Verify filter uses YYYY-MM-DD format (2025-01-10) not ISO format + // The filter should use ~ contains operator, not >= range + expect(lastDailyLogFilter).toBeDefined(); + expect(lastDailyLogFilter).toContain('date~"2025-01-10"'); + // Should NOT contain ISO format with T separator + expect(lastDailyLogFilter).not.toContain("T"); + }); + }); + describe("biometrics data", () => { it("returns biometrics from daily log when available", async () => { currentMockUser = createMockUser(); diff --git a/src/app/api/today/route.ts b/src/app/api/today/route.ts index 67f1f21..0419717 100644 --- a/src/app/api/today/route.ts +++ b/src/app/api/today/route.ts @@ -74,24 +74,16 @@ export const GET = withAuth(async (_request, user, pb) => { // Sort by created DESC to get the most recent record if multiple exist let biometrics = { ...DEFAULT_BIOMETRICS, phaseLimit }; try { - // Use date range query for proper date field comparison - // PocketBase date fields need >= and < operators, not string contains - const todayStart = new Date(); - todayStart.setUTCHours(0, 0, 0, 0); - const tomorrowStart = new Date(todayStart.getTime() + 86400000); - const todayISO = todayStart.toISOString(); - const tomorrowISO = tomorrowStart.toISOString(); + // Use YYYY-MM-DD format with contains operator for PocketBase date field + // PocketBase filters don't accept ISO format with T separator + const today = new Date().toISOString().split("T")[0]; - logger.info( - { userId: user.id, todayISO, tomorrowISO }, - "Fetching dailyLog", - ); + logger.info({ userId: user.id, date: today }, "Fetching dailyLog"); const dailyLog = await pb .collection("dailyLogs") - .getFirstListItem( - `user="${user.id}" && date>="${todayISO}" && date<"${tomorrowISO}"`, - { sort: "-created" }, - ); + .getFirstListItem(`user="${user.id}" && date~"${today}"`, { + sort: "-created", + }); logger.info( {