using System.Net.Http.Json; using System.Text.Json; using Elternbeirat.Contracts; namespace Elternbeirat.PocketBase; /// /// Reads published content from a PocketBase instance over its REST API. /// /// /// /// There is one method per collection (pages, posts, events, faqs). Each returns /// only records with public = true and leaves filtering and sorting to /// PocketBase via query parameters. /// /// /// Responses are deserialized with the web defaults. Date conversion from UTC /// to Berlin wall-clock time is declared on the contracts themselves via /// [JsonConverter] (e.g. , ), /// so no serializer options are configured here. /// /// /// Register the class as a typed client via AddHttpClient, with /// set to the PocketBase base URL. /// /// /// /// The HTTP client; its must point to PocketBase. /// public sealed class PocketBaseClient(HttpClient httpClient) { /// /// Gets all public pages, ordered by . /// /// /// A token to cancel the request. /// /// /// The public pages, or an empty list if there are none. /// /// /// PocketBase could not be reached, timed out, or returned an unreadable response. /// /// /// was canceled by the caller. /// public Task> GetPagesAsync(CancellationToken token = default) => GetRecordsAsync("pages", "order", token); /// /// Gets all public posts, ordered by , newest first. /// /// /// /// The public posts, or an empty list if there are none. /// /// public Task> GetPostsAsync(CancellationToken token = default) => GetRecordsAsync("posts", "-date", token); /// /// Gets all public events, ordered by , earliest first. /// /// /// /// The public events, or an empty list if there are none. /// /// public Task> GetEventsAsync(CancellationToken token = default) => GetRecordsAsync("events", "start", token); /// /// Gets all public FAQ entries, ordered by , then by /// . /// /// /// The second sort key makes the order total: sorting by topic alone leaves /// entries within a topic in an undefined order, so the same content could /// render differently between requests. Adding the question as a tie-breaker /// keeps the FAQ list and any teaser drawn from it stable. /// /// /// /// The public FAQ entries, or an empty list if there are none. /// /// public Task> GetFaqsAsync(CancellationToken token = default) => GetRecordsAsync("faqs", "topic,question", token); /// /// Gets all public records of a collection in a single request. /// /// /// The record type to deserialize into. /// /// /// The PocketBase collection name, e.g. "posts". /// /// /// The PocketBase sort expression; a leading - sorts descending. /// /// /// A token to cancel the request. /// /// /// The public records, or an empty list if there are none. /// /// /// PocketBase could not be reached, timed out, or returned an unreadable response. /// /// /// was canceled by the caller. /// private async Task> GetRecordsAsync( string collection, string sort, CancellationToken token) { // filter=public=true keeps drafts out; perPage is large enough to fetch // every record in a single page given the small content volume. var url = $"/api/collections/{collection}/records" + $"?perPage=500&filter={Uri.EscapeDataString("public=true")}" + $"&sort={Uri.EscapeDataString(sort)}"; try { var result = await httpClient.GetFromJsonAsync>(url, token); return result?.Items ?? []; } // These three mean PocketBase itself failed: it was unreachable // (HttpRequestException), the request to it timed out (a TaskCanceledException // whose inner exception is a TimeoutException, which is how HttpClient surfaces // its own timeout), or it answered with something we could not read // (JsonException). A plain TaskCanceledException with no inner TimeoutException // is the caller's own cancellation via token and is deliberately not caught -- // it propagates as OperationCanceledException so an aborted request stays an // abort, not a fault. catch (Exception exception) when ( exception is HttpRequestException or JsonException or TaskCanceledException { InnerException: TimeoutException }) { throw new PocketBaseUnavailableException($"Could not read collection '{collection}'.", exception); } } }