aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-08-31 13:09:45 +0000
committerMohamed Bassem <me@mbassem.com>2025-08-31 16:52:21 +0000
commit15efda6dfc71f4a5e593fba93d349236baee3ea4 (patch)
treea64541ec2f6d1f58739ce5b118c4ec027908e055 /packages
parentce9a006aa9667cf1b9598a3e5cee0a7b0f900e2f (diff)
downloadkarakeep-15efda6dfc71f4a5e593fba93d349236baee3ea4.tar.zst
refactor: Move feed object into models
Diffstat (limited to 'packages')
-rw-r--r--packages/trpc/models/feeds.ts119
-rw-r--r--packages/trpc/routers/feeds.ts109
2 files changed, 134 insertions, 94 deletions
diff --git a/packages/trpc/models/feeds.ts b/packages/trpc/models/feeds.ts
new file mode 100644
index 00000000..eab1ad65
--- /dev/null
+++ b/packages/trpc/models/feeds.ts
@@ -0,0 +1,119 @@
+import { TRPCError } from "@trpc/server";
+import { and, eq } from "drizzle-orm";
+import { z } from "zod";
+
+import { rssFeedsTable } from "@karakeep/db/schema";
+import {
+ zFeedSchema,
+ zNewFeedSchema,
+ zUpdateFeedSchema,
+} from "@karakeep/shared/types/feeds";
+
+import { AuthedContext } from "..";
+import { PrivacyAware } from "./privacy";
+
+export class Feed implements PrivacyAware {
+ constructor(
+ protected ctx: AuthedContext,
+ private feed: typeof rssFeedsTable.$inferSelect,
+ ) {}
+
+ static async fromId(ctx: AuthedContext, id: string): Promise<Feed> {
+ const feed = await ctx.db.query.rssFeedsTable.findFirst({
+ where: eq(rssFeedsTable.id, id),
+ });
+
+ if (!feed) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Feed not found",
+ });
+ }
+
+ // If it exists but belongs to another user, throw forbidden error
+ if (feed.userId !== ctx.user.id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "User is not allowed to access resource",
+ });
+ }
+
+ return new Feed(ctx, feed);
+ }
+
+ static async create(
+ ctx: AuthedContext,
+ input: z.infer<typeof zNewFeedSchema>,
+ ): Promise<Feed> {
+ const [result] = await ctx.db
+ .insert(rssFeedsTable)
+ .values({
+ name: input.name,
+ url: input.url,
+ userId: ctx.user.id,
+ enabled: input.enabled,
+ })
+ .returning();
+
+ return new Feed(ctx, result);
+ }
+
+ static async getAll(ctx: AuthedContext): Promise<Feed[]> {
+ const feeds = await ctx.db.query.rssFeedsTable.findMany({
+ where: eq(rssFeedsTable.userId, ctx.user.id),
+ });
+
+ return feeds.map((f) => new Feed(ctx, f));
+ }
+
+ ensureCanAccess(ctx: AuthedContext): void {
+ if (this.feed.userId !== ctx.user.id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "User is not allowed to access resource",
+ });
+ }
+ }
+
+ async delete(): Promise<void> {
+ const res = await this.ctx.db
+ .delete(rssFeedsTable)
+ .where(
+ and(
+ eq(rssFeedsTable.id, this.feed.id),
+ eq(rssFeedsTable.userId, this.ctx.user.id),
+ ),
+ );
+
+ if (res.changes === 0) {
+ throw new TRPCError({ code: "NOT_FOUND" });
+ }
+ }
+
+ async update(input: z.infer<typeof zUpdateFeedSchema>): Promise<void> {
+ const result = await this.ctx.db
+ .update(rssFeedsTable)
+ .set({
+ name: input.name,
+ url: input.url,
+ enabled: input.enabled,
+ })
+ .where(
+ and(
+ eq(rssFeedsTable.id, this.feed.id),
+ eq(rssFeedsTable.userId, this.ctx.user.id),
+ ),
+ )
+ .returning();
+
+ if (result.length === 0) {
+ throw new TRPCError({ code: "NOT_FOUND" });
+ }
+
+ this.feed = result[0];
+ }
+
+ asPublicFeed(): z.infer<typeof zFeedSchema> {
+ return this.feed;
+ }
+}
diff --git a/packages/trpc/routers/feeds.ts b/packages/trpc/routers/feeds.ts
index 07c48f0a..27eefdf1 100644
--- a/packages/trpc/routers/feeds.ts
+++ b/packages/trpc/routers/feeds.ts
@@ -1,8 +1,5 @@
-import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
-import { and, eq } from "drizzle-orm";
import { z } from "zod";
-import { rssFeedsTable } from "@karakeep/db/schema";
import { FeedQueue } from "@karakeep/shared/queues";
import {
zFeedSchema,
@@ -10,79 +7,24 @@ import {
zUpdateFeedSchema,
} from "@karakeep/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();
-});
+import { authedProcedure, router } from "../index";
+import { Feed } from "../models/feeds";
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,
- enabled: input.enabled,
- })
- .returning();
- return feed;
+ const feed = await Feed.create(ctx, input);
+ return feed.asPublicFeed();
}),
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,
- enabled: input.enabled,
- })
- .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];
+ const feed = await Feed.fromId(ctx, input.feedId);
+ await feed.update(input);
+ return feed.asPublicFeed();
}),
get: authedProcedure
.input(
@@ -91,26 +33,15 @@ export const feedsAppRouter = router({
}),
)
.output(zFeedSchema)
- .use(ensureFeedOwnership)
.query(async ({ ctx, input }) => {
- const feed = await ctx.db.query.rssFeedsTable.findFirst({
- where: and(
- eq(rssFeedsTable.userId, ctx.user.id),
- eq(rssFeedsTable.id, input.feedId),
- ),
- });
- if (!feed) {
- throw new TRPCError({ code: "NOT_FOUND" });
- }
- return feed;
+ const feed = await Feed.fromId(ctx, input.feedId);
+ return feed.asPublicFeed();
}),
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 };
+ const feeds = await Feed.getAll(ctx);
+ return { feeds: feeds.map((f) => f.asPublicFeed()) };
}),
delete: authedProcedure
.input(
@@ -118,24 +49,14 @@ export const feedsAppRouter = router({
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" });
- }
+ const feed = await Feed.fromId(ctx, input.feedId);
+ await feed.delete();
}),
fetchNow: authedProcedure
.input(z.object({ feedId: z.string() }))
- .use(ensureFeedOwnership)
- .mutation(async ({ input }) => {
+ .mutation(async ({ input, ctx }) => {
+ await Feed.fromId(ctx, input.feedId);
await FeedQueue.enqueue({
feedId: input.feedId,
});