Fix Garmin sync to handle PocketBase date strings
All checks were successful
Deploy / deploy (push) Successful in 2m38s
All checks were successful
Deploy / deploy (push) Successful in 2m38s
PocketBase returns date fields as ISO strings, not Date objects. The sync was failing with "e.getTime is not a function" because the code expected Date objects. - Export mapRecordToUser from pocketbase.ts - Use mapRecordToUser in cron route to properly parse dates - Add test for handling date fields as ISO strings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,15 @@ const mockPbCreate = vi.fn().mockResolvedValue({ id: "log123" });
|
||||
// Track user updates
|
||||
const mockPbUpdate = vi.fn().mockResolvedValue({});
|
||||
|
||||
// Helper to parse date values - handles both Date objects and ISO strings
|
||||
function parseDate(value: unknown): Date | null {
|
||||
if (!value) return null;
|
||||
if (value instanceof Date) return value;
|
||||
if (typeof value !== "string") return null;
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
// Mock PocketBase
|
||||
vi.mock("@/lib/pocketbase", () => ({
|
||||
createPocketBaseClient: vi.fn(() => ({
|
||||
@@ -26,6 +35,23 @@ vi.mock("@/lib/pocketbase", () => ({
|
||||
authWithPassword: vi.fn().mockResolvedValue({ token: "admin-token" }),
|
||||
})),
|
||||
})),
|
||||
mapRecordToUser: vi.fn((record: Record<string, unknown>) => ({
|
||||
id: record.id,
|
||||
email: record.email,
|
||||
garminConnected: record.garminConnected,
|
||||
garminOauth1Token: record.garminOauth1Token,
|
||||
garminOauth2Token: record.garminOauth2Token,
|
||||
garminTokenExpiresAt: parseDate(record.garminTokenExpiresAt),
|
||||
garminRefreshTokenExpiresAt: parseDate(record.garminRefreshTokenExpiresAt),
|
||||
calendarToken: record.calendarToken,
|
||||
lastPeriodDate: parseDate(record.lastPeriodDate),
|
||||
cycleLength: record.cycleLength,
|
||||
notificationTime: record.notificationTime,
|
||||
timezone: record.timezone,
|
||||
activeOverrides: record.activeOverrides || [],
|
||||
created: new Date(record.created as string),
|
||||
updated: new Date(record.updated as string),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock decryption
|
||||
@@ -218,6 +244,37 @@ describe("POST /api/cron/garmin-sync", () => {
|
||||
expect(body.usersProcessed).toBe(0);
|
||||
expect(body.success).toBe(true);
|
||||
});
|
||||
|
||||
it("handles date fields as ISO strings from PocketBase", async () => {
|
||||
// PocketBase returns date fields as ISO strings, not Date objects
|
||||
// This simulates the raw response from pb.collection("users").getFullList()
|
||||
const rawPocketBaseRecord = {
|
||||
id: "user123",
|
||||
email: "test@example.com",
|
||||
garminConnected: true,
|
||||
garminOauth1Token: "encrypted:oauth1-token",
|
||||
garminOauth2Token: "encrypted:oauth2-token",
|
||||
garminTokenExpiresAt: "2026-02-15T00:00:00.000Z", // ISO string, not Date
|
||||
garminRefreshTokenExpiresAt: "2026-02-15T00:00:00.000Z", // ISO string, not Date
|
||||
calendarToken: "cal-token",
|
||||
lastPeriodDate: "2025-01-01T00:00:00.000Z", // ISO string, not Date
|
||||
cycleLength: 28,
|
||||
notificationTime: "07:00",
|
||||
timezone: "America/New_York",
|
||||
activeOverrides: [],
|
||||
created: "2024-01-01T00:00:00.000Z",
|
||||
updated: "2025-01-10T00:00:00.000Z",
|
||||
};
|
||||
// Cast to User to simulate what getFullList<User>() returns
|
||||
mockUsers = [rawPocketBaseRecord as unknown as User];
|
||||
|
||||
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body.usersProcessed).toBe(1);
|
||||
expect(body.errors).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Token handling", () => {
|
||||
|
||||
@@ -22,8 +22,7 @@ import {
|
||||
garminSyncDuration,
|
||||
garminSyncTotal,
|
||||
} from "@/lib/metrics";
|
||||
import { createPocketBaseClient } from "@/lib/pocketbase";
|
||||
import type { User } from "@/types";
|
||||
import { createPocketBaseClient, mapRecordToUser } from "@/lib/pocketbase";
|
||||
|
||||
interface SyncResult {
|
||||
success: boolean;
|
||||
@@ -84,9 +83,10 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch all users (we'll filter garminConnected in code to avoid PocketBase query syntax issues)
|
||||
// Also filter out users without required date fields (garminTokenExpiresAt, lastPeriodDate)
|
||||
const allUsers = await pb.collection("users").getFullList<User>();
|
||||
// Fetch all users and map to typed User objects (PocketBase returns dates as strings)
|
||||
// Filter to users with Garmin connected and required date fields
|
||||
const rawUsers = await pb.collection("users").getFullList();
|
||||
const allUsers = rawUsers.map(mapRecordToUser);
|
||||
const users = allUsers.filter(
|
||||
(u) => u.garminConnected && u.garminTokenExpiresAt && u.lastPeriodDate,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user