implement edit bookmark dialog

This commit is contained in:
2025-05-25 15:40:16 +01:00
parent 255acfcb32
commit 6afb5dee9a
22 changed files with 630 additions and 127 deletions

View File

@@ -1,8 +1,9 @@
import type { Bookmark, BookmarkId, BookmarkTag } from "@markone/core/bookmark"
import type { Bookmark, BookmarkId, Tag, TaggedBookmark } from "@markone/core"
import type { User } from "@markone/core/user"
import { Readability } from "@mozilla/readability"
import { JSDOM } from "jsdom"
import { db } from "~/database.ts"
import { findTagsByNames, insertTags } from "~/tag/tag.js"
import { DEMO_BOOKMARKS } from "./demo-bookmarks.ts"
class LinkUnreachable {}
@@ -52,7 +53,7 @@ function findBookmarkCachedContent(id: string, user: User): Buffer | null {
}
function findBookmark(id: string, user: User): Bookmark | null {
const bookmarkQuery = db.query<Bookmark, { id: string; userId: string }>(
const bookmarkQuery = db.query<TaggedBookmark, { id: string; userId: string }>(
"SELECT id, title, url FROM bookmarks WHERE id = $id AND user_id = $userId",
)
const bookmark = bookmarkQuery.get({ id, userId: user.id })
@@ -60,7 +61,7 @@ function findBookmark(id: string, user: User): Bookmark | null {
return null
}
const tagsQuery = db.query<BookmarkTag, { bookmarkId: string }>(`
const tagsQuery = db.query<Tag, { bookmarkId: string }>(`
SELECT tags.id, tags.name FROM tags
INNER JOIN bookmark_tags
ON bookmark_tags.tag_id = tags.id AND bookmark_tags.bookmark_id = $bookmarkId
@@ -72,6 +73,15 @@ function findBookmark(id: string, user: User): Bookmark | null {
return bookmark
}
function updateBookmarkTitle(bookmark: Bookmark, newTitle: string, user: User) {
const query = db.query("UPDATE bookmarks SET title = $title WHERE id = $id AND user_id = $userId")
query.run({
title: newTitle,
id: bookmark.id,
userId: user.id,
})
}
function deleteBookmark(id: BookmarkId, user: User) {
db.query("DELETE FROM bookmarks WHERE user_id = $userId AND id = $id").run({
id,
@@ -141,9 +151,9 @@ async function cacheContent(url: string): Promise<CachedContent | null> {
throw new UnsupportedLink()
}
function assignTagsToBookmark(tags: BookmarkTag[], bookmark: Bookmark) {
function assignTagsToBookmark(tags: Tag[], bookmark: Bookmark) {
const query = db.query(`
INSERT INTO bookmark_tags (tag_id, bookmark_id)
INSERT OR IGNORE INTO bookmark_tags (tag_id, bookmark_id)
VALUES ${Array(tags.length).fill("(?,?)").join(",")}
`)
@@ -156,8 +166,8 @@ function assignTagsToBookmark(tags: BookmarkTag[], bookmark: Bookmark) {
query.run(...args)
}
function findBookmarkTags(bookmark: Bookmark): BookmarkTag[] {
const query = db.query<BookmarkTag, { bookmarkId: string }>(`
function findBookmarkTags(bookmark: Bookmark): Tag[] {
const query = db.query<Tag, { bookmarkId: string }>(`
SELECT tags.name as name, tags.id as id FROM bookmark_tags
INNER JOIN tags
ON tags.id = bookmark_tags.tag_id
@@ -167,14 +177,40 @@ function findBookmarkTags(bookmark: Bookmark): BookmarkTag[] {
return tags
}
function updateBookmarkTags(bookmark: Bookmark, tagNames: string[], user: User) {
const tags = findTagsByNames(tagNames, user)
const existingTagNames = new Set<string>()
for (const tag of tags) {
existingTagNames.add(tag.name)
}
const newTagNames: string[] = []
for (const name of tagNames) {
if (!existingTagNames.has(name)) {
newTagNames.push(name)
}
}
if (newTagNames.length > 0) {
const newTags = insertTags(newTagNames, user)
tags.push(...newTags)
}
assignTagsToBookmark(tags, bookmark)
return tags
}
export {
insertDemoBookmarks,
insertBookmark,
updateBookmarkTitle,
deleteBookmark,
findBookmark,
findBookmarkCachedContent,
cacheContent,
assignTagsToBookmark,
findBookmarkTags,
updateBookmarkTags,
}
export { LinkUnreachable, UnsupportedLink }

View File

@@ -1,6 +1,6 @@
import type { Bookmark } from "@markone/core/bookmark"
import type { TaggedBookmark } from "@markone/core"
const DEMO_BOOKMARKS: Bookmark[] = [
const DEMO_BOOKMARKS: TaggedBookmark[] = [
{
id: "01HYN4G66K0000000000000000",
title: "Google",

View File

@@ -1,4 +1,4 @@
import type { Bookmark, BookmarkTag } from "@markone/core/bookmark"
import type { Bookmark, Tag, TaggedBookmark } from "@markone/core"
import { DEMO_USER } from "@markone/core/user"
import { type } from "arktype"
import { ulid } from "ulid"
@@ -15,8 +15,11 @@ import {
findBookmarkCachedContent,
findBookmarkTags,
insertBookmark,
updateBookmarkTags,
updateBookmarkTitle,
} from "./bookmark.ts"
import { insertTags } from "./tag.ts"
import { insertTags } from "~/tag/tag.js"
const BOOKMARK_PAGINATION_LIMIT = 100
@@ -33,6 +36,11 @@ const AddBookmarkRequestBody = type({
"force?": "boolean",
})
const UpdateBookmarkRequestBody = type({
"title?": "string",
"tags?": "string[]",
})
const AddTagRequestBody = type({
name: "string",
})
@@ -93,7 +101,6 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User
if (user.id !== DEMO_USER.id) {
const body = AddBookmarkRequestBody(await request.json())
if (body instanceof type.errors) {
console.log(body)
throw new HttpError(400)
}
@@ -118,7 +125,7 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User
}
}
const bookmark: Bookmark = {
const bookmark: TaggedBookmark = {
id: ulid(),
title: "",
url: body.url,
@@ -134,7 +141,7 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User
insertBookmark(bookmark, cachedContent, user)
if (tagNames.size > 0) {
const tagQuery = db.query<Partial<BookmarkTag>, string[]>(`
const tagQuery = db.query<Partial<Tag>, string[]>(`
SELECT id, name FROM tags
WHERE user_id = ? AND name IN (${Array(tagNames.size).fill("?").join(",")})
`)
@@ -142,7 +149,7 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User
for (const tag of tags) {
if (tag.id && tag.name) {
bookmark.tags.push(tag as BookmarkTag)
bookmark.tags.push(tag as Tag)
tagNames.delete(tag.name)
}
}
@@ -190,7 +197,7 @@ async function fetchBookmark(request: Bun.BunRequest<"/api/bookmarks/:id">, user
}
async function listUserTags(request: Bun.BunRequest<"/api/tags">, user: User) {
const query = db.query<BookmarkTag, { id: string }>("SELECT id, name FROM tags WHERE user_id = $id")
const query = db.query<Tag, { id: string }>("SELECT id, name FROM tags WHERE user_id = $id")
const tags = query.all({ id: user.id })
return Response.json(tags, { status: 200 })
}
@@ -208,7 +215,7 @@ async function createUserTag(request: Bun.BunRequest<"/api/tags">, user: User) {
throw new HttpError(400)
}
const tag: BookmarkTag = {
const tag: Tag = {
id: ulid(),
name: body.name,
}
@@ -230,6 +237,44 @@ async function listBookmarkTags(request: Bun.BunRequest<"/api/bookmarks/:id/tags
return Response.json(tags, { status: 200 })
}
async function updateBookmark(request: Bun.BunRequest<"/api/bookmarks/:id">, user: User) {
const bodyJson = await request.json().catch(() => {
throw new HttpError(400)
})
const body = UpdateBookmarkRequestBody(bodyJson)
if (body instanceof type.errors) {
throw new HttpError(400)
}
if (!body.title || !body.tags) {
return Response.json(undefined, { status: 204 })
}
const bookmark = findBookmark(request.params.id, user)
if (!bookmark) {
throw new HttpError(404)
}
if (body.title) {
updateBookmarkTitle(bookmark, body.title, user)
bookmark.title = body.title
}
if (body.tags) {
const taggedBookmark = bookmark as TaggedBookmark
for (const tag of body.tags) {
if (tag.length === 0 || /[\s#]/g.test(tag)) {
throw new HttpError(400, "InvalidTag", "Tags cannot contain '#' or whitespaces")
}
}
taggedBookmark.tags = updateBookmarkTags(bookmark, body.tags, user)
return Response.json(taggedBookmark, { status: 200 })
}
return Response.json(bookmark, { status: 200 })
}
export {
addBookmark,
fetchBookmark,
@@ -238,4 +283,5 @@ export {
listUserTags,
createUserTag,
listBookmarkTags,
updateBookmark,
}

View File

@@ -1,29 +0,0 @@
import type { BookmarkTag } from "@markone/core/bookmark"
import type { User } from "@markone/core/user"
import { ulid } from "ulid"
import { db } from "~/database.ts"
function insertTags(names: string[], user: User): BookmarkTag[] {
console.log("======== insert tags", names)
const insertTags = db.query(`
INSERT INTO tags (id, name, user_id)
VALUES ${Array(names.length).fill("(?,?,?)").join(",")}
`)
const args: Parameters<typeof insertTags.run> = []
const tags: BookmarkTag[] = []
for (const name of names) {
const tag: BookmarkTag = {
id: ulid(),
name,
}
args.push(tag.id, tag.name, user.id)
tags.push(tag)
}
insertTags.run(...args)
return tags
}
export { insertTags }