mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
refactor: move KV repositories to infrastructure (Track P — points 2, 6c)
Make the domain stop depending on infrastructure ("imports point inward").
- Point 2: relocate the four KV adapters (FeedRepository, IconRepository,
WebSubSubscriptionRepository, CountersRepository) from domain/ to
infrastructure/, where the logger import is legitimate. The domain now keeps
only the pure key schema (feed-keys.ts), the Feed aggregate and value objects;
it imports nothing outward. Deliberately no hand-rolled 24-method port
interface (YAGNI without DI) — relocation alone fixes the direction.
- Point 6c: EmailParser.extractFeedId now returns a validated FeedId value
object instead of a raw string, so the most untrusted input (an inbound
recipient address) is guarded at the parse boundary and no longer round-trips
through FeedId.fromTrusted in the ingest path.
All import paths updated; CLAUDE.md source layout/KV-schema notes updated.
351 tests pass; tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -56,17 +56,14 @@ src/
|
|||||||
index.ts # App entrypoint: CORS, IP middleware, route mounting, email handler export
|
index.ts # App entrypoint: CORS, IP middleware, route mounting, email handler export
|
||||||
config/constants.ts # Shared constants (TTLs, limits)
|
config/constants.ts # Shared constants (TTLs, limits)
|
||||||
types/index.ts # Env, FeedConfig, EmailData, WebSubSubscription, etc.
|
types/index.ts # Env, FeedConfig, EmailData, WebSubSubscription, etc.
|
||||||
domain/ # Framework-agnostic core (no Hono imports leak out)
|
domain/ # Framework-agnostic core (no Hono/infra imports leak out)
|
||||||
feed.aggregate.ts # Feed aggregate: consistency boundary; all config/metadata mutations go through it
|
feed.aggregate.ts # Feed aggregate: consistency boundary; all config/metadata mutations go through it
|
||||||
feed.ts # Pure invariant functions (expiry, sender policy, byte budget) the aggregate delegates to
|
feed.ts # Pure invariant functions (expiry, sender policy, byte budget) the aggregate delegates to
|
||||||
feed-keys.ts # The KV key schema (pure string builders), shared by every repository
|
feed-keys.ts # The KV key schema (pure string builders), shared by every repository
|
||||||
feed-repository.ts # KV access for the Feed aggregate + global feed list + email bodies (load/save)
|
clock.ts # Clock port (systemClock) — injected into the aggregate; no ambient Date.now()
|
||||||
icon-repository.ts # KV access for cached favicons (icon:*)
|
|
||||||
websub-subscription-repository.ts # KV access for WebSub subscriber lists (websub:subs:*)
|
|
||||||
counters-repository.ts # KV access for the monitoring counters singleton (stats:counters)
|
|
||||||
email-parser.ts # Email parsing (addresses, headers, encoded words)
|
email-parser.ts # Email parsing (addresses, headers, encoded words)
|
||||||
format.ts # Pure formatting helpers (formatBytes)
|
format.ts # Pure formatting helpers (formatBytes)
|
||||||
value-objects/ # FeedId, EmailAddress, Domain (immutable, self-validating)
|
value-objects/ # FeedId, EmailAddress, Domain, SenderPolicy (immutable, self-validating)
|
||||||
application/ # Use-cases / orchestration (wires domain + infrastructure)
|
application/ # Use-cases / orchestration (wires domain + infrastructure)
|
||||||
feed-service.ts # createFeedRecord / renameFeed / editFeed / deleteFeedRecord (admin UI + REST API)
|
feed-service.ts # createFeedRecord / renameFeed / editFeed / deleteFeedRecord (admin UI + REST API)
|
||||||
email-processor.ts # Core ingestion: load aggregate → accepts? → feed.ingest → persist
|
email-processor.ts # Core ingestion: load aggregate → accepts? → feed.ingest → persist
|
||||||
@@ -74,6 +71,10 @@ src/
|
|||||||
stats.ts # Monitoring counters increment policy + storage scans
|
stats.ts # Monitoring counters increment policy + storage scans
|
||||||
infrastructure/ # Adapters: KV/R2, outbound HTTP, logging, framework glue
|
infrastructure/ # Adapters: KV/R2, outbound HTTP, logging, framework glue
|
||||||
logger.ts # JSON structured logger
|
logger.ts # JSON structured logger
|
||||||
|
feed-repository.ts # KV adapter for the Feed aggregate + global feed list + email bodies (load/save)
|
||||||
|
icon-repository.ts # KV adapter for cached favicons (icon:*)
|
||||||
|
websub-subscription-repository.ts # KV adapter for WebSub subscriber lists (websub:subs:*)
|
||||||
|
counters-repository.ts # KV adapter for the monitoring counters singleton (stats:counters)
|
||||||
auth.ts # timingSafeEqual, proxy-auth check, API bearer middleware
|
auth.ts # timingSafeEqual, proxy-auth check, API bearer middleware
|
||||||
cloudflare-email.ts # Cloudflare Email routing handler
|
cloudflare-email.ts # Cloudflare Email routing handler
|
||||||
forwardemail.ts # ForwardEmail webhook types/parsing
|
forwardemail.ts # ForwardEmail webhook types/parsing
|
||||||
@@ -130,7 +131,7 @@ All data lives in the `EMAIL_STORAGE` KV namespace:
|
|||||||
| `icon:<domain>` | Cached favicon record (base64 + content type; negative entries allowed) |
|
| `icon:<domain>` | Cached favicon record (base64 + content type; negative entries allowed) |
|
||||||
| `stats:counters` | `Counters` (cumulative monitoring counters singleton) |
|
| `stats:counters` | `Counters` (cumulative monitoring counters singleton) |
|
||||||
|
|
||||||
The KV key schema lives in `src/domain/feed-keys.ts` — never inline a `feed:`/`feeds:list`/`websub:`/`icon:`/`stats:counters` key string anywhere else. KV access is owned by four repositories, each for one concern: `FeedRepository` (the Feed aggregate + global list + email bodies), `IconRepository` (`icon:*`), `WebSubSubscriptionRepository` (`websub:subs:*`), and `CountersRepository` (`stats:counters`). Go through a repository, never `env.EMAIL_STORAGE.get/put` directly.
|
The KV key schema lives in `src/domain/feed-keys.ts` (pure, framework-agnostic) — never inline a `feed:`/`feeds:list`/`websub:`/`icon:`/`stats:counters` key string anywhere else. KV access is owned by four repository **adapters** in `src/infrastructure/`, each for one concern: `FeedRepository` (the Feed aggregate + global list + email bodies), `IconRepository` (`icon:*`), `WebSubSubscriptionRepository` (`websub:subs:*`), and `CountersRepository` (`stats:counters`). Go through a repository, never `env.EMAIL_STORAGE.get/put` directly. The domain depends only on the key schema, not on these adapters.
|
||||||
|
|
||||||
### Domain & layering rules
|
### Domain & layering rules
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import {
|
|||||||
} from "../infrastructure/favicon-fetcher";
|
} from "../infrastructure/favicon-fetcher";
|
||||||
import { parseOneClickUnsubscribe } from "../infrastructure/unsubscribe";
|
import { parseOneClickUnsubscribe } from "../infrastructure/unsubscribe";
|
||||||
import { getAttachmentBucket } from "../infrastructure/attachments";
|
import { getAttachmentBucket } from "../infrastructure/attachments";
|
||||||
import { FeedRepository } from "../domain/feed-repository";
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
||||||
import { Feed } from "../domain/feed.aggregate";
|
import { Feed } from "../domain/feed.aggregate";
|
||||||
import { FeedId } from "../domain/value-objects/feed-id";
|
|
||||||
import { logger } from "../infrastructure/logger";
|
import { logger } from "../infrastructure/logger";
|
||||||
import { FEED_MAX_BYTES } from "../config/constants";
|
import { FEED_MAX_BYTES } from "../config/constants";
|
||||||
|
|
||||||
@@ -85,18 +84,18 @@ async function loadAcceptingFeed(
|
|||||||
return { ok: false, reason: "invalid_address" };
|
return { ok: false, reason: "invalid_address" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const feed = await FeedRepository.from(env).load(FeedId.fromTrusted(feedId));
|
const feed = await FeedRepository.from(env).load(feedId);
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
logger.error("Feed not found", { feedId });
|
logger.error("Feed not found", { feedId: feedId.value });
|
||||||
return { ok: false, reason: "feed_not_found" };
|
return { ok: false, reason: "feed_not_found" };
|
||||||
}
|
}
|
||||||
if (feed.isExpired()) {
|
if (feed.isExpired()) {
|
||||||
logger.warn("Rejected email: feed expired", { feedId });
|
logger.warn("Rejected email: feed expired", { feedId: feedId.value });
|
||||||
return { ok: false, reason: "feed_expired" };
|
return { ok: false, reason: "feed_expired" };
|
||||||
}
|
}
|
||||||
if (feed.accepts(input.senders) === "blocked") {
|
if (feed.accepts(input.senders) === "blocked") {
|
||||||
logger.warn("Rejected email: sender filter", {
|
logger.warn("Rejected email: sender filter", {
|
||||||
feedId,
|
feedId: feedId.value,
|
||||||
senders: input.senders,
|
senders: input.senders,
|
||||||
allowedSenders: feed.config.allowed_senders,
|
allowedSenders: feed.config.allowed_senders,
|
||||||
blockedSenders: feed.config.blocked_senders,
|
blockedSenders: feed.config.blocked_senders,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Env, FeedConfig, EmailData } from "../types";
|
import { Env, FeedConfig, EmailData } from "../types";
|
||||||
import { MAX_FEED_ITEMS } from "../config/constants";
|
import { MAX_FEED_ITEMS } from "../config/constants";
|
||||||
import { FeedRepository } from "../domain/feed-repository";
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
||||||
import { FeedId } from "../domain/value-objects/feed-id";
|
import { FeedId } from "../domain/value-objects/feed-id";
|
||||||
|
|
||||||
export interface FeedData {
|
export interface FeedData {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { bumpCounters } from "../application/stats";
|
|||||||
import { waitUntilSafe } from "../infrastructure/worker";
|
import { waitUntilSafe } from "../infrastructure/worker";
|
||||||
import { sendUnsubscribes } from "../infrastructure/unsubscribe";
|
import { sendUnsubscribes } from "../infrastructure/unsubscribe";
|
||||||
import { getAttachmentBucket } from "../infrastructure/attachments";
|
import { getAttachmentBucket } from "../infrastructure/attachments";
|
||||||
import { FeedRepository } from "../domain/feed-repository";
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
||||||
import { FeedId } from "../domain/value-objects/feed-id";
|
import { FeedId } from "../domain/value-objects/feed-id";
|
||||||
import {
|
import {
|
||||||
Feed,
|
Feed,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Counters, Env, StatsResponse } from "../types";
|
import { Counters, Env, StatsResponse } from "../types";
|
||||||
import { logger } from "../infrastructure/logger";
|
import { logger } from "../infrastructure/logger";
|
||||||
import { FeedRepository } from "../domain/feed-repository";
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
||||||
import { CountersRepository } from "../domain/counters-repository";
|
import { CountersRepository } from "../infrastructure/counters-repository";
|
||||||
import { WebSubSubscriptionRepository } from "../domain/websub-subscription-repository";
|
import { WebSubSubscriptionRepository } from "../infrastructure/websub-subscription-repository";
|
||||||
import { FeedId } from "../domain/value-objects/feed-id";
|
import { FeedId } from "../domain/value-objects/feed-id";
|
||||||
import { getAttachmentBucket } from "../infrastructure/attachments";
|
import { getAttachmentBucket } from "../infrastructure/attachments";
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import { EmailParser } from "./email-parser";
|
|||||||
|
|
||||||
describe("EmailParser.extractFeedId", () => {
|
describe("EmailParser.extractFeedId", () => {
|
||||||
it("extracts a valid feed ID from an email address", () => {
|
it("extracts a valid feed ID from an email address", () => {
|
||||||
expect(EmailParser.extractFeedId("river.castle.42@example.com")).toBe(
|
expect(
|
||||||
"river.castle.42",
|
EmailParser.extractFeedId("river.castle.42@example.com")?.value,
|
||||||
);
|
).toBe("river.castle.42");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is case-insensitive for the local part", () => {
|
it("is case-insensitive for the local part", () => {
|
||||||
expect(EmailParser.extractFeedId("River.Castle.42@example.com")).toBe(
|
expect(
|
||||||
"River.Castle.42",
|
EmailParser.extractFeedId("River.Castle.42@example.com")?.value,
|
||||||
);
|
).toBe("River.Castle.42");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null for an address with no feed ID format", () => {
|
it("returns null for an address with no feed ID format", () => {
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { EmailData } from "../types";
|
import { EmailData } from "../types";
|
||||||
import { FeedId } from "../domain/value-objects/feed-id";
|
import { FeedId } from "./value-objects/feed-id";
|
||||||
|
|
||||||
export class EmailParser {
|
export class EmailParser {
|
||||||
// Matches noun1.noun2.XY (the feed ID format) before the @ symbol
|
/**
|
||||||
static extractFeedId(emailAddress: string): string | null {
|
* Extract the feed id from an inbound recipient address. Returns a validated
|
||||||
return FeedId.parse(emailAddress)?.value ?? null;
|
* `FeedId` value object (not a raw string) so the most untrusted input in the
|
||||||
|
* system — an address typed by a sender — is guarded at the parse boundary and
|
||||||
|
* never needs `FeedId.fromTrusted` downstream.
|
||||||
|
*/
|
||||||
|
static extractFeedId(emailAddress: string): FeedId | null {
|
||||||
|
return FeedId.parse(emailAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { createMockEnv } from "../test/setup";
|
import { createMockEnv } from "../test/setup";
|
||||||
import { Feed, CreateFeedInput } from "./feed.aggregate";
|
import { Feed, CreateFeedInput } from "./feed.aggregate";
|
||||||
import { FeedRepository } from "./feed-repository";
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
||||||
import { FeedId } from "./value-objects/feed-id";
|
import { FeedId } from "./value-objects/feed-id";
|
||||||
import { Clock } from "./clock";
|
import { Clock } from "./clock";
|
||||||
import type { Env, EmailMetadata } from "../types";
|
import type { Env, EmailMetadata } from "../types";
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import { apiApp } from "./routes/api";
|
|||||||
import { handleCloudflareEmail } from "./infrastructure/cloudflare-email";
|
import { handleCloudflareEmail } from "./infrastructure/cloudflare-email";
|
||||||
import { Env } from "./types";
|
import { Env } from "./types";
|
||||||
import { logger } from "./infrastructure/logger";
|
import { logger } from "./infrastructure/logger";
|
||||||
import { FeedRepository } from "./domain/feed-repository";
|
import { FeedRepository } from "./infrastructure/feed-repository";
|
||||||
import { purgeExpiredFeeds } from "./routes/admin/helpers";
|
import { purgeExpiredFeeds } from "./routes/admin/helpers";
|
||||||
import {
|
import {
|
||||||
bumpCounters,
|
bumpCounters,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Counters, Env } from "../types";
|
import { Counters, Env } from "../types";
|
||||||
import { STATS_KEY } from "./feed-keys";
|
import { STATS_KEY } from "../domain/feed-keys";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KV access for the monitoring counters singleton (`stats:counters`). The
|
* KV access for the monitoring counters singleton (`stats:counters`). The
|
||||||
@@ -4,9 +4,9 @@ import {
|
|||||||
ICON_TTL_SECONDS,
|
ICON_TTL_SECONDS,
|
||||||
MAX_ICON_BYTES,
|
MAX_ICON_BYTES,
|
||||||
} from "../config/constants";
|
} from "../config/constants";
|
||||||
import { IconRepository } from "../domain/icon-repository";
|
import { IconRepository } from "./icon-repository";
|
||||||
import { EmailAddress } from "../domain/value-objects/email-address";
|
import { EmailAddress } from "../domain/value-objects/email-address";
|
||||||
import { logger } from "../infrastructure/logger";
|
import { logger } from "./logger";
|
||||||
|
|
||||||
interface IconRecord {
|
interface IconRecord {
|
||||||
data: string | null; // base64 icon bytes, or null for a negative cache entry
|
data: string | null; // base64 icon bytes, or null for a negative cache entry
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { createMockEnv } from "../test/setup";
|
import { createMockEnv } from "../test/setup";
|
||||||
import { FeedRepository } from "./feed-repository";
|
import { FeedRepository } from "./feed-repository";
|
||||||
import { FeedId } from "./value-objects/feed-id";
|
import { FeedId } from "../domain/value-objects/feed-id";
|
||||||
import type { Env, FeedConfig, FeedMetadata, EmailData } from "../types";
|
import type { Env, FeedConfig, FeedMetadata, EmailData } from "../types";
|
||||||
|
|
||||||
const mockEnv = () => createMockEnv() as unknown as Env;
|
const mockEnv = () => createMockEnv() as unknown as Env;
|
||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
FeedMetadata,
|
FeedMetadata,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { FEEDS_LIST_KEY } from "../config/constants";
|
import { FEEDS_LIST_KEY } from "../config/constants";
|
||||||
import { feedKeys } from "./feed-keys";
|
import { feedKeys } from "../domain/feed-keys";
|
||||||
import { Feed } from "./feed.aggregate";
|
import { Feed } from "../domain/feed.aggregate";
|
||||||
import { FeedId } from "./value-objects/feed-id";
|
import { FeedId } from "../domain/value-objects/feed-id";
|
||||||
import { logger } from "../infrastructure/logger";
|
import { logger } from "./logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single source of truth for KV access to the Feed aggregate. The key schema
|
* Single source of truth for KV access to the Feed aggregate. The key schema
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Env } from "../types";
|
import { Env } from "../types";
|
||||||
import { feedKeys } from "./feed-keys";
|
import { feedKeys } from "../domain/feed-keys";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KV access for cached per-domain favicons (`icon:<domain>`). Entries may be
|
* KV access for cached per-domain favicons (`icon:<domain>`). Entries may be
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { Env, WebSubSubscription } from "../types";
|
import { Env, WebSubSubscription } from "../types";
|
||||||
import { feedKeys } from "./feed-keys";
|
import { feedKeys } from "../domain/feed-keys";
|
||||||
import { logger } from "../infrastructure/logger";
|
import { logger } from "./logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KV access for per-feed WebSub subscriber lists (`websub:subs:<feedId>`).
|
* KV access for per-feed WebSub subscriber lists (`websub:subs:<feedId>`).
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Env, FeedConfig, EmailData, WebSubSubscription } from "../types";
|
import { Env, FeedConfig, EmailData, WebSubSubscription } from "../types";
|
||||||
import { generateRssFeed, generateAtomFeed } from "./feed-generator";
|
import { generateRssFeed, generateAtomFeed } from "./feed-generator";
|
||||||
import { baseUrl, feedRssUrl, feedAtomUrl, feedUrl } from "./urls";
|
import { baseUrl, feedRssUrl, feedAtomUrl, feedUrl } from "./urls";
|
||||||
import { FeedRepository } from "../domain/feed-repository";
|
import { FeedRepository } from "./feed-repository";
|
||||||
import { WebSubSubscriptionRepository } from "../domain/websub-subscription-repository";
|
import { WebSubSubscriptionRepository } from "./websub-subscription-repository";
|
||||||
import { FeedId } from "../domain/value-objects/feed-id";
|
import { FeedId } from "../domain/value-objects/feed-id";
|
||||||
|
|
||||||
export async function getSubscriptions(
|
export async function getSubscriptions(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ADMIN_COOKIE_MAX_AGE } from "../config/constants";
|
|||||||
import { logger } from "../infrastructure/logger";
|
import { logger } from "../infrastructure/logger";
|
||||||
import { timingSafeEqual, checkProxyAuth } from "../infrastructure/auth";
|
import { timingSafeEqual, checkProxyAuth } from "../infrastructure/auth";
|
||||||
import { Layout, clampText } from "./admin/ui";
|
import { Layout, clampText } from "./admin/ui";
|
||||||
import { FeedRepository } from "../domain/feed-repository";
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
||||||
import { renameFeed } from "../application/feed-service";
|
import { renameFeed } from "../application/feed-service";
|
||||||
import {
|
import {
|
||||||
feedRssUrl,
|
feedRssUrl,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
deleteAttachmentsForEmails,
|
deleteAttachmentsForEmails,
|
||||||
deleteKeysWithConcurrency,
|
deleteKeysWithConcurrency,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
import { FeedRepository } from "../../domain/feed-repository";
|
import { FeedRepository } from "../../infrastructure/feed-repository";
|
||||||
import { FeedId } from "../../domain/value-objects/feed-id";
|
import { FeedId } from "../../domain/value-objects/feed-id";
|
||||||
import {
|
import {
|
||||||
feedRssUrl,
|
feedRssUrl,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { sendUnsubscribes } from "../../infrastructure/unsubscribe";
|
|||||||
import { getAttachmentBucket } from "../../infrastructure/attachments";
|
import { getAttachmentBucket } from "../../infrastructure/attachments";
|
||||||
import { Layout } from "./ui";
|
import { Layout } from "./ui";
|
||||||
import { purgeFeedKeysStep, collectUnsubscribeUrls } from "./helpers";
|
import { purgeFeedKeysStep, collectUnsubscribeUrls } from "./helpers";
|
||||||
import { FeedRepository } from "../../domain/feed-repository";
|
import { FeedRepository } from "../../infrastructure/feed-repository";
|
||||||
import { FeedId } from "../../domain/value-objects/feed-id";
|
import { FeedId } from "../../domain/value-objects/feed-id";
|
||||||
import {
|
import {
|
||||||
createFeedRecord,
|
createFeedRecord,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { EmailData, EmailMetadata, Env } from "../../types";
|
import { EmailData, EmailMetadata, Env } from "../../types";
|
||||||
import { logger } from "../../infrastructure/logger";
|
import { logger } from "../../infrastructure/logger";
|
||||||
import { getAttachmentBucket } from "../../infrastructure/attachments";
|
import { getAttachmentBucket } from "../../infrastructure/attachments";
|
||||||
import { FeedRepository } from "../../domain/feed-repository";
|
import { FeedRepository } from "../../infrastructure/feed-repository";
|
||||||
import { FeedId } from "../../domain/value-objects/feed-id";
|
import { FeedId } from "../../domain/value-objects/feed-id";
|
||||||
|
|
||||||
// Delete the R2 attachments belonging to the given email keys. Call before the
|
// Delete the R2 attachments belonging to the given email keys. Call before the
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
deleteFeedRecord,
|
deleteFeedRecord,
|
||||||
} from "../../application/feed-service";
|
} from "../../application/feed-service";
|
||||||
import { deleteAttachmentsForEmails } from "../admin/helpers";
|
import { deleteAttachmentsForEmails } from "../admin/helpers";
|
||||||
import { FeedRepository } from "../../domain/feed-repository";
|
import { FeedRepository } from "../../infrastructure/feed-repository";
|
||||||
import { FeedId } from "../../domain/value-objects/feed-id";
|
import { FeedId } from "../../domain/value-objects/feed-id";
|
||||||
import { getStats } from "../../application/stats";
|
import { getStats } from "../../application/stats";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { html, raw } from "hono/html";
|
|||||||
import { Env } from "../types";
|
import { Env } from "../types";
|
||||||
import { processEmailContent } from "../infrastructure/html-processor";
|
import { processEmailContent } from "../infrastructure/html-processor";
|
||||||
import { formatBytes } from "../domain/format";
|
import { formatBytes } from "../domain/format";
|
||||||
import { FeedRepository } from "../domain/feed-repository";
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
||||||
import { FeedId } from "../domain/value-objects/feed-id";
|
import { FeedId } from "../domain/value-objects/feed-id";
|
||||||
import { isExpired } from "../domain/feed";
|
import { isExpired } from "../domain/feed";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Context } from "hono";
|
import { Context } from "hono";
|
||||||
import { Env } from "../types";
|
import { Env } from "../types";
|
||||||
import { FeedRepository } from "../domain/feed-repository";
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
||||||
import { FeedId } from "../domain/value-objects/feed-id";
|
import { FeedId } from "../domain/value-objects/feed-id";
|
||||||
import {
|
import {
|
||||||
cacheFaviconForDomain,
|
cacheFaviconForDomain,
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
|||||||
import { waitUntilSafe } from "../infrastructure/worker";
|
import { waitUntilSafe } from "../infrastructure/worker";
|
||||||
import { DEFAULT_LEASE_SECONDS, MAX_LEASE_SECONDS } from "../config/constants";
|
import { DEFAULT_LEASE_SECONDS, MAX_LEASE_SECONDS } from "../config/constants";
|
||||||
import { feedTopicPattern } from "../infrastructure/urls";
|
import { feedTopicPattern } from "../infrastructure/urls";
|
||||||
import { FeedRepository } from "../domain/feed-repository";
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
||||||
import { FeedId } from "../domain/value-objects/feed-id";
|
import { FeedId } from "../domain/value-objects/feed-id";
|
||||||
|
|
||||||
type AppEnv = { Bindings: Env };
|
type AppEnv = { Bindings: Env };
|
||||||
|
|||||||
Reference in New Issue
Block a user