refactor(domain): introduce FeedRepository as the single KV access layer
Centralise the KV key schema and all get/put access behind a FeedRepository class under src/domain/. Every feed/email/list/icon/websub/counter key was previously inlined across ~12 modules with two divergent storeEmail and addFeedToList implementations; the dead src/utils/storage.ts write path is removed and the email key convention unified on feed:<id>:<ts>. Behaviour-preserving: existing tests pass unchanged in logic, plus a new feed-repository.test.ts covering CRUD, key builders, list ops and counters. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
a0eaebe749
commit
2b3f00f7e3
22 files changed
+616
-539
No files matched your search
+10
-30
@@ -1,25 +1,13 @@
|
||||
import {
|
||||
Env,
|
||||
FeedConfig,
|
||||
FeedMetadata,
|
||||
EmailData,
|
||||
WebSubSubscription,
|
||||
} from "../types";
|
||||
import { Env, FeedConfig, EmailData, WebSubSubscription } from "../types";
|
||||
import { generateRssFeed, generateAtomFeed } from "./feed-generator";
|
||||
import { baseUrl, feedRssUrl, feedAtomUrl, feedUrl } from "./urls";
|
||||
|
||||
const KV_PREFIX = "websub:subs:";
|
||||
|
||||
export function subscriptionKey(feedId: string): string {
|
||||
return `${KV_PREFIX}${feedId}`;
|
||||
}
|
||||
import { FeedRepository } from "../domain/feed-repository";
|
||||
|
||||
export async function getSubscriptions(
|
||||
feedId: string,
|
||||
env: Env,
|
||||
): Promise<WebSubSubscription[]> {
|
||||
const raw = await env.EMAIL_STORAGE.get(subscriptionKey(feedId), "json");
|
||||
return (raw as WebSubSubscription[] | null) ?? [];
|
||||
return FeedRepository.from(env).getSubscriptions(feedId);
|
||||
}
|
||||
|
||||
export async function saveSubscriptions(
|
||||
@@ -27,10 +15,7 @@ export async function saveSubscriptions(
|
||||
subscriptions: WebSubSubscription[],
|
||||
env: Env,
|
||||
): Promise<void> {
|
||||
await env.EMAIL_STORAGE.put(
|
||||
subscriptionKey(feedId),
|
||||
JSON.stringify(subscriptions),
|
||||
);
|
||||
await FeedRepository.from(env).saveSubscriptions(feedId, subscriptions);
|
||||
}
|
||||
|
||||
export async function buildHmacSignature(
|
||||
@@ -60,16 +45,16 @@ async function buildFeedXml(
|
||||
env: Env,
|
||||
format: "rss" | "atom" = "rss",
|
||||
): Promise<string | null> {
|
||||
const [rawMetadata, rawConfig] = await Promise.all([
|
||||
env.EMAIL_STORAGE.get(`feed:${feedId}:metadata`, "json"),
|
||||
env.EMAIL_STORAGE.get(`feed:${feedId}:config`, "json"),
|
||||
const repo = FeedRepository.from(env);
|
||||
const [feedMetadata, rawConfig] = await Promise.all([
|
||||
repo.getMetadata(feedId),
|
||||
repo.getConfig(feedId),
|
||||
]);
|
||||
|
||||
const feedMetadata = rawMetadata as FeedMetadata | null;
|
||||
if (!feedMetadata) return null;
|
||||
|
||||
const base = baseUrl(env);
|
||||
const feedConfig = (rawConfig as FeedConfig | null) ?? {
|
||||
const feedConfig: FeedConfig = rawConfig ?? {
|
||||
title: `Newsletter Feed ${feedId}`,
|
||||
description: "Converted email newsletter",
|
||||
language: "en",
|
||||
@@ -78,12 +63,7 @@ async function buildFeedXml(
|
||||
|
||||
const emails = feedMetadata.emails.slice(0, 20);
|
||||
const emailsData = (
|
||||
await Promise.all(
|
||||
emails.map(
|
||||
(m) =>
|
||||
env.EMAIL_STORAGE.get(m.key, "json") as Promise<EmailData | null>,
|
||||
),
|
||||
)
|
||||
await Promise.all(emails.map((m) => repo.getEmail(m.key)))
|
||||
).filter((d): d is EmailData => d !== null);
|
||||
|
||||
if (format === "atom") {
|
||||
|
||||
Reference in new issue
Block a user