import { Router } from "@oak/oak"; import { authMiddleware } from "../middleware/auth.ts"; import { type APIResponse, type Dump, type FollowStatus, type PaginatedData, } from "../model/interfaces.ts"; import { parsePagination } from "../lib/pagination.ts"; import { followPlaylist, followUser, getFollowedPlaylistsDumpFeed, getFollowedUsersDumpFeed, getFollowStatus, unfollowPlaylist, unfollowUser, } from "../services/follow-service.ts"; import { getCategoryIdBySlug } from "../services/category-service.ts"; // Resolves the optional ?category= filter. Returns the matching category // id, or "missing" when a slug was given but matches nothing (so the feed comes // back empty rather than unfiltered), or undefined when no filter was requested. function resolveCategoryFilter( params: URLSearchParams, ): string | "missing" | undefined { const slug = params.get("category"); if (!slug) return undefined; return getCategoryIdBySlug(slug) ?? "missing"; } const EMPTY_FEED: { items: Dump[]; total: number } = { items: [], total: 0 }; const router = new Router({ prefix: "/api/follows" }); // Static routes first to prevent Oak matching "status"/"feed" as a :param // GET /api/follows/status router.get("/status", authMiddleware, (ctx) => { const status = getFollowStatus(ctx.state.user.userId); const body: APIResponse = { success: true, data: status }; ctx.response.body = body; }); // GET /api/follows/feed/users?page=&limit= router.get("/feed/users", authMiddleware, (ctx) => { const { page, limit } = parsePagination(ctx.request.url.searchParams); const categoryId = resolveCategoryFilter(ctx.request.url.searchParams); const { items, total } = categoryId === "missing" ? EMPTY_FEED : getFollowedUsersDumpFeed(ctx.state.user.userId, page, limit, categoryId); const data: PaginatedData = { items, total, hasMore: page * limit < total, }; const body: APIResponse> = { success: true, data }; ctx.response.body = body; }); // GET /api/follows/feed/playlists?page=&limit= router.get("/feed/playlists", authMiddleware, (ctx) => { const { page, limit } = parsePagination(ctx.request.url.searchParams); const categoryId = resolveCategoryFilter(ctx.request.url.searchParams); const { items, total } = categoryId === "missing" ? EMPTY_FEED : getFollowedPlaylistsDumpFeed( ctx.state.user.userId, page, limit, categoryId, ); const data: PaginatedData = { items, total, hasMore: page * limit < total, }; const body: APIResponse> = { success: true, data }; ctx.response.body = body; }); // POST /api/follows/users/:userId router.post("/users/:userId", authMiddleware, (ctx) => { followUser(ctx.state.user.userId, ctx.params.userId); ctx.response.status = 204; }); // DELETE /api/follows/users/:userId router.delete("/users/:userId", authMiddleware, (ctx) => { unfollowUser(ctx.state.user.userId, ctx.params.userId); ctx.response.status = 204; }); // POST /api/follows/playlists/:playlistId router.post("/playlists/:playlistId", authMiddleware, (ctx) => { followPlaylist(ctx.state.user.userId, ctx.params.playlistId); ctx.response.status = 204; }); // DELETE /api/follows/playlists/:playlistId router.delete("/playlists/:playlistId", authMiddleware, (ctx) => { unfollowPlaylist(ctx.state.user.userId, ctx.params.playlistId); ctx.response.status = 204; }); export default router;