Files
Elternbeirat/Elternbeirat.PocketBase/PocketBaseClient.cs
T

135 lines
5.1 KiB
C#

using System.Net.Http.Json;
using System.Text.Json;
using Elternbeirat.Contracts;
namespace Elternbeirat.PocketBase;
/// <summary>
/// Reads published content from a PocketBase instance over its REST API.
/// </summary>
/// <remarks>
/// <para>
/// There is one method per collection (pages, posts, events, faqs). Each returns
/// only records with <c>public = true</c> and leaves filtering and sorting to
/// PocketBase via query parameters.
/// </para>
/// <para>
/// Register the class as a typed client via <c>AddHttpClient</c>, with
/// <see cref="HttpClient.BaseAddress"/> set to the PocketBase base URL.
/// </para>
/// </remarks>
/// <param name="http">
/// The HTTP client; its <see cref="HttpClient.BaseAddress"/> must point to PocketBase.
/// </param>
public sealed class PocketBaseClient(HttpClient http)
{
/// <summary>
/// Serializer options that map PocketBase UTC dates to Berlin wall-clock time.
/// </summary>
private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
/// <summary>
/// Gets all public pages, ordered by <see cref="Page.Order"/>.
/// </summary>
/// <param name="ct">
/// A token to cancel the request.
/// </param>
/// <returns>
/// The public pages, or an empty list if there are none.
/// </returns>
/// <exception cref="HttpRequestException">
/// PocketBase could not be reached or returned a non-success status code.
/// </exception>
/// <exception cref="JsonException">
/// The response could not be deserialized.
/// </exception>
/// <exception cref="OperationCanceledException">
/// <paramref name="ct"/> was canceled or the request timed out.
/// </exception>
public Task<IReadOnlyList<Page>> GetPagesAsync(CancellationToken ct = default)
=> GetRecordsAsync<Page>("pages", "order", ct);
/// <summary>
/// Gets all public posts, ordered by <see cref="Post.Date"/>, newest first.
/// </summary>
/// <inheritdoc cref="GetPagesAsync" path="/param"/>
/// <returns>
/// The public posts, or an empty list if there are none.
/// </returns>
/// <inheritdoc cref="GetPagesAsync" path="/exception"/>
public Task<IReadOnlyList<Post>> GetPostsAsync(CancellationToken ct = default)
=> GetRecordsAsync<Post>("posts", "-date", ct);
/// <summary>
/// Gets all public events, ordered by <see cref="Event.Start"/>, earliest first.
/// </summary>
/// <inheritdoc cref="GetPagesAsync" path="/param"/>
/// <returns>
/// The public events, or an empty list if there are none.
/// </returns>
/// <inheritdoc cref="GetPagesAsync" path="/exception"/>
public Task<IReadOnlyList<Event>> GetEventsAsync(CancellationToken ct = default)
=> GetRecordsAsync<Event>("events", "start", ct);
/// <summary>
/// Gets all public FAQ entries, ordered by <see cref="Faq.Topic"/>.
/// </summary>
/// <inheritdoc cref="GetPagesAsync" path="/param"/>
/// <returns>
/// The public FAQ entries, or an empty list if there are none.
/// </returns>
/// <inheritdoc cref="GetPagesAsync" path="/exception"/>
public Task<IReadOnlyList<Faq>> GetFaqsAsync(CancellationToken ct = default)
=> GetRecordsAsync<Faq>("faqs", "topic", ct);
/// <summary>
/// Creates the serializer options with both wall-clock date converters registered.
/// </summary>
/// <returns>
/// The configured options.
/// </returns>
private static JsonSerializerOptions CreateJsonOptions()
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
options.Converters.Add(new LocalDateTimeConverter());
options.Converters.Add(new NullableLocalDateTimeConverter());
return options;
}
/// <summary>
/// Gets all public records of a collection in a single request.
/// </summary>
/// <typeparam name="T">
/// The record type to deserialize into.
/// </typeparam>
/// <param name="collection">
/// The PocketBase collection name, e.g. <c>"posts"</c>.
/// </param>
/// <param name="sort">
/// The PocketBase sort expression; a leading <c>-</c> sorts descending.
/// </param>
/// <param name="ct">
/// A token to cancel the request.
/// </param>
/// <returns>
/// The public records, or an empty list if there are none.
/// </returns>
private async Task<IReadOnlyList<T>> GetRecordsAsync<T>(
string collection,
string sort,
CancellationToken ct)
{
// 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)}";
var result = await http.GetFromJsonAsync<RecordList<T>>(url, JsonOptions, ct);
return result?.Items ?? [];
}
}