mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-21 06:13:48 +00:00
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:
@@ -815,6 +815,10 @@ describe("Admin Routes", () => {
|
||||
"feeds:list",
|
||||
JSON.stringify({ feeds: [{ id: feedId, title: "F" }] }),
|
||||
);
|
||||
await r2Env.EMAIL_STORAGE.put(
|
||||
`feed:${feedId}:config`,
|
||||
JSON.stringify({ title: "F", language: "en", created_at: 1 }),
|
||||
);
|
||||
const emailKey = `feed:${feedId}:1`;
|
||||
await r2Env.EMAIL_STORAGE.put(
|
||||
emailKey,
|
||||
|
||||
+14
-25
@@ -650,18 +650,13 @@ emailsRouter.post("/emails/:emailKey/delete", async (c) => {
|
||||
return c.text("Feed ID is required", 400);
|
||||
}
|
||||
|
||||
const feedMetadata = await repo.getMetadata(feedId);
|
||||
const feed = await repo.load(feedId);
|
||||
|
||||
await repo.deleteEmail(emailKey);
|
||||
await deleteAttachmentsForEmails(env, feedMetadata?.emails ?? [], [
|
||||
emailKey,
|
||||
]);
|
||||
|
||||
if (feedMetadata) {
|
||||
feedMetadata.emails = feedMetadata.emails.filter(
|
||||
(email) => email.key !== emailKey,
|
||||
);
|
||||
await repo.putMetadata(feedId, feedMetadata);
|
||||
if (feed) {
|
||||
const { removed } = feed.removeEmails([emailKey]);
|
||||
await deleteAttachmentsForEmails(env, removed, [emailKey]);
|
||||
await repo.saveMetadata(feed);
|
||||
}
|
||||
|
||||
if (wantsJson) return c.json({ ok: true, emailKey, feedId });
|
||||
@@ -690,15 +685,15 @@ emailsRouter.post("/feeds/:feedId/emails/bulk-delete", async (c) => {
|
||||
(c.req.header("Accept") || "").includes("application/json");
|
||||
|
||||
try {
|
||||
const feedMetadata = await repo.getMetadata(feedId);
|
||||
const feed = await repo.load(feedId);
|
||||
|
||||
if (!feedMetadata) {
|
||||
if (!feed) {
|
||||
return wantsJson
|
||||
? c.json({ ok: false, error: "Feed not found" }, 404)
|
||||
: c.text("Feed not found", 404);
|
||||
}
|
||||
|
||||
const allowedKeys = new Set(feedMetadata.emails.map((email) => email.key));
|
||||
const allowedKeys = new Set(feed.metadata.emails.map((email) => email.key));
|
||||
|
||||
if (wantsJson) {
|
||||
const body = (await c.req.json().catch(() => null)) as {
|
||||
@@ -728,13 +723,10 @@ emailsRouter.post("/feeds/:feedId/emails/bulk-delete", async (c) => {
|
||||
|
||||
const { ok: deletedOk, failed: failedEmailKeys } =
|
||||
await deleteKeysWithConcurrency(emailStorage, candidates, 35);
|
||||
await deleteAttachmentsForEmails(env, feedMetadata.emails, candidates);
|
||||
await deleteAttachmentsForEmails(env, feed.metadata.emails, candidates);
|
||||
|
||||
const deletedSet = new Set(deletedOk);
|
||||
feedMetadata.emails = feedMetadata.emails.filter(
|
||||
(email) => !deletedSet.has(email.key),
|
||||
);
|
||||
await repo.putMetadata(feedId, feedMetadata);
|
||||
feed.removeEmails(deletedOk);
|
||||
await repo.saveMetadata(feed);
|
||||
|
||||
return c.json({
|
||||
ok: failedEmailKeys.length === 0,
|
||||
@@ -759,13 +751,10 @@ emailsRouter.post("/feeds/:feedId/emails/bulk-delete", async (c) => {
|
||||
candidates,
|
||||
35,
|
||||
);
|
||||
await deleteAttachmentsForEmails(env, feedMetadata.emails, candidates);
|
||||
await deleteAttachmentsForEmails(env, feed.metadata.emails, candidates);
|
||||
|
||||
const deletedSet = new Set(deletedOk);
|
||||
feedMetadata.emails = feedMetadata.emails.filter(
|
||||
(email) => !deletedSet.has(email.key),
|
||||
);
|
||||
await repo.putMetadata(feedId, feedMetadata);
|
||||
feed.removeEmails(deletedOk);
|
||||
await repo.saveMetadata(feed);
|
||||
|
||||
return c.redirect(
|
||||
`/admin/feeds/${feedId}/emails?message=bulkDeleted&count=${deletedOk.length}`,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user