mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat(api): expose nativeFeeds on the REST Feed schema (read-only)
GET /v1/feeds/{id} and PATCH /v1/feeds/{id} now include a required
nativeFeeds array (possibly empty) derived from the feed metadata via
unionNativeFeeds. POST /v1/feeds always returns [].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@ import { Hono } from "hono";
|
||||
import { apiApp } from "./index";
|
||||
import { createMockEnv } from "../../test/setup";
|
||||
import { Env } from "../../types";
|
||||
import { FeedRepository } from "../../infrastructure/feed-repository";
|
||||
import { FeedId } from "../../domain/value-objects/feed-id";
|
||||
|
||||
const PASSWORD = "test-password";
|
||||
const authHeaders = { Authorization: `Bearer ${PASSWORD}` };
|
||||
@@ -212,6 +214,65 @@ describe("REST API (/api/v1)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("nativeFeeds field", () => {
|
||||
it("returns nativeFeeds as empty array for a brand-new feed", async () => {
|
||||
const feedId = await createFeed("Native Feed Test");
|
||||
const res = await request(`/api/v1/feeds/${feedId}`, {
|
||||
headers: authHeaders,
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { nativeFeeds: unknown };
|
||||
expect(body.nativeFeeds).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns nativeFeeds populated when the feed metadata has native feeds", async () => {
|
||||
const feedId = await createFeed("Native Feed With Data");
|
||||
const id = FeedId.unchecked(feedId);
|
||||
const repo = FeedRepository.from(mockEnv);
|
||||
const feed = await repo.load(id);
|
||||
expect(feed).not.toBeNull();
|
||||
const receivedAt = Date.now();
|
||||
feed!.ingest(
|
||||
{
|
||||
key: `feed:${feedId}:email:${receivedAt}`,
|
||||
subject: "Newsletter",
|
||||
receivedAt,
|
||||
},
|
||||
{
|
||||
maxBytes: 1e9,
|
||||
nativeFeeds: {
|
||||
senderKey: "author@blog.example.com",
|
||||
feeds: [{ url: "https://blog.example.com/feed.xml", type: "rss" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
await repo.save(feed!);
|
||||
|
||||
const res = await request(`/api/v1/feeds/${feedId}`, {
|
||||
headers: authHeaders,
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
nativeFeeds: { url: string; type: string }[];
|
||||
};
|
||||
expect(body.nativeFeeds).toEqual([
|
||||
{ url: "https://blog.example.com/feed.xml", type: "rss" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("PATCH response also includes nativeFeeds", async () => {
|
||||
const feedId = await createFeed("Patch Native Feed Test");
|
||||
const res = await request(`/api/v1/feeds/${feedId}`, {
|
||||
method: "PATCH",
|
||||
headers: { ...authHeaders, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: "Updated Title" }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { nativeFeeds: unknown };
|
||||
expect(body.nativeFeeds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Emails", () => {
|
||||
it("lists, reads and deletes an email", async () => {
|
||||
const feedId = await createFeed();
|
||||
|
||||
+19
-4
@@ -1,7 +1,8 @@
|
||||
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
||||
import { cors } from "hono/cors";
|
||||
import { Scalar } from "@scalar/hono-api-reference";
|
||||
import { Env, FeedConfig } from "../../types";
|
||||
import { Env, FeedConfig, NativeFeed } from "../../types";
|
||||
import { unionNativeFeeds } from "../../domain/native-feed";
|
||||
import { apiAuthMiddleware } from "../../infrastructure/auth";
|
||||
import {
|
||||
createFeedRecord,
|
||||
@@ -51,6 +52,7 @@ function toFeed(
|
||||
config: FeedConfig,
|
||||
emailCount: number,
|
||||
env: Env,
|
||||
nativeFeeds: NativeFeed[],
|
||||
): z.infer<typeof FeedSchema> {
|
||||
return {
|
||||
id,
|
||||
@@ -67,6 +69,7 @@ function toFeed(
|
||||
emailAddress: feedEmailAddress(config.mailbox_id, env),
|
||||
rssUrl: feedRssUrl(id, env),
|
||||
atomUrl: feedAtomUrl(id, env),
|
||||
nativeFeeds,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -156,7 +159,7 @@ apiApp.openapi(
|
||||
senderInTitle: body.senderInTitle,
|
||||
lifetimeHours: body.lifetimeHours,
|
||||
});
|
||||
return c.json(toFeed(feedId, config, 0, env), 201);
|
||||
return c.json(toFeed(feedId, config, 0, env, []), 201);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -183,7 +186,13 @@ apiApp.openapi(
|
||||
if (!config) return c.json({ error: "Feed not found" }, 404);
|
||||
const metadata = await repo.getMetadata(id);
|
||||
return c.json(
|
||||
toFeed(feedId, config, metadata?.emails.length ?? 0, env),
|
||||
toFeed(
|
||||
feedId,
|
||||
config,
|
||||
metadata?.emails.length ?? 0,
|
||||
env,
|
||||
unionNativeFeeds(metadata?.nativeFeeds),
|
||||
),
|
||||
200,
|
||||
);
|
||||
},
|
||||
@@ -228,7 +237,13 @@ apiApp.openapi(
|
||||
return c.json({ error: "Feed has expired and cannot be modified" }, 409);
|
||||
const metadata = await FeedRepository.from(env).getMetadata(id);
|
||||
return c.json(
|
||||
toFeed(feedId, result.config, metadata?.emails.length ?? 0, env),
|
||||
toFeed(
|
||||
feedId,
|
||||
result.config,
|
||||
metadata?.emails.length ?? 0,
|
||||
env,
|
||||
unionNativeFeeds(metadata?.nativeFeeds),
|
||||
),
|
||||
200,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -99,6 +99,12 @@ export const FeedSchema = z
|
||||
emailAddress: z.string(),
|
||||
rssUrl: z.string(),
|
||||
atomUrl: z.string(),
|
||||
nativeFeeds: z.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
type: z.enum(["rss", "atom", "json"]),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.openapi("Feed");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user