aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/server/api
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-01 18:00:58 +0000
committerMohamedBassem <me@mbassem.com>2024-03-01 18:00:58 +0000
commit75d315dda4232ee3b89abf054f0b6ee10105ffe3 (patch)
treef0796a136578f3b5aa82b4b3313e54fa3061ff5f /packages/web/server/api
parent588471d65039e6920751ac2add8874ee932bc2f1 (diff)
downloadkarakeep-75d315dda4232ee3b89abf054f0b6ee10105ffe3.tar.zst
feature: Add support for creating and updating lists
Diffstat (limited to 'packages/web/server/api')
-rw-r--r--packages/web/server/api/routers/_app.ts2
-rw-r--r--packages/web/server/api/routers/bookmarks.ts3
-rw-r--r--packages/web/server/api/routers/lists.ts173
3 files changed, 178 insertions, 0 deletions
diff --git a/packages/web/server/api/routers/_app.ts b/packages/web/server/api/routers/_app.ts
index b958ef8f..6a1b05e9 100644
--- a/packages/web/server/api/routers/_app.ts
+++ b/packages/web/server/api/routers/_app.ts
@@ -1,11 +1,13 @@
import { router } from "../trpc";
import { apiKeysAppRouter } from "./apiKeys";
import { bookmarksAppRouter } from "./bookmarks";
+import { listsAppRouter } from "./lists";
import { usersAppRouter } from "./users";
export const appRouter = router({
bookmarks: bookmarksAppRouter,
apiKeys: apiKeysAppRouter,
users: usersAppRouter,
+ lists: listsAppRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
diff --git a/packages/web/server/api/routers/bookmarks.ts b/packages/web/server/api/routers/bookmarks.ts
index 4e98eb2f..8b59f1ef 100644
--- a/packages/web/server/api/routers/bookmarks.ts
+++ b/packages/web/server/api/routers/bookmarks.ts
@@ -284,6 +284,9 @@ export const bookmarksAppRouter = router({
.input(zGetBookmarksRequestSchema)
.output(zGetBookmarksResponseSchema)
.query(async ({ input, ctx }) => {
+ if (input.ids && input.ids.length == 0) {
+ return { bookmarks: [] };
+ }
const results = await ctx.db.query.bookmarks.findMany({
where: and(
eq(bookmarks.userId, ctx.user.id),
diff --git a/packages/web/server/api/routers/lists.ts b/packages/web/server/api/routers/lists.ts
new file mode 100644
index 00000000..7bf5eed5
--- /dev/null
+++ b/packages/web/server/api/routers/lists.ts
@@ -0,0 +1,173 @@
+import { Context, authedProcedure, router } from "../trpc";
+import { SqliteError } from "@hoarder/db";
+import { z } from "zod";
+import { TRPCError, experimental_trpcMiddleware } from "@trpc/server";
+import { bookmarkLists, bookmarksInLists } from "@hoarder/db/schema";
+import { and, eq } from "drizzle-orm";
+import { zBookmarkListSchema } from "@/lib/types/api/lists";
+
+const ensureListOwnership = experimental_trpcMiddleware<{
+ ctx: Context;
+ input: { listId: string };
+}>().create(async (opts) => {
+ const list = await opts.ctx.db.query.bookmarkLists.findFirst({
+ where: eq(bookmarkLists.id, opts.input.listId),
+ columns: {
+ userId: true,
+ },
+ });
+ if (!opts.ctx.user) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "User is not authorized",
+ });
+ }
+ if (!list) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "List not found",
+ });
+ }
+ if (list.userId != opts.ctx.user.id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "User is not allowed to access resource",
+ });
+ }
+
+ return opts.next();
+});
+
+export const listsAppRouter = router({
+ create: authedProcedure
+ .input(
+ z.object({
+ name: z.string().min(1).max(20),
+ icon: z.string(),
+ }),
+ )
+ .output(zBookmarkListSchema)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ const result = await ctx.db
+ .insert(bookmarkLists)
+ .values({
+ name: input.name,
+ icon: input.icon,
+ userId: ctx.user.id,
+ })
+ .returning();
+ return result[0];
+ } catch (e) {
+ if (e instanceof SqliteError) {
+ if (e.code == "SQLITE_CONSTRAINT_UNIQUE") {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "List already exists",
+ });
+ }
+ }
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Something went wrong",
+ });
+ }
+ }),
+ delete: authedProcedure
+ .input(
+ z.object({
+ listId: z.string(),
+ }),
+ )
+ .use(ensureListOwnership)
+ .mutation(async ({ input, ctx }) => {
+ const res = await ctx.db
+ .delete(bookmarkLists)
+ .where(
+ and(
+ eq(bookmarkLists.id, input.listId),
+ eq(bookmarkLists.userId, ctx.user.id),
+ ),
+ );
+ if (res.changes == 0) {
+ throw new TRPCError({ code: "NOT_FOUND" });
+ }
+ }),
+ addToList: authedProcedure
+ .input(
+ z.object({
+ listId: z.string(),
+ bookmarkId: z.string(),
+ }),
+ )
+ .use(ensureListOwnership)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ await ctx.db.insert(bookmarksInLists).values({
+ listId: input.listId,
+ bookmarkId: input.bookmarkId,
+ });
+ } catch (e) {
+ if (e instanceof SqliteError) {
+ if (e.code == "SQLITE_CONSTRAINT_PRIMARYKEY") {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Bookmark already in the list",
+ });
+ }
+ }
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Something went wrong",
+ });
+ }
+ }),
+ get: authedProcedure
+ .input(
+ z.object({
+ listId: z.string(),
+ }),
+ )
+ .output(
+ zBookmarkListSchema.merge(
+ z.object({
+ bookmarks: z.array(z.string()),
+ }),
+ ),
+ )
+ .use(ensureListOwnership)
+ .query(async ({ input, ctx }) => {
+ const res = await ctx.db.query.bookmarkLists.findFirst({
+ where: and(
+ eq(bookmarkLists.id, input.listId),
+ eq(bookmarkLists.userId, ctx.user.id),
+ ),
+ with: {
+ bookmarksInLists: true,
+ },
+ });
+ if (!res) {
+ throw new TRPCError({ code: "NOT_FOUND" });
+ }
+
+ return {
+ id: res.id,
+ name: res.name,
+ icon: res.icon,
+ bookmarks: res.bookmarksInLists.map((b) => b.bookmarkId),
+ };
+ }),
+ list: authedProcedure
+ .output(
+ z.object({
+ lists: z.array(zBookmarkListSchema),
+ }),
+ )
+ .query(async ({ ctx }) => {
+ const lists = await ctx.db.query.bookmarkLists.findMany({
+ where: and(eq(bookmarkLists.userId, ctx.user.id)),
+ });
+
+ return { lists };
+ }),
+});