Implemented phase 1 for the POC prototype in the Website service.

This commit is contained in:
javier committed 2026-09-21 11:16:52 +02:00
1 parent 6d7ab4bcba
commit ea89ea892a
37 files changed
+4173 -43

No files matched your search

@@ -11,8 +11,8 @@ import WebsiteLibrary
/// Reads the log level, server name, static files location, minimum response size to compress, and security headers from the configuration, then assembles
/// the router, server configuration, and logger. It warns when the localization catalog cannot be read, since pages would serve raw localization keys.
/// It also builds the persistence driver, registers its migrations, and attaches the `Fluent` service so it starts
/// and stops alongside the HTTP server; the ephemeral in-memory backend is migrated on startup, while a PostgreSQL backend is migrated out of
/// band (so a shared database is never migrated on boot).
/// and stops alongside the HTTP server; the ephemeral in-memory backend is migrated here, before the landing page reads its listings, while a
/// PostgreSQL backend is migrated out of band (so a shared database is never migrated on boot).
/// - Parameter reader: the configuration reader the values are read from.
/// - Returns: the configured application, ready to run as a service.
/// - Throws: a ``ConfigError`` when a `database.*` key holds an unrecognized token, or an error when the persistence service cannot be built
@@ -46,6 +46,15 @@ func application(
await prepareDB(for: fluent)
// The in-memory backend is recreated on every launch, so it is migrated — and seeded — before the landing page
// reads its listings. The PostgreSQL backend is left untouched: a shared database is migrated out of band to avoid
// multi-instance races.
if case .inMemory = driver {
try await fluent.migrate()
}
let repository = CatalogueRepository(fluent: fluent)
var app = Application(
router: router(
staticFilesPath: reader.staticFilesPath,
@@ -60,7 +69,12 @@ func application(
// building both against an empty host.
siteOrigin: reader.siteOrigin.isEmpty ? nil : reader.siteOrigin,
logLevel: reader.logLevel,
probe: Probe(fluent: fluent)
probe: Probe(fluent: fluent),
repository: repository,
listings: await homeListings(
from: repository,
logger: logger
)
),
configuration: ApplicationConfiguration(
reader: reader.scoped(to: "http")
@@ -70,14 +84,6 @@ func application(
app.addServices(fluent)
// The in-memory backend is recreated on every launch, so it is migrated on startup. The PostgreSQL backend is
// left untouched here: a shared database is migrated out of band to avoid multi-instance races.
if case .inMemory = driver {
app.beforeServerStarts {
try await fluent.migrate()
}
}
return app
}
@@ -121,6 +127,26 @@ func migration(
/// The request context type the application serves its routes with.
private typealias AppRequestContext = WebsiteRequestContext
/// Reads the listings the landing page shows, once: the page is rendered at startup and cached.
///
/// A catalogue that cannot be read — a PostgreSQL backend not migrated yet — leaves the page without its rows rather than failing the launch.
/// - Parameters:
/// - repository: the repository the listings are read through.
/// - logger: the logger the failure is reported to.
/// - Returns: the landing page's listings, or none when the catalogue cannot be read.
private func homeListings(
from repository: CatalogueRepository,
logger: Logger
) async -> HomeListings {
do {
return .init(from: try await repository.listings())
} catch {
logger.warning("Catalogue is unreadable; the landing page will show no listings: \(error)")
return .init()
}
}
/// Builds the application's logger.
/// - Parameters:
/// - serverName: the label applied to the logger.
@@ -156,6 +182,8 @@ private func logger(
/// - siteOrigin: the public origin the pages derive their canonical URLs and language alternates from, or `nil` to omit them.
/// - logLevel: the level the request-logging middleware logs at.
/// - probe: the probe consulted by the `HealthController` readiness route.
/// - repository: the repository the `CatalogueController` reads the shops and listings through.
/// - listings: the listings the landing page shows in its rows.
/// - Returns: the configured router.
private func router(
staticFilesPath: String,
@@ -168,7 +196,9 @@ private func router(
securityHeaders: SecurityHeadersMiddleware<AppRequestContext>.Configuration,
siteOrigin: String?,
logLevel: Logger.Level,
probe: Probe
probe: Probe,
repository: CatalogueRepository,
listings: HomeListings
) -> Router<AppRequestContext> {
// HEAD siblings are generated for every GET route, so uptime monitors and crawlers probing with HEAD requests get
// the page's status and headers instead of a 404.
@@ -204,6 +234,12 @@ private func router(
RootController<AppRequestContext>(
assetVersion: assetVersion,
siteOrigin: siteOrigin,
analytics: analytics,
listings: listings
)
CatalogueController<AppRequestContext>(
repository: repository,
assetVersion: assetVersion,
analytics: analytics
)
HealthController<AppRequestContext>(