Features/visual improvements #7

Merged
ilia merged 12 commits from features/visual-improvements into main 2026-05-21 15:16:17 +00:00
3 changed files with 67 additions and 19 deletions
Showing only changes of commit 0697e9ad72 - Show all commits
+40
View File
@@ -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);
});
});
});
@@ -1,13 +1,10 @@
import { PBHttpError } from './error'; import { PBHttpError } from '../error';
import type { ListResponse } from './types'; import type { ListResponse } from '../types';
/* /*
* Native fetch wrapper for PocketBase API requests. * 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. * Options for PocketBase collection fetching.
*/ */
@@ -40,12 +37,15 @@ export type PBFetchOptions = {
* Fetch a list of records from a PocketBase collection. * Fetch a list of records from a PocketBase collection.
*/ */
export async function getCollection<T>(collection: string, options: PBFetchOptions = {}): Promise<ListResponse<T>> { export async function getCollection<T>(collection: string, options: PBFetchOptions = {}): Promise<ListResponse<T>> {
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'); throw new Error('PB_URL is required in production');
} }
const { sort, filter, expand, tags, revalidate } = options;
const params = new URLSearchParams(); const params = new URLSearchParams();
if (sort) { if (sort) {
params.set('sort', sort); params.set('sort', sort);
@@ -57,8 +57,9 @@ export async function getCollection<T>(collection: string, options: PBFetchOptio
params.set('expand', expand); params.set('expand', expand);
} }
const url = `${PB_URL}/api/collections/${collection}/records?${params.toString()}`; const url = `${pbUrl}/api/collections/${collection}/records?${params.toString()}`;
try {
const res = await fetch(url, { const res = await fetch(url, {
next: { next: {
tags: tags ?? [], tags: tags ?? [],
@@ -71,6 +72,13 @@ export async function getCollection<T>(collection: string, options: PBFetchOptio
} }
return res.json(); 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 };
}
} }
/** /**
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './client'; export * from './client/client';
export * from './types'; export * from './types';