mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat(infra): project pendingConfirmation into feeds:list
saveMetadata now also upserts the list entry so the pendingConfirmation flag is reflected in the dashboard without an extra per-feed KV read. toListItemDTO gains an optional third parameter for the flag. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ describe("feed-mapper", () => {
|
|||||||
description: "desc",
|
description: "desc",
|
||||||
mailbox_id: "a.b.42",
|
mailbox_id: "a.b.42",
|
||||||
expires_at: 3000,
|
expires_at: 3000,
|
||||||
|
pendingConfirmation: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,12 +43,17 @@ export function toConfigDTO(state: FeedState): FeedConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Domain state → the projection cached in the global `feeds:list` registry. */
|
/** Domain state → the projection cached in the global `feeds:list` registry. */
|
||||||
export function toListItemDTO(id: FeedId, state: FeedState): FeedListItem {
|
export function toListItemDTO(
|
||||||
|
id: FeedId,
|
||||||
|
state: FeedState,
|
||||||
|
pendingConfirmation = false,
|
||||||
|
): FeedListItem {
|
||||||
return {
|
return {
|
||||||
id: id.value,
|
id: id.value,
|
||||||
title: state.title,
|
title: state.title,
|
||||||
description: state.description,
|
description: state.description,
|
||||||
mailbox_id: state.mailboxId,
|
mailbox_id: state.mailboxId,
|
||||||
expires_at: state.expiresAt,
|
expires_at: state.expiresAt,
|
||||||
|
...(pendingConfirmation !== undefined ? { pendingConfirmation } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,3 +214,68 @@ describe("FeedRepository feed list", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("FeedRepository pendingConfirmation projection", () => {
|
||||||
|
function makeFeed(): Feed {
|
||||||
|
return Feed.create(
|
||||||
|
FeedId.generate(),
|
||||||
|
{
|
||||||
|
title: "T",
|
||||||
|
description: "",
|
||||||
|
language: "en",
|
||||||
|
allowedSenders: [],
|
||||||
|
blockedSenders: [],
|
||||||
|
},
|
||||||
|
{ mailboxId: MailboxId.unchecked("alpha.beta.11") },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("saveMetadata projects pendingConfirmation into feeds:list", async () => {
|
||||||
|
const repo = new FeedRepository(mockEnv().EMAIL_STORAGE);
|
||||||
|
const feed = makeFeed();
|
||||||
|
await repo.save(feed);
|
||||||
|
|
||||||
|
feed.ingest(
|
||||||
|
{
|
||||||
|
key: "k1",
|
||||||
|
subject: "s",
|
||||||
|
receivedAt: Date.now(),
|
||||||
|
size: 10,
|
||||||
|
confirmation: { links: ["https://x/confirm"] },
|
||||||
|
},
|
||||||
|
{ maxBytes: 1_000_000 },
|
||||||
|
);
|
||||||
|
await repo.saveMetadata(feed);
|
||||||
|
|
||||||
|
const list = await repo.listFeeds();
|
||||||
|
const entry = list.find((f) => f.id === feed.id.value);
|
||||||
|
expect(entry?.pendingConfirmation).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saveMetadata clears the projected flag after dismiss", async () => {
|
||||||
|
const repo = new FeedRepository(mockEnv().EMAIL_STORAGE);
|
||||||
|
const feed = makeFeed();
|
||||||
|
feed.ingest(
|
||||||
|
{
|
||||||
|
key: "k1",
|
||||||
|
subject: "s",
|
||||||
|
receivedAt: Date.now(),
|
||||||
|
size: 10,
|
||||||
|
confirmation: { links: ["https://x/confirm"] },
|
||||||
|
},
|
||||||
|
{ maxBytes: 1_000_000 },
|
||||||
|
);
|
||||||
|
await repo.save(feed);
|
||||||
|
expect(
|
||||||
|
(await repo.listFeeds()).find((f) => f.id === feed.id.value)
|
||||||
|
?.pendingConfirmation,
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
feed.dismissConfirmation();
|
||||||
|
await repo.saveMetadata(feed);
|
||||||
|
expect(
|
||||||
|
(await repo.listFeeds()).find((f) => f.id === feed.id.value)
|
||||||
|
?.pendingConfirmation,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -87,18 +87,26 @@ export class FeedRepository {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.putConfig(feed.id, toConfigDTO(feed.state())),
|
this.putConfig(feed.id, toConfigDTO(feed.state())),
|
||||||
this.putMetadata(feed.id, feed.toMetadataSnapshot()),
|
this.putMetadata(feed.id, feed.toMetadataSnapshot()),
|
||||||
this.upsertListEntry(toListItemDTO(feed.id, feed.state())),
|
this.upsertListEntry(
|
||||||
|
toListItemDTO(feed.id, feed.state(), feed.pendingConfirmation),
|
||||||
|
),
|
||||||
this.putInboundIndex(feed.mailboxId, feed.id),
|
this.putInboundIndex(feed.mailboxId, feed.id),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist only the email index. Used by the ingest/delete paths where config
|
* Persist only the email index. Used by the ingest/delete paths where config
|
||||||
* is unchanged — avoids a redundant config write on the hot path. The list
|
* is unchanged — avoids a redundant config write on the hot path. Also
|
||||||
* projection (title/description/expiry) is untouched, so it is not rewritten.
|
* refreshes the `feeds:list` entry's `pendingConfirmation` projection so the
|
||||||
|
* dashboard reflects the latest flag state with a single subsequent KV read.
|
||||||
*/
|
*/
|
||||||
async saveMetadata(feed: Feed): Promise<void> {
|
async saveMetadata(feed: Feed): Promise<void> {
|
||||||
await this.putMetadata(feed.id, feed.toMetadataSnapshot());
|
await Promise.all([
|
||||||
|
this.putMetadata(feed.id, feed.toMetadataSnapshot()),
|
||||||
|
this.upsertListEntry(
|
||||||
|
toListItemDTO(feed.id, feed.state(), feed.pendingConfirmation),
|
||||||
|
),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,7 +117,9 @@ export class FeedRepository {
|
|||||||
async saveConfig(feed: Feed): Promise<void> {
|
async saveConfig(feed: Feed): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.putConfig(feed.id, toConfigDTO(feed.state())),
|
this.putConfig(feed.id, toConfigDTO(feed.state())),
|
||||||
this.upsertListEntry(toListItemDTO(feed.id, feed.state())),
|
this.upsertListEntry(
|
||||||
|
toListItemDTO(feed.id, feed.state(), feed.pendingConfirmation),
|
||||||
|
),
|
||||||
this.putInboundIndex(feed.mailboxId, feed.id),
|
this.putInboundIndex(feed.mailboxId, feed.id),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user