refactor(domain): make FeedId circulate through the domain and repository

FeedId is now the type of Feed.id and of every single-feed method on
FeedRepository; callers wrap raw strings via FeedId.fromTrusted at the
repository boundary. String-medium operations (URLs, logs, JSON,
list registry, email keys) stay string. Drop the redundant
generateFeedId wrapper in favour of FeedId.generate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-24 00:44:24 +02:00
parent 05388b45c8
commit ab1c15e69a
18 changed files with 162 additions and 124 deletions
+6 -4
View File
@@ -7,6 +7,7 @@ import {
deleteKeysWithConcurrency,
} from "./helpers";
import { FeedRepository } from "../../domain/feed-repository";
import { FeedId } from "../../domain/value-objects/feed-id";
import { feedRssUrl, feedAtomUrl, feedEmailAddress } from "../../utils/urls";
import { formatBytes } from "../../utils/format";
import { EmailAddress } from "../../domain/value-objects/email-address";
@@ -153,8 +154,9 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
const message = c.req.query("message");
const count = Number(c.req.query("count") || "0");
const feedConfig = await repo.getConfig(feedId);
const feedMetadata = await repo.getMetadata(feedId);
const id = FeedId.fromTrusted(feedId);
const feedConfig = await repo.getConfig(id);
const feedMetadata = await repo.getMetadata(id);
if (!feedConfig || !feedMetadata) {
return c.text("Feed not found", 404);
@@ -650,7 +652,7 @@ emailsRouter.post("/emails/:emailKey/delete", async (c) => {
return c.text("Feed ID is required", 400);
}
const feed = await repo.load(feedId);
const feed = await repo.load(FeedId.fromTrusted(feedId));
await repo.deleteEmail(emailKey);
if (feed) {
@@ -685,7 +687,7 @@ emailsRouter.post("/feeds/:feedId/emails/bulk-delete", async (c) => {
(c.req.header("Accept") || "").includes("application/json");
try {
const feed = await repo.load(feedId);
const feed = await repo.load(FeedId.fromTrusted(feedId));
if (!feed) {
return wantsJson
+7 -3
View File
@@ -10,6 +10,7 @@ import { getAttachmentBucket } from "../../utils/attachments";
import { Layout } from "./ui";
import { purgeFeedKeysStep, collectUnsubscribeUrls } from "./helpers";
import { FeedRepository } from "../../domain/feed-repository";
import { FeedId } from "../../domain/value-objects/feed-id";
import {
createFeedRecord,
editFeed,
@@ -148,7 +149,9 @@ feedsRouter.get("/:feedId/edit", async (c) => {
const env = c.env;
const feedId = c.req.param("feedId");
const feedConfig = await FeedRepository.from(env).getConfig(feedId);
const feedConfig = await FeedRepository.from(env).getConfig(
FeedId.fromTrusted(feedId),
);
if (!feedConfig) {
return c.text("Feed not found", 404);
@@ -359,6 +362,7 @@ feedsRouter.post("/:feedId/edit", async (c) => {
feedsRouter.post("/:feedId/sender-filter", async (c) => {
const env = c.env;
const feedId = c.req.param("feedId");
const id = FeedId.fromTrusted(feedId);
const repo = FeedRepository.from(env);
const body = await c.req.json().catch(() => null);
@@ -370,7 +374,7 @@ feedsRouter.post("/:feedId/sender-filter", async (c) => {
const { action, value } = parsed.data;
const normalized = value.trim().toLowerCase();
const feedConfig = await repo.getConfig(feedId);
const feedConfig = await repo.getConfig(id);
if (!feedConfig) return c.json({ ok: false, error: "Feed not found" }, 404);
const allowedSenders = (feedConfig.allowed_senders || []).map((s) =>
@@ -397,7 +401,7 @@ feedsRouter.post("/:feedId/sender-filter", async (c) => {
if (!targetList.includes(normalized)) {
targetList.push(normalized);
await repo.putConfig(feedId, {
await repo.putConfig(id, {
...feedConfig,
allowed_senders: allowedSenders,
blocked_senders: blockedSenders,
+7 -3
View File
@@ -2,6 +2,7 @@ import { EmailData, EmailMetadata, Env } from "../../types";
import { logger } from "../../lib/logger";
import { getAttachmentBucket } from "../../utils/attachments";
import { FeedRepository } from "../../domain/feed-repository";
import { FeedId } from "../../domain/value-objects/feed-id";
// Delete the R2 attachments belonging to the given email keys. Call before the
// emails are removed from feed metadata, while `emails` still carries their
@@ -60,7 +61,9 @@ export async function collectUnsubscribeUrls(
feedId: string,
): Promise<string[]> {
try {
const metadata = await new FeedRepository(emailStorage).getMetadata(feedId);
const metadata = await new FeedRepository(emailStorage).getMetadata(
FeedId.fromTrusted(feedId),
);
return Object.values(metadata?.unsubscribe ?? {});
} catch (error) {
logger.error("Error reading unsubscribe URLs", {
@@ -82,14 +85,15 @@ export async function purgeFeedKeysStep(
listComplete: boolean;
}> {
const repo = new FeedRepository(emailStorage);
const listed = await repo.listFeedKeys(feedId, {
const id = FeedId.fromTrusted(feedId);
const listed = await repo.listFeedKeys(id, {
cursor: options.cursor,
limit: options.limit,
});
const keys = listed.names;
if (options.bucket && keys.length > 0) {
const emailKeys = keys.filter((k) => repo.isEmailKey(feedId, k));
const emailKeys = keys.filter((k) => repo.isEmailKey(id, k));
if (emailKeys.length > 0) {
const emailDataResults = await Promise.allSettled(
emailKeys.map((k) => repo.getEmail(k)),