Address five modeling tensions in one pass: - Encapsulation: the Feed aggregate no longer exposes raw config/metadata (a shallow Readonly still leaked mutable arrays). It now offers intention-revealing accessors that return copies, plus toConfigSnapshot/toMetadataSnapshot for the repository and summary() for the global registry. - feeds:list consistency: FeedRepository.save/saveConfig upsert the registry entry from feed.summary(), so services no longer mirror title/description/ expiry by hand (the old add/updateInList footgun is gone). - domain/feed.ts: drop the dead applySenderPolicy, internalise resolveExpiresAt and trimToByteBudget into the aggregate; feed.ts keeps only the shared isExpired predicate used by the read-model routes. - Single edit path: remove editDetails; edit(patch, deps) is the sole config mutation, with a systematic expired guard. Renaming an expired feed now 403s. - FeedId flows through the application and infrastructure signatures; fromTrusted/parse happen once at the edge, .value only at the serialisation boundaries (urls, feed-generator, feed-keys, logs, JSON). 347 tests green, tsc clean, Worker bundle builds. Co-Authored-By: Claude Opus 4.7 <[email protected]>
51 lines
1.5 KiB
TypeScript
51 lines
1.5 KiB
TypeScript
import { Context } from "hono";
|
|
import { Env } from "../types";
|
|
import { generateAtomFeed } from "../infrastructure/feed-generator";
|
|
import { fetchFeedData } from "../application/feed-fetcher";
|
|
import { baseUrl, feedAtomUrl } from "../infrastructure/urls";
|
|
import { isExpired } from "../domain/feed";
|
|
import { FeedId } from "../domain/value-objects/feed-id";
|
|
|
|
export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
|
|
try {
|
|
const feedId = c.req.param("feedId");
|
|
if (!feedId) {
|
|
return new Response("Feed ID is required", { status: 400 });
|
|
}
|
|
|
|
const feedData = await fetchFeedData(FeedId.fromTrusted(feedId), c.env);
|
|
if (!feedData) {
|
|
return new Response("Feed not found", { status: 404 });
|
|
}
|
|
if (isExpired(feedData.feedConfig)) {
|
|
return new Response("Feed has expired", { status: 410 });
|
|
}
|
|
|
|
const base = baseUrl(c.env);
|
|
const selfUrl = new URL(c.req.url).origin + `/atom/${feedId}`;
|
|
const atomXml = generateAtomFeed(
|
|
feedData.feedConfig,
|
|
feedData.emails,
|
|
base,
|
|
feedId,
|
|
selfUrl,
|
|
);
|
|
const linkHeader = [
|
|
`<${base}/hub>; rel="hub"`,
|
|
`<${feedAtomUrl(feedId, c.env)}>; rel="self"`,
|
|
].join(", ");
|
|
|
|
return new Response(atomXml, {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/atom+xml",
|
|
"Cache-Control": "max-age=1800",
|
|
Link: linkHeader,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("Error generating Atom feed:", error);
|
|
return new Response("Internal Server Error", { status: 500 });
|
|
}
|
|
}
|