aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/routers
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-11-03 17:09:47 +0000
committerMohamed Bassem <me@mbassem.com>2024-11-03 17:09:47 +0000
commitcf1a25131fd45ab7c9a72b837be525c24457cd8b (patch)
treecb8b3d4a57a4ce06e500e4c7ceea43924d64a5d4 /packages/trpc/routers
parent2efc7c8c01866fafad5322fb8783d94821e32ff1 (diff)
downloadkarakeep-cf1a25131fd45ab7c9a72b837be525c24457cd8b.tar.zst
feature: Add support for subscribing to RSS feeds. Fixes #202
Diffstat (limited to 'packages/trpc/routers')
-rw-r--r--packages/trpc/routers/_app.ts2
-rw-r--r--packages/trpc/routers/feeds.ts121
2 files changed, 123 insertions, 0 deletions
diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts
index 01c92e6a..ea1e0ca8 100644
--- a/packages/trpc/routers/_app.ts
+++ b/packages/trpc/routers/_app.ts
@@ -2,6 +2,7 @@ import { router } from "../index";
import { adminAppRouter } from "./admin";
import { apiKeysAppRouter } from "./apiKeys";
import { bookmarksAppRouter } from "./bookmarks";
+import { feedsAppRouter } from "./feeds";
import { listsAppRouter } from "./lists";
import { promptsAppRouter } from "./prompts";
import { tagsAppRouter } from "./tags";
@@ -15,6 +16,7 @@ export const appRouter = router({
tags: tagsAppRouter,
prompts: promptsAppRouter,
admin: adminAppRouter,
+ feeds: feedsAppRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
diff --git a/packages/trpc/routers/feeds.ts b/packages/trpc/routers/feeds.ts
new file mode 100644
index 00000000..a8025dfb
--- /dev/null
+++ b/packages/trpc/routers/feeds.ts
@@ -0,0 +1,121 @@
+import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
+import { and, eq } from "drizzle-orm";
+import { z } from "zod";
+
+import { rssFeedsTable } from "@hoarder/db/schema";
+import { FeedQueue } from "@hoarder/shared/queues";
+import {
+ zFeedSchema,
+ zNewFeedSchema,
+ zUpdateFeedSchema,
+} from "@hoarder/shared/types/feeds";
+
+import { authedProcedure, Context, router } from "../index";
+
+export const ensureFeedOwnership = experimental_trpcMiddleware<{
+ ctx: Context;
+ input: { feedId: string };
+}>().create(async (opts) => {
+ const feed = await opts.ctx.db.query.rssFeedsTable.findFirst({
+ where: eq(rssFeedsTable.id, opts.input.feedId),
+ columns: {
+ userId: true,
+ },
+ });
+ if (!opts.ctx.user) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "User is not authorized",
+ });
+ }
+ if (!feed) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Feed not found",
+ });
+ }
+ if (feed.userId != opts.ctx.user.id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "User is not allowed to access resource",
+ });
+ }
+
+ return opts.next();
+});
+
+export const feedsAppRouter = router({
+ create: authedProcedure
+ .input(zNewFeedSchema)
+ .output(zFeedSchema)
+ .mutation(async ({ input, ctx }) => {
+ const [feed] = await ctx.db
+ .insert(rssFeedsTable)
+ .values({
+ name: input.name,
+ url: input.url,
+ userId: ctx.user.id,
+ })
+ .returning();
+ return feed;
+ }),
+ update: authedProcedure
+ .input(zUpdateFeedSchema)
+ .output(zFeedSchema)
+ .use(ensureFeedOwnership)
+ .mutation(async ({ input, ctx }) => {
+ const feed = await ctx.db
+ .update(rssFeedsTable)
+ .set({
+ name: input.name,
+ url: input.url,
+ })
+ .where(
+ and(
+ eq(rssFeedsTable.userId, ctx.user.id),
+ eq(rssFeedsTable.id, input.feedId),
+ ),
+ )
+ .returning();
+ if (feed.length == 0) {
+ throw new TRPCError({ code: "NOT_FOUND" });
+ }
+ return feed[0];
+ }),
+ list: authedProcedure
+ .output(z.object({ feeds: z.array(zFeedSchema) }))
+ .query(async ({ ctx }) => {
+ const feeds = await ctx.db.query.rssFeedsTable.findMany({
+ where: eq(rssFeedsTable.userId, ctx.user.id),
+ });
+ return { feeds };
+ }),
+ delete: authedProcedure
+ .input(
+ z.object({
+ feedId: z.string(),
+ }),
+ )
+ .use(ensureFeedOwnership)
+ .mutation(async ({ input, ctx }) => {
+ const res = await ctx.db
+ .delete(rssFeedsTable)
+ .where(
+ and(
+ eq(rssFeedsTable.userId, ctx.user.id),
+ eq(rssFeedsTable.id, input.feedId),
+ ),
+ );
+ if (res.changes == 0) {
+ throw new TRPCError({ code: "NOT_FOUND" });
+ }
+ }),
+ fetchNow: authedProcedure
+ .input(z.object({ feedId: z.string() }))
+ .use(ensureFeedOwnership)
+ .mutation(async ({ input }) => {
+ await FeedQueue.enqueue({
+ feedId: input.feedId,
+ });
+ }),
+});