feat(projection): persist the read projection and expose webhook + read APIs (refs #7)
Add the projection persistence and the two services around it:
- Projection.ReadModel: a shared EF Core (Npgsql) read model owning the projection
schema — register_projection + the subscriber's processed_notifications log — plus
EfProjectionStore / EfNotificationLog (atomic record-or-skip on the PK for idempotency)
and the initial migration. One rebuildable store, written by the subscriber and read
by projection-api (ADR-0008).
- EventSubscriber.Api: POST /notifications NRC callback (enforces the abonnement bearer,
401 without it per ADR-0007), POST /admin/rebuild, /health. Migrates on start.
- ProjectionApi.Api: GET /register, GET /register/{id}, /health — the read side.
dotnet-ef pinned as a local tool for migrations; NuGetAuditMode=direct so EF's
design-time-only tooling transitive doesn't flag the shipped build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
services/event-subscriber/EventSubscriber.Api/Program.cs
Normal file
56
services/event-subscriber/EventSubscriber.Api/Program.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using EventSubscriber.Application;
|
||||
using Projection.ReadModel;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("Projection")
|
||||
?? throw new InvalidOperationException("Missing connection string 'ConnectionStrings:Projection'");
|
||||
// The exact Authorization header value Open Notificaties sends on each abonnement callback.
|
||||
// NRC probes the callback during registration and refuses it unless it returns 401 without
|
||||
// this value (ADR-0007), so the webhook enforces it.
|
||||
var webhookToken = builder.Configuration["EventSubscriber:Webhook:AuthToken"]
|
||||
?? throw new InvalidOperationException("Missing configuration 'EventSubscriber:Webhook:AuthToken'");
|
||||
|
||||
builder.Services.AddProjectionReadModel(connectionString);
|
||||
builder.Services.AddProjectionWriteSide();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Apply migrations on start so a fresh stack reaches a usable schema unattended.
|
||||
await app.Services.MigrateProjectionAsync();
|
||||
|
||||
app.MapGet("/health", () => "Healthy");
|
||||
|
||||
// The NRC abonnement callback. Open Notificaties POSTs a notification here; we project it.
|
||||
// Auth-on-callback is mandatory: without the configured Authorization, return 401 (NRC's
|
||||
// registration probe depends on this — ADR-0007).
|
||||
app.MapPost("/notifications", async (
|
||||
HttpRequest request,
|
||||
NotificationDto body,
|
||||
NotificationProjector projector,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (request.Headers.Authorization != webhookToken)
|
||||
return Results.Unauthorized();
|
||||
|
||||
await projector.HandleAsync(body.ToNotification(), ct);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
// Admin: rebuild the projection from the durable notification log (PRD §8.4 — rebuildable).
|
||||
app.MapPost("/admin/rebuild", async (NotificationProjector projector, CancellationToken ct) =>
|
||||
{
|
||||
await projector.RebuildAsync(ct);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
/// <summary>The NRC notification body, as Open Notificaties POSTs it. Only the fields the
|
||||
/// projection needs are bound; <c>aanmaakdatum</c>/<c>kenmerken</c> are ignored for the minimal slice.</summary>
|
||||
public sealed record NotificationDto(string Kanaal, string Resource, string Actie, Uri ResourceUrl)
|
||||
{
|
||||
public Notification ToNotification() => new(Kanaal, Resource, Actie, ResourceUrl);
|
||||
}
|
||||
|
||||
public partial class Program;
|
||||
Reference in New Issue
Block a user