refactor(domain): introduce the Feed aggregate as the write-path API

Add a Feed aggregate class owning config + the email index, with create,
ingest, removeEmails, isExpired and accepts delegating to the existing
pure invariant functions. FeedRepository gains load/save/saveMetadata
that reconstitute and persist the aggregate.

All write paths now go through it: createFeedRecord (Feed.create),
email ingestion (feed.ingest), and every email deletion in the admin UI
and REST API (feed.removeEmails) — no route mutates metadata.emails
directly anymore. KV key strings unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-24 00:33:14 +02:00
parent a31ff42f59
commit c45f6677fe
8 changed files with 415 additions and 131 deletions
+8 -7
View File
@@ -345,15 +345,16 @@ apiApp.openapi(
const repo = FeedRepository.from(env);
const { feedId, entryId } = c.req.valid("param");
const receivedAt = parseInt(entryId, 10);
const metadata = await repo.getMetadata(feedId);
const metaEntry = metadata?.emails.find((e) => e.receivedAt === receivedAt);
if (!metadata || !metaEntry)
return c.json({ error: "Email not found" }, 404);
const feed = await repo.load(feedId);
const metaEntry = feed?.metadata.emails.find(
(e) => e.receivedAt === receivedAt,
);
if (!feed || !metaEntry) return c.json({ error: "Email not found" }, 404);
await repo.deleteEmail(metaEntry.key);
await deleteAttachmentsForEmails(env, metadata.emails, [metaEntry.key]);
metadata.emails = metadata.emails.filter((e) => e.key !== metaEntry.key);
await repo.putMetadata(feedId, metadata);
const { removed } = feed.removeEmails([metaEntry.key]);
await deleteAttachmentsForEmails(env, removed, [metaEntry.key]);
await repo.saveMetadata(feed);
return c.json({ ok: true }, 200);
},