diff --git a/packages/server/src/auth/auth.ts b/packages/server/src/auth/auth.ts index 0f75298..54d866d 100644 --- a/packages/server/src/auth/auth.ts +++ b/packages/server/src/auth/auth.ts @@ -119,6 +119,13 @@ async function verifyAuthToken(cookies: Bun.CookieMap): Promise { return user } +function startBackgroundAuthTokenCleanup() { + const query = db.query("DELETE FROM auth_tokens WHERE expires_at_unix_ms < $time") + setInterval(() => { + query.run({ time: new Date().valueOf() }) + }, 5000) +} + async function signUp(request: Bun.BunRequest<"/api/sign-up">) { const body = await request.json().catch(() => { throw new HttpError(500) @@ -199,4 +206,4 @@ async function logout(request: Bun.BunRequest<"/api/logout">, user: User): Promi return new Response(undefined, { status: 204 }) } -export { authenticated, signUp, login, logout } +export { authenticated, signUp, login, logout, startBackgroundAuthTokenCleanup } diff --git a/packages/server/src/bookmark/bookmark.ts b/packages/server/src/bookmark/bookmark.ts index 8621f02..413e507 100644 --- a/packages/server/src/bookmark/bookmark.ts +++ b/packages/server/src/bookmark/bookmark.ts @@ -1,4 +1,4 @@ -import type { Bookmark, BookmarkTag } from "@markone/core/bookmark" +import type { Bookmark, BookmarkId, BookmarkTag } from "@markone/core/bookmark" import type { User } from "@markone/core/user" import { Readability } from "@mozilla/readability" import { JSDOM } from "jsdom" @@ -72,6 +72,14 @@ function findBookmark(id: string, user: User): Bookmark | null { return bookmark } +function deleteBookmark(id: BookmarkId, user: User) { + db.query("DELETE FROM bookmarks WHERE user_id = $userId AND id = $id").run({ + id, + userId: user.id, + }) + db.query("DELETE FROM bookmark_tags WHERE bookmark_id = ?").run(id) +} + async function cacheContent(url: string): Promise { const res = await fetch(url).catch(() => { throw new LinkUnreachable() @@ -162,6 +170,7 @@ function findBookmarkTags(bookmark: Bookmark): BookmarkTag[] { export { insertDemoBookmarks, insertBookmark, + deleteBookmark, findBookmark, findBookmarkCachedContent, cacheContent, diff --git a/packages/server/src/bookmark/handlers.ts b/packages/server/src/bookmark/handlers.ts index b95f675..fb1ecab 100644 --- a/packages/server/src/bookmark/handlers.ts +++ b/packages/server/src/bookmark/handlers.ts @@ -10,6 +10,7 @@ import { UnsupportedLink, assignTagsToBookmark, cacheContent, + deleteBookmark, findBookmark, findBookmarkCachedContent, findBookmarkTags, @@ -22,6 +23,7 @@ const BOOKMARK_PAGINATION_LIMIT = 100 const ListUserBookmarksParams = type({ limit: ["number", "=", BOOKMARK_PAGINATION_LIMIT], skip: ["number", "=", 0], + "tags?": "string", }) const AddBookmarkRequestBody = type({ @@ -36,36 +38,53 @@ const AddTagRequestBody = type({ }) async function listUserBookmarks(request: Bun.BunRequest<"/api/bookmarks">, user: User) { - const queryParams = ListUserBookmarksParams(request.params) + const { searchParams } = new URL(request.url) + const queryParams = ListUserBookmarksParams(Object.fromEntries(searchParams)) if (queryParams instanceof type.errors) { throw new HttpError(400, "", queryParams.summary) } - const listBookmarksQuery = db.query( - ` -SELECT bookmarks.id, bookmarks.title, bookmarks.url FROM bookmarks -WHERE bookmarks.user_id = $userId -ORDER BY bookmarks.id DESC -LIMIT $limit OFFSET $skip -`, - ) + let results: Bookmark[] + if (queryParams.tags) { + const tagNames = queryParams.tags.split(",") - const results = listBookmarksQuery.all({ - userId: user.id, - limit: queryParams.limit, - skip: queryParams.skip, - }) + const tagIdsQuery = db.query<{ id: string }, string[]>( + `SELECT id FROM tags WHERE name IN (${Array(tagNames.length).fill("?").join(",")})`, + ) + + const tagIds = tagIdsQuery.all(...tagNames).map(({ id }) => id) + + const query = db.query(` + SELECT bookmarks.id, bookmarks.title, bookmarks.url FROM bookmarks + INNER JOIN bookmark_tags + ON bookmark_tags.bookmark_id = bookmarks.id + WHERE bookmarks.user_id = ? AND bookmark_tags.tag_id IN (${Array(tagIds.length).fill("?").join(",")}) + ORDER BY bookmarks.id DESC + LIMIT ? OFFSET ? + `) + + results = query.all(...[user.id, ...tagIds, queryParams.limit, queryParams.skip]) as Bookmark[] + } else { + const query = db.query(` + SELECT bookmarks.id, bookmarks.title, bookmarks.url FROM bookmarks + WHERE bookmarks.user_id = $userId + ORDER BY bookmarks.id DESC + LIMIT $limit OFFSET $skip + `) + + results = query.all({ + userId: user.id, + limit: queryParams.limit, + skip: queryParams.skip, + }) as Bookmark[] + } return Response.json(results, { status: 200 }) } async function deleteUserBookmark(request: Bun.BunRequest<"/api/bookmarks/:id">, user: User) { if (user.id !== DEMO_USER.id) { - const deleteBookmarkQuery = db.query("DELETE FROM bookmarks WHERE user_id = $userId AND id = $id") - const tx = db.transaction(() => { - deleteBookmarkQuery.run({ userId: user.id, id: request.params.id }) - }) - tx() + deleteBookmark(request.params.id, user) } return Response.json(undefined, { status: 204 }) } @@ -94,7 +113,7 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User const tagNames = new Set(body.tags) for (const tag of tagNames) { - if (/[\s#]/g.test(tag)) { + if (tag.length === 0 || /[\s#]/g.test(tag)) { throw new HttpError(400, "InvalidTag", "Tags cannot contain '#' or whitespaces") } } @@ -116,9 +135,9 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User if (tagNames.size > 0) { const tagQuery = db.query, string[]>(` - SELECT id, name FROM tags - WHERE user_id = ? AND name IN (${Array(tagNames.size).fill("?").join(",")}) - `) + SELECT id, name FROM tags + WHERE user_id = ? AND name IN (${Array(tagNames.size).fill("?").join(",")}) + `) const tags = tagQuery.all(user.id, ...tagNames) for (const tag of tags) { @@ -128,8 +147,12 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User } } - const createdTags = insertTags([...tagNames], user) - assignTagsToBookmark(createdTags, bookmark) + if (tagNames.size > 0) { + const createdTags = insertTags([...tagNames], user) + bookmark.tags.push(...createdTags) + } + + assignTagsToBookmark(bookmark.tags, bookmark) } return Response.json(bookmark, { status: 200 }) diff --git a/packages/server/src/bookmark/tag.ts b/packages/server/src/bookmark/tag.ts index dce22aa..3f7c798 100644 --- a/packages/server/src/bookmark/tag.ts +++ b/packages/server/src/bookmark/tag.ts @@ -4,7 +4,7 @@ import { ulid } from "ulid" import { db } from "~/database.ts" function insertTags(names: string[], user: User): BookmarkTag[] { - console.log(names) + console.log("======== insert tags", names) const insertTags = db.query(` INSERT INTO tags (id, name, user_id) VALUES ${Array(names.length).fill("(?,?,?)").join(",")} diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index f3af9fc..8171e4e 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,4 +1,4 @@ -import { authenticated, login, logout, signUp } from "./auth/auth.ts" +import { authenticated, login, logout, signUp, startBackgroundAuthTokenCleanup } from "./auth/auth.ts" import { startBackgroundSessionCleanup } from "./auth/session.ts" import { insertDemoBookmarks } from "./bookmark/bookmark.ts" import { @@ -19,6 +19,7 @@ async function main() { const user = await createDemoUser() insertDemoBookmarks(user) startBackgroundSessionCleanup() + startBackgroundAuthTokenCleanup() Bun.serve({ routes: {