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 { apiApp } from "./index";
|
||||||
import { createMockEnv } from "../../test/setup";
|
import { createMockEnv } from "../../test/setup";
|
||||||
import { Env } from "../../types";
|
import { Env } from "../../types";
|
||||||
|
import { FeedRepository } from "../../infrastructure/feed-repository";
|
||||||
|
import { FeedId } from "../../domain/value-objects/feed-id";
|
||||||
|
|
||||||
const PASSWORD = "test-password";
|
const PASSWORD = "test-password";
|
||||||
const authHeaders = { Authorization: `Bearer ${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", () => {
|
describe("Emails", () => {
|
||||||
it("lists, reads and deletes an email", async () => {
|
it("lists, reads and deletes an email", async () => {
|
||||||
const feedId = await createFeed();
|
const feedId = await createFeed();
|
||||||
|
|||||||
+19
-4
@@ -1,7 +1,8 @@
|
|||||||
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
||||||
import { cors } from "hono/cors";
|
import { cors } from "hono/cors";
|
||||||
import { Scalar } from "@scalar/hono-api-reference";
|
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 { apiAuthMiddleware } from "../../infrastructure/auth";
|
||||||
import {
|
import {
|
||||||
createFeedRecord,
|
createFeedRecord,
|
||||||
@@ -51,6 +52,7 @@ function toFeed(
|
|||||||
config: FeedConfig,
|
config: FeedConfig,
|
||||||
emailCount: number,
|
emailCount: number,
|
||||||
env: Env,
|
env: Env,
|
||||||
|
nativeFeeds: NativeFeed[],
|
||||||
): z.infer<typeof FeedSchema> {
|
): z.infer<typeof FeedSchema> {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -67,6 +69,7 @@ function toFeed(
|
|||||||
emailAddress: feedEmailAddress(config.mailbox_id, env),
|
emailAddress: feedEmailAddress(config.mailbox_id, env),
|
||||||
rssUrl: feedRssUrl(id, env),
|
rssUrl: feedRssUrl(id, env),
|
||||||
atomUrl: feedAtomUrl(id, env),
|
atomUrl: feedAtomUrl(id, env),
|
||||||
|
nativeFeeds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +159,7 @@ apiApp.openapi(
|
|||||||
senderInTitle: body.senderInTitle,
|
senderInTitle: body.senderInTitle,
|
||||||
lifetimeHours: body.lifetimeHours,
|
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);
|
if (!config) return c.json({ error: "Feed not found" }, 404);
|
||||||
const metadata = await repo.getMetadata(id);
|
const metadata = await repo.getMetadata(id);
|
||||||
return c.json(
|
return c.json(
|
||||||
toFeed(feedId, config, metadata?.emails.length ?? 0, env),
|
toFeed(
|
||||||
|
feedId,
|
||||||
|
config,
|
||||||
|
metadata?.emails.length ?? 0,
|
||||||
|
env,
|
||||||
|
unionNativeFeeds(metadata?.nativeFeeds),
|
||||||
|
),
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -228,7 +237,13 @@ apiApp.openapi(
|
|||||||
return c.json({ error: "Feed has expired and cannot be modified" }, 409);
|
return c.json({ error: "Feed has expired and cannot be modified" }, 409);
|
||||||
const metadata = await FeedRepository.from(env).getMetadata(id);
|
const metadata = await FeedRepository.from(env).getMetadata(id);
|
||||||
return c.json(
|
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,
|
200,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ export const FeedSchema = z
|
|||||||
emailAddress: z.string(),
|
emailAddress: z.string(),
|
||||||
rssUrl: z.string(),
|
rssUrl: z.string(),
|
||||||
atomUrl: z.string(),
|
atomUrl: z.string(),
|
||||||
|
nativeFeeds: z.array(
|
||||||
|
z.object({
|
||||||
|
url: z.string(),
|
||||||
|
type: z.enum(["rss", "atom", "json"]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.openapi("Feed");
|
.openapi("Feed");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user