mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-21 06:13:48 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
|
||||
+12
-6
@@ -10,6 +10,7 @@ import {
|
||||
} from "../../lib/feed-service";
|
||||
import { deleteAttachmentsForEmails } from "../admin/helpers";
|
||||
import { FeedRepository } from "../../domain/feed-repository";
|
||||
import { FeedId } from "../../domain/value-objects/feed-id";
|
||||
import { getStats } from "../../utils/stats";
|
||||
import { feedEmailAddress, feedRssUrl, feedAtomUrl } from "../../utils/urls";
|
||||
import {
|
||||
@@ -170,9 +171,10 @@ apiApp.openapi(
|
||||
const env = c.env;
|
||||
const { feedId } = c.req.valid("param");
|
||||
const repo = FeedRepository.from(env);
|
||||
const config = await repo.getConfig(feedId);
|
||||
const id = FeedId.fromTrusted(feedId);
|
||||
const config = await repo.getConfig(id);
|
||||
if (!config) return c.json({ error: "Feed not found" }, 404);
|
||||
const metadata = await repo.getMetadata(feedId);
|
||||
const metadata = await repo.getMetadata(id);
|
||||
return c.json(
|
||||
toFeed(feedId, config, metadata?.emails.length ?? 0, env),
|
||||
200,
|
||||
@@ -215,7 +217,9 @@ apiApp.openapi(
|
||||
return c.json({ error: "Feed not found" }, 404);
|
||||
if (result.status === "expired")
|
||||
return c.json({ error: "Feed has expired and cannot be modified" }, 409);
|
||||
const metadata = await FeedRepository.from(env).getMetadata(feedId);
|
||||
const metadata = await FeedRepository.from(env).getMetadata(
|
||||
FeedId.fromTrusted(feedId),
|
||||
);
|
||||
return c.json(
|
||||
toFeed(feedId, result.config, metadata?.emails.length ?? 0, env),
|
||||
200,
|
||||
@@ -265,7 +269,9 @@ apiApp.openapi(
|
||||
async (c) => {
|
||||
const env = c.env;
|
||||
const { feedId } = c.req.valid("param");
|
||||
const metadata = await FeedRepository.from(env).getMetadata(feedId);
|
||||
const metadata = await FeedRepository.from(env).getMetadata(
|
||||
FeedId.fromTrusted(feedId),
|
||||
);
|
||||
if (!metadata) return c.json({ error: "Feed not found" }, 404);
|
||||
return c.json(
|
||||
{
|
||||
@@ -301,7 +307,7 @@ apiApp.openapi(
|
||||
const { feedId, entryId } = c.req.valid("param");
|
||||
const receivedAt = parseInt(entryId, 10);
|
||||
const repo = FeedRepository.from(env);
|
||||
const metadata = await repo.getMetadata(feedId);
|
||||
const metadata = await repo.getMetadata(FeedId.fromTrusted(feedId));
|
||||
const metaEntry = metadata?.emails.find((e) => e.receivedAt === receivedAt);
|
||||
if (!metaEntry) return c.json({ error: "Email not found" }, 404);
|
||||
const data = await repo.getEmail(metaEntry.key);
|
||||
@@ -345,7 +351,7 @@ apiApp.openapi(
|
||||
const repo = FeedRepository.from(env);
|
||||
const { feedId, entryId } = c.req.valid("param");
|
||||
const receivedAt = parseInt(entryId, 10);
|
||||
const feed = await repo.load(feedId);
|
||||
const feed = await repo.load(FeedId.fromTrusted(feedId));
|
||||
const metaEntry = feed?.metadata.emails.find(
|
||||
(e) => e.receivedAt === receivedAt,
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Env } from "../types";
|
||||
import { processEmailContent } from "../utils/html-processor";
|
||||
import { formatBytes } from "../utils/format";
|
||||
import { FeedRepository } from "../domain/feed-repository";
|
||||
import { FeedId } from "../domain/value-objects/feed-id";
|
||||
import { isExpired } from "../domain/feed";
|
||||
|
||||
export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
|
||||
@@ -15,10 +16,11 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
|
||||
}
|
||||
|
||||
const repo = FeedRepository.from(c.env);
|
||||
const id = FeedId.fromTrusted(feedId);
|
||||
|
||||
const [feedMetadata, feedConfig] = await Promise.all([
|
||||
repo.getMetadata(feedId),
|
||||
repo.getConfig(feedId),
|
||||
repo.getMetadata(id),
|
||||
repo.getConfig(id),
|
||||
]);
|
||||
if (!feedMetadata) {
|
||||
return new Response("Feed not found", { status: 404 });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Context } from "hono";
|
||||
import { Env } from "../types";
|
||||
import { FeedRepository } from "../domain/feed-repository";
|
||||
import { FeedId } from "../domain/value-objects/feed-id";
|
||||
import { cacheFaviconForDomain, getCachedIcon } from "../utils/favicon-fetcher";
|
||||
|
||||
export const FAVICON_PATH = "/favicon.svg";
|
||||
@@ -40,7 +41,9 @@ export async function handleFeedFavicon(
|
||||
const feedId = c.req.param("feedId");
|
||||
if (!feedId) return projectFavicon();
|
||||
|
||||
const metadata = await FeedRepository.from(env).getMetadata(feedId);
|
||||
const metadata = await FeedRepository.from(env).getMetadata(
|
||||
FeedId.fromTrusted(feedId),
|
||||
);
|
||||
const domain = metadata?.iconDomain;
|
||||
if (!domain) return projectFavicon();
|
||||
|
||||
|
||||
+4
-1
@@ -8,6 +8,7 @@ import { waitUntilSafe } from "../utils/worker";
|
||||
import { DEFAULT_LEASE_SECONDS, MAX_LEASE_SECONDS } from "../config/constants";
|
||||
import { feedTopicPattern } from "../utils/urls";
|
||||
import { FeedRepository } from "../domain/feed-repository";
|
||||
import { FeedId } from "../domain/value-objects/feed-id";
|
||||
|
||||
type AppEnv = { Bindings: Env };
|
||||
|
||||
@@ -74,7 +75,9 @@ hubRouter.post("/", async (c) => {
|
||||
const feedId = match[2];
|
||||
|
||||
// Verify the feed exists before accepting any subscription
|
||||
const feedConfig = await FeedRepository.from(env).getConfig(feedId);
|
||||
const feedConfig = await FeedRepository.from(env).getConfig(
|
||||
FeedId.fromTrusted(feedId),
|
||||
);
|
||||
if (!feedConfig) {
|
||||
return c.text("Not Found: feed does not exist", 404);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user