Files
ccn/Packages/Persistence/README.md
T
javierandClaude Opus 5.5 911424de24 Project updates from Template
This commit contains the latest updates from the generic Website template, which move the toolchain to Swift 6.4, gate analytics and structured data behind traits, and rework page routing:

- Updated the Swift toolchain for all the package dependencies and the Website service to version 6.4. (345b305)
- Adapted all the existing middlewares in the Infrastructure package to Swift 6.4. (26008df)
- Moved test resource teardown in the Persistence and the Website library into defer blocks. (27984a9)
- Gated analytics behind a trait and reworked structured data in the Infrastructure package. (c7d4769)
- Enabled the Analytics trait in the Website service. (f6a6bbd)
- Reworked page routing in the Website service. (d1cb2c1)
- Gated the structured data features in the Infrastructure package behind a trait. (f0a8791)
- Enabled the StructuredData trait of the Infrastructure package in the Website service. (bbfdeb3)
- Fixed the asset pin and a build warning in the Dockerfile of the Website service. (6ed0f9b)

The root README change in 345b305 was left out, as this project no longer carries that file. From d1cb2c1, the RootController rework was left out, as the prototype serves a session-aware landing page and routes its other pages through their own controllers; the view-event reporter was appended to the prototype's shared script instead.

The prototype code was adapted to the stricter Swift 6.4 settings: `String(format:)` was swapped for safe formatting, the NoIndex middleware takes a `@concurrent` next handler, and the tests flagged by warnings-as-errors were tightened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:55:43 +01:00

6.9 KiB

Persistence

The Fluent-based data layer the platform's services build on: runtime selection between a PostgreSQL backend and an ephemeral in-memory SQLite one, single-place migration registration, and a database readiness probe.

Overview

Role Types
Backend selection Driver (postgres or inMemory), Configuration (the PostgreSQL connection parameters), TLS (the connection's TLS posture)
Service Service, which builds the Fluent service configured for the chosen driver
Migrations PrepareDB, the single registrar declaring every migration, in order
Readiness Probe, which reports whether the default database answers a SELECT 1 within a deadline
Accounts AccountRepository, which finds an account by email or creates it, returning a User snapshot, and keeps each account's Passkeys — one is only ever registered together with a new account; CreateUser seeds the prototype's demo accounts
Collecting CollectingRepository, which keeps an account's Wants with the listings meeting each, its collection of owned issues, the Series catalogue, and the upcoming Events with the shops trading at each
Trade TradeRepository, which reads and writes Conversation, Offer, and Sale snapshots and enforces who may do what, throwing a TradeError otherwise; Sale carries the five-step handshake's rules as pure functions
Catalogue CatalogueRepository, which opens a shop from a ShopDraft, publishes a seller's ListingDraft under a slug of its own, and returns Shop and Listing snapshots narrowed and ordered by a ListingFilter; SeedCatalogue fills it with the prototype's demo data

Design rules

  • The package reads no configuration. The executable maps its database.* keys onto a Driver and hands it over; connection values arrive as plain data. The Website service's ConfigReader+Properties has the mapping.
  • One default database. Service registers the selected backend as the default database, so repositories resolve it with a plain fluent.db() and stay agnostic of which driver is in use.
  • Migrations are declared once, and append-only. PrepareDB registers every migration in the order it must run; alter the schema by adding a migration, never by editing one that has already run. Registering does not apply them: the in-memory backend is migrated at launch, while a shared PostgreSQL database is migrated out of band (the executable's migrate-and-exit mode), so booting instances never race.
  • Models never cross a concurrency boundary. FluentKit models are mutable reference types, so they stay internal to the package and repositories return Sendable value-type snapshots (e.g. Listing) instead.
  • Registration order is not creation order. AddTradeColumns, AddListingDeclarations, and AddShopRegistration are registered ahead of SeedCatalogue, whose models write their columns; Fluent tracks migrations by name, so a database already past the seed still gains them.
  • Prototype shortcuts. CatalogueRepository filters in memory, the catalogue fields (series, issue, publisher) are folded into the listing, and the demo data ships as a migration. All three go once real data arrives.
  • Readiness never throws, and never hangs. Probe runs a schema-independent SELECT 1, maps every failure to false, and races the query against a deadline (2 seconds by default) — a hanging database yields a prompt "not ready" instead of a stalled endpoint.
  • A single connection for the in-memory store. The SQLite backend is capped at one connection per event loop, so every query reaches the same in-memory database instead of each pooled connection getting a private one.
  • Method structs. Service, PrepareDB, and Probe hold their lifetime-fixed configuration in init and take only per-call inputs in callAsFunction.

Note: the driver itself enforces both TLS postures — prefer upgrades only when the server advertises TLS and continues in plaintext otherwise, require refuses a server that offers none. Tests pin both against a fake plaintext-only server.

Layout

Sources are split by visibility, then by kind, one type per file:

Sources/
├── Public/
│   ├── Enumerations/      Driver, TLS, ShopError, TradeError
│   ├── Methods/           Service, PrepareDB, Probe
│   ├── Models/            Shop, ShopDraft, Listing, ListingDraft, ListingFilter, User, Passkey, Want, Series, Event, Conversation, Offer, Sale
│   ├── Repositories/      CatalogueRepository, AccountRepository, CollectingRepository, TradeRepository
│   └── Types/             Configuration
└── Internal/
    ├── Migrations/        CreateShop, CreateListing, AddTradeColumns, AddListingDeclarations, AddShopRegistration, SeedCatalogue, CreateUser, CreateTrade, CreatePasskey, CreateCollecting, CreateCommunity
    ├── Models/            ShopRecord, ListingRecord, UserRecord, PasskeyRecord, WantRecord, SeriesRecord, EventRecord, EventShopRecord, OwnedIssueRecord, ConversationRecord, MessageRecord, OfferRecord, SaleRecord
    └── Seeds/             Seed
Tests/
├── Cases/                 the test suites, mirroring the Sources/ layout
└── Utils/                 the NotSQL* fakes backing the probe's non-SQL-database case, the
                           plaintext-only and silent fake PostgreSQL servers, and the suite Tag constants

Testing

The suite runs against the in-memory backend by default, so swift test needs no database. The PostgreSQL integration test is skipped unless POSTGRES_TEST_HOST points at one (with optional POSTGRES_TEST_PORT, POSTGRES_TEST_NAME, POSTGRES_TEST_USERNAME, and POSTGRES_TEST_PASSWORD); it reverts its migrations afterwards, leaving a shared database as it was found:

swift test                               # in-memory only
POSTGRES_TEST_HOST=127.0.0.1 swift test  # against the local PostgreSQL (make db-mount)

Outside the application's service group, a built Fluent service must be shut down explicitly — even on failure — or its connection pool asserts on deinit; the suites' do/catch around fluent.shutdown() is the shape to follow.

Every suite carries a tag for the kind of API it exercises — .enumeration or .method, declared in Tests/Utils/Extensions/Tag+Constants.swift — so test plans and summaries can slice a run by kind. A new suite adopts the tag matching its subject, or adds one when none fits.

Requirements

  • Swift 6.4 toolchain (swift-tools-version:6.4).
  • macOS 15, matching the sibling packages (the services deploy to Linux containers; the packages carry no UI platforms).
  • Package dependencies: hummingbird-fluent, fluent-postgres-driver, fluent-sqlite-driver, sql-kit, postgres-nio, and swift-nio-ssl; the test target additionally depends on swift-nio for the TLS fallback tests' fake server.