mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
docs(claude): document the DDD layering, Feed aggregate and repo split
Refresh the source layout for domain/application/infrastructure, replace the single-repository rule with the four-repository split + feed-keys, and add domain rules: the Feed aggregate is the only writer of config + the email index, and FeedId circulates through domain and repository. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -56,10 +56,35 @@ 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/KV/R2 imports leak out)
|
domain/ # Framework-agnostic core (no Hono imports leak out)
|
||||||
feed-repository.ts # Single KV access layer: owns the key schema + all get/put
|
feed.aggregate.ts # Feed aggregate: consistency boundary; all config/metadata mutations go through it
|
||||||
feed.ts # Feed aggregate invariants (expiry, sender policy, size budget)
|
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-repository.ts # KV access for the Feed aggregate + global feed list + email bodies (load/save)
|
||||||
|
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)
|
||||||
|
format.ts # Pure formatting helpers (formatBytes)
|
||||||
value-objects/ # FeedId, EmailAddress, Domain (immutable, self-validating)
|
value-objects/ # FeedId, EmailAddress, Domain (immutable, self-validating)
|
||||||
|
application/ # Use-cases / orchestration (wires domain + infrastructure)
|
||||||
|
feed-service.ts # createFeedRecord / renameFeed / editFeed / deleteFeedRecord (admin UI + REST API)
|
||||||
|
email-processor.ts # Core ingestion: load aggregate → accepts? → feed.ingest → persist
|
||||||
|
feed-fetcher.ts # Read model for RSS/Atom rendering (config + email bodies; bypasses the aggregate)
|
||||||
|
stats.ts # Monitoring counters increment policy + storage scans
|
||||||
|
infrastructure/ # Adapters: KV/R2, outbound HTTP, logging, framework glue
|
||||||
|
logger.ts # JSON structured logger
|
||||||
|
auth.ts # timingSafeEqual, proxy-auth check, API bearer middleware
|
||||||
|
cloudflare-email.ts # Cloudflare Email routing handler
|
||||||
|
forwardemail.ts # ForwardEmail webhook types/parsing
|
||||||
|
worker.ts # Typed worker / waitUntil helper
|
||||||
|
attachments.ts # R2 bucket accessor
|
||||||
|
favicon-fetcher.ts # Outbound favicon fetch + cache (uses IconRepository)
|
||||||
|
feed-generator.ts # RSS/Atom XML generation
|
||||||
|
html-processor.ts # Email HTML sanitization / inline cid: rewriting
|
||||||
|
websub.ts # WebSub subscription management + delivery
|
||||||
|
unsubscribe.ts # RFC 8058 one-click unsubscribe dispatch
|
||||||
|
urls.ts # URL builders
|
||||||
routes/
|
routes/
|
||||||
inbound.ts # ForwardEmail webhook handler
|
inbound.ts # ForwardEmail webhook handler
|
||||||
rss.ts # RSS feed renderer
|
rss.ts # RSS feed renderer
|
||||||
@@ -77,19 +102,6 @@ src/
|
|||||||
api/ # Versioned REST API (@hono/zod-openapi)
|
api/ # Versioned REST API (@hono/zod-openapi)
|
||||||
index.ts # OpenAPIHono app: /v1 routes + /openapi.json + /docs
|
index.ts # OpenAPIHono app: /v1 routes + /openapi.json + /docs
|
||||||
schemas.ts # Zod schemas (validation + OpenAPI source of truth)
|
schemas.ts # Zod schemas (validation + OpenAPI source of truth)
|
||||||
lib/
|
|
||||||
auth.ts # timingSafeEqual, proxy-auth check, API bearer middleware
|
|
||||||
feed-service.ts # Shared feed create/update/delete (admin UI + REST API)
|
|
||||||
cloudflare-email.ts # Cloudflare Email routing handler
|
|
||||||
email-parser.ts # Email parsing (mailparser)
|
|
||||||
email-processor.ts # Core ingestion logic (parse → store)
|
|
||||||
feed-fetcher.ts # KV feed/email fetch helpers
|
|
||||||
feed-generator.ts # RSS/Atom XML generation
|
|
||||||
forwardemail.ts # ForwardEmail webhook types/parsing
|
|
||||||
id-generator.ts # Feed/entry ID generation
|
|
||||||
logger.ts # JSON structured logger
|
|
||||||
websub.ts # WebSub subscription management
|
|
||||||
worker.ts # Typed worker export helper
|
|
||||||
scripts/
|
scripts/
|
||||||
client/ # TypeScript client scripts (compiled by esbuild)
|
client/ # TypeScript client scripts (compiled by esbuild)
|
||||||
dashboard.ts # Admin dashboard interactions
|
dashboard.ts # Admin dashboard interactions
|
||||||
@@ -110,7 +122,7 @@ All data lives in the `EMAIL_STORAGE` KV namespace:
|
|||||||
|
|
||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --------------------------- | ------------------------------------------------------------------------ |
|
| --------------------------- | ------------------------------------------------------------------------ |
|
||||||
| `feeds:list` | `{ feeds: Array<{ id, title, description? }> }` |
|
| `feeds:list` | `{ feeds: Array<{ id, title, description?, expires_at? }> }` |
|
||||||
| `feed:<feedId>:config` | `FeedConfig` |
|
| `feed:<feedId>:config` | `FeedConfig` |
|
||||||
| `feed:<feedId>:metadata` | `{ emails: Array<{ key, subject, receivedAt, size?, attachmentIds? }> }` |
|
| `feed:<feedId>:metadata` | `{ emails: Array<{ key, subject, receivedAt, size?, attachmentIds? }> }` |
|
||||||
| `feed:<feedId>:<timestamp>` | Full `EmailData` |
|
| `feed:<feedId>:<timestamp>` | Full `EmailData` |
|
||||||
@@ -118,7 +130,15 @@ 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) |
|
||||||
|
|
||||||
`src/domain/feed-repository.ts` (`FeedRepository`) owns the KV key schema and all get/put access — go through it; never inline `feed:`/`feeds:list`/`websub:`/`icon:`/`stats:counters` key strings elsewhere.
|
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.
|
||||||
|
|
||||||
|
### Domain & layering rules
|
||||||
|
|
||||||
|
- **Layers**: `domain/` is framework-agnostic (no Hono). `application/` orchestrates use-cases. `infrastructure/` holds adapters (KV/R2, HTTP, logging). `routes/` is the HTTP edge. Imports point inward: routes → application → domain; infrastructure implements ports the inner layers call.
|
||||||
|
- **The `Feed` aggregate is the only writer of feed config + the email index.** Load it with `FeedRepository.load(feedId)`, mutate via its methods (`ingest`, `removeEmails`, `rename`, `edit`), then persist with `save`/`saveMetadata`/`saveConfig`. No route or service mutates `metadata.emails` directly. Email **bodies** are large blobs outside the aggregate — flush them (`putEmail`/`deleteEmail`) alongside the metadata save.
|
||||||
|
- Read-only RSS/Atom rendering uses the `feed-fetcher` read model, not the aggregate (no invariant to enforce on the hot path).
|
||||||
|
- KV has no multi-key transaction; the aggregate is the seam a future Durable Object would wrap to serialise concurrent ingests (see `email-processor.ts`).
|
||||||
|
- **`FeedId`** is the type used by the domain (`Feed.id`) and every single-feed `FeedRepository` method. Wrap a raw id string with `FeedId.fromTrusted(value)` at the call site; keep `.value` (string) for URLs, logs, JSON and the feed-list registry. Mint new ids with `FeedId.generate()`.
|
||||||
|
|
||||||
### Worker bindings (`Env`)
|
### Worker bindings (`Env`)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user