diff --git a/src/shared/api/client/client.test.ts b/src/shared/api/client/client.test.ts new file mode 100644 index 0000000..a56005c --- /dev/null +++ b/src/shared/api/client/client.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { PBHttpError } from '../error'; +import { getCollection } from './client'; + +describe('getCollection', () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + }); + + describe('when PocketBase is unreachable', () => { + it('returns an empty list instead of throwing', async () => { + vi.stubEnv('PB_URL', 'http://localhost:8090'); + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new TypeError('fetch failed'))); + + const result = await getCollection('projects'); + + expect(result.items).toEqual([]); + expect(result.totalItems).toBe(0); + }); + }); + + describe('when PocketBase returns an HTTP error', () => { + it('rethrows PBHttpError', async () => { + vi.stubEnv('PB_URL', 'http://localhost:8090'); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: false, + status: 403, + statusText: 'Forbidden', + json: vi.fn(), + }), + ); + + await expect(getCollection('projects')).rejects.toBeInstanceOf(PBHttpError); + }); + }); +}); diff --git a/src/shared/api/client.ts b/src/shared/api/client/client.ts similarity index 67% rename from src/shared/api/client.ts rename to src/shared/api/client/client.ts index d936ff3..c3a7dd7 100644 --- a/src/shared/api/client.ts +++ b/src/shared/api/client/client.ts @@ -1,13 +1,10 @@ -import { PBHttpError } from './error'; -import type { ListResponse } from './types'; +import { PBHttpError } from '../error'; +import type { ListResponse } from '../types'; /* * Native fetch wrapper for PocketBase API requests. */ -/* Required in production; falls back to localhost in development. */ -const PB_URL = process.env.PB_URL ?? (process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8090' : undefined); - /** * Options for PocketBase collection fetching. */ @@ -40,12 +37,15 @@ export type PBFetchOptions = { * Fetch a list of records from a PocketBase collection. */ export async function getCollection(collection: string, options: PBFetchOptions = {}): Promise> { - const { sort, filter, expand, tags, revalidate } = options; + /* Required in production; falls back to localhost in development. */ + const pbUrl = process.env.PB_URL ?? (process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8090' : undefined); - if (!PB_URL) { + if (!pbUrl) { throw new Error('PB_URL is required in production'); } + const { sort, filter, expand, tags, revalidate } = options; + const params = new URLSearchParams(); if (sort) { params.set('sort', sort); @@ -57,20 +57,28 @@ export async function getCollection(collection: string, options: PBFetchOptio params.set('expand', expand); } - const url = `${PB_URL}/api/collections/${collection}/records?${params.toString()}`; + const url = `${pbUrl}/api/collections/${collection}/records?${params.toString()}`; - const res = await fetch(url, { - next: { - tags: tags ?? [], - revalidate: revalidate ?? 3600, - }, - }); + try { + const res = await fetch(url, { + next: { + tags: tags ?? [], + revalidate: revalidate ?? 3600, + }, + }); - if (!res.ok) { - throw new PBHttpError(res.status, collection, res.statusText); + if (!res.ok) { + throw new PBHttpError(res.status, collection, res.statusText); + } + + return res.json(); + } catch (err) { + if (err instanceof PBHttpError) { + throw err; + } + console.warn(`[getCollection] "${collection}" unreachable — returning empty list`, err); + return { items: [], page: 1, perPage: 0, totalItems: 0, totalPages: 0 }; } - - return res.json(); } /** diff --git a/src/shared/api/index.ts b/src/shared/api/index.ts index d860d06..6f88229 100644 --- a/src/shared/api/index.ts +++ b/src/shared/api/index.ts @@ -1,2 +1,2 @@ -export * from './client'; +export * from './client/client'; export * from './types';