From a6e657c1233f8018b5b58935e078e03c16931ec6 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Mon, 27 Jan 2025 17:57:19 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20feat:=20Implement=20database?= =?UTF-8?q?=20schema=20for=20bookmarks=20and=20related=20content=20with=20?= =?UTF-8?q?migration=20scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../000012_split_bookmark_content.down.sql | 5 ++ .../000012_split_bookmark_content.up.sql | 77 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 database/migrations/000012_split_bookmark_content.down.sql create mode 100644 database/migrations/000012_split_bookmark_content.up.sql diff --git a/database/migrations/000012_split_bookmark_content.down.sql b/database/migrations/000012_split_bookmark_content.down.sql new file mode 100644 index 0000000..4829cd7 --- /dev/null +++ b/database/migrations/000012_split_bookmark_content.down.sql @@ -0,0 +1,5 @@ +-- Drop tables in reverse order to handle dependencies +DROP TABLE IF EXISTS bookmark_content_tags_mapping; +DROP TABLE IF EXISTS bookmark_content_tags; +DROP TABLE IF EXISTS bookmarks; +DROP TABLE IF EXISTS bookmark_content; diff --git a/database/migrations/000012_split_bookmark_content.up.sql b/database/migrations/000012_split_bookmark_content.up.sql new file mode 100644 index 0000000..0a8effd --- /dev/null +++ b/database/migrations/000012_split_bookmark_content.up.sql @@ -0,0 +1,77 @@ +CREATE TABLE bookmark_content( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(50) NOT NULL, -- type of bookmark_content(bookmark, pdf, epub, image, podcast, video, etc.) + title TEXT NOT NULL, + description TEXT, + url TEXT, -- URL of the bookmark + domain TEXT, -- domain of the URL + s3_key TEXT, -- S3 key for storing raw content like pdf, epub, video, etc. + summary TEXT, -- AI generated summary + content TEXT, -- content in markdown format + html TEXT, -- html content for web page + metadata JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +-- Indexes +CREATE INDEX idx_bookmark_content_type ON bookmark_content(type); +CREATE INDEX idx_bookmark_content_url ON bookmark_content(url); +CREATE INDEX idx_bookmark_content_domain ON bookmark_content(domain); +CREATE INDEX idx_bookmark_content_created_at ON bookmark_content(created_at); +CREATE INDEX idx_bookmark_content_metadata ON bookmark_content USING gin(metadata jsonb_path_ops); + +-- BM25 index on bookmark_content +-- https://docs.paradedb.com/documentation/indexing/create_index +CREATE INDEX idx_bookmark_content_bm25_search ON bookmark_content USING bm25(id, title, description, summary, content, metadata) WITH (key_field = 'id'); +CREATE TRIGGER update_bookmark_content_updated_at BEFORE UPDATE ON bookmark_content FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + + +-- new bookmarks table +DROP TABLE IF EXISTS bookmarks; +CREATE TABLE bookmarks ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(uuid), + content_id UUID REFERENCES bookmark_content(id), + is_favorite BOOLEAN DEFAULT FALSE, + is_archive BOOLEAN DEFAULT FALSE, + is_public BOOLEAN DEFAULT FALSE, + reading_progress INTEGER DEFAULT 0, + metadata JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +CREATE INDEX idx_bookmarks_user_created_at ON bookmarks(user_id, created_at); +CREATE INDEX idx_bookmarks_favorite ON bookmarks(user_id, is_favorite); +CREATE INDEX idx_bookmarks_archive ON bookmarks(user_id, is_archive); +CREATE INDEX idx_bookmarks_public ON bookmarks(user_id, is_public); +CREATE INDEX idx_bookmarks_metadata ON bookmarks USING gin(metadata jsonb_path_ops); +CREATE TRIGGER update_bookmarks_updated_at BEFORE UPDATE ON bookmarks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + + + +-- bookmark_content_tags table +CREATE TABLE bookmark_content_tags ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(50) NOT NULL, + user_id uuid NOT NULL REFERENCES users(uuid), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +CREATE UNIQUE INDEX uidx_bookmark_content_user_id_name ON bookmark_content_tags(user_id, name); +CREATE INDEX idx_bookmark_content_tags_name ON bookmark_content_tags(name); +CREATE TRIGGER update_bookmark_content_tags_updated_at BEFORE UPDATE ON bookmark_content_tags FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- -- bookmark_content tags relationship +CREATE TABLE bookmark_content_tags_mapping( + content_id uuid REFERENCES bookmark_content(id) ON DELETE CASCADE, + tag_id uuid REFERENCES bookmark_content_tags(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY(content_id, tag_id) + ); + +CREATE INDEX idx_bookmark_content_tags_mapping_tag_id ON bookmark_content_tags_mapping(tag_id); +CREATE TRIGGER update_bookmark_content_tags_mapping_updated_at BEFORE UPDATE ON bookmark_content_tags_mapping FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); From 1c2eff383d31a7d85787d5964b09e5bad94d1a08 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Mon, 27 Jan 2025 18:07:43 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E2=9C=A8=20feat:=20Refactor=20bookmark?= =?UTF-8?q?=20schema=20and=20queries=20to=20split=20content=20into=20separ?= =?UTF-8?q?ate=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added new migration files for splitting bookmark content into separate table - Refactored bookmark queries to work with new schema structure - Added new models for bookmark content, tags, and tag mappings - Implemented new queries for listing, searching, and managing bookmarks with tags - Added support for filtering bookmarks by domains, types, and tags - Improved search functionality to include full-text search across multiple content fields --- database/bindata.go | 48 +- database/queries/bookmarks.sql | 254 ++++++++-- internal/pkg/db/bookmarks.sql.go | 829 +++++++++++++++++++++++++------ internal/pkg/db/models.go | 55 +- 4 files changed, 966 insertions(+), 220 deletions(-) diff --git a/database/bindata.go b/database/bindata.go index 80a5774..309e078 100644 --- a/database/bindata.go +++ b/database/bindata.go @@ -22,6 +22,8 @@ // 000010_share_content.up.sql (785B) // 000011_create_s3_resources_mapping_table.down.sql (264B) // 000011_create_s3_resources_mapping_table.up.sql (1.401kB) +// 000012_split_bookmark_content.down.sql (222B) +// 000012_split_bookmark_content.up.sql (3.944kB) package migrations @@ -404,7 +406,7 @@ func _000008_comprehensive_bookmarksUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000008_comprehensive_bookmarks.up.sql", size: 4309, mode: os.FileMode(0644), modTime: time.Unix(1736065371, 0)} + info := bindataFileInfo{name: "000008_comprehensive_bookmarks.up.sql", size: 4309, mode: os.FileMode(0644), modTime: time.Unix(1737951315, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x28, 0x76, 0xf9, 0xff, 0xf9, 0x71, 0xec, 0xa6, 0x77, 0xb6, 0x23, 0x92, 0xc, 0x0, 0x54, 0x93, 0x8, 0x4e, 0xf4, 0xb7, 0xc7, 0xa9, 0x8c, 0xc2, 0x4, 0x44, 0xc6, 0xaf, 0xb9, 0xe9, 0x1d, 0xb6}} return a, nil } @@ -529,6 +531,46 @@ func _000011_create_s3_resources_mapping_tableUpSql() (*asset, error) { return a, nil } +var __000012_split_bookmark_contentDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x8c\x3f\x0a\x83\x30\x14\xc6\x77\x4f\xf1\x5d\xc0\x13\x38\xb5\x68\x41\x28\xb4\x54\x87\x6e\x21\x9a\x0f\x1b\xd4\xf7\xc2\x4b\xe8\xf9\x3b\x75\x16\xe7\xdf\x9f\xba\x46\x6b\x9a\x50\xfc\xb4\x31\x23\x0a\x8c\x5f\x5a\x26\xd4\x02\x0d\x45\xf1\xf1\x12\x36\x22\x30\x51\x02\x65\x8e\xcc\x55\xfb\x7a\x3c\x31\x5e\xae\xf7\x0e\xfd\x0d\xdd\xbb\x1f\xc6\x01\x93\xea\xba\x7b\x5b\xdd\xac\x52\x28\xc5\x15\xbf\x64\xb7\xfb\x94\xa2\x2c\xcd\x89\xe6\xc0\x3d\xe2\xff\x57\x53\xfd\x02\x00\x00\xff\xff\xe6\x1f\x88\x83\xde\x00\x00\x00") + +func _000012_split_bookmark_contentDownSqlBytes() ([]byte, error) { + return bindataRead( + __000012_split_bookmark_contentDownSql, + "000012_split_bookmark_content.down.sql", + ) +} + +func _000012_split_bookmark_contentDownSql() (*asset, error) { + bytes, err := _000012_split_bookmark_contentDownSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "000012_split_bookmark_content.down.sql", size: 222, mode: os.FileMode(0644), modTime: time.Unix(1737972290, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x22, 0x38, 0x53, 0xc9, 0xcd, 0x90, 0x6b, 0x26, 0x6a, 0xf7, 0x16, 0xd6, 0x8a, 0xd2, 0xd, 0xc8, 0x9, 0xf6, 0x57, 0x74, 0xe0, 0x31, 0xdb, 0xa4, 0xac, 0x3a, 0xa9, 0xc4, 0xab, 0xd6, 0xfa, 0x8f}} + return a, nil +} + +var __000012_split_bookmark_contentUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x56\x4f\x73\xdb\xb6\x13\xbd\xeb\x53\xec\xcd\xd4\x0c\x65\x67\x92\xf1\xe5\x97\xf9\x1d\x68\x09\xb2\xd9\xca\xa4\x4b\x91\x8d\xd3\x0b\x06\x22\x60\x1a\xb5\x08\x70\x08\xd0\x8e\xa7\xd3\xef\xde\x01\xf8\x4f\x8e\x29\x89\x8e\x7b\x48\x8f\x20\xde\x3e\x60\xf7\x3d\xec\x72\x1e\x21\x2f\x46\x10\x7b\x17\x2b\x04\x1b\x29\x1f\x72\x52\x3e\xe0\x54\x0a\xcd\x84\x76\x26\x00\x00\x9c\x42\x55\x71\x0a\x37\x91\x7f\xed\x45\x5f\xe1\x57\xf4\x15\x16\x68\xe9\x25\xab\x18\x32\x26\x70\x49\x04\x95\x39\x36\x18\x67\xea\xda\x10\xfd\x5c\x30\xf8\xdd\x8b\xe6\x57\x5e\xe4\x9c\x7f\x98\x42\x10\xc6\x10\x24\xab\x95\x0b\xb3\x59\xbd\x2b\xef\x5e\x1f\xd7\x7e\x70\xa1\xa0\x77\x2e\xb0\xa2\xda\xb8\xc0\x73\x92\x31\x17\x0a\x49\x53\xa2\xb4\x0b\x8f\x9c\x32\xe9\x02\xd3\xe9\xe9\xb4\x3e\x8c\xeb\x2d\x83\x18\xdd\xc6\xfd\x31\x76\x83\x32\x95\x96\xbc\xd0\x5c\x0a\xbb\x5d\x7f\xad\xca\x6d\xbd\x32\x57\x49\xa2\x95\xb9\x89\xbe\x67\xdd\x6d\xea\x50\x99\x13\x2e\x7a\x5c\xb3\x6e\xa0\x49\xb4\xb2\x28\xf5\x09\x3f\xb0\xe7\x1e\xb5\xfe\x04\x66\x7d\x27\x4b\x50\x5a\x96\x5c\x64\x50\x92\x27\x68\xd2\x83\x2d\x7f\x60\xbb\x99\xed\x64\x52\xd3\x55\x79\x4e\xca\x1d\x3e\xcf\x37\x05\x66\x25\xd1\x8c\xb6\xbb\x16\xd9\x32\x76\xc8\xf6\x03\x17\x60\x72\xa0\xf2\x49\x98\x6b\xe4\x44\x5b\xfc\xbd\xce\x77\x92\xb6\xab\x36\xc2\x5c\xf6\x89\x6d\xa0\x20\x19\xb3\xd8\x9c\x69\x42\x89\x26\xf0\xcb\x3a\x0c\x2e\x3a\xa5\x4f\xfe\xfa\xfb\xa4\x2e\x60\x5a\x32\x73\x21\x4c\x34\xc4\xfe\x35\x5a\xc7\xde\xf5\x0d\x7c\xf1\xe3\x2b\xbb\x84\x3f\xc2\x00\x75\x42\x74\xe1\xf3\x24\x8a\x50\x10\xe3\x2e\xa2\x11\xa3\xa0\xff\x02\xd7\x04\x60\xfa\x79\x32\x99\xcd\xc0\x17\x94\x7d\x63\x6a\xd2\xd8\xda\x0f\x16\xe8\x16\x38\xfd\x86\xbf\xf7\x1a\xb6\x26\x0c\x83\xd7\x26\x34\x1b\xd3\xcf\x23\x18\x8c\x91\x86\x08\xaa\x72\x3b\x2a\xbe\xf1\xd4\x10\x45\xbd\x35\x8a\x65\x47\x8d\x21\xa6\x7e\x7b\x14\x5b\x27\xfe\x00\x17\x24\x6b\x3f\xb8\x84\x8c\x0b\xa7\x83\xfd\xa9\xa4\xd8\xe0\x82\xe8\x7b\x2c\x0b\xd5\x88\x70\x71\xfd\xf1\x1c\xb8\x51\x02\xa4\x78\x45\x33\xb1\x0e\xd4\x85\xfa\xdf\xd9\x19\x95\xa9\x3a\x2d\x48\x49\x28\xa3\x9b\xd3\x54\xe6\xe6\x4b\x95\x33\xa1\x89\x79\xb5\x67\x96\x84\x8b\xec\xac\x4e\x03\xdb\xf5\x88\x34\x36\xf9\xc7\x73\xac\x18\x29\xd3\xfb\x03\x99\x18\x94\xc3\xa9\x5b\x77\x10\x77\xb7\x5f\xb8\xed\x73\x73\xdb\x87\xe2\x76\x0f\x63\x5a\x1b\xd4\x79\x60\xcf\xf8\x8e\xb3\x2d\x85\xff\xc3\x09\xa7\x27\x7d\x81\xe3\xc8\xbf\xbc\x44\x51\xe3\xee\x01\xe7\xf4\xae\xbf\x40\xcb\x30\x42\x90\xdc\x2c\x4c\xe0\xd0\x5d\x97\x61\x04\xc8\x9b\x5f\x41\x14\x7e\x01\x74\x8b\xe6\x49\x8c\x60\x99\x04\xf3\xd8\x0f\x83\xf6\x88\x9e\x11\xa7\x72\x5b\xe5\xc2\x31\x5a\x98\x52\x0b\xf6\xd4\x71\x2a\xd0\x64\xb3\x65\x93\x45\x14\xde\x34\x2d\xdf\x5f\x02\xba\xf5\xd7\xf1\xba\x07\xf5\x69\xbc\x18\x0a\x0a\x7e\x78\x1c\x54\x8a\x95\x98\x53\x48\x12\x7f\x01\x11\x5a\xa2\x08\x05\x73\xb4\xb6\xdf\x95\x63\x90\x0d\xb0\x2d\xd0\x00\xf6\x95\xb3\xbb\x20\xae\xf0\x1d\x79\x94\x25\xd7\x0c\x2e\xc2\x70\x85\xbc\xa0\xbb\xd0\xd2\x5b\xad\x51\x07\x33\x7e\xe0\x8f\xc7\x50\x45\xb5\xd9\xf2\xf4\x10\xa8\x64\x84\x72\x91\xe1\xa2\x94\x59\xc9\x94\x02\x3f\x88\x91\x11\xbc\xc5\x7e\x70\xff\xe3\xad\x74\xef\x13\x53\xd8\x8a\x39\xdc\x75\x94\xd3\x28\xed\xc2\xb8\xbe\xb3\xa3\xdc\x30\xcd\x8e\xb6\x87\x79\x5a\x69\xf7\xd2\x34\x80\xc3\x2c\x8d\xf4\x7b\x49\xea\xfd\xc3\x1c\x43\x2d\x54\x8d\xea\x9d\x87\xbb\x87\x1a\xd5\x36\xd4\xbb\xfa\x85\x69\x18\xaf\x27\x25\xc9\xda\xc6\x71\xf0\x6f\xb1\x06\xfe\x70\x93\x10\x24\xdf\xf3\xcf\xf8\xa2\x87\x58\xda\xce\xc7\x07\x9b\xc9\x4f\xfe\xb8\x92\xc0\xff\x2d\x69\x3d\x54\x0d\xff\x62\xd4\x49\x63\x5b\x9b\x81\xe1\x60\x4b\xde\x3b\xd4\xc0\x46\x8d\x79\x13\x76\x84\xf4\x25\xd7\xb1\x89\x66\x09\xdf\x32\xd6\x6a\xb3\xbc\xc3\xab\xb3\x19\x0c\x98\x15\x2c\x6d\xc9\xb6\xf6\xff\x41\xdd\xf3\x62\x84\x67\x71\x4e\x8a\x82\x8b\xcc\xf9\x7e\x06\x59\xaf\x1d\x99\x41\x26\xb9\x05\x5a\xa1\x18\xc1\xdc\x5b\xcf\xbd\x45\x33\x23\x34\xc9\xc6\x50\xd4\xc5\x3e\xc0\xf3\xf3\xb9\xd8\x05\x4b\xb6\xf3\xb4\x9d\xbe\x66\x6e\x93\xf8\xf4\xd8\x20\x19\x54\x00\x37\x55\xdb\x67\x98\x4e\xa9\xe6\x90\xb7\xf9\xb3\x3d\xe4\xcd\x3e\x6d\x23\xdf\xe1\xd7\x7f\x02\x00\x00\xff\xff\x38\xa1\x7a\xd6\x68\x0f\x00\x00") + +func _000012_split_bookmark_contentUpSqlBytes() ([]byte, error) { + return bindataRead( + __000012_split_bookmark_contentUpSql, + "000012_split_bookmark_content.up.sql", + ) +} + +func _000012_split_bookmark_contentUpSql() (*asset, error) { + bytes, err := _000012_split_bookmark_contentUpSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 3944, mode: os.FileMode(0644), modTime: time.Unix(1737971608, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x44, 0x6a, 0x9e, 0xa7, 0xa1, 0x3a, 0x89, 0x4, 0x27, 0xa6, 0x72, 0xde, 0x3a, 0x20, 0x51, 0x65, 0x3b, 0x7a, 0x33, 0xc3, 0x80, 0xd7, 0x63, 0xa3, 0x56, 0x36, 0x99, 0x99, 0x4d, 0x94, 0x2f, 0xec}} + return a, nil +} + // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. @@ -642,6 +684,8 @@ var _bindata = map[string]func() (*asset, error){ "000010_share_content.up.sql": _000010_share_contentUpSql, "000011_create_s3_resources_mapping_table.down.sql": _000011_create_s3_resources_mapping_tableDownSql, "000011_create_s3_resources_mapping_table.up.sql": _000011_create_s3_resources_mapping_tableUpSql, + "000012_split_bookmark_content.down.sql": _000012_split_bookmark_contentDownSql, + "000012_split_bookmark_content.up.sql": _000012_split_bookmark_contentUpSql, } // AssetDebug is true if the assets were built with the debug flag enabled. @@ -712,6 +756,8 @@ var _bintree = &bintree{nil, map[string]*bintree{ "000010_share_content.up.sql": {_000010_share_contentUpSql, map[string]*bintree{}}, "000011_create_s3_resources_mapping_table.down.sql": {_000011_create_s3_resources_mapping_tableDownSql, map[string]*bintree{}}, "000011_create_s3_resources_mapping_table.up.sql": {_000011_create_s3_resources_mapping_tableUpSql, map[string]*bintree{}}, + "000012_split_bookmark_content.down.sql": {_000012_split_bookmark_contentDownSql, map[string]*bintree{}}, + "000012_split_bookmark_content.up.sql": {_000012_split_bookmark_contentUpSql, map[string]*bintree{}}, }} // RestoreAsset restores an asset under the given directory. diff --git a/database/queries/bookmarks.sql b/database/queries/bookmarks.sql index 953d1be..827425a 100644 --- a/database/queries/bookmarks.sql +++ b/database/queries/bookmarks.sql @@ -1,62 +1,216 @@ --- name: CreateBookmark :one -INSERT INTO bookmarks ( - uuid, - user_id, - url, - title, - summary, - summary_embeddings, - content, - content_embeddings, - html, - metadata, - screenshot -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 -) RETURNING *; - --- name: GetBookmarkByUUID :one -SELECT * FROM bookmarks WHERE uuid = $1; - --- name: GetBookmarkByURL :one -SELECT * FROM bookmarks WHERE url = $1 AND user_id = $2; - -- name: ListBookmarks :many WITH total AS ( - SELECT COUNT(*) AS total_count - FROM bookmarks - WHERE user_id = $1 + SELECT COUNT(DISTINCT b.*) AS total_count + FROM bookmarks AS b + JOIN bookmark_content AS bc ON b.content_id = bc.id + LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + WHERE b.user_id = $1 + AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) + AND (sqlc.narg('types')::text[] IS NULL OR bc.type = ANY(sqlc.narg('types')::text[])) + AND (sqlc.narg('tags')::text[] IS NULL OR bct.name = ANY(sqlc.narg('tags')::text[])) ) -SELECT b.*, t.total_count -FROM bookmarks b, total t -WHERE b.user_id = $1 -ORDER BY b.updated_at DESC +SELECT b.*, + bc.*, + t.total_count, + COALESCE( + array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), + ARRAY[]::VARCHAR[] + ) AS tags +FROM bookmarks AS b + JOIN bookmark_content AS bc ON b.content_id = bc.id + CROSS JOIN total AS t + LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id +WHERE b.user_id = $1 + AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) + AND (sqlc.narg('types')::text[] IS NULL OR bc.type = ANY(sqlc.narg('types')::text[])) + AND (sqlc.narg('tags')::text[] IS NULL OR bct.name = ANY(sqlc.narg('tags')::text[])) +GROUP BY b.id, bc.id, t.total_count +ORDER BY b.created_at DESC LIMIT $2 OFFSET $3; +-- name: SearchBookmarks :many +WITH total AS ( + SELECT COUNT(DISTINCT b.*) AS total_count + FROM bookmarks AS b + JOIN bookmark_content AS bc ON b.content_id = bc.id + LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + WHERE b.user_id = $1 + AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) + AND (sqlc.narg('types')::text[] IS NULL OR bc.type = ANY(sqlc.narg('types')::text[])) + AND (sqlc.narg('tags')::text[] IS NULL OR bct.name = ANY(sqlc.narg('tags')::text[])) + AND ( + sqlc.narg('query')::text IS NULL + OR bc.title @@@ sqlc.narg('query') + OR bc.description @@@ sqlc.narg('query') + OR bc.summary @@@ sqlc.narg('query') + OR bc.content @@@ sqlc.narg('query') + OR bc.metadata @@@ sqlc.narg('query') + ) +) +SELECT b.*, + bc.*, + t.total_count, + COALESCE( + array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), + ARRAY[]::VARCHAR[] + ) AS tags +FROM bookmarks AS b + JOIN bookmark_content AS bc ON b.content_id = bc.id + CROSS JOIN total AS t + LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id +WHERE b.user_id = $1 + AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) + AND (sqlc.narg('types')::text[] IS NULL OR bc.type = ANY(sqlc.narg('types')::text[])) + AND (sqlc.narg('tags')::text[] IS NULL OR bct.name = ANY(sqlc.narg('tags')::text[])) + AND ( + sqlc.narg('query')::text IS NULL + OR bc.title @@@ sqlc.narg('query') + OR bc.description @@@ sqlc.narg('query') + OR bc.summary @@@ sqlc.narg('query') + OR bc.content @@@ sqlc.narg('query') + OR bc.metadata @@@ sqlc.narg('query') + ) +GROUP BY b.id, bc.id, t.total_count +ORDER BY b.created_at DESC +LIMIT $2 OFFSET $3; + +-- name: GetBookmark :one +SELECT b.*, + bc.*, + COALESCE( + array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), + ARRAY[]::VARCHAR[] + ) as tags +FROM bookmarks b + JOIN bookmark_content bc ON b.content_id = bc.id + LEFT JOIN bookmark_content_tags_mapping bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags bct ON bctm.tag_id = bct.id +WHERE b.id = $1 + AND b.user_id = $2 +GROUP BY b.id, bc.id +LIMIT 1; + +-- name: IsBookmarkExistWithURL :one +SELECT EXISTS ( + SELECT 1 + FROM bookmarks b + JOIN bookmark_content bc ON b.content_id = bc.id + WHERE bc.url = $1 + AND b.user_id = $2 +); + +-- name: CreateBookmarkContent :one +INSERT INTO bookmark_content ( + type, title, description, url, domain, s3_key, + summary, content, html, metadata +) +VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 +) +RETURNING *; + +-- name: CreateBookmark :one +INSERT INTO bookmarks ( + user_id, content_id, is_favorite, is_archive, + is_public, reading_progress, metadata +) +VALUES ( + $1, $2, $3, $4, $5, $6, $7 +) +RETURNING *; + -- name: UpdateBookmark :one -UPDATE bookmarks -SET - title = COALESCE($3, title), - summary = COALESCE($4, summary), - summary_embeddings = COALESCE($5, summary_embeddings), - content = COALESCE($6, content), - content_embeddings = COALESCE($7, content_embeddings), - html = COALESCE($8, html), - metadata = COALESCE($9, metadata), - screenshot = COALESCE($10, screenshot), - updated_at = CURRENT_TIMESTAMP -WHERE uuid = $1 AND user_id = $2 +UPDATE bookmarks +SET is_favorite = COALESCE(sqlc.narg('is_favorite'), is_favorite), + is_archive = COALESCE(sqlc.narg('is_archive'), is_archive), + is_public = COALESCE(sqlc.narg('is_public'), is_public), + reading_progress = COALESCE(sqlc.narg('reading_progress'), reading_progress), + metadata = COALESCE(sqlc.narg('metadata'), metadata) +WHERE id = $1 + AND user_id = $2 +RETURNING *; + +-- name: UpdateBookmarkContent :one +UPDATE bookmark_content +SET title = COALESCE(sqlc.narg('title'), title), + description = COALESCE(sqlc.narg('description'), description), + url = COALESCE(sqlc.narg('url'), url), + domain = COALESCE(sqlc.narg('domain'), domain), + s3_key = COALESCE(sqlc.narg('s3_key'), s3_key), + summary = COALESCE(sqlc.narg('summary'), summary), + content = COALESCE(sqlc.narg('content'), content), + html = COALESCE(sqlc.narg('html'), html), + metadata = COALESCE(sqlc.narg('metadata'), metadata) +WHERE id = $1 RETURNING *; -- name: DeleteBookmark :exec -DELETE FROM bookmarks WHERE uuid = $1 AND user_id = $2; +DELETE FROM bookmarks +WHERE id = $1 AND user_id = $2; -- name: DeleteBookmarksByUser :exec -DELETE FROM bookmarks WHERE user_id = $1; - --- name: OwnerTransferBookmark :exec -UPDATE bookmarks -SET - user_id = $2, - updated_at = CURRENT_TIMESTAMP +DELETE FROM bookmarks WHERE user_id = $1; + +-- Tags related queries similar to content.sql but adapted for new schema +-- name: ListBookmarkTagsByUser :many +SELECT bct.name, count(bctm.*) as count +FROM bookmark_content_tags bct + JOIN bookmark_content_tags_mapping bctm ON bct.id = bctm.tag_id +WHERE bct.user_id = $1 +GROUP BY bct.name +ORDER BY count DESC; + +-- name: ListBookmarkContentTags :many +SELECT bct.name +FROM bookmark_content_tags bct + JOIN bookmark_content_tags_mapping bctm ON bct.id = bctm.tag_id +WHERE bctm.content_id = $1 + AND bct.user_id = $2; + +-- name: ListBookmarkDomains :many +SELECT bc.domain, count(*) as count +FROM bookmarks b + JOIN bookmark_content bc ON b.content_id = bc.id +WHERE b.user_id = $1 +AND bc.domain IS NOT NULL +GROUP BY bc.domain +ORDER BY count DESC, domain ASC; + +-- name: CreateBookmarkContentTag :one +INSERT INTO bookmark_content_tags (name, user_id) +VALUES ($1, $2) +ON CONFLICT (name, user_id) DO UPDATE + SET usage_count = bookmark_content_tags.usage_count + 1 +RETURNING *; + +-- name: DeleteBookmarkContentTag :exec +DELETE FROM bookmark_content_tags +WHERE id = $1 + AND user_id = $2; + +-- name: LinkBookmarkContentWithTags :exec +INSERT INTO bookmark_content_tags_mapping (content_id, tag_id) +SELECT $1, bct.id +FROM bookmark_content_tags bct +WHERE bct.name = ANY ($2::text[]) + AND bct.user_id = $3; + +-- name: UnLinkBookmarkContentWithTags :exec +DELETE FROM bookmark_content_tags_mapping +WHERE content_id = $1 + AND tag_id IN (SELECT id + FROM bookmark_content_tags + WHERE name = ANY ($2::text[]) + AND user_id = $3); + +-- name: ListExistingBookmarkTagsByTags :many +SELECT name +FROM bookmark_content_tags +WHERE name = ANY ($1::text[]) + AND user_id = $2; + diff --git a/internal/pkg/db/bookmarks.sql.go b/internal/pkg/db/bookmarks.sql.go index 4f7c7f9..04220a3 100644 --- a/internal/pkg/db/bookmarks.sql.go +++ b/internal/pkg/db/bookmarks.sql.go @@ -10,69 +10,131 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" - pgv "github.com/pgvector/pgvector-go" ) const createBookmark = `-- name: CreateBookmark :one INSERT INTO bookmarks ( - uuid, - user_id, - url, - title, - summary, - summary_embeddings, - content, - content_embeddings, - html, - metadata, - screenshot -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 -) RETURNING id, uuid, user_id, url, title, summary, summary_embeddings, content, content_embeddings, html, metadata, screenshot, created_at, updated_at + user_id, content_id, is_favorite, is_archive, + is_public, reading_progress, metadata +) +VALUES ( + $1, $2, $3, $4, $5, $6, $7 +) +RETURNING id, user_id, content_id, is_favorite, is_archive, is_public, reading_progress, metadata, created_at, updated_at ` type CreateBookmarkParams struct { - Uuid uuid.UUID - UserID pgtype.UUID - Url string - Title pgtype.Text - Summary pgtype.Text - SummaryEmbeddings *pgv.Vector - Content pgtype.Text - ContentEmbeddings *pgv.Vector - Html pgtype.Text - Metadata []byte - Screenshot pgtype.Text + UserID pgtype.UUID + ContentID pgtype.UUID + IsFavorite pgtype.Bool + IsArchive pgtype.Bool + IsPublic pgtype.Bool + ReadingProgress pgtype.Int4 + Metadata []byte } func (q *Queries) CreateBookmark(ctx context.Context, db DBTX, arg CreateBookmarkParams) (Bookmark, error) { row := db.QueryRow(ctx, createBookmark, - arg.Uuid, arg.UserID, - arg.Url, + arg.ContentID, + arg.IsFavorite, + arg.IsArchive, + arg.IsPublic, + arg.ReadingProgress, + arg.Metadata, + ) + var i Bookmark + err := row.Scan( + &i.ID, + &i.UserID, + &i.ContentID, + &i.IsFavorite, + &i.IsArchive, + &i.IsPublic, + &i.ReadingProgress, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const createBookmarkContent = `-- name: CreateBookmarkContent :one +INSERT INTO bookmark_content ( + type, title, description, url, domain, s3_key, + summary, content, html, metadata +) +VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 +) +RETURNING id, type, title, description, url, domain, s3_key, summary, content, html, metadata, created_at, updated_at +` + +type CreateBookmarkContentParams struct { + Type string + Title string + Description pgtype.Text + Url pgtype.Text + Domain pgtype.Text + S3Key pgtype.Text + Summary pgtype.Text + Content pgtype.Text + Html pgtype.Text + Metadata []byte +} + +func (q *Queries) CreateBookmarkContent(ctx context.Context, db DBTX, arg CreateBookmarkContentParams) (BookmarkContent, error) { + row := db.QueryRow(ctx, createBookmarkContent, + arg.Type, arg.Title, + arg.Description, + arg.Url, + arg.Domain, + arg.S3Key, arg.Summary, - arg.SummaryEmbeddings, arg.Content, - arg.ContentEmbeddings, arg.Html, arg.Metadata, - arg.Screenshot, ) - var i Bookmark + var i BookmarkContent err := row.Scan( &i.ID, - &i.Uuid, - &i.UserID, - &i.Url, + &i.Type, &i.Title, + &i.Description, + &i.Url, + &i.Domain, + &i.S3Key, &i.Summary, - &i.SummaryEmbeddings, &i.Content, - &i.ContentEmbeddings, &i.Html, &i.Metadata, - &i.Screenshot, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const createBookmarkContentTag = `-- name: CreateBookmarkContentTag :one +INSERT INTO bookmark_content_tags (name, user_id) +VALUES ($1, $2) +ON CONFLICT (name, user_id) DO UPDATE + SET usage_count = bookmark_content_tags.usage_count + 1 +RETURNING id, name, user_id, created_at, updated_at +` + +type CreateBookmarkContentTagParams struct { + Name string + UserID uuid.UUID +} + +func (q *Queries) CreateBookmarkContentTag(ctx context.Context, db DBTX, arg CreateBookmarkContentTagParams) (BookmarkContentTag, error) { + row := db.QueryRow(ctx, createBookmarkContentTag, arg.Name, arg.UserID) + var i BookmarkContentTag + err := row.Scan( + &i.ID, + &i.Name, + &i.UserID, &i.CreatedAt, &i.UpdatedAt, ) @@ -80,21 +142,39 @@ func (q *Queries) CreateBookmark(ctx context.Context, db DBTX, arg CreateBookmar } const deleteBookmark = `-- name: DeleteBookmark :exec -DELETE FROM bookmarks WHERE uuid = $1 AND user_id = $2 +DELETE FROM bookmarks +WHERE id = $1 AND user_id = $2 ` type DeleteBookmarkParams struct { - Uuid uuid.UUID + ID uuid.UUID UserID pgtype.UUID } func (q *Queries) DeleteBookmark(ctx context.Context, db DBTX, arg DeleteBookmarkParams) error { - _, err := db.Exec(ctx, deleteBookmark, arg.Uuid, arg.UserID) + _, err := db.Exec(ctx, deleteBookmark, arg.ID, arg.UserID) + return err +} + +const deleteBookmarkContentTag = `-- name: DeleteBookmarkContentTag :exec +DELETE FROM bookmark_content_tags +WHERE id = $1 + AND user_id = $2 +` + +type DeleteBookmarkContentTagParams struct { + ID uuid.UUID + UserID uuid.UUID +} + +func (q *Queries) DeleteBookmarkContentTag(ctx context.Context, db DBTX, arg DeleteBookmarkContentTagParams) error { + _, err := db.Exec(ctx, deleteBookmarkContentTag, arg.ID, arg.UserID) return err } const deleteBookmarksByUser = `-- name: DeleteBookmarksByUser :exec -DELETE FROM bookmarks WHERE user_id = $1 +DELETE FROM bookmarks +WHERE user_id = $1 ` func (q *Queries) DeleteBookmarksByUser(ctx context.Context, db DBTX, userID pgtype.UUID) error { @@ -102,102 +182,310 @@ func (q *Queries) DeleteBookmarksByUser(ctx context.Context, db DBTX, userID pgt return err } -const getBookmarkByURL = `-- name: GetBookmarkByURL :one -SELECT id, uuid, user_id, url, title, summary, summary_embeddings, content, content_embeddings, html, metadata, screenshot, created_at, updated_at FROM bookmarks WHERE url = $1 AND user_id = $2 +const getBookmark = `-- name: GetBookmark :one +SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, + bc.id, bc.type, bc.title, bc.description, bc.url, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.metadata, bc.created_at, bc.updated_at, + COALESCE( + array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), + ARRAY[]::VARCHAR[] + ) as tags +FROM bookmarks b + JOIN bookmark_content bc ON b.content_id = bc.id + LEFT JOIN bookmark_content_tags_mapping bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags bct ON bctm.tag_id = bct.id +WHERE b.id = $1 + AND b.user_id = $2 +GROUP BY b.id, bc.id +LIMIT 1 ` -type GetBookmarkByURLParams struct { - Url string +type GetBookmarkParams struct { + ID uuid.UUID UserID pgtype.UUID } -func (q *Queries) GetBookmarkByURL(ctx context.Context, db DBTX, arg GetBookmarkByURLParams) (Bookmark, error) { - row := db.QueryRow(ctx, getBookmarkByURL, arg.Url, arg.UserID) - var i Bookmark +type GetBookmarkRow struct { + ID uuid.UUID + UserID pgtype.UUID + ContentID pgtype.UUID + IsFavorite pgtype.Bool + IsArchive pgtype.Bool + IsPublic pgtype.Bool + ReadingProgress pgtype.Int4 + Metadata []byte + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz + ID_2 uuid.UUID + Type string + Title string + Description pgtype.Text + Url pgtype.Text + Domain pgtype.Text + S3Key pgtype.Text + Summary pgtype.Text + Content pgtype.Text + Html pgtype.Text + Metadata_2 []byte + CreatedAt_2 pgtype.Timestamptz + UpdatedAt_2 pgtype.Timestamptz + Tags interface{} +} + +func (q *Queries) GetBookmark(ctx context.Context, db DBTX, arg GetBookmarkParams) (GetBookmarkRow, error) { + row := db.QueryRow(ctx, getBookmark, arg.ID, arg.UserID) + var i GetBookmarkRow err := row.Scan( &i.ID, - &i.Uuid, &i.UserID, - &i.Url, + &i.ContentID, + &i.IsFavorite, + &i.IsArchive, + &i.IsPublic, + &i.ReadingProgress, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.ID_2, + &i.Type, &i.Title, + &i.Description, + &i.Url, + &i.Domain, + &i.S3Key, &i.Summary, - &i.SummaryEmbeddings, &i.Content, - &i.ContentEmbeddings, &i.Html, - &i.Metadata, - &i.Screenshot, - &i.CreatedAt, - &i.UpdatedAt, + &i.Metadata_2, + &i.CreatedAt_2, + &i.UpdatedAt_2, + &i.Tags, ) return i, err } -const getBookmarkByUUID = `-- name: GetBookmarkByUUID :one -SELECT id, uuid, user_id, url, title, summary, summary_embeddings, content, content_embeddings, html, metadata, screenshot, created_at, updated_at FROM bookmarks WHERE uuid = $1 +const isBookmarkExistWithURL = `-- name: IsBookmarkExistWithURL :one +SELECT EXISTS ( + SELECT 1 + FROM bookmarks b + JOIN bookmark_content bc ON b.content_id = bc.id + WHERE bc.url = $1 + AND b.user_id = $2 +) ` -func (q *Queries) GetBookmarkByUUID(ctx context.Context, db DBTX, argUuid uuid.UUID) (Bookmark, error) { - row := db.QueryRow(ctx, getBookmarkByUUID, argUuid) - var i Bookmark - err := row.Scan( - &i.ID, - &i.Uuid, - &i.UserID, - &i.Url, - &i.Title, - &i.Summary, - &i.SummaryEmbeddings, - &i.Content, - &i.ContentEmbeddings, - &i.Html, - &i.Metadata, - &i.Screenshot, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err +type IsBookmarkExistWithURLParams struct { + Url pgtype.Text + UserID pgtype.UUID +} + +func (q *Queries) IsBookmarkExistWithURL(ctx context.Context, db DBTX, arg IsBookmarkExistWithURLParams) (bool, error) { + row := db.QueryRow(ctx, isBookmarkExistWithURL, arg.Url, arg.UserID) + var exists bool + err := row.Scan(&exists) + return exists, err +} + +const linkBookmarkContentWithTags = `-- name: LinkBookmarkContentWithTags :exec +INSERT INTO bookmark_content_tags_mapping (content_id, tag_id) +SELECT $1, bct.id +FROM bookmark_content_tags bct +WHERE bct.name = ANY ($2::text[]) + AND bct.user_id = $3 +` + +type LinkBookmarkContentWithTagsParams struct { + ContentID uuid.UUID + Column2 []string + UserID uuid.UUID +} + +func (q *Queries) LinkBookmarkContentWithTags(ctx context.Context, db DBTX, arg LinkBookmarkContentWithTagsParams) error { + _, err := db.Exec(ctx, linkBookmarkContentWithTags, arg.ContentID, arg.Column2, arg.UserID) + return err +} + +const listBookmarkContentTags = `-- name: ListBookmarkContentTags :many +SELECT bct.name +FROM bookmark_content_tags bct + JOIN bookmark_content_tags_mapping bctm ON bct.id = bctm.tag_id +WHERE bctm.content_id = $1 + AND bct.user_id = $2 +` + +type ListBookmarkContentTagsParams struct { + ContentID uuid.UUID + UserID uuid.UUID +} + +func (q *Queries) ListBookmarkContentTags(ctx context.Context, db DBTX, arg ListBookmarkContentTagsParams) ([]string, error) { + rows, err := db.Query(ctx, listBookmarkContentTags, arg.ContentID, arg.UserID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + items = append(items, name) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listBookmarkDomains = `-- name: ListBookmarkDomains :many +SELECT bc.domain, count(*) as count +FROM bookmarks b + JOIN bookmark_content bc ON b.content_id = bc.id +WHERE b.user_id = $1 +AND bc.domain IS NOT NULL +GROUP BY bc.domain +ORDER BY count DESC, domain ASC +` + +type ListBookmarkDomainsRow struct { + Domain pgtype.Text + Count int64 +} + +func (q *Queries) ListBookmarkDomains(ctx context.Context, db DBTX, userID pgtype.UUID) ([]ListBookmarkDomainsRow, error) { + rows, err := db.Query(ctx, listBookmarkDomains, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListBookmarkDomainsRow + for rows.Next() { + var i ListBookmarkDomainsRow + if err := rows.Scan(&i.Domain, &i.Count); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listBookmarkTagsByUser = `-- name: ListBookmarkTagsByUser :many +SELECT bct.name, count(bctm.*) as count +FROM bookmark_content_tags bct + JOIN bookmark_content_tags_mapping bctm ON bct.id = bctm.tag_id +WHERE bct.user_id = $1 +GROUP BY bct.name +ORDER BY count DESC +` + +type ListBookmarkTagsByUserRow struct { + Name string + Count int64 +} + +// Tags related queries similar to content.sql but adapted for new schema +func (q *Queries) ListBookmarkTagsByUser(ctx context.Context, db DBTX, userID uuid.UUID) ([]ListBookmarkTagsByUserRow, error) { + rows, err := db.Query(ctx, listBookmarkTagsByUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListBookmarkTagsByUserRow + for rows.Next() { + var i ListBookmarkTagsByUserRow + if err := rows.Scan(&i.Name, &i.Count); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil } const listBookmarks = `-- name: ListBookmarks :many WITH total AS ( - SELECT COUNT(*) AS total_count - FROM bookmarks - WHERE user_id = $1 + SELECT COUNT(DISTINCT b.*) AS total_count + FROM bookmarks AS b + JOIN bookmark_content AS bc ON b.content_id = bc.id + LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + WHERE b.user_id = $1 + AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) + AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) + AND ($6::text[] IS NULL OR bct.name = ANY($6::text[])) ) -SELECT b.id, b.uuid, b.user_id, b.url, b.title, b.summary, b.summary_embeddings, b.content, b.content_embeddings, b.html, b.metadata, b.screenshot, b.created_at, b.updated_at, t.total_count -FROM bookmarks b, total t -WHERE b.user_id = $1 -ORDER BY b.updated_at DESC +SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, + bc.id, bc.type, bc.title, bc.description, bc.url, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.metadata, bc.created_at, bc.updated_at, + t.total_count, + COALESCE( + array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), + ARRAY[]::VARCHAR[] + ) AS tags +FROM bookmarks AS b + JOIN bookmark_content AS bc ON b.content_id = bc.id + CROSS JOIN total AS t + LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id +WHERE b.user_id = $1 + AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) + AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) + AND ($6::text[] IS NULL OR bct.name = ANY($6::text[])) +GROUP BY b.id, bc.id, t.total_count +ORDER BY b.created_at DESC LIMIT $2 OFFSET $3 ` type ListBookmarksParams struct { - UserID pgtype.UUID - Limit int32 - Offset int32 + UserID pgtype.UUID + Limit int32 + Offset int32 + Domains []string + Types []string + Tags []string } type ListBookmarksRow struct { - ID int32 - Uuid uuid.UUID - UserID pgtype.UUID - Url string - Title pgtype.Text - Summary pgtype.Text - SummaryEmbeddings *pgv.Vector - Content pgtype.Text - ContentEmbeddings *pgv.Vector - Html pgtype.Text - Metadata []byte - Screenshot pgtype.Text - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz - TotalCount int64 + ID uuid.UUID + UserID pgtype.UUID + ContentID pgtype.UUID + IsFavorite pgtype.Bool + IsArchive pgtype.Bool + IsPublic pgtype.Bool + ReadingProgress pgtype.Int4 + Metadata []byte + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz + ID_2 uuid.UUID + Type string + Title string + Description pgtype.Text + Url pgtype.Text + Domain pgtype.Text + S3Key pgtype.Text + Summary pgtype.Text + Content pgtype.Text + Html pgtype.Text + Metadata_2 []byte + CreatedAt_2 pgtype.Timestamptz + UpdatedAt_2 pgtype.Timestamptz + TotalCount int64 + Tags interface{} } func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksParams) ([]ListBookmarksRow, error) { - rows, err := db.Query(ctx, listBookmarks, arg.UserID, arg.Limit, arg.Offset) + rows, err := db.Query(ctx, listBookmarks, + arg.UserID, + arg.Limit, + arg.Offset, + arg.Domains, + arg.Types, + arg.Tags, + ) if err != nil { return nil, err } @@ -207,20 +495,203 @@ func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksP var i ListBookmarksRow if err := rows.Scan( &i.ID, - &i.Uuid, &i.UserID, - &i.Url, + &i.ContentID, + &i.IsFavorite, + &i.IsArchive, + &i.IsPublic, + &i.ReadingProgress, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.ID_2, + &i.Type, &i.Title, + &i.Description, + &i.Url, + &i.Domain, + &i.S3Key, &i.Summary, - &i.SummaryEmbeddings, &i.Content, - &i.ContentEmbeddings, &i.Html, + &i.Metadata_2, + &i.CreatedAt_2, + &i.UpdatedAt_2, + &i.TotalCount, + &i.Tags, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listExistingBookmarkTagsByTags = `-- name: ListExistingBookmarkTagsByTags :many +SELECT name +FROM bookmark_content_tags +WHERE name = ANY ($1::text[]) + AND user_id = $2 +` + +type ListExistingBookmarkTagsByTagsParams struct { + Column1 []string + UserID uuid.UUID +} + +func (q *Queries) ListExistingBookmarkTagsByTags(ctx context.Context, db DBTX, arg ListExistingBookmarkTagsByTagsParams) ([]string, error) { + rows, err := db.Query(ctx, listExistingBookmarkTagsByTags, arg.Column1, arg.UserID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + items = append(items, name) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchBookmarks = `-- name: SearchBookmarks :many +WITH total AS ( + SELECT COUNT(DISTINCT b.*) AS total_count + FROM bookmarks AS b + JOIN bookmark_content AS bc ON b.content_id = bc.id + LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + WHERE b.user_id = $1 + AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) + AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) + AND ($6::text[] IS NULL OR bct.name = ANY($6::text[])) + AND ( + $7::text IS NULL + OR bc.title @@@ $7 + OR bc.description @@@ $7 + OR bc.summary @@@ $7 + OR bc.content @@@ $7 + OR bc.metadata @@@ $7 + ) +) +SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, + bc.id, bc.type, bc.title, bc.description, bc.url, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.metadata, bc.created_at, bc.updated_at, + t.total_count, + COALESCE( + array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), + ARRAY[]::VARCHAR[] + ) AS tags +FROM bookmarks AS b + JOIN bookmark_content AS bc ON b.content_id = bc.id + CROSS JOIN total AS t + LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id +WHERE b.user_id = $1 + AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) + AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) + AND ($6::text[] IS NULL OR bct.name = ANY($6::text[])) + AND ( + $7::text IS NULL + OR bc.title @@@ $7 + OR bc.description @@@ $7 + OR bc.summary @@@ $7 + OR bc.content @@@ $7 + OR bc.metadata @@@ $7 + ) +GROUP BY b.id, bc.id, t.total_count +ORDER BY b.created_at DESC +LIMIT $2 OFFSET $3 +` + +type SearchBookmarksParams struct { + UserID pgtype.UUID + Limit int32 + Offset int32 + Domains []string + Types []string + Tags []string + Query pgtype.Text +} + +type SearchBookmarksRow struct { + ID uuid.UUID + UserID pgtype.UUID + ContentID pgtype.UUID + IsFavorite pgtype.Bool + IsArchive pgtype.Bool + IsPublic pgtype.Bool + ReadingProgress pgtype.Int4 + Metadata []byte + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz + ID_2 uuid.UUID + Type string + Title string + Description pgtype.Text + Url pgtype.Text + Domain pgtype.Text + S3Key pgtype.Text + Summary pgtype.Text + Content pgtype.Text + Html pgtype.Text + Metadata_2 []byte + CreatedAt_2 pgtype.Timestamptz + UpdatedAt_2 pgtype.Timestamptz + TotalCount int64 + Tags interface{} +} + +func (q *Queries) SearchBookmarks(ctx context.Context, db DBTX, arg SearchBookmarksParams) ([]SearchBookmarksRow, error) { + rows, err := db.Query(ctx, searchBookmarks, + arg.UserID, + arg.Limit, + arg.Offset, + arg.Domains, + arg.Types, + arg.Tags, + arg.Query, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SearchBookmarksRow + for rows.Next() { + var i SearchBookmarksRow + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.ContentID, + &i.IsFavorite, + &i.IsArchive, + &i.IsPublic, + &i.ReadingProgress, &i.Metadata, - &i.Screenshot, &i.CreatedAt, &i.UpdatedAt, + &i.ID_2, + &i.Type, + &i.Title, + &i.Description, + &i.Url, + &i.Domain, + &i.S3Key, + &i.Summary, + &i.Content, + &i.Html, + &i.Metadata_2, + &i.CreatedAt_2, + &i.UpdatedAt_2, &i.TotalCount, + &i.Tags, ); err != nil { return nil, err } @@ -232,80 +703,128 @@ func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksP return items, nil } -const ownerTransferBookmark = `-- name: OwnerTransferBookmark :exec -UPDATE bookmarks -SET - user_id = $2, - updated_at = CURRENT_TIMESTAMP -WHERE user_id = $1 +const unLinkBookmarkContentWithTags = `-- name: UnLinkBookmarkContentWithTags :exec +DELETE FROM bookmark_content_tags_mapping +WHERE content_id = $1 + AND tag_id IN (SELECT id + FROM bookmark_content_tags + WHERE name = ANY ($2::text[]) + AND user_id = $3) ` -type OwnerTransferBookmarkParams struct { - UserID pgtype.UUID - UserID_2 pgtype.UUID +type UnLinkBookmarkContentWithTagsParams struct { + ContentID uuid.UUID + Column2 []string + UserID uuid.UUID } -func (q *Queries) OwnerTransferBookmark(ctx context.Context, db DBTX, arg OwnerTransferBookmarkParams) error { - _, err := db.Exec(ctx, ownerTransferBookmark, arg.UserID, arg.UserID_2) +func (q *Queries) UnLinkBookmarkContentWithTags(ctx context.Context, db DBTX, arg UnLinkBookmarkContentWithTagsParams) error { + _, err := db.Exec(ctx, unLinkBookmarkContentWithTags, arg.ContentID, arg.Column2, arg.UserID) return err } const updateBookmark = `-- name: UpdateBookmark :one -UPDATE bookmarks -SET - title = COALESCE($3, title), - summary = COALESCE($4, summary), - summary_embeddings = COALESCE($5, summary_embeddings), - content = COALESCE($6, content), - content_embeddings = COALESCE($7, content_embeddings), - html = COALESCE($8, html), - metadata = COALESCE($9, metadata), - screenshot = COALESCE($10, screenshot), - updated_at = CURRENT_TIMESTAMP -WHERE uuid = $1 AND user_id = $2 -RETURNING id, uuid, user_id, url, title, summary, summary_embeddings, content, content_embeddings, html, metadata, screenshot, created_at, updated_at +UPDATE bookmarks +SET is_favorite = COALESCE($3, is_favorite), + is_archive = COALESCE($4, is_archive), + is_public = COALESCE($5, is_public), + reading_progress = COALESCE($6, reading_progress), + metadata = COALESCE($7, metadata) +WHERE id = $1 + AND user_id = $2 +RETURNING id, user_id, content_id, is_favorite, is_archive, is_public, reading_progress, metadata, created_at, updated_at ` type UpdateBookmarkParams struct { - Uuid uuid.UUID - UserID pgtype.UUID - Title pgtype.Text - Summary pgtype.Text - SummaryEmbeddings *pgv.Vector - Content pgtype.Text - ContentEmbeddings *pgv.Vector - Html pgtype.Text - Metadata []byte - Screenshot pgtype.Text + ID uuid.UUID + UserID pgtype.UUID + IsFavorite pgtype.Bool + IsArchive pgtype.Bool + IsPublic pgtype.Bool + ReadingProgress pgtype.Int4 + Metadata []byte } func (q *Queries) UpdateBookmark(ctx context.Context, db DBTX, arg UpdateBookmarkParams) (Bookmark, error) { row := db.QueryRow(ctx, updateBookmark, - arg.Uuid, + arg.ID, arg.UserID, + arg.IsFavorite, + arg.IsArchive, + arg.IsPublic, + arg.ReadingProgress, + arg.Metadata, + ) + var i Bookmark + err := row.Scan( + &i.ID, + &i.UserID, + &i.ContentID, + &i.IsFavorite, + &i.IsArchive, + &i.IsPublic, + &i.ReadingProgress, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateBookmarkContent = `-- name: UpdateBookmarkContent :one +UPDATE bookmark_content +SET title = COALESCE($2, title), + description = COALESCE($3, description), + url = COALESCE($4, url), + domain = COALESCE($5, domain), + s3_key = COALESCE($6, s3_key), + summary = COALESCE($7, summary), + content = COALESCE($8, content), + html = COALESCE($9, html), + metadata = COALESCE($10, metadata) +WHERE id = $1 +RETURNING id, type, title, description, url, domain, s3_key, summary, content, html, metadata, created_at, updated_at +` + +type UpdateBookmarkContentParams struct { + ID uuid.UUID + Title pgtype.Text + Description pgtype.Text + Url pgtype.Text + Domain pgtype.Text + S3Key pgtype.Text + Summary pgtype.Text + Content pgtype.Text + Html pgtype.Text + Metadata []byte +} + +func (q *Queries) UpdateBookmarkContent(ctx context.Context, db DBTX, arg UpdateBookmarkContentParams) (BookmarkContent, error) { + row := db.QueryRow(ctx, updateBookmarkContent, + arg.ID, arg.Title, + arg.Description, + arg.Url, + arg.Domain, + arg.S3Key, arg.Summary, - arg.SummaryEmbeddings, arg.Content, - arg.ContentEmbeddings, arg.Html, arg.Metadata, - arg.Screenshot, ) - var i Bookmark + var i BookmarkContent err := row.Scan( &i.ID, - &i.Uuid, - &i.UserID, - &i.Url, + &i.Type, &i.Title, + &i.Description, + &i.Url, + &i.Domain, + &i.S3Key, &i.Summary, - &i.SummaryEmbeddings, &i.Content, - &i.ContentEmbeddings, &i.Html, &i.Metadata, - &i.Screenshot, &i.CreatedAt, &i.UpdatedAt, ) diff --git a/internal/pkg/db/models.go b/internal/pkg/db/models.go index 80969b5..39e528b 100644 --- a/internal/pkg/db/models.go +++ b/internal/pkg/db/models.go @@ -117,20 +117,47 @@ type AuthUserOauthConnection struct { } type Bookmark struct { - ID int32 - Uuid uuid.UUID - UserID pgtype.UUID - Url string - Title pgtype.Text - Summary pgtype.Text - SummaryEmbeddings *pgv.Vector - Content pgtype.Text - ContentEmbeddings *pgv.Vector - Html pgtype.Text - Metadata []byte - Screenshot pgtype.Text - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + ID uuid.UUID + UserID pgtype.UUID + ContentID pgtype.UUID + IsFavorite pgtype.Bool + IsArchive pgtype.Bool + IsPublic pgtype.Bool + ReadingProgress pgtype.Int4 + Metadata []byte + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +type BookmarkContent struct { + ID uuid.UUID + Type string + Title string + Description pgtype.Text + Url pgtype.Text + Domain pgtype.Text + S3Key pgtype.Text + Summary pgtype.Text + Content pgtype.Text + Html pgtype.Text + Metadata []byte + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +type BookmarkContentTag struct { + ID uuid.UUID + Name string + UserID uuid.UUID + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +type BookmarkContentTagsMapping struct { + ContentID uuid.UUID + TagID uuid.UUID + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz } type Cache struct { From 19c91bef6095313f022cbb9980d29af9974137fd Mon Sep 17 00:00:00 2001 From: Vaayne Date: Mon, 27 Jan 2025 22:58:00 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=93=A6=20refact:=20reorganize=20and?= =?UTF-8?q?=20update=20bookmark=20and=20share=20content=20queries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renamed `content_tags` and `bookmark_content_tags` to `bookmark_tags` for better clarity. - Updated `bookmark_content` schema to include `user_id`, `url`, and `tags`. - Moved share content related queries to a new `share_content.sql.go` file. - Refactored `bookmarks.sql.go` to include new fields and corrected parameter mappings. - Deleted `content.sql.go` as content schema and related queries merged into bookmarks. - Updated `models.go` to match schema changes in database tables. --- database/bindata.go | 10 +- .../000012_split_bookmark_content.up.sql | 38 +- database/queries/bookmarks.sql | 101 +- database/queries/content.sql | 314 ------ database/queries/share_content.sql | 49 + internal/pkg/db/bookmarks.sql.go | 296 +++--- internal/pkg/db/content.sql.go | 978 ------------------ internal/pkg/db/models.go | 18 +- internal/pkg/db/share_content.sql.go | 209 ++++ 9 files changed, 509 insertions(+), 1504 deletions(-) delete mode 100644 database/queries/content.sql create mode 100644 database/queries/share_content.sql delete mode 100644 internal/pkg/db/content.sql.go create mode 100644 internal/pkg/db/share_content.sql.go diff --git a/database/bindata.go b/database/bindata.go index 309e078..e158d50 100644 --- a/database/bindata.go +++ b/database/bindata.go @@ -23,7 +23,7 @@ // 000011_create_s3_resources_mapping_table.down.sql (264B) // 000011_create_s3_resources_mapping_table.up.sql (1.401kB) // 000012_split_bookmark_content.down.sql (222B) -// 000012_split_bookmark_content.up.sql (3.944kB) +// 000012_split_bookmark_content.up.sql (4.043kB) package migrations @@ -366,7 +366,7 @@ func _000007_new_auth_flowsUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000007_new_auth_flows.up.sql", size: 6277, mode: os.FileMode(0644), modTime: time.Unix(1736570339, 0)} + info := bindataFileInfo{name: "000007_new_auth_flows.up.sql", size: 6277, mode: os.FileMode(0644), modTime: time.Unix(1737987537, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x37, 0xa6, 0x6b, 0x7, 0x8c, 0x5b, 0xc5, 0x5, 0xc8, 0xc1, 0xf9, 0xa8, 0xad, 0x4a, 0x5b, 0x62, 0x9b, 0x13, 0x73, 0xa3, 0x7f, 0x36, 0x94, 0x20, 0xc2, 0x5, 0x9, 0xfa, 0x77, 0x58, 0x9a, 0x2a}} return a, nil } @@ -551,7 +551,7 @@ func _000012_split_bookmark_contentDownSql() (*asset, error) { return a, nil } -var __000012_split_bookmark_contentUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x56\x4f\x73\xdb\xb6\x13\xbd\xeb\x53\xec\xcd\xd4\x0c\x65\x67\x92\xf1\xe5\x97\xf9\x1d\x68\x09\xb2\xd9\xca\xa4\x4b\x91\x8d\xd3\x0b\x06\x22\x60\x1a\xb5\x08\x70\x08\xd0\x8e\xa7\xd3\xef\xde\x01\xf8\x4f\x8e\x29\x89\x8e\x7b\x48\x8f\x20\xde\x3e\x60\xf7\x3d\xec\x72\x1e\x21\x2f\x46\x10\x7b\x17\x2b\x04\x1b\x29\x1f\x72\x52\x3e\xe0\x54\x0a\xcd\x84\x76\x26\x00\x00\x9c\x42\x55\x71\x0a\x37\x91\x7f\xed\x45\x5f\xe1\x57\xf4\x15\x16\x68\xe9\x25\xab\x18\x32\x26\x70\x49\x04\x95\x39\x36\x18\x67\xea\xda\x10\xfd\x5c\x30\xf8\xdd\x8b\xe6\x57\x5e\xe4\x9c\x7f\x98\x42\x10\xc6\x10\x24\xab\x95\x0b\xb3\x59\xbd\x2b\xef\x5e\x1f\xd7\x7e\x70\xa1\xa0\x77\x2e\xb0\xa2\xda\xb8\xc0\x73\x92\x31\x17\x0a\x49\x53\xa2\xb4\x0b\x8f\x9c\x32\xe9\x02\xd3\xe9\xe9\xb4\x3e\x8c\xeb\x2d\x83\x18\xdd\xc6\xfd\x31\x76\x83\x32\x95\x96\xbc\xd0\x5c\x0a\xbb\x5d\x7f\xad\xca\x6d\xbd\x32\x57\x49\xa2\x95\xb9\x89\xbe\x67\xdd\x6d\xea\x50\x99\x13\x2e\x7a\x5c\xb3\x6e\xa0\x49\xb4\xb2\x28\xf5\x09\x3f\xb0\xe7\x1e\xb5\xfe\x04\x66\x7d\x27\x4b\x50\x5a\x96\x5c\x64\x50\x92\x27\x68\xd2\x83\x2d\x7f\x60\xbb\x99\xed\x64\x52\xd3\x55\x79\x4e\xca\x1d\x3e\xcf\x37\x05\x66\x25\xd1\x8c\xb6\xbb\x16\xd9\x32\x76\xc8\xf6\x03\x17\x60\x72\xa0\xf2\x49\x98\x6b\xe4\x44\x5b\xfc\xbd\xce\x77\x92\xb6\xab\x36\xc2\x5c\xf6\x89\x6d\xa0\x20\x19\xb3\xd8\x9c\x69\x42\x89\x26\xf0\xcb\x3a\x0c\x2e\x3a\xa5\x4f\xfe\xfa\xfb\xa4\x2e\x60\x5a\x32\x73\x21\x4c\x34\xc4\xfe\x35\x5a\xc7\xde\xf5\x0d\x7c\xf1\xe3\x2b\xbb\x84\x3f\xc2\x00\x75\x42\x74\xe1\xf3\x24\x8a\x50\x10\xe3\x2e\xa2\x11\xa3\xa0\xff\x02\xd7\x04\x60\xfa\x79\x32\x99\xcd\xc0\x17\x94\x7d\x63\x6a\xd2\xd8\xda\x0f\x16\xe8\x16\x38\xfd\x86\xbf\xf7\x1a\xb6\x26\x0c\x83\xd7\x26\x34\x1b\xd3\xcf\x23\x18\x8c\x91\x86\x08\xaa\x72\x3b\x2a\xbe\xf1\xd4\x10\x45\xbd\x35\x8a\x65\x47\x8d\x21\xa6\x7e\x7b\x14\x5b\x27\xfe\x00\x17\x24\x6b\x3f\xb8\x84\x8c\x0b\xa7\x83\xfd\xa9\xa4\xd8\xe0\x82\xe8\x7b\x2c\x0b\xd5\x88\x70\x71\xfd\xf1\x1c\xb8\x51\x02\xa4\x78\x45\x33\xb1\x0e\xd4\x85\xfa\xdf\xd9\x19\x95\xa9\x3a\x2d\x48\x49\x28\xa3\x9b\xd3\x54\xe6\xe6\x4b\x95\x33\xa1\x89\x79\xb5\x67\x96\x84\x8b\xec\xac\x4e\x03\xdb\xf5\x88\x34\x36\xf9\xc7\x73\xac\x18\x29\xd3\xfb\x03\x99\x18\x94\xc3\xa9\x5b\x77\x10\x77\xb7\x5f\xb8\xed\x73\x73\xdb\x87\xe2\x76\x0f\x63\x5a\x1b\xd4\x79\x60\xcf\xf8\x8e\xb3\x2d\x85\xff\xc3\x09\xa7\x27\x7d\x81\xe3\xc8\xbf\xbc\x44\x51\xe3\xee\x01\xe7\xf4\xae\xbf\x40\xcb\x30\x42\x90\xdc\x2c\x4c\xe0\xd0\x5d\x97\x61\x04\xc8\x9b\x5f\x41\x14\x7e\x01\x74\x8b\xe6\x49\x8c\x60\x99\x04\xf3\xd8\x0f\x83\xf6\x88\x9e\x11\xa7\x72\x5b\xe5\xc2\x31\x5a\x98\x52\x0b\xf6\xd4\x71\x2a\xd0\x64\xb3\x65\x93\x45\x14\xde\x34\x2d\xdf\x5f\x02\xba\xf5\xd7\xf1\xba\x07\xf5\x69\xbc\x18\x0a\x0a\x7e\x78\x1c\x54\x8a\x95\x98\x53\x48\x12\x7f\x01\x11\x5a\xa2\x08\x05\x73\xb4\xb6\xdf\x95\x63\x90\x0d\xb0\x2d\xd0\x00\xf6\x95\xb3\xbb\x20\xae\xf0\x1d\x79\x94\x25\xd7\x0c\x2e\xc2\x70\x85\xbc\xa0\xbb\xd0\xd2\x5b\xad\x51\x07\x33\x7e\xe0\x8f\xc7\x50\x45\xb5\xd9\xf2\xf4\x10\xa8\x64\x84\x72\x91\xe1\xa2\x94\x59\xc9\x94\x02\x3f\x88\x91\x11\xbc\xc5\x7e\x70\xff\xe3\xad\x74\xef\x13\x53\xd8\x8a\x39\xdc\x75\x94\xd3\x28\xed\xc2\xb8\xbe\xb3\xa3\xdc\x30\xcd\x8e\xb6\x87\x79\x5a\x69\xf7\xd2\x34\x80\xc3\x2c\x8d\xf4\x7b\x49\xea\xfd\xc3\x1c\x43\x2d\x54\x8d\xea\x9d\x87\xbb\x87\x1a\xd5\x36\xd4\xbb\xfa\x85\x69\x18\xaf\x27\x25\xc9\xda\xc6\x71\xf0\x6f\xb1\x06\xfe\x70\x93\x10\x24\xdf\xf3\xcf\xf8\xa2\x87\x58\xda\xce\xc7\x07\x9b\xc9\x4f\xfe\xb8\x92\xc0\xff\x2d\x69\x3d\x54\x0d\xff\x62\xd4\x49\x63\x5b\x9b\x81\xe1\x60\x4b\xde\x3b\xd4\xc0\x46\x8d\x79\x13\x76\x84\xf4\x25\xd7\xb1\x89\x66\x09\xdf\x32\xd6\x6a\xb3\xbc\xc3\xab\xb3\x19\x0c\x98\x15\x2c\x6d\xc9\xb6\xf6\xff\x41\xdd\xf3\x62\x84\x67\x71\x4e\x8a\x82\x8b\xcc\xf9\x7e\x06\x59\xaf\x1d\x99\x41\x26\xb9\x05\x5a\xa1\x18\xc1\xdc\x5b\xcf\xbd\x45\x33\x23\x34\xc9\xc6\x50\xd4\xc5\x3e\xc0\xf3\xf3\xb9\xd8\x05\x4b\xb6\xf3\xb4\x9d\xbe\x66\x6e\x93\xf8\xf4\xd8\x20\x19\x54\x00\x37\x55\xdb\x67\x98\x4e\xa9\xe6\x90\xb7\xf9\xb3\x3d\xe4\xcd\x3e\x6d\x23\xdf\xe1\xd7\x7f\x02\x00\x00\xff\xff\x38\xa1\x7a\xd6\x68\x0f\x00\x00") +var __000012_split_bookmark_contentUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x56\xdf\x6f\xdb\x36\x10\x7e\xf7\x5f\x71\x6f\x91\x01\xb9\x29\x5a\xf4\x65\xc5\x1e\x14\x9b\x4e\xb4\x39\x52\x26\x4b\x6b\xba\x61\x10\x68\x93\x96\xb9\x48\xa4\x40\x52\x71\x83\x61\xff\xfb\x40\xea\x87\x95\x46\x71\xb4\x66\x0f\xeb\x23\xc9\xef\x3e\xf2\xee\xbe\xbb\xe3\x3c\x42\x5e\x8c\x20\xf6\x2e\x56\x08\x36\x42\xdc\x15\x58\xde\xa5\x5b\xc1\x35\xe5\xda\x99\x00\x00\x30\x02\x55\xc5\x08\xdc\x44\xfe\xb5\x17\x7d\x86\x9f\xd1\x67\x58\xa0\xa5\x97\xac\x62\xc8\x28\x4f\x25\xe6\x44\x14\xa9\xc1\x38\x53\xd7\x9a\xe8\x87\x92\xc2\xaf\x5e\x34\xbf\xf2\x22\xe7\xc3\xdb\x29\x04\x61\x0c\x41\xb2\x5a\xb9\x30\x9b\xd5\xa7\x62\xf7\xf4\xba\x76\xc3\x85\x92\xec\x5c\xa0\x65\xb5\x71\x81\x15\x38\xa3\x2e\x94\x82\x6c\xb1\xd2\x2e\xdc\x33\x42\x85\x0b\x54\x6f\xdf\x4c\xed\x65\x95\xcc\x21\x46\xb7\xf1\xe3\x4b\x92\x68\x65\xee\xd0\x7b\xda\xdd\x53\xa3\x15\x95\x69\xeb\x52\xeb\x46\x67\x75\xd8\x53\x6e\x21\xc0\x14\xf0\x2a\xcf\x5d\xd0\x7b\xa6\xa0\x79\xa1\xd9\x55\x7b\x2c\x29\x71\x6b\x28\xd3\x67\x0a\xb8\xd0\x0d\x96\x69\x38\xb0\x3c\x87\x0d\xcd\x05\xcf\x40\x0b\x7b\xbf\xe1\xab\xc3\xc2\x74\x4e\xed\x5b\xeb\x30\x11\xaa\xb6\x92\x95\x9a\x09\xde\xdf\x15\x05\x66\xcd\x86\x79\x53\xb3\x6e\x9c\x49\xa2\x95\x45\xa9\xf7\xe9\x1d\x7d\x38\xa2\xd6\xef\xc1\xac\x77\x42\x82\xd2\x42\x32\x9e\x81\xc4\x87\xee\xe1\x39\xbb\xa3\xfd\xa8\xf6\xa2\x58\xd3\x55\x45\x81\x65\x8f\xcf\xf3\x4d\x72\xa9\xc4\x9a\x92\xf6\xd4\x22\x5b\xc6\x0e\xd9\xc5\x86\x83\x89\x32\x11\x07\x6e\x9e\x51\x60\x6d\xf1\x7b\x5d\xe4\x47\xb0\x5d\xb5\x16\xe6\xb1\x07\xba\x81\x12\x67\xb4\x0e\x10\xce\x54\x5f\x37\xbf\xff\xd1\xa5\xe8\xec\xaf\xbf\xcf\x6a\xf5\x18\x8c\xb1\x34\xd1\x68\x99\x36\x0f\x40\xe8\x0e\x57\xb9\x06\x5a\x94\xfa\x01\xb0\x94\xb8\x7e\x6f\x41\x35\x26\x58\x63\xf8\x69\x1d\x06\x17\x8f\xf9\x6a\x87\x24\x35\x4e\xa6\x58\x43\xec\x5f\xa3\x75\xec\x5d\xdf\xc0\x27\x3f\xbe\xb2\x4b\xf8\x2d\x0c\x50\x27\xad\xce\x7c\x9e\x44\x11\x0a\xe2\xb4\xb3\xa8\xb9\xaa\x92\xfc\x67\x5c\x49\xe0\xff\x92\x20\xa7\x92\xb9\xdb\xaa\xd6\x08\x7e\xfa\x71\x32\x99\xcd\xc0\xe7\x84\x7e\xa1\x6a\xd2\x14\xb0\x1f\x2c\xd0\x2d\x30\xf2\x25\xfd\xba\xaa\x52\x5b\x6e\x61\xf0\xb4\xdc\xcc\xc1\xf4\xe3\x08\x06\x53\x61\x43\x04\x95\xcc\x47\xd9\x37\x0a\x1e\xa2\xa8\x8f\x46\xb1\xf4\xf2\x34\xc4\x74\x3c\x1e\xc5\xd6\xc9\x62\x80\x0b\x92\xb5\x1f\x5c\x42\xc6\xb8\xd3\xc1\xfe\x54\x82\x6f\xd2\x12\xeb\x7d\x2a\x4a\xd5\x24\xe1\xe2\xfa\xdd\x07\x60\x26\x13\x20\xf8\x13\x9a\x89\xd5\xbb\x2e\xd5\x0f\xe7\xe7\x44\x6c\xd5\x9b\x12\x4b\x4c\x28\xd9\xbc\xd9\x8a\xc2\xec\x54\x05\xe5\x1a\x9b\xf2\x3f\xb7\x24\x8c\x67\xe7\xb5\x1b\xa9\x5d\x8f\x70\x63\x53\xbc\xfb\x90\x2a\x8a\xe5\x76\x7f\xc2\x13\x83\x72\x18\x71\xeb\x0e\xe4\xf6\x1b\x8f\xdb\x16\xb7\xdb\x16\x93\xdb\x95\xcc\xb4\x96\xae\x73\x47\x1f\xd2\x1d\xa3\x39\x81\x1f\xe1\x8c\x91\xb3\x63\x80\xe3\xc8\xbf\xbc\x44\x51\xa3\xfb\x01\xe5\x1c\xeb\xe1\x02\x2d\xc3\x08\x41\x72\xb3\x30\x86\x43\x6f\x5d\x86\x11\x20\x6f\x7e\x05\x51\xf8\x09\xd0\x2d\x9a\x27\x31\x82\x65\x12\xcc\x63\x3f\x0c\xda\x2b\x8e\x8c\xe9\x56\xe4\x55\xc1\x1d\x93\x0b\x13\x6a\x4e\x0f\x1d\xa7\x02\x8d\x37\x39\x9d\x2c\xa2\xf0\xa6\x19\x6e\xfe\x12\xd0\xad\xbf\x8e\xd7\x47\xd0\xd1\x8d\x47\xe3\x4f\xc1\x37\x0f\xbe\x76\xba\x24\x89\xbf\x80\x08\x2d\x51\x84\x82\x39\x5a\xdb\x7d\xe5\x18\xe4\xd4\xb8\xbe\x40\x2b\x14\x23\x98\x7b\xeb\xb9\xb7\x40\x6e\xbf\xaf\x0e\x59\x3f\xd1\x3a\x23\xcd\x7d\x4c\xa5\x3b\x7c\x2f\x24\xd3\x14\x2e\xc2\x70\x85\xbc\xa0\x7b\xe2\xd2\x5b\xad\x51\x07\x33\x0a\x61\xf7\x2f\xa1\xca\x6a\x93\xb3\xed\x29\x90\xa4\x98\x30\x9e\xa5\xa5\x14\x99\xa4\x4a\x81\x1f\xc4\xc8\x48\xa0\xc5\xbe\x75\xbf\xd7\xb6\xdb\x34\xd7\x67\x8b\x4e\xa5\x36\xbd\xc3\x7d\x48\x39\x4d\xee\x5d\x18\xd7\x89\x7a\x99\x1b\xa6\xe9\xe5\xf6\x34\x4f\x9b\xda\x67\x69\x1a\xc0\x69\x96\x26\xf5\xcf\x92\xd4\xe7\xa7\x39\x86\x9a\xaa\x1a\xd5\x4d\x4f\xf7\x13\x35\xaa\x91\xa8\xd7\x76\x90\xae\xce\xec\x1f\xa3\xee\x20\xc3\x1f\x64\x0b\xf8\xe6\x2e\xc1\x71\xf1\xcc\xf7\xf8\xe9\x17\xb5\x93\xed\xbf\xec\x26\xff\xbb\xea\x7a\xfc\xa9\x69\x85\x65\x42\x31\x7d\xa9\xf0\x6c\xb8\x53\x1b\xb5\xfe\xdc\x30\xbb\x8e\x25\x78\x71\x1e\x59\x82\x51\xc3\xc8\x66\xf6\x15\x3a\x9a\xcd\xa0\xaf\xa4\x76\xb6\x59\x5a\x49\x73\x3b\xed\xd5\x9e\x95\x27\x84\x95\x16\xb8\x2c\x19\xcf\x6a\x7d\x75\x47\xad\x22\x06\x06\x83\x72\x4e\x48\x41\xe3\xec\x94\x6d\x1d\xc6\xef\x4b\x4a\x60\xc9\x7a\x15\xe7\xf4\xa2\xe4\x36\x1e\x8f\x94\x55\x13\xec\xb4\x09\xd3\xd7\x5a\xe8\x92\xd1\x90\x8e\x93\x5a\x4b\x3a\x5a\x72\xad\xc5\x2b\xa4\xf7\x4f\x00\x00\x00\xff\xff\x0f\x1e\xa2\xfa\xcb\x0f\x00\x00") func _000012_split_bookmark_contentUpSqlBytes() ([]byte, error) { return bindataRead( @@ -566,8 +566,8 @@ func _000012_split_bookmark_contentUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 3944, mode: os.FileMode(0644), modTime: time.Unix(1737971608, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x44, 0x6a, 0x9e, 0xa7, 0xa1, 0x3a, 0x89, 0x4, 0x27, 0xa6, 0x72, 0xde, 0x3a, 0x20, 0x51, 0x65, 0x3b, 0x7a, 0x33, 0xc3, 0x80, 0xd7, 0x63, 0xa3, 0x56, 0x36, 0x99, 0x99, 0x4d, 0x94, 0x2f, 0xec}} + info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 4043, mode: os.FileMode(0644), modTime: time.Unix(1737989741, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd0, 0x9, 0xf9, 0x95, 0xd7, 0xe4, 0x60, 0x97, 0x8b, 0xbb, 0xcd, 0xc7, 0x8d, 0xf5, 0xe1, 0xbc, 0x66, 0xfc, 0x31, 0x35, 0x1d, 0x8d, 0xce, 0x60, 0xc5, 0x6c, 0x75, 0xc3, 0xdb, 0xa4, 0x93, 0xb1}} return a, nil } diff --git a/database/migrations/000012_split_bookmark_content.up.sql b/database/migrations/000012_split_bookmark_content.up.sql index 0a8effd..5cfe4f8 100644 --- a/database/migrations/000012_split_bookmark_content.up.sql +++ b/database/migrations/000012_split_bookmark_content.up.sql @@ -1,17 +1,20 @@ CREATE TABLE bookmark_content( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), type VARCHAR(50) NOT NULL, -- type of bookmark_content(bookmark, pdf, epub, image, podcast, video, etc.) - title TEXT NOT NULL, + url TEXT NOT NULL, -- URL of the bookmark + user_id uuid DEFAULT NULL, -- when user is null, this content is shared, when it's not null, it will belong to the user + title TEXT, description TEXT, - url TEXT, -- URL of the bookmark domain TEXT, -- domain of the URL s3_key TEXT, -- S3 key for storing raw content like pdf, epub, video, etc. summary TEXT, -- AI generated summary content TEXT, -- content in markdown format html TEXT, -- html content for web page + tags VARCHAR(50)[] DEFAULT '{}', -- tags for the content by default empty array metadata JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(url, user_id) ); -- Indexes @@ -31,7 +34,7 @@ CREATE TRIGGER update_bookmark_content_updated_at BEFORE UPDATE ON bookmark_cont DROP TABLE IF EXISTS bookmarks; CREATE TABLE bookmarks ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID REFERENCES users(uuid), + user_id UUID REFERENCES users(uuid) ON DELETE CASCADE, content_id UUID REFERENCES bookmark_content(id), is_favorite BOOLEAN DEFAULT FALSE, is_archive BOOLEAN DEFAULT FALSE, @@ -50,28 +53,27 @@ CREATE INDEX idx_bookmarks_metadata ON bookmarks USING gin(metadata jsonb_path_o CREATE TRIGGER update_bookmarks_updated_at BEFORE UPDATE ON bookmarks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - --- bookmark_content_tags table -CREATE TABLE bookmark_content_tags ( +-- bookmark_tags table +CREATE TABLE bookmark_tags ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(50) NOT NULL, - user_id uuid NOT NULL REFERENCES users(uuid), + user_id uuid NOT NULL REFERENCES users(uuid) ON DELETE CASCADE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, name) ); -CREATE UNIQUE INDEX uidx_bookmark_content_user_id_name ON bookmark_content_tags(user_id, name); -CREATE INDEX idx_bookmark_content_tags_name ON bookmark_content_tags(name); -CREATE TRIGGER update_bookmark_content_tags_updated_at BEFORE UPDATE ON bookmark_content_tags FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE INDEX idx_bookmark_tags_name ON bookmark_tags(name); +CREATE TRIGGER update_bookmark_tags_updated_at BEFORE UPDATE ON bookmark_tags FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- -- bookmark_content tags relationship -CREATE TABLE bookmark_content_tags_mapping( - content_id uuid REFERENCES bookmark_content(id) ON DELETE CASCADE, - tag_id uuid REFERENCES bookmark_content_tags(id) ON DELETE CASCADE, +CREATE TABLE bookmark_tags_mapping( + bookmark_id uuid REFERENCES bookmarks(id) ON DELETE CASCADE, + tag_id uuid REFERENCES bookmark_tags(id) ON DELETE CASCADE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY(content_id, tag_id) + PRIMARY KEY(bookmark_id, tag_id) ); -CREATE INDEX idx_bookmark_content_tags_mapping_tag_id ON bookmark_content_tags_mapping(tag_id); -CREATE TRIGGER update_bookmark_content_tags_mapping_updated_at BEFORE UPDATE ON bookmark_content_tags_mapping FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE INDEX idx_bookmark_tags_mapping_tag_id ON bookmark_tags_mapping(tag_id); +CREATE TRIGGER update_bookmark_tags_mapping_updated_at BEFORE UPDATE ON bookmark_tags_mapping FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); diff --git a/database/queries/bookmarks.sql b/database/queries/bookmarks.sql index 827425a..c387cbb 100644 --- a/database/queries/bookmarks.sql +++ b/database/queries/bookmarks.sql @@ -3,8 +3,8 @@ WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) AND (sqlc.narg('types')::text[] IS NULL OR bc.type = ANY(sqlc.narg('types')::text[])) @@ -20,8 +20,8 @@ SELECT b.*, FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) AND (sqlc.narg('types')::text[] IS NULL OR bc.type = ANY(sqlc.narg('types')::text[])) @@ -35,8 +35,8 @@ WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) AND (sqlc.narg('types')::text[] IS NULL OR bc.type = ANY(sqlc.narg('types')::text[])) @@ -60,8 +60,8 @@ SELECT b.*, FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) AND (sqlc.narg('types')::text[] IS NULL OR bc.type = ANY(sqlc.narg('types')::text[])) @@ -87,8 +87,8 @@ SELECT b.*, ) as tags FROM bookmarks b JOIN bookmark_content bc ON b.content_id = bc.id - LEFT JOIN bookmark_content_tags_mapping bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags bct ON bctm.tag_id = bct.id WHERE b.id = $1 AND b.user_id = $2 GROUP BY b.id, bc.id @@ -103,13 +103,21 @@ SELECT EXISTS ( AND b.user_id = $2 ); +-- name: IsBookmarkContentExistWithURL :one +SELECT EXISTS ( + SELECT 1 + FROM bookmark_content bc + WHERE bc.url = $1 +); + + -- name: CreateBookmarkContent :one INSERT INTO bookmark_content ( - type, title, description, url, domain, s3_key, + type, title, description, user_id, url, domain, s3_key, summary, content, html, metadata ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 ) RETURNING *; @@ -138,7 +146,6 @@ RETURNING *; UPDATE bookmark_content SET title = COALESCE(sqlc.narg('title'), title), description = COALESCE(sqlc.narg('description'), description), - url = COALESCE(sqlc.narg('url'), url), domain = COALESCE(sqlc.narg('domain'), domain), s3_key = COALESCE(sqlc.narg('s3_key'), s3_key), summary = COALESCE(sqlc.narg('summary'), summary), @@ -156,61 +163,59 @@ WHERE id = $1 AND user_id = $2; DELETE FROM bookmarks WHERE user_id = $1; --- Tags related queries similar to content.sql but adapted for new schema +-- name: OwnerTransferBookmark :exec +UPDATE bookmarks +SET + user_id = $2, + updated_at = CURRENT_TIMESTAMP +WHERE user_id = $1; + -- name: ListBookmarkTagsByUser :many -SELECT bct.name, count(bctm.*) as count -FROM bookmark_content_tags bct - JOIN bookmark_content_tags_mapping bctm ON bct.id = bctm.tag_id -WHERE bct.user_id = $1 -GROUP BY bct.name -ORDER BY count DESC; - --- name: ListBookmarkContentTags :many -SELECT bct.name -FROM bookmark_content_tags bct - JOIN bookmark_content_tags_mapping bctm ON bct.id = bctm.tag_id -WHERE bctm.content_id = $1 - AND bct.user_id = $2; +SELECT name FROM bookmark_tags +WHERE user_id = $1; + +-- name: ListBookmarkTagsByBookmarkId :many +SELECT bt.name +FROM bookmark_tags bt + JOIN bookmark_tags_mapping btm ON bt.id = btm.tag_id +WHERE btm.bookmark_id = $1; -- name: ListBookmarkDomains :many -SELECT bc.domain, count(*) as count +SELECT bc.domain, count(*) as cnt FROM bookmarks b - JOIN bookmark_content bc ON b.content_id = bc.id + JOIN bookmark_content bc ON b.content_id = bc.id WHERE b.user_id = $1 AND bc.domain IS NOT NULL GROUP BY bc.domain -ORDER BY count DESC, domain ASC; +ORDER BY cnt DESC, domain ASC; --- name: CreateBookmarkContentTag :one -INSERT INTO bookmark_content_tags (name, user_id) +-- name: CreateBookmarkTag :one +INSERT INTO bookmark_tags (name, user_id) VALUES ($1, $2) -ON CONFLICT (name, user_id) DO UPDATE - SET usage_count = bookmark_content_tags.usage_count + 1 RETURNING *; --- name: DeleteBookmarkContentTag :exec -DELETE FROM bookmark_content_tags +-- name: DeleteBookmarkTag :exec +DELETE FROM bookmark_tags WHERE id = $1 AND user_id = $2; --- name: LinkBookmarkContentWithTags :exec -INSERT INTO bookmark_content_tags_mapping (content_id, tag_id) -SELECT $1, bct.id -FROM bookmark_content_tags bct -WHERE bct.name = ANY ($2::text[]) - AND bct.user_id = $3; +-- name: LinkBookmarkWithTags :exec +INSERT INTO bookmark_tags_mapping (bookmark_id, tag_id) +SELECT $1, bt.id +FROM bookmark_tags bt +WHERE bt.name = ANY ($2::text[]) + AND bt.user_id = $3; --- name: UnLinkBookmarkContentWithTags :exec -DELETE FROM bookmark_content_tags_mapping -WHERE content_id = $1 +-- name: UnLinkBookmarkWithTags :exec +DELETE FROM bookmark_tags_mapping +WHERE bookmark_id = $1 AND tag_id IN (SELECT id - FROM bookmark_content_tags + FROM bookmark_tags WHERE name = ANY ($2::text[]) AND user_id = $3); -- name: ListExistingBookmarkTagsByTags :many SELECT name -FROM bookmark_content_tags +FROM bookmark_tags WHERE name = ANY ($1::text[]) AND user_id = $2; - diff --git a/database/queries/content.sql b/database/queries/content.sql deleted file mode 100644 index 8e19463..0000000 --- a/database/queries/content.sql +++ /dev/null @@ -1,314 +0,0 @@ --- name: ListContents :many -WITH total AS ( - SELECT COUNT( DISTINCT tc.*) AS total_count - FROM content AS tc - LEFT JOIN content_tags_mapping AS tctm ON tc.id = tctm.content_id - LEFT JOIN content_tags AS tct ON tctm.tag_id = tct.id - WHERE tc.user_id = $1 - AND ( - sqlc.narg('domains') :: text[] IS NULL - OR tc.domain = ANY (sqlc.narg('domains') :: text[]) - ) - AND ( - sqlc.narg('types') :: text[] IS NULL - OR tc.type = ANY (sqlc.narg('types') :: text[]) - ) - AND ( - sqlc.narg('tags') :: text[] IS NULL - OR tct.name = ANY (sqlc.narg('tags') :: text[]) - ) -) -SELECT c.*, - t.total_count, - COALESCE( - array_agg(ct.name) FILTER ( - WHERE - ct.name IS NOT NULL - ), - ARRAY [] :: VARCHAR[] - ) AS tags -FROM content AS c - CROSS JOIN total AS t - LEFT JOIN content_tags_mapping AS ctm ON c.id = ctm.content_id - LEFT JOIN content_tags AS ct ON ctm.tag_id = ct.id -WHERE c.user_id = $1 - AND ( - sqlc.narg('domains') :: text[] IS NULL - OR c.domain = ANY (sqlc.narg('domains') :: text[]) - ) - AND ( - sqlc.narg('types') :: text[] IS NULL - OR c.type = ANY (sqlc.narg('types') :: text[]) - ) - AND ( - sqlc.narg('tags') :: text[] IS NULL - OR ct.name = ANY (sqlc.narg('tags') :: text[]) - ) -GROUP BY c.id, - t.total_count -ORDER BY c.created_at DESC -LIMIT $2 OFFSET $3; - - --- name: SearchContentsWithFilter :many -WITH total AS ( - SELECT COUNT( DISTINCT tc.*) AS total_count - FROM content AS tc - LEFT JOIN content_tags_mapping AS tctm ON tc.id = tctm.content_id - LEFT JOIN content_tags AS tct ON tctm.tag_id = tct.id - WHERE tc.user_id = $1 - AND ( - sqlc.narg('domains') :: text[] IS NULL - OR tc.domain = ANY (sqlc.narg('domains') :: text[]) - ) - AND ( - sqlc.narg('types') :: text[] IS NULL - OR tc.type = ANY (sqlc.narg('types') :: text[]) - ) - AND ( - sqlc.narg('tags') :: text[] IS NULL - OR tct.name = ANY (sqlc.narg('tags') :: text[]) - ) - AND ( - sqlc.narg('query') :: text IS NULL - OR tc.title @@@ sqlc.narg('query') - OR tc.description @@@ sqlc.narg('query') - OR tc.summary @@@ sqlc.narg('query') - OR tc.content @@@ sqlc.narg('query') - OR tc.metadata @@@ sqlc.narg('query') - ) -) -SELECT c.*, - t.total_count, - COALESCE( - array_agg(ct.name) FILTER ( - WHERE - ct.name IS NOT NULL - ), - ARRAY [] :: VARCHAR[] - ) AS tags -FROM content AS c - CROSS JOIN total AS t - LEFT JOIN content_tags_mapping AS ctm ON c.id = ctm.content_id - LEFT JOIN content_tags AS ct ON ctm.tag_id = ct.id -WHERE c.user_id = $1 - AND ( - sqlc.narg('domains') :: text[] IS NULL - OR c.domain = ANY (sqlc.narg('domains') :: text[]) - ) - AND ( - sqlc.narg('types') :: text[] IS NULL - OR c.type = ANY (sqlc.narg('types') :: text[]) - ) - AND ( - sqlc.narg('tags') :: text[] IS NULL - OR ct.name = ANY (sqlc.narg('tags') :: text[]) - ) - AND ( - sqlc.narg('query') :: text IS NULL - OR c.title @@@ sqlc.narg('query') - OR c.description @@@ sqlc.narg('query') - OR c.summary @@@ sqlc.narg('query') - OR c.content @@@ sqlc.narg('query') - OR c.metadata @@@ sqlc.narg('query') - ) -GROUP BY c.id, - t.total_count -ORDER BY c.created_at DESC -LIMIT $2 OFFSET $3; - --- name: GetContent :one -SELECT c.*, - COALESCE( - array_agg(ct.name) FILTER ( - WHERE - ct.name IS NOT NULL - ), - ARRAY [] :: VARCHAR[] - ) as tags -FROM content c - LEFT JOIN content_tags_mapping ctm ON c.id = ctm.content_id - LEFT JOIN content_tags ct ON ctm.tag_id = ct.id -WHERE c.id = $1 - AND c.user_id = $2 -GROUP BY c.id -LIMIT 1; - --- name: IsContentExistWithURL :one -SELECT EXISTS (SELECT 1 - FROM content - WHERE url = $1 - AND user_id = $2); - --- name: CreateContent :one -INSERT INTO content (user_id, - type, - title, - description, - url, - domain, - s3_key, - summary, - content, - html, - metadata, - is_favorite) -VALUES ($1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12) -RETURNING *; - --- name: UpdateContent :one -UPDATE - content -SET title = COALESCE(sqlc.narg('title'), title), - description = COALESCE(sqlc.narg('description'), description), - url = COALESCE(sqlc.narg('url'), url), - domain = COALESCE(sqlc.narg('domain'), domain), - s3_key = COALESCE(sqlc.narg('s3_key'), s3_key), - summary = COALESCE(sqlc.narg('summary'), summary), - content = COALESCE(sqlc.narg('content'), content), - html = COALESCE(sqlc.narg('html'), html), - metadata = COALESCE(sqlc.narg('metadata'), metadata), - is_favorite = COALESCE(sqlc.narg('is_favorite'), is_favorite) -WHERE id = $1 - AND user_id = $2 -RETURNING *; - --- name: DeleteContent :exec -DELETE -FROM content -WHERE id = $1 - AND user_id = $2; - --- name: DeleteContentsByUser :exec -DELETE -FROM content -WHERE user_id = $1; - --- name: OwnerTransferContent :exec -UPDATE - content -SET user_id = $2 -WHERE id = $1 - AND user_id = $3; - --- name: ListTagsByUser :many -SELECT ct.name, count(ctm.*) as count -FROM content_tags ct - JOIN content_tags_mapping ctm ON ct.id = ctm.tag_id -WHERE ct.user_id = $1 -GROUP BY ct.name -ORDER BY count DESC; - --- name: ListContentTags :many -SELECT ct.name -FROM content_tags ct - JOIN content_tags_mapping ctm ON ct.id = ctm.tag_id -WHERE ctm.content_id = $1 - AND ct.user_id = $2; - --- name: ListContentDomains :many -SELECT domain, count(*) as count -FROM content -WHERE user_id = $1 -AND domain IS NOT NULL -GROUP BY domain -ORDER BY count DESC, domain ASC; - --- name: CreateContentTag :one -INSERT INTO content_tags (name, user_id) -VALUES ($1, $2) -ON CONFLICT (name, user_id) DO UPDATE - SET usage_count = content_tags.usage_count + 1 -RETURNING *; - --- name: DeleteContentTag :exec -DELETE -FROM content_tags -WHERE id = $1 - AND user_id = $2; - --- name: LinkContentWithTags :exec --- $1: content_id, $2: text[], $3: user_id -INSERT INTO content_tags_mapping (content_id, tag_id) -SELECT $1, - ct.id -FROM content_tags ct -WHERE ct.name = ANY ($2 :: text[]) - AND ct.user_id = $3; - --- name: UnLinkContentWithTags :exec --- $1: content_id, $2: text[], $3: user_id -DELETE FROM content_tags_mapping -WHERE content_id = $1 - AND tag_id IN (SELECT id - FROM content_tags - WHERE name = ANY ($2 :: text[]) - AND user_id = $3); - --- name: ListExistingTagsByTags :many -SELECT name -FROM content_tags -WHERE name = ANY ($1 :: text[]) - AND user_id = $2; - - --- name: CreateShareContent :one -INSERT INTO content_share (user_id, content_id, expires_at) -VALUES ($1, $2, $3) -RETURNING *; - --- name: GetSharedContent :one --- get the shared content from content table -SELECT c.* -FROM content_share AS cs - JOIN content AS c ON cs.content_id = c.id -WHERE cs.id = $1 - AND (cs.expires_at is NULL OR cs.expires_at > now()); - --- name: GetShareContent :one --- info about the shared content -SELECT * -FROM content_share -WHERE content_id = $1 - AND user_id = $2; - --- name: ListShareContent :many -SELECT c.* -FROM content_share AS cs - JOIN content AS c ON cs.content_id = c.id -WHERE cs.user_id = $1 - AND cs.expires_at is NULL OR cs.expires_at > now() -ORDER BY cs.created_at DESC -LIMIT $2 OFFSET $3; - --- name: UpdateShareContent :one -UPDATE content_share cs -SET expires_at = $3 -FROM content c -WHERE cs.content_id = c.id - AND c.id = $1 - AND c.user_id = $2 -RETURNING cs.*; - --- name: DeleteShareContent :exec -DELETE FROM content_share cs -USING content c -WHERE cs.content_id = c.id - AND c.id = $1 - AND c.user_id = $2; - --- name: DeleteExpiredShareContent :exec -DELETE -FROM content_share -WHERE expires_at < now(); diff --git a/database/queries/share_content.sql b/database/queries/share_content.sql new file mode 100644 index 0000000..86d2404 --- /dev/null +++ b/database/queries/share_content.sql @@ -0,0 +1,49 @@ +-- name: CreateShareContent :one +INSERT INTO content_share (user_id, content_id, expires_at) +VALUES ($1, $2, $3) +RETURNING *; + +-- name: GetSharedContent :one +-- get the shared content from content table +SELECT c.* +FROM content_share AS cs + JOIN content AS c ON cs.content_id = c.id +WHERE cs.id = $1 + AND (cs.expires_at is NULL OR cs.expires_at > now()); + +-- name: GetShareContent :one +-- info about the shared content +SELECT * +FROM content_share +WHERE content_id = $1 + AND user_id = $2; + +-- name: ListShareContent :many +SELECT c.* +FROM content_share AS cs + JOIN content AS c ON cs.content_id = c.id +WHERE cs.user_id = $1 + AND cs.expires_at is NULL OR cs.expires_at > now() +ORDER BY cs.created_at DESC +LIMIT $2 OFFSET $3; + +-- name: UpdateShareContent :one +UPDATE content_share cs +SET expires_at = $3 +FROM content c +WHERE cs.content_id = c.id + AND c.id = $1 + AND c.user_id = $2 +RETURNING cs.*; + +-- name: DeleteShareContent :exec +DELETE FROM content_share cs +USING content c +WHERE cs.content_id = c.id + AND c.id = $1 + AND c.user_id = $2; + +-- name: DeleteExpiredShareContent :exec +DELETE +FROM content_share +WHERE expires_at < now(); diff --git a/internal/pkg/db/bookmarks.sql.go b/internal/pkg/db/bookmarks.sql.go index 04220a3..30e9332 100644 --- a/internal/pkg/db/bookmarks.sql.go +++ b/internal/pkg/db/bookmarks.sql.go @@ -61,20 +61,21 @@ func (q *Queries) CreateBookmark(ctx context.Context, db DBTX, arg CreateBookmar const createBookmarkContent = `-- name: CreateBookmarkContent :one INSERT INTO bookmark_content ( - type, title, description, url, domain, s3_key, + type, title, description, user_id, url, domain, s3_key, summary, content, html, metadata ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 ) -RETURNING id, type, title, description, url, domain, s3_key, summary, content, html, metadata, created_at, updated_at +RETURNING id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at ` type CreateBookmarkContentParams struct { Type string - Title string + Title pgtype.Text Description pgtype.Text - Url pgtype.Text + UserID pgtype.UUID + Url string Domain pgtype.Text S3Key pgtype.Text Summary pgtype.Text @@ -88,6 +89,7 @@ func (q *Queries) CreateBookmarkContent(ctx context.Context, db DBTX, arg Create arg.Type, arg.Title, arg.Description, + arg.UserID, arg.Url, arg.Domain, arg.S3Key, @@ -100,14 +102,16 @@ func (q *Queries) CreateBookmarkContent(ctx context.Context, db DBTX, arg Create err := row.Scan( &i.ID, &i.Type, + &i.Url, + &i.UserID, &i.Title, &i.Description, - &i.Url, &i.Domain, &i.S3Key, &i.Summary, &i.Content, &i.Html, + &i.Tags, &i.Metadata, &i.CreatedAt, &i.UpdatedAt, @@ -115,22 +119,20 @@ func (q *Queries) CreateBookmarkContent(ctx context.Context, db DBTX, arg Create return i, err } -const createBookmarkContentTag = `-- name: CreateBookmarkContentTag :one -INSERT INTO bookmark_content_tags (name, user_id) +const createBookmarkTag = `-- name: CreateBookmarkTag :one +INSERT INTO bookmark_tags (name, user_id) VALUES ($1, $2) -ON CONFLICT (name, user_id) DO UPDATE - SET usage_count = bookmark_content_tags.usage_count + 1 RETURNING id, name, user_id, created_at, updated_at ` -type CreateBookmarkContentTagParams struct { +type CreateBookmarkTagParams struct { Name string UserID uuid.UUID } -func (q *Queries) CreateBookmarkContentTag(ctx context.Context, db DBTX, arg CreateBookmarkContentTagParams) (BookmarkContentTag, error) { - row := db.QueryRow(ctx, createBookmarkContentTag, arg.Name, arg.UserID) - var i BookmarkContentTag +func (q *Queries) CreateBookmarkTag(ctx context.Context, db DBTX, arg CreateBookmarkTagParams) (BookmarkTag, error) { + row := db.QueryRow(ctx, createBookmarkTag, arg.Name, arg.UserID) + var i BookmarkTag err := row.Scan( &i.ID, &i.Name, @@ -156,19 +158,19 @@ func (q *Queries) DeleteBookmark(ctx context.Context, db DBTX, arg DeleteBookmar return err } -const deleteBookmarkContentTag = `-- name: DeleteBookmarkContentTag :exec -DELETE FROM bookmark_content_tags +const deleteBookmarkTag = `-- name: DeleteBookmarkTag :exec +DELETE FROM bookmark_tags WHERE id = $1 AND user_id = $2 ` -type DeleteBookmarkContentTagParams struct { +type DeleteBookmarkTagParams struct { ID uuid.UUID UserID uuid.UUID } -func (q *Queries) DeleteBookmarkContentTag(ctx context.Context, db DBTX, arg DeleteBookmarkContentTagParams) error { - _, err := db.Exec(ctx, deleteBookmarkContentTag, arg.ID, arg.UserID) +func (q *Queries) DeleteBookmarkTag(ctx context.Context, db DBTX, arg DeleteBookmarkTagParams) error { + _, err := db.Exec(ctx, deleteBookmarkTag, arg.ID, arg.UserID) return err } @@ -184,15 +186,15 @@ func (q *Queries) DeleteBookmarksByUser(ctx context.Context, db DBTX, userID pgt const getBookmark = `-- name: GetBookmark :one SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, - bc.id, bc.type, bc.title, bc.description, bc.url, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.metadata, bc.created_at, bc.updated_at, + bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), ARRAY[]::VARCHAR[] ) as tags FROM bookmarks b JOIN bookmark_content bc ON b.content_id = bc.id - LEFT JOIN bookmark_content_tags_mapping bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags bct ON bctm.tag_id = bct.id WHERE b.id = $1 AND b.user_id = $2 GROUP BY b.id, bc.id @@ -217,18 +219,20 @@ type GetBookmarkRow struct { UpdatedAt pgtype.Timestamptz ID_2 uuid.UUID Type string - Title string + Url string + UserID_2 pgtype.UUID + Title pgtype.Text Description pgtype.Text - Url pgtype.Text Domain pgtype.Text S3Key pgtype.Text Summary pgtype.Text Content pgtype.Text Html pgtype.Text + Tags []string Metadata_2 []byte CreatedAt_2 pgtype.Timestamptz UpdatedAt_2 pgtype.Timestamptz - Tags interface{} + Tags_2 interface{} } func (q *Queries) GetBookmark(ctx context.Context, db DBTX, arg GetBookmarkParams) (GetBookmarkRow, error) { @@ -247,22 +251,39 @@ func (q *Queries) GetBookmark(ctx context.Context, db DBTX, arg GetBookmarkParam &i.UpdatedAt, &i.ID_2, &i.Type, + &i.Url, + &i.UserID_2, &i.Title, &i.Description, - &i.Url, &i.Domain, &i.S3Key, &i.Summary, &i.Content, &i.Html, + &i.Tags, &i.Metadata_2, &i.CreatedAt_2, &i.UpdatedAt_2, - &i.Tags, + &i.Tags_2, ) return i, err } +const isBookmarkContentExistWithURL = `-- name: IsBookmarkContentExistWithURL :one +SELECT EXISTS ( + SELECT 1 + FROM bookmark_content bc + WHERE bc.url = $1 +) +` + +func (q *Queries) IsBookmarkContentExistWithURL(ctx context.Context, db DBTX, url string) (bool, error) { + row := db.QueryRow(ctx, isBookmarkContentExistWithURL, url) + var exists bool + err := row.Scan(&exists) + return exists, err +} + const isBookmarkExistWithURL = `-- name: IsBookmarkExistWithURL :one SELECT EXISTS ( SELECT 1 @@ -274,7 +295,7 @@ SELECT EXISTS ( ` type IsBookmarkExistWithURLParams struct { - Url pgtype.Text + Url string UserID pgtype.UUID } @@ -285,51 +306,53 @@ func (q *Queries) IsBookmarkExistWithURL(ctx context.Context, db DBTX, arg IsBoo return exists, err } -const linkBookmarkContentWithTags = `-- name: LinkBookmarkContentWithTags :exec -INSERT INTO bookmark_content_tags_mapping (content_id, tag_id) -SELECT $1, bct.id -FROM bookmark_content_tags bct -WHERE bct.name = ANY ($2::text[]) - AND bct.user_id = $3 +const linkBookmarkWithTags = `-- name: LinkBookmarkWithTags :exec +INSERT INTO bookmark_tags_mapping (bookmark_id, tag_id) +SELECT $1, bt.id +FROM bookmark_tags bt +WHERE bt.name = ANY ($2::text[]) + AND bt.user_id = $3 ` -type LinkBookmarkContentWithTagsParams struct { - ContentID uuid.UUID - Column2 []string - UserID uuid.UUID +type LinkBookmarkWithTagsParams struct { + BookmarkID uuid.UUID + Column2 []string + UserID uuid.UUID } -func (q *Queries) LinkBookmarkContentWithTags(ctx context.Context, db DBTX, arg LinkBookmarkContentWithTagsParams) error { - _, err := db.Exec(ctx, linkBookmarkContentWithTags, arg.ContentID, arg.Column2, arg.UserID) +func (q *Queries) LinkBookmarkWithTags(ctx context.Context, db DBTX, arg LinkBookmarkWithTagsParams) error { + _, err := db.Exec(ctx, linkBookmarkWithTags, arg.BookmarkID, arg.Column2, arg.UserID) return err } -const listBookmarkContentTags = `-- name: ListBookmarkContentTags :many -SELECT bct.name -FROM bookmark_content_tags bct - JOIN bookmark_content_tags_mapping bctm ON bct.id = bctm.tag_id -WHERE bctm.content_id = $1 - AND bct.user_id = $2 +const listBookmarkDomains = `-- name: ListBookmarkDomains :many +SELECT bc.domain, count(*) as cnt +FROM bookmarks b + JOIN bookmark_content bc ON b.content_id = bc.id +WHERE b.user_id = $1 +AND bc.domain IS NOT NULL +GROUP BY bc.domain +ORDER BY cnt DESC, domain ASC ` -type ListBookmarkContentTagsParams struct { - ContentID uuid.UUID - UserID uuid.UUID +type ListBookmarkDomainsRow struct { + Domain pgtype.Text + Cnt int64 } -func (q *Queries) ListBookmarkContentTags(ctx context.Context, db DBTX, arg ListBookmarkContentTagsParams) ([]string, error) { - rows, err := db.Query(ctx, listBookmarkContentTags, arg.ContentID, arg.UserID) +func (q *Queries) ListBookmarkDomains(ctx context.Context, db DBTX, userID pgtype.UUID) ([]ListBookmarkDomainsRow, error) { + rows, err := db.Query(ctx, listBookmarkDomains, userID) if err != nil { return nil, err } defer rows.Close() - var items []string + var items []ListBookmarkDomainsRow for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { + var i ListBookmarkDomainsRow + if err := rows.Scan(&i.Domain, &i.Cnt); err != nil { return nil, err } - items = append(items, name) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -337,34 +360,26 @@ func (q *Queries) ListBookmarkContentTags(ctx context.Context, db DBTX, arg List return items, nil } -const listBookmarkDomains = `-- name: ListBookmarkDomains :many -SELECT bc.domain, count(*) as count -FROM bookmarks b - JOIN bookmark_content bc ON b.content_id = bc.id -WHERE b.user_id = $1 -AND bc.domain IS NOT NULL -GROUP BY bc.domain -ORDER BY count DESC, domain ASC +const listBookmarkTagsByBookmarkId = `-- name: ListBookmarkTagsByBookmarkId :many +SELECT bt.name +FROM bookmark_tags bt + JOIN bookmark_tags_mapping btm ON bt.id = btm.tag_id +WHERE btm.bookmark_id = $1 ` -type ListBookmarkDomainsRow struct { - Domain pgtype.Text - Count int64 -} - -func (q *Queries) ListBookmarkDomains(ctx context.Context, db DBTX, userID pgtype.UUID) ([]ListBookmarkDomainsRow, error) { - rows, err := db.Query(ctx, listBookmarkDomains, userID) +func (q *Queries) ListBookmarkTagsByBookmarkId(ctx context.Context, db DBTX, bookmarkID uuid.UUID) ([]string, error) { + rows, err := db.Query(ctx, listBookmarkTagsByBookmarkId, bookmarkID) if err != nil { return nil, err } defer rows.Close() - var items []ListBookmarkDomainsRow + var items []string for rows.Next() { - var i ListBookmarkDomainsRow - if err := rows.Scan(&i.Domain, &i.Count); err != nil { + var name string + if err := rows.Scan(&name); err != nil { return nil, err } - items = append(items, i) + items = append(items, name) } if err := rows.Err(); err != nil { return nil, err @@ -373,33 +388,23 @@ func (q *Queries) ListBookmarkDomains(ctx context.Context, db DBTX, userID pgtyp } const listBookmarkTagsByUser = `-- name: ListBookmarkTagsByUser :many -SELECT bct.name, count(bctm.*) as count -FROM bookmark_content_tags bct - JOIN bookmark_content_tags_mapping bctm ON bct.id = bctm.tag_id -WHERE bct.user_id = $1 -GROUP BY bct.name -ORDER BY count DESC +SELECT name FROM bookmark_tags +WHERE user_id = $1 ` -type ListBookmarkTagsByUserRow struct { - Name string - Count int64 -} - -// Tags related queries similar to content.sql but adapted for new schema -func (q *Queries) ListBookmarkTagsByUser(ctx context.Context, db DBTX, userID uuid.UUID) ([]ListBookmarkTagsByUserRow, error) { +func (q *Queries) ListBookmarkTagsByUser(ctx context.Context, db DBTX, userID uuid.UUID) ([]string, error) { rows, err := db.Query(ctx, listBookmarkTagsByUser, userID) if err != nil { return nil, err } defer rows.Close() - var items []ListBookmarkTagsByUserRow + var items []string for rows.Next() { - var i ListBookmarkTagsByUserRow - if err := rows.Scan(&i.Name, &i.Count); err != nil { + var name string + if err := rows.Scan(&name); err != nil { return nil, err } - items = append(items, i) + items = append(items, name) } if err := rows.Err(); err != nil { return nil, err @@ -412,15 +417,15 @@ WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) AND ($6::text[] IS NULL OR bct.name = ANY($6::text[])) ) SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, - bc.id, bc.type, bc.title, bc.description, bc.url, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.metadata, bc.created_at, bc.updated_at, + bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -429,8 +434,8 @@ SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) @@ -462,19 +467,21 @@ type ListBookmarksRow struct { UpdatedAt pgtype.Timestamptz ID_2 uuid.UUID Type string - Title string + Url string + UserID_2 pgtype.UUID + Title pgtype.Text Description pgtype.Text - Url pgtype.Text Domain pgtype.Text S3Key pgtype.Text Summary pgtype.Text Content pgtype.Text Html pgtype.Text + Tags []string Metadata_2 []byte CreatedAt_2 pgtype.Timestamptz UpdatedAt_2 pgtype.Timestamptz TotalCount int64 - Tags interface{} + Tags_2 interface{} } func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksParams) ([]ListBookmarksRow, error) { @@ -506,19 +513,21 @@ func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksP &i.UpdatedAt, &i.ID_2, &i.Type, + &i.Url, + &i.UserID_2, &i.Title, &i.Description, - &i.Url, &i.Domain, &i.S3Key, &i.Summary, &i.Content, &i.Html, + &i.Tags, &i.Metadata_2, &i.CreatedAt_2, &i.UpdatedAt_2, &i.TotalCount, - &i.Tags, + &i.Tags_2, ); err != nil { return nil, err } @@ -532,7 +541,7 @@ func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksP const listExistingBookmarkTagsByTags = `-- name: ListExistingBookmarkTagsByTags :many SELECT name -FROM bookmark_content_tags +FROM bookmark_tags WHERE name = ANY ($1::text[]) AND user_id = $2 ` @@ -562,13 +571,31 @@ func (q *Queries) ListExistingBookmarkTagsByTags(ctx context.Context, db DBTX, a return items, nil } +const ownerTransferBookmark = `-- name: OwnerTransferBookmark :exec +UPDATE bookmarks +SET + user_id = $2, + updated_at = CURRENT_TIMESTAMP +WHERE user_id = $1 +` + +type OwnerTransferBookmarkParams struct { + UserID pgtype.UUID + UserID_2 pgtype.UUID +} + +func (q *Queries) OwnerTransferBookmark(ctx context.Context, db DBTX, arg OwnerTransferBookmarkParams) error { + _, err := db.Exec(ctx, ownerTransferBookmark, arg.UserID, arg.UserID_2) + return err +} + const searchBookmarks = `-- name: SearchBookmarks :many WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) @@ -583,7 +610,7 @@ WITH total AS ( ) ) SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, - bc.id, bc.type, bc.title, bc.description, bc.url, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.metadata, bc.created_at, bc.updated_at, + bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -592,8 +619,8 @@ SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_content_tags_mapping AS bctm ON bc.id = bctm.content_id - LEFT JOIN bookmark_content_tags AS bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) @@ -634,19 +661,21 @@ type SearchBookmarksRow struct { UpdatedAt pgtype.Timestamptz ID_2 uuid.UUID Type string - Title string + Url string + UserID_2 pgtype.UUID + Title pgtype.Text Description pgtype.Text - Url pgtype.Text Domain pgtype.Text S3Key pgtype.Text Summary pgtype.Text Content pgtype.Text Html pgtype.Text + Tags []string Metadata_2 []byte CreatedAt_2 pgtype.Timestamptz UpdatedAt_2 pgtype.Timestamptz TotalCount int64 - Tags interface{} + Tags_2 interface{} } func (q *Queries) SearchBookmarks(ctx context.Context, db DBTX, arg SearchBookmarksParams) ([]SearchBookmarksRow, error) { @@ -679,19 +708,21 @@ func (q *Queries) SearchBookmarks(ctx context.Context, db DBTX, arg SearchBookma &i.UpdatedAt, &i.ID_2, &i.Type, + &i.Url, + &i.UserID_2, &i.Title, &i.Description, - &i.Url, &i.Domain, &i.S3Key, &i.Summary, &i.Content, &i.Html, + &i.Tags, &i.Metadata_2, &i.CreatedAt_2, &i.UpdatedAt_2, &i.TotalCount, - &i.Tags, + &i.Tags_2, ); err != nil { return nil, err } @@ -703,23 +734,23 @@ func (q *Queries) SearchBookmarks(ctx context.Context, db DBTX, arg SearchBookma return items, nil } -const unLinkBookmarkContentWithTags = `-- name: UnLinkBookmarkContentWithTags :exec -DELETE FROM bookmark_content_tags_mapping -WHERE content_id = $1 +const unLinkBookmarkWithTags = `-- name: UnLinkBookmarkWithTags :exec +DELETE FROM bookmark_tags_mapping +WHERE bookmark_id = $1 AND tag_id IN (SELECT id - FROM bookmark_content_tags + FROM bookmark_tags WHERE name = ANY ($2::text[]) AND user_id = $3) ` -type UnLinkBookmarkContentWithTagsParams struct { - ContentID uuid.UUID - Column2 []string - UserID uuid.UUID +type UnLinkBookmarkWithTagsParams struct { + BookmarkID uuid.UUID + Column2 []string + UserID uuid.UUID } -func (q *Queries) UnLinkBookmarkContentWithTags(ctx context.Context, db DBTX, arg UnLinkBookmarkContentWithTagsParams) error { - _, err := db.Exec(ctx, unLinkBookmarkContentWithTags, arg.ContentID, arg.Column2, arg.UserID) +func (q *Queries) UnLinkBookmarkWithTags(ctx context.Context, db DBTX, arg UnLinkBookmarkWithTagsParams) error { + _, err := db.Exec(ctx, unLinkBookmarkWithTags, arg.BookmarkID, arg.Column2, arg.UserID) return err } @@ -775,22 +806,20 @@ const updateBookmarkContent = `-- name: UpdateBookmarkContent :one UPDATE bookmark_content SET title = COALESCE($2, title), description = COALESCE($3, description), - url = COALESCE($4, url), - domain = COALESCE($5, domain), - s3_key = COALESCE($6, s3_key), - summary = COALESCE($7, summary), - content = COALESCE($8, content), - html = COALESCE($9, html), - metadata = COALESCE($10, metadata) + domain = COALESCE($4, domain), + s3_key = COALESCE($5, s3_key), + summary = COALESCE($6, summary), + content = COALESCE($7, content), + html = COALESCE($8, html), + metadata = COALESCE($9, metadata) WHERE id = $1 -RETURNING id, type, title, description, url, domain, s3_key, summary, content, html, metadata, created_at, updated_at +RETURNING id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at ` type UpdateBookmarkContentParams struct { ID uuid.UUID Title pgtype.Text Description pgtype.Text - Url pgtype.Text Domain pgtype.Text S3Key pgtype.Text Summary pgtype.Text @@ -804,7 +833,6 @@ func (q *Queries) UpdateBookmarkContent(ctx context.Context, db DBTX, arg Update arg.ID, arg.Title, arg.Description, - arg.Url, arg.Domain, arg.S3Key, arg.Summary, @@ -816,14 +844,16 @@ func (q *Queries) UpdateBookmarkContent(ctx context.Context, db DBTX, arg Update err := row.Scan( &i.ID, &i.Type, + &i.Url, + &i.UserID, &i.Title, &i.Description, - &i.Url, &i.Domain, &i.S3Key, &i.Summary, &i.Content, &i.Html, + &i.Tags, &i.Metadata, &i.CreatedAt, &i.UpdatedAt, diff --git a/internal/pkg/db/content.sql.go b/internal/pkg/db/content.sql.go deleted file mode 100644 index 2c3a349..0000000 --- a/internal/pkg/db/content.sql.go +++ /dev/null @@ -1,978 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: content.sql - -package db - -import ( - "context" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" -) - -const createContent = `-- name: CreateContent :one -INSERT INTO content (user_id, - type, - title, - description, - url, - domain, - s3_key, - summary, - content, - html, - metadata, - is_favorite) -VALUES ($1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12) -RETURNING id, user_id, type, title, description, url, domain, s3_key, summary, content, html, metadata, is_favorite, created_at, updated_at -` - -type CreateContentParams struct { - UserID uuid.UUID - Type string - Title string - Description pgtype.Text - Url pgtype.Text - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Metadata []byte - IsFavorite pgtype.Bool -} - -func (q *Queries) CreateContent(ctx context.Context, db DBTX, arg CreateContentParams) (Content, error) { - row := db.QueryRow(ctx, createContent, - arg.UserID, - arg.Type, - arg.Title, - arg.Description, - arg.Url, - arg.Domain, - arg.S3Key, - arg.Summary, - arg.Content, - arg.Html, - arg.Metadata, - arg.IsFavorite, - ) - var i Content - err := row.Scan( - &i.ID, - &i.UserID, - &i.Type, - &i.Title, - &i.Description, - &i.Url, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Metadata, - &i.IsFavorite, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const createContentTag = `-- name: CreateContentTag :one -INSERT INTO content_tags (name, user_id) -VALUES ($1, $2) -ON CONFLICT (name, user_id) DO UPDATE - SET usage_count = content_tags.usage_count + 1 -RETURNING id, name, user_id, usage_count, created_at, updated_at -` - -type CreateContentTagParams struct { - Name string - UserID uuid.UUID -} - -func (q *Queries) CreateContentTag(ctx context.Context, db DBTX, arg CreateContentTagParams) (ContentTag, error) { - row := db.QueryRow(ctx, createContentTag, arg.Name, arg.UserID) - var i ContentTag - err := row.Scan( - &i.ID, - &i.Name, - &i.UserID, - &i.UsageCount, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const createShareContent = `-- name: CreateShareContent :one -INSERT INTO content_share (user_id, content_id, expires_at) -VALUES ($1, $2, $3) -RETURNING id, user_id, content_id, expires_at, created_at, updated_at -` - -type CreateShareContentParams struct { - UserID uuid.UUID - ContentID pgtype.UUID - ExpiresAt pgtype.Timestamptz -} - -func (q *Queries) CreateShareContent(ctx context.Context, db DBTX, arg CreateShareContentParams) (ContentShare, error) { - row := db.QueryRow(ctx, createShareContent, arg.UserID, arg.ContentID, arg.ExpiresAt) - var i ContentShare - err := row.Scan( - &i.ID, - &i.UserID, - &i.ContentID, - &i.ExpiresAt, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteContent = `-- name: DeleteContent :exec -DELETE -FROM content -WHERE id = $1 - AND user_id = $2 -` - -type DeleteContentParams struct { - ID uuid.UUID - UserID uuid.UUID -} - -func (q *Queries) DeleteContent(ctx context.Context, db DBTX, arg DeleteContentParams) error { - _, err := db.Exec(ctx, deleteContent, arg.ID, arg.UserID) - return err -} - -const deleteContentTag = `-- name: DeleteContentTag :exec -DELETE -FROM content_tags -WHERE id = $1 - AND user_id = $2 -` - -type DeleteContentTagParams struct { - ID uuid.UUID - UserID uuid.UUID -} - -func (q *Queries) DeleteContentTag(ctx context.Context, db DBTX, arg DeleteContentTagParams) error { - _, err := db.Exec(ctx, deleteContentTag, arg.ID, arg.UserID) - return err -} - -const deleteContentsByUser = `-- name: DeleteContentsByUser :exec -DELETE -FROM content -WHERE user_id = $1 -` - -func (q *Queries) DeleteContentsByUser(ctx context.Context, db DBTX, userID uuid.UUID) error { - _, err := db.Exec(ctx, deleteContentsByUser, userID) - return err -} - -const deleteExpiredShareContent = `-- name: DeleteExpiredShareContent :exec -DELETE -FROM content_share -WHERE expires_at < now() -` - -func (q *Queries) DeleteExpiredShareContent(ctx context.Context, db DBTX) error { - _, err := db.Exec(ctx, deleteExpiredShareContent) - return err -} - -const deleteShareContent = `-- name: DeleteShareContent :exec -DELETE FROM content_share cs -USING content c -WHERE cs.content_id = c.id - AND c.id = $1 - AND c.user_id = $2 -` - -type DeleteShareContentParams struct { - ID uuid.UUID - UserID uuid.UUID -} - -func (q *Queries) DeleteShareContent(ctx context.Context, db DBTX, arg DeleteShareContentParams) error { - _, err := db.Exec(ctx, deleteShareContent, arg.ID, arg.UserID) - return err -} - -const getContent = `-- name: GetContent :one -SELECT c.id, c.user_id, c.type, c.title, c.description, c.url, c.domain, c.s3_key, c.summary, c.content, c.html, c.metadata, c.is_favorite, c.created_at, c.updated_at, - COALESCE( - array_agg(ct.name) FILTER ( - WHERE - ct.name IS NOT NULL - ), - ARRAY [] :: VARCHAR[] - ) as tags -FROM content c - LEFT JOIN content_tags_mapping ctm ON c.id = ctm.content_id - LEFT JOIN content_tags ct ON ctm.tag_id = ct.id -WHERE c.id = $1 - AND c.user_id = $2 -GROUP BY c.id -LIMIT 1 -` - -type GetContentParams struct { - ID uuid.UUID - UserID uuid.UUID -} - -type GetContentRow struct { - ID uuid.UUID - UserID uuid.UUID - Type string - Title string - Description pgtype.Text - Url pgtype.Text - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Metadata []byte - IsFavorite pgtype.Bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz - Tags interface{} -} - -func (q *Queries) GetContent(ctx context.Context, db DBTX, arg GetContentParams) (GetContentRow, error) { - row := db.QueryRow(ctx, getContent, arg.ID, arg.UserID) - var i GetContentRow - err := row.Scan( - &i.ID, - &i.UserID, - &i.Type, - &i.Title, - &i.Description, - &i.Url, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Metadata, - &i.IsFavorite, - &i.CreatedAt, - &i.UpdatedAt, - &i.Tags, - ) - return i, err -} - -const getShareContent = `-- name: GetShareContent :one -SELECT id, user_id, content_id, expires_at, created_at, updated_at -FROM content_share -WHERE content_id = $1 - AND user_id = $2 -` - -type GetShareContentParams struct { - ContentID pgtype.UUID - UserID uuid.UUID -} - -// info about the shared content -func (q *Queries) GetShareContent(ctx context.Context, db DBTX, arg GetShareContentParams) (ContentShare, error) { - row := db.QueryRow(ctx, getShareContent, arg.ContentID, arg.UserID) - var i ContentShare - err := row.Scan( - &i.ID, - &i.UserID, - &i.ContentID, - &i.ExpiresAt, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const getSharedContent = `-- name: GetSharedContent :one -SELECT c.id, c.user_id, c.type, c.title, c.description, c.url, c.domain, c.s3_key, c.summary, c.content, c.html, c.metadata, c.is_favorite, c.created_at, c.updated_at -FROM content_share AS cs - JOIN content AS c ON cs.content_id = c.id -WHERE cs.id = $1 - AND (cs.expires_at is NULL OR cs.expires_at > now()) -` - -// get the shared content from content table -func (q *Queries) GetSharedContent(ctx context.Context, db DBTX, id uuid.UUID) (Content, error) { - row := db.QueryRow(ctx, getSharedContent, id) - var i Content - err := row.Scan( - &i.ID, - &i.UserID, - &i.Type, - &i.Title, - &i.Description, - &i.Url, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Metadata, - &i.IsFavorite, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const isContentExistWithURL = `-- name: IsContentExistWithURL :one -SELECT EXISTS (SELECT 1 - FROM content - WHERE url = $1 - AND user_id = $2) -` - -type IsContentExistWithURLParams struct { - Url pgtype.Text - UserID uuid.UUID -} - -func (q *Queries) IsContentExistWithURL(ctx context.Context, db DBTX, arg IsContentExistWithURLParams) (bool, error) { - row := db.QueryRow(ctx, isContentExistWithURL, arg.Url, arg.UserID) - var exists bool - err := row.Scan(&exists) - return exists, err -} - -const linkContentWithTags = `-- name: LinkContentWithTags :exec -INSERT INTO content_tags_mapping (content_id, tag_id) -SELECT $1, - ct.id -FROM content_tags ct -WHERE ct.name = ANY ($2 :: text[]) - AND ct.user_id = $3 -` - -type LinkContentWithTagsParams struct { - ContentID uuid.UUID - Column2 []string - UserID uuid.UUID -} - -// $1: content_id, $2: text[], $3: user_id -func (q *Queries) LinkContentWithTags(ctx context.Context, db DBTX, arg LinkContentWithTagsParams) error { - _, err := db.Exec(ctx, linkContentWithTags, arg.ContentID, arg.Column2, arg.UserID) - return err -} - -const listContentDomains = `-- name: ListContentDomains :many -SELECT domain, count(*) as count -FROM content -WHERE user_id = $1 -AND domain IS NOT NULL -GROUP BY domain -ORDER BY count DESC, domain ASC -` - -type ListContentDomainsRow struct { - Domain pgtype.Text - Count int64 -} - -func (q *Queries) ListContentDomains(ctx context.Context, db DBTX, userID uuid.UUID) ([]ListContentDomainsRow, error) { - rows, err := db.Query(ctx, listContentDomains, userID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ListContentDomainsRow - for rows.Next() { - var i ListContentDomainsRow - if err := rows.Scan(&i.Domain, &i.Count); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listContentTags = `-- name: ListContentTags :many -SELECT ct.name -FROM content_tags ct - JOIN content_tags_mapping ctm ON ct.id = ctm.tag_id -WHERE ctm.content_id = $1 - AND ct.user_id = $2 -` - -type ListContentTagsParams struct { - ContentID uuid.UUID - UserID uuid.UUID -} - -func (q *Queries) ListContentTags(ctx context.Context, db DBTX, arg ListContentTagsParams) ([]string, error) { - rows, err := db.Query(ctx, listContentTags, arg.ContentID, arg.UserID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []string - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - return nil, err - } - items = append(items, name) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listContents = `-- name: ListContents :many -WITH total AS ( - SELECT COUNT( DISTINCT tc.*) AS total_count - FROM content AS tc - LEFT JOIN content_tags_mapping AS tctm ON tc.id = tctm.content_id - LEFT JOIN content_tags AS tct ON tctm.tag_id = tct.id - WHERE tc.user_id = $1 - AND ( - $4 :: text[] IS NULL - OR tc.domain = ANY ($4 :: text[]) - ) - AND ( - $5 :: text[] IS NULL - OR tc.type = ANY ($5 :: text[]) - ) - AND ( - $6 :: text[] IS NULL - OR tct.name = ANY ($6 :: text[]) - ) -) -SELECT c.id, c.user_id, c.type, c.title, c.description, c.url, c.domain, c.s3_key, c.summary, c.content, c.html, c.metadata, c.is_favorite, c.created_at, c.updated_at, - t.total_count, - COALESCE( - array_agg(ct.name) FILTER ( - WHERE - ct.name IS NOT NULL - ), - ARRAY [] :: VARCHAR[] - ) AS tags -FROM content AS c - CROSS JOIN total AS t - LEFT JOIN content_tags_mapping AS ctm ON c.id = ctm.content_id - LEFT JOIN content_tags AS ct ON ctm.tag_id = ct.id -WHERE c.user_id = $1 - AND ( - $4 :: text[] IS NULL - OR c.domain = ANY ($4 :: text[]) - ) - AND ( - $5 :: text[] IS NULL - OR c.type = ANY ($5 :: text[]) - ) - AND ( - $6 :: text[] IS NULL - OR ct.name = ANY ($6 :: text[]) - ) -GROUP BY c.id, - t.total_count -ORDER BY c.created_at DESC -LIMIT $2 OFFSET $3 -` - -type ListContentsParams struct { - UserID uuid.UUID - Limit int32 - Offset int32 - Domains []string - Types []string - Tags []string -} - -type ListContentsRow struct { - ID uuid.UUID - UserID uuid.UUID - Type string - Title string - Description pgtype.Text - Url pgtype.Text - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Metadata []byte - IsFavorite pgtype.Bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz - TotalCount int64 - Tags interface{} -} - -func (q *Queries) ListContents(ctx context.Context, db DBTX, arg ListContentsParams) ([]ListContentsRow, error) { - rows, err := db.Query(ctx, listContents, - arg.UserID, - arg.Limit, - arg.Offset, - arg.Domains, - arg.Types, - arg.Tags, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ListContentsRow - for rows.Next() { - var i ListContentsRow - if err := rows.Scan( - &i.ID, - &i.UserID, - &i.Type, - &i.Title, - &i.Description, - &i.Url, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Metadata, - &i.IsFavorite, - &i.CreatedAt, - &i.UpdatedAt, - &i.TotalCount, - &i.Tags, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listExistingTagsByTags = `-- name: ListExistingTagsByTags :many -SELECT name -FROM content_tags -WHERE name = ANY ($1 :: text[]) - AND user_id = $2 -` - -type ListExistingTagsByTagsParams struct { - Column1 []string - UserID uuid.UUID -} - -func (q *Queries) ListExistingTagsByTags(ctx context.Context, db DBTX, arg ListExistingTagsByTagsParams) ([]string, error) { - rows, err := db.Query(ctx, listExistingTagsByTags, arg.Column1, arg.UserID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []string - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - return nil, err - } - items = append(items, name) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listShareContent = `-- name: ListShareContent :many -SELECT c.id, c.user_id, c.type, c.title, c.description, c.url, c.domain, c.s3_key, c.summary, c.content, c.html, c.metadata, c.is_favorite, c.created_at, c.updated_at -FROM content_share AS cs - JOIN content AS c ON cs.content_id = c.id -WHERE cs.user_id = $1 - AND cs.expires_at is NULL OR cs.expires_at > now() -ORDER BY cs.created_at DESC -LIMIT $2 OFFSET $3 -` - -type ListShareContentParams struct { - UserID uuid.UUID - Limit int32 - Offset int32 -} - -func (q *Queries) ListShareContent(ctx context.Context, db DBTX, arg ListShareContentParams) ([]Content, error) { - rows, err := db.Query(ctx, listShareContent, arg.UserID, arg.Limit, arg.Offset) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Content - for rows.Next() { - var i Content - if err := rows.Scan( - &i.ID, - &i.UserID, - &i.Type, - &i.Title, - &i.Description, - &i.Url, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Metadata, - &i.IsFavorite, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listTagsByUser = `-- name: ListTagsByUser :many -SELECT ct.name, count(ctm.*) as count -FROM content_tags ct - JOIN content_tags_mapping ctm ON ct.id = ctm.tag_id -WHERE ct.user_id = $1 -GROUP BY ct.name -ORDER BY count DESC -` - -type ListTagsByUserRow struct { - Name string - Count int64 -} - -func (q *Queries) ListTagsByUser(ctx context.Context, db DBTX, userID uuid.UUID) ([]ListTagsByUserRow, error) { - rows, err := db.Query(ctx, listTagsByUser, userID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ListTagsByUserRow - for rows.Next() { - var i ListTagsByUserRow - if err := rows.Scan(&i.Name, &i.Count); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const ownerTransferContent = `-- name: OwnerTransferContent :exec -UPDATE - content -SET user_id = $2 -WHERE id = $1 - AND user_id = $3 -` - -type OwnerTransferContentParams struct { - ID uuid.UUID - UserID uuid.UUID - UserID_2 uuid.UUID -} - -func (q *Queries) OwnerTransferContent(ctx context.Context, db DBTX, arg OwnerTransferContentParams) error { - _, err := db.Exec(ctx, ownerTransferContent, arg.ID, arg.UserID, arg.UserID_2) - return err -} - -const searchContentsWithFilter = `-- name: SearchContentsWithFilter :many -WITH total AS ( - SELECT COUNT( DISTINCT tc.*) AS total_count - FROM content AS tc - LEFT JOIN content_tags_mapping AS tctm ON tc.id = tctm.content_id - LEFT JOIN content_tags AS tct ON tctm.tag_id = tct.id - WHERE tc.user_id = $1 - AND ( - $4 :: text[] IS NULL - OR tc.domain = ANY ($4 :: text[]) - ) - AND ( - $5 :: text[] IS NULL - OR tc.type = ANY ($5 :: text[]) - ) - AND ( - $6 :: text[] IS NULL - OR tct.name = ANY ($6 :: text[]) - ) - AND ( - $7 :: text IS NULL - OR tc.title @@@ $7 - OR tc.description @@@ $7 - OR tc.summary @@@ $7 - OR tc.content @@@ $7 - OR tc.metadata @@@ $7 - ) -) -SELECT c.id, c.user_id, c.type, c.title, c.description, c.url, c.domain, c.s3_key, c.summary, c.content, c.html, c.metadata, c.is_favorite, c.created_at, c.updated_at, - t.total_count, - COALESCE( - array_agg(ct.name) FILTER ( - WHERE - ct.name IS NOT NULL - ), - ARRAY [] :: VARCHAR[] - ) AS tags -FROM content AS c - CROSS JOIN total AS t - LEFT JOIN content_tags_mapping AS ctm ON c.id = ctm.content_id - LEFT JOIN content_tags AS ct ON ctm.tag_id = ct.id -WHERE c.user_id = $1 - AND ( - $4 :: text[] IS NULL - OR c.domain = ANY ($4 :: text[]) - ) - AND ( - $5 :: text[] IS NULL - OR c.type = ANY ($5 :: text[]) - ) - AND ( - $6 :: text[] IS NULL - OR ct.name = ANY ($6 :: text[]) - ) - AND ( - $7 :: text IS NULL - OR c.title @@@ $7 - OR c.description @@@ $7 - OR c.summary @@@ $7 - OR c.content @@@ $7 - OR c.metadata @@@ $7 - ) -GROUP BY c.id, - t.total_count -ORDER BY c.created_at DESC -LIMIT $2 OFFSET $3 -` - -type SearchContentsWithFilterParams struct { - UserID uuid.UUID - Limit int32 - Offset int32 - Domains []string - Types []string - Tags []string - Query pgtype.Text -} - -type SearchContentsWithFilterRow struct { - ID uuid.UUID - UserID uuid.UUID - Type string - Title string - Description pgtype.Text - Url pgtype.Text - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Metadata []byte - IsFavorite pgtype.Bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz - TotalCount int64 - Tags interface{} -} - -func (q *Queries) SearchContentsWithFilter(ctx context.Context, db DBTX, arg SearchContentsWithFilterParams) ([]SearchContentsWithFilterRow, error) { - rows, err := db.Query(ctx, searchContentsWithFilter, - arg.UserID, - arg.Limit, - arg.Offset, - arg.Domains, - arg.Types, - arg.Tags, - arg.Query, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []SearchContentsWithFilterRow - for rows.Next() { - var i SearchContentsWithFilterRow - if err := rows.Scan( - &i.ID, - &i.UserID, - &i.Type, - &i.Title, - &i.Description, - &i.Url, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Metadata, - &i.IsFavorite, - &i.CreatedAt, - &i.UpdatedAt, - &i.TotalCount, - &i.Tags, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const unLinkContentWithTags = `-- name: UnLinkContentWithTags :exec -DELETE FROM content_tags_mapping -WHERE content_id = $1 - AND tag_id IN (SELECT id - FROM content_tags - WHERE name = ANY ($2 :: text[]) - AND user_id = $3) -` - -type UnLinkContentWithTagsParams struct { - ContentID uuid.UUID - Column2 []string - UserID uuid.UUID -} - -// $1: content_id, $2: text[], $3: user_id -func (q *Queries) UnLinkContentWithTags(ctx context.Context, db DBTX, arg UnLinkContentWithTagsParams) error { - _, err := db.Exec(ctx, unLinkContentWithTags, arg.ContentID, arg.Column2, arg.UserID) - return err -} - -const updateContent = `-- name: UpdateContent :one -UPDATE - content -SET title = COALESCE($3, title), - description = COALESCE($4, description), - url = COALESCE($5, url), - domain = COALESCE($6, domain), - s3_key = COALESCE($7, s3_key), - summary = COALESCE($8, summary), - content = COALESCE($9, content), - html = COALESCE($10, html), - metadata = COALESCE($11, metadata), - is_favorite = COALESCE($12, is_favorite) -WHERE id = $1 - AND user_id = $2 -RETURNING id, user_id, type, title, description, url, domain, s3_key, summary, content, html, metadata, is_favorite, created_at, updated_at -` - -type UpdateContentParams struct { - ID uuid.UUID - UserID uuid.UUID - Title pgtype.Text - Description pgtype.Text - Url pgtype.Text - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Metadata []byte - IsFavorite pgtype.Bool -} - -func (q *Queries) UpdateContent(ctx context.Context, db DBTX, arg UpdateContentParams) (Content, error) { - row := db.QueryRow(ctx, updateContent, - arg.ID, - arg.UserID, - arg.Title, - arg.Description, - arg.Url, - arg.Domain, - arg.S3Key, - arg.Summary, - arg.Content, - arg.Html, - arg.Metadata, - arg.IsFavorite, - ) - var i Content - err := row.Scan( - &i.ID, - &i.UserID, - &i.Type, - &i.Title, - &i.Description, - &i.Url, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Metadata, - &i.IsFavorite, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const updateShareContent = `-- name: UpdateShareContent :one -UPDATE content_share cs -SET expires_at = $3 -FROM content c -WHERE cs.content_id = c.id - AND c.id = $1 - AND c.user_id = $2 -RETURNING cs.id, cs.user_id, cs.content_id, cs.expires_at, cs.created_at, cs.updated_at -` - -type UpdateShareContentParams struct { - ID uuid.UUID - UserID uuid.UUID - ExpiresAt pgtype.Timestamptz -} - -func (q *Queries) UpdateShareContent(ctx context.Context, db DBTX, arg UpdateShareContentParams) (ContentShare, error) { - row := db.QueryRow(ctx, updateShareContent, arg.ID, arg.UserID, arg.ExpiresAt) - var i ContentShare - err := row.Scan( - &i.ID, - &i.UserID, - &i.ContentID, - &i.ExpiresAt, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} diff --git a/internal/pkg/db/models.go b/internal/pkg/db/models.go index 39e528b..ca8bb9a 100644 --- a/internal/pkg/db/models.go +++ b/internal/pkg/db/models.go @@ -132,20 +132,22 @@ type Bookmark struct { type BookmarkContent struct { ID uuid.UUID Type string - Title string + Url string + UserID pgtype.UUID + Title pgtype.Text Description pgtype.Text - Url pgtype.Text Domain pgtype.Text S3Key pgtype.Text Summary pgtype.Text Content pgtype.Text Html pgtype.Text + Tags []string Metadata []byte CreatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz } -type BookmarkContentTag struct { +type BookmarkTag struct { ID uuid.UUID Name string UserID uuid.UUID @@ -153,11 +155,11 @@ type BookmarkContentTag struct { UpdatedAt pgtype.Timestamptz } -type BookmarkContentTagsMapping struct { - ContentID uuid.UUID - TagID uuid.UUID - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz +type BookmarkTagsMapping struct { + BookmarkID uuid.UUID + TagID uuid.UUID + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz } type Cache struct { diff --git a/internal/pkg/db/share_content.sql.go b/internal/pkg/db/share_content.sql.go new file mode 100644 index 0000000..fbb62ee --- /dev/null +++ b/internal/pkg/db/share_content.sql.go @@ -0,0 +1,209 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: share_content.sql + +package db + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createShareContent = `-- name: CreateShareContent :one +INSERT INTO content_share (user_id, content_id, expires_at) +VALUES ($1, $2, $3) +RETURNING id, user_id, content_id, expires_at, created_at, updated_at +` + +type CreateShareContentParams struct { + UserID uuid.UUID + ContentID pgtype.UUID + ExpiresAt pgtype.Timestamptz +} + +func (q *Queries) CreateShareContent(ctx context.Context, db DBTX, arg CreateShareContentParams) (ContentShare, error) { + row := db.QueryRow(ctx, createShareContent, arg.UserID, arg.ContentID, arg.ExpiresAt) + var i ContentShare + err := row.Scan( + &i.ID, + &i.UserID, + &i.ContentID, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteExpiredShareContent = `-- name: DeleteExpiredShareContent :exec +DELETE +FROM content_share +WHERE expires_at < now() +` + +func (q *Queries) DeleteExpiredShareContent(ctx context.Context, db DBTX) error { + _, err := db.Exec(ctx, deleteExpiredShareContent) + return err +} + +const deleteShareContent = `-- name: DeleteShareContent :exec +DELETE FROM content_share cs +USING content c +WHERE cs.content_id = c.id + AND c.id = $1 + AND c.user_id = $2 +` + +type DeleteShareContentParams struct { + ID uuid.UUID + UserID uuid.UUID +} + +func (q *Queries) DeleteShareContent(ctx context.Context, db DBTX, arg DeleteShareContentParams) error { + _, err := db.Exec(ctx, deleteShareContent, arg.ID, arg.UserID) + return err +} + +const getShareContent = `-- name: GetShareContent :one +SELECT id, user_id, content_id, expires_at, created_at, updated_at +FROM content_share +WHERE content_id = $1 + AND user_id = $2 +` + +type GetShareContentParams struct { + ContentID pgtype.UUID + UserID uuid.UUID +} + +// info about the shared content +func (q *Queries) GetShareContent(ctx context.Context, db DBTX, arg GetShareContentParams) (ContentShare, error) { + row := db.QueryRow(ctx, getShareContent, arg.ContentID, arg.UserID) + var i ContentShare + err := row.Scan( + &i.ID, + &i.UserID, + &i.ContentID, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getSharedContent = `-- name: GetSharedContent :one +SELECT c.id, c.user_id, c.type, c.title, c.description, c.url, c.domain, c.s3_key, c.summary, c.content, c.html, c.metadata, c.is_favorite, c.created_at, c.updated_at +FROM content_share AS cs + JOIN content AS c ON cs.content_id = c.id +WHERE cs.id = $1 + AND (cs.expires_at is NULL OR cs.expires_at > now()) +` + +// get the shared content from content table +func (q *Queries) GetSharedContent(ctx context.Context, db DBTX, id uuid.UUID) (Content, error) { + row := db.QueryRow(ctx, getSharedContent, id) + var i Content + err := row.Scan( + &i.ID, + &i.UserID, + &i.Type, + &i.Title, + &i.Description, + &i.Url, + &i.Domain, + &i.S3Key, + &i.Summary, + &i.Content, + &i.Html, + &i.Metadata, + &i.IsFavorite, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listShareContent = `-- name: ListShareContent :many +SELECT c.id, c.user_id, c.type, c.title, c.description, c.url, c.domain, c.s3_key, c.summary, c.content, c.html, c.metadata, c.is_favorite, c.created_at, c.updated_at +FROM content_share AS cs + JOIN content AS c ON cs.content_id = c.id +WHERE cs.user_id = $1 + AND cs.expires_at is NULL OR cs.expires_at > now() +ORDER BY cs.created_at DESC +LIMIT $2 OFFSET $3 +` + +type ListShareContentParams struct { + UserID uuid.UUID + Limit int32 + Offset int32 +} + +func (q *Queries) ListShareContent(ctx context.Context, db DBTX, arg ListShareContentParams) ([]Content, error) { + rows, err := db.Query(ctx, listShareContent, arg.UserID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Content + for rows.Next() { + var i Content + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Type, + &i.Title, + &i.Description, + &i.Url, + &i.Domain, + &i.S3Key, + &i.Summary, + &i.Content, + &i.Html, + &i.Metadata, + &i.IsFavorite, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateShareContent = `-- name: UpdateShareContent :one +UPDATE content_share cs +SET expires_at = $3 +FROM content c +WHERE cs.content_id = c.id + AND c.id = $1 + AND c.user_id = $2 +RETURNING cs.id, cs.user_id, cs.content_id, cs.expires_at, cs.created_at, cs.updated_at +` + +type UpdateShareContentParams struct { + ID uuid.UUID + UserID uuid.UUID + ExpiresAt pgtype.Timestamptz +} + +func (q *Queries) UpdateShareContent(ctx context.Context, db DBTX, arg UpdateShareContentParams) (ContentShare, error) { + row := db.QueryRow(ctx, updateShareContent, arg.ID, arg.UserID, arg.ExpiresAt) + var i ContentShare + err := row.Scan( + &i.ID, + &i.UserID, + &i.ContentID, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} From 5ec6e1d52c86df160ed54e1a777f322ed4adf72a Mon Sep 17 00:00:00 2001 From: Vaayne Date: Wed, 29 Jan 2025 17:44:41 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=F0=9F=94=80=20refactor:=20Reorganize=20b?= =?UTF-8?q?ookmark=20models=20and=20services?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename `ContentDTO` to `BookmarkContentDTO` for clarity. - Introduce new models: `BookmarkDTO`, `BookmarkWithContentDTO`, `BookmarkMetadata`, `BookmarkContentMetadata`, and `BookmarkShareDTO`. - Update `BookmarkService` methods to use new models and improve functionality. - Refactor `CreateBookmark` to accept a `BookmarkContentDTO` and return a `BookmarkDTO`. - Add `GetBookmarkWithContent`, `UpdateBookmark`, `DeleteBookmark`, and `DeleteBookmarksByUser` methods. - Modify `ListBookmarks` to return `BookmarkDTO` instead of `BookmarkContentDTO`. - Update `FetchContent` and `SummarierContent` to return `BookmarkContentDTO`. - Refactor `BookmarkShareService` methods to use new `BookmarkShareDTO` model. - Add `GetBookmarkShareContent`, `CreateBookmarkShare`, `UpdateSharedContent`, and `DeleteSharedContent` methods. - Adjust `WebSummaryHandler` in `bots` package to use new `BookmarkService` methods. - Update Swagger documentation to reflect changes in models and API endpoints. --- database/bindata.go | 16 +- .../000012_split_bookmark_content.down.sql | 1 + .../000012_split_bookmark_content.up.sql | 20 + database/queries/bookmark_content.sql | 49 +++ database/queries/bookmark_share.sql | 39 ++ database/queries/bookmark_tags.sql | 45 ++ database/queries/bookmarks.sql | 75 +--- database/queries/share_content.sql | 49 --- docs/swagger/docs.go | 207 ++++++--- docs/swagger/swagger.json | 207 ++++++--- docs/swagger/swagger.yaml | 145 +++++-- .../core/bookmarks/bookmark_content_model.go | 168 ++++++++ .../bookmarks/bookmark_content_service.go | 145 +++++++ internal/core/bookmarks/bookmark_model.go | 228 ++++++++++ internal/core/bookmarks/bookmark_service.go | 203 +++++++++ .../core/bookmarks/bookmark_share_model.go | 38 ++ .../core/bookmarks/bookmark_share_service.go | 79 ++++ .../core/bookmarks/bookmark_tag_service.go | 71 ++++ internal/core/bookmarks/dao.go | 22 - internal/core/bookmarks/dto.go | 276 ------------ internal/core/bookmarks/service.go | 395 +----------------- .../core/bookmarks/share_content_service.go | 100 ----- internal/core/bookmarks/utils.go | 81 ++++ internal/pkg/db/bookmark_content.sql.go | 219 ++++++++++ internal/pkg/db/bookmark_share.sql.go | 156 +++++++ internal/pkg/db/bookmark_tags.sql.go | 183 ++++++++ internal/pkg/db/bookmarks.sql.go | 379 +---------------- internal/pkg/db/models.go | 9 + internal/port/bots/handlers/websummary.go | 29 +- internal/port/httpserver/handler_bookmark.go | 114 ++--- .../port/httpserver/handler_share_content.go | 6 +- 31 files changed, 2221 insertions(+), 1533 deletions(-) create mode 100644 database/queries/bookmark_content.sql create mode 100644 database/queries/bookmark_share.sql create mode 100644 database/queries/bookmark_tags.sql delete mode 100644 database/queries/share_content.sql create mode 100644 internal/core/bookmarks/bookmark_content_model.go create mode 100644 internal/core/bookmarks/bookmark_content_service.go create mode 100644 internal/core/bookmarks/bookmark_model.go create mode 100644 internal/core/bookmarks/bookmark_service.go create mode 100644 internal/core/bookmarks/bookmark_share_model.go create mode 100644 internal/core/bookmarks/bookmark_share_service.go create mode 100644 internal/core/bookmarks/bookmark_tag_service.go delete mode 100644 internal/core/bookmarks/dto.go delete mode 100644 internal/core/bookmarks/share_content_service.go create mode 100644 internal/core/bookmarks/utils.go create mode 100644 internal/pkg/db/bookmark_content.sql.go create mode 100644 internal/pkg/db/bookmark_share.sql.go create mode 100644 internal/pkg/db/bookmark_tags.sql.go diff --git a/database/bindata.go b/database/bindata.go index e158d50..b40e17f 100644 --- a/database/bindata.go +++ b/database/bindata.go @@ -22,8 +22,8 @@ // 000010_share_content.up.sql (785B) // 000011_create_s3_resources_mapping_table.down.sql (264B) // 000011_create_s3_resources_mapping_table.up.sql (1.401kB) -// 000012_split_bookmark_content.down.sql (222B) -// 000012_split_bookmark_content.up.sql (4.043kB) +// 000012_split_bookmark_content.down.sql (259B) +// 000012_split_bookmark_content.up.sql (4.835kB) package migrations @@ -531,7 +531,7 @@ func _000011_create_s3_resources_mapping_tableUpSql() (*asset, error) { return a, nil } -var __000012_split_bookmark_contentDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x8c\x3f\x0a\x83\x30\x14\xc6\x77\x4f\xf1\x5d\xc0\x13\x38\xb5\x68\x41\x28\xb4\x54\x87\x6e\x21\x9a\x0f\x1b\xd4\xf7\xc2\x4b\xe8\xf9\x3b\x75\x16\xe7\xdf\x9f\xba\x46\x6b\x9a\x50\xfc\xb4\x31\x23\x0a\x8c\x5f\x5a\x26\xd4\x02\x0d\x45\xf1\xf1\x12\x36\x22\x30\x51\x02\x65\x8e\xcc\x55\xfb\x7a\x3c\x31\x5e\xae\xf7\x0e\xfd\x0d\xdd\xbb\x1f\xc6\x01\x93\xea\xba\x7b\x5b\xdd\xac\x52\x28\xc5\x15\xbf\x64\xb7\xfb\x94\xa2\x2c\xcd\x89\xe6\xc0\x3d\xe2\xff\x57\x53\xfd\x02\x00\x00\xff\xff\xe6\x1f\x88\x83\xde\x00\x00\x00") +var __000012_split_bookmark_contentDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\xcd\x31\x0a\x83\x40\x10\x85\xe1\xde\x53\xbc\x0b\x78\x02\xab\x04\x0d\x08\x81\x84\x68\x91\x4e\x46\xf7\xa1\xa2\xce\x2c\xb3\x4b\xce\x9f\x26\xbd\xa4\xff\xde\xfb\xcb\x12\xb5\x5b\x44\x96\x71\x67\xc2\xaa\x70\x7e\xe8\x89\x30\x0f\x74\x64\xc3\x22\x1a\x76\x22\x30\x52\x03\x75\x5a\x99\x8a\xfa\xf5\x78\xa2\xbf\x5c\xef\x0d\xda\x1b\x9a\x77\xdb\xf5\x1d\x46\xb3\xed\x10\xdf\x86\xc9\x34\x53\xf3\x90\x65\x4e\xc3\x21\x31\xae\x3a\x57\x7f\x6c\xce\x6c\x5a\xc4\x79\x82\x4e\x4f\x7e\xc1\xaa\xf8\x06\x00\x00\xff\xff\x2a\x0e\xfa\x94\x03\x01\x00\x00") func _000012_split_bookmark_contentDownSqlBytes() ([]byte, error) { return bindataRead( @@ -546,12 +546,12 @@ func _000012_split_bookmark_contentDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000012_split_bookmark_content.down.sql", size: 222, mode: os.FileMode(0644), modTime: time.Unix(1737972290, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x22, 0x38, 0x53, 0xc9, 0xcd, 0x90, 0x6b, 0x26, 0x6a, 0xf7, 0x16, 0xd6, 0x8a, 0xd2, 0xd, 0xc8, 0x9, 0xf6, 0x57, 0x74, 0xe0, 0x31, 0xdb, 0xa4, 0xac, 0x3a, 0xa9, 0xc4, 0xab, 0xd6, 0xfa, 0x8f}} + info := bindataFileInfo{name: "000012_split_bookmark_content.down.sql", size: 259, mode: os.FileMode(0644), modTime: time.Unix(1738137401, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x18, 0xd, 0x5e, 0x4f, 0x9, 0xc5, 0x5e, 0x30, 0xd6, 0x3a, 0xd1, 0x4d, 0xdb, 0x8b, 0xfb, 0x31, 0x19, 0xb5, 0x81, 0xb9, 0x82, 0x4f, 0x56, 0x20, 0x2b, 0x5f, 0x9d, 0x23, 0x26, 0xb1, 0xf0, 0xb}} return a, nil } -var __000012_split_bookmark_contentUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x56\xdf\x6f\xdb\x36\x10\x7e\xf7\x5f\x71\x6f\x91\x01\xb9\x29\x5a\xf4\x65\xc5\x1e\x14\x9b\x4e\xb4\x39\x52\x26\x4b\x6b\xba\x61\x10\x68\x93\x96\xb9\x48\xa4\x40\x52\x71\x83\x61\xff\xfb\x40\xea\x87\x95\x46\x71\xb4\x66\x0f\xeb\x23\xc9\xef\x3e\xf2\xee\xbe\xbb\xe3\x3c\x42\x5e\x8c\x20\xf6\x2e\x56\x08\x36\x42\xdc\x15\x58\xde\xa5\x5b\xc1\x35\xe5\xda\x99\x00\x00\x30\x02\x55\xc5\x08\xdc\x44\xfe\xb5\x17\x7d\x86\x9f\xd1\x67\x58\xa0\xa5\x97\xac\x62\xc8\x28\x4f\x25\xe6\x44\x14\xa9\xc1\x38\x53\xd7\x9a\xe8\x87\x92\xc2\xaf\x5e\x34\xbf\xf2\x22\xe7\xc3\xdb\x29\x04\x61\x0c\x41\xb2\x5a\xb9\x30\x9b\xd5\xa7\x62\xf7\xf4\xba\x76\xc3\x85\x92\xec\x5c\xa0\x65\xb5\x71\x81\x15\x38\xa3\x2e\x94\x82\x6c\xb1\xd2\x2e\xdc\x33\x42\x85\x0b\x54\x6f\xdf\x4c\xed\x65\x95\xcc\x21\x46\xb7\xf1\xe3\x4b\x92\x68\x65\xee\xd0\x7b\xda\xdd\x53\xa3\x15\x95\x69\xeb\x52\xeb\x46\x67\x75\xd8\x53\x6e\x21\xc0\x14\xf0\x2a\xcf\x5d\xd0\x7b\xa6\xa0\x79\xa1\xd9\x55\x7b\x2c\x29\x71\x6b\x28\xd3\x67\x0a\xb8\xd0\x0d\x96\x69\x38\xb0\x3c\x87\x0d\xcd\x05\xcf\x40\x0b\x7b\xbf\xe1\xab\xc3\xc2\x74\x4e\xed\x5b\xeb\x30\x11\xaa\xb6\x92\x95\x9a\x09\xde\xdf\x15\x05\x66\xcd\x86\x79\x53\xb3\x6e\x9c\x49\xa2\x95\x45\xa9\xf7\xe9\x1d\x7d\x38\xa2\xd6\xef\xc1\xac\x77\x42\x82\xd2\x42\x32\x9e\x81\xc4\x87\xee\xe1\x39\xbb\xa3\xfd\xa8\xf6\xa2\x58\xd3\x55\x45\x81\x65\x8f\xcf\xf3\x4d\x72\xa9\xc4\x9a\x92\xf6\xd4\x22\x5b\xc6\x0e\xd9\xc5\x86\x83\x89\x32\x11\x07\x6e\x9e\x51\x60\x6d\xf1\x7b\x5d\xe4\x47\xb0\x5d\xb5\x16\xe6\xb1\x07\xba\x81\x12\x67\xb4\x0e\x10\xce\x54\x5f\x37\xbf\xff\xd1\xa5\xe8\xec\xaf\xbf\xcf\x6a\xf5\x18\x8c\xb1\x34\xd1\x68\x99\x36\x0f\x40\xe8\x0e\x57\xb9\x06\x5a\x94\xfa\x01\xb0\x94\xb8\x7e\x6f\x41\x35\x26\x58\x63\xf8\x69\x1d\x06\x17\x8f\xf9\x6a\x87\x24\x35\x4e\xa6\x58\x43\xec\x5f\xa3\x75\xec\x5d\xdf\xc0\x27\x3f\xbe\xb2\x4b\xf8\x2d\x0c\x50\x27\xad\xce\x7c\x9e\x44\x11\x0a\xe2\xb4\xb3\xa8\xb9\xaa\x92\xfc\x67\x5c\x49\xe0\xff\x92\x20\xa7\x92\xb9\xdb\xaa\xd6\x08\x7e\xfa\x71\x32\x99\xcd\xc0\xe7\x84\x7e\xa1\x6a\xd2\x14\xb0\x1f\x2c\xd0\x2d\x30\xf2\x25\xfd\xba\xaa\x52\x5b\x6e\x61\xf0\xb4\xdc\xcc\xc1\xf4\xe3\x08\x06\x53\x61\x43\x04\x95\xcc\x47\xd9\x37\x0a\x1e\xa2\xa8\x8f\x46\xb1\xf4\xf2\x34\xc4\x74\x3c\x1e\xc5\xd6\xc9\x62\x80\x0b\x92\xb5\x1f\x5c\x42\xc6\xb8\xd3\xc1\xfe\x54\x82\x6f\xd2\x12\xeb\x7d\x2a\x4a\xd5\x24\xe1\xe2\xfa\xdd\x07\x60\x26\x13\x20\xf8\x13\x9a\x89\xd5\xbb\x2e\xd5\x0f\xe7\xe7\x44\x6c\xd5\x9b\x12\x4b\x4c\x28\xd9\xbc\xd9\x8a\xc2\xec\x54\x05\xe5\x1a\x9b\xf2\x3f\xb7\x24\x8c\x67\xe7\xb5\x1b\xa9\x5d\x8f\x70\x63\x53\xbc\xfb\x90\x2a\x8a\xe5\x76\x7f\xc2\x13\x83\x72\x18\x71\xeb\x0e\xe4\xf6\x1b\x8f\xdb\x16\xb7\xdb\x16\x93\xdb\x95\xcc\xb4\x96\xae\x73\x47\x1f\xd2\x1d\xa3\x39\x81\x1f\xe1\x8c\x91\xb3\x63\x80\xe3\xc8\xbf\xbc\x44\x51\xa3\xfb\x01\xe5\x1c\xeb\xe1\x02\x2d\xc3\x08\x41\x72\xb3\x30\x86\x43\x6f\x5d\x86\x11\x20\x6f\x7e\x05\x51\xf8\x09\xd0\x2d\x9a\x27\x31\x82\x65\x12\xcc\x63\x3f\x0c\xda\x2b\x8e\x8c\xe9\x56\xe4\x55\xc1\x1d\x93\x0b\x13\x6a\x4e\x0f\x1d\xa7\x02\x8d\x37\x39\x9d\x2c\xa2\xf0\xa6\x19\x6e\xfe\x12\xd0\xad\xbf\x8e\xd7\x47\xd0\xd1\x8d\x47\xe3\x4f\xc1\x37\x0f\xbe\x76\xba\x24\x89\xbf\x80\x08\x2d\x51\x84\x82\x39\x5a\xdb\x7d\xe5\x18\xe4\xd4\xb8\xbe\x40\x2b\x14\x23\x98\x7b\xeb\xb9\xb7\x40\x6e\xbf\xaf\x0e\x59\x3f\xd1\x3a\x23\xcd\x7d\x4c\xa5\x3b\x7c\x2f\x24\xd3\x14\x2e\xc2\x70\x85\xbc\xa0\x7b\xe2\xd2\x5b\xad\x51\x07\x33\x0a\x61\xf7\x2f\xa1\xca\x6a\x93\xb3\xed\x29\x90\xa4\x98\x30\x9e\xa5\xa5\x14\x99\xa4\x4a\x81\x1f\xc4\xc8\x48\xa0\xc5\xbe\x75\xbf\xd7\xb6\xdb\x34\xd7\x67\x8b\x4e\xa5\x36\xbd\xc3\x7d\x48\x39\x4d\xee\x5d\x18\xd7\x89\x7a\x99\x1b\xa6\xe9\xe5\xf6\x34\x4f\x9b\xda\x67\x69\x1a\xc0\x69\x96\x26\xf5\xcf\x92\xd4\xe7\xa7\x39\x86\x9a\xaa\x1a\xd5\x4d\x4f\xf7\x13\x35\xaa\x91\xa8\xd7\x76\x90\xae\xce\xec\x1f\xa3\xee\x20\xc3\x1f\x64\x0b\xf8\xe6\x2e\xc1\x71\xf1\xcc\xf7\xf8\xe9\x17\xb5\x93\xed\xbf\xec\x26\xff\xbb\xea\x7a\xfc\xa9\x69\x85\x65\x42\x31\x7d\xa9\xf0\x6c\xb8\x53\x1b\xb5\xfe\xdc\x30\xbb\x8e\x25\x78\x71\x1e\x59\x82\x51\xc3\xc8\x66\xf6\x15\x3a\x9a\xcd\xa0\xaf\xa4\x76\xb6\x59\x5a\x49\x73\x3b\xed\xd5\x9e\x95\x27\x84\x95\x16\xb8\x2c\x19\xcf\x6a\x7d\x75\x47\xad\x22\x06\x06\x83\x72\x4e\x48\x41\xe3\xec\x94\x6d\x1d\xc6\xef\x4b\x4a\x60\xc9\x7a\x15\xe7\xf4\xa2\xe4\x36\x1e\x8f\x94\x55\x13\xec\xb4\x09\xd3\xd7\x5a\xe8\x92\xd1\x90\x8e\x93\x5a\x4b\x3a\x5a\x72\xad\xc5\x2b\xa4\xf7\x4f\x00\x00\x00\xff\xff\x0f\x1e\xa2\xfa\xcb\x0f\x00\x00") +var __000012_split_bookmark_contentUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x57\x5d\x6f\xdb\x36\x14\x7d\xf7\xaf\xb8\x6f\x91\x01\xb9\x29\x5a\xf4\x65\xc5\x1e\x14\x9b\x6e\xb5\x39\x72\x26\x4b\x6b\xba\x61\x10\x68\x93\x96\xb9\x48\xa4\x40\x52\x75\x83\x61\xff\x7d\x20\xf5\x61\x39\x56\x1c\xb5\xc9\xc3\xfa\x28\xf2\xdc\xc3\xfb\x71\xee\x25\x35\x0d\x91\x17\x21\x88\xbc\xab\x05\x82\xb5\x10\x77\x39\x96\x77\xc9\x46\x70\x4d\xb9\x76\x46\x00\x00\x8c\x40\x59\x32\x02\x37\xa1\x7f\xed\x85\x9f\xe1\x57\xf4\x19\x66\x68\xee\xc5\x8b\x08\x52\xca\x13\x89\x39\x11\x79\x62\x30\xce\xd8\xb5\x26\xfa\xbe\xa0\xf0\xbb\x17\x4e\x3f\x7a\xa1\xf3\xee\xf5\x18\x82\x65\x04\x41\xbc\x58\xb8\x30\x99\x54\xbb\x62\x7b\x7a\x5c\xb3\xe0\x42\x41\xb6\x2e\xd0\xa2\x5c\xbb\xc0\x72\x9c\x52\x17\x0a\x41\x36\x58\x69\x17\xbe\x30\x42\x85\x0b\x54\x6f\x5e\x8d\xed\x61\xa5\xcc\x20\x42\xb7\xd1\xf1\x21\x71\xb8\x30\x67\xe8\x1d\x6d\xcf\xa9\xd0\x8a\xca\xa4\x09\xa9\x09\xa3\xb5\xda\xef\x28\xb7\x10\x60\x0a\x78\x99\x65\x2e\xe8\x1d\x53\x50\x7b\x68\x56\xd5\x0e\x4b\x4a\xdc\x0a\xca\xf4\x85\x02\x2e\x74\x8d\x65\x1a\xf6\x2c\xcb\x60\x4d\x33\xc1\x53\xd0\xc2\x9e\x6f\xf8\xaa\xb4\x30\x9d\x51\xeb\x6b\x95\x26\x42\xd5\x46\xb2\x42\x33\xc1\xbb\xab\x22\xc7\xac\x5e\x30\x3e\xd5\xdf\x75\x30\x71\xb8\xb0\x28\xf5\x36\xb9\xa3\xf7\x07\xd4\xea\x2d\x98\xef\xad\x90\xa0\xb4\x90\x8c\xa7\x20\xf1\xbe\x75\x3c\x63\x77\xb4\x9b\xd5\x4e\x16\x2b\xba\x32\xcf\xb1\xec\xf0\x79\xbe\x29\x2e\x95\x58\x53\xd2\xec\x5a\x64\xc3\xd8\x22\xdb\xdc\x70\x30\x59\x26\x62\xcf\x8d\x1b\x39\xd6\x16\xbf\xd3\x79\x76\x00\xdb\xaf\xc6\xc2\x38\xbb\xa7\x6b\x28\x70\x4a\xab\x04\xe1\x54\x75\x75\xf3\xe7\x5f\x6d\x89\x2e\xfe\xf9\xf7\xa2\x52\x8f\xc1\x18\x4b\x93\x8d\x86\x69\x7d\x0f\x84\x6e\x71\x99\x69\xa0\x79\xa1\xef\x01\x4b\x89\x2b\x7f\x73\xaa\x31\xc1\x1a\xc3\x2f\xab\x65\x70\x75\xcc\x57\x05\x24\xa9\x09\x32\xc1\x1a\x22\xff\x1a\xad\x22\xef\xfa\x06\x3e\xf9\xd1\x47\xfb\x09\x7f\x2c\x03\xd4\x4a\xab\x35\x9f\xc6\x61\x88\x82\x28\x69\x2d\x2a\xae\xb2\x20\x2f\xc6\x15\x07\xfe\x6f\x31\x72\x4a\x99\xb9\x8d\x6a\x8d\xe0\xc7\xef\x47\xa3\xc9\x04\x7c\x4e\xe8\x57\xaa\x46\x75\x03\xfb\xc1\x0c\xdd\x02\x23\x5f\x93\x87\x5d\x95\xd8\x76\x5b\x06\xa7\xed\x66\x36\xc6\xef\x07\x30\x98\x0e\xeb\x23\x28\x65\x36\xc8\xbe\x56\x70\x1f\x45\xb5\x35\x88\xa5\x53\xa7\x3e\xa6\xc3\xf6\x20\xb6\x56\x16\x3d\x5c\x10\xaf\xfc\xe0\x03\xa4\x8c\x3b\x2d\xec\x6f\x25\xf8\x3a\x29\xb0\xde\x25\xa2\x50\x75\x11\xae\xae\xdf\xbc\x03\x66\x2a\x01\x82\x9f\xd0\x8c\xac\xde\x75\xa1\x7e\xba\xbc\x24\x62\xa3\x5e\x15\x58\x62\x42\xc9\xfa\xd5\x46\xe4\x66\xa5\xcc\x29\xd7\xd8\xb4\xff\xa5\x25\x61\x3c\xbd\xac\xc2\x48\xec\xf7\x80\x30\xd6\xf9\x9b\x77\x89\xa2\x58\x6e\x76\x67\x22\x31\x28\x87\x11\xb7\x9a\x40\x6e\x77\xf0\xb8\x4d\x73\xbb\x4d\x33\xb9\x6d\xcb\x8c\x2b\xe9\x3a\x77\xf4\x3e\xd9\x32\x9a\x11\xf8\x19\x2e\x18\xb9\x38\x24\x38\x0a\xfd\x0f\x1f\x50\x58\xeb\xbe\x47\x39\x87\x7e\xb8\x42\xf3\x65\x88\x20\xbe\x99\x19\xc3\x3e\x5f\xe7\xcb\x10\x90\x37\xfd\x08\xe1\xf2\x13\xa0\x5b\x34\x8d\x23\x04\xf3\x38\x98\x46\xfe\x32\x68\x8e\x38\x30\x26\x1b\x91\x95\x39\x77\x4c\x2d\x4c\xaa\x39\xdd\xb7\x9c\x0a\x34\x5e\x67\x74\x34\x0b\x97\x37\xf5\xe5\xe6\xcf\x01\xdd\xfa\xab\x68\x75\x00\x1d\xc2\x38\xba\xfe\x14\x7c\xf7\xc5\xd7\xdc\x2e\x71\xec\xcf\x20\x44\x73\x14\xa2\x60\x8a\x56\x76\x5d\x39\x06\x39\x36\xa1\xcf\xd0\x02\x45\x08\xa6\xde\x6a\xea\xcd\x90\xdb\x9d\xab\x7d\xd6\x27\x5a\x67\xa4\x3e\x8f\xa9\x64\x8b\xbf\x08\xc9\x34\x85\xab\xe5\x72\x81\xbc\xa0\x75\x71\xee\x2d\x56\xa8\x85\x19\x85\xb0\x2f\x4f\xa1\x8a\x72\x9d\xb1\xcd\x39\x90\xa4\x98\x30\x9e\x26\x85\x14\xa9\xa4\x4a\x81\x1f\x44\xc8\x48\xa0\xc1\xbe\x76\x7f\xd4\xb1\x5b\x0f\xd7\x47\x9b\x4e\x25\xb6\xbc\xfd\x73\x48\x39\x75\xed\x5d\x18\x36\x89\x3a\x95\xeb\xa7\xe9\xd4\xf6\x3c\x4f\x53\xda\x47\x69\x6a\xc0\x79\x96\xba\xf4\x8f\x92\x54\xfb\xe7\x39\xfa\x86\xaa\x1a\x34\x4d\xcf\xcf\x13\x35\x68\x90\xa8\xe7\x4e\x90\xb6\xcf\xec\x1b\xa3\x9a\x20\xfd\x0f\x64\x0b\xf8\xee\x29\xc1\x71\xfe\xc8\xf3\xf8\xf4\x89\xda\xca\xf6\x1b\xa7\xc9\xff\xae\xbb\x8e\x1f\x35\x8d\xb0\x4c\x2a\xc6\x4f\x35\x9e\x4d\x77\x62\xb3\xd6\xbd\x37\xcc\xaa\x63\x09\x9e\xbc\x8f\x2c\xc1\xa0\xcb\xc8\x56\xf6\x19\x3a\x9a\x4c\xa0\xab\xa4\xe6\x6e\xb3\xb4\x92\x66\xf6\xb6\x57\x3b\x56\x9c\x11\x56\x92\xe3\xa2\x60\x3c\xad\xf4\xd5\x6e\x35\x8a\xe8\xb9\x18\x94\x73\x46\x0a\x1a\xa7\xe7\x6c\xab\x34\xfe\x58\x52\x02\x4b\xd6\xe9\x38\xa7\x93\x25\xb7\x8e\x78\xa0\xac\xea\x64\x27\x75\x9a\x1e\x6a\xa1\x2d\x46\x4d\x3a\x4c\x6a\x0d\xe9\x60\xc9\x35\x16\xcf\x1a\x61\xfd\x92\xb2\x3f\xac\xcf\x7f\xd2\x0c\x99\x46\xee\xcb\x28\x96\x7e\x2d\x98\xa4\xea\xac\x4a\x8e\x7e\xdd\x07\xeb\xf4\x05\xe4\x39\x7c\xc0\x19\x29\x76\x72\x31\x1e\x9d\xa8\xf1\x58\x8e\xb6\x50\x49\x93\xef\xae\x42\xec\x4e\x33\x34\xcf\xfe\xde\x54\x1c\x9d\xb7\xe4\x29\x4d\xd7\xa5\xf7\xa3\xfa\x89\x5c\x8b\xf9\xf0\x48\x7e\x28\xeb\xda\xb9\x43\x82\x4e\x88\x9f\xec\x8c\x87\x14\x36\x5f\x8f\xb7\x85\x85\x5b\x4c\xb7\x27\xec\xc2\x37\xf5\xc5\x7f\x01\x00\x00\xff\xff\x2f\xc2\x5d\xee\xe3\x12\x00\x00") func _000012_split_bookmark_contentUpSqlBytes() ([]byte, error) { return bindataRead( @@ -566,8 +566,8 @@ func _000012_split_bookmark_contentUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 4043, mode: os.FileMode(0644), modTime: time.Unix(1737989741, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd0, 0x9, 0xf9, 0x95, 0xd7, 0xe4, 0x60, 0x97, 0x8b, 0xbb, 0xcd, 0xc7, 0x8d, 0xf5, 0xe1, 0xbc, 0x66, 0xfc, 0x31, 0x35, 0x1d, 0x8d, 0xce, 0x60, 0xc5, 0x6c, 0x75, 0xc3, 0xdb, 0xa4, 0x93, 0xb1}} + info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 4835, mode: os.FileMode(0644), modTime: time.Unix(1738137330, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x24, 0xf8, 0x64, 0xc3, 0x2d, 0x98, 0x56, 0xc4, 0x5a, 0x95, 0x7c, 0x3e, 0x87, 0xe6, 0x3, 0xdc, 0x20, 0x8c, 0xfc, 0x6b, 0x6a, 0x27, 0xec, 0xdc, 0x57, 0x7f, 0xd8, 0x64, 0x5b, 0x1e, 0xbf, 0x50}} return a, nil } diff --git a/database/migrations/000012_split_bookmark_content.down.sql b/database/migrations/000012_split_bookmark_content.down.sql index 4829cd7..7b30d9a 100644 --- a/database/migrations/000012_split_bookmark_content.down.sql +++ b/database/migrations/000012_split_bookmark_content.down.sql @@ -1,5 +1,6 @@ -- Drop tables in reverse order to handle dependencies DROP TABLE IF EXISTS bookmark_content_tags_mapping; DROP TABLE IF EXISTS bookmark_content_tags; +DROP TABLE IF EXISTS bookmark_share; DROP TABLE IF EXISTS bookmarks; DROP TABLE IF EXISTS bookmark_content; diff --git a/database/migrations/000012_split_bookmark_content.up.sql b/database/migrations/000012_split_bookmark_content.up.sql index 5cfe4f8..9cb4794 100644 --- a/database/migrations/000012_split_bookmark_content.up.sql +++ b/database/migrations/000012_split_bookmark_content.up.sql @@ -77,3 +77,23 @@ CREATE TABLE bookmark_tags_mapping( CREATE INDEX idx_bookmark_tags_mapping_tag_id ON bookmark_tags_mapping(tag_id); CREATE TRIGGER update_bookmark_tags_mapping_updated_at BEFORE UPDATE ON bookmark_tags_mapping FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + + +CREATE TABLE bookmark_share ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(uuid), + bookmark_id uuid REFERENCES bookmarks(id) ON DELETE CASCADE, + expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(userid, bookmark_id) +); + +CREATE INDEX idx_bookmark_share_user_id ON bookmark_share(user_id); +CREATE INDEX idx_bookmark_share_content_id ON bookmark_share(bookmark_id); + +DROP TRIGGER IF EXISTS update_bookmark_share_updated_at ON bookmark_share; +CREATE TRIGGER update_bookmark_share_updated_at + BEFORE UPDATE ON bookmark_share + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); diff --git a/database/queries/bookmark_content.sql b/database/queries/bookmark_content.sql new file mode 100644 index 0000000..d71591e --- /dev/null +++ b/database/queries/bookmark_content.sql @@ -0,0 +1,49 @@ +-- name: IsBookmarkContentExistByURL :one +SELECT EXISTS ( + SELECT 1 + FROM bookmark_content + WHERE url = $1 +); + +-- name: CreateBookmarkContent :one +INSERT INTO bookmark_content ( + type, + url, + user_id, + title, + description, + domain, + s3_key, + summary, + content, + html, + tags, + metadata +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 +) RETURNING *; + + +-- name: GetBookmarkContentByID :one +SELECT * +FROM bookmark_content +WHERE id = $1; + +-- name: GetBookmarkContentByURL :one +-- First try to get user specific content, then the shared content +SELECT * +FROM bookmark_content +WHERE url = $1 AND (user_id = $2 OR user_id IS NULL) +LIMIT 1; + +-- name: UpdateBookmarkContent :one +UPDATE bookmark_content +SET title = COALESCE(sqlc.narg('title'), title), + description = COALESCE(sqlc.narg('description'), description), + s3_key = COALESCE(sqlc.narg('s3_key'), s3_key), + summary = COALESCE(sqlc.narg('summary'), summary), + content = COALESCE(sqlc.narg('content'), content), + html = COALESCE(sqlc.narg('html'), html), + metadata = COALESCE(sqlc.narg('metadata'), metadata) +WHERE id = $1 +RETURNING *; diff --git a/database/queries/bookmark_share.sql b/database/queries/bookmark_share.sql new file mode 100644 index 0000000..5f232dc --- /dev/null +++ b/database/queries/bookmark_share.sql @@ -0,0 +1,39 @@ +-- name: CreateBookmarkShare :one +INSERT INTO bookmark_share (user_id, bookmark_id, expires_at) +VALUES ($1, $2, $3) +RETURNING *; + +-- name: GetBookmarkShareContent :one +SELECT bc.* +FROM bookmark_share AS bs + JOIN bookmarks AS b ON bs.bookmark_id = b.id + JOIN bookmark_content AS bc ON b.content_id = bc.id +WHERE bs.id = $1 + AND (bs.expires_at is NULL OR bs.expires_at > now()); + +-- name: GetBookmarkShare :one +SELECT * +FROM bookmark_share +WHERE bookmark_id = $1 + AND user_id = $2; + +-- name: UpdateBookmarkShareByBookmarkId :one +UPDATE bookmark_share bs +SET expires_at = $3 +FROM bookmarks b +WHERE bs.bookmark_id = b.id + AND b.id = $1 + AND b.user_id = $2 +RETURNING bs.*; + +-- name: DeleteBookmarkShareByBookmarkId :exec +DELETE FROM bookmark_share bs +USING bookmarks b +WHERE bs.bookmark_id = b.id + AND b.id = $1 + AND b.user_id = $2; + +-- name: DeleteExpiredBookmarkShare :exec +DELETE +FROM bookmark_share +WHERE expires_at < now(); diff --git a/database/queries/bookmark_tags.sql b/database/queries/bookmark_tags.sql new file mode 100644 index 0000000..328847d --- /dev/null +++ b/database/queries/bookmark_tags.sql @@ -0,0 +1,45 @@ + +-- name: CreateBookmarkTag :one +INSERT INTO bookmark_tags (name, user_id) +VALUES ($1, $2) +RETURNING *; + +-- name: DeleteBookmarkTag :exec +DELETE FROM bookmark_tags +WHERE id = $1 + AND user_id = $2; + +-- name: LinkBookmarkWithTags :exec +INSERT INTO bookmark_tags_mapping (bookmark_id, tag_id) +SELECT $1, bt.id +FROM bookmark_tags bt +WHERE bt.name = ANY ($2::text[]) + AND bt.user_id = $3; + +-- name: UnLinkBookmarkWithTags :exec +DELETE FROM bookmark_tags_mapping +WHERE bookmark_id = $1 + AND tag_id IN (SELECT id + FROM bookmark_tags + WHERE name = ANY ($2::text[]) + AND user_id = $3); + +-- name: ListExistingBookmarkTagsByTags :many +SELECT name +FROM bookmark_tags +WHERE name = ANY ($1::text[]) + AND user_id = $2; + + +-- name: ListBookmarkTagsByUser :many +SELECT name, count(*) as cnt +FROM bookmark_tags +WHERE user_id = $1 +GROUP BY name +ORDER BY cnt DESC; + +-- name: ListBookmarkTagsByBookmarkId :many +SELECT bt.name +FROM bookmark_tags bt + JOIN bookmark_tags_mapping btm ON bt.id = btm.tag_id +WHERE btm.bookmark_id = $1; diff --git a/database/queries/bookmarks.sql b/database/queries/bookmarks.sql index c387cbb..9522a4c 100644 --- a/database/queries/bookmarks.sql +++ b/database/queries/bookmarks.sql @@ -11,7 +11,6 @@ WITH total AS ( AND (sqlc.narg('tags')::text[] IS NULL OR bct.name = ANY(sqlc.narg('tags')::text[])) ) SELECT b.*, - bc.*, t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -51,7 +50,6 @@ WITH total AS ( ) ) SELECT b.*, - bc.*, t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -78,7 +76,7 @@ GROUP BY b.id, bc.id, t.total_count ORDER BY b.created_at DESC LIMIT $2 OFFSET $3; --- name: GetBookmark :one +-- name: GetBookmarkWithContent :one SELECT b.*, bc.*, COALESCE( @@ -103,24 +101,6 @@ SELECT EXISTS ( AND b.user_id = $2 ); --- name: IsBookmarkContentExistWithURL :one -SELECT EXISTS ( - SELECT 1 - FROM bookmark_content bc - WHERE bc.url = $1 -); - - --- name: CreateBookmarkContent :one -INSERT INTO bookmark_content ( - type, title, description, user_id, url, domain, s3_key, - summary, content, html, metadata -) -VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 -) -RETURNING *; - -- name: CreateBookmark :one INSERT INTO bookmarks ( user_id, content_id, is_favorite, is_archive, @@ -142,18 +122,6 @@ WHERE id = $1 AND user_id = $2 RETURNING *; --- name: UpdateBookmarkContent :one -UPDATE bookmark_content -SET title = COALESCE(sqlc.narg('title'), title), - description = COALESCE(sqlc.narg('description'), description), - domain = COALESCE(sqlc.narg('domain'), domain), - s3_key = COALESCE(sqlc.narg('s3_key'), s3_key), - summary = COALESCE(sqlc.narg('summary'), summary), - content = COALESCE(sqlc.narg('content'), content), - html = COALESCE(sqlc.narg('html'), html), - metadata = COALESCE(sqlc.narg('metadata'), metadata) -WHERE id = $1 -RETURNING *; -- name: DeleteBookmark :exec DELETE FROM bookmarks @@ -170,16 +138,6 @@ SET updated_at = CURRENT_TIMESTAMP WHERE user_id = $1; --- name: ListBookmarkTagsByUser :many -SELECT name FROM bookmark_tags -WHERE user_id = $1; - --- name: ListBookmarkTagsByBookmarkId :many -SELECT bt.name -FROM bookmark_tags bt - JOIN bookmark_tags_mapping btm ON bt.id = btm.tag_id -WHERE btm.bookmark_id = $1; - -- name: ListBookmarkDomains :many SELECT bc.domain, count(*) as cnt FROM bookmarks b @@ -188,34 +146,3 @@ WHERE b.user_id = $1 AND bc.domain IS NOT NULL GROUP BY bc.domain ORDER BY cnt DESC, domain ASC; - --- name: CreateBookmarkTag :one -INSERT INTO bookmark_tags (name, user_id) -VALUES ($1, $2) -RETURNING *; - --- name: DeleteBookmarkTag :exec -DELETE FROM bookmark_tags -WHERE id = $1 - AND user_id = $2; - --- name: LinkBookmarkWithTags :exec -INSERT INTO bookmark_tags_mapping (bookmark_id, tag_id) -SELECT $1, bt.id -FROM bookmark_tags bt -WHERE bt.name = ANY ($2::text[]) - AND bt.user_id = $3; - --- name: UnLinkBookmarkWithTags :exec -DELETE FROM bookmark_tags_mapping -WHERE bookmark_id = $1 - AND tag_id IN (SELECT id - FROM bookmark_tags - WHERE name = ANY ($2::text[]) - AND user_id = $3); - --- name: ListExistingBookmarkTagsByTags :many -SELECT name -FROM bookmark_tags -WHERE name = ANY ($1::text[]) - AND user_id = $2; diff --git a/database/queries/share_content.sql b/database/queries/share_content.sql deleted file mode 100644 index 86d2404..0000000 --- a/database/queries/share_content.sql +++ /dev/null @@ -1,49 +0,0 @@ --- name: CreateShareContent :one -INSERT INTO content_share (user_id, content_id, expires_at) -VALUES ($1, $2, $3) -RETURNING *; - --- name: GetSharedContent :one --- get the shared content from content table -SELECT c.* -FROM content_share AS cs - JOIN content AS c ON cs.content_id = c.id -WHERE cs.id = $1 - AND (cs.expires_at is NULL OR cs.expires_at > now()); - --- name: GetShareContent :one --- info about the shared content -SELECT * -FROM content_share -WHERE content_id = $1 - AND user_id = $2; - --- name: ListShareContent :many -SELECT c.* -FROM content_share AS cs - JOIN content AS c ON cs.content_id = c.id -WHERE cs.user_id = $1 - AND cs.expires_at is NULL OR cs.expires_at > now() -ORDER BY cs.created_at DESC -LIMIT $2 OFFSET $3; - --- name: UpdateShareContent :one -UPDATE content_share cs -SET expires_at = $3 -FROM content c -WHERE cs.content_id = c.id - AND c.id = $1 - AND c.user_id = $2 -RETURNING cs.*; - --- name: DeleteShareContent :exec -DELETE FROM content_share cs -USING content c -WHERE cs.content_id = c.id - AND c.id = $1 - AND c.user_id = $2; - --- name: DeleteExpiredShareContent :exec -DELETE -FROM content_share -WHERE expires_at < now(); diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index ad39a4f..f43e1bf 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -2760,7 +2760,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } } } @@ -3102,7 +3102,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkWithContentDTO" } } } @@ -3225,7 +3225,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } } } @@ -3446,7 +3446,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } } } @@ -3562,7 +3562,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkContentDTO" } } } @@ -3667,7 +3667,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkShareDTO" } } } @@ -3772,7 +3772,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkShareDTO" } } } @@ -5001,7 +5001,7 @@ const docTemplate = `{ } } }, - "bookmarks.ContentDTO": { + "bookmarks.BookmarkContentDTO": { "type": "object", "properties": { "content": { @@ -5022,11 +5022,8 @@ const docTemplate = `{ "id": { "type": "string" }, - "is_favorite": { - "type": "boolean" - }, "metadata": { - "$ref": "#/definitions/bookmarks.Metadata" + "$ref": "#/definitions/bookmarks.BookmarkContentMetadata" }, "s3_key": { "type": "string" @@ -5057,7 +5054,36 @@ const docTemplate = `{ } } }, - "bookmarks.ContentShareDTO": { + "bookmarks.BookmarkContentMetadata": { + "type": "object", + "properties": { + "author": { + "type": "string" + }, + "cover": { + "type": "string" + }, + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "favicon": { + "type": "string" + }, + "image": { + "type": "string" + }, + "published_at": { + "type": "string" + }, + "site_name": { + "type": "string" + } + } + }, + "bookmarks.BookmarkDTO": { "type": "object", "properties": { "content_id": { @@ -5066,6 +5092,67 @@ const docTemplate = `{ "created_at": { "type": "string" }, + "id": { + "type": "string" + }, + "is_archive": { + "type": "boolean" + }, + "is_favorite": { + "type": "boolean" + }, + "is_public": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/definitions/bookmarks.BookmarkMetadata" + }, + "reading_progress": { + "type": "integer" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "bookmarks.BookmarkMetadata": { + "type": "object", + "properties": { + "highlights": { + "type": "array", + "items": { + "$ref": "#/definitions/bookmarks.Highlight" + } + }, + "last_read_at": { + "type": "string" + }, + "reading_progress": { + "type": "integer" + }, + "share": { + "$ref": "#/definitions/bookmarks.BookmarkShareDTO" + } + } + }, + "bookmarks.BookmarkShareDTO": { + "type": "object", + "properties": { + "bookmark_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, "expires_at": { "type": "string" }, @@ -5080,6 +5167,50 @@ const docTemplate = `{ } } }, + "bookmarks.BookmarkWithContentDTO": { + "type": "object", + "properties": { + "content": { + "$ref": "#/definitions/bookmarks.BookmarkContentDTO" + }, + "content_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_archive": { + "type": "boolean" + }, + "is_favorite": { + "type": "boolean" + }, + "is_public": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/definitions/bookmarks.BookmarkMetadata" + }, + "reading_progress": { + "type": "integer" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "bookmarks.ContentType": { "type": "string", "enum": [ @@ -5134,50 +5265,6 @@ const docTemplate = `{ } } }, - "bookmarks.Metadata": { - "type": "object", - "properties": { - "author": { - "type": "string" - }, - "cover": { - "type": "string" - }, - "description": { - "type": "string" - }, - "domain": { - "type": "string" - }, - "favicon": { - "type": "string" - }, - "highlights": { - "type": "array", - "items": { - "$ref": "#/definitions/bookmarks.Highlight" - } - }, - "image": { - "type": "string" - }, - "published_at": { - "type": "string" - }, - "share": { - "$ref": "#/definitions/bookmarks.ContentShareDTO" - }, - "site_name": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, "bookmarks.TagDTO": { "type": "object", "properties": { @@ -5288,7 +5375,7 @@ const docTemplate = `{ "type": "string" }, "metadata": { - "$ref": "#/definitions/bookmarks.Metadata" + "$ref": "#/definitions/bookmarks.BookmarkContentMetadata" }, "tags": { "type": "array", @@ -5364,7 +5451,7 @@ const docTemplate = `{ "bookmarks": { "type": "array", "items": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } }, "limit": { @@ -5493,7 +5580,7 @@ const docTemplate = `{ "type": "string" }, "metadata": { - "$ref": "#/definitions/bookmarks.Metadata" + "$ref": "#/definitions/bookmarks.BookmarkContentMetadata" }, "summary": { "type": "string" diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 72d6ee2..b192ab8 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -2754,7 +2754,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } } } @@ -3096,7 +3096,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkWithContentDTO" } } } @@ -3219,7 +3219,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } } } @@ -3440,7 +3440,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } } } @@ -3556,7 +3556,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkContentDTO" } } } @@ -3661,7 +3661,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkShareDTO" } } } @@ -3766,7 +3766,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkShareDTO" } } } @@ -4995,7 +4995,7 @@ } } }, - "bookmarks.ContentDTO": { + "bookmarks.BookmarkContentDTO": { "type": "object", "properties": { "content": { @@ -5016,11 +5016,8 @@ "id": { "type": "string" }, - "is_favorite": { - "type": "boolean" - }, "metadata": { - "$ref": "#/definitions/bookmarks.Metadata" + "$ref": "#/definitions/bookmarks.BookmarkContentMetadata" }, "s3_key": { "type": "string" @@ -5051,7 +5048,36 @@ } } }, - "bookmarks.ContentShareDTO": { + "bookmarks.BookmarkContentMetadata": { + "type": "object", + "properties": { + "author": { + "type": "string" + }, + "cover": { + "type": "string" + }, + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "favicon": { + "type": "string" + }, + "image": { + "type": "string" + }, + "published_at": { + "type": "string" + }, + "site_name": { + "type": "string" + } + } + }, + "bookmarks.BookmarkDTO": { "type": "object", "properties": { "content_id": { @@ -5060,6 +5086,67 @@ "created_at": { "type": "string" }, + "id": { + "type": "string" + }, + "is_archive": { + "type": "boolean" + }, + "is_favorite": { + "type": "boolean" + }, + "is_public": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/definitions/bookmarks.BookmarkMetadata" + }, + "reading_progress": { + "type": "integer" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "bookmarks.BookmarkMetadata": { + "type": "object", + "properties": { + "highlights": { + "type": "array", + "items": { + "$ref": "#/definitions/bookmarks.Highlight" + } + }, + "last_read_at": { + "type": "string" + }, + "reading_progress": { + "type": "integer" + }, + "share": { + "$ref": "#/definitions/bookmarks.BookmarkShareDTO" + } + } + }, + "bookmarks.BookmarkShareDTO": { + "type": "object", + "properties": { + "bookmark_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, "expires_at": { "type": "string" }, @@ -5074,6 +5161,50 @@ } } }, + "bookmarks.BookmarkWithContentDTO": { + "type": "object", + "properties": { + "content": { + "$ref": "#/definitions/bookmarks.BookmarkContentDTO" + }, + "content_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_archive": { + "type": "boolean" + }, + "is_favorite": { + "type": "boolean" + }, + "is_public": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/definitions/bookmarks.BookmarkMetadata" + }, + "reading_progress": { + "type": "integer" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "bookmarks.ContentType": { "type": "string", "enum": [ @@ -5128,50 +5259,6 @@ } } }, - "bookmarks.Metadata": { - "type": "object", - "properties": { - "author": { - "type": "string" - }, - "cover": { - "type": "string" - }, - "description": { - "type": "string" - }, - "domain": { - "type": "string" - }, - "favicon": { - "type": "string" - }, - "highlights": { - "type": "array", - "items": { - "$ref": "#/definitions/bookmarks.Highlight" - } - }, - "image": { - "type": "string" - }, - "published_at": { - "type": "string" - }, - "share": { - "$ref": "#/definitions/bookmarks.ContentShareDTO" - }, - "site_name": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, "bookmarks.TagDTO": { "type": "object", "properties": { @@ -5282,7 +5369,7 @@ "type": "string" }, "metadata": { - "$ref": "#/definitions/bookmarks.Metadata" + "$ref": "#/definitions/bookmarks.BookmarkContentMetadata" }, "tags": { "type": "array", @@ -5358,7 +5445,7 @@ "bookmarks": { "type": "array", "items": { - "$ref": "#/definitions/bookmarks.ContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } }, "limit": { @@ -5487,7 +5574,7 @@ "type": "string" }, "metadata": { - "$ref": "#/definitions/bookmarks.Metadata" + "$ref": "#/definitions/bookmarks.BookmarkContentMetadata" }, "summary": { "type": "string" diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index c49cdda..fc45ac8 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -197,7 +197,7 @@ definitions: summary_options: $ref: '#/definitions/auth.SummaryConfig' type: object - bookmarks.ContentDTO: + bookmarks.BookmarkContentDTO: properties: content: type: string @@ -211,10 +211,8 @@ definitions: type: string id: type: string - is_favorite: - type: boolean metadata: - $ref: '#/definitions/bookmarks.Metadata' + $ref: '#/definitions/bookmarks.BookmarkContentMetadata' s3_key: type: string summary: @@ -234,12 +232,71 @@ definitions: user_id: type: string type: object - bookmarks.ContentShareDTO: + bookmarks.BookmarkContentMetadata: + properties: + author: + type: string + cover: + type: string + description: + type: string + domain: + type: string + favicon: + type: string + image: + type: string + published_at: + type: string + site_name: + type: string + type: object + bookmarks.BookmarkDTO: properties: content_id: type: string created_at: type: string + id: + type: string + is_archive: + type: boolean + is_favorite: + type: boolean + is_public: + type: boolean + metadata: + $ref: '#/definitions/bookmarks.BookmarkMetadata' + reading_progress: + type: integer + tags: + items: + type: string + type: array + updated_at: + type: string + user_id: + type: string + type: object + bookmarks.BookmarkMetadata: + properties: + highlights: + items: + $ref: '#/definitions/bookmarks.Highlight' + type: array + last_read_at: + type: string + reading_progress: + type: integer + share: + $ref: '#/definitions/bookmarks.BookmarkShareDTO' + type: object + bookmarks.BookmarkShareDTO: + properties: + bookmark_id: + type: string + created_at: + type: string expires_at: type: string id: @@ -249,6 +306,35 @@ definitions: user_id: type: string type: object + bookmarks.BookmarkWithContentDTO: + properties: + content: + $ref: '#/definitions/bookmarks.BookmarkContentDTO' + content_id: + type: string + created_at: + type: string + id: + type: string + is_archive: + type: boolean + is_favorite: + type: boolean + is_public: + type: boolean + metadata: + $ref: '#/definitions/bookmarks.BookmarkMetadata' + reading_progress: + type: integer + tags: + items: + type: string + type: array + updated_at: + type: string + user_id: + type: string + type: object bookmarks.ContentType: enum: - bookmark @@ -289,35 +375,6 @@ definitions: text: type: string type: object - bookmarks.Metadata: - properties: - author: - type: string - cover: - type: string - description: - type: string - domain: - type: string - favicon: - type: string - highlights: - items: - $ref: '#/definitions/bookmarks.Highlight' - type: array - image: - type: string - published_at: - type: string - share: - $ref: '#/definitions/bookmarks.ContentShareDTO' - site_name: - type: string - tags: - items: - type: string - type: array - type: object bookmarks.TagDTO: properties: count: @@ -393,7 +450,7 @@ definitions: html: type: string metadata: - $ref: '#/definitions/bookmarks.Metadata' + $ref: '#/definitions/bookmarks.BookmarkContentMetadata' tags: items: type: string @@ -445,7 +502,7 @@ definitions: properties: bookmarks: items: - $ref: '#/definitions/bookmarks.ContentDTO' + $ref: '#/definitions/bookmarks.BookmarkDTO' type: array limit: type: integer @@ -528,7 +585,7 @@ definitions: html: type: string metadata: - $ref: '#/definitions/bookmarks.Metadata' + $ref: '#/definitions/bookmarks.BookmarkContentMetadata' summary: type: string required: @@ -2194,7 +2251,7 @@ paths: - $ref: '#/definitions/httpserver.JSONResult' - properties: data: - $ref: '#/definitions/bookmarks.ContentDTO' + $ref: '#/definitions/bookmarks.BookmarkDTO' type: object "400": description: Bad Request @@ -2299,7 +2356,7 @@ paths: - $ref: '#/definitions/httpserver.JSONResult' - properties: data: - $ref: '#/definitions/bookmarks.ContentDTO' + $ref: '#/definitions/bookmarks.BookmarkWithContentDTO' type: object "400": description: Bad Request @@ -2366,7 +2423,7 @@ paths: - $ref: '#/definitions/httpserver.JSONResult' - properties: data: - $ref: '#/definitions/bookmarks.ContentDTO' + $ref: '#/definitions/bookmarks.BookmarkDTO' type: object "400": description: Bad Request @@ -2434,7 +2491,7 @@ paths: - $ref: '#/definitions/httpserver.JSONResult' - properties: data: - $ref: '#/definitions/bookmarks.ContentDTO' + $ref: '#/definitions/bookmarks.BookmarkDTO' type: object "400": description: Bad Request @@ -2539,7 +2596,7 @@ paths: - $ref: '#/definitions/httpserver.JSONResult' - properties: data: - $ref: '#/definitions/bookmarks.ContentDTO' + $ref: '#/definitions/bookmarks.BookmarkContentDTO' type: object "400": description: Bad Request @@ -2597,7 +2654,7 @@ paths: - $ref: '#/definitions/httpserver.JSONResult' - properties: data: - $ref: '#/definitions/bookmarks.ContentDTO' + $ref: '#/definitions/bookmarks.BookmarkShareDTO' type: object "400": description: Bad Request @@ -2664,7 +2721,7 @@ paths: - $ref: '#/definitions/httpserver.JSONResult' - properties: data: - $ref: '#/definitions/bookmarks.ContentDTO' + $ref: '#/definitions/bookmarks.BookmarkShareDTO' type: object "400": description: Bad Request diff --git a/internal/core/bookmarks/bookmark_content_model.go b/internal/core/bookmarks/bookmark_content_model.go new file mode 100644 index 0000000..106a868 --- /dev/null +++ b/internal/core/bookmarks/bookmark_content_model.go @@ -0,0 +1,168 @@ +package bookmarks + +import ( + "encoding/json" + "recally/internal/pkg/db" + "recally/internal/pkg/webreader" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +type ContentType string + +const ( + ContentTypeBookmark ContentType = "bookmark" + ContentTypePDF ContentType = "pdf" + ContentTypeEPUB ContentType = "epub" + ContentTypeRSS ContentType = "rss" + ContentTypeNewsletter ContentType = "newsletter" + ContentTypeImage ContentType = "image" + ContentTypePodcast ContentType = "podcast" + ContentTypeVideo ContentType = "video" +) + +type BookmarkContentMetadata struct { + Author string `json:"author,omitempty"` + PublishedAt time.Time `json:"published_at,omitempty"` + Description string `json:"description,omitempty"` + SiteName string `json:"site_name,omitempty"` + Domain string `json:"domain,omitempty"` + + Image string `json:"image,omitempty"` + Favicon string `json:"favicon"` + Cover string `json:"cover,omitempty"` +} + +type BookmarkContentDTO struct { + ID uuid.UUID `json:"id"` + Type ContentType `json:"type"` + URL string `json:"url"` + UserID uuid.UUID `json:"user_id"` + Title string `json:"title"` + Description string `json:"description"` + Domain string `json:"domain"` + S3Key string `json:"s3_key"` + Summary string `json:"summary"` + Content string `json:"content"` + Html string `json:"html"` + Tags []string `json:"tags"` + Metadata BookmarkContentMetadata `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (b *BookmarkContentDTO) Load(dbo *db.BookmarkContent) { + b.ID = dbo.ID + b.Type = ContentType(dbo.Type) + b.URL = dbo.Url + b.UserID = dbo.UserID.Bytes + b.Title = dbo.Title.String + b.Description = dbo.Description.String + b.Domain = dbo.Domain.String + b.S3Key = dbo.S3Key.String + b.Summary = dbo.Summary.String + b.Content = dbo.Content.String + b.Html = dbo.Html.String + b.Tags = dbo.Tags + b.CreatedAt = dbo.CreatedAt.Time + b.UpdatedAt = dbo.UpdatedAt.Time + + if dbo.Metadata != nil { + b.Metadata = loadBookmarkContentMetadata(dbo.Metadata) + } +} + +func (b *BookmarkContentDTO) Dump() db.CreateBookmarkContentParams { + metadata, _ := json.Marshal(b.Metadata) + + return db.CreateBookmarkContentParams{ + Type: string(b.Type), + Url: b.URL, + UserID: pgtype.UUID{Bytes: b.UserID, Valid: b.UserID != uuid.Nil}, + Title: pgtype.Text{ + String: b.Title, + Valid: b.Title != "", + }, + Description: pgtype.Text{ + String: b.Description, + Valid: b.Description != "", + }, + Domain: pgtype.Text{ + String: b.Domain, + Valid: b.Domain != "", + }, + S3Key: pgtype.Text{ + String: b.S3Key, + Valid: b.S3Key != "", + }, + Summary: pgtype.Text{ + String: b.Summary, + Valid: b.Summary != "", + }, + Content: pgtype.Text{ + String: b.Content, + Valid: b.Content != "", + }, + Html: pgtype.Text{ + String: b.Html, + Valid: b.Html != "", + }, + Metadata: metadata, + } +} + +func (b *BookmarkContentDTO) DumpToUpdateParams() db.UpdateBookmarkContentParams { + return db.UpdateBookmarkContentParams{ + ID: b.ID, + Title: pgtype.Text{ + String: b.Title, + Valid: b.Title != "", + }, + Description: pgtype.Text{ + String: b.Description, + Valid: b.Description != "", + }, + S3Key: pgtype.Text{ + String: b.S3Key, + Valid: b.S3Key != "", + }, + Summary: pgtype.Text{ + String: b.Summary, + Valid: b.Summary != "", + }, + Content: pgtype.Text{ + String: b.Content, + Valid: b.Content != "", + }, + Html: pgtype.Text{ + String: b.Html, + Valid: b.Html != "", + }, + Metadata: dumpBookmarkContentMetadata(b.Metadata), + } +} + +func (c *BookmarkContentDTO) FromReaderContent(article *webreader.Content) { + c.Content = article.Markwdown + c.Title = article.Title + c.Html = article.Html + + // Update metadata + c.Metadata.Author = article.Author + c.Metadata.SiteName = article.SiteName + c.Metadata.Description = article.Description + + c.Metadata.Cover = article.Cover + c.Metadata.Favicon = article.Favicon + if article.Cover != "" { + c.Metadata.Image = article.Cover + } else { + c.Metadata.Image = article.Favicon + } + + if article.PublishedTime != nil { + c.Metadata.PublishedAt = *article.PublishedTime + } +} diff --git a/internal/core/bookmarks/bookmark_content_service.go b/internal/core/bookmarks/bookmark_content_service.go new file mode 100644 index 0000000..f824df7 --- /dev/null +++ b/internal/core/bookmarks/bookmark_content_service.go @@ -0,0 +1,145 @@ +package bookmarks + +import ( + "context" + "fmt" + "net/url" + "recally/internal/pkg/auth" + "recally/internal/pkg/cache" + "recally/internal/pkg/db" + "recally/internal/pkg/logger" + "recally/internal/pkg/webreader" + "recally/internal/pkg/webreader/fetcher" + "recally/internal/pkg/webreader/processor" + "recally/internal/pkg/webreader/reader" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Service) IsBookmarkContentExistByURL(ctx context.Context, tx db.DBTX, url string) (bool, error) { + return s.dao.IsBookmarkContentExistByURL(ctx, tx, url) +} + +func (s *Service) CreateBookmarkContent(ctx context.Context, tx db.DBTX, content *BookmarkContentDTO) (*BookmarkContentDTO, error) { + params := content.Dump() + dbo, err := s.dao.CreateBookmarkContent(ctx, tx, params) + if err != nil { + return nil, err + } + + result := &BookmarkContentDTO{} + result.Load(&dbo) + return result, nil +} + +func (s *Service) GetBookmarkContentByID(ctx context.Context, tx db.DBTX, id uuid.UUID) (*BookmarkContentDTO, error) { + dbo, err := s.dao.GetBookmarkContentByID(ctx, tx, id) + if err != nil { + return nil, err + } + + result := &BookmarkContentDTO{} + result.Load(&dbo) + return result, nil +} + +func (s *Service) GetBookmarkContentByURL(ctx context.Context, tx db.DBTX, url string, userID uuid.UUID) (*BookmarkContentDTO, error) { + dbo, err := s.dao.GetBookmarkContentByURL(ctx, tx, db.GetBookmarkContentByURLParams{ + Url: url, + UserID: pgtype.UUID{Bytes: userID, Valid: userID != uuid.Nil}, + }) + if err != nil { + return nil, err + } + + result := &BookmarkContentDTO{} + result.Load(&dbo) + return result, nil +} + +func (s *Service) UpdateBookmarkContent(ctx context.Context, tx db.DBTX, content *BookmarkContentDTO) (*BookmarkContentDTO, error) { + params := content.DumpToUpdateParams() + dbo, err := s.dao.UpdateBookmarkContent(ctx, tx, params) + if err != nil { + return nil, err + } + + result := &BookmarkContentDTO{} + result.Load(&dbo) + return result, nil +} + +func (s *Service) FetchContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID, opts fetcher.FetchOptions) (*BookmarkContentDTO, error) { + dto, err := s.GetBookmarkContentByID(ctx, tx, id) + if err != nil { + return nil, fmt.Errorf("failed to get bookmark by id '%s': %w", id.String(), err) + } + if dto.Content != "" && !opts.Force { + return dto, nil + } + content, err := s.FetchContentWithCache(ctx, dto.URL, opts) + if err != nil { + return nil, fmt.Errorf("failed to fetch content: %w", err) + } + + dto.FromReaderContent(content) + return s.UpdateBookmarkContent(ctx, tx, dto) +} + +func (s *Service) FetchContentWithCache(ctx context.Context, uri string, opts fetcher.FetchOptions) (*webreader.Content, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, fmt.Errorf("invalid url '%s': %w", uri, err) + } + + reader, err := reader.New(u.Host, opts) + if err != nil { + return nil, fmt.Errorf("failed to create reader: %w", err) + } + + content, err := cache.RunInCache(ctx, cache.DefaultDBCache, cache.NewCacheKey(fmt.Sprintf("WebReader-%s", opts.FecherType), uri), 24*time.Hour, func() (*webreader.Content, error) { + return reader.Fetch(ctx, uri) + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch content: %w", err) + } + return reader.Process(ctx, content) +} + +func (s *Service) SummarierContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) (*BookmarkContentDTO, error) { + user, err := auth.LoadUser(ctx, tx, userID) + if err != nil { + return nil, err + } + + dto, err := s.GetBookmarkContentByID(ctx, tx, id) + if err != nil { + return nil, err + } + + content := &webreader.Content{ + Markwdown: dto.Content, + } + + summarier := processor.NewSummaryProcessor(s.llm, processor.WithSummaryOptionUser(user)) + + if len(content.Markwdown) < 1000 { + logger.FromContext(ctx).Info("content is too short to summarise") + return dto, nil + } + + if err := summarier.Process(ctx, content); err != nil { + logger.Default.Error("failed to generate summary", "err", err) + } else { + tags, summary := parseTagsFromSummary(content.Summary) + if len(tags) > 0 { + if err := s.linkContentTags(ctx, tx, dto.Tags, tags, id, userID); err != nil { + return nil, err + } + } + dto.Summary = summary + } + return s.UpdateBookmarkContent(ctx, tx, dto) +} diff --git a/internal/core/bookmarks/bookmark_model.go b/internal/core/bookmarks/bookmark_model.go new file mode 100644 index 0000000..2c9b080 --- /dev/null +++ b/internal/core/bookmarks/bookmark_model.go @@ -0,0 +1,228 @@ +package bookmarks + +import ( + "encoding/json" + "recally/internal/pkg/db" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +type Highlight struct { + ID string `json:"id"` + Text string `json:"text"` + StartOffset int `json:"start_offset"` + EndOffset int `json:"end_offset"` + Note string `json:"note,omitempty"` +} + +type BookmarkMetadata struct { + ReadingProgress int `json:"reading_progress,omitempty"` + LastReadAt time.Time `json:"last_read_at,omitempty"` + + Highlights []Highlight `json:"highlights,omitempty"` + Share *BookmarkShareDTO `json:"share,omitempty"` +} + +type BookmarkDTO struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + ContentID uuid.UUID `json:"content_id"` + IsFavorite bool `json:"is_favorite"` + IsArchive bool `json:"is_archive"` + IsPublic bool `json:"is_public"` + ReadingProgress int `json:"reading_progress"` + Metadata BookmarkMetadata `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Tags []string `json:"tags"` +} + +func (b *BookmarkDTO) Load(dbo *db.Bookmark) { + b.ID = dbo.ID + b.UserID = dbo.UserID.Bytes + b.ContentID = dbo.ContentID.Bytes + b.IsFavorite = dbo.IsFavorite.Bool + b.IsArchive = dbo.IsArchive.Bool + b.IsPublic = dbo.IsPublic.Bool + b.ReadingProgress = int(dbo.ReadingProgress.Int32) + b.CreatedAt = dbo.CreatedAt.Time + b.UpdatedAt = dbo.UpdatedAt.Time + + if dbo.Metadata != nil { + b.Metadata = loadBookmarkMetadata(dbo.Metadata) + } +} + +func (b *BookmarkDTO) Dump() db.CreateBookmarkParams { + return db.CreateBookmarkParams{ + UserID: pgtype.UUID{Bytes: b.UserID, Valid: b.UserID != uuid.Nil}, + ContentID: pgtype.UUID{Bytes: b.ContentID, Valid: b.ContentID != uuid.Nil}, + IsFavorite: pgtype.Bool{ + Bool: b.IsFavorite, + Valid: true, + }, + IsArchive: pgtype.Bool{ + Bool: b.IsArchive, + Valid: true, + }, + IsPublic: pgtype.Bool{ + Bool: b.IsPublic, + Valid: true, + }, + ReadingProgress: pgtype.Int4{ + Int32: int32(b.ReadingProgress), + Valid: true, + }, + Metadata: dumpBookmarkMetadata(b.Metadata), + } +} + +func (b *BookmarkDTO) DumpToUpdateParams() db.UpdateBookmarkParams { + return db.UpdateBookmarkParams{ + ID: b.ID, + UserID: pgtype.UUID{Bytes: b.UserID, Valid: b.UserID != uuid.Nil}, + IsFavorite: pgtype.Bool{ + Bool: b.IsFavorite, + Valid: true, + }, + IsArchive: pgtype.Bool{ + Bool: b.IsArchive, + Valid: true, + }, + IsPublic: pgtype.Bool{ + Bool: b.IsPublic, + Valid: true, + }, + ReadingProgress: pgtype.Int4{ + Int32: int32(b.ReadingProgress), + Valid: true, + }, + Metadata: dumpBookmarkMetadata(b.Metadata), + } +} + +type BookmarkWithContentDTO struct { + BookmarkDTO + Content BookmarkContentDTO `json:"content"` +} + +func (d *BookmarkWithContentDTO) Load(dbo *db.GetBookmarkWithContentRow) { + // Load bookmark data + d.ID = dbo.ID + d.UserID = dbo.UserID.Bytes + d.ContentID = dbo.ID_2 + d.IsFavorite = dbo.IsFavorite.Bool + d.IsArchive = dbo.IsArchive.Bool + d.IsPublic = dbo.IsPublic.Bool + d.ReadingProgress = int(dbo.ReadingProgress.Int32) + d.CreatedAt = dbo.CreatedAt.Time + d.UpdatedAt = dbo.UpdatedAt.Time + + // Load bookmark metadata + if dbo.Metadata != nil { + d.Metadata = loadBookmarkMetadata(dbo.Metadata) + } + + // Load tags from the aggregated tags field + d.Tags = loadBookmarkTags(dbo.Tags) + + // Load content data + d.Content.ID = dbo.ID_2 + d.Content.Type = ContentType(dbo.Type) + d.Content.URL = dbo.Url + d.Content.UserID = dbo.UserID_2.Bytes + d.Content.Title = dbo.Title.String + d.Content.Description = dbo.Description.String + d.Content.Domain = dbo.Domain.String + d.Content.S3Key = dbo.S3Key.String + d.Content.Summary = dbo.Summary.String + d.Content.Content = dbo.Content.String + d.Content.Html = dbo.Html.String + d.Content.Tags = dbo.Tags + d.Content.CreatedAt = dbo.CreatedAt_2.Time + d.Content.UpdatedAt = dbo.UpdatedAt_2.Time + + // Load content metadata + if dbo.Metadata_2 != nil { + d.Content.Metadata = loadBookmarkContentMetadata(dbo.Metadata_2) + } +} + +func loadListBookmarks(dbos []db.ListBookmarksRow) []BookmarkDTO { + bookmarks := make([]BookmarkDTO, len(dbos)) + for i, dbo := range dbos { + b := &bookmarks[i] + b.ID = dbo.ID + b.UserID = dbo.UserID.Bytes + b.ContentID = dbo.ContentID.Bytes + b.IsFavorite = dbo.IsFavorite.Bool + b.IsArchive = dbo.IsArchive.Bool + b.IsPublic = dbo.IsPublic.Bool + b.ReadingProgress = int(dbo.ReadingProgress.Int32) + b.CreatedAt = dbo.CreatedAt.Time + b.UpdatedAt = dbo.UpdatedAt.Time + + if dbo.Metadata != nil { + b.Metadata = loadBookmarkMetadata(dbo.Metadata) + } + + b.Tags = loadBookmarkTags(dbo.Tags) + } + return bookmarks +} + +func loadSearchBookmarks(dbos []db.SearchBookmarksRow) []BookmarkDTO { + bookmarks := make([]BookmarkDTO, len(dbos)) + for i, dbo := range dbos { + b := &bookmarks[i] + b.ID = dbo.ID + b.UserID = dbo.UserID.Bytes + b.ContentID = dbo.ContentID.Bytes + b.IsFavorite = dbo.IsFavorite.Bool + b.IsArchive = dbo.IsArchive.Bool + b.IsPublic = dbo.IsPublic.Bool + b.ReadingProgress = int(dbo.ReadingProgress.Int32) + b.CreatedAt = dbo.CreatedAt.Time + b.UpdatedAt = dbo.UpdatedAt.Time + + if dbo.Metadata != nil { + b.Metadata = loadBookmarkMetadata(dbo.Metadata) + } + + b.Tags = loadBookmarkTags(dbo.Tags) + } + return bookmarks +} + +func loadBookmarkTags(input interface{}) []string { + if tags, ok := input.([]string); ok { + return tags + } + return nil +} + +func loadBookmarkMetadata(input interface{}) BookmarkMetadata { + if metadata, ok := input.(BookmarkMetadata); ok { + return metadata + } + return BookmarkMetadata{} +} + +func dumpBookmarkMetadata(input BookmarkMetadata) []byte { + metadata, _ := json.Marshal(input) + return metadata +} + +func loadBookmarkContentMetadata(input interface{}) BookmarkContentMetadata { + if metadata, ok := input.(BookmarkContentMetadata); ok { + return metadata + } + return BookmarkContentMetadata{} +} + +func dumpBookmarkContentMetadata(input BookmarkContentMetadata) []byte { + metadata, _ := json.Marshal(input) + return metadata +} diff --git a/internal/core/bookmarks/bookmark_service.go b/internal/core/bookmarks/bookmark_service.go new file mode 100644 index 0000000..3f22211 --- /dev/null +++ b/internal/core/bookmarks/bookmark_service.go @@ -0,0 +1,203 @@ +// Package bookmarks provides functionality for managing user bookmarks and their content +package bookmarks + +import ( + "context" + "fmt" + "net/url" + "recally/internal/pkg/db" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +// CreateBookmark creates a new bookmark for a user with the given content. +// It first validates the URL and checks if content already exists for the URL. +// If content exists and belongs to the user, returns ErrDuplicate. +// If content doesn't exist, creates new content. +// Finally creates the bookmark linking the user and content. +// +// Parameters: +// - ctx: Context for the operation +// - tx: Database transaction +// - userId: UUID of the user creating the bookmark +// - dto: BookmarkContentDTO containing bookmark details +// +// Returns: +// - *BookmarkDTO: Created bookmark data +// - error: ErrInvalidInput for invalid URL +// ErrDuplicate if bookmark already exists +// Other errors for database operations +func (s *Service) CreateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, dto *BookmarkContentDTO) (*BookmarkDTO, error) { + // Validate URL format before proceeding + if _, err := url.ParseRequestURI(dto.URL); err != nil { + return nil, fmt.Errorf("%w: invalid URL", ErrInvalidInput) + } + + // Track if content already exists in the database + isContentExist := false + + // Check if bookmark content already exists for this URL and user + content, err := s.dao.GetBookmarkContentByURL(ctx, tx, db.GetBookmarkContentByURLParams{ + Url: dto.URL, + UserID: pgtype.UUID{Bytes: userId, Valid: true}, + }) + + // Handle the response from content lookup + if err == nil { + isContentExist = true + } else if !db.IsNotFoundError(err) { + // Return error if it's not a "not found" error + return nil, fmt.Errorf("failed to check existing bookmark for url '%s': %w", dto.URL, err) + } + + if isContentExist { + if content.UserID.Valid { + // If content exists and belongs to a user, return duplicate error + return nil, fmt.Errorf("%w, id: %s", ErrDuplicate, content.ID) + } + } else { + // Create new content if it doesn't exist + createBookmarkContentParams := dto.Dump() + // Set UserID to Nil as this content can be shared + createBookmarkContentParams.UserID = pgtype.UUID{Bytes: uuid.Nil, Valid: false} + content, err = s.dao.CreateBookmarkContent(ctx, tx, dto.Dump()) + if err != nil { + return nil, fmt.Errorf("failed to create new bookmark content: %w", err) + } + } + + // Create the bookmark entry linking user to content + bookmark, err := s.dao.CreateBookmark(ctx, tx, db.CreateBookmarkParams{ + UserID: pgtype.UUID{Bytes: userId, Valid: true}, + ContentID: pgtype.UUID{Bytes: content.ID, Valid: true}, + }) + if err != nil { + return nil, fmt.Errorf("failed to create bookmark: %w", err) + } + + // Convert database model to DTO + var bookmarkDTO BookmarkDTO + bookmarkDTO.Load(&bookmark) + + return &bookmarkDTO, nil +} + +func (s *Service) GetBookmarkWithContent(ctx context.Context, tx db.DBTX, userId, id uuid.UUID) (*BookmarkWithContentDTO, error) { + bookmark, err := s.dao.GetBookmarkWithContent(ctx, tx, db.GetBookmarkWithContentParams{ + ID: id, + UserID: pgtype.UUID{Bytes: userId, Valid: true}, + }) + if err != nil { + return nil, err + } + var result BookmarkWithContentDTO + result.Load(&bookmark) + return &result, nil +} + +func (s *Service) ListBookmarks(ctx context.Context, tx db.DBTX, userID uuid.UUID, filters []string, query string, limit, offset int32) ([]BookmarkDTO, int64, error) { + if limit <= 0 || limit > 100 { + limit = 50 // Default limit + } + if offset < 0 { + offset = 0 + } + + // Use List instead of Search if no query provided since Search has worse performance + if query != "" { + return s.SearchBookmarks(ctx, tx, userID, filters, query, limit, offset) + } + + domains, contentTypes, tags := parseListFilter(filters) + totalCount := int64(0) + bs, err := s.dao.ListBookmarks(ctx, tx, db.ListBookmarksParams{ + UserID: pgtype.UUID{Bytes: userID, Valid: true}, + Limit: limit, + Offset: offset, + Domains: domains, + Types: contentTypes, + Tags: tags, + }) + if err != nil { + return nil, totalCount, fmt.Errorf("failed to list bookmarks: %w", err) + } + + dtos := loadListBookmarks(bs) + + if len(bs) > 0 { + totalCount = bs[0].TotalCount + } + + return dtos, totalCount, nil +} + +func (s *Service) SearchBookmarks(ctx context.Context, tx db.DBTX, userID uuid.UUID, filters []string, query string, limit, offset int32) ([]BookmarkDTO, int64, error) { + if limit <= 0 || limit > 100 { + limit = 50 // Default limit + } + if offset < 0 { + offset = 0 + } + domains, contentTypes, tags := parseListFilter(filters) + totalCount := int64(0) + + bs, err := s.dao.SearchBookmarks(ctx, tx, db.SearchBookmarksParams{ + UserID: pgtype.UUID{Bytes: userID, Valid: true}, + Limit: limit, + Offset: offset, + Domains: domains, + Types: contentTypes, + Tags: tags, + Query: pgtype.Text{ + String: query, + Valid: query != "", + }, + }) + if err != nil { + return nil, totalCount, fmt.Errorf("failed to search bookmarks: %w", err) + } + + dtos := loadSearchBookmarks(bs) + if len(bs) > 0 { + totalCount = bs[0].TotalCount + } + return dtos, totalCount, nil +} + +func (s *Service) DeleteBookmark(ctx context.Context, tx db.DBTX, userId, id uuid.UUID) error { + return s.dao.DeleteBookmark(ctx, tx, db.DeleteBookmarkParams{ + ID: id, + UserID: pgtype.UUID{Bytes: userId, Valid: true}, + }) +} + +func (s *Service) DeleteBookmarksByUser(ctx context.Context, tx db.DBTX, userId uuid.UUID) error { + return s.dao.DeleteBookmarksByUser(ctx, tx, pgtype.UUID{Bytes: userId, Valid: true}) +} + +func (s *Service) UpdateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, id uuid.UUID, content *BookmarkContentDTO) (*BookmarkDTO, error) { + bookmark, err := s.GetBookmarkWithContent(ctx, tx, userId, id) + if err != nil { + return nil, err + } + + updateContent := bookmark.Content + // Update content if it's changed + if content.Content != "" { + updateContent.Content = content.Content + } + if content.Description != "" { + updateContent.Description = content.Description + } + if content.Html != "" { + updateContent.Html = content.Html + } + if content.Summary != "" { + updateContent.Summary = content.Summary + } + if _, err = s.UpdateBookmarkContent(ctx, tx, &updateContent); err != nil { + return nil, err + } + return &bookmark.BookmarkDTO, nil +} diff --git a/internal/core/bookmarks/bookmark_share_model.go b/internal/core/bookmarks/bookmark_share_model.go new file mode 100644 index 0000000..f2e5627 --- /dev/null +++ b/internal/core/bookmarks/bookmark_share_model.go @@ -0,0 +1,38 @@ +package bookmarks + +import ( + "recally/internal/pkg/db" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +type BookmarkShareDTO struct { + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + BookmarkID uuid.UUID `json:"bookmark_id"` + ExpiresAt time.Time `json:"expires_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (c *BookmarkShareDTO) Load(dbo *db.BookmarkShare) { + c.ID = dbo.ID + c.UserID = dbo.UserID + c.BookmarkID = dbo.BookmarkID.Bytes + c.ExpiresAt = dbo.ExpiresAt.Time + c.CreatedAt = dbo.CreatedAt.Time + c.UpdatedAt = dbo.UpdatedAt.Time +} + +func (c *BookmarkShareDTO) Dump() db.CreateShareContentParams { + return db.CreateShareContentParams{ + UserID: c.UserID, + ContentID: pgtype.UUID{Bytes: c.BookmarkID, Valid: c.BookmarkID != uuid.Nil}, + ExpiresAt: pgtype.Timestamptz{ + Time: c.ExpiresAt, + Valid: !c.ExpiresAt.IsZero(), + }, + } +} diff --git a/internal/core/bookmarks/bookmark_share_service.go b/internal/core/bookmarks/bookmark_share_service.go new file mode 100644 index 0000000..a7e3c49 --- /dev/null +++ b/internal/core/bookmarks/bookmark_share_service.go @@ -0,0 +1,79 @@ +package bookmarks + +import ( + "context" + "fmt" + "recally/internal/pkg/db" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Service) CreateBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID, expiresAt time.Time) (*BookmarkShareDTO, error) { + cs, err := s.dao.CreateBookmarkShare(ctx, tx, db.CreateBookmarkShareParams{ + UserID: userID, + BookmarkID: pgtype.UUID{Bytes: contentID, Valid: true}, + ExpiresAt: pgtype.Timestamptz{ + Time: expiresAt, + Valid: !expiresAt.IsZero(), + }, + }) + + var dto BookmarkShareDTO + dto.Load(&cs) + return &dto, err +} + +func (s *Service) GetBookmarkShareContent(ctx context.Context, tx db.DBTX, sharedID uuid.UUID) (*BookmarkContentDTO, error) { + sharedContent, err := s.dao.GetBookmarkShareContent(ctx, tx, sharedID) + if err != nil { + return nil, fmt.Errorf("failed to get shared content: %w", err) + } + + var dto BookmarkContentDTO + dto.Load(&sharedContent) + return &dto, nil +} + +func (s *Service) GetBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID) (*BookmarkShareDTO, error) { + sharedContent, err := s.dao.GetBookmarkShare(ctx, tx, db.GetBookmarkShareParams{ + BookmarkID: pgtype.UUID{Bytes: contentID, Valid: true}, + UserID: userID, + }) + if err != nil { + return nil, fmt.Errorf("failed to get shared content: %w", err) + } + + var dto BookmarkShareDTO + dto.Load(&sharedContent) + return &dto, nil +} + +func (s *Service) UpdateSharedContent(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID, expiresAt time.Time) (*BookmarkShareDTO, error) { + sc, err := s.dao.UpdateBookmarkShareByBookmarkId(ctx, tx, db.UpdateBookmarkShareByBookmarkIdParams{ + ID: contentID, + UserID: pgtype.UUID{Bytes: userID, Valid: true}, + ExpiresAt: pgtype.Timestamptz{ + Time: expiresAt, + Valid: !expiresAt.IsZero(), + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to update shared content: %w", err) + } + + var dto BookmarkShareDTO + dto.Load(&sc) + return &dto, nil +} + +func (s *Service) DeleteSharedContent(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID) error { + if err := s.dao.DeleteShareContent(ctx, tx, db.DeleteShareContentParams{ + ID: contentID, + UserID: userID, + }); err != nil { + return fmt.Errorf("failed to delete shared content: %w", err) + } + return nil +} diff --git a/internal/core/bookmarks/bookmark_tag_service.go b/internal/core/bookmarks/bookmark_tag_service.go new file mode 100644 index 0000000..132b2bf --- /dev/null +++ b/internal/core/bookmarks/bookmark_tag_service.go @@ -0,0 +1,71 @@ +package bookmarks + +import ( + "context" + "fmt" + "recally/internal/pkg/db" + "recally/internal/pkg/logger" + "slices" + + "github.com/google/uuid" +) + +func (s *Service) linkContentTags(ctx context.Context, tx db.DBTX, originTags, newTags []string, contentID, userID uuid.UUID) error { + // create tags if not exist + allExistingTags, err := s.dao.ListExistingBookmarkTagsByTags(ctx, tx, db.ListExistingBookmarkTagsByTagsParams{ + Column1: newTags, + UserID: userID, + }) + if err != nil { + return fmt.Errorf("failed to list existing tags: %w", err) + } + for _, tag := range newTags { + if slices.Contains(allExistingTags, tag) { + continue + } + if _, err := s.dao.CreateBookmarkTag(ctx, tx, db.CreateBookmarkTagParams{ + Name: tag, + UserID: userID, + }); err != nil { + return fmt.Errorf("failed to create tag '%s': %w", tag, err) + } + } + + // link content with tags that not linked before + contentExistingTags, err := s.dao.ListBookmarkTagsByBookmarkId(ctx, tx, contentID) + if err != nil { + return fmt.Errorf("failed to list content tags: %w", err) + } + newLinkedTags := make([]string, 0) + for _, tag := range newTags { + if !slices.Contains(contentExistingTags, tag) { + newLinkedTags = append(newLinkedTags, tag) + } + } + if err := s.dao.LinkBookmarkWithTags(ctx, tx, db.LinkBookmarkWithTagsParams{ + BookmarkID: contentID, + Column2: newLinkedTags, + UserID: userID, + }); err != nil { + return fmt.Errorf("failed to link tags with content: %w", err) + } + + // unlink content with tags in original but not in new + removedTags := make([]string, 0) + for _, tag := range originTags { + if !slices.Contains(newTags, tag) { + removedTags = append(removedTags, tag) + } + } + + if err := s.dao.UnLinkBookmarkWithTags(ctx, tx, db.UnLinkBookmarkWithTagsParams{ + BookmarkID: contentID, + Column2: removedTags, + UserID: userID, + }); err != nil { + return fmt.Errorf("failed to unlink tags with content: %w", err) + } + + logger.FromContext(ctx).Info("link content with tags", "content_id", contentID, "new_tags", newTags, "origin_tags", originTags, "removed_tags", removedTags, "new_linked_tags", newLinkedTags) + return nil +} diff --git a/internal/core/bookmarks/dao.go b/internal/core/bookmarks/dao.go index 9f799a1..7929ed1 100644 --- a/internal/core/bookmarks/dao.go +++ b/internal/core/bookmarks/dao.go @@ -13,31 +13,9 @@ type DAO interface { CreateBookmark(ctx context.Context, tx db.DBTX, arg db.CreateBookmarkParams) (db.Bookmark, error) DeleteBookmark(ctx context.Context, db db.DBTX, arg db.DeleteBookmarkParams) error DeleteBookmarksByUser(ctx context.Context, db db.DBTX, userID pgtype.UUID) error - GetBookmarkByUUID(ctx context.Context, db db.DBTX, argUuid uuid.UUID) (db.Bookmark, error) - GetBookmarkByURL(ctx context.Context, db db.DBTX, arg db.GetBookmarkByURLParams) (db.Bookmark, error) ListBookmarks(ctx context.Context, db db.DBTX, arg db.ListBookmarksParams) ([]db.ListBookmarksRow, error) UpdateBookmark(ctx context.Context, db db.DBTX, arg db.UpdateBookmarkParams) (db.Bookmark, error) - CreateContent(ctx context.Context, db db.DBTX, arg db.CreateContentParams) (db.Content, error) - CreateContentTag(ctx context.Context, db db.DBTX, arg db.CreateContentTagParams) (db.ContentTag, error) - DeleteContent(ctx context.Context, db db.DBTX, arg db.DeleteContentParams) error - DeleteContentTag(ctx context.Context, db db.DBTX, arg db.DeleteContentTagParams) error - DeleteContentsByUser(ctx context.Context, db db.DBTX, userID uuid.UUID) error - GetContent(ctx context.Context, db db.DBTX, arg db.GetContentParams) (db.GetContentRow, error) - IsContentExistWithURL(ctx context.Context, db db.DBTX, arg db.IsContentExistWithURLParams) (bool, error) - - ListContentTags(ctx context.Context, db db.DBTX, arg db.ListContentTagsParams) ([]string, error) - ListContentDomains(ctx context.Context, db db.DBTX, userID uuid.UUID) ([]db.ListContentDomainsRow, error) - ListContents(ctx context.Context, db db.DBTX, arg db.ListContentsParams) ([]db.ListContentsRow, error) - ListTagsByUser(ctx context.Context, db db.DBTX, userID uuid.UUID) ([]db.ListTagsByUserRow, error) - OwnerTransferContent(ctx context.Context, db db.DBTX, arg db.OwnerTransferContentParams) error - UpdateContent(ctx context.Context, db db.DBTX, arg db.UpdateContentParams) (db.Content, error) - SearchContentsWithFilter(ctx context.Context, db db.DBTX, arg db.SearchContentsWithFilterParams) ([]db.SearchContentsWithFilterRow, error) - - ListExistingTagsByTags(ctx context.Context, db db.DBTX, arg db.ListExistingTagsByTagsParams) ([]string, error) - LinkContentWithTags(ctx context.Context, db db.DBTX, arg db.LinkContentWithTagsParams) error - UnLinkContentWithTags(ctx context.Context, db db.DBTX, arg db.UnLinkContentWithTagsParams) error - CreateShareContent(ctx context.Context, db db.DBTX, arg db.CreateShareContentParams) (db.ContentShare, error) DeleteShareContent(ctx context.Context, db db.DBTX, arg db.DeleteShareContentParams) error GetSharedContent(ctx context.Context, db db.DBTX, id uuid.UUID) (db.Content, error) diff --git a/internal/core/bookmarks/dto.go b/internal/core/bookmarks/dto.go deleted file mode 100644 index 296da86..0000000 --- a/internal/core/bookmarks/dto.go +++ /dev/null @@ -1,276 +0,0 @@ -package bookmarks - -import ( - "encoding/json" - "net/url" - "recally/internal/pkg/db" - "recally/internal/pkg/logger" - "recally/internal/pkg/webreader" - "time" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" -) - -type Highlight struct { - ID string `json:"id"` - Text string `json:"text"` - StartOffset int `json:"start_offset"` - EndOffset int `json:"end_offset"` - Note string `json:"note,omitempty"` -} - -type Metadata struct { - Author string `json:"author,omitempty"` - PublishedAt time.Time `json:"published_at,omitempty"` - Description string `json:"description,omitempty"` - SiteName string `json:"site_name,omitempty"` - Domain string `json:"domain,omitempty"` - - Image string `json:"image,omitempty"` - Favicon string `json:"favicon"` - Cover string `json:"cover,omitempty"` - - Tags []string `json:"tags,omitempty"` - Highlights []Highlight `json:"highlights,omitempty"` - - Share *ContentShareDTO `json:"share,omitempty"` -} - -type ContentType string - -const ( - ContentTypeBookmark ContentType = "bookmark" - ContentTypePDF ContentType = "pdf" - ContentTypeEPUB ContentType = "epub" - ContentTypeRSS ContentType = "rss" - ContentTypeNewsletter ContentType = "newsletter" - ContentTypeImage ContentType = "image" - ContentTypePodcast ContentType = "podcast" - ContentTypeVideo ContentType = "video" -) - -type ContentDTO struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` - Type ContentType `json:"type"` - URL string `json:"url,omitempty"` - Domain string `json:"domain,omitempty"` - S3Key string `json:"s3_key,omitempty"` - Title string `json:"title"` - Description string `json:"description,omitempty"` - Tags []string `json:"tags,omitempty"` - Summary string `json:"summary,omitempty"` - Content string `json:"content,omitempty"` - HTML string `json:"html,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` - IsFavorite bool `json:"is_favorite,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -func loadTag(dbTags interface{}) []string { - if dbTags != nil { - tags := make([]string, 0, len(dbTags.([]interface{}))) - for _, tag := range dbTags.([]interface{}) { - if str, ok := tag.(string); ok { - tags = append(tags, str) - } - } - return tags - } - return nil -} - -func (c *ContentDTO) Load(dbo *db.Content) { - c.ID = dbo.ID - c.UserID = dbo.UserID - c.Type = ContentType(dbo.Type) - c.URL = dbo.Url.String - c.Domain = dbo.Domain.String - c.S3Key = dbo.S3Key.String - c.Title = dbo.Title - c.Description = dbo.Description.String - c.Summary = dbo.Summary.String - c.Content = dbo.Content.String - c.HTML = dbo.Html.String - c.IsFavorite = dbo.IsFavorite.Bool - c.CreatedAt = dbo.CreatedAt.Time - c.UpdatedAt = dbo.UpdatedAt.Time - - if dbo.Metadata != nil { - if err := json.Unmarshal(dbo.Metadata, &c.Metadata); err != nil { - logger.Default.Warn("failed to unmarshal Content metadata", - "err", err, "metadata", string(dbo.Metadata)) - } - } -} - -func (c *ContentDTO) LoadWithTags(dbo *db.GetContentRow) { - c.ID = dbo.ID - c.UserID = dbo.UserID - c.Type = ContentType(dbo.Type) - c.URL = dbo.Url.String - c.Domain = dbo.Domain.String - c.S3Key = dbo.S3Key.String - c.Title = dbo.Title - c.Description = dbo.Description.String - c.Summary = dbo.Summary.String - c.Content = dbo.Content.String - c.HTML = dbo.Html.String - c.IsFavorite = dbo.IsFavorite.Bool - c.CreatedAt = dbo.CreatedAt.Time - c.UpdatedAt = dbo.UpdatedAt.Time - c.Tags = loadTag(dbo.Tags) - - if dbo.Metadata != nil { - if err := json.Unmarshal(dbo.Metadata, &c.Metadata); err != nil { - logger.Default.Warn("failed to unmarshal Content metadata", - "err", err, "metadata", string(dbo.Metadata)) - } - } -} - -func (c *ContentDTO) LoadWithTagsAndTotalCount(dbo *db.ListContentsRow) { - c.ID = dbo.ID - c.UserID = dbo.UserID - c.Type = ContentType(dbo.Type) - c.URL = dbo.Url.String - c.Domain = dbo.Domain.String - c.S3Key = dbo.S3Key.String - c.Title = dbo.Title - c.Description = dbo.Description.String - c.Summary = dbo.Summary.String - c.Content = dbo.Content.String - c.HTML = dbo.Html.String - c.IsFavorite = dbo.IsFavorite.Bool - c.CreatedAt = dbo.CreatedAt.Time - c.UpdatedAt = dbo.UpdatedAt.Time - c.Tags = loadTag(dbo.Tags) - - if dbo.Metadata != nil { - if err := json.Unmarshal(dbo.Metadata, &c.Metadata); err != nil { - logger.Default.Warn("failed to unmarshal Content metadata", - "err", err, "metadata", string(dbo.Metadata)) - } - } -} - -func (c *ContentDTO) LoadWithTagsAndTotalCountFromSearch(dbo *db.SearchContentsWithFilterRow) { - c.ID = dbo.ID - c.UserID = dbo.UserID - c.Type = ContentType(dbo.Type) - c.URL = dbo.Url.String - c.Domain = dbo.Domain.String - c.S3Key = dbo.S3Key.String - c.Title = dbo.Title - c.Description = dbo.Description.String - c.Summary = dbo.Summary.String - c.Content = dbo.Content.String - c.HTML = dbo.Html.String - c.IsFavorite = dbo.IsFavorite.Bool - c.CreatedAt = dbo.CreatedAt.Time - c.UpdatedAt = dbo.UpdatedAt.Time - c.Tags = loadTag(dbo.Tags) - - if dbo.Metadata != nil { - if err := json.Unmarshal(dbo.Metadata, &c.Metadata); err != nil { - logger.Default.Warn("failed to unmarshal Content metadata", - "err", err, "metadata", string(dbo.Metadata)) - } - } -} - -func (c *ContentDTO) Dump() db.CreateContentParams { - metadata, _ := json.Marshal(c.Metadata) - - if (c.Domain == "") && (c.URL != "") { - u, _ := url.Parse(c.URL) - c.Domain = u.Host - } - - return db.CreateContentParams{ - UserID: c.UserID, - Type: string(c.Type), - Title: c.Title, - Description: pgtype.Text{String: c.Description, Valid: c.Description != ""}, - Url: pgtype.Text{String: c.URL, Valid: c.URL != ""}, - Domain: pgtype.Text{String: c.Domain, Valid: c.Domain != ""}, - S3Key: pgtype.Text{String: c.S3Key, Valid: c.S3Key != ""}, - Summary: pgtype.Text{String: c.Summary, Valid: c.Summary != ""}, - Content: pgtype.Text{String: c.Content, Valid: c.Content != ""}, - Html: pgtype.Text{String: c.HTML, Valid: c.HTML != ""}, - IsFavorite: pgtype.Bool{Bool: c.IsFavorite, Valid: true}, - Metadata: metadata, - } -} - -func (c *ContentDTO) DumpToUpdateParams() db.UpdateContentParams { - metadata, _ := json.Marshal(c.Metadata) - return db.UpdateContentParams{ - ID: c.ID, - UserID: c.UserID, - Title: pgtype.Text{String: c.Title, Valid: c.Title != ""}, - Description: pgtype.Text{String: c.Description, Valid: c.Description != ""}, - Url: pgtype.Text{String: c.URL, Valid: c.URL != ""}, - Domain: pgtype.Text{String: c.Domain, Valid: c.Domain != ""}, - S3Key: pgtype.Text{String: c.S3Key, Valid: c.S3Key != ""}, - Summary: pgtype.Text{String: c.Summary, Valid: c.Summary != ""}, - Content: pgtype.Text{String: c.Content, Valid: c.Content != ""}, - Html: pgtype.Text{String: c.HTML, Valid: c.HTML != ""}, - IsFavorite: pgtype.Bool{Bool: c.IsFavorite, Valid: true}, - Metadata: metadata, - } -} - -func (c *ContentDTO) FromReaderContent(article *webreader.Content) { - c.Content = article.Markwdown - c.Title = article.Title - c.HTML = article.Html - - // Update metadata - c.Metadata.Author = article.Author - c.Metadata.SiteName = article.SiteName - c.Metadata.Description = article.Description - - c.Metadata.Cover = article.Cover - c.Metadata.Favicon = article.Favicon - if article.Cover != "" { - c.Metadata.Image = article.Cover - } else { - c.Metadata.Image = article.Favicon - } - - if article.PublishedTime != nil { - c.Metadata.PublishedAt = *article.PublishedTime - } -} - -type ContentShareDTO struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` - ContentID uuid.UUID `json:"content_id"` - ExpiresAt time.Time `json:"expires_at,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -func (c *ContentShareDTO) Load(dbo *db.ContentShare) { - c.ID = dbo.ID - c.UserID = dbo.UserID - c.ContentID = dbo.ContentID.Bytes - c.ExpiresAt = dbo.ExpiresAt.Time - c.CreatedAt = dbo.CreatedAt.Time - c.UpdatedAt = dbo.UpdatedAt.Time -} - -func (c *ContentShareDTO) Dump() db.CreateShareContentParams { - return db.CreateShareContentParams{ - UserID: c.UserID, - ContentID: pgtype.UUID{Bytes: c.ContentID, Valid: c.ContentID != uuid.Nil}, - ExpiresAt: pgtype.Timestamptz{ - Time: c.ExpiresAt, - Valid: !c.ExpiresAt.IsZero(), - }, - } -} diff --git a/internal/core/bookmarks/service.go b/internal/core/bookmarks/service.go index 4852013..3097400 100644 --- a/internal/core/bookmarks/service.go +++ b/internal/core/bookmarks/service.go @@ -3,27 +3,15 @@ package bookmarks import ( "context" "fmt" - "net/url" - "recally/internal/pkg/auth" - "recally/internal/pkg/cache" "recally/internal/pkg/db" "recally/internal/pkg/llms" - "recally/internal/pkg/logger" - "recally/internal/pkg/webreader" - "recally/internal/pkg/webreader/fetcher" - "recally/internal/pkg/webreader/processor" - "recally/internal/pkg/webreader/reader" - "regexp" - "slices" - "strings" - "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" ) type Service struct { - dao DAO + dao *db.Queries llm *llms.LLM } @@ -34,177 +22,13 @@ func NewService(llm *llms.LLM) *Service { } } -// CreateBookmark creates a new bookmark with content fetching and embedding generation -func (s *Service) Create(ctx context.Context, tx db.DBTX, dto *ContentDTO) (*ContentDTO, error) { - // Validate URL - if _, err := url.ParseRequestURI(dto.URL); err != nil { - return nil, fmt.Errorf("%w: invalid URL", ErrInvalidInput) - } - - // Check for existing bookmark - isExisting, err := s.dao.IsContentExistWithURL(ctx, tx, db.IsContentExistWithURLParams{ - Url: pgtype.Text{String: dto.URL, Valid: true}, - UserID: dto.UserID, - }) - if err != nil && !db.IsNotFoundError(err) { - return nil, fmt.Errorf("failed to check existing bookmark for url '%s': %w", dto.URL, err) - } - if isExisting { - return nil, fmt.Errorf("%w, id: %s", ErrDuplicate, dto.URL) - } - - // create content - c, err := s.dao.CreateContent(ctx, tx, dto.Dump()) - if err != nil { - return nil, fmt.Errorf("failed to create bookmark for url '%s': %w", dto.URL, err) - } - dto.Load(&c) - - if len(dto.Tags) > 0 { - if err := s.linkContentTags(ctx, tx, []string{}, dto.Tags, c.ID, dto.UserID); err != nil { - return nil, err - } - } - return dto, nil -} - -// GetBookmark retrieves a bookmark by ID -func (s *Service) Get(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) (*ContentDTO, error) { - c, err := s.dao.GetContent(ctx, tx, db.GetContentParams{ - ID: id, - UserID: userID, - }) - if err != nil { - return nil, fmt.Errorf("failed to get content by id '%s': %w", id.String(), err) - } - - var dto ContentDTO - dto.LoadWithTags(&c) - // Clear content and HTML - dto.HTML = "" - - // add share content info - if sc, err := s.GetShareContent(ctx, tx, id); err == nil { - dto.Metadata.Share = sc - } - - return &dto, nil -} - -func parseListFilter(filters []string) (domains, contentTypes, tags []string) { - if len(filters) == 0 { - return - } - - domains = make([]string, 0) - contentTypes = make([]string, 0) - tags = make([]string, 0) - - // Parse filter=category:article;type:rss - for _, part := range filters { - kv := strings.Split(part, ":") - if len(kv) != 2 { - continue - } - switch kv[0] { - case "domain": - domains = append(domains, kv[1]) - case "type": - contentTypes = append(contentTypes, kv[1]) - case "tag": - tags = append(tags, kv[1]) - } - } - if len(domains) == 0 { - domains = nil - } - if len(contentTypes) == 0 { - contentTypes = nil - } - if len(tags) == 0 { - tags = nil - } - return -} - -// ListBookmarks retrieves a paginated list of bookmarks for a user -func (s *Service) List(ctx context.Context, tx db.DBTX, userID uuid.UUID, filters []string, query string, limit, offset int32) ([]*ContentDTO, int64, error) { - if limit <= 0 || limit > 100 { - limit = 50 // Default limit - } - if offset < 0 { - offset = 0 - } - - // Use List instead of Search if no query provided since Search has worse performance - if query != "" { - return s.Search(ctx, tx, userID, filters, query, limit, offset) - } - - domains, contentTypes, tags := parseListFilter(filters) - totalCount := int64(0) - cs, err := s.dao.ListContents(ctx, tx, db.ListContentsParams{ - UserID: userID, - Limit: limit, - Offset: offset, - Domains: domains, - Types: contentTypes, - Tags: tags, - }) - - dtos := make([]*ContentDTO, 0, len(cs)) - for _, c := range cs { - var dto ContentDTO - dto.LoadWithTagsAndTotalCount(&c) - dto.HTML = "" - dto.Content = "" - dto.Summary = "" - dtos = append(dtos, &dto) - totalCount = c.TotalCount - } - return dtos, totalCount, err -} - -func (s *Service) Search(ctx context.Context, tx db.DBTX, userID uuid.UUID, filters []string, query string, limit, offset int32) ([]*ContentDTO, int64, error) { - if limit <= 0 || limit > 100 { - limit = 50 // Default limit - } - if offset < 0 { - offset = 0 - } - - domains, contentTypes, tags := parseListFilter(filters) - totalCount := int64(0) - cs, err := s.dao.SearchContentsWithFilter(ctx, tx, db.SearchContentsWithFilterParams{ - UserID: userID, - Limit: limit, - Offset: offset, - Domains: domains, - Types: contentTypes, - Tags: tags, - Query: pgtype.Text{String: query, Valid: query != ""}, - }) - - dtos := make([]*ContentDTO, 0, len(cs)) - for _, c := range cs { - var dto ContentDTO - dto.LoadWithTagsAndTotalCountFromSearch(&c) - dto.HTML = "" - dto.Content = "" - dto.Summary = "" - dtos = append(dtos, &dto) - totalCount = c.TotalCount - } - return dtos, totalCount, err -} - type TagDTO struct { Name string `json:"name"` Count int64 `json:"count"` } func (s *Service) ListTags(ctx context.Context, tx db.DBTX, userID uuid.UUID) ([]TagDTO, error) { - tags, err := s.dao.ListTagsByUser(ctx, tx, userID) + tags, err := s.dao.ListBookmarkTagsByUser(ctx, tx, userID) if err != nil { return nil, fmt.Errorf("failed to list tags for user '%s': %w", userID.String(), err) } @@ -213,7 +37,7 @@ func (s *Service) ListTags(ctx context.Context, tx db.DBTX, userID uuid.UUID) ([ for _, tag := range tags { tagsList = append(tagsList, TagDTO{ Name: tag.Name, - Count: tag.Count, + Count: tag.Cnt, }) } return tagsList, nil @@ -225,7 +49,7 @@ type DomainDTO struct { } func (s *Service) ListDomains(ctx context.Context, tx db.DBTX, userID uuid.UUID) ([]DomainDTO, error) { - domains, err := s.dao.ListContentDomains(ctx, tx, userID) + domains, err := s.dao.ListBookmarkDomains(ctx, tx, pgtype.UUID{Bytes: userID, Valid: true}) if err != nil { return nil, fmt.Errorf("failed to list domains for user '%s': %w", userID.String(), err) } @@ -233,217 +57,8 @@ func (s *Service) ListDomains(ctx context.Context, tx db.DBTX, userID uuid.UUID) for _, domain := range domains { tags = append(tags, DomainDTO{ Name: domain.Domain.String, - Count: domain.Count, + Count: domain.Cnt, }) } return tags, nil } - -// UpdateBookmark updates an existing bookmark, PUT full update -func (s *Service) Update(ctx context.Context, tx db.DBTX, id, userID uuid.UUID, dto *ContentDTO) (*ContentDTO, error) { - _, err := s.Get(ctx, tx, id, userID) - if err != nil { - return nil, err - } - updateParams := dto.DumpToUpdateParams() - c, err := s.dao.UpdateContent(ctx, tx, updateParams) - if err != nil { - return nil, err - } - - dto.Load(&c) - return dto, nil -} - -// DeleteBookmark removes a bookmark -func (s *Service) Delete(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) error { - _, err := s.Get(ctx, tx, id, userID) - if err != nil { - return err - } - - return s.dao.DeleteContent(ctx, tx, db.DeleteContentParams{ - ID: id, - UserID: userID, - }) -} - -// DeleteUserBookmarks removes all bookmarks for a user -func (s *Service) DeleteUserBookmarks(ctx context.Context, tx db.DBTX, userID uuid.UUID) error { - return s.dao.DeleteContentsByUser(ctx, tx, userID) -} - -func (s *Service) FetchContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID, opts fetcher.FetchOptions) (*ContentDTO, error) { - dto, err := s.Get(ctx, tx, id, userID) - if err != nil { - return nil, fmt.Errorf("failed to get bookmark by id '%s': %w", id.String(), err) - } - if dto.Content != "" && !opts.Force { - return dto, nil - } - content, err := s.FetchContentWithCache(ctx, dto.URL, opts) - if err != nil { - return nil, fmt.Errorf("failed to fetch content: %w", err) - } - - dto.FromReaderContent(content) - return s.Update(ctx, tx, id, userID, dto) -} - -func (s *Service) FetchContentWithCache(ctx context.Context, uri string, opts fetcher.FetchOptions) (*webreader.Content, error) { - u, err := url.Parse(uri) - if err != nil { - return nil, fmt.Errorf("invalid url '%s': %w", uri, err) - } - - reader, err := reader.New(u.Host, opts) - if err != nil { - return nil, fmt.Errorf("failed to create reader: %w", err) - } - - content, err := cache.RunInCache(ctx, cache.DefaultDBCache, cache.NewCacheKey(fmt.Sprintf("WebReader-%s", opts.FecherType), uri), 24*time.Hour, func() (*webreader.Content, error) { - return reader.Fetch(ctx, uri) - }) - if err != nil { - return nil, fmt.Errorf("failed to fetch content: %w", err) - } - return reader.Process(ctx, content) -} - -func (s *Service) SummarierContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) (*ContentDTO, error) { - user, err := auth.LoadUser(ctx, tx, userID) - if err != nil { - return nil, err - } - - dto, err := s.Get(ctx, tx, id, user.ID) - if err != nil { - return nil, err - } - - content := &webreader.Content{ - Markwdown: dto.Content, - } - - summarier := processor.NewSummaryProcessor(s.llm, processor.WithSummaryOptionUser(user)) - - if len(content.Markwdown) < 1000 { - logger.FromContext(ctx).Info("content is too short to summarise") - return dto, nil - } - - if err := summarier.Process(ctx, content); err != nil { - logger.Default.Error("failed to generate summary", "err", err) - } else { - tags, summary := parseTagsFromSummary(content.Summary) - if len(tags) > 0 { - if err := s.linkContentTags(ctx, tx, dto.Tags, tags, id, userID); err != nil { - return nil, err - } - } - dto.Summary = summary - } - return s.Update(ctx, tx, id, userID, dto) -} - -// parseTagsFromSummary extracts tags from a string and returns the tags array and the string without tags -func parseTagsFromSummary(input string) ([]string, string) { - // Regular expression to match the tags section - tagsRegex := regexp.MustCompile(`(?s).*?`) - - // Find tags section - tagsSection := tagsRegex.FindString(input) - - // If no tags section found, return empty array and original string - if tagsSection == "" { - return []string{}, input - } - - // Extract content between tags - content := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(tagsSection, ""), "")) - - // Split content by whitespace - words := strings.Fields(content) - - // Process valid tags - tagMap := make(map[string]bool) // Use map to ensure uniqueness - var tags []string - - for _, word := range words { - if strings.HasPrefix(word, "#") { - tag := strings.TrimPrefix(word, "#") - if tag != "" && !tagMap[tag] { - tagMap[tag] = true - tags = append(tags, tag) - } - } - } - - // Remove tags section from original string - cleanedString := strings.TrimSpace(tagsRegex.ReplaceAllString(input, "\n")) - - return tags, cleanedString -} - -func (s *Service) linkContentTags(ctx context.Context, tx db.DBTX, originTags, newTags []string, contentID, userID uuid.UUID) error { - // create tags if not exist - allExistingTags, err := s.dao.ListExistingTagsByTags(ctx, tx, db.ListExistingTagsByTagsParams{ - Column1: newTags, - UserID: userID, - }) - if err != nil { - return fmt.Errorf("failed to list existing tags: %w", err) - } - for _, tag := range newTags { - if slices.Contains(allExistingTags, tag) { - continue - } - if _, err := s.dao.CreateContentTag(ctx, tx, db.CreateContentTagParams{ - Name: tag, - UserID: userID, - }); err != nil { - return fmt.Errorf("failed to create tag '%s': %w", tag, err) - } - } - - // link content with tags that not linked before - contentExistingTags, err := s.dao.ListContentTags(ctx, tx, db.ListContentTagsParams{ - ContentID: contentID, - UserID: userID, - }) - if err != nil { - return fmt.Errorf("failed to list content tags: %w", err) - } - newLinkedTags := make([]string, 0) - for _, tag := range newTags { - if !slices.Contains(contentExistingTags, tag) { - newLinkedTags = append(newLinkedTags, tag) - } - } - if err := s.dao.LinkContentWithTags(ctx, tx, db.LinkContentWithTagsParams{ - ContentID: contentID, - Column2: newLinkedTags, - UserID: userID, - }); err != nil { - return fmt.Errorf("failed to link tags with content: %w", err) - } - - // unlink content with tags in original but not in new - removedTags := make([]string, 0) - for _, tag := range originTags { - if !slices.Contains(newTags, tag) { - removedTags = append(removedTags, tag) - } - } - - if err := s.dao.UnLinkContentWithTags(ctx, tx, db.UnLinkContentWithTagsParams{ - ContentID: contentID, - Column2: removedTags, - UserID: userID, - }); err != nil { - return fmt.Errorf("failed to unlink tags with content: %w", err) - } - - logger.FromContext(ctx).Info("link content with tags", "content_id", contentID, "new_tags", newTags, "origin_tags", originTags, "removed_tags", removedTags, "new_linked_tags", newLinkedTags) - return nil -} diff --git a/internal/core/bookmarks/share_content_service.go b/internal/core/bookmarks/share_content_service.go deleted file mode 100644 index e4819b5..0000000 --- a/internal/core/bookmarks/share_content_service.go +++ /dev/null @@ -1,100 +0,0 @@ -package bookmarks - -import ( - "context" - "fmt" - "recally/internal/pkg/auth" - "recally/internal/pkg/db" - "time" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" -) - -func (s *Service) ShareContent(ctx context.Context, tx db.DBTX, contentID uuid.UUID, expiresAt time.Time) (*ContentShareDTO, error) { - user, err := auth.LoadUserFromContext(ctx) - if err != nil { - return nil, err - } - - cs, err := s.dao.CreateShareContent(ctx, tx, db.CreateShareContentParams{ - UserID: user.ID, - ContentID: pgtype.UUID{Bytes: contentID, Valid: true}, - ExpiresAt: pgtype.Timestamptz{ - Time: expiresAt, - Valid: !expiresAt.IsZero(), - }, - }) - - var dto ContentShareDTO - dto.Load(&cs) - return &dto, err -} - -func (s *Service) GetSharedContent(ctx context.Context, tx db.DBTX, sharedID uuid.UUID) (*ContentDTO, error) { - sharedContent, err := s.dao.GetSharedContent(ctx, tx, sharedID) - if err != nil { - return nil, fmt.Errorf("failed to get shared content: %w", err) - } - - var dto ContentDTO - dto.Load(&sharedContent) - return &dto, nil -} - -func (s *Service) GetShareContent(ctx context.Context, tx db.DBTX, contentID uuid.UUID) (*ContentShareDTO, error) { - user, err := auth.LoadUserFromContext(ctx) - if err != nil { - return nil, err - } - - sharedContent, err := s.dao.GetShareContent(ctx, tx, db.GetShareContentParams{ - ContentID: pgtype.UUID{Bytes: contentID, Valid: true}, - UserID: user.ID, - }) - if err != nil { - return nil, fmt.Errorf("failed to get shared content: %w", err) - } - - var dto ContentShareDTO - dto.Load(&sharedContent) - return &dto, nil -} - -func (s *Service) UpdateSharedContent(ctx context.Context, tx db.DBTX, contentID uuid.UUID, expiresAt time.Time) (*ContentShareDTO, error) { - user, err := auth.LoadUserFromContext(ctx) - if err != nil { - return nil, err - } - - sc, err := s.dao.UpdateShareContent(ctx, tx, db.UpdateShareContentParams{ - ID: contentID, - UserID: user.ID, - ExpiresAt: pgtype.Timestamptz{ - Time: expiresAt, - Valid: !expiresAt.IsZero(), - }, - }) - if err != nil { - return nil, fmt.Errorf("failed to update shared content: %w", err) - } - - var dto ContentShareDTO - dto.Load(&sc) - return &dto, nil -} - -func (s *Service) DeleteSharedContent(ctx context.Context, tx db.DBTX, contentID uuid.UUID) error { - user, err := auth.LoadUserFromContext(ctx) - if err != nil { - return err - } - - if err := s.dao.DeleteShareContent(ctx, tx, db.DeleteShareContentParams{ - ID: contentID, - UserID: user.ID, - }); err != nil { - return fmt.Errorf("failed to delete shared content: %w", err) - } - return err -} diff --git a/internal/core/bookmarks/utils.go b/internal/core/bookmarks/utils.go new file mode 100644 index 0000000..e6d310a --- /dev/null +++ b/internal/core/bookmarks/utils.go @@ -0,0 +1,81 @@ +package bookmarks + +import ( + "regexp" + "strings" +) + +func parseListFilter(filters []string) (domains, contentTypes, tags []string) { + if len(filters) == 0 { + return + } + + domains = make([]string, 0) + contentTypes = make([]string, 0) + tags = make([]string, 0) + + // Parse filter=category:article;type:rss + for _, part := range filters { + kv := strings.Split(part, ":") + if len(kv) != 2 { + continue + } + switch kv[0] { + case "domain": + domains = append(domains, kv[1]) + case "type": + contentTypes = append(contentTypes, kv[1]) + case "tag": + tags = append(tags, kv[1]) + } + } + if len(domains) == 0 { + domains = nil + } + if len(contentTypes) == 0 { + contentTypes = nil + } + if len(tags) == 0 { + tags = nil + } + return +} + +// parseTagsFromSummary extracts tags from a string and returns the tags array and the string without tags +func parseTagsFromSummary(input string) ([]string, string) { + // Regular expression to match the tags section + tagsRegex := regexp.MustCompile(`(?s).*?`) + + // Find tags section + tagsSection := tagsRegex.FindString(input) + + // If no tags section found, return empty array and original string + if tagsSection == "" { + return []string{}, input + } + + // Extract content between tags + content := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(tagsSection, ""), "")) + + // Split content by whitespace + words := strings.Fields(content) + + // Process valid tags + tagMap := make(map[string]bool) // Use map to ensure uniqueness + var tags []string + + for _, word := range words { + if strings.HasPrefix(word, "#") { + tag := strings.TrimPrefix(word, "#") + if tag != "" && !tagMap[tag] { + tagMap[tag] = true + tags = append(tags, tag) + } + } + } + + // Remove tags section from original string + cleanedString := strings.TrimSpace(tagsRegex.ReplaceAllString(input, "\n")) + + return tags, cleanedString +} diff --git a/internal/pkg/db/bookmark_content.sql.go b/internal/pkg/db/bookmark_content.sql.go new file mode 100644 index 0000000..313c89a --- /dev/null +++ b/internal/pkg/db/bookmark_content.sql.go @@ -0,0 +1,219 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: bookmark_content.sql + +package db + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createBookmarkContent = `-- name: CreateBookmarkContent :one +INSERT INTO bookmark_content ( + type, + url, + user_id, + title, + description, + domain, + s3_key, + summary, + content, + html, + tags, + metadata +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 +) RETURNING id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at +` + +type CreateBookmarkContentParams struct { + Type string + Url string + UserID pgtype.UUID + Title pgtype.Text + Description pgtype.Text + Domain pgtype.Text + S3Key pgtype.Text + Summary pgtype.Text + Content pgtype.Text + Html pgtype.Text + Tags []string + Metadata []byte +} + +func (q *Queries) CreateBookmarkContent(ctx context.Context, db DBTX, arg CreateBookmarkContentParams) (BookmarkContent, error) { + row := db.QueryRow(ctx, createBookmarkContent, + arg.Type, + arg.Url, + arg.UserID, + arg.Title, + arg.Description, + arg.Domain, + arg.S3Key, + arg.Summary, + arg.Content, + arg.Html, + arg.Tags, + arg.Metadata, + ) + var i BookmarkContent + err := row.Scan( + &i.ID, + &i.Type, + &i.Url, + &i.UserID, + &i.Title, + &i.Description, + &i.Domain, + &i.S3Key, + &i.Summary, + &i.Content, + &i.Html, + &i.Tags, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getBookmarkContentByID = `-- name: GetBookmarkContentByID :one +SELECT id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at +FROM bookmark_content +WHERE id = $1 +` + +func (q *Queries) GetBookmarkContentByID(ctx context.Context, db DBTX, id uuid.UUID) (BookmarkContent, error) { + row := db.QueryRow(ctx, getBookmarkContentByID, id) + var i BookmarkContent + err := row.Scan( + &i.ID, + &i.Type, + &i.Url, + &i.UserID, + &i.Title, + &i.Description, + &i.Domain, + &i.S3Key, + &i.Summary, + &i.Content, + &i.Html, + &i.Tags, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getBookmarkContentByURL = `-- name: GetBookmarkContentByURL :one +SELECT id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at +FROM bookmark_content +WHERE url = $1 AND (user_id = $2 OR user_id IS NULL) +LIMIT 1 +` + +type GetBookmarkContentByURLParams struct { + Url string + UserID pgtype.UUID +} + +// First try to get user specific content, then the shared content +func (q *Queries) GetBookmarkContentByURL(ctx context.Context, db DBTX, arg GetBookmarkContentByURLParams) (BookmarkContent, error) { + row := db.QueryRow(ctx, getBookmarkContentByURL, arg.Url, arg.UserID) + var i BookmarkContent + err := row.Scan( + &i.ID, + &i.Type, + &i.Url, + &i.UserID, + &i.Title, + &i.Description, + &i.Domain, + &i.S3Key, + &i.Summary, + &i.Content, + &i.Html, + &i.Tags, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const isBookmarkContentExistByURL = `-- name: IsBookmarkContentExistByURL :one +SELECT EXISTS ( + SELECT 1 + FROM bookmark_content + WHERE url = $1 +) +` + +func (q *Queries) IsBookmarkContentExistByURL(ctx context.Context, db DBTX, url string) (bool, error) { + row := db.QueryRow(ctx, isBookmarkContentExistByURL, url) + var exists bool + err := row.Scan(&exists) + return exists, err +} + +const updateBookmarkContent = `-- name: UpdateBookmarkContent :one +UPDATE bookmark_content +SET title = COALESCE($2, title), + description = COALESCE($3, description), + s3_key = COALESCE($4, s3_key), + summary = COALESCE($5, summary), + content = COALESCE($6, content), + html = COALESCE($7, html), + metadata = COALESCE($8, metadata) +WHERE id = $1 +RETURNING id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at +` + +type UpdateBookmarkContentParams struct { + ID uuid.UUID + Title pgtype.Text + Description pgtype.Text + S3Key pgtype.Text + Summary pgtype.Text + Content pgtype.Text + Html pgtype.Text + Metadata []byte +} + +func (q *Queries) UpdateBookmarkContent(ctx context.Context, db DBTX, arg UpdateBookmarkContentParams) (BookmarkContent, error) { + row := db.QueryRow(ctx, updateBookmarkContent, + arg.ID, + arg.Title, + arg.Description, + arg.S3Key, + arg.Summary, + arg.Content, + arg.Html, + arg.Metadata, + ) + var i BookmarkContent + err := row.Scan( + &i.ID, + &i.Type, + &i.Url, + &i.UserID, + &i.Title, + &i.Description, + &i.Domain, + &i.S3Key, + &i.Summary, + &i.Content, + &i.Html, + &i.Tags, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/internal/pkg/db/bookmark_share.sql.go b/internal/pkg/db/bookmark_share.sql.go new file mode 100644 index 0000000..e84afba --- /dev/null +++ b/internal/pkg/db/bookmark_share.sql.go @@ -0,0 +1,156 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: bookmark_share.sql + +package db + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createBookmarkShare = `-- name: CreateBookmarkShare :one +INSERT INTO bookmark_share (user_id, bookmark_id, expires_at) +VALUES ($1, $2, $3) +RETURNING id, user_id, bookmark_id, expires_at, created_at, updated_at +` + +type CreateBookmarkShareParams struct { + UserID uuid.UUID + BookmarkID pgtype.UUID + ExpiresAt pgtype.Timestamptz +} + +func (q *Queries) CreateBookmarkShare(ctx context.Context, db DBTX, arg CreateBookmarkShareParams) (BookmarkShare, error) { + row := db.QueryRow(ctx, createBookmarkShare, arg.UserID, arg.BookmarkID, arg.ExpiresAt) + var i BookmarkShare + err := row.Scan( + &i.ID, + &i.UserID, + &i.BookmarkID, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteBookmarkShareByBookmarkId = `-- name: DeleteBookmarkShareByBookmarkId :exec +DELETE FROM bookmark_share bs +USING bookmarks b +WHERE bs.bookmark_id = b.id + AND b.id = $1 + AND b.user_id = $2 +` + +type DeleteBookmarkShareByBookmarkIdParams struct { + ID uuid.UUID + UserID pgtype.UUID +} + +func (q *Queries) DeleteBookmarkShareByBookmarkId(ctx context.Context, db DBTX, arg DeleteBookmarkShareByBookmarkIdParams) error { + _, err := db.Exec(ctx, deleteBookmarkShareByBookmarkId, arg.ID, arg.UserID) + return err +} + +const deleteExpiredBookmarkShare = `-- name: DeleteExpiredBookmarkShare :exec +DELETE +FROM bookmark_share +WHERE expires_at < now() +` + +func (q *Queries) DeleteExpiredBookmarkShare(ctx context.Context, db DBTX) error { + _, err := db.Exec(ctx, deleteExpiredBookmarkShare) + return err +} + +const getBookmarkShare = `-- name: GetBookmarkShare :one +SELECT id, user_id, bookmark_id, expires_at, created_at, updated_at +FROM bookmark_share +WHERE bookmark_id = $1 + AND user_id = $2 +` + +type GetBookmarkShareParams struct { + BookmarkID pgtype.UUID + UserID uuid.UUID +} + +func (q *Queries) GetBookmarkShare(ctx context.Context, db DBTX, arg GetBookmarkShareParams) (BookmarkShare, error) { + row := db.QueryRow(ctx, getBookmarkShare, arg.BookmarkID, arg.UserID) + var i BookmarkShare + err := row.Scan( + &i.ID, + &i.UserID, + &i.BookmarkID, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getBookmarkShareContent = `-- name: GetBookmarkShareContent :one +SELECT bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at +FROM bookmark_share AS bs + JOIN bookmarks AS b ON bs.bookmark_id = b.id + JOIN bookmark_content AS bc ON b.content_id = bc.id +WHERE bs.id = $1 + AND (bs.expires_at is NULL OR bs.expires_at > now()) +` + +func (q *Queries) GetBookmarkShareContent(ctx context.Context, db DBTX, id uuid.UUID) (BookmarkContent, error) { + row := db.QueryRow(ctx, getBookmarkShareContent, id) + var i BookmarkContent + err := row.Scan( + &i.ID, + &i.Type, + &i.Url, + &i.UserID, + &i.Title, + &i.Description, + &i.Domain, + &i.S3Key, + &i.Summary, + &i.Content, + &i.Html, + &i.Tags, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateBookmarkShareByBookmarkId = `-- name: UpdateBookmarkShareByBookmarkId :one +UPDATE bookmark_share bs +SET expires_at = $3 +FROM bookmarks b +WHERE bs.bookmark_id = b.id + AND b.id = $1 + AND b.user_id = $2 +RETURNING bs.id, bs.user_id, bs.bookmark_id, bs.expires_at, bs.created_at, bs.updated_at +` + +type UpdateBookmarkShareByBookmarkIdParams struct { + ID uuid.UUID + UserID pgtype.UUID + ExpiresAt pgtype.Timestamptz +} + +func (q *Queries) UpdateBookmarkShareByBookmarkId(ctx context.Context, db DBTX, arg UpdateBookmarkShareByBookmarkIdParams) (BookmarkShare, error) { + row := db.QueryRow(ctx, updateBookmarkShareByBookmarkId, arg.ID, arg.UserID, arg.ExpiresAt) + var i BookmarkShare + err := row.Scan( + &i.ID, + &i.UserID, + &i.BookmarkID, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/internal/pkg/db/bookmark_tags.sql.go b/internal/pkg/db/bookmark_tags.sql.go new file mode 100644 index 0000000..51eaf0c --- /dev/null +++ b/internal/pkg/db/bookmark_tags.sql.go @@ -0,0 +1,183 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: bookmark_tags.sql + +package db + +import ( + "context" + + "github.com/google/uuid" +) + +const createBookmarkTag = `-- name: CreateBookmarkTag :one +INSERT INTO bookmark_tags (name, user_id) +VALUES ($1, $2) +RETURNING id, name, user_id, created_at, updated_at +` + +type CreateBookmarkTagParams struct { + Name string + UserID uuid.UUID +} + +func (q *Queries) CreateBookmarkTag(ctx context.Context, db DBTX, arg CreateBookmarkTagParams) (BookmarkTag, error) { + row := db.QueryRow(ctx, createBookmarkTag, arg.Name, arg.UserID) + var i BookmarkTag + err := row.Scan( + &i.ID, + &i.Name, + &i.UserID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteBookmarkTag = `-- name: DeleteBookmarkTag :exec +DELETE FROM bookmark_tags +WHERE id = $1 + AND user_id = $2 +` + +type DeleteBookmarkTagParams struct { + ID uuid.UUID + UserID uuid.UUID +} + +func (q *Queries) DeleteBookmarkTag(ctx context.Context, db DBTX, arg DeleteBookmarkTagParams) error { + _, err := db.Exec(ctx, deleteBookmarkTag, arg.ID, arg.UserID) + return err +} + +const linkBookmarkWithTags = `-- name: LinkBookmarkWithTags :exec +INSERT INTO bookmark_tags_mapping (bookmark_id, tag_id) +SELECT $1, bt.id +FROM bookmark_tags bt +WHERE bt.name = ANY ($2::text[]) + AND bt.user_id = $3 +` + +type LinkBookmarkWithTagsParams struct { + BookmarkID uuid.UUID + Column2 []string + UserID uuid.UUID +} + +func (q *Queries) LinkBookmarkWithTags(ctx context.Context, db DBTX, arg LinkBookmarkWithTagsParams) error { + _, err := db.Exec(ctx, linkBookmarkWithTags, arg.BookmarkID, arg.Column2, arg.UserID) + return err +} + +const listBookmarkTagsByBookmarkId = `-- name: ListBookmarkTagsByBookmarkId :many +SELECT bt.name +FROM bookmark_tags bt + JOIN bookmark_tags_mapping btm ON bt.id = btm.tag_id +WHERE btm.bookmark_id = $1 +` + +func (q *Queries) ListBookmarkTagsByBookmarkId(ctx context.Context, db DBTX, bookmarkID uuid.UUID) ([]string, error) { + rows, err := db.Query(ctx, listBookmarkTagsByBookmarkId, bookmarkID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + items = append(items, name) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listBookmarkTagsByUser = `-- name: ListBookmarkTagsByUser :many +SELECT name, count(*) as cnt +FROM bookmark_tags +WHERE user_id = $1 +GROUP BY name +ORDER BY cnt DESC +` + +type ListBookmarkTagsByUserRow struct { + Name string + Cnt int64 +} + +func (q *Queries) ListBookmarkTagsByUser(ctx context.Context, db DBTX, userID uuid.UUID) ([]ListBookmarkTagsByUserRow, error) { + rows, err := db.Query(ctx, listBookmarkTagsByUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListBookmarkTagsByUserRow + for rows.Next() { + var i ListBookmarkTagsByUserRow + if err := rows.Scan(&i.Name, &i.Cnt); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listExistingBookmarkTagsByTags = `-- name: ListExistingBookmarkTagsByTags :many +SELECT name +FROM bookmark_tags +WHERE name = ANY ($1::text[]) + AND user_id = $2 +` + +type ListExistingBookmarkTagsByTagsParams struct { + Column1 []string + UserID uuid.UUID +} + +func (q *Queries) ListExistingBookmarkTagsByTags(ctx context.Context, db DBTX, arg ListExistingBookmarkTagsByTagsParams) ([]string, error) { + rows, err := db.Query(ctx, listExistingBookmarkTagsByTags, arg.Column1, arg.UserID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + items = append(items, name) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const unLinkBookmarkWithTags = `-- name: UnLinkBookmarkWithTags :exec +DELETE FROM bookmark_tags_mapping +WHERE bookmark_id = $1 + AND tag_id IN (SELECT id + FROM bookmark_tags + WHERE name = ANY ($2::text[]) + AND user_id = $3) +` + +type UnLinkBookmarkWithTagsParams struct { + BookmarkID uuid.UUID + Column2 []string + UserID uuid.UUID +} + +func (q *Queries) UnLinkBookmarkWithTags(ctx context.Context, db DBTX, arg UnLinkBookmarkWithTagsParams) error { + _, err := db.Exec(ctx, unLinkBookmarkWithTags, arg.BookmarkID, arg.Column2, arg.UserID) + return err +} diff --git a/internal/pkg/db/bookmarks.sql.go b/internal/pkg/db/bookmarks.sql.go index 30e9332..63cce7c 100644 --- a/internal/pkg/db/bookmarks.sql.go +++ b/internal/pkg/db/bookmarks.sql.go @@ -59,90 +59,6 @@ func (q *Queries) CreateBookmark(ctx context.Context, db DBTX, arg CreateBookmar return i, err } -const createBookmarkContent = `-- name: CreateBookmarkContent :one -INSERT INTO bookmark_content ( - type, title, description, user_id, url, domain, s3_key, - summary, content, html, metadata -) -VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 -) -RETURNING id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at -` - -type CreateBookmarkContentParams struct { - Type string - Title pgtype.Text - Description pgtype.Text - UserID pgtype.UUID - Url string - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Metadata []byte -} - -func (q *Queries) CreateBookmarkContent(ctx context.Context, db DBTX, arg CreateBookmarkContentParams) (BookmarkContent, error) { - row := db.QueryRow(ctx, createBookmarkContent, - arg.Type, - arg.Title, - arg.Description, - arg.UserID, - arg.Url, - arg.Domain, - arg.S3Key, - arg.Summary, - arg.Content, - arg.Html, - arg.Metadata, - ) - var i BookmarkContent - err := row.Scan( - &i.ID, - &i.Type, - &i.Url, - &i.UserID, - &i.Title, - &i.Description, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Tags, - &i.Metadata, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const createBookmarkTag = `-- name: CreateBookmarkTag :one -INSERT INTO bookmark_tags (name, user_id) -VALUES ($1, $2) -RETURNING id, name, user_id, created_at, updated_at -` - -type CreateBookmarkTagParams struct { - Name string - UserID uuid.UUID -} - -func (q *Queries) CreateBookmarkTag(ctx context.Context, db DBTX, arg CreateBookmarkTagParams) (BookmarkTag, error) { - row := db.QueryRow(ctx, createBookmarkTag, arg.Name, arg.UserID) - var i BookmarkTag - err := row.Scan( - &i.ID, - &i.Name, - &i.UserID, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - const deleteBookmark = `-- name: DeleteBookmark :exec DELETE FROM bookmarks WHERE id = $1 AND user_id = $2 @@ -158,22 +74,6 @@ func (q *Queries) DeleteBookmark(ctx context.Context, db DBTX, arg DeleteBookmar return err } -const deleteBookmarkTag = `-- name: DeleteBookmarkTag :exec -DELETE FROM bookmark_tags -WHERE id = $1 - AND user_id = $2 -` - -type DeleteBookmarkTagParams struct { - ID uuid.UUID - UserID uuid.UUID -} - -func (q *Queries) DeleteBookmarkTag(ctx context.Context, db DBTX, arg DeleteBookmarkTagParams) error { - _, err := db.Exec(ctx, deleteBookmarkTag, arg.ID, arg.UserID) - return err -} - const deleteBookmarksByUser = `-- name: DeleteBookmarksByUser :exec DELETE FROM bookmarks WHERE user_id = $1 @@ -184,7 +84,7 @@ func (q *Queries) DeleteBookmarksByUser(ctx context.Context, db DBTX, userID pgt return err } -const getBookmark = `-- name: GetBookmark :one +const getBookmarkWithContent = `-- name: GetBookmarkWithContent :one SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, COALESCE( @@ -201,12 +101,12 @@ GROUP BY b.id, bc.id LIMIT 1 ` -type GetBookmarkParams struct { +type GetBookmarkWithContentParams struct { ID uuid.UUID UserID pgtype.UUID } -type GetBookmarkRow struct { +type GetBookmarkWithContentRow struct { ID uuid.UUID UserID pgtype.UUID ContentID pgtype.UUID @@ -235,9 +135,9 @@ type GetBookmarkRow struct { Tags_2 interface{} } -func (q *Queries) GetBookmark(ctx context.Context, db DBTX, arg GetBookmarkParams) (GetBookmarkRow, error) { - row := db.QueryRow(ctx, getBookmark, arg.ID, arg.UserID) - var i GetBookmarkRow +func (q *Queries) GetBookmarkWithContent(ctx context.Context, db DBTX, arg GetBookmarkWithContentParams) (GetBookmarkWithContentRow, error) { + row := db.QueryRow(ctx, getBookmarkWithContent, arg.ID, arg.UserID) + var i GetBookmarkWithContentRow err := row.Scan( &i.ID, &i.UserID, @@ -269,21 +169,6 @@ func (q *Queries) GetBookmark(ctx context.Context, db DBTX, arg GetBookmarkParam return i, err } -const isBookmarkContentExistWithURL = `-- name: IsBookmarkContentExistWithURL :one -SELECT EXISTS ( - SELECT 1 - FROM bookmark_content bc - WHERE bc.url = $1 -) -` - -func (q *Queries) IsBookmarkContentExistWithURL(ctx context.Context, db DBTX, url string) (bool, error) { - row := db.QueryRow(ctx, isBookmarkContentExistWithURL, url) - var exists bool - err := row.Scan(&exists) - return exists, err -} - const isBookmarkExistWithURL = `-- name: IsBookmarkExistWithURL :one SELECT EXISTS ( SELECT 1 @@ -306,25 +191,6 @@ func (q *Queries) IsBookmarkExistWithURL(ctx context.Context, db DBTX, arg IsBoo return exists, err } -const linkBookmarkWithTags = `-- name: LinkBookmarkWithTags :exec -INSERT INTO bookmark_tags_mapping (bookmark_id, tag_id) -SELECT $1, bt.id -FROM bookmark_tags bt -WHERE bt.name = ANY ($2::text[]) - AND bt.user_id = $3 -` - -type LinkBookmarkWithTagsParams struct { - BookmarkID uuid.UUID - Column2 []string - UserID uuid.UUID -} - -func (q *Queries) LinkBookmarkWithTags(ctx context.Context, db DBTX, arg LinkBookmarkWithTagsParams) error { - _, err := db.Exec(ctx, linkBookmarkWithTags, arg.BookmarkID, arg.Column2, arg.UserID) - return err -} - const listBookmarkDomains = `-- name: ListBookmarkDomains :many SELECT bc.domain, count(*) as cnt FROM bookmarks b @@ -360,58 +226,6 @@ func (q *Queries) ListBookmarkDomains(ctx context.Context, db DBTX, userID pgtyp return items, nil } -const listBookmarkTagsByBookmarkId = `-- name: ListBookmarkTagsByBookmarkId :many -SELECT bt.name -FROM bookmark_tags bt - JOIN bookmark_tags_mapping btm ON bt.id = btm.tag_id -WHERE btm.bookmark_id = $1 -` - -func (q *Queries) ListBookmarkTagsByBookmarkId(ctx context.Context, db DBTX, bookmarkID uuid.UUID) ([]string, error) { - rows, err := db.Query(ctx, listBookmarkTagsByBookmarkId, bookmarkID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []string - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - return nil, err - } - items = append(items, name) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listBookmarkTagsByUser = `-- name: ListBookmarkTagsByUser :many -SELECT name FROM bookmark_tags -WHERE user_id = $1 -` - -func (q *Queries) ListBookmarkTagsByUser(ctx context.Context, db DBTX, userID uuid.UUID) ([]string, error) { - rows, err := db.Query(ctx, listBookmarkTagsByUser, userID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []string - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - return nil, err - } - items = append(items, name) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const listBookmarks = `-- name: ListBookmarks :many WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count @@ -425,7 +239,6 @@ WITH total AS ( AND ($6::text[] IS NULL OR bct.name = ANY($6::text[])) ) SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, - bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -465,23 +278,8 @@ type ListBookmarksRow struct { Metadata []byte CreatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz - ID_2 uuid.UUID - Type string - Url string - UserID_2 pgtype.UUID - Title pgtype.Text - Description pgtype.Text - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Tags []string - Metadata_2 []byte - CreatedAt_2 pgtype.Timestamptz - UpdatedAt_2 pgtype.Timestamptz TotalCount int64 - Tags_2 interface{} + Tags interface{} } func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksParams) ([]ListBookmarksRow, error) { @@ -511,23 +309,8 @@ func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksP &i.Metadata, &i.CreatedAt, &i.UpdatedAt, - &i.ID_2, - &i.Type, - &i.Url, - &i.UserID_2, - &i.Title, - &i.Description, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Tags, - &i.Metadata_2, - &i.CreatedAt_2, - &i.UpdatedAt_2, &i.TotalCount, - &i.Tags_2, + &i.Tags, ); err != nil { return nil, err } @@ -539,38 +322,6 @@ func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksP return items, nil } -const listExistingBookmarkTagsByTags = `-- name: ListExistingBookmarkTagsByTags :many -SELECT name -FROM bookmark_tags -WHERE name = ANY ($1::text[]) - AND user_id = $2 -` - -type ListExistingBookmarkTagsByTagsParams struct { - Column1 []string - UserID uuid.UUID -} - -func (q *Queries) ListExistingBookmarkTagsByTags(ctx context.Context, db DBTX, arg ListExistingBookmarkTagsByTagsParams) ([]string, error) { - rows, err := db.Query(ctx, listExistingBookmarkTagsByTags, arg.Column1, arg.UserID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []string - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - return nil, err - } - items = append(items, name) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const ownerTransferBookmark = `-- name: OwnerTransferBookmark :exec UPDATE bookmarks SET @@ -610,7 +361,6 @@ WITH total AS ( ) ) SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, - bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -659,23 +409,8 @@ type SearchBookmarksRow struct { Metadata []byte CreatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz - ID_2 uuid.UUID - Type string - Url string - UserID_2 pgtype.UUID - Title pgtype.Text - Description pgtype.Text - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Tags []string - Metadata_2 []byte - CreatedAt_2 pgtype.Timestamptz - UpdatedAt_2 pgtype.Timestamptz TotalCount int64 - Tags_2 interface{} + Tags interface{} } func (q *Queries) SearchBookmarks(ctx context.Context, db DBTX, arg SearchBookmarksParams) ([]SearchBookmarksRow, error) { @@ -706,23 +441,8 @@ func (q *Queries) SearchBookmarks(ctx context.Context, db DBTX, arg SearchBookma &i.Metadata, &i.CreatedAt, &i.UpdatedAt, - &i.ID_2, - &i.Type, - &i.Url, - &i.UserID_2, - &i.Title, - &i.Description, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Tags, - &i.Metadata_2, - &i.CreatedAt_2, - &i.UpdatedAt_2, &i.TotalCount, - &i.Tags_2, + &i.Tags, ); err != nil { return nil, err } @@ -734,26 +454,6 @@ func (q *Queries) SearchBookmarks(ctx context.Context, db DBTX, arg SearchBookma return items, nil } -const unLinkBookmarkWithTags = `-- name: UnLinkBookmarkWithTags :exec -DELETE FROM bookmark_tags_mapping -WHERE bookmark_id = $1 - AND tag_id IN (SELECT id - FROM bookmark_tags - WHERE name = ANY ($2::text[]) - AND user_id = $3) -` - -type UnLinkBookmarkWithTagsParams struct { - BookmarkID uuid.UUID - Column2 []string - UserID uuid.UUID -} - -func (q *Queries) UnLinkBookmarkWithTags(ctx context.Context, db DBTX, arg UnLinkBookmarkWithTagsParams) error { - _, err := db.Exec(ctx, unLinkBookmarkWithTags, arg.BookmarkID, arg.Column2, arg.UserID) - return err -} - const updateBookmark = `-- name: UpdateBookmark :one UPDATE bookmarks SET is_favorite = COALESCE($3, is_favorite), @@ -801,62 +501,3 @@ func (q *Queries) UpdateBookmark(ctx context.Context, db DBTX, arg UpdateBookmar ) return i, err } - -const updateBookmarkContent = `-- name: UpdateBookmarkContent :one -UPDATE bookmark_content -SET title = COALESCE($2, title), - description = COALESCE($3, description), - domain = COALESCE($4, domain), - s3_key = COALESCE($5, s3_key), - summary = COALESCE($6, summary), - content = COALESCE($7, content), - html = COALESCE($8, html), - metadata = COALESCE($9, metadata) -WHERE id = $1 -RETURNING id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at -` - -type UpdateBookmarkContentParams struct { - ID uuid.UUID - Title pgtype.Text - Description pgtype.Text - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Metadata []byte -} - -func (q *Queries) UpdateBookmarkContent(ctx context.Context, db DBTX, arg UpdateBookmarkContentParams) (BookmarkContent, error) { - row := db.QueryRow(ctx, updateBookmarkContent, - arg.ID, - arg.Title, - arg.Description, - arg.Domain, - arg.S3Key, - arg.Summary, - arg.Content, - arg.Html, - arg.Metadata, - ) - var i BookmarkContent - err := row.Scan( - &i.ID, - &i.Type, - &i.Url, - &i.UserID, - &i.Title, - &i.Description, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, - &i.Tags, - &i.Metadata, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} diff --git a/internal/pkg/db/models.go b/internal/pkg/db/models.go index ca8bb9a..d0013fb 100644 --- a/internal/pkg/db/models.go +++ b/internal/pkg/db/models.go @@ -147,6 +147,15 @@ type BookmarkContent struct { UpdatedAt pgtype.Timestamptz } +type BookmarkShare struct { + ID uuid.UUID + UserID uuid.UUID + BookmarkID pgtype.UUID + ExpiresAt pgtype.Timestamptz + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + type BookmarkTag struct { ID uuid.UUID Name string diff --git a/internal/port/bots/handlers/websummary.go b/internal/port/bots/handlers/websummary.go index 18c6da7..03862e7 100644 --- a/internal/port/bots/handlers/websummary.go +++ b/internal/port/bots/handlers/websummary.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "recally/internal/core/bookmarks" - "recally/internal/core/queue" "recally/internal/pkg/cache" "recally/internal/pkg/db" "recally/internal/pkg/llms" @@ -98,6 +97,7 @@ func (h *Handler) WebSummaryHandler(c tele.Context) error { } } + var bookmarkContentDTO *bookmarks.BookmarkContentDTO // cache the summary summary, err := cache.RunInCache[string](ctx, cache.DefaultDBCache, cache.NewCacheKey("WebSummary", url), 24*time.Hour, func() (*string, error) { isSummaryCached = false @@ -108,6 +108,7 @@ func (h *Handler) WebSummaryHandler(c tele.Context) error { if err != nil { return nil, fmt.Errorf("failed to get content: %w", err) } + bookmarkContentDTO.FromReaderContent(content) // process the summary summarier := processor.NewSummaryProcessor(h.llm, processor.WithSummaryOptionUser(user)) @@ -125,9 +126,9 @@ func (h *Handler) WebSummaryHandler(c tele.Context) error { logger.FromContext(ctx).Error("TextHandler failed to send message", "err", err, "text", text) } } else { - h.saveBookmark(ctx, tx, url, user.ID, resp) + bookmarkContentDTO.Summary = resp + h.saveBookmark(ctx, tx, user.ID, bookmarkContentDTO) } - return nil } @@ -135,27 +136,11 @@ func getUrlFromText(text string) string { return urlPattern.FindString(text) } -func (h *Handler) saveBookmark(ctx context.Context, tx db.DBTX, url string, userId uuid.UUID, summary string) { - bookmark, err := h.bookmarkService.Create(ctx, tx, &bookmarks.ContentDTO{ - UserID: userId, - URL: url, - Summary: summary, - CreatedAt: time.Now(), - }) +func (h *Handler) saveBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, bookmarkContent *bookmarks.BookmarkContentDTO) { + bookmark, err := h.bookmarkService.CreateBookmark(ctx, tx, userId, bookmarkContent) if err != nil { logger.FromContext(ctx).Error("save bookmark from reader bot error", "err", err.Error()) } else { - logger.FromContext(ctx).Info("save bookmark from reader bot", "id", bookmark.ID, "title", bookmark.Title) - } - - result, err := h.queue.Insert(ctx, queue.CrawlerWorkerArgs{ - ID: bookmark.ID, - UserID: bookmark.UserID, - FetchOptions: fetcher.FetchOptions{FecherType: fetcher.TypeJinaReader}, // use jina reader as we already fetch the content using jina reader, so it will hit the cache - }, nil) - if err != nil { - logger.FromContext(ctx).Error("failed to insert job", "err", err) - } else { - logger.FromContext(ctx).Info("success inserted job", "result", result, "err", err) + logger.FromContext(ctx).Info("save bookmark from reader bot", "id", bookmark.ID) } } diff --git a/internal/port/httpserver/handler_bookmark.go b/internal/port/httpserver/handler_bookmark.go index 1c5c47e..c956ae8 100644 --- a/internal/port/httpserver/handler_bookmark.go +++ b/internal/port/httpserver/handler_bookmark.go @@ -20,21 +20,23 @@ import ( // BookmarkService defines operations for managing bookmarks type BookmarkService interface { - Create(ctx context.Context, tx db.DBTX, dto *bookmarks.ContentDTO) (*bookmarks.ContentDTO, error) - Get(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) (*bookmarks.ContentDTO, error) - List(ctx context.Context, tx db.DBTX, userID uuid.UUID, filter []string, query string, limit, offset int32) ([]*bookmarks.ContentDTO, int64, error) + ListBookmarks(ctx context.Context, tx db.DBTX, userID uuid.UUID, filters []string, query string, limit, offset int32) ([]bookmarks.BookmarkDTO, int64, error) + CreateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, dto *bookmarks.BookmarkContentDTO) (*bookmarks.BookmarkDTO, error) + GetBookmarkWithContent(ctx context.Context, tx db.DBTX, userId, id uuid.UUID) (*bookmarks.BookmarkWithContentDTO, error) + UpdateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, id uuid.UUID, content *bookmarks.BookmarkContentDTO) (*bookmarks.BookmarkDTO, error) + DeleteBookmark(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) error + DeleteBookmarksByUser(ctx context.Context, tx db.DBTX, userID uuid.UUID) error + + FetchContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID, opts fetcher.FetchOptions) (*bookmarks.BookmarkContentDTO, error) + SummarierContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) (*bookmarks.BookmarkContentDTO, error) + ListTags(ctx context.Context, tx db.DBTX, userID uuid.UUID) ([]bookmarks.TagDTO, error) ListDomains(ctx context.Context, tx db.DBTX, userID uuid.UUID) ([]bookmarks.DomainDTO, error) - Update(ctx context.Context, tx db.DBTX, id, userID uuid.UUID, dto *bookmarks.ContentDTO) (*bookmarks.ContentDTO, error) - Delete(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) error - DeleteUserBookmarks(ctx context.Context, tx db.DBTX, userID uuid.UUID) error - FetchContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID, fetchOptions fetcher.FetchOptions) (*bookmarks.ContentDTO, error) - SummarierContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) (*bookmarks.ContentDTO, error) - - GetShareContent(ctx context.Context, tx db.DBTX, contentID uuid.UUID) (*bookmarks.ContentShareDTO, error) - ShareContent(ctx context.Context, tx db.DBTX, contentID uuid.UUID, expiresAt time.Time) (*bookmarks.ContentShareDTO, error) - UpdateSharedContent(ctx context.Context, tx db.DBTX, contentID uuid.UUID, expiresAt time.Time) (*bookmarks.ContentShareDTO, error) - DeleteSharedContent(ctx context.Context, tx db.DBTX, contentID uuid.UUID) error + + GetBookmarkShareContent(ctx context.Context, tx db.DBTX, sharedID uuid.UUID) (*bookmarks.BookmarkContentDTO, error) + CreateBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID, expiresAt time.Time) (*bookmarks.BookmarkShareDTO, error) + UpdateSharedContent(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID, expiresAt time.Time) (*bookmarks.BookmarkShareDTO, error) + DeleteSharedContent(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID) error } // bookmarkServiceImpl implements BookmarkService @@ -71,7 +73,7 @@ type listBookmarksRequest struct { } type listBookmarksResponse struct { - Bookmarks []*bookmarks.ContentDTO `json:"bookmarks"` + Bookmarks []bookmarks.BookmarkDTO `json:"bookmarks"` Total int64 `json:"total"` Limit int32 `json:"limit"` Offset int32 `json:"offset"` @@ -107,7 +109,7 @@ func (h *bookmarksHandler) listBookmarks(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - bookmarks, total, err := h.service.List(ctx, tx, user.ID, req.Filter, req.Query, req.Limit, req.Offset) + bookmarks, total, err := h.service.ListBookmarks(ctx, tx, user.ID, req.Filter, req.Query, req.Limit, req.Offset) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } @@ -139,7 +141,7 @@ func (h *bookmarksHandler) listTags(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - tags, err := cache.RunInCache[[]bookmarks.TagDTO](ctx, cache.MemCache, + tags, err := cache.RunInCache(ctx, cache.MemCache, cache.NewCacheKey("bookmarks", "tags"), time.Minute, func() (*[]bookmarks.TagDTO, error) { @@ -175,7 +177,7 @@ func (h *bookmarksHandler) listDomains(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - domains, err := cache.RunInCache[[]bookmarks.DomainDTO](ctx, cache.MemCache, + domains, err := cache.RunInCache(ctx, cache.MemCache, cache.NewCacheKey("bookmarks", "domains"), time.Minute, func() (*[]bookmarks.DomainDTO, error) { @@ -193,13 +195,13 @@ func (h *bookmarksHandler) listDomains(c echo.Context) error { } type createBookmarkRequest struct { - URL string `json:"url" validate:"required,url"` - Title string `json:"title"` - Description string `json:"description,omitempty"` - Tags []string `json:"tags,omitempty"` - Content string `json:"content,omitempty"` - HTML string `json:"html,omitempty"` - Metadata bookmarks.Metadata `json:"metadata"` + URL string `json:"url" validate:"required,url"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` + Content string `json:"content,omitempty"` + HTML string `json:"html,omitempty"` + Metadata bookmarks.BookmarkContentMetadata `json:"metadata"` } // createBookmark handles POST /bookmarks @@ -210,7 +212,7 @@ type createBookmarkRequest struct { // @Accept json // @Produce json // @Param bookmark body createBookmarkRequest true "Bookmark to create" -// @Success 201 {object} JSONResult{data=bookmarks.ContentDTO} "Created" +// @Success 201 {object} JSONResult{data=bookmarks.BookmarkDTO} "Created" // @Failure 400 {object} JSONResult{data=nil} "Bad Request" // @Failure 401 {object} JSONResult{data=nil} "Unauthorized" // @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" @@ -228,18 +230,18 @@ func (h *bookmarksHandler) createBookmark(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - bookmark := &bookmarks.ContentDTO{ + bookmark := &bookmarks.BookmarkContentDTO{ UserID: user.ID, URL: req.URL, Type: bookmarks.ContentTypeBookmark, Title: req.Title, Tags: req.Tags, Content: req.Content, - HTML: req.HTML, + Html: req.HTML, Metadata: req.Metadata, } - created, err := h.service.Create(ctx, tx, bookmark) + created, err := h.service.CreateBookmark(ctx, tx, user.ID, bookmark) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } @@ -268,7 +270,7 @@ type getBookmarkRequest struct { // @Accept json // @Produce json // @Param bookmark-id path string true "Bookmark ID" -// @Success 200 {object} JSONResult{data=bookmarks.ContentDTO} "Success" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkWithContentDTO} "Success" // @Failure 400 {object} JSONResult{data=nil} "Bad Request" // @Failure 401 {object} JSONResult{data=nil} "Unauthorized" // @Failure 404 {object} JSONResult{data=nil} "Not Found" @@ -287,7 +289,7 @@ func (h *bookmarksHandler) getBookmark(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - bookmark, err := h.service.Get(ctx, tx, req.BookmarkID, user.ID) + bookmark, err := h.service.GetBookmarkWithContent(ctx, tx, req.BookmarkID, user.ID) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } @@ -299,11 +301,11 @@ func (h *bookmarksHandler) getBookmark(c echo.Context) error { } type updateBookmarkRequest struct { - BookmarkID uuid.UUID `param:"bookmark-id" validate:"required,uuid4"` - Summary string `json:"summary"` - Content string `json:"content"` - HTML string `json:"html"` - Metadata bookmarks.Metadata `json:"metadata"` + BookmarkID uuid.UUID `param:"bookmark-id" validate:"required,uuid4"` + Summary string `json:"summary"` + Content string `json:"content"` + HTML string `json:"html"` + Metadata bookmarks.BookmarkContentMetadata `json:"metadata"` } // updateBookmark handles PUT /bookmarks/:bookmark-id @@ -315,7 +317,7 @@ type updateBookmarkRequest struct { // @Produce json // @Param bookmark-id path string true "Bookmark ID" // @Param bookmark body updateBookmarkRequest true "Updated bookmark data" -// @Success 200 {object} JSONResult{data=bookmarks.ContentDTO} "Success" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkDTO} "Success" // @Failure 400 {object} JSONResult{data=nil} "Bad Request" // @Failure 401 {object} JSONResult{data=nil} "Unauthorized" // @Failure 404 {object} JSONResult{data=nil} "Not Found" @@ -334,7 +336,7 @@ func (h *bookmarksHandler) updateBookmark(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - bookmark := &bookmarks.ContentDTO{ + bookmark := &bookmarks.BookmarkContentDTO{ ID: req.BookmarkID, UserID: user.ID, } @@ -348,10 +350,10 @@ func (h *bookmarksHandler) updateBookmark(c echo.Context) error { } if req.HTML != "" { - bookmark.HTML = req.HTML + bookmark.Html = req.HTML } - updated, err := h.service.Update(ctx, tx, req.BookmarkID, user.ID, bookmark) + updated, err := h.service.UpdateBookmark(ctx, tx, user.ID, req.BookmarkID, bookmark) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } @@ -388,7 +390,7 @@ func (h *bookmarksHandler) deleteBookmark(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - if err := h.service.Delete(ctx, tx, bookmarkID, user.ID); err != nil { + if err := h.service.DeleteBookmark(ctx, tx, bookmarkID, user.ID); err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } @@ -430,7 +432,7 @@ func (h *bookmarksHandler) deleteUserBookmarks(c echo.Context) error { return ErrorResponse(c, http.StatusUnauthorized, fmt.Errorf("unauthorized")) } - if err := h.service.DeleteUserBookmarks(ctx, tx, req.UserID); err != nil { + if err := h.service.DeleteBookmarksByUser(ctx, tx, req.UserID); err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } @@ -453,7 +455,7 @@ type refreshBookmarkRequest struct { // @Produce json // @Param bookmark-id path string true "Bookmark ID" // @Param request body refreshBookmarkRequest true "Refresh options" -// @Success 200 {object} JSONResult{data=bookmarks.ContentDTO} "Success" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkDTO} "Success" // @Failure 400 {object} JSONResult{data=nil} "Bad Request" // @Failure 401 {object} JSONResult{data=nil} "Unauthorized" // @Failure 404 {object} JSONResult{data=nil} "Not Found" @@ -472,10 +474,10 @@ func (h *bookmarksHandler) refreshBookmark(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - var bookmark *bookmarks.ContentDTO + var content *bookmarks.BookmarkContentDTO if req.Fetcher != "" { - bookmark, err = h.service.FetchContent(ctx, tx, req.BookmarkID, user.ID, fetcher.FetchOptions{ + content, err = h.service.FetchContent(ctx, tx, req.BookmarkID, user.ID, fetcher.FetchOptions{ FecherType: fetcher.FecherType(req.Fetcher), IsProxyImage: req.IsProxyImage, }) @@ -485,13 +487,13 @@ func (h *bookmarksHandler) refreshBookmark(c echo.Context) error { } if req.RegenerateSummary { - bookmark, err = h.service.SummarierContent(ctx, tx, req.BookmarkID, user.ID) + content, err = h.service.SummarierContent(ctx, tx, req.BookmarkID, user.ID) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } } - return JsonResponse(c, http.StatusOK, bookmark) + return JsonResponse(c, http.StatusOK, content) } type shareBookmarkRequest struct { @@ -508,7 +510,7 @@ type shareBookmarkRequest struct { // @Produce json // @Param bookmark-id path string true "Bookmark ID" // @Param request body shareBookmarkRequest true "Share options" -// @Success 200 {object} JSONResult{data=bookmarks.ContentDTO} "Success" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkShareDTO} "Success" // @Failure 400 {object} JSONResult{data=nil} "Bad Request" // @Failure 401 {object} JSONResult{data=nil} "Unauthorized" // @Failure 404 {object} JSONResult{data=nil} "Not Found" @@ -522,12 +524,12 @@ func (h *bookmarksHandler) shareBookmark(c echo.Context) error { return err } - tx, _, err := initContext(ctx) + tx, user, err := initContext(ctx) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } - shared, err := h.service.ShareContent(ctx, tx, req.BookmarkID, req.ExpiresAt) + shared, err := h.service.CreateBookmarkShare(ctx, tx, user.ID, req.BookmarkID, req.ExpiresAt) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } @@ -549,7 +551,7 @@ type updateSharedBookmarkRequest struct { // @Produce json // @Param bookmark-id path string true "Bookmark ID" // @Param request body updateSharedBookmarkRequest true "Update options" -// @Success 200 {object} JSONResult{data=bookmarks.ContentDTO} "Success" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkShareDTO} "Success" // @Failure 400 {object} JSONResult{data=nil} "Bad Request" // @Failure 404 {object} JSONResult{data=nil} "Not Found" // @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" @@ -561,12 +563,12 @@ func (h *bookmarksHandler) updateSharedBookmark(c echo.Context) error { return err } - tx, _, err := initContext(ctx) + tx, user, err := initContext(ctx) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } - content, err := h.service.UpdateSharedContent(ctx, tx, req.BookmarkID, req.ExpiresAt) + content, err := h.service.UpdateSharedContent(ctx, tx, user.ID, req.BookmarkID, req.ExpiresAt) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } @@ -594,12 +596,12 @@ func (h *bookmarksHandler) deleteSharedBookmark(c echo.Context) error { return ErrorResponse(c, http.StatusBadRequest, err) } - tx, _, err := initContext(ctx) + tx, user, err := initContext(ctx) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } - if err := h.service.DeleteSharedContent(ctx, tx, bookmarkID); err != nil { + if err := h.service.DeleteSharedContent(ctx, tx, user.ID, bookmarkID); err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } @@ -618,7 +620,7 @@ type getShareContentRequest struct { // @Accept json // @Produce json // @Param bookmark-id path string true "Bookmark ID" -// @Success 200 {object} JSONResult{data=bookmarks.ContentDTO} "Success" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkContentDTO} "Success" // @Failure 400 {object} JSONResult{data=nil} "Bad Request" // @Failure 404 {object} JSONResult{data=nil} "Not Found" // @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" @@ -635,7 +637,7 @@ func (h *bookmarksHandler) getShareBookmark(c echo.Context) error { return errors.New("tx not found") } - cs, err := h.service.GetShareContent(ctx, tx, req.BookmarkID) + cs, err := h.service.GetBookmarkShareContent(ctx, tx, req.BookmarkID) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } diff --git a/internal/port/httpserver/handler_share_content.go b/internal/port/httpserver/handler_share_content.go index 7da15db..6dd791d 100644 --- a/internal/port/httpserver/handler_share_content.go +++ b/internal/port/httpserver/handler_share_content.go @@ -16,7 +16,7 @@ import ( ) type BookmarkShareService interface { - GetSharedContent(ctx context.Context, tx db.DBTX, sharedID uuid.UUID) (*bookmarks.ContentDTO, error) + GetBookmarkShareContent(ctx context.Context, tx db.DBTX, sharedID uuid.UUID) (*bookmarks.BookmarkContentDTO, error) } // bookmarkServiceImpl implements BookmarkService @@ -50,7 +50,7 @@ type sharedFileRequest struct { // @Accept json // @Produce json // @Param token path string true "Bookmark ID" -// @Success 200 {object} JSONResult{data=bookmarks.ContentDTO} "Success" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkContentDTO} "Success" // @Failure 400 {object} JSONResult{data=nil} "Bad Request" // @Failure 404 {object} JSONResult{data=nil} "Not Found" // @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" @@ -67,7 +67,7 @@ func (h *bookmarkShareHandler) getSharedBookmark(c echo.Context) error { return errors.New("tx not found") } - bookmark, err := h.service.GetSharedContent(ctx, tx, req.Token) + bookmark, err := h.service.GetBookmarkShareContent(ctx, tx, req.Token) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } From e380f38df568485551163868ced115e42cb7e1d6 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Wed, 29 Jan 2025 23:11:04 +0800 Subject: [PATCH 05/11] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20refactor:=20Remov?= =?UTF-8?q?e=20BookmarkWithContentDTO=20and=20update=20references=20to=20B?= =?UTF-8?q?ookmarkDTO=20(main)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed `BookmarkWithContentDTO` struct as it was redundant and superseded by `BookmarkDTO`. - Updated all references from `BookmarkWithContentDTO` to `BookmarkDTO` in service and handler files. - Adjusted the JSON schema in Swagger documentation to reflect the changes. - Modified `BookmarkContentMetadata` by removing the `Image` field and its handling in the `FromReaderContent` method. - Updated `BookmarkDTO` to include a `Content` field of type `BookmarkContentDTO` for nested content representation. - Refactored the `LoadWithContent` method within `BookmarkDTO` to handle loading nested content. - Modified methods in `BookmarkService` to use `BookmarkDTO` for consistency. - Updated API documentation to match the changes in response payloads. --- database/bindata.go | 4 +- docs/swagger/docs.go | 52 +------- docs/swagger/swagger.json | 52 +------- docs/swagger/swagger.yaml | 35 +----- .../core/bookmarks/bookmark_content_model.go | 6 - internal/core/bookmarks/bookmark_model.go | 112 +++++++++--------- internal/core/bookmarks/bookmark_service.go | 10 +- internal/port/httpserver/handler_bookmark.go | 34 +++--- .../port/httpserver/handler_share_content.go | 42 +++---- 9 files changed, 110 insertions(+), 237 deletions(-) diff --git a/database/bindata.go b/database/bindata.go index b40e17f..220a6b4 100644 --- a/database/bindata.go +++ b/database/bindata.go @@ -546,7 +546,7 @@ func _000012_split_bookmark_contentDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000012_split_bookmark_content.down.sql", size: 259, mode: os.FileMode(0644), modTime: time.Unix(1738137401, 0)} + info := bindataFileInfo{name: "000012_split_bookmark_content.down.sql", size: 259, mode: os.FileMode(0644), modTime: time.Unix(1738162357, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x18, 0xd, 0x5e, 0x4f, 0x9, 0xc5, 0x5e, 0x30, 0xd6, 0x3a, 0xd1, 0x4d, 0xdb, 0x8b, 0xfb, 0x31, 0x19, 0xb5, 0x81, 0xb9, 0x82, 0x4f, 0x56, 0x20, 0x2b, 0x5f, 0x9d, 0x23, 0x26, 0xb1, 0xf0, 0xb}} return a, nil } @@ -566,7 +566,7 @@ func _000012_split_bookmark_contentUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 4835, mode: os.FileMode(0644), modTime: time.Unix(1738137330, 0)} + info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 4835, mode: os.FileMode(0644), modTime: time.Unix(1738162357, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x24, 0xf8, 0x64, 0xc3, 0x2d, 0x98, 0x56, 0xc4, 0x5a, 0x95, 0x7c, 0x3e, 0x87, 0xe6, 0x3, 0xdc, 0x20, 0x8c, 0xfc, 0x6b, 0x6a, 0x27, 0xec, 0xdc, 0x57, 0x7f, 0xd8, 0x64, 0x5b, 0x1e, 0xbf, 0x50}} return a, nil } diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index f43e1bf..dd81844 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -3102,7 +3102,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.BookmarkWithContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } } } @@ -5072,9 +5072,6 @@ const docTemplate = `{ "favicon": { "type": "string" }, - "image": { - "type": "string" - }, "published_at": { "type": "string" }, @@ -5086,6 +5083,9 @@ const docTemplate = `{ "bookmarks.BookmarkDTO": { "type": "object", "properties": { + "content": { + "$ref": "#/definitions/bookmarks.BookmarkContentDTO" + }, "content_id": { "type": "string" }, @@ -5167,50 +5167,6 @@ const docTemplate = `{ } } }, - "bookmarks.BookmarkWithContentDTO": { - "type": "object", - "properties": { - "content": { - "$ref": "#/definitions/bookmarks.BookmarkContentDTO" - }, - "content_id": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_archive": { - "type": "boolean" - }, - "is_favorite": { - "type": "boolean" - }, - "is_public": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/definitions/bookmarks.BookmarkMetadata" - }, - "reading_progress": { - "type": "integer" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "updated_at": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, "bookmarks.ContentType": { "type": "string", "enum": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index b192ab8..cd068ef 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -3096,7 +3096,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/bookmarks.BookmarkWithContentDTO" + "$ref": "#/definitions/bookmarks.BookmarkDTO" } } } @@ -5066,9 +5066,6 @@ "favicon": { "type": "string" }, - "image": { - "type": "string" - }, "published_at": { "type": "string" }, @@ -5080,6 +5077,9 @@ "bookmarks.BookmarkDTO": { "type": "object", "properties": { + "content": { + "$ref": "#/definitions/bookmarks.BookmarkContentDTO" + }, "content_id": { "type": "string" }, @@ -5161,50 +5161,6 @@ } } }, - "bookmarks.BookmarkWithContentDTO": { - "type": "object", - "properties": { - "content": { - "$ref": "#/definitions/bookmarks.BookmarkContentDTO" - }, - "content_id": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_archive": { - "type": "boolean" - }, - "is_favorite": { - "type": "boolean" - }, - "is_public": { - "type": "boolean" - }, - "metadata": { - "$ref": "#/definitions/bookmarks.BookmarkMetadata" - }, - "reading_progress": { - "type": "integer" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "updated_at": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, "bookmarks.ContentType": { "type": "string", "enum": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index fc45ac8..ef946b8 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -244,8 +244,6 @@ definitions: type: string favicon: type: string - image: - type: string published_at: type: string site_name: @@ -253,6 +251,8 @@ definitions: type: object bookmarks.BookmarkDTO: properties: + content: + $ref: '#/definitions/bookmarks.BookmarkContentDTO' content_id: type: string created_at: @@ -306,35 +306,6 @@ definitions: user_id: type: string type: object - bookmarks.BookmarkWithContentDTO: - properties: - content: - $ref: '#/definitions/bookmarks.BookmarkContentDTO' - content_id: - type: string - created_at: - type: string - id: - type: string - is_archive: - type: boolean - is_favorite: - type: boolean - is_public: - type: boolean - metadata: - $ref: '#/definitions/bookmarks.BookmarkMetadata' - reading_progress: - type: integer - tags: - items: - type: string - type: array - updated_at: - type: string - user_id: - type: string - type: object bookmarks.ContentType: enum: - bookmark @@ -2356,7 +2327,7 @@ paths: - $ref: '#/definitions/httpserver.JSONResult' - properties: data: - $ref: '#/definitions/bookmarks.BookmarkWithContentDTO' + $ref: '#/definitions/bookmarks.BookmarkDTO' type: object "400": description: Bad Request diff --git a/internal/core/bookmarks/bookmark_content_model.go b/internal/core/bookmarks/bookmark_content_model.go index 106a868..f436e8c 100644 --- a/internal/core/bookmarks/bookmark_content_model.go +++ b/internal/core/bookmarks/bookmark_content_model.go @@ -30,7 +30,6 @@ type BookmarkContentMetadata struct { SiteName string `json:"site_name,omitempty"` Domain string `json:"domain,omitempty"` - Image string `json:"image,omitempty"` Favicon string `json:"favicon"` Cover string `json:"cover,omitempty"` } @@ -156,11 +155,6 @@ func (c *BookmarkContentDTO) FromReaderContent(article *webreader.Content) { c.Metadata.Cover = article.Cover c.Metadata.Favicon = article.Favicon - if article.Cover != "" { - c.Metadata.Image = article.Cover - } else { - c.Metadata.Image = article.Favicon - } if article.PublishedTime != nil { c.Metadata.PublishedAt = *article.PublishedTime diff --git a/internal/core/bookmarks/bookmark_model.go b/internal/core/bookmarks/bookmark_model.go index 2c9b080..4971102 100644 --- a/internal/core/bookmarks/bookmark_model.go +++ b/internal/core/bookmarks/bookmark_model.go @@ -26,17 +26,18 @@ type BookmarkMetadata struct { } type BookmarkDTO struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` - ContentID uuid.UUID `json:"content_id"` - IsFavorite bool `json:"is_favorite"` - IsArchive bool `json:"is_archive"` - IsPublic bool `json:"is_public"` - ReadingProgress int `json:"reading_progress"` - Metadata BookmarkMetadata `json:"metadata"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Tags []string `json:"tags"` + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + ContentID uuid.UUID `json:"content_id"` + IsFavorite bool `json:"is_favorite"` + IsArchive bool `json:"is_archive"` + IsPublic bool `json:"is_public"` + ReadingProgress int `json:"reading_progress"` + Metadata BookmarkMetadata `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Tags []string `json:"tags"` + Content BookmarkContentDTO `json:"content"` } func (b *BookmarkDTO) Load(dbo *db.Bookmark) { @@ -55,6 +56,48 @@ func (b *BookmarkDTO) Load(dbo *db.Bookmark) { } } +func (b *BookmarkDTO) LoadWithContent(dbo *db.GetBookmarkWithContentRow) { + // Load bookmark data + b.ID = dbo.ID + b.UserID = dbo.UserID.Bytes + b.ContentID = dbo.ID_2 + b.IsFavorite = dbo.IsFavorite.Bool + b.IsArchive = dbo.IsArchive.Bool + b.IsPublic = dbo.IsPublic.Bool + b.ReadingProgress = int(dbo.ReadingProgress.Int32) + b.CreatedAt = dbo.CreatedAt.Time + b.UpdatedAt = dbo.UpdatedAt.Time + + // Load bookmark metadata + if dbo.Metadata != nil { + b.Metadata = loadBookmarkMetadata(dbo.Metadata) + } + + // Load tags from the aggregated tags field + b.Tags = loadBookmarkTags(dbo.Tags) + + // Load content data + b.Content.ID = dbo.ID_2 + b.Content.Type = ContentType(dbo.Type) + b.Content.URL = dbo.Url + b.Content.UserID = dbo.UserID_2.Bytes + b.Content.Title = dbo.Title.String + b.Content.Description = dbo.Description.String + b.Content.Domain = dbo.Domain.String + b.Content.S3Key = dbo.S3Key.String + b.Content.Summary = dbo.Summary.String + b.Content.Content = dbo.Content.String + b.Content.Html = dbo.Html.String + b.Content.Tags = dbo.Tags + b.Content.CreatedAt = dbo.CreatedAt_2.Time + b.Content.UpdatedAt = dbo.UpdatedAt_2.Time + + // Load content metadata + if dbo.Metadata_2 != nil { + b.Content.Metadata = loadBookmarkContentMetadata(dbo.Metadata_2) + } +} + func (b *BookmarkDTO) Dump() db.CreateBookmarkParams { return db.CreateBookmarkParams{ UserID: pgtype.UUID{Bytes: b.UserID, Valid: b.UserID != uuid.Nil}, @@ -103,53 +146,6 @@ func (b *BookmarkDTO) DumpToUpdateParams() db.UpdateBookmarkParams { } } -type BookmarkWithContentDTO struct { - BookmarkDTO - Content BookmarkContentDTO `json:"content"` -} - -func (d *BookmarkWithContentDTO) Load(dbo *db.GetBookmarkWithContentRow) { - // Load bookmark data - d.ID = dbo.ID - d.UserID = dbo.UserID.Bytes - d.ContentID = dbo.ID_2 - d.IsFavorite = dbo.IsFavorite.Bool - d.IsArchive = dbo.IsArchive.Bool - d.IsPublic = dbo.IsPublic.Bool - d.ReadingProgress = int(dbo.ReadingProgress.Int32) - d.CreatedAt = dbo.CreatedAt.Time - d.UpdatedAt = dbo.UpdatedAt.Time - - // Load bookmark metadata - if dbo.Metadata != nil { - d.Metadata = loadBookmarkMetadata(dbo.Metadata) - } - - // Load tags from the aggregated tags field - d.Tags = loadBookmarkTags(dbo.Tags) - - // Load content data - d.Content.ID = dbo.ID_2 - d.Content.Type = ContentType(dbo.Type) - d.Content.URL = dbo.Url - d.Content.UserID = dbo.UserID_2.Bytes - d.Content.Title = dbo.Title.String - d.Content.Description = dbo.Description.String - d.Content.Domain = dbo.Domain.String - d.Content.S3Key = dbo.S3Key.String - d.Content.Summary = dbo.Summary.String - d.Content.Content = dbo.Content.String - d.Content.Html = dbo.Html.String - d.Content.Tags = dbo.Tags - d.Content.CreatedAt = dbo.CreatedAt_2.Time - d.Content.UpdatedAt = dbo.UpdatedAt_2.Time - - // Load content metadata - if dbo.Metadata_2 != nil { - d.Content.Metadata = loadBookmarkContentMetadata(dbo.Metadata_2) - } -} - func loadListBookmarks(dbos []db.ListBookmarksRow) []BookmarkDTO { bookmarks := make([]BookmarkDTO, len(dbos)) for i, dbo := range dbos { diff --git a/internal/core/bookmarks/bookmark_service.go b/internal/core/bookmarks/bookmark_service.go index 3f22211..a7048c7 100644 --- a/internal/core/bookmarks/bookmark_service.go +++ b/internal/core/bookmarks/bookmark_service.go @@ -61,7 +61,7 @@ func (s *Service) CreateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UU createBookmarkContentParams := dto.Dump() // Set UserID to Nil as this content can be shared createBookmarkContentParams.UserID = pgtype.UUID{Bytes: uuid.Nil, Valid: false} - content, err = s.dao.CreateBookmarkContent(ctx, tx, dto.Dump()) + content, err = s.dao.CreateBookmarkContent(ctx, tx, createBookmarkContentParams) if err != nil { return nil, fmt.Errorf("failed to create new bookmark content: %w", err) } @@ -83,7 +83,7 @@ func (s *Service) CreateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UU return &bookmarkDTO, nil } -func (s *Service) GetBookmarkWithContent(ctx context.Context, tx db.DBTX, userId, id uuid.UUID) (*BookmarkWithContentDTO, error) { +func (s *Service) GetBookmarkWithContent(ctx context.Context, tx db.DBTX, userId, id uuid.UUID) (*BookmarkDTO, error) { bookmark, err := s.dao.GetBookmarkWithContent(ctx, tx, db.GetBookmarkWithContentParams{ ID: id, UserID: pgtype.UUID{Bytes: userId, Valid: true}, @@ -91,8 +91,8 @@ func (s *Service) GetBookmarkWithContent(ctx context.Context, tx db.DBTX, userId if err != nil { return nil, err } - var result BookmarkWithContentDTO - result.Load(&bookmark) + var result BookmarkDTO + result.LoadWithContent(&bookmark) return &result, nil } @@ -199,5 +199,5 @@ func (s *Service) UpdateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UU if _, err = s.UpdateBookmarkContent(ctx, tx, &updateContent); err != nil { return nil, err } - return &bookmark.BookmarkDTO, nil + return bookmark, nil } diff --git a/internal/port/httpserver/handler_bookmark.go b/internal/port/httpserver/handler_bookmark.go index c956ae8..f3a4770 100644 --- a/internal/port/httpserver/handler_bookmark.go +++ b/internal/port/httpserver/handler_bookmark.go @@ -22,7 +22,7 @@ import ( type BookmarkService interface { ListBookmarks(ctx context.Context, tx db.DBTX, userID uuid.UUID, filters []string, query string, limit, offset int32) ([]bookmarks.BookmarkDTO, int64, error) CreateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, dto *bookmarks.BookmarkContentDTO) (*bookmarks.BookmarkDTO, error) - GetBookmarkWithContent(ctx context.Context, tx db.DBTX, userId, id uuid.UUID) (*bookmarks.BookmarkWithContentDTO, error) + GetBookmarkWithContent(ctx context.Context, tx db.DBTX, userId, id uuid.UUID) (*bookmarks.BookmarkDTO, error) UpdateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, id uuid.UUID, content *bookmarks.BookmarkContentDTO) (*bookmarks.BookmarkDTO, error) DeleteBookmark(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) error DeleteBookmarksByUser(ctx context.Context, tx db.DBTX, userID uuid.UUID) error @@ -270,7 +270,7 @@ type getBookmarkRequest struct { // @Accept json // @Produce json // @Param bookmark-id path string true "Bookmark ID" -// @Success 200 {object} JSONResult{data=bookmarks.BookmarkWithContentDTO} "Success" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkDTO} "Success" // @Failure 400 {object} JSONResult{data=nil} "Bad Request" // @Failure 401 {object} JSONResult{data=nil} "Unauthorized" // @Failure 404 {object} JSONResult{data=nil} "Not Found" @@ -508,13 +508,13 @@ type shareBookmarkRequest struct { // @Tags Bookmarks // @Accept json // @Produce json -// @Param bookmark-id path string true "Bookmark ID" -// @Param request body shareBookmarkRequest true "Share options" +// @Param bookmark-id path string true "Bookmark ID" +// @Param request body shareBookmarkRequest true "Share options" // @Success 200 {object} JSONResult{data=bookmarks.BookmarkShareDTO} "Success" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 401 {object} JSONResult{data=nil} "Unauthorized" -// @Failure 404 {object} JSONResult{data=nil} "Not Found" -// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 401 {object} JSONResult{data=nil} "Unauthorized" +// @Failure 404 {object} JSONResult{data=nil} "Not Found" +// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" // @Router /bookmarks/{bookmark-id}/share [post] func (h *bookmarksHandler) shareBookmark(c echo.Context) error { ctx := c.Request().Context() @@ -549,12 +549,12 @@ type updateSharedBookmarkRequest struct { // @Tags Bookmarks // @Accept json // @Produce json -// @Param bookmark-id path string true "Bookmark ID" -// @Param request body updateSharedBookmarkRequest true "Update options" +// @Param bookmark-id path string true "Bookmark ID" +// @Param request body updateSharedBookmarkRequest true "Update options" // @Success 200 {object} JSONResult{data=bookmarks.BookmarkShareDTO} "Success" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 404 {object} JSONResult{data=nil} "Not Found" -// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 404 {object} JSONResult{data=nil} "Not Found" +// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" // @Router /bookmarks/{bookmark-id}/share [put] func (h *bookmarksHandler) updateSharedBookmark(c echo.Context) error { ctx := c.Request().Context() @@ -619,11 +619,11 @@ type getShareContentRequest struct { // @Tags Bookmarks // @Accept json // @Produce json -// @Param bookmark-id path string true "Bookmark ID" +// @Param bookmark-id path string true "Bookmark ID" // @Success 200 {object} JSONResult{data=bookmarks.BookmarkContentDTO} "Success" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 404 {object} JSONResult{data=nil} "Not Found" -// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 404 {object} JSONResult{data=nil} "Not Found" +// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" // @Router /bookmarks/{bookmark-id}/share [get] func (h *bookmarksHandler) getShareBookmark(c echo.Context) error { ctx := c.Request().Context() diff --git a/internal/port/httpserver/handler_share_content.go b/internal/port/httpserver/handler_share_content.go index 6dd791d..b4e4ce3 100644 --- a/internal/port/httpserver/handler_share_content.go +++ b/internal/port/httpserver/handler_share_content.go @@ -49,11 +49,11 @@ type sharedFileRequest struct { // @Tags Bookmarks // @Accept json // @Produce json -// @Param token path string true "Bookmark ID" +// @Param token path string true "Bookmark ID" // @Success 200 {object} JSONResult{data=bookmarks.BookmarkContentDTO} "Success" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 404 {object} JSONResult{data=nil} "Not Found" -// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 404 {object} JSONResult{data=nil} "Not Found" +// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" // @Router /bookmarks/{bookmark-id}/share [get] func (h *bookmarkShareHandler) getSharedBookmark(c echo.Context) error { ctx := c.Request().Context() @@ -82,16 +82,16 @@ func (h *bookmarkShareHandler) getSharedBookmark(c echo.Context) error { return JsonResponse(c, http.StatusOK, bookmark) } -// @Summary Redirect to file -// @Description Get a redirect to the file's presigned URL -// @Tags files -// @Produce json -// @Param id path string true "File ID" -// @Success 302 {string} string "Redirect to file URL" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 401 {object} JSONResult{data=nil} "Unauthorized" -// @Failure 404 {object} JSONResult{data=nil} "Object not found" -// @Router /files/{id} [get] +// @Summary Redirect to file +// @Description Get a redirect to the file's presigned URL +// @Tags files +// @Produce json +// @Param id path string true "File ID" +// @Success 302 {string} string "Redirect to file URL" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 401 {object} JSONResult{data=nil} "Unauthorized" +// @Failure 404 {object} JSONResult{data=nil} "Object not found" +// @Router /files/{id} [get] func (h *bookmarkShareHandler) redirectToFile(c echo.Context) error { ctx := c.Request().Context() req := new(sharedFileRequest) @@ -117,13 +117,13 @@ func (h *bookmarkShareHandler) redirectToFile(c echo.Context) error { return c.Redirect(http.StatusFound, presignedURL) } -// @Summary Get file metadata -// @Description Get metadata for a shared file without downloading it -// @Tags files -// @Param key path string true "File key" -// @Success 200 "Success" -// @Failure 404 {object} JSONResult{data=nil} "File not found" -// @Router /shared/files/{key} [head] +// @Summary Get file metadata +// @Description Get metadata for a shared file without downloading it +// @Tags files +// @Param key path string true "File key" +// @Success 200 "Success" +// @Failure 404 {object} JSONResult{data=nil} "File not found" +// @Router /shared/files/{key} [head] func (h *bookmarkShareHandler) getFileMetadata(c echo.Context) error { ctx := c.Request().Context() req := new(sharedFileRequest) From e991da864ea5f9622e17e87af3f0c723a5ea4154 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Wed, 29 Jan 2025 23:29:22 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Rename?= =?UTF-8?q?=20ListBookmarkDomains=20to=20ListBookmarkDomainsByUser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renamed the SQL query `ListBookmarkDomains` to `ListBookmarkDomainsByUser` for clarity and consistency. - Updated the corresponding Go function in `bookmarks.sql.go` to reflect the new name. - Modified the `Service` struct in `service.go` to use the renamed DAO method `ListBookmarkDomainsByUser`. - Adjusted the method call in `ListDomains` function to use the updated DAO method. - Updated the `DAO` interface in `dao.go` to include the renamed method signature. --- database/queries/bookmarks.sql | 2 +- internal/core/bookmarks/dao.go | 30 ++++++++++++---- internal/core/bookmarks/service.go | 4 +-- internal/pkg/db/bookmarks.sql.go | 12 +++---- .../port/httpserver/handler_share_content.go | 34 +++++++++---------- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/database/queries/bookmarks.sql b/database/queries/bookmarks.sql index 9522a4c..2091140 100644 --- a/database/queries/bookmarks.sql +++ b/database/queries/bookmarks.sql @@ -138,7 +138,7 @@ SET updated_at = CURRENT_TIMESTAMP WHERE user_id = $1; --- name: ListBookmarkDomains :many +-- name: ListBookmarkDomainsByUser :many SELECT bc.domain, count(*) as cnt FROM bookmarks b JOIN bookmark_content bc ON b.content_id = bc.id diff --git a/internal/core/bookmarks/dao.go b/internal/core/bookmarks/dao.go index 7929ed1..e16c688 100644 --- a/internal/core/bookmarks/dao.go +++ b/internal/core/bookmarks/dao.go @@ -10,15 +10,31 @@ import ( // DAO provides data access operations for bookmarks type DAO interface { - CreateBookmark(ctx context.Context, tx db.DBTX, arg db.CreateBookmarkParams) (db.Bookmark, error) + IsBookmarkContentExistByURL(ctx context.Context, db db.DBTX, url string) (bool, error) + CreateBookmarkContent(ctx context.Context, db db.DBTX, arg db.CreateBookmarkContentParams) (db.BookmarkContent, error) + GetBookmarkContentByID(ctx context.Context, db db.DBTX, id uuid.UUID) (db.BookmarkContent, error) + GetBookmarkContentByURL(ctx context.Context, db db.DBTX, arg db.GetBookmarkContentByURLParams) (db.BookmarkContent, error) + UpdateBookmarkContent(ctx context.Context, db db.DBTX, arg db.UpdateBookmarkContentParams) (db.BookmarkContent, error) + + CreateBookmark(ctx context.Context, db db.DBTX, arg db.CreateBookmarkParams) (db.Bookmark, error) + GetBookmarkWithContent(ctx context.Context, db db.DBTX, arg db.GetBookmarkWithContentParams) (db.GetBookmarkWithContentRow, error) + ListBookmarks(ctx context.Context, db db.DBTX, arg db.ListBookmarksParams) ([]db.ListBookmarksRow, error) + SearchBookmarks(ctx context.Context, db db.DBTX, arg db.SearchBookmarksParams) ([]db.SearchBookmarksRow, error) DeleteBookmark(ctx context.Context, db db.DBTX, arg db.DeleteBookmarkParams) error DeleteBookmarksByUser(ctx context.Context, db db.DBTX, userID pgtype.UUID) error - ListBookmarks(ctx context.Context, db db.DBTX, arg db.ListBookmarksParams) ([]db.ListBookmarksRow, error) - UpdateBookmark(ctx context.Context, db db.DBTX, arg db.UpdateBookmarkParams) (db.Bookmark, error) - CreateShareContent(ctx context.Context, db db.DBTX, arg db.CreateShareContentParams) (db.ContentShare, error) + CreateBookmarkShare(ctx context.Context, db db.DBTX, arg db.CreateBookmarkShareParams) (db.BookmarkShare, error) + GetBookmarkShareContent(ctx context.Context, db db.DBTX, id uuid.UUID) (db.BookmarkContent, error) + GetBookmarkShare(ctx context.Context, db db.DBTX, arg db.GetBookmarkShareParams) (db.BookmarkShare, error) + UpdateBookmarkShareByBookmarkId(ctx context.Context, db db.DBTX, arg db.UpdateBookmarkShareByBookmarkIdParams) (db.BookmarkShare, error) DeleteShareContent(ctx context.Context, db db.DBTX, arg db.DeleteShareContentParams) error - GetSharedContent(ctx context.Context, db db.DBTX, id uuid.UUID) (db.Content, error) - GetShareContent(ctx context.Context, db db.DBTX, arg db.GetShareContentParams) (db.ContentShare, error) - UpdateShareContent(ctx context.Context, db db.DBTX, arg db.UpdateShareContentParams) (db.ContentShare, error) + + ListExistingBookmarkTagsByTags(ctx context.Context, db db.DBTX, arg db.ListExistingBookmarkTagsByTagsParams) ([]string, error) + CreateBookmarkTag(ctx context.Context, db db.DBTX, arg db.CreateBookmarkTagParams) (db.BookmarkTag, error) + ListBookmarkTagsByBookmarkId(ctx context.Context, db db.DBTX, bookmarkID uuid.UUID) ([]string, error) + LinkBookmarkWithTags(ctx context.Context, db db.DBTX, arg db.LinkBookmarkWithTagsParams) error + UnLinkBookmarkWithTags(ctx context.Context, db db.DBTX, arg db.UnLinkBookmarkWithTagsParams) error + + ListBookmarkTagsByUser(ctx context.Context, db db.DBTX, userID uuid.UUID) ([]db.ListBookmarkTagsByUserRow, error) + ListBookmarkDomainsByUser(ctx context.Context, db db.DBTX, userID pgtype.UUID) ([]db.ListBookmarkDomainsByUserRow, error) } diff --git a/internal/core/bookmarks/service.go b/internal/core/bookmarks/service.go index 3097400..8ac93ae 100644 --- a/internal/core/bookmarks/service.go +++ b/internal/core/bookmarks/service.go @@ -11,7 +11,7 @@ import ( ) type Service struct { - dao *db.Queries + dao DAO llm *llms.LLM } @@ -49,7 +49,7 @@ type DomainDTO struct { } func (s *Service) ListDomains(ctx context.Context, tx db.DBTX, userID uuid.UUID) ([]DomainDTO, error) { - domains, err := s.dao.ListBookmarkDomains(ctx, tx, pgtype.UUID{Bytes: userID, Valid: true}) + domains, err := s.dao.ListBookmarkDomainsByUser(ctx, tx, pgtype.UUID{Bytes: userID, Valid: true}) if err != nil { return nil, fmt.Errorf("failed to list domains for user '%s': %w", userID.String(), err) } diff --git a/internal/pkg/db/bookmarks.sql.go b/internal/pkg/db/bookmarks.sql.go index 63cce7c..06398f0 100644 --- a/internal/pkg/db/bookmarks.sql.go +++ b/internal/pkg/db/bookmarks.sql.go @@ -191,7 +191,7 @@ func (q *Queries) IsBookmarkExistWithURL(ctx context.Context, db DBTX, arg IsBoo return exists, err } -const listBookmarkDomains = `-- name: ListBookmarkDomains :many +const listBookmarkDomainsByUser = `-- name: ListBookmarkDomainsByUser :many SELECT bc.domain, count(*) as cnt FROM bookmarks b JOIN bookmark_content bc ON b.content_id = bc.id @@ -201,20 +201,20 @@ GROUP BY bc.domain ORDER BY cnt DESC, domain ASC ` -type ListBookmarkDomainsRow struct { +type ListBookmarkDomainsByUserRow struct { Domain pgtype.Text Cnt int64 } -func (q *Queries) ListBookmarkDomains(ctx context.Context, db DBTX, userID pgtype.UUID) ([]ListBookmarkDomainsRow, error) { - rows, err := db.Query(ctx, listBookmarkDomains, userID) +func (q *Queries) ListBookmarkDomainsByUser(ctx context.Context, db DBTX, userID pgtype.UUID) ([]ListBookmarkDomainsByUserRow, error) { + rows, err := db.Query(ctx, listBookmarkDomainsByUser, userID) if err != nil { return nil, err } defer rows.Close() - var items []ListBookmarkDomainsRow + var items []ListBookmarkDomainsByUserRow for rows.Next() { - var i ListBookmarkDomainsRow + var i ListBookmarkDomainsByUserRow if err := rows.Scan(&i.Domain, &i.Cnt); err != nil { return nil, err } diff --git a/internal/port/httpserver/handler_share_content.go b/internal/port/httpserver/handler_share_content.go index b4e4ce3..7f5014d 100644 --- a/internal/port/httpserver/handler_share_content.go +++ b/internal/port/httpserver/handler_share_content.go @@ -82,16 +82,16 @@ func (h *bookmarkShareHandler) getSharedBookmark(c echo.Context) error { return JsonResponse(c, http.StatusOK, bookmark) } -// @Summary Redirect to file -// @Description Get a redirect to the file's presigned URL -// @Tags files -// @Produce json -// @Param id path string true "File ID" -// @Success 302 {string} string "Redirect to file URL" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 401 {object} JSONResult{data=nil} "Unauthorized" -// @Failure 404 {object} JSONResult{data=nil} "Object not found" -// @Router /files/{id} [get] +// @Summary Redirect to file +// @Description Get a redirect to the file's presigned URL +// @Tags files +// @Produce json +// @Param id path string true "File ID" +// @Success 302 {string} string "Redirect to file URL" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 401 {object} JSONResult{data=nil} "Unauthorized" +// @Failure 404 {object} JSONResult{data=nil} "Object not found" +// @Router /files/{id} [get] func (h *bookmarkShareHandler) redirectToFile(c echo.Context) error { ctx := c.Request().Context() req := new(sharedFileRequest) @@ -117,13 +117,13 @@ func (h *bookmarkShareHandler) redirectToFile(c echo.Context) error { return c.Redirect(http.StatusFound, presignedURL) } -// @Summary Get file metadata -// @Description Get metadata for a shared file without downloading it -// @Tags files -// @Param key path string true "File key" -// @Success 200 "Success" -// @Failure 404 {object} JSONResult{data=nil} "File not found" -// @Router /shared/files/{key} [head] +// @Summary Get file metadata +// @Description Get metadata for a shared file without downloading it +// @Tags files +// @Param key path string true "File key" +// @Success 200 "Success" +// @Failure 404 {object} JSONResult{data=nil} "File not found" +// @Router /shared/files/{key} [head] func (h *bookmarkShareHandler) getFileMetadata(c echo.Context) error { ctx := c.Request().Context() req := new(sharedFileRequest) From 50990ceed46b9b82c9d5f98c3eca6811aebc2cd2 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Thu, 30 Jan 2025 10:48:05 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E2=9C=A8=20refactor:=20separate=20bookma?= =?UTF-8?q?rk=20content=20from=20bookmark=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduces `BookmarkContent` to encapsulate content details like title, URL, summary, and metadata. - Updates `Bookmark` object to reference `BookmarkContent` via `content_id`. - Modifies `BookmarkMetadata` to include reading progress, last read date, highlights, and share information. - Refactors components `ArticleReader`, `BookmarkList`, and `SharedArticleReader` to access content details through `bookmark.content`. - Adjusts the API layer in `bookmarks.ts` to align with the new data structure. - Removes unused `bookmarks.ts` file. --- web/src/api/bookmarks.ts | 0 .../components/bookmarks/bookmark-content.tsx | 14 ++++--- .../components/bookmarks/bookmarks-list.tsx | 20 +++++----- web/src/components/shared/content.tsx | 14 ++++--- web/src/lib/apis/bookmarks.ts | 37 ++++++++++++------- 5 files changed, 51 insertions(+), 34 deletions(-) delete mode 100644 web/src/api/bookmarks.ts diff --git a/web/src/api/bookmarks.ts b/web/src/api/bookmarks.ts deleted file mode 100644 index e69de29..0000000 diff --git a/web/src/components/bookmarks/bookmark-content.tsx b/web/src/components/bookmarks/bookmark-content.tsx index d073fb5..f6d48da 100644 --- a/web/src/components/bookmarks/bookmark-content.tsx +++ b/web/src/components/bookmarks/bookmark-content.tsx @@ -20,9 +20,11 @@ export const ArticleReader: React.FC = ({ return ( <> {/* Tags */} @@ -38,9 +40,9 @@ export const ArticleReader: React.FC = ({ - {bookmark.summary && ( + {bookmark.content.summary && ( onRegenerateSummary(bookmark.id) @@ -51,7 +53,7 @@ export const ArticleReader: React.FC = ({ {/* Main Content */}
- +
); diff --git a/web/src/components/bookmarks/bookmarks-list.tsx b/web/src/components/bookmarks/bookmarks-list.tsx index d97f973..6a9a9ec 100644 --- a/web/src/components/bookmarks/bookmarks-list.tsx +++ b/web/src/components/bookmarks/bookmarks-list.tsx @@ -51,21 +51,21 @@ export default function BookmarkList({ className="overflow-hidden transition-all hover:shadow-lg hover:-translate-y-1" > - {bookmark.metadata?.image && ( + {bookmark.content.metadata?.cover && (
{bookmark.title}
)} - {bookmark.title} + {bookmark.content.title} - {bookmark.url} + {bookmark.content.url} @@ -87,21 +87,21 @@ export default function BookmarkList({ return (
- {bookmark.metadata?.image && ( + {bookmark.content.metadata?.cover && (
{bookmark.title}
)}

- {bookmark.title} + {bookmark.content.title}

- {bookmark.url} + {bookmark.content.url}

{bookmark.tags?.map((tag) => ( diff --git a/web/src/components/shared/content.tsx b/web/src/components/shared/content.tsx index b0cdccb..27e7654 100644 --- a/web/src/components/shared/content.tsx +++ b/web/src/components/shared/content.tsx @@ -26,9 +26,11 @@ export const SharedArticleReader: React.FC = ({
{/* Tags */} @@ -44,11 +46,13 @@ export const SharedArticleReader: React.FC = ({ - {bookmark.summary && } + {bookmark.content.summary && ( + + )} {/* Main Content */}
- +
diff --git a/web/src/lib/apis/bookmarks.ts b/web/src/lib/apis/bookmarks.ts index f0c814e..b84158a 100644 --- a/web/src/lib/apis/bookmarks.ts +++ b/web/src/lib/apis/bookmarks.ts @@ -10,10 +10,15 @@ export interface Highlight { note?: string; } -export interface Metadata { - tags?: string[]; +export interface BookmarkMetadata { + reading_progress?: number; + last_read_at?: string; + highlights?: Highlight[]; + share?: ShareContent; +} +export interface BookmarkContentMetadata { author?: string; published_at?: string; description?: string; @@ -22,14 +27,9 @@ export interface Metadata { favicon?: string; cover?: string; - image?: string; - - share?: ShareContent; } -export interface Bookmark { - id: string; - userId: string; +export interface BookmarkContent { type: string; url?: string; domain?: string; @@ -37,11 +37,22 @@ export interface Bookmark { description?: string; summary?: string; content?: string; - tags?: string[]; html?: string; - metadata?: Metadata; + metadata?: BookmarkContentMetadata; +} + +export interface Bookmark { + id: string; + userId: string; + content_id: string; + is_favorite: boolean; + is_archive: boolean; + is_public: boolean; + metadata?: BookmarkMetadata; created_at: string; updated_at: string; + content: BookmarkContent; + tags?: string[]; } export interface ListBookmarksResponse { @@ -63,14 +74,14 @@ export interface Domain { interface BookmarkCreateInput { url: string; - metadata?: Metadata; + metadata?: BookmarkContentMetadata; } interface BookmarkUpdateInput { summary?: string; content?: string; html?: string; - metadata?: Metadata; + metadata?: BookmarkContentMetadata; } interface ShareContentUpdateInput { @@ -89,7 +100,7 @@ export interface ShareBookmarkRequest { export interface ShareContent { id: string; - content_id: string; + bookmark_id: string; expires_at?: string; created_at: string; } From d3e1bb7081bd09a01bf98e16c9bdaf50611176e6 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Thu, 30 Jan 2025 21:48:29 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Refactor?= =?UTF-8?q?=20bookmark=20model=20and=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated the database schema and Go code to reflect the new structure of the bookmark model. - Updated the bookmark content service to use the new model and to support fetching content by bookmark ID. - Updated the crawler worker to use the bookmark ID instead of the content ID when enqueuing summarization jobs. - Removed the `reading_progress` field from the bookmark model as it is no longer used. - Updated the database migration scripts to reflect the new schema. - Updated the database queries to use the new schema and to support the new functionality. - Updated the HTTP handlers to use the new model and service. - Fixed the order of tables dropped in the down migration script. - Added a new query to get bookmark content by bookmark ID. - Updated the `CreateBookmarkTag` query to handle conflicts on the `name` and `user_id` columns. - Updated the `ListBookmarks` and `SearchBookmarks` queries to return the bookmark content as well. - Updated the `GetBookmarkWithContent` query to use the `bookmark_id` column in the `bookmark_tags_mapping` table. - Updated the `CreateBookmark` query to remove the `reading_progress` column. - Updated the `UpdateBookmark` query to remove the `reading_progress` column. - Updated the `BookmarkDTO` model to remove the `reading_progress` field. - Updated the `BookmarkContentDTO` model to remove the `reading_progress` field. - Updated the `GetBookmarkContentByBookmarkID` function in the `DAO` interface. - Updated the `FetchContent` function in the `bookmark_content_service` to use the bookmark ID instead of the content ID. - Updated the `SummarierContent` function in the `bookmark_content_service` to use the bookmark ID instead of the content ID. - Updated the `getBookmark` handler to use the new `GetBookmarkWithContent` function. --- database/bindata.go | 16 +- .../000012_split_bookmark_content.down.sql | 4 +- .../000012_split_bookmark_content.up.sql | 9 +- database/queries/bookmark_content.sql | 6 + database/queries/bookmark_tags.sql | 2 + database/queries/bookmarks.sql | 25 +- .../bookmarks/bookmark_content_service.go | 17 +- internal/core/bookmarks/bookmark_model.go | 140 +++-------- internal/core/bookmarks/dao.go | 1 + internal/core/queue/crawler_worker.go | 4 +- internal/pkg/db/bookmark_content.sql.go | 30 +++ internal/pkg/db/bookmark_tags.sql.go | 2 + internal/pkg/db/bookmarks.sql.go | 229 ++++++++---------- internal/pkg/db/models.go | 19 +- internal/port/httpserver/handler_bookmark.go | 2 +- 15 files changed, 232 insertions(+), 274 deletions(-) diff --git a/database/bindata.go b/database/bindata.go index 220a6b4..c540c3b 100644 --- a/database/bindata.go +++ b/database/bindata.go @@ -22,8 +22,8 @@ // 000010_share_content.up.sql (785B) // 000011_create_s3_resources_mapping_table.down.sql (264B) // 000011_create_s3_resources_mapping_table.up.sql (1.401kB) -// 000012_split_bookmark_content.down.sql (259B) -// 000012_split_bookmark_content.up.sql (4.835kB) +// 000012_split_bookmark_content.down.sql (243B) +// 000012_split_bookmark_content.up.sql (4.823kB) package migrations @@ -531,7 +531,7 @@ func _000011_create_s3_resources_mapping_tableUpSql() (*asset, error) { return a, nil } -var __000012_split_bookmark_contentDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\xcd\x31\x0a\x83\x40\x10\x85\xe1\xde\x53\xbc\x0b\x78\x02\xab\x04\x0d\x08\x81\x84\x68\x91\x4e\x46\xf7\xa1\xa2\xce\x2c\xb3\x4b\xce\x9f\x26\xbd\xa4\xff\xde\xfb\xcb\x12\xb5\x5b\x44\x96\x71\x67\xc2\xaa\x70\x7e\xe8\x89\x30\x0f\x74\x64\xc3\x22\x1a\x76\x22\x30\x52\x03\x75\x5a\x99\x8a\xfa\xf5\x78\xa2\xbf\x5c\xef\x0d\xda\x1b\x9a\x77\xdb\xf5\x1d\x46\xb3\xed\x10\xdf\x86\xc9\x34\x53\xf3\x90\x65\x4e\xc3\x21\x31\xae\x3a\x57\x7f\x6c\xce\x6c\x5a\xc4\x79\x82\x4e\x4f\x7e\xc1\xaa\xf8\x06\x00\x00\xff\xff\x2a\x0e\xfa\x94\x03\x01\x00\x00") +var __000012_split_bookmark_contentDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\xca\x41\x8a\x83\x40\x10\x05\xd0\xbd\xa7\xf8\x17\xf0\x04\xae\x66\xd0\x80\x10\x48\x88\x2e\xb2\x93\xd2\xfe\xa8\xa8\x55\x4d\x75\x93\xf3\xe7\x08\x9d\xf5\x7b\x75\x8d\xd6\x2d\x22\xcb\x7c\x32\x61\x57\x38\x3f\xf4\x44\x98\x07\x3a\xb2\x61\x13\x0d\x27\x11\x18\xa9\x81\xba\xec\x4c\x55\xfb\x7a\x3c\x31\xfe\xfd\xdf\x3b\xf4\x37\x74\xef\x7e\x18\x07\xcc\x66\xc7\x25\x7e\x4c\x69\x13\x67\x53\x48\x59\xd6\x34\x5d\x12\xe3\xae\xeb\x2f\xb7\x70\x4a\x3e\x2d\xa6\x99\x9a\x9b\xea\x1b\x00\x00\xff\xff\x9d\x40\xb5\x99\xf3\x00\x00\x00") func _000012_split_bookmark_contentDownSqlBytes() ([]byte, error) { return bindataRead( @@ -546,12 +546,12 @@ func _000012_split_bookmark_contentDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000012_split_bookmark_content.down.sql", size: 259, mode: os.FileMode(0644), modTime: time.Unix(1738162357, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x18, 0xd, 0x5e, 0x4f, 0x9, 0xc5, 0x5e, 0x30, 0xd6, 0x3a, 0xd1, 0x4d, 0xdb, 0x8b, 0xfb, 0x31, 0x19, 0xb5, 0x81, 0xb9, 0x82, 0x4f, 0x56, 0x20, 0x2b, 0x5f, 0x9d, 0x23, 0x26, 0xb1, 0xf0, 0xb}} + info := bindataFileInfo{name: "000012_split_bookmark_content.down.sql", size: 243, mode: os.FileMode(0644), modTime: time.Unix(1738207780, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xce, 0x84, 0xce, 0xbb, 0x83, 0x91, 0x9a, 0xfc, 0xf8, 0x6b, 0x1d, 0x24, 0x13, 0x79, 0xf2, 0x2f, 0x6b, 0x3c, 0x56, 0xd5, 0x9a, 0xa7, 0xcd, 0x9c, 0xb2, 0x67, 0x98, 0xac, 0xe6, 0xfc, 0xde, 0x9a}} return a, nil } -var __000012_split_bookmark_contentUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x57\x5d\x6f\xdb\x36\x14\x7d\xf7\xaf\xb8\x6f\x91\x01\xb9\x29\x5a\xf4\x65\xc5\x1e\x14\x9b\x6e\xb5\x39\x72\x26\x4b\x6b\xba\x61\x10\x68\x93\x96\xb9\x48\xa4\x40\x52\x75\x83\x61\xff\x7d\x20\xf5\x61\x39\x56\x1c\xb5\xc9\xc3\xfa\x28\xf2\xdc\xc3\xfb\x71\xee\x25\x35\x0d\x91\x17\x21\x88\xbc\xab\x05\x82\xb5\x10\x77\x39\x96\x77\xc9\x46\x70\x4d\xb9\x76\x46\x00\x00\x8c\x40\x59\x32\x02\x37\xa1\x7f\xed\x85\x9f\xe1\x57\xf4\x19\x66\x68\xee\xc5\x8b\x08\x52\xca\x13\x89\x39\x11\x79\x62\x30\xce\xd8\xb5\x26\xfa\xbe\xa0\xf0\xbb\x17\x4e\x3f\x7a\xa1\xf3\xee\xf5\x18\x82\x65\x04\x41\xbc\x58\xb8\x30\x99\x54\xbb\x62\x7b\x7a\x5c\xb3\xe0\x42\x41\xb6\x2e\xd0\xa2\x5c\xbb\xc0\x72\x9c\x52\x17\x0a\x41\x36\x58\x69\x17\xbe\x30\x42\x85\x0b\x54\x6f\x5e\x8d\xed\x61\xa5\xcc\x20\x42\xb7\xd1\xf1\x21\x71\xb8\x30\x67\xe8\x1d\x6d\xcf\xa9\xd0\x8a\xca\xa4\x09\xa9\x09\xa3\xb5\xda\xef\x28\xb7\x10\x60\x0a\x78\x99\x65\x2e\xe8\x1d\x53\x50\x7b\x68\x56\xd5\x0e\x4b\x4a\xdc\x0a\xca\xf4\x85\x02\x2e\x74\x8d\x65\x1a\xf6\x2c\xcb\x60\x4d\x33\xc1\x53\xd0\xc2\x9e\x6f\xf8\xaa\xb4\x30\x9d\x51\xeb\x6b\x95\x26\x42\xd5\x46\xb2\x42\x33\xc1\xbb\xab\x22\xc7\xac\x5e\x30\x3e\xd5\xdf\x75\x30\x71\xb8\xb0\x28\xf5\x36\xb9\xa3\xf7\x07\xd4\xea\x2d\x98\xef\xad\x90\xa0\xb4\x90\x8c\xa7\x20\xf1\xbe\x75\x3c\x63\x77\xb4\x9b\xd5\x4e\x16\x2b\xba\x32\xcf\xb1\xec\xf0\x79\xbe\x29\x2e\x95\x58\x53\xd2\xec\x5a\x64\xc3\xd8\x22\xdb\xdc\x70\x30\x59\x26\x62\xcf\x8d\x1b\x39\xd6\x16\xbf\xd3\x79\x76\x00\xdb\xaf\xc6\xc2\x38\xbb\xa7\x6b\x28\x70\x4a\xab\x04\xe1\x54\x75\x75\xf3\xe7\x5f\x6d\x89\x2e\xfe\xf9\xf7\xa2\x52\x8f\xc1\x18\x4b\x93\x8d\x86\x69\x7d\x0f\x84\x6e\x71\x99\x69\xa0\x79\xa1\xef\x01\x4b\x89\x2b\x7f\x73\xaa\x31\xc1\x1a\xc3\x2f\xab\x65\x70\x75\xcc\x57\x05\x24\xa9\x09\x32\xc1\x1a\x22\xff\x1a\xad\x22\xef\xfa\x06\x3e\xf9\xd1\x47\xfb\x09\x7f\x2c\x03\xd4\x4a\xab\x35\x9f\xc6\x61\x88\x82\x28\x69\x2d\x2a\xae\xb2\x20\x2f\xc6\x15\x07\xfe\x6f\x31\x72\x4a\x99\xb9\x8d\x6a\x8d\xe0\xc7\xef\x47\xa3\xc9\x04\x7c\x4e\xe8\x57\xaa\x46\x75\x03\xfb\xc1\x0c\xdd\x02\x23\x5f\x93\x87\x5d\x95\xd8\x76\x5b\x06\xa7\xed\x66\x36\xc6\xef\x07\x30\x98\x0e\xeb\x23\x28\x65\x36\xc8\xbe\x56\x70\x1f\x45\xb5\x35\x88\xa5\x53\xa7\x3e\xa6\xc3\xf6\x20\xb6\x56\x16\x3d\x5c\x10\xaf\xfc\xe0\x03\xa4\x8c\x3b\x2d\xec\x6f\x25\xf8\x3a\x29\xb0\xde\x25\xa2\x50\x75\x11\xae\xae\xdf\xbc\x03\x66\x2a\x01\x82\x9f\xd0\x8c\xac\xde\x75\xa1\x7e\xba\xbc\x24\x62\xa3\x5e\x15\x58\x62\x42\xc9\xfa\xd5\x46\xe4\x66\xa5\xcc\x29\xd7\xd8\xb4\xff\xa5\x25\x61\x3c\xbd\xac\xc2\x48\xec\xf7\x80\x30\xd6\xf9\x9b\x77\x89\xa2\x58\x6e\x76\x67\x22\x31\x28\x87\x11\xb7\x9a\x40\x6e\x77\xf0\xb8\x4d\x73\xbb\x4d\x33\xb9\x6d\xcb\x8c\x2b\xe9\x3a\x77\xf4\x3e\xd9\x32\x9a\x11\xf8\x19\x2e\x18\xb9\x38\x24\x38\x0a\xfd\x0f\x1f\x50\x58\xeb\xbe\x47\x39\x87\x7e\xb8\x42\xf3\x65\x88\x20\xbe\x99\x19\xc3\x3e\x5f\xe7\xcb\x10\x90\x37\xfd\x08\xe1\xf2\x13\xa0\x5b\x34\x8d\x23\x04\xf3\x38\x98\x46\xfe\x32\x68\x8e\x38\x30\x26\x1b\x91\x95\x39\x77\x4c\x2d\x4c\xaa\x39\xdd\xb7\x9c\x0a\x34\x5e\x67\x74\x34\x0b\x97\x37\xf5\xe5\xe6\xcf\x01\xdd\xfa\xab\x68\x75\x00\x1d\xc2\x38\xba\xfe\x14\x7c\xf7\xc5\xd7\xdc\x2e\x71\xec\xcf\x20\x44\x73\x14\xa2\x60\x8a\x56\x76\x5d\x39\x06\x39\x36\xa1\xcf\xd0\x02\x45\x08\xa6\xde\x6a\xea\xcd\x90\xdb\x9d\xab\x7d\xd6\x27\x5a\x67\xa4\x3e\x8f\xa9\x64\x8b\xbf\x08\xc9\x34\x85\xab\xe5\x72\x81\xbc\xa0\x75\x71\xee\x2d\x56\xa8\x85\x19\x85\xb0\x2f\x4f\xa1\x8a\x72\x9d\xb1\xcd\x39\x90\xa4\x98\x30\x9e\x26\x85\x14\xa9\xa4\x4a\x81\x1f\x44\xc8\x48\xa0\xc1\xbe\x76\x7f\xd4\xb1\x5b\x0f\xd7\x47\x9b\x4e\x25\xb6\xbc\xfd\x73\x48\x39\x75\xed\x5d\x18\x36\x89\x3a\x95\xeb\xa7\xe9\xd4\xf6\x3c\x4f\x53\xda\x47\x69\x6a\xc0\x79\x96\xba\xf4\x8f\x92\x54\xfb\xe7\x39\xfa\x86\xaa\x1a\x34\x4d\xcf\xcf\x13\x35\x68\x90\xa8\xe7\x4e\x90\xb6\xcf\xec\x1b\xa3\x9a\x20\xfd\x0f\x64\x0b\xf8\xee\x29\xc1\x71\xfe\xc8\xf3\xf8\xf4\x89\xda\xca\xf6\x1b\xa7\xc9\xff\xae\xbb\x8e\x1f\x35\x8d\xb0\x4c\x2a\xc6\x4f\x35\x9e\x4d\x77\x62\xb3\xd6\xbd\x37\xcc\xaa\x63\x09\x9e\xbc\x8f\x2c\xc1\xa0\xcb\xc8\x56\xf6\x19\x3a\x9a\x4c\xa0\xab\xa4\xe6\x6e\xb3\xb4\x92\x66\xf6\xb6\x57\x3b\x56\x9c\x11\x56\x92\xe3\xa2\x60\x3c\xad\xf4\xd5\x6e\x35\x8a\xe8\xb9\x18\x94\x73\x46\x0a\x1a\xa7\xe7\x6c\xab\x34\xfe\x58\x52\x02\x4b\xd6\xe9\x38\xa7\x93\x25\xb7\x8e\x78\xa0\xac\xea\x64\x27\x75\x9a\x1e\x6a\xa1\x2d\x46\x4d\x3a\x4c\x6a\x0d\xe9\x60\xc9\x35\x16\xcf\x1a\x61\xfd\x92\xb2\x3f\xac\xcf\x7f\xd2\x0c\x99\x46\xee\xcb\x28\x96\x7e\x2d\x98\xa4\xea\xac\x4a\x8e\x7e\xdd\x07\xeb\xf4\x05\xe4\x39\x7c\xc0\x19\x29\x76\x72\x31\x1e\x9d\xa8\xf1\x58\x8e\xb6\x50\x49\x93\xef\xae\x42\xec\x4e\x33\x34\xcf\xfe\xde\x54\x1c\x9d\xb7\xe4\x29\x4d\xd7\xa5\xf7\xa3\xfa\x89\x5c\x8b\xf9\xf0\x48\x7e\x28\xeb\xda\xb9\x43\x82\x4e\x88\x9f\xec\x8c\x87\x14\x36\x5f\x8f\xb7\x85\x85\x5b\x4c\xb7\x27\xec\xc2\x37\xf5\xc5\x7f\x01\x00\x00\xff\xff\x2f\xc2\x5d\xee\xe3\x12\x00\x00") +var __000012_split_bookmark_contentUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x57\x41\x73\xa3\x36\x14\xbe\xfb\x57\xbc\x5b\xf0\x0c\x4e\x3a\xbb\x93\x4b\x33\x3d\x10\x5b\x4e\x68\x1d\x48\x31\x74\xb3\xed\x74\x18\xd9\xc8\xb6\x1a\x90\x18\x24\xe2\x64\x3a\xfd\xef\x1d\x09\x81\x71\x4c\x1c\xb2\xc9\x61\xf7\x88\xf4\xe9\x93\xde\xf7\xbe\xf7\x24\xc6\x01\x72\x42\x04\xa1\x73\x39\x43\xb0\xe0\xfc\x3e\xc3\xc5\x7d\xbc\xe4\x4c\x12\x26\xad\x01\x00\x00\x4d\xa0\x2c\x69\x02\xb7\x81\x7b\xe3\x04\x5f\xe1\x37\xf4\x15\x26\x68\xea\x44\xb3\x10\xd6\x84\xc5\x05\x66\x09\xcf\x62\x85\xb1\x86\xb6\x5e\x22\x9f\x72\x02\x7f\x38\xc1\xf8\xda\x09\xac\xf3\x9f\x86\xe0\xf9\x21\x78\xd1\x6c\x66\xc3\x68\x54\xcd\xf2\xd5\xe1\x76\xf5\x80\x0d\x79\xb2\xb2\x81\xe4\xe5\xc2\x06\x9a\xe1\x35\xb1\x21\xe7\xc9\x12\x0b\x69\xc3\x03\x4d\x08\xb7\x81\xc8\xe5\xe9\x50\x6f\x56\x16\x29\x84\xe8\x2e\xdc\xdf\x24\x0a\x66\x6a\x0f\xb9\x21\xcd\x3e\x15\x5a\x90\x22\xae\x43\xaa\xc3\x68\x56\x6d\x37\x84\x69\x08\x50\x01\xac\x4c\x53\x1b\xe4\x86\x0a\x30\x27\x54\xa3\x62\x83\x0b\x92\xd8\x15\x94\xca\x13\x01\x8c\x4b\x83\xa5\x12\xb6\x34\x4d\x61\x41\x52\xce\xd6\x20\xb9\xde\x5f\xf1\x55\xb2\x50\x99\x12\x7d\xd6\x4a\xa6\x84\x88\x65\x41\x73\x49\x39\x6b\x8f\xf2\x0c\x53\x33\xa0\xce\x64\xbe\x4d\x30\x51\x30\xd3\x28\xf1\x39\xbe\x27\x4f\x3b\xd4\xfc\x33\xa8\xef\x15\x2f\x40\x48\x5e\x50\xb6\x86\x02\x6f\x9b\x83\xa7\xf4\x9e\xb4\x55\x6d\xa9\x58\xd1\x95\x59\x86\x8b\x16\x9f\xe3\xaa\xe4\x92\x02\x4b\x92\xd4\xb3\x1a\x59\x33\x36\xc8\x46\x1b\x06\x4a\xe5\x84\x6f\x99\x3a\x46\x86\xa5\xc6\x6f\x64\x96\xee\xc0\xfa\xab\x5e\xa1\x0e\xbb\x25\x0b\xc8\xf1\x9a\x54\x02\xe1\xb5\x68\xfb\xe6\xaf\xbf\x9b\x14\x9d\xfc\xfb\xdf\x49\xe5\x1e\x85\x51\x2b\x95\x1a\x35\xd3\xe2\x09\x12\xb2\xc2\x65\x2a\x81\x64\xb9\x7c\x02\x5c\x14\xb8\x3a\x6f\x46\x24\x4e\xb0\xc4\xf0\xeb\xdc\xf7\x2e\xf7\xf9\xaa\x80\x0a\xa2\x82\x8c\xb1\x84\xd0\xbd\x41\xf3\xd0\xb9\xb9\x85\x2f\x6e\x78\xad\x3f\xe1\x4f\xdf\x43\x8d\xb5\x9a\xe5\xe3\x28\x08\x90\x17\xc6\xcd\x8a\x8a\xab\xcc\x93\x0f\xe3\x8a\x3c\xf7\xf7\x08\x59\x65\x91\xda\xb5\x6b\x95\xe1\x87\x17\x83\xc1\x68\x04\x2e\x4b\xc8\x23\x11\x03\x53\xc0\xae\x37\x41\x77\x40\x93\xc7\xf8\x79\x55\xc5\xba\xdc\x7c\xef\xb0\xdc\xd4\xc4\xf0\xa2\x07\x83\xaa\xb0\x2e\x82\xb2\x48\x7b\xad\x37\x0e\xee\xa2\xa8\xa6\x7a\xb1\xb4\xf2\xd4\xc5\xb4\x9b\xee\xc5\xd6\xd8\xa2\x83\x0b\xa2\xb9\xeb\x5d\xc1\x9a\x32\xab\x81\xfd\x23\x38\x5b\xc4\x39\x96\x9b\x98\xe7\xc2\x24\xe1\xf2\xe6\xd3\x39\x50\x95\x09\xe0\xec\x80\x66\xa0\xfd\x2e\x73\xf1\xf3\xd9\x59\xc2\x97\xe2\x34\xc7\x05\x4e\x48\xb2\x38\x5d\xf2\x4c\x8d\x94\x19\x61\x12\xab\xf2\x3f\xd3\x24\x94\xad\xcf\xaa\x30\x62\xfd\xdd\x23\x8c\x45\xf6\xe9\x3c\x16\x04\x17\xcb\xcd\x91\x48\x14\xca\xa2\x89\x5d\x75\x20\xbb\xdd\x78\xec\xba\xb8\xed\xba\x98\xec\xa6\x64\x86\x95\x75\xad\x7b\xf2\x14\xaf\x28\x49\x13\xf8\x05\x4e\x68\x72\xb2\x13\x38\x0c\xdc\xab\x2b\x14\x18\xdf\x77\x38\x67\x57\x0f\x97\x68\xea\x07\x08\xa2\xdb\x89\x5a\xd8\x75\xd6\xa9\x1f\x00\x72\xc6\xd7\x10\xf8\x5f\x00\xdd\xa1\x71\x14\x22\x98\x46\xde\x38\x74\x7d\xaf\xde\x62\xc7\x18\x2f\x79\x5a\x66\xcc\x52\xb9\x50\x52\x33\xb2\x6d\x38\x05\x48\xbc\x48\xc9\x60\x12\xf8\xb7\xe6\x72\x73\xa7\x80\xee\xdc\x79\x38\xdf\x81\x76\x61\xec\x5d\x7f\x02\xbe\xf9\xe2\xab\x6f\x97\x28\x72\x27\x10\xa0\x29\x0a\x90\x37\x46\x73\x3d\x2e\x2c\x85\x1c\xaa\xd0\x27\x68\x86\x42\x04\x63\x67\x3e\x76\x26\xc8\x6e\xf7\xd5\xae\xd5\x07\x5e\xa7\x89\xd9\x8f\x8a\x78\x85\x1f\x78\x41\x25\x81\x4b\xdf\x9f\x21\xc7\x3b\xec\x2f\x53\x67\x36\x47\x0d\x5e\x59\x85\x3e\xf4\x86\xe7\xe5\x22\xa5\xcb\x5e\xe8\x1f\xb0\xd3\x9a\x7e\xfa\x62\x9d\x89\x58\x67\xb4\xbb\xf5\x08\xcb\xa4\xdb\x86\x7e\xcd\xa7\x95\xac\x6e\x9a\x56\x3a\x8f\xf3\xd4\x49\x7c\x91\xc6\x00\x8e\xb3\x98\xdc\xbe\x48\x52\xcd\x1f\xe7\xe8\xea\xa3\xa2\x57\x03\x3d\xde\x42\x44\xaf\xde\x21\xde\xdb\x34\x9a\xd2\xd2\xcf\x8a\xaa\x69\x74\xbf\x89\x35\xe0\x9b\x1b\x03\xc3\xd9\x0b\x2f\xe2\xc3\x57\x69\x63\xdb\x37\x36\x90\xef\xae\xba\xf6\xdf\x31\xb5\xb1\x94\x14\xc3\xd7\x0a\x4f\xcb\x1d\x6b\xd5\xda\x57\x85\x1a\xb5\x34\xc1\xab\x57\x90\x26\xe8\x75\xff\xe8\xcc\xbe\xc3\x47\xa3\x11\xb4\x9d\x54\x5f\x67\x9a\xb6\x20\xa9\xbe\xe0\xc5\x86\xe6\x47\x8c\x15\x67\x38\xcf\x29\x5b\x57\xfe\x6a\xa6\x6a\x47\x74\xdc\x05\xc2\x3a\x62\x05\x89\xd7\xc7\xd6\x56\x32\xfe\x58\x56\x02\x4d\xd6\xaa\x38\xab\xa5\x92\x6d\x22\xee\x69\x2b\x23\x76\x6c\x64\x7a\xee\x85\x26\x19\x86\xb4\x9f\xd5\x6a\xd2\xde\x96\xab\x57\xbc\xab\x85\x75\x5b\x4a\xff\xa3\xbe\xff\x15\xd3\xa7\x1b\xd9\x1f\xe3\x58\xf2\x98\xd3\x82\x88\xa3\x2e\xd9\xfb\x5b\xef\xed\xd3\x0f\xb0\xe7\x1b\x1b\x5c\x4b\x8c\xe1\xe0\xc0\x8e\xfb\x7e\xd4\x99\x8a\x6b\xc1\xdb\x16\xd1\x33\x35\xe9\xd1\x5f\x9a\x8a\xa3\xf5\x7e\x3c\xa4\x69\x1f\xe9\x62\x60\x9e\xc5\xc6\xcd\xbb\x87\xf1\x73\x5f\x9b\xc3\xed\x14\x3a\x20\x7e\xb5\x34\x9e\x53\x68\xc1\x5e\xae\x0b\x0d\xd7\x98\x76\x51\xe8\x81\x37\x15\xc6\xff\x01\x00\x00\xff\xff\x18\xb2\x74\xf3\xd7\x12\x00\x00") func _000012_split_bookmark_contentUpSqlBytes() ([]byte, error) { return bindataRead( @@ -566,8 +566,8 @@ func _000012_split_bookmark_contentUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 4835, mode: os.FileMode(0644), modTime: time.Unix(1738162357, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x24, 0xf8, 0x64, 0xc3, 0x2d, 0x98, 0x56, 0xc4, 0x5a, 0x95, 0x7c, 0x3e, 0x87, 0xe6, 0x3, 0xdc, 0x20, 0x8c, 0xfc, 0x6b, 0x6a, 0x27, 0xec, 0xdc, 0x57, 0x7f, 0xd8, 0x64, 0x5b, 0x1e, 0xbf, 0x50}} + info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 4823, mode: os.FileMode(0644), modTime: time.Unix(1738244025, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x46, 0x8c, 0xb3, 0x75, 0x11, 0xf2, 0x65, 0x65, 0xa8, 0xc8, 0x15, 0x1a, 0xf6, 0x20, 0x3b, 0xbd, 0xbb, 0xa0, 0xf6, 0xce, 0x26, 0x7e, 0xea, 0x65, 0xe5, 0x90, 0x95, 0x14, 0x1, 0x90, 0x2, 0xca}} return a, nil } diff --git a/database/migrations/000012_split_bookmark_content.down.sql b/database/migrations/000012_split_bookmark_content.down.sql index 7b30d9a..686584b 100644 --- a/database/migrations/000012_split_bookmark_content.down.sql +++ b/database/migrations/000012_split_bookmark_content.down.sql @@ -1,6 +1,6 @@ -- Drop tables in reverse order to handle dependencies -DROP TABLE IF EXISTS bookmark_content_tags_mapping; -DROP TABLE IF EXISTS bookmark_content_tags; DROP TABLE IF EXISTS bookmark_share; +DROP TABLE IF EXISTS bookmark_tags_mapping; +DROP TABLE IF EXISTS bookmark_tags; DROP TABLE IF EXISTS bookmarks; DROP TABLE IF EXISTS bookmark_content; diff --git a/database/migrations/000012_split_bookmark_content.up.sql b/database/migrations/000012_split_bookmark_content.up.sql index 9cb4794..d1319f3 100644 --- a/database/migrations/000012_split_bookmark_content.up.sql +++ b/database/migrations/000012_split_bookmark_content.up.sql @@ -36,10 +36,9 @@ CREATE TABLE bookmarks ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(uuid) ON DELETE CASCADE, content_id UUID REFERENCES bookmark_content(id), - is_favorite BOOLEAN DEFAULT FALSE, - is_archive BOOLEAN DEFAULT FALSE, - is_public BOOLEAN DEFAULT FALSE, - reading_progress INTEGER DEFAULT 0, + is_favorite BOOLEAN NOT NULL DEFAULT FALSE, + is_archive BOOLEAN NOT NULL DEFAULT FALSE, + is_public BOOLEAN NOT NULL DEFAULT FALSE, metadata JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP @@ -86,7 +85,7 @@ CREATE TABLE bookmark_share ( expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - UNIQUE(userid, bookmark_id) + UNIQUE(user_id, bookmark_id) ); CREATE INDEX idx_bookmark_share_user_id ON bookmark_share(user_id); diff --git a/database/queries/bookmark_content.sql b/database/queries/bookmark_content.sql index d71591e..b5ca16f 100644 --- a/database/queries/bookmark_content.sql +++ b/database/queries/bookmark_content.sql @@ -29,6 +29,12 @@ SELECT * FROM bookmark_content WHERE id = $1; +-- name: GetBookmarkContentByBookmarkID :one +SELECT bc.* +FROM bookmarks b + JOIN bookmark_content bc ON b.content_id = bc.id +WHERE b.id = $1; + -- name: GetBookmarkContentByURL :one -- First try to get user specific content, then the shared content SELECT * diff --git a/database/queries/bookmark_tags.sql b/database/queries/bookmark_tags.sql index 328847d..200e981 100644 --- a/database/queries/bookmark_tags.sql +++ b/database/queries/bookmark_tags.sql @@ -2,6 +2,8 @@ -- name: CreateBookmarkTag :one INSERT INTO bookmark_tags (name, user_id) VALUES ($1, $2) +ON CONFLICT (name, user_id) DO UPDATE +SET name = EXCLUDED.name RETURNING *; -- name: DeleteBookmarkTag :exec diff --git a/database/queries/bookmarks.sql b/database/queries/bookmarks.sql index 2091140..16d088b 100644 --- a/database/queries/bookmarks.sql +++ b/database/queries/bookmarks.sql @@ -3,14 +3,15 @@ WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) AND (sqlc.narg('types')::text[] IS NULL OR bc.type = ANY(sqlc.narg('types')::text[])) AND (sqlc.narg('tags')::text[] IS NULL OR bct.name = ANY(sqlc.narg('tags')::text[])) ) -SELECT b.*, +SELECT sqlc.embed(b), + sqlc.embed(bc), t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -19,7 +20,7 @@ SELECT b.*, FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) @@ -34,7 +35,7 @@ WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) @@ -49,7 +50,8 @@ WITH total AS ( OR bc.metadata @@@ sqlc.narg('query') ) ) -SELECT b.*, +SELECT sqlc.embed(b), + sqlc.embed(bc), t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -58,7 +60,7 @@ SELECT b.*, FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) @@ -77,15 +79,15 @@ ORDER BY b.created_at DESC LIMIT $2 OFFSET $3; -- name: GetBookmarkWithContent :one -SELECT b.*, - bc.*, +SELECT sqlc.embed(b), + sqlc.embed(bc), COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), ARRAY[]::VARCHAR[] ) as tags FROM bookmarks b JOIN bookmark_content bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags bct ON bctm.tag_id = bct.id WHERE b.id = $1 AND b.user_id = $2 @@ -104,10 +106,10 @@ SELECT EXISTS ( -- name: CreateBookmark :one INSERT INTO bookmarks ( user_id, content_id, is_favorite, is_archive, - is_public, reading_progress, metadata + is_public, metadata ) VALUES ( - $1, $2, $3, $4, $5, $6, $7 + $1, $2, $3, $4, $5, $6 ) RETURNING *; @@ -116,7 +118,6 @@ UPDATE bookmarks SET is_favorite = COALESCE(sqlc.narg('is_favorite'), is_favorite), is_archive = COALESCE(sqlc.narg('is_archive'), is_archive), is_public = COALESCE(sqlc.narg('is_public'), is_public), - reading_progress = COALESCE(sqlc.narg('reading_progress'), reading_progress), metadata = COALESCE(sqlc.narg('metadata'), metadata) WHERE id = $1 AND user_id = $2 diff --git a/internal/core/bookmarks/bookmark_content_service.go b/internal/core/bookmarks/bookmark_content_service.go index f824df7..665a87c 100644 --- a/internal/core/bookmarks/bookmark_content_service.go +++ b/internal/core/bookmarks/bookmark_content_service.go @@ -34,8 +34,8 @@ func (s *Service) CreateBookmarkContent(ctx context.Context, tx db.DBTX, content return result, nil } -func (s *Service) GetBookmarkContentByID(ctx context.Context, tx db.DBTX, id uuid.UUID) (*BookmarkContentDTO, error) { - dbo, err := s.dao.GetBookmarkContentByID(ctx, tx, id) +func (s *Service) GetBookmarkContentByBookmarkID(ctx context.Context, tx db.DBTX, bookmarkID uuid.UUID) (*BookmarkContentDTO, error) { + dbo, err := s.dao.GetBookmarkContentByBookmarkID(ctx, tx, bookmarkID) if err != nil { return nil, err } @@ -71,10 +71,10 @@ func (s *Service) UpdateBookmarkContent(ctx context.Context, tx db.DBTX, content return result, nil } -func (s *Service) FetchContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID, opts fetcher.FetchOptions) (*BookmarkContentDTO, error) { - dto, err := s.GetBookmarkContentByID(ctx, tx, id) +func (s *Service) FetchContent(ctx context.Context, tx db.DBTX, bookmarkID, userID uuid.UUID, opts fetcher.FetchOptions) (*BookmarkContentDTO, error) { + dto, err := s.GetBookmarkContentByBookmarkID(ctx, tx, bookmarkID) if err != nil { - return nil, fmt.Errorf("failed to get bookmark by id '%s': %w", id.String(), err) + return nil, fmt.Errorf("failed to get bookmark content by id '%s': %w", bookmarkID.String(), err) } if dto.Content != "" && !opts.Force { return dto, nil @@ -108,13 +108,13 @@ func (s *Service) FetchContentWithCache(ctx context.Context, uri string, opts fe return reader.Process(ctx, content) } -func (s *Service) SummarierContent(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) (*BookmarkContentDTO, error) { +func (s *Service) SummarierContent(ctx context.Context, tx db.DBTX, bookmarkID, userID uuid.UUID) (*BookmarkContentDTO, error) { user, err := auth.LoadUser(ctx, tx, userID) if err != nil { return nil, err } - dto, err := s.GetBookmarkContentByID(ctx, tx, id) + dto, err := s.GetBookmarkContentByBookmarkID(ctx, tx, bookmarkID) if err != nil { return nil, err } @@ -135,10 +135,11 @@ func (s *Service) SummarierContent(ctx context.Context, tx db.DBTX, id, userID u } else { tags, summary := parseTagsFromSummary(content.Summary) if len(tags) > 0 { - if err := s.linkContentTags(ctx, tx, dto.Tags, tags, id, userID); err != nil { + if err := s.linkContentTags(ctx, tx, dto.Tags, tags, bookmarkID, userID); err != nil { return nil, err } } + dto.Tags = tags dto.Summary = summary } return s.UpdateBookmarkContent(ctx, tx, dto) diff --git a/internal/core/bookmarks/bookmark_model.go b/internal/core/bookmarks/bookmark_model.go index 4971102..fbf5583 100644 --- a/internal/core/bookmarks/bookmark_model.go +++ b/internal/core/bookmarks/bookmark_model.go @@ -26,28 +26,26 @@ type BookmarkMetadata struct { } type BookmarkDTO struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` - ContentID uuid.UUID `json:"content_id"` - IsFavorite bool `json:"is_favorite"` - IsArchive bool `json:"is_archive"` - IsPublic bool `json:"is_public"` - ReadingProgress int `json:"reading_progress"` - Metadata BookmarkMetadata `json:"metadata"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Tags []string `json:"tags"` - Content BookmarkContentDTO `json:"content"` + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + ContentID uuid.UUID `json:"content_id"` + IsFavorite bool `json:"is_favorite"` + IsArchive bool `json:"is_archive"` + IsPublic bool `json:"is_public"` + Metadata BookmarkMetadata `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Tags []string `json:"tags"` + Content BookmarkContentDTO `json:"content"` } func (b *BookmarkDTO) Load(dbo *db.Bookmark) { b.ID = dbo.ID b.UserID = dbo.UserID.Bytes b.ContentID = dbo.ContentID.Bytes - b.IsFavorite = dbo.IsFavorite.Bool - b.IsArchive = dbo.IsArchive.Bool - b.IsPublic = dbo.IsPublic.Bool - b.ReadingProgress = int(dbo.ReadingProgress.Int32) + b.IsFavorite = dbo.IsFavorite + b.IsArchive = dbo.IsArchive + b.IsPublic = dbo.IsPublic b.CreatedAt = dbo.CreatedAt.Time b.UpdatedAt = dbo.UpdatedAt.Time @@ -58,67 +56,23 @@ func (b *BookmarkDTO) Load(dbo *db.Bookmark) { func (b *BookmarkDTO) LoadWithContent(dbo *db.GetBookmarkWithContentRow) { // Load bookmark data - b.ID = dbo.ID - b.UserID = dbo.UserID.Bytes - b.ContentID = dbo.ID_2 - b.IsFavorite = dbo.IsFavorite.Bool - b.IsArchive = dbo.IsArchive.Bool - b.IsPublic = dbo.IsPublic.Bool - b.ReadingProgress = int(dbo.ReadingProgress.Int32) - b.CreatedAt = dbo.CreatedAt.Time - b.UpdatedAt = dbo.UpdatedAt.Time - - // Load bookmark metadata - if dbo.Metadata != nil { - b.Metadata = loadBookmarkMetadata(dbo.Metadata) - } - + b.Load(&dbo.Bookmark) + // load bookmark content + var content BookmarkContentDTO + content.Load(&dbo.BookmarkContent) + b.Content = content // Load tags from the aggregated tags field b.Tags = loadBookmarkTags(dbo.Tags) - - // Load content data - b.Content.ID = dbo.ID_2 - b.Content.Type = ContentType(dbo.Type) - b.Content.URL = dbo.Url - b.Content.UserID = dbo.UserID_2.Bytes - b.Content.Title = dbo.Title.String - b.Content.Description = dbo.Description.String - b.Content.Domain = dbo.Domain.String - b.Content.S3Key = dbo.S3Key.String - b.Content.Summary = dbo.Summary.String - b.Content.Content = dbo.Content.String - b.Content.Html = dbo.Html.String - b.Content.Tags = dbo.Tags - b.Content.CreatedAt = dbo.CreatedAt_2.Time - b.Content.UpdatedAt = dbo.UpdatedAt_2.Time - - // Load content metadata - if dbo.Metadata_2 != nil { - b.Content.Metadata = loadBookmarkContentMetadata(dbo.Metadata_2) - } } func (b *BookmarkDTO) Dump() db.CreateBookmarkParams { return db.CreateBookmarkParams{ - UserID: pgtype.UUID{Bytes: b.UserID, Valid: b.UserID != uuid.Nil}, - ContentID: pgtype.UUID{Bytes: b.ContentID, Valid: b.ContentID != uuid.Nil}, - IsFavorite: pgtype.Bool{ - Bool: b.IsFavorite, - Valid: true, - }, - IsArchive: pgtype.Bool{ - Bool: b.IsArchive, - Valid: true, - }, - IsPublic: pgtype.Bool{ - Bool: b.IsPublic, - Valid: true, - }, - ReadingProgress: pgtype.Int4{ - Int32: int32(b.ReadingProgress), - Valid: true, - }, - Metadata: dumpBookmarkMetadata(b.Metadata), + UserID: pgtype.UUID{Bytes: b.UserID, Valid: b.UserID != uuid.Nil}, + ContentID: pgtype.UUID{Bytes: b.ContentID, Valid: b.ContentID != uuid.Nil}, + IsFavorite: b.IsFavorite, + IsArchive: b.IsArchive, + IsPublic: b.IsPublic, + Metadata: dumpBookmarkMetadata(b.Metadata), } } @@ -138,10 +92,6 @@ func (b *BookmarkDTO) DumpToUpdateParams() db.UpdateBookmarkParams { Bool: b.IsPublic, Valid: true, }, - ReadingProgress: pgtype.Int4{ - Int32: int32(b.ReadingProgress), - Valid: true, - }, Metadata: dumpBookmarkMetadata(b.Metadata), } } @@ -150,20 +100,12 @@ func loadListBookmarks(dbos []db.ListBookmarksRow) []BookmarkDTO { bookmarks := make([]BookmarkDTO, len(dbos)) for i, dbo := range dbos { b := &bookmarks[i] - b.ID = dbo.ID - b.UserID = dbo.UserID.Bytes - b.ContentID = dbo.ContentID.Bytes - b.IsFavorite = dbo.IsFavorite.Bool - b.IsArchive = dbo.IsArchive.Bool - b.IsPublic = dbo.IsPublic.Bool - b.ReadingProgress = int(dbo.ReadingProgress.Int32) - b.CreatedAt = dbo.CreatedAt.Time - b.UpdatedAt = dbo.UpdatedAt.Time - - if dbo.Metadata != nil { - b.Metadata = loadBookmarkMetadata(dbo.Metadata) - } - + b.Load(&dbo.Bookmark) + // load bookmaek content + var content BookmarkContentDTO + content.Load(&dbo.BookmarkContent) + b.Content = content + // Load tags b.Tags = loadBookmarkTags(dbo.Tags) } return bookmarks @@ -173,20 +115,12 @@ func loadSearchBookmarks(dbos []db.SearchBookmarksRow) []BookmarkDTO { bookmarks := make([]BookmarkDTO, len(dbos)) for i, dbo := range dbos { b := &bookmarks[i] - b.ID = dbo.ID - b.UserID = dbo.UserID.Bytes - b.ContentID = dbo.ContentID.Bytes - b.IsFavorite = dbo.IsFavorite.Bool - b.IsArchive = dbo.IsArchive.Bool - b.IsPublic = dbo.IsPublic.Bool - b.ReadingProgress = int(dbo.ReadingProgress.Int32) - b.CreatedAt = dbo.CreatedAt.Time - b.UpdatedAt = dbo.UpdatedAt.Time - - if dbo.Metadata != nil { - b.Metadata = loadBookmarkMetadata(dbo.Metadata) - } - + b.Load(&dbo.Bookmark) + // load bookmaek content + var content BookmarkContentDTO + content.Load(&dbo.BookmarkContent) + b.Content = content + // Load tags b.Tags = loadBookmarkTags(dbo.Tags) } return bookmarks diff --git a/internal/core/bookmarks/dao.go b/internal/core/bookmarks/dao.go index e16c688..175e99b 100644 --- a/internal/core/bookmarks/dao.go +++ b/internal/core/bookmarks/dao.go @@ -13,6 +13,7 @@ type DAO interface { IsBookmarkContentExistByURL(ctx context.Context, db db.DBTX, url string) (bool, error) CreateBookmarkContent(ctx context.Context, db db.DBTX, arg db.CreateBookmarkContentParams) (db.BookmarkContent, error) GetBookmarkContentByID(ctx context.Context, db db.DBTX, id uuid.UUID) (db.BookmarkContent, error) + GetBookmarkContentByBookmarkID(ctx context.Context, db db.DBTX, id uuid.UUID) (db.BookmarkContent, error) GetBookmarkContentByURL(ctx context.Context, db db.DBTX, arg db.GetBookmarkContentByURLParams) (db.BookmarkContent, error) UpdateBookmarkContent(ctx context.Context, db db.DBTX, arg db.UpdateBookmarkContentParams) (db.BookmarkContent, error) diff --git a/internal/core/queue/crawler_worker.go b/internal/core/queue/crawler_worker.go index ae1dd1b..de42d36 100644 --- a/internal/core/queue/crawler_worker.go +++ b/internal/core/queue/crawler_worker.go @@ -58,8 +58,8 @@ func (w *CrawlerWorker) work(ctx context.Context, tx pgx.Tx, job *river.Job[Craw // insert summaries job if summary is empty if dto.Summary == "" { if result, err := DefaultQueue.InsertTx(ctx, tx, SummarierWorkerArgs{ - ID: dto.ID, - UserID: dto.UserID, + ID: job.Args.ID, // bookmark id + UserID: job.Args.UserID, // bookmark user id }, nil); err != nil { logger.FromContext(ctx).Error("failed to insert summaries job", "err", err, "content_id", dto.ID) } else { diff --git a/internal/pkg/db/bookmark_content.sql.go b/internal/pkg/db/bookmark_content.sql.go index 313c89a..6af66a3 100644 --- a/internal/pkg/db/bookmark_content.sql.go +++ b/internal/pkg/db/bookmark_content.sql.go @@ -82,6 +82,36 @@ func (q *Queries) CreateBookmarkContent(ctx context.Context, db DBTX, arg Create return i, err } +const getBookmarkContentByBookmarkID = `-- name: GetBookmarkContentByBookmarkID :one +SELECT bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at +FROM bookmarks b + JOIN bookmark_content bc ON b.content_id = bc.id +WHERE b.id = $1 +` + +func (q *Queries) GetBookmarkContentByBookmarkID(ctx context.Context, db DBTX, id uuid.UUID) (BookmarkContent, error) { + row := db.QueryRow(ctx, getBookmarkContentByBookmarkID, id) + var i BookmarkContent + err := row.Scan( + &i.ID, + &i.Type, + &i.Url, + &i.UserID, + &i.Title, + &i.Description, + &i.Domain, + &i.S3Key, + &i.Summary, + &i.Content, + &i.Html, + &i.Tags, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const getBookmarkContentByID = `-- name: GetBookmarkContentByID :one SELECT id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at FROM bookmark_content diff --git a/internal/pkg/db/bookmark_tags.sql.go b/internal/pkg/db/bookmark_tags.sql.go index 51eaf0c..1330ee8 100644 --- a/internal/pkg/db/bookmark_tags.sql.go +++ b/internal/pkg/db/bookmark_tags.sql.go @@ -14,6 +14,8 @@ import ( const createBookmarkTag = `-- name: CreateBookmarkTag :one INSERT INTO bookmark_tags (name, user_id) VALUES ($1, $2) +ON CONFLICT (name, user_id) DO UPDATE +SET name = EXCLUDED.name RETURNING id, name, user_id, created_at, updated_at ` diff --git a/internal/pkg/db/bookmarks.sql.go b/internal/pkg/db/bookmarks.sql.go index 06398f0..caabfd8 100644 --- a/internal/pkg/db/bookmarks.sql.go +++ b/internal/pkg/db/bookmarks.sql.go @@ -15,22 +15,21 @@ import ( const createBookmark = `-- name: CreateBookmark :one INSERT INTO bookmarks ( user_id, content_id, is_favorite, is_archive, - is_public, reading_progress, metadata + is_public, metadata ) VALUES ( - $1, $2, $3, $4, $5, $6, $7 + $1, $2, $3, $4, $5, $6 ) -RETURNING id, user_id, content_id, is_favorite, is_archive, is_public, reading_progress, metadata, created_at, updated_at +RETURNING id, user_id, content_id, is_favorite, is_archive, is_public, metadata, created_at, updated_at ` type CreateBookmarkParams struct { - UserID pgtype.UUID - ContentID pgtype.UUID - IsFavorite pgtype.Bool - IsArchive pgtype.Bool - IsPublic pgtype.Bool - ReadingProgress pgtype.Int4 - Metadata []byte + UserID pgtype.UUID + ContentID pgtype.UUID + IsFavorite bool + IsArchive bool + IsPublic bool + Metadata []byte } func (q *Queries) CreateBookmark(ctx context.Context, db DBTX, arg CreateBookmarkParams) (Bookmark, error) { @@ -40,7 +39,6 @@ func (q *Queries) CreateBookmark(ctx context.Context, db DBTX, arg CreateBookmar arg.IsFavorite, arg.IsArchive, arg.IsPublic, - arg.ReadingProgress, arg.Metadata, ) var i Bookmark @@ -51,7 +49,6 @@ func (q *Queries) CreateBookmark(ctx context.Context, db DBTX, arg CreateBookmar &i.IsFavorite, &i.IsArchive, &i.IsPublic, - &i.ReadingProgress, &i.Metadata, &i.CreatedAt, &i.UpdatedAt, @@ -85,7 +82,7 @@ func (q *Queries) DeleteBookmarksByUser(ctx context.Context, db DBTX, userID pgt } const getBookmarkWithContent = `-- name: GetBookmarkWithContent :one -SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, +SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.metadata, b.created_at, b.updated_at, bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -93,7 +90,7 @@ SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, ) as tags FROM bookmarks b JOIN bookmark_content bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags bct ON bctm.tag_id = bct.id WHERE b.id = $1 AND b.user_id = $2 @@ -107,64 +104,40 @@ type GetBookmarkWithContentParams struct { } type GetBookmarkWithContentRow struct { - ID uuid.UUID - UserID pgtype.UUID - ContentID pgtype.UUID - IsFavorite pgtype.Bool - IsArchive pgtype.Bool - IsPublic pgtype.Bool - ReadingProgress pgtype.Int4 - Metadata []byte - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz - ID_2 uuid.UUID - Type string - Url string - UserID_2 pgtype.UUID - Title pgtype.Text - Description pgtype.Text - Domain pgtype.Text - S3Key pgtype.Text - Summary pgtype.Text - Content pgtype.Text - Html pgtype.Text - Tags []string - Metadata_2 []byte - CreatedAt_2 pgtype.Timestamptz - UpdatedAt_2 pgtype.Timestamptz - Tags_2 interface{} + Bookmark Bookmark + BookmarkContent BookmarkContent + Tags interface{} } func (q *Queries) GetBookmarkWithContent(ctx context.Context, db DBTX, arg GetBookmarkWithContentParams) (GetBookmarkWithContentRow, error) { row := db.QueryRow(ctx, getBookmarkWithContent, arg.ID, arg.UserID) var i GetBookmarkWithContentRow err := row.Scan( - &i.ID, - &i.UserID, - &i.ContentID, - &i.IsFavorite, - &i.IsArchive, - &i.IsPublic, - &i.ReadingProgress, - &i.Metadata, - &i.CreatedAt, - &i.UpdatedAt, - &i.ID_2, - &i.Type, - &i.Url, - &i.UserID_2, - &i.Title, - &i.Description, - &i.Domain, - &i.S3Key, - &i.Summary, - &i.Content, - &i.Html, + &i.Bookmark.ID, + &i.Bookmark.UserID, + &i.Bookmark.ContentID, + &i.Bookmark.IsFavorite, + &i.Bookmark.IsArchive, + &i.Bookmark.IsPublic, + &i.Bookmark.Metadata, + &i.Bookmark.CreatedAt, + &i.Bookmark.UpdatedAt, + &i.BookmarkContent.ID, + &i.BookmarkContent.Type, + &i.BookmarkContent.Url, + &i.BookmarkContent.UserID, + &i.BookmarkContent.Title, + &i.BookmarkContent.Description, + &i.BookmarkContent.Domain, + &i.BookmarkContent.S3Key, + &i.BookmarkContent.Summary, + &i.BookmarkContent.Content, + &i.BookmarkContent.Html, + &i.BookmarkContent.Tags, + &i.BookmarkContent.Metadata, + &i.BookmarkContent.CreatedAt, + &i.BookmarkContent.UpdatedAt, &i.Tags, - &i.Metadata_2, - &i.CreatedAt_2, - &i.UpdatedAt_2, - &i.Tags_2, ) return i, err } @@ -231,14 +204,15 @@ WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) AND ($6::text[] IS NULL OR bct.name = ANY($6::text[])) ) -SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, +SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.metadata, b.created_at, b.updated_at, + bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -247,7 +221,7 @@ SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) @@ -268,16 +242,8 @@ type ListBookmarksParams struct { } type ListBookmarksRow struct { - ID uuid.UUID - UserID pgtype.UUID - ContentID pgtype.UUID - IsFavorite pgtype.Bool - IsArchive pgtype.Bool - IsPublic pgtype.Bool - ReadingProgress pgtype.Int4 - Metadata []byte - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + Bookmark Bookmark + BookmarkContent BookmarkContent TotalCount int64 Tags interface{} } @@ -299,16 +265,30 @@ func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksP for rows.Next() { var i ListBookmarksRow if err := rows.Scan( - &i.ID, - &i.UserID, - &i.ContentID, - &i.IsFavorite, - &i.IsArchive, - &i.IsPublic, - &i.ReadingProgress, - &i.Metadata, - &i.CreatedAt, - &i.UpdatedAt, + &i.Bookmark.ID, + &i.Bookmark.UserID, + &i.Bookmark.ContentID, + &i.Bookmark.IsFavorite, + &i.Bookmark.IsArchive, + &i.Bookmark.IsPublic, + &i.Bookmark.Metadata, + &i.Bookmark.CreatedAt, + &i.Bookmark.UpdatedAt, + &i.BookmarkContent.ID, + &i.BookmarkContent.Type, + &i.BookmarkContent.Url, + &i.BookmarkContent.UserID, + &i.BookmarkContent.Title, + &i.BookmarkContent.Description, + &i.BookmarkContent.Domain, + &i.BookmarkContent.S3Key, + &i.BookmarkContent.Summary, + &i.BookmarkContent.Content, + &i.BookmarkContent.Html, + &i.BookmarkContent.Tags, + &i.BookmarkContent.Metadata, + &i.BookmarkContent.CreatedAt, + &i.BookmarkContent.UpdatedAt, &i.TotalCount, &i.Tags, ); err != nil { @@ -345,7 +325,7 @@ WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) @@ -360,7 +340,8 @@ WITH total AS ( OR bc.metadata @@@ $7 ) ) -SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.reading_progress, b.metadata, b.created_at, b.updated_at, +SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.metadata, b.created_at, b.updated_at, + bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, t.total_count, COALESCE( array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), @@ -369,7 +350,7 @@ SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.content_id + LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) @@ -399,16 +380,8 @@ type SearchBookmarksParams struct { } type SearchBookmarksRow struct { - ID uuid.UUID - UserID pgtype.UUID - ContentID pgtype.UUID - IsFavorite pgtype.Bool - IsArchive pgtype.Bool - IsPublic pgtype.Bool - ReadingProgress pgtype.Int4 - Metadata []byte - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + Bookmark Bookmark + BookmarkContent BookmarkContent TotalCount int64 Tags interface{} } @@ -431,16 +404,30 @@ func (q *Queries) SearchBookmarks(ctx context.Context, db DBTX, arg SearchBookma for rows.Next() { var i SearchBookmarksRow if err := rows.Scan( - &i.ID, - &i.UserID, - &i.ContentID, - &i.IsFavorite, - &i.IsArchive, - &i.IsPublic, - &i.ReadingProgress, - &i.Metadata, - &i.CreatedAt, - &i.UpdatedAt, + &i.Bookmark.ID, + &i.Bookmark.UserID, + &i.Bookmark.ContentID, + &i.Bookmark.IsFavorite, + &i.Bookmark.IsArchive, + &i.Bookmark.IsPublic, + &i.Bookmark.Metadata, + &i.Bookmark.CreatedAt, + &i.Bookmark.UpdatedAt, + &i.BookmarkContent.ID, + &i.BookmarkContent.Type, + &i.BookmarkContent.Url, + &i.BookmarkContent.UserID, + &i.BookmarkContent.Title, + &i.BookmarkContent.Description, + &i.BookmarkContent.Domain, + &i.BookmarkContent.S3Key, + &i.BookmarkContent.Summary, + &i.BookmarkContent.Content, + &i.BookmarkContent.Html, + &i.BookmarkContent.Tags, + &i.BookmarkContent.Metadata, + &i.BookmarkContent.CreatedAt, + &i.BookmarkContent.UpdatedAt, &i.TotalCount, &i.Tags, ); err != nil { @@ -459,21 +446,19 @@ UPDATE bookmarks SET is_favorite = COALESCE($3, is_favorite), is_archive = COALESCE($4, is_archive), is_public = COALESCE($5, is_public), - reading_progress = COALESCE($6, reading_progress), - metadata = COALESCE($7, metadata) + metadata = COALESCE($6, metadata) WHERE id = $1 AND user_id = $2 -RETURNING id, user_id, content_id, is_favorite, is_archive, is_public, reading_progress, metadata, created_at, updated_at +RETURNING id, user_id, content_id, is_favorite, is_archive, is_public, metadata, created_at, updated_at ` type UpdateBookmarkParams struct { - ID uuid.UUID - UserID pgtype.UUID - IsFavorite pgtype.Bool - IsArchive pgtype.Bool - IsPublic pgtype.Bool - ReadingProgress pgtype.Int4 - Metadata []byte + ID uuid.UUID + UserID pgtype.UUID + IsFavorite pgtype.Bool + IsArchive pgtype.Bool + IsPublic pgtype.Bool + Metadata []byte } func (q *Queries) UpdateBookmark(ctx context.Context, db DBTX, arg UpdateBookmarkParams) (Bookmark, error) { @@ -483,7 +468,6 @@ func (q *Queries) UpdateBookmark(ctx context.Context, db DBTX, arg UpdateBookmar arg.IsFavorite, arg.IsArchive, arg.IsPublic, - arg.ReadingProgress, arg.Metadata, ) var i Bookmark @@ -494,7 +478,6 @@ func (q *Queries) UpdateBookmark(ctx context.Context, db DBTX, arg UpdateBookmar &i.IsFavorite, &i.IsArchive, &i.IsPublic, - &i.ReadingProgress, &i.Metadata, &i.CreatedAt, &i.UpdatedAt, diff --git a/internal/pkg/db/models.go b/internal/pkg/db/models.go index d0013fb..95c477f 100644 --- a/internal/pkg/db/models.go +++ b/internal/pkg/db/models.go @@ -117,16 +117,15 @@ type AuthUserOauthConnection struct { } type Bookmark struct { - ID uuid.UUID - UserID pgtype.UUID - ContentID pgtype.UUID - IsFavorite pgtype.Bool - IsArchive pgtype.Bool - IsPublic pgtype.Bool - ReadingProgress pgtype.Int4 - Metadata []byte - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + ID uuid.UUID + UserID pgtype.UUID + ContentID pgtype.UUID + IsFavorite bool + IsArchive bool + IsPublic bool + Metadata []byte + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz } type BookmarkContent struct { diff --git a/internal/port/httpserver/handler_bookmark.go b/internal/port/httpserver/handler_bookmark.go index f3a4770..0def30f 100644 --- a/internal/port/httpserver/handler_bookmark.go +++ b/internal/port/httpserver/handler_bookmark.go @@ -289,7 +289,7 @@ func (h *bookmarksHandler) getBookmark(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - bookmark, err := h.service.GetBookmarkWithContent(ctx, tx, req.BookmarkID, user.ID) + bookmark, err := h.service.GetBookmarkWithContent(ctx, tx, user.ID, req.BookmarkID) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } From 938ce0c986935edd63ebbbe3bab0bf83dab7a8b8 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Thu, 30 Jan 2025 22:38:05 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=8E=A8=20refactor:=20remove=20sideb?= =?UTF-8?q?ar=20trigger=20from=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed `SidebarTrigger` component from header elements in multiple components. - Added `SidebarTrigger` to the `SidebarComponent` as a menu item to allow triggering the sidebar. - Updated styling and layout to accommodate the changes. - Removed unnecessary `Link` component from `BookmarkList` component. - Updated styling for `SettingsPageComponenrt` to improve readability. --- .../components/bookmarks/bookmark-detail.tsx | 10 +--- .../bookmarks/bookmarks-list-page.tsx | 48 +++++++++---------- .../components/bookmarks/bookmarks-list.tsx | 8 +++- web/src/components/settings/settings.tsx | 12 ++--- web/src/components/sidebar/sidebar.tsx | 11 ++++- 5 files changed, 45 insertions(+), 44 deletions(-) diff --git a/web/src/components/bookmarks/bookmark-detail.tsx b/web/src/components/bookmarks/bookmark-detail.tsx index 71b88fd..8527250 100644 --- a/web/src/components/bookmarks/bookmark-detail.tsx +++ b/web/src/components/bookmarks/bookmark-detail.tsx @@ -11,11 +11,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { useToast } from "@/hooks/use-toast"; import { useBookmark, @@ -225,9 +221,7 @@ export default function BookmarkDetailPage({ id }: { id: string }) {
-
- -
+
setShowDeleteDialog(true)} diff --git a/web/src/components/bookmarks/bookmarks-list-page.tsx b/web/src/components/bookmarks/bookmarks-list-page.tsx index 9ba464e..5d6a691 100644 --- a/web/src/components/bookmarks/bookmarks-list-page.tsx +++ b/web/src/components/bookmarks/bookmarks-list-page.tsx @@ -11,11 +11,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useBookmarkMutations, useBookmarks } from "@/lib/apis/bookmarks"; import { useRouter } from "@tanstack/react-router"; @@ -109,13 +105,11 @@ export default function BookmarksListView({ return ( + {/* {BookmarksSidebarContent()} */}
-
-
- - -
+
+
- {isLoading ? ( -
- -
- ) : ( - - )} +
+ {isLoading ? ( +
+ +
+ ) : ( + + )} +
diff --git a/web/src/components/bookmarks/bookmarks-list.tsx b/web/src/components/bookmarks/bookmarks-list.tsx index 6a9a9ec..0c88791 100644 --- a/web/src/components/bookmarks/bookmarks-list.tsx +++ b/web/src/components/bookmarks/bookmarks-list.tsx @@ -85,8 +85,12 @@ export default function BookmarkList({ const listView = (bookmark: Bookmark) => { return ( - -
+ +
{bookmark.content.metadata?.cover && (
{items.map((item) => ( - - + + {item.icon && } {item.title} - - + + ))} @@ -61,7 +60,6 @@ export function SettingsPageComponenrt({
-

Preferences

diff --git a/web/src/components/sidebar/sidebar.tsx b/web/src/components/sidebar/sidebar.tsx index a1e591c..d2ee0b9 100644 --- a/web/src/components/sidebar/sidebar.tsx +++ b/web/src/components/sidebar/sidebar.tsx @@ -1,6 +1,7 @@ import type * as React from "react"; import { NavUser } from "@/components/sidebar-nav-user"; +import ThemeToggle from "@/components/theme-toggle"; import { Sidebar, SidebarContent, @@ -11,11 +12,11 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarRail, + SidebarTrigger, } from "@/components/ui/sidebar"; import { ROUTES } from "@/lib/router"; import { Link } from "@tanstack/react-router"; import { Settings2 } from "lucide-react"; -import ThemeToggle from "../theme-toggle"; export function SidebarComponent({ children, @@ -47,6 +48,14 @@ export function SidebarComponent({ + + +
+ + Trigger Sidebar +
+
+
From 42e7640107bbb922111bbd5690dc1135d120eaff Mon Sep 17 00:00:00 2001 From: Vaayne Date: Fri, 31 Jan 2025 22:09:06 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E2=9C=A8=20feat:=20Update=20bookmark=20c?= =?UTF-8?q?ontent=20handling=20and=20tag=20linking=20(main)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added tags field to bookmark content update SQL query - Removed reading_progress field from Swagger docs - Renamed FetchContentWithCache to FetchWebContentWithCache for clarity - Improved tag linking logic in bookmark_tag_service.go: - Added helper functions for set difference and ensuring tags exist - Refactored linkContentTags to handle tag additions/removals more efficiently - Moved tag linking to background goroutine in summarization - Simplified bookmark creation logic in bookmark_service.go - Updated variable names for consistency across bookmark content handling - Fixed Swagger documentation to match updated bookmark content model --- database/queries/bookmark_content.sql | 1 + docs/swagger/docs.go | 3 - docs/swagger/swagger.json | 3 - docs/swagger/swagger.yaml | 2 - .../core/bookmarks/bookmark_content_model.go | 1 + .../bookmarks/bookmark_content_service.go | 39 +++--- internal/core/bookmarks/bookmark_service.go | 20 +-- .../core/bookmarks/bookmark_tag_service.go | 121 ++++++++++++------ internal/pkg/db/bookmark_content.sql.go | 5 +- internal/port/bots/handlers/websummary.go | 2 +- internal/port/httpserver/handler_bookmark.go | 10 +- 11 files changed, 121 insertions(+), 86 deletions(-) diff --git a/database/queries/bookmark_content.sql b/database/queries/bookmark_content.sql index b5ca16f..357f8b6 100644 --- a/database/queries/bookmark_content.sql +++ b/database/queries/bookmark_content.sql @@ -50,6 +50,7 @@ SET title = COALESCE(sqlc.narg('title'), title), summary = COALESCE(sqlc.narg('summary'), summary), content = COALESCE(sqlc.narg('content'), content), html = COALESCE(sqlc.narg('html'), html), + tags = COALESCE(sqlc.narg('tags'), tags), metadata = COALESCE(sqlc.narg('metadata'), metadata) WHERE id = $1 RETURNING *; diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index dd81844..3a3af86 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -5107,9 +5107,6 @@ const docTemplate = `{ "metadata": { "$ref": "#/definitions/bookmarks.BookmarkMetadata" }, - "reading_progress": { - "type": "integer" - }, "tags": { "type": "array", "items": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index cd068ef..64bb199 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -5101,9 +5101,6 @@ "metadata": { "$ref": "#/definitions/bookmarks.BookmarkMetadata" }, - "reading_progress": { - "type": "integer" - }, "tags": { "type": "array", "items": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index ef946b8..82d2f99 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -267,8 +267,6 @@ definitions: type: boolean metadata: $ref: '#/definitions/bookmarks.BookmarkMetadata' - reading_progress: - type: integer tags: items: type: string diff --git a/internal/core/bookmarks/bookmark_content_model.go b/internal/core/bookmarks/bookmark_content_model.go index f436e8c..15c00c1 100644 --- a/internal/core/bookmarks/bookmark_content_model.go +++ b/internal/core/bookmarks/bookmark_content_model.go @@ -139,6 +139,7 @@ func (b *BookmarkContentDTO) DumpToUpdateParams() db.UpdateBookmarkContentParams String: b.Html, Valid: b.Html != "", }, + Tags: b.Tags, Metadata: dumpBookmarkContentMetadata(b.Metadata), } } diff --git a/internal/core/bookmarks/bookmark_content_service.go b/internal/core/bookmarks/bookmark_content_service.go index 665a87c..ba5ebc5 100644 --- a/internal/core/bookmarks/bookmark_content_service.go +++ b/internal/core/bookmarks/bookmark_content_service.go @@ -15,6 +15,7 @@ import ( "time" "github.com/google/uuid" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -72,23 +73,23 @@ func (s *Service) UpdateBookmarkContent(ctx context.Context, tx db.DBTX, content } func (s *Service) FetchContent(ctx context.Context, tx db.DBTX, bookmarkID, userID uuid.UUID, opts fetcher.FetchOptions) (*BookmarkContentDTO, error) { - dto, err := s.GetBookmarkContentByBookmarkID(ctx, tx, bookmarkID) + bookmarkContent, err := s.GetBookmarkContentByBookmarkID(ctx, tx, bookmarkID) if err != nil { return nil, fmt.Errorf("failed to get bookmark content by id '%s': %w", bookmarkID.String(), err) } - if dto.Content != "" && !opts.Force { - return dto, nil + if bookmarkContent.Content != "" && !opts.Force { + return bookmarkContent, nil } - content, err := s.FetchContentWithCache(ctx, dto.URL, opts) + webContent, err := s.FetchWebContentWithCache(ctx, bookmarkContent.URL, opts) if err != nil { return nil, fmt.Errorf("failed to fetch content: %w", err) } - dto.FromReaderContent(content) - return s.UpdateBookmarkContent(ctx, tx, dto) + bookmarkContent.FromReaderContent(webContent) + return s.UpdateBookmarkContent(ctx, tx, bookmarkContent) } -func (s *Service) FetchContentWithCache(ctx context.Context, uri string, opts fetcher.FetchOptions) (*webreader.Content, error) { +func (s *Service) FetchWebContentWithCache(ctx context.Context, uri string, opts fetcher.FetchOptions) (*webreader.Content, error) { u, err := url.Parse(uri) if err != nil { return nil, fmt.Errorf("invalid url '%s': %w", uri, err) @@ -114,33 +115,39 @@ func (s *Service) SummarierContent(ctx context.Context, tx db.DBTX, bookmarkID, return nil, err } - dto, err := s.GetBookmarkContentByBookmarkID(ctx, tx, bookmarkID) + bookmarkContent, err := s.GetBookmarkContentByBookmarkID(ctx, tx, bookmarkID) if err != nil { return nil, err } content := &webreader.Content{ - Markwdown: dto.Content, + Markwdown: bookmarkContent.Content, } summarier := processor.NewSummaryProcessor(s.llm, processor.WithSummaryOptionUser(user)) if len(content.Markwdown) < 1000 { logger.FromContext(ctx).Info("content is too short to summarise") - return dto, nil + return bookmarkContent, nil } if err := summarier.Process(ctx, content); err != nil { logger.Default.Error("failed to generate summary", "err", err) } else { tags, summary := parseTagsFromSummary(content.Summary) + bookmarkContent.Summary = summary if len(tags) > 0 { - if err := s.linkContentTags(ctx, tx, dto.Tags, tags, bookmarkID, userID); err != nil { - return nil, err - } + bookmarkContent.Tags = tags + // link tags in background + newUserCtx := auth.SetUserToContext(context.Background(), user) + go func() { + if err := db.RunInTransaction(newUserCtx, db.DefaultPool.Pool, func(ctx context.Context, tx pgx.Tx) error { + return s.linkContentTags(ctx, tx, bookmarkContent.Tags, tags, bookmarkID, userID) + }); err != nil { + logger.Default.Error("failed to link content tags", "err", err, "bookmark_id", bookmarkID) + } + }() } - dto.Tags = tags - dto.Summary = summary } - return s.UpdateBookmarkContent(ctx, tx, dto) + return s.UpdateBookmarkContent(ctx, tx, bookmarkContent) } diff --git a/internal/core/bookmarks/bookmark_service.go b/internal/core/bookmarks/bookmark_service.go index a7048c7..4f096be 100644 --- a/internal/core/bookmarks/bookmark_service.go +++ b/internal/core/bookmarks/bookmark_service.go @@ -34,29 +34,18 @@ func (s *Service) CreateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UU return nil, fmt.Errorf("%w: invalid URL", ErrInvalidInput) } - // Track if content already exists in the database - isContentExist := false - // Check if bookmark content already exists for this URL and user content, err := s.dao.GetBookmarkContentByURL(ctx, tx, db.GetBookmarkContentByURLParams{ Url: dto.URL, UserID: pgtype.UUID{Bytes: userId, Valid: true}, }) - // Handle the response from content lookup if err == nil { - isContentExist = true - } else if !db.IsNotFoundError(err) { - // Return error if it's not a "not found" error - return nil, fmt.Errorf("failed to check existing bookmark for url '%s': %w", dto.URL, err) - } - - if isContentExist { - if content.UserID.Valid { - // If content exists and belongs to a user, return duplicate error + // If content exists and belongs to the user, return duplicate error + if content.UserID.Valid && content.UserID.Bytes == userId { return nil, fmt.Errorf("%w, id: %s", ErrDuplicate, content.ID) } - } else { + } else if db.IsNotFoundError(err) { // Create new content if it doesn't exist createBookmarkContentParams := dto.Dump() // Set UserID to Nil as this content can be shared @@ -65,6 +54,9 @@ func (s *Service) CreateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UU if err != nil { return nil, fmt.Errorf("failed to create new bookmark content: %w", err) } + } else { + // return other errors + return nil, fmt.Errorf("failed to check existing bookmark for url '%s': %w", dto.URL, err) } // Create the bookmark entry linking user to content diff --git a/internal/core/bookmarks/bookmark_tag_service.go b/internal/core/bookmarks/bookmark_tag_service.go index 132b2bf..d77bb3a 100644 --- a/internal/core/bookmarks/bookmark_tag_service.go +++ b/internal/core/bookmarks/bookmark_tag_service.go @@ -10,62 +10,101 @@ import ( "github.com/google/uuid" ) -func (s *Service) linkContentTags(ctx context.Context, tx db.DBTX, originTags, newTags []string, contentID, userID uuid.UUID) error { - // create tags if not exist - allExistingTags, err := s.dao.ListExistingBookmarkTagsByTags(ctx, tx, db.ListExistingBookmarkTagsByTagsParams{ - Column1: newTags, - UserID: userID, - }) +func (s *Service) linkContentTags( + ctx context.Context, + tx db.DBTX, + originTags, newTags []string, + contentID, userID uuid.UUID, +) error { + // 1) Fetch all tags currently linked to this bookmark + currentLinkedTags, err := s.dao.ListBookmarkTagsByBookmarkId(ctx, tx, contentID) if err != nil { - return fmt.Errorf("failed to list existing tags: %w", err) + return fmt.Errorf("failed to list existing linked tags for content: %w", err) } - for _, tag := range newTags { - if slices.Contains(allExistingTags, tag) { - continue + + // 2) Figure out which tags need adding/removing + toAdd := difference(newTags, currentLinkedTags) + toRemove := difference(currentLinkedTags, newTags) + + // 3) Ensure any “toAdd” tags actually exist in the user’s DB + if err := s.ensureTagsExist(ctx, tx, toAdd, userID); err != nil { + return fmt.Errorf("failed to ensure tags exist: %w", err) + } + + // 4) Link new tags + if len(toAdd) > 0 { + if err := s.dao.LinkBookmarkWithTags(ctx, tx, db.LinkBookmarkWithTagsParams{ + BookmarkID: contentID, + Column2: toAdd, + UserID: userID, + }); err != nil { + return fmt.Errorf("failed to link new tags: %w", err) } - if _, err := s.dao.CreateBookmarkTag(ctx, tx, db.CreateBookmarkTagParams{ - Name: tag, - UserID: userID, + } + + // 5) Unlink removed tags + if len(toRemove) > 0 { + if err := s.dao.UnLinkBookmarkWithTags(ctx, tx, db.UnLinkBookmarkWithTagsParams{ + BookmarkID: contentID, + Column2: toRemove, + UserID: userID, }); err != nil { - return fmt.Errorf("failed to create tag '%s': %w", tag, err) + return fmt.Errorf("failed to unlink removed tags: %w", err) } } - // link content with tags that not linked before - contentExistingTags, err := s.dao.ListBookmarkTagsByBookmarkId(ctx, tx, contentID) - if err != nil { - return fmt.Errorf("failed to list content tags: %w", err) + // 6) Log result + logger.FromContext(ctx).Info("link content with tags", + "content_id", contentID, + "origin_tags", originTags, + "new_tags", newTags, + "added_tags", toAdd, + "removed_tags", toRemove, + ) + + return nil +} + +// difference returns elements in sliceA that are not in sliceB. +func difference(sliceA, sliceB []string) []string { + setB := make(map[string]struct{}, len(sliceB)) + for _, val := range sliceB { + setB[val] = struct{}{} } - newLinkedTags := make([]string, 0) - for _, tag := range newTags { - if !slices.Contains(contentExistingTags, tag) { - newLinkedTags = append(newLinkedTags, tag) + var diff []string + for _, val := range sliceA { + if _, found := setB[val]; !found { + diff = append(diff, val) } } - if err := s.dao.LinkBookmarkWithTags(ctx, tx, db.LinkBookmarkWithTagsParams{ - BookmarkID: contentID, - Column2: newLinkedTags, - UserID: userID, - }); err != nil { - return fmt.Errorf("failed to link tags with content: %w", err) + return diff +} + +// ensureTagsExist creates any tags in targetTags that do not currently exist in the DB. +func (s *Service) ensureTagsExist(ctx context.Context, tx db.DBTX, targetTags []string, userID uuid.UUID) error { + if len(targetTags) == 0 { + return nil } - // unlink content with tags in original but not in new - removedTags := make([]string, 0) - for _, tag := range originTags { - if !slices.Contains(newTags, tag) { - removedTags = append(removedTags, tag) - } + existing, err := s.dao.ListExistingBookmarkTagsByTags(ctx, tx, db.ListExistingBookmarkTagsByTagsParams{ + Column1: targetTags, + UserID: userID, + }) + if err != nil { + return fmt.Errorf("failed to list existing tags: %w", err) } - if err := s.dao.UnLinkBookmarkWithTags(ctx, tx, db.UnLinkBookmarkWithTagsParams{ - BookmarkID: contentID, - Column2: removedTags, - UserID: userID, - }); err != nil { - return fmt.Errorf("failed to unlink tags with content: %w", err) + for _, tag := range targetTags { + if slices.Contains(existing, tag) { + continue + } + if _, err := s.dao.CreateBookmarkTag(ctx, tx, db.CreateBookmarkTagParams{ + Name: tag, + UserID: userID, + }); err != nil { + return fmt.Errorf("failed to create tag '%s': %w", tag, err) + } } - logger.FromContext(ctx).Info("link content with tags", "content_id", contentID, "new_tags", newTags, "origin_tags", originTags, "removed_tags", removedTags, "new_linked_tags", newLinkedTags) return nil } diff --git a/internal/pkg/db/bookmark_content.sql.go b/internal/pkg/db/bookmark_content.sql.go index 6af66a3..85b00e5 100644 --- a/internal/pkg/db/bookmark_content.sql.go +++ b/internal/pkg/db/bookmark_content.sql.go @@ -200,7 +200,8 @@ SET title = COALESCE($2, title), summary = COALESCE($5, summary), content = COALESCE($6, content), html = COALESCE($7, html), - metadata = COALESCE($8, metadata) + tags = COALESCE($8, tags), + metadata = COALESCE($9, metadata) WHERE id = $1 RETURNING id, type, url, user_id, title, description, domain, s3_key, summary, content, html, tags, metadata, created_at, updated_at ` @@ -213,6 +214,7 @@ type UpdateBookmarkContentParams struct { Summary pgtype.Text Content pgtype.Text Html pgtype.Text + Tags []string Metadata []byte } @@ -225,6 +227,7 @@ func (q *Queries) UpdateBookmarkContent(ctx context.Context, db DBTX, arg Update arg.Summary, arg.Content, arg.Html, + arg.Tags, arg.Metadata, ) var i BookmarkContent diff --git a/internal/port/bots/handlers/websummary.go b/internal/port/bots/handlers/websummary.go index 03862e7..8f4ee86 100644 --- a/internal/port/bots/handlers/websummary.go +++ b/internal/port/bots/handlers/websummary.go @@ -102,7 +102,7 @@ func (h *Handler) WebSummaryHandler(c tele.Context) error { summary, err := cache.RunInCache[string](ctx, cache.DefaultDBCache, cache.NewCacheKey("WebSummary", url), 24*time.Hour, func() (*string, error) { isSummaryCached = false // cache the content - content, err := h.bookmarkService.FetchContentWithCache(ctx, url, fetcher.FetchOptions{ + content, err := h.bookmarkService.FetchWebContentWithCache(ctx, url, fetcher.FetchOptions{ FecherType: fetcher.TypeJinaReader, }) if err != nil { diff --git a/internal/port/httpserver/handler_bookmark.go b/internal/port/httpserver/handler_bookmark.go index 0def30f..0141a5f 100644 --- a/internal/port/httpserver/handler_bookmark.go +++ b/internal/port/httpserver/handler_bookmark.go @@ -230,7 +230,7 @@ func (h *bookmarksHandler) createBookmark(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - bookmark := &bookmarks.BookmarkContentDTO{ + bookmarkContent := &bookmarks.BookmarkContentDTO{ UserID: user.ID, URL: req.URL, Type: bookmarks.ContentTypeBookmark, @@ -241,13 +241,13 @@ func (h *bookmarksHandler) createBookmark(c echo.Context) error { Metadata: req.Metadata, } - created, err := h.service.CreateBookmark(ctx, tx, user.ID, bookmark) + bookmark, err := h.service.CreateBookmark(ctx, tx, user.ID, bookmarkContent) if err != nil { return ErrorResponse(c, http.StatusInternalServerError, err) } result, err := h.queue.InsertTx(ctx, tx, queue.CrawlerWorkerArgs{ - ID: created.ID, - UserID: created.UserID, + ID: bookmark.ID, + UserID: bookmark.UserID, FetchOptions: fetcher.FetchOptions{FecherType: fetcher.TypeHttp}, }, nil) if err != nil { @@ -255,7 +255,7 @@ func (h *bookmarksHandler) createBookmark(c echo.Context) error { } else { logger.FromContext(ctx).Info("success inserted job", "result", result, "err", err) } - return JsonResponse(c, http.StatusCreated, created) + return JsonResponse(c, http.StatusCreated, bookmark) } type getBookmarkRequest struct { From 2a3d61b90f0e3eaaec78afca9e3137384c47c4a1 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Fri, 31 Jan 2025 23:32:36 +0800 Subject: [PATCH 11/11] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Remove=20is=5Fpubli?= =?UTF-8?q?c=20column=20and=20refactor=20bookmark=20sharing=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed `is_public` column from bookmarks table - Updated bookmark sharing logic to use `bookmark_share` table - Refactored bookmark model and DTOs to handle sharing metadata separately - Updated API endpoints and Swagger documentation for bookmark sharing - Fixed frontend to use new bookmark share structure --- database/bindata.go | 8 +- .../000012_split_bookmark_content.up.sql | 1 - database/queries/bookmarks.sql | 20 +- docs/swagger/docs.go | 53 ++--- docs/swagger/swagger.json | 53 ++--- docs/swagger/swagger.yaml | 36 ++-- .../core/bookmarks/bookmark_content_model.go | 30 +-- internal/core/bookmarks/bookmark_model.go | 91 +++++--- internal/core/bookmarks/bookmark_service.go | 42 ++-- .../core/bookmarks/bookmark_share_service.go | 16 +- internal/core/bookmarks/dao.go | 1 + internal/pkg/db/bookmarks.sql.go | 48 ++--- internal/pkg/db/models.go | 1 - internal/port/httpserver/handler_bookmark.go | 197 +++--------------- .../port/httpserver/handler_bookmark_share.go | 158 ++++++++++++++ .../components/bookmarks/bookmark-detail.tsx | 29 +-- web/src/lib/apis/bookmarks.ts | 4 +- 17 files changed, 395 insertions(+), 393 deletions(-) create mode 100644 internal/port/httpserver/handler_bookmark_share.go diff --git a/database/bindata.go b/database/bindata.go index c540c3b..90df9db 100644 --- a/database/bindata.go +++ b/database/bindata.go @@ -23,7 +23,7 @@ // 000011_create_s3_resources_mapping_table.down.sql (264B) // 000011_create_s3_resources_mapping_table.up.sql (1.401kB) // 000012_split_bookmark_content.down.sql (243B) -// 000012_split_bookmark_content.up.sql (4.823kB) +// 000012_split_bookmark_content.up.sql (4.777kB) package migrations @@ -551,7 +551,7 @@ func _000012_split_bookmark_contentDownSql() (*asset, error) { return a, nil } -var __000012_split_bookmark_contentUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x57\x41\x73\xa3\x36\x14\xbe\xfb\x57\xbc\x5b\xf0\x0c\x4e\x3a\xbb\x93\x4b\x33\x3d\x10\x5b\x4e\x68\x1d\x48\x31\x74\xb3\xed\x74\x18\xd9\xc8\xb6\x1a\x90\x18\x24\xe2\x64\x3a\xfd\xef\x1d\x09\x81\x71\x4c\x1c\xb2\xc9\x61\xf7\x88\xf4\xe9\x93\xde\xf7\xbe\xf7\x24\xc6\x01\x72\x42\x04\xa1\x73\x39\x43\xb0\xe0\xfc\x3e\xc3\xc5\x7d\xbc\xe4\x4c\x12\x26\xad\x01\x00\x00\x4d\xa0\x2c\x69\x02\xb7\x81\x7b\xe3\x04\x5f\xe1\x37\xf4\x15\x26\x68\xea\x44\xb3\x10\xd6\x84\xc5\x05\x66\x09\xcf\x62\x85\xb1\x86\xb6\x5e\x22\x9f\x72\x02\x7f\x38\xc1\xf8\xda\x09\xac\xf3\x9f\x86\xe0\xf9\x21\x78\xd1\x6c\x66\xc3\x68\x54\xcd\xf2\xd5\xe1\x76\xf5\x80\x0d\x79\xb2\xb2\x81\xe4\xe5\xc2\x06\x9a\xe1\x35\xb1\x21\xe7\xc9\x12\x0b\x69\xc3\x03\x4d\x08\xb7\x81\xc8\xe5\xe9\x50\x6f\x56\x16\x29\x84\xe8\x2e\xdc\xdf\x24\x0a\x66\x6a\x0f\xb9\x21\xcd\x3e\x15\x5a\x90\x22\xae\x43\xaa\xc3\x68\x56\x6d\x37\x84\x69\x08\x50\x01\xac\x4c\x53\x1b\xe4\x86\x0a\x30\x27\x54\xa3\x62\x83\x0b\x92\xd8\x15\x94\xca\x13\x01\x8c\x4b\x83\xa5\x12\xb6\x34\x4d\x61\x41\x52\xce\xd6\x20\xb9\xde\x5f\xf1\x55\xb2\x50\x99\x12\x7d\xd6\x4a\xa6\x84\x88\x65\x41\x73\x49\x39\x6b\x8f\xf2\x0c\x53\x33\xa0\xce\x64\xbe\x4d\x30\x51\x30\xd3\x28\xf1\x39\xbe\x27\x4f\x3b\xd4\xfc\x33\xa8\xef\x15\x2f\x40\x48\x5e\x50\xb6\x86\x02\x6f\x9b\x83\xa7\xf4\x9e\xb4\x55\x6d\xa9\x58\xd1\x95\x59\x86\x8b\x16\x9f\xe3\xaa\xe4\x92\x02\x4b\x92\xd4\xb3\x1a\x59\x33\x36\xc8\x46\x1b\x06\x4a\xe5\x84\x6f\x99\x3a\x46\x86\xa5\xc6\x6f\x64\x96\xee\xc0\xfa\xab\x5e\xa1\x0e\xbb\x25\x0b\xc8\xf1\x9a\x54\x02\xe1\xb5\x68\xfb\xe6\xaf\xbf\x9b\x14\x9d\xfc\xfb\xdf\x49\xe5\x1e\x85\x51\x2b\x95\x1a\x35\xd3\xe2\x09\x12\xb2\xc2\x65\x2a\x81\x64\xb9\x7c\x02\x5c\x14\xb8\x3a\x6f\x46\x24\x4e\xb0\xc4\xf0\xeb\xdc\xf7\x2e\xf7\xf9\xaa\x80\x0a\xa2\x82\x8c\xb1\x84\xd0\xbd\x41\xf3\xd0\xb9\xb9\x85\x2f\x6e\x78\xad\x3f\xe1\x4f\xdf\x43\x8d\xb5\x9a\xe5\xe3\x28\x08\x90\x17\xc6\xcd\x8a\x8a\xab\xcc\x93\x0f\xe3\x8a\x3c\xf7\xf7\x08\x59\x65\x91\xda\xb5\x6b\x95\xe1\x87\x17\x83\xc1\x68\x04\x2e\x4b\xc8\x23\x11\x03\x53\xc0\xae\x37\x41\x77\x40\x93\xc7\xf8\x79\x55\xc5\xba\xdc\x7c\xef\xb0\xdc\xd4\xc4\xf0\xa2\x07\x83\xaa\xb0\x2e\x82\xb2\x48\x7b\xad\x37\x0e\xee\xa2\xa8\xa6\x7a\xb1\xb4\xf2\xd4\xc5\xb4\x9b\xee\xc5\xd6\xd8\xa2\x83\x0b\xa2\xb9\xeb\x5d\xc1\x9a\x32\xab\x81\xfd\x23\x38\x5b\xc4\x39\x96\x9b\x98\xe7\xc2\x24\xe1\xf2\xe6\xd3\x39\x50\x95\x09\xe0\xec\x80\x66\xa0\xfd\x2e\x73\xf1\xf3\xd9\x59\xc2\x97\xe2\x34\xc7\x05\x4e\x48\xb2\x38\x5d\xf2\x4c\x8d\x94\x19\x61\x12\xab\xf2\x3f\xd3\x24\x94\xad\xcf\xaa\x30\x62\xfd\xdd\x23\x8c\x45\xf6\xe9\x3c\x16\x04\x17\xcb\xcd\x91\x48\x14\xca\xa2\x89\x5d\x75\x20\xbb\xdd\x78\xec\xba\xb8\xed\xba\x98\xec\xa6\x64\x86\x95\x75\xad\x7b\xf2\x14\xaf\x28\x49\x13\xf8\x05\x4e\x68\x72\xb2\x13\x38\x0c\xdc\xab\x2b\x14\x18\xdf\x77\x38\x67\x57\x0f\x97\x68\xea\x07\x08\xa2\xdb\x89\x5a\xd8\x75\xd6\xa9\x1f\x00\x72\xc6\xd7\x10\xf8\x5f\x00\xdd\xa1\x71\x14\x22\x98\x46\xde\x38\x74\x7d\xaf\xde\x62\xc7\x18\x2f\x79\x5a\x66\xcc\x52\xb9\x50\x52\x33\xb2\x6d\x38\x05\x48\xbc\x48\xc9\x60\x12\xf8\xb7\xe6\x72\x73\xa7\x80\xee\xdc\x79\x38\xdf\x81\x76\x61\xec\x5d\x7f\x02\xbe\xf9\xe2\xab\x6f\x97\x28\x72\x27\x10\xa0\x29\x0a\x90\x37\x46\x73\x3d\x2e\x2c\x85\x1c\xaa\xd0\x27\x68\x86\x42\x04\x63\x67\x3e\x76\x26\xc8\x6e\xf7\xd5\xae\xd5\x07\x5e\xa7\x89\xd9\x8f\x8a\x78\x85\x1f\x78\x41\x25\x81\x4b\xdf\x9f\x21\xc7\x3b\xec\x2f\x53\x67\x36\x47\x0d\x5e\x59\x85\x3e\xf4\x86\xe7\xe5\x22\xa5\xcb\x5e\xe8\x1f\xb0\xd3\x9a\x7e\xfa\x62\x9d\x89\x58\x67\xb4\xbb\xf5\x08\xcb\xa4\xdb\x86\x7e\xcd\xa7\x95\xac\x6e\x9a\x56\x3a\x8f\xf3\xd4\x49\x7c\x91\xc6\x00\x8e\xb3\x98\xdc\xbe\x48\x52\xcd\x1f\xe7\xe8\xea\xa3\xa2\x57\x03\x3d\xde\x42\x44\xaf\xde\x21\xde\xdb\x34\x9a\xd2\xd2\xcf\x8a\xaa\x69\x74\xbf\x89\x35\xe0\x9b\x1b\x03\xc3\xd9\x0b\x2f\xe2\xc3\x57\x69\x63\xdb\x37\x36\x90\xef\xae\xba\xf6\xdf\x31\xb5\xb1\x94\x14\xc3\xd7\x0a\x4f\xcb\x1d\x6b\xd5\xda\x57\x85\x1a\xb5\x34\xc1\xab\x57\x90\x26\xe8\x75\xff\xe8\xcc\xbe\xc3\x47\xa3\x11\xb4\x9d\x54\x5f\x67\x9a\xb6\x20\xa9\xbe\xe0\xc5\x86\xe6\x47\x8c\x15\x67\x38\xcf\x29\x5b\x57\xfe\x6a\xa6\x6a\x47\x74\xdc\x05\xc2\x3a\x62\x05\x89\xd7\xc7\xd6\x56\x32\xfe\x58\x56\x02\x4d\xd6\xaa\x38\xab\xa5\x92\x6d\x22\xee\x69\x2b\x23\x76\x6c\x64\x7a\xee\x85\x26\x19\x86\xb4\x9f\xd5\x6a\xd2\xde\x96\xab\x57\xbc\xab\x85\x75\x5b\x4a\xff\xa3\xbe\xff\x15\xd3\xa7\x1b\xd9\x1f\xe3\x58\xf2\x98\xd3\x82\x88\xa3\x2e\xd9\xfb\x5b\xef\xed\xd3\x0f\xb0\xe7\x1b\x1b\x5c\x4b\x8c\xe1\xe0\xc0\x8e\xfb\x7e\xd4\x99\x8a\x6b\xc1\xdb\x16\xd1\x33\x35\xe9\xd1\x5f\x9a\x8a\xa3\xf5\x7e\x3c\xa4\x69\x1f\xe9\x62\x60\x9e\xc5\xc6\xcd\xbb\x87\xf1\x73\x5f\x9b\xc3\xed\x14\x3a\x20\x7e\xb5\x34\x9e\x53\x68\xc1\x5e\xae\x0b\x0d\xd7\x98\x76\x51\xe8\x81\x37\x15\xc6\xff\x01\x00\x00\xff\xff\x18\xb2\x74\xf3\xd7\x12\x00\x00") +var __000012_split_bookmark_contentUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x57\x4d\x6f\xe3\x36\x10\xbd\xfb\x57\xcc\x2d\x32\x20\x27\xc5\x2e\x72\x69\xd0\x83\x62\xd3\x89\x5a\x47\x4a\x65\xa9\x9b\x6d\x51\x08\xb4\x49\xdb\x6c\x24\x52\x10\xa9\x38\x41\xd1\xff\x5e\x90\xfa\xb0\x1c\x2b\x8e\xb2\xc9\x61\xf7\x28\xf2\xf1\x91\xf3\xe6\xcd\x90\x1a\x07\xc8\x09\x11\x84\xce\xe5\x0c\xc1\x42\x88\xfb\x14\xe7\xf7\xf1\x52\x70\x45\xb9\xb2\x06\x00\x00\x8c\x40\x51\x30\x02\xb7\x81\x7b\xe3\x04\x5f\xe1\x37\xf4\x15\x26\x68\xea\x44\xb3\x10\xd6\x94\xc7\x39\xe6\x44\xa4\xb1\xc6\x58\x43\xdb\x2c\x51\x4f\x19\x85\x3f\x9c\x60\x7c\xed\x04\xd6\xf9\x4f\x43\xf0\xfc\x10\xbc\x68\x36\xb3\x61\x34\x2a\x67\xc5\xea\x70\xbb\x7a\xc0\x86\x8c\xac\x6c\xa0\x59\xb1\xb0\x81\xa5\x78\x4d\x6d\xc8\x04\x59\x62\xa9\x6c\x78\x60\x84\x0a\x1b\xa8\x5a\x9e\x0e\xcd\x66\x45\x9e\x40\x88\xee\xc2\xfd\x4d\xa2\x60\xa6\xf7\x50\x1b\xda\xec\x53\xa2\x25\xcd\xe3\x3a\xa4\x3a\x8c\x66\xd5\x76\x43\xb9\x81\x00\x93\xc0\x8b\x24\xb1\x41\x6d\x98\x84\xea\x84\x7a\x54\x6e\x70\x4e\x89\x5d\x42\x99\x3a\x91\xc0\x85\xaa\xb0\x4c\xc1\x96\x25\x09\x2c\x68\x22\xf8\x1a\x94\x30\xfb\x6b\xbe\x52\x16\xa6\x12\x6a\xce\x5a\xca\x44\xa8\x5c\xe6\x2c\x53\x4c\xf0\xf6\xa8\x48\x31\xab\x06\xf4\x99\xaa\xef\x2a\x98\x28\x98\x19\x94\xfc\x1c\xdf\xd3\xa7\x1d\x6a\xfe\x19\xf4\xf7\x4a\xe4\x20\x95\xc8\x19\x5f\x43\x8e\xb7\xcd\xc1\x13\x76\x4f\xdb\xaa\xb6\x54\x2c\xe9\x8a\x34\xc5\x79\x8b\xcf\x71\x75\x72\x69\x8e\x15\x25\xf5\xac\x41\xd6\x8c\x0d\xb2\xd1\x86\x83\x56\x99\x88\x2d\xd7\xc7\x48\xb1\x32\xf8\x8d\x4a\x93\x1d\xd8\x7c\xd5\x2b\xf4\x61\xb7\x74\x01\x19\x5e\xd3\x52\x20\xbc\x96\x6d\xdf\xfc\xf5\x77\x93\xa2\x93\x7f\xff\x3b\x29\xdd\xa3\x31\x7a\xa5\x56\xa3\x66\x5a\x3c\x01\xa1\x2b\x5c\x24\x0a\x68\x9a\xa9\x27\xc0\x79\x8e\xcb\xf3\xa6\x54\x61\x82\x15\x86\x5f\xe7\xbe\x77\xb9\xcf\x57\x06\x94\x53\x1d\x64\x8c\x15\x84\xee\x0d\x9a\x87\xce\xcd\x2d\x7c\x71\xc3\x6b\xf3\x09\x7f\xfa\x1e\x6a\xac\xd5\x2c\x1f\x47\x41\x80\xbc\x30\x6e\x56\x94\x5c\x45\x46\x3e\x8c\x2b\xf2\xdc\xdf\x23\x64\x15\x79\x62\xd7\xae\xd5\x86\x1f\x5e\x0c\x06\xa3\x11\xb8\x9c\xd0\x47\x2a\x07\x55\x01\xbb\xde\x04\xdd\x01\x23\x8f\xf1\xf3\xaa\x8a\x4d\xb9\xf9\xde\x61\xb9\xe9\x89\xe1\x45\x0f\x06\x5d\x61\x5d\x04\x45\x9e\xf4\x5a\x5f\x39\xb8\x8b\xa2\x9c\xea\xc5\xd2\xca\x53\x17\xd3\x6e\xba\x17\x5b\x63\x8b\x0e\x2e\x88\xe6\xae\x77\x05\x6b\xc6\xad\x06\xf6\x8f\x14\x7c\x11\x67\x58\x6d\x62\x91\xc9\x2a\x09\x97\x37\x9f\xce\x81\xe9\x4c\x80\xe0\x07\x34\x03\xe3\x77\x95\xc9\x9f\xcf\xce\x88\x58\xca\xd3\x0c\xe7\x98\x50\xb2\x38\x5d\x8a\x54\x8f\x14\x29\xe5\x0a\xeb\xf2\x3f\x33\x24\x8c\xaf\xcf\xca\x30\x62\xf3\xdd\x23\x8c\x45\xfa\xe9\x3c\x96\x14\xe7\xcb\xcd\x91\x48\x34\xca\x62\xc4\x2e\x3b\x90\xdd\x6e\x3c\x76\x5d\xdc\x76\x5d\x4c\x76\x53\x32\xc3\xd2\xba\xd6\x3d\x7d\x8a\x57\x8c\x26\x04\x7e\x81\x13\x46\x4e\x76\x02\x87\x81\x7b\x75\x85\x82\xca\xf7\x1d\xce\xd9\xd5\xc3\x25\x9a\xfa\x01\x82\xe8\x76\xa2\x17\x76\x9d\x75\xea\x07\x80\x9c\xf1\x35\x04\xfe\x17\x40\x77\x68\x1c\x85\x08\xa6\x91\x37\x0e\x5d\xdf\xab\xb7\xd8\x31\xc6\x4b\x91\x14\x29\xb7\x74\x2e\xb4\xd4\x9c\x6e\x1b\x4e\x09\x0a\x2f\x12\x3a\x98\x04\xfe\x6d\x75\xb9\xb9\x53\x40\x77\xee\x3c\x9c\xef\x40\xbb\x30\xf6\xae\x3f\x09\xdf\x7c\xf1\xd5\xb7\x4b\x14\xb9\x13\x08\xd0\x14\x05\xc8\x1b\xa3\xb9\x19\x97\x96\x46\x0e\x75\xe8\x13\x34\x43\x21\x82\xb1\x33\x1f\x3b\x13\x64\xb7\xfb\x6a\xd7\xea\x03\xaf\x33\x52\xed\xc7\x64\xbc\xc2\x0f\x22\x67\x8a\xc2\xa5\xef\xcf\x90\xe3\x1d\xf6\x97\xa9\x33\x9b\xa3\x06\xaf\xad\xc2\x1e\xfa\xc1\x7f\xc0\xde\x59\x75\xc8\x17\x2b\x47\xc6\x26\x47\xdd\xcd\x44\x5a\x55\x02\x6d\xe8\xd7\x4e\x5a\xf2\x77\xd3\xb4\x12\x74\x9c\xa7\x4e\xcb\x8b\x34\x15\xe0\x38\x4b\x56\x2c\x12\xb6\x7c\x99\xa4\x9c\x3f\xce\xd1\xd5\x19\x65\xaf\x96\x78\xbc\x29\xc8\x5e\xdd\x40\xbe\xb7\x0d\x34\xc5\x62\x1e\x0a\x65\x1b\xe8\x7e\xe5\x1a\xc0\x37\x97\x3a\xc7\xe9\x0b\x6f\xdc\xc3\x77\x66\x63\xdb\x37\xb6\x84\xef\xae\xba\xf6\x5f\x26\xb5\xb1\xb4\x14\xc3\xd7\x0a\xcf\xc8\x1d\x1b\xd5\xda\xcd\x5f\x8f\x5a\x86\xe0\xd5\x4b\xc5\x10\xf4\xba\x51\x4c\x66\xdf\xe1\xa3\xd1\x08\xda\x4e\xaa\x2f\x28\x43\x9b\xd3\xc4\x5c\xd9\x72\xc3\xb2\x23\xc6\x8a\x53\x9c\x65\x8c\xaf\x4b\x7f\x35\x53\xb5\x23\x3a\xba\xbb\xb4\x8e\x58\x41\xe1\xf5\xb1\xb5\xa5\x8c\x3f\x96\x95\xc0\x90\xb5\x2a\xce\x6a\xa9\x64\x57\x11\xf7\xb4\x55\x25\x76\x5c\xc9\xf4\xdc\x0b\x4d\x32\x2a\xd2\x7e\x56\xab\x49\x7b\x5b\xae\x5e\xf1\xae\x16\xd6\x6d\x29\xf3\xd7\xf9\xfe\x77\x49\x9f\x6e\x64\x7f\x8c\x63\xe9\x63\xc6\x72\x2a\x8f\xba\x64\xef\xff\xbb\xb7\x4f\x3f\xc0\x9e\x6f\x6c\x70\x2d\x31\x86\x83\x03\x3b\xee\xfb\xd1\x64\x2a\xae\x05\x6f\x5b\xc4\xcc\xd4\xa4\x47\x7f\x52\x4a\x8e\xd6\x8b\xf0\x90\xa6\x7d\xa4\x8b\x41\xf5\xd0\xad\xdc\xbc\x7b\xea\x3e\xf7\x75\x75\xb8\x9d\x42\x07\xc4\xaf\x96\xc6\x73\x0a\x23\xd8\xcb\x75\x61\xe0\x06\xd3\x2e\x0a\x33\xf0\xa6\xc2\xf8\x3f\x00\x00\xff\xff\xba\xcb\x29\xd5\xa9\x12\x00\x00") func _000012_split_bookmark_contentUpSqlBytes() ([]byte, error) { return bindataRead( @@ -566,8 +566,8 @@ func _000012_split_bookmark_contentUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 4823, mode: os.FileMode(0644), modTime: time.Unix(1738244025, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x46, 0x8c, 0xb3, 0x75, 0x11, 0xf2, 0x65, 0x65, 0xa8, 0xc8, 0x15, 0x1a, 0xf6, 0x20, 0x3b, 0xbd, 0xbb, 0xa0, 0xf6, 0xce, 0x26, 0x7e, 0xea, 0x65, 0xe5, 0x90, 0x95, 0x14, 0x1, 0x90, 0x2, 0xca}} + info := bindataFileInfo{name: "000012_split_bookmark_content.up.sql", size: 4777, mode: os.FileMode(0644), modTime: time.Unix(1738335316, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xc1, 0x4, 0x74, 0x1f, 0xfe, 0x77, 0x90, 0xcf, 0xdf, 0xea, 0xd0, 0x43, 0xe1, 0xfe, 0x16, 0x68, 0x8b, 0x46, 0x32, 0x6, 0xa1, 0x37, 0xe, 0xc2, 0x43, 0x9d, 0x78, 0xd7, 0x28, 0xa0, 0xa1, 0xb6}} return a, nil } diff --git a/database/migrations/000012_split_bookmark_content.up.sql b/database/migrations/000012_split_bookmark_content.up.sql index d1319f3..4307f00 100644 --- a/database/migrations/000012_split_bookmark_content.up.sql +++ b/database/migrations/000012_split_bookmark_content.up.sql @@ -38,7 +38,6 @@ CREATE TABLE bookmarks ( content_id UUID REFERENCES bookmark_content(id), is_favorite BOOLEAN NOT NULL DEFAULT FALSE, is_archive BOOLEAN NOT NULL DEFAULT FALSE, - is_public BOOLEAN NOT NULL DEFAULT FALSE, metadata JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP diff --git a/database/queries/bookmarks.sql b/database/queries/bookmarks.sql index 16d088b..7c2b795 100644 --- a/database/queries/bookmarks.sql +++ b/database/queries/bookmarks.sql @@ -20,7 +20,7 @@ SELECT sqlc.embed(b), FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id + LEFT JOIN bookmark_tags_mapping AS bctm ON b.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) @@ -35,7 +35,7 @@ WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id + LEFT JOIN bookmark_tags_mapping AS bctm ON b.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND (sqlc.narg('domains')::text[] IS NULL OR bc.domain = ANY(sqlc.narg('domains')::text[])) @@ -81,17 +81,19 @@ LIMIT $2 OFFSET $3; -- name: GetBookmarkWithContent :one SELECT sqlc.embed(b), sqlc.embed(bc), + sqlc.embed(bs), COALESCE( - array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), + array_agg(bt.name) FILTER (WHERE bt.name IS NOT NULL), ARRAY[]::VARCHAR[] ) as tags FROM bookmarks b JOIN bookmark_content bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping bctm ON bc.id = bctm.bookmark_id - LEFT JOIN bookmark_tags bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_share bs ON bs.bookmark_id = b.id + LEFT JOIN bookmark_tags_mapping btm ON btm.bookmark_id = b.id + LEFT JOIN bookmark_tags bt ON btm.tag_id = bt.id WHERE b.id = $1 AND b.user_id = $2 -GROUP BY b.id, bc.id +GROUP BY b.id, bc.id, bs.id LIMIT 1; -- name: IsBookmarkExistWithURL :one @@ -105,11 +107,10 @@ SELECT EXISTS ( -- name: CreateBookmark :one INSERT INTO bookmarks ( - user_id, content_id, is_favorite, is_archive, - is_public, metadata + user_id, content_id, is_favorite, is_archive, metadata ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3, $4, $5 ) RETURNING *; @@ -117,7 +118,6 @@ RETURNING *; UPDATE bookmarks SET is_favorite = COALESCE(sqlc.narg('is_favorite'), is_favorite), is_archive = COALESCE(sqlc.narg('is_archive'), is_archive), - is_public = COALESCE(sqlc.narg('is_public'), is_public), metadata = COALESCE(sqlc.narg('metadata'), metadata) WHERE id = $1 AND user_id = $2 diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 3a3af86..28b50cc 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -3651,7 +3651,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/httpserver.updateSharedBookmarkRequest" + "$ref": "#/definitions/httpserver.createBookmarkShareRequest" } } ], @@ -3756,7 +3756,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/httpserver.shareBookmarkRequest" + "$ref": "#/definitions/httpserver.createBookmarkShareRequest" } } ], @@ -5107,6 +5107,9 @@ const docTemplate = `{ "metadata": { "$ref": "#/definitions/bookmarks.BookmarkMetadata" }, + "share": { + "$ref": "#/definitions/bookmarks.BookmarkShareDTO" + }, "tags": { "type": "array", "items": { @@ -5135,9 +5138,6 @@ const docTemplate = `{ }, "reading_progress": { "type": "integer" - }, - "share": { - "$ref": "#/definitions/bookmarks.BookmarkShareDTO" } } }, @@ -5344,6 +5344,20 @@ const docTemplate = `{ } } }, + "httpserver.createBookmarkShareRequest": { + "type": "object", + "required": [ + "bookmarkID" + ], + "properties": { + "bookmarkID": { + "type": "string" + }, + "expires_at": { + "type": "string" + } + } + }, "httpserver.createThreadMessageRequest": { "type": "object", "required": [ @@ -5468,20 +5482,6 @@ const docTemplate = `{ } } }, - "httpserver.shareBookmarkRequest": { - "type": "object", - "required": [ - "bookmarkID" - ], - "properties": { - "bookmarkID": { - "type": "string" - }, - "expires_at": { - "type": "string" - } - } - }, "httpserver.updateAssistantRequest": { "type": "object", "required": [ @@ -5540,21 +5540,6 @@ const docTemplate = `{ } } }, - "httpserver.updateSharedBookmarkRequest": { - "type": "object", - "required": [ - "bookmarkID", - "expires_at" - ], - "properties": { - "bookmarkID": { - "type": "string" - }, - "expires_at": { - "type": "string" - } - } - }, "httpserver.updateUserInfoRequest": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 64bb199..3022abd 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -3645,7 +3645,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/httpserver.updateSharedBookmarkRequest" + "$ref": "#/definitions/httpserver.createBookmarkShareRequest" } } ], @@ -3750,7 +3750,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/httpserver.shareBookmarkRequest" + "$ref": "#/definitions/httpserver.createBookmarkShareRequest" } } ], @@ -5101,6 +5101,9 @@ "metadata": { "$ref": "#/definitions/bookmarks.BookmarkMetadata" }, + "share": { + "$ref": "#/definitions/bookmarks.BookmarkShareDTO" + }, "tags": { "type": "array", "items": { @@ -5129,9 +5132,6 @@ }, "reading_progress": { "type": "integer" - }, - "share": { - "$ref": "#/definitions/bookmarks.BookmarkShareDTO" } } }, @@ -5338,6 +5338,20 @@ } } }, + "httpserver.createBookmarkShareRequest": { + "type": "object", + "required": [ + "bookmarkID" + ], + "properties": { + "bookmarkID": { + "type": "string" + }, + "expires_at": { + "type": "string" + } + } + }, "httpserver.createThreadMessageRequest": { "type": "object", "required": [ @@ -5462,20 +5476,6 @@ } } }, - "httpserver.shareBookmarkRequest": { - "type": "object", - "required": [ - "bookmarkID" - ], - "properties": { - "bookmarkID": { - "type": "string" - }, - "expires_at": { - "type": "string" - } - } - }, "httpserver.updateAssistantRequest": { "type": "object", "required": [ @@ -5534,21 +5534,6 @@ } } }, - "httpserver.updateSharedBookmarkRequest": { - "type": "object", - "required": [ - "bookmarkID", - "expires_at" - ], - "properties": { - "bookmarkID": { - "type": "string" - }, - "expires_at": { - "type": "string" - } - } - }, "httpserver.updateUserInfoRequest": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 82d2f99..7db0436 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -267,6 +267,8 @@ definitions: type: boolean metadata: $ref: '#/definitions/bookmarks.BookmarkMetadata' + share: + $ref: '#/definitions/bookmarks.BookmarkShareDTO' tags: items: type: string @@ -286,8 +288,6 @@ definitions: type: string reading_progress: type: integer - share: - $ref: '#/definitions/bookmarks.BookmarkShareDTO' type: object bookmarks.BookmarkShareDTO: properties: @@ -431,6 +431,15 @@ definitions: required: - url type: object + httpserver.createBookmarkShareRequest: + properties: + bookmarkID: + type: string + expires_at: + type: string + required: + - bookmarkID + type: object httpserver.createThreadMessageRequest: properties: assistantId: @@ -513,15 +522,6 @@ definitions: username: type: string type: object - httpserver.shareBookmarkRequest: - properties: - bookmarkID: - type: string - expires_at: - type: string - required: - - bookmarkID - type: object httpserver.updateAssistantRequest: properties: assistantId: @@ -560,16 +560,6 @@ definitions: required: - bookmarkID type: object - httpserver.updateSharedBookmarkRequest: - properties: - bookmarkID: - type: string - expires_at: - type: string - required: - - bookmarkID - - expires_at - type: object httpserver.updateUserInfoRequest: properties: email: @@ -2612,7 +2602,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/httpserver.shareBookmarkRequest' + $ref: '#/definitions/httpserver.createBookmarkShareRequest' produces: - application/json responses: @@ -2679,7 +2669,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/httpserver.updateSharedBookmarkRequest' + $ref: '#/definitions/httpserver.createBookmarkShareRequest' produces: - application/json responses: diff --git a/internal/core/bookmarks/bookmark_content_model.go b/internal/core/bookmarks/bookmark_content_model.go index 15c00c1..ae63643 100644 --- a/internal/core/bookmarks/bookmark_content_model.go +++ b/internal/core/bookmarks/bookmark_content_model.go @@ -35,21 +35,21 @@ type BookmarkContentMetadata struct { } type BookmarkContentDTO struct { - ID uuid.UUID `json:"id"` - Type ContentType `json:"type"` - URL string `json:"url"` - UserID uuid.UUID `json:"user_id"` - Title string `json:"title"` - Description string `json:"description"` - Domain string `json:"domain"` - S3Key string `json:"s3_key"` - Summary string `json:"summary"` - Content string `json:"content"` - Html string `json:"html"` - Tags []string `json:"tags"` - Metadata BookmarkContentMetadata `json:"metadata"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + Type ContentType `json:"type"` + URL string `json:"url"` + UserID uuid.UUID `json:"user_id"` + Title string `json:"title"` + Description string `json:"description"` + Domain string `json:"domain"` + S3Key string `json:"s3_key"` + Summary string `json:"summary"` + Content string `json:"content"` + Html string `json:"html"` + Tags []string `json:"tags"` + Metadata *BookmarkContentMetadata `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (b *BookmarkContentDTO) Load(dbo *db.BookmarkContent) { diff --git a/internal/core/bookmarks/bookmark_model.go b/internal/core/bookmarks/bookmark_model.go index fbf5583..824493f 100644 --- a/internal/core/bookmarks/bookmark_model.go +++ b/internal/core/bookmarks/bookmark_model.go @@ -3,6 +3,7 @@ package bookmarks import ( "encoding/json" "recally/internal/pkg/db" + "slices" "time" "github.com/google/uuid" @@ -21,22 +22,22 @@ type BookmarkMetadata struct { ReadingProgress int `json:"reading_progress,omitempty"` LastReadAt time.Time `json:"last_read_at,omitempty"` - Highlights []Highlight `json:"highlights,omitempty"` - Share *BookmarkShareDTO `json:"share,omitempty"` + Highlights []Highlight `json:"highlights,omitempty"` } type BookmarkDTO struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` - ContentID uuid.UUID `json:"content_id"` - IsFavorite bool `json:"is_favorite"` - IsArchive bool `json:"is_archive"` - IsPublic bool `json:"is_public"` - Metadata BookmarkMetadata `json:"metadata"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Tags []string `json:"tags"` - Content BookmarkContentDTO `json:"content"` + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` + ContentID uuid.UUID `json:"content_id"` + IsFavorite bool `json:"is_favorite"` + IsArchive bool `json:"is_archive"` + IsPublic bool `json:"is_public"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Tags []string `json:"tags"` + Metadata *BookmarkMetadata `json:"metadata"` + Content *BookmarkContentDTO `json:"content"` + Share *BookmarkShareDTO `json:"share,omitempty"` } func (b *BookmarkDTO) Load(dbo *db.Bookmark) { @@ -45,7 +46,6 @@ func (b *BookmarkDTO) Load(dbo *db.Bookmark) { b.ContentID = dbo.ContentID.Bytes b.IsFavorite = dbo.IsFavorite b.IsArchive = dbo.IsArchive - b.IsPublic = dbo.IsPublic b.CreatedAt = dbo.CreatedAt.Time b.UpdatedAt = dbo.UpdatedAt.Time @@ -60,7 +60,15 @@ func (b *BookmarkDTO) LoadWithContent(dbo *db.GetBookmarkWithContentRow) { // load bookmark content var content BookmarkContentDTO content.Load(&dbo.BookmarkContent) - b.Content = content + b.Content = &content + + if dbo.BookmarkShare.ID != uuid.Nil { + var share BookmarkShareDTO + share.Load(&dbo.BookmarkShare) + b.Share = &share + b.IsPublic = true + } + // Load tags from the aggregated tags field b.Tags = loadBookmarkTags(dbo.Tags) } @@ -71,7 +79,6 @@ func (b *BookmarkDTO) Dump() db.CreateBookmarkParams { ContentID: pgtype.UUID{Bytes: b.ContentID, Valid: b.ContentID != uuid.Nil}, IsFavorite: b.IsFavorite, IsArchive: b.IsArchive, - IsPublic: b.IsPublic, Metadata: dumpBookmarkMetadata(b.Metadata), } } @@ -82,15 +89,11 @@ func (b *BookmarkDTO) DumpToUpdateParams() db.UpdateBookmarkParams { UserID: pgtype.UUID{Bytes: b.UserID, Valid: b.UserID != uuid.Nil}, IsFavorite: pgtype.Bool{ Bool: b.IsFavorite, - Valid: true, + Valid: b.IsFavorite, }, IsArchive: pgtype.Bool{ Bool: b.IsArchive, - Valid: true, - }, - IsPublic: pgtype.Bool{ - Bool: b.IsPublic, - Valid: true, + Valid: b.IsArchive, }, Metadata: dumpBookmarkMetadata(b.Metadata), } @@ -104,7 +107,7 @@ func loadListBookmarks(dbos []db.ListBookmarksRow) []BookmarkDTO { // load bookmaek content var content BookmarkContentDTO content.Load(&dbo.BookmarkContent) - b.Content = content + b.Content = &content // Load tags b.Tags = loadBookmarkTags(dbo.Tags) } @@ -119,7 +122,7 @@ func loadSearchBookmarks(dbos []db.SearchBookmarksRow) []BookmarkDTO { // load bookmaek content var content BookmarkContentDTO content.Load(&dbo.BookmarkContent) - b.Content = content + b.Content = &content // Load tags b.Tags = loadBookmarkTags(dbo.Tags) } @@ -127,32 +130,56 @@ func loadSearchBookmarks(dbos []db.SearchBookmarksRow) []BookmarkDTO { } func loadBookmarkTags(input interface{}) []string { + if input == nil { + return nil + } + + // Handle case where input is []interface{} + if interfaceSlice, ok := input.([]interface{}); ok { + tags := make([]string, len(interfaceSlice)) + for i, v := range interfaceSlice { + if str, ok := v.(string); ok { + tags[i] = str + } + } + slices.Sort(tags) + return tags + } + + // Handle case where input is already []string if tags, ok := input.([]string); ok { return tags } + return nil } -func loadBookmarkMetadata(input interface{}) BookmarkMetadata { +func loadBookmarkMetadata(input interface{}) *BookmarkMetadata { if metadata, ok := input.(BookmarkMetadata); ok { - return metadata + return &metadata } - return BookmarkMetadata{} + return nil } -func dumpBookmarkMetadata(input BookmarkMetadata) []byte { +func dumpBookmarkMetadata(input *BookmarkMetadata) []byte { + if input == nil { + return nil + } metadata, _ := json.Marshal(input) return metadata } -func loadBookmarkContentMetadata(input interface{}) BookmarkContentMetadata { +func loadBookmarkContentMetadata(input interface{}) *BookmarkContentMetadata { if metadata, ok := input.(BookmarkContentMetadata); ok { - return metadata + return &metadata } - return BookmarkContentMetadata{} + return nil } -func dumpBookmarkContentMetadata(input BookmarkContentMetadata) []byte { +func dumpBookmarkContentMetadata(input *BookmarkContentMetadata) []byte { + if input == nil { + return nil + } metadata, _ := json.Marshal(input) return metadata } diff --git a/internal/core/bookmarks/bookmark_service.go b/internal/core/bookmarks/bookmark_service.go index 4f096be..ec0a6ac 100644 --- a/internal/core/bookmarks/bookmark_service.go +++ b/internal/core/bookmarks/bookmark_service.go @@ -168,28 +168,36 @@ func (s *Service) DeleteBookmarksByUser(ctx context.Context, tx db.DBTX, userId return s.dao.DeleteBookmarksByUser(ctx, tx, pgtype.UUID{Bytes: userId, Valid: true}) } -func (s *Service) UpdateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, id uuid.UUID, content *BookmarkContentDTO) (*BookmarkDTO, error) { +func (s *Service) UpdateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, id uuid.UUID, dto *BookmarkDTO) (*BookmarkDTO, error) { bookmark, err := s.GetBookmarkWithContent(ctx, tx, userId, id) if err != nil { return nil, err } - - updateContent := bookmark.Content - // Update content if it's changed - if content.Content != "" { - updateContent.Content = content.Content - } - if content.Description != "" { - updateContent.Description = content.Description - } - if content.Html != "" { - updateContent.Html = content.Html - } - if content.Summary != "" { - updateContent.Summary = content.Summary + if dto.Content != nil { + new := dto.Content + old := bookmark.Content + // Update content if it's changed + if new.Content != "" { + old.Content = new.Content + } + if new.Description != "" { + old.Description = new.Description + } + if new.Html != "" { + old.Html = new.Html + } + if new.Summary != "" { + old.Summary = new.Summary + } + if _, err = s.UpdateBookmarkContent(ctx, tx, old); err != nil { + return nil, fmt.Errorf("failed to update bookmark content: %w", err) + } } - if _, err = s.UpdateBookmarkContent(ctx, tx, &updateContent); err != nil { - return nil, err + + dbo, err := s.dao.UpdateBookmark(ctx, tx, dto.DumpToUpdateParams()) + if err != nil { + return nil, fmt.Errorf("failed to update bookmark: %w", err) } + bookmark.Load(&dbo) return bookmark, nil } diff --git a/internal/core/bookmarks/bookmark_share_service.go b/internal/core/bookmarks/bookmark_share_service.go index a7e3c49..78d0970 100644 --- a/internal/core/bookmarks/bookmark_share_service.go +++ b/internal/core/bookmarks/bookmark_share_service.go @@ -10,10 +10,10 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (s *Service) CreateBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID, expiresAt time.Time) (*BookmarkShareDTO, error) { +func (s *Service) CreateBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, bookmarkID uuid.UUID, expiresAt time.Time) (*BookmarkShareDTO, error) { cs, err := s.dao.CreateBookmarkShare(ctx, tx, db.CreateBookmarkShareParams{ UserID: userID, - BookmarkID: pgtype.UUID{Bytes: contentID, Valid: true}, + BookmarkID: pgtype.UUID{Bytes: bookmarkID, Valid: true}, ExpiresAt: pgtype.Timestamptz{ Time: expiresAt, Valid: !expiresAt.IsZero(), @@ -36,9 +36,9 @@ func (s *Service) GetBookmarkShareContent(ctx context.Context, tx db.DBTX, share return &dto, nil } -func (s *Service) GetBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID) (*BookmarkShareDTO, error) { +func (s *Service) GetBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, bookmarkID uuid.UUID) (*BookmarkShareDTO, error) { sharedContent, err := s.dao.GetBookmarkShare(ctx, tx, db.GetBookmarkShareParams{ - BookmarkID: pgtype.UUID{Bytes: contentID, Valid: true}, + BookmarkID: pgtype.UUID{Bytes: bookmarkID, Valid: true}, UserID: userID, }) if err != nil { @@ -50,9 +50,9 @@ func (s *Service) GetBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid. return &dto, nil } -func (s *Service) UpdateSharedContent(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID, expiresAt time.Time) (*BookmarkShareDTO, error) { +func (s *Service) UpdateBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, bookmarkID uuid.UUID, expiresAt time.Time) (*BookmarkShareDTO, error) { sc, err := s.dao.UpdateBookmarkShareByBookmarkId(ctx, tx, db.UpdateBookmarkShareByBookmarkIdParams{ - ID: contentID, + ID: bookmarkID, UserID: pgtype.UUID{Bytes: userID, Valid: true}, ExpiresAt: pgtype.Timestamptz{ Time: expiresAt, @@ -68,9 +68,9 @@ func (s *Service) UpdateSharedContent(ctx context.Context, tx db.DBTX, userID uu return &dto, nil } -func (s *Service) DeleteSharedContent(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID) error { +func (s *Service) DeleteBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, bookmarkID uuid.UUID) error { if err := s.dao.DeleteShareContent(ctx, tx, db.DeleteShareContentParams{ - ID: contentID, + ID: bookmarkID, UserID: userID, }); err != nil { return fmt.Errorf("failed to delete shared content: %w", err) diff --git a/internal/core/bookmarks/dao.go b/internal/core/bookmarks/dao.go index 175e99b..56e29cb 100644 --- a/internal/core/bookmarks/dao.go +++ b/internal/core/bookmarks/dao.go @@ -21,6 +21,7 @@ type DAO interface { GetBookmarkWithContent(ctx context.Context, db db.DBTX, arg db.GetBookmarkWithContentParams) (db.GetBookmarkWithContentRow, error) ListBookmarks(ctx context.Context, db db.DBTX, arg db.ListBookmarksParams) ([]db.ListBookmarksRow, error) SearchBookmarks(ctx context.Context, db db.DBTX, arg db.SearchBookmarksParams) ([]db.SearchBookmarksRow, error) + UpdateBookmark(ctx context.Context, db db.DBTX, arg db.UpdateBookmarkParams) (db.Bookmark, error) DeleteBookmark(ctx context.Context, db db.DBTX, arg db.DeleteBookmarkParams) error DeleteBookmarksByUser(ctx context.Context, db db.DBTX, userID pgtype.UUID) error diff --git a/internal/pkg/db/bookmarks.sql.go b/internal/pkg/db/bookmarks.sql.go index caabfd8..0094981 100644 --- a/internal/pkg/db/bookmarks.sql.go +++ b/internal/pkg/db/bookmarks.sql.go @@ -14,13 +14,12 @@ import ( const createBookmark = `-- name: CreateBookmark :one INSERT INTO bookmarks ( - user_id, content_id, is_favorite, is_archive, - is_public, metadata + user_id, content_id, is_favorite, is_archive, metadata ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3, $4, $5 ) -RETURNING id, user_id, content_id, is_favorite, is_archive, is_public, metadata, created_at, updated_at +RETURNING id, user_id, content_id, is_favorite, is_archive, metadata, created_at, updated_at ` type CreateBookmarkParams struct { @@ -28,7 +27,6 @@ type CreateBookmarkParams struct { ContentID pgtype.UUID IsFavorite bool IsArchive bool - IsPublic bool Metadata []byte } @@ -38,7 +36,6 @@ func (q *Queries) CreateBookmark(ctx context.Context, db DBTX, arg CreateBookmar arg.ContentID, arg.IsFavorite, arg.IsArchive, - arg.IsPublic, arg.Metadata, ) var i Bookmark @@ -48,7 +45,6 @@ func (q *Queries) CreateBookmark(ctx context.Context, db DBTX, arg CreateBookmar &i.ContentID, &i.IsFavorite, &i.IsArchive, - &i.IsPublic, &i.Metadata, &i.CreatedAt, &i.UpdatedAt, @@ -82,19 +78,21 @@ func (q *Queries) DeleteBookmarksByUser(ctx context.Context, db DBTX, userID pgt } const getBookmarkWithContent = `-- name: GetBookmarkWithContent :one -SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.metadata, b.created_at, b.updated_at, +SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.metadata, b.created_at, b.updated_at, bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, + bs.id, bs.user_id, bs.bookmark_id, bs.expires_at, bs.created_at, bs.updated_at, COALESCE( - array_agg(bct.name) FILTER (WHERE bct.name IS NOT NULL), + array_agg(bt.name) FILTER (WHERE bt.name IS NOT NULL), ARRAY[]::VARCHAR[] ) as tags FROM bookmarks b JOIN bookmark_content bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping bctm ON bc.id = bctm.bookmark_id - LEFT JOIN bookmark_tags bct ON bctm.tag_id = bct.id + LEFT JOIN bookmark_share bs ON bs.bookmark_id = b.id + LEFT JOIN bookmark_tags_mapping btm ON btm.bookmark_id = b.id + LEFT JOIN bookmark_tags bt ON btm.tag_id = bt.id WHERE b.id = $1 AND b.user_id = $2 -GROUP BY b.id, bc.id +GROUP BY b.id, bc.id, bs.id LIMIT 1 ` @@ -106,6 +104,7 @@ type GetBookmarkWithContentParams struct { type GetBookmarkWithContentRow struct { Bookmark Bookmark BookmarkContent BookmarkContent + BookmarkShare BookmarkShare Tags interface{} } @@ -118,7 +117,6 @@ func (q *Queries) GetBookmarkWithContent(ctx context.Context, db DBTX, arg GetBo &i.Bookmark.ContentID, &i.Bookmark.IsFavorite, &i.Bookmark.IsArchive, - &i.Bookmark.IsPublic, &i.Bookmark.Metadata, &i.Bookmark.CreatedAt, &i.Bookmark.UpdatedAt, @@ -137,6 +135,12 @@ func (q *Queries) GetBookmarkWithContent(ctx context.Context, db DBTX, arg GetBo &i.BookmarkContent.Metadata, &i.BookmarkContent.CreatedAt, &i.BookmarkContent.UpdatedAt, + &i.BookmarkShare.ID, + &i.BookmarkShare.UserID, + &i.BookmarkShare.BookmarkID, + &i.BookmarkShare.ExpiresAt, + &i.BookmarkShare.CreatedAt, + &i.BookmarkShare.UpdatedAt, &i.Tags, ) return i, err @@ -211,7 +215,7 @@ WITH total AS ( AND ($5::text[] IS NULL OR bc.type = ANY($5::text[])) AND ($6::text[] IS NULL OR bct.name = ANY($6::text[])) ) -SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.metadata, b.created_at, b.updated_at, +SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.metadata, b.created_at, b.updated_at, bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, t.total_count, COALESCE( @@ -221,7 +225,7 @@ SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id CROSS JOIN total AS t - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id + LEFT JOIN bookmark_tags_mapping AS bctm ON b.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) @@ -270,7 +274,6 @@ func (q *Queries) ListBookmarks(ctx context.Context, db DBTX, arg ListBookmarksP &i.Bookmark.ContentID, &i.Bookmark.IsFavorite, &i.Bookmark.IsArchive, - &i.Bookmark.IsPublic, &i.Bookmark.Metadata, &i.Bookmark.CreatedAt, &i.Bookmark.UpdatedAt, @@ -325,7 +328,7 @@ WITH total AS ( SELECT COUNT(DISTINCT b.*) AS total_count FROM bookmarks AS b JOIN bookmark_content AS bc ON b.content_id = bc.id - LEFT JOIN bookmark_tags_mapping AS bctm ON bc.id = bctm.bookmark_id + LEFT JOIN bookmark_tags_mapping AS bctm ON b.id = bctm.bookmark_id LEFT JOIN bookmark_tags AS bct ON bctm.tag_id = bct.id WHERE b.user_id = $1 AND ($4::text[] IS NULL OR bc.domain = ANY($4::text[])) @@ -340,7 +343,7 @@ WITH total AS ( OR bc.metadata @@@ $7 ) ) -SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.is_public, b.metadata, b.created_at, b.updated_at, +SELECT b.id, b.user_id, b.content_id, b.is_favorite, b.is_archive, b.metadata, b.created_at, b.updated_at, bc.id, bc.type, bc.url, bc.user_id, bc.title, bc.description, bc.domain, bc.s3_key, bc.summary, bc.content, bc.html, bc.tags, bc.metadata, bc.created_at, bc.updated_at, t.total_count, COALESCE( @@ -409,7 +412,6 @@ func (q *Queries) SearchBookmarks(ctx context.Context, db DBTX, arg SearchBookma &i.Bookmark.ContentID, &i.Bookmark.IsFavorite, &i.Bookmark.IsArchive, - &i.Bookmark.IsPublic, &i.Bookmark.Metadata, &i.Bookmark.CreatedAt, &i.Bookmark.UpdatedAt, @@ -445,11 +447,10 @@ const updateBookmark = `-- name: UpdateBookmark :one UPDATE bookmarks SET is_favorite = COALESCE($3, is_favorite), is_archive = COALESCE($4, is_archive), - is_public = COALESCE($5, is_public), - metadata = COALESCE($6, metadata) + metadata = COALESCE($5, metadata) WHERE id = $1 AND user_id = $2 -RETURNING id, user_id, content_id, is_favorite, is_archive, is_public, metadata, created_at, updated_at +RETURNING id, user_id, content_id, is_favorite, is_archive, metadata, created_at, updated_at ` type UpdateBookmarkParams struct { @@ -457,7 +458,6 @@ type UpdateBookmarkParams struct { UserID pgtype.UUID IsFavorite pgtype.Bool IsArchive pgtype.Bool - IsPublic pgtype.Bool Metadata []byte } @@ -467,7 +467,6 @@ func (q *Queries) UpdateBookmark(ctx context.Context, db DBTX, arg UpdateBookmar arg.UserID, arg.IsFavorite, arg.IsArchive, - arg.IsPublic, arg.Metadata, ) var i Bookmark @@ -477,7 +476,6 @@ func (q *Queries) UpdateBookmark(ctx context.Context, db DBTX, arg UpdateBookmar &i.ContentID, &i.IsFavorite, &i.IsArchive, - &i.IsPublic, &i.Metadata, &i.CreatedAt, &i.UpdatedAt, diff --git a/internal/pkg/db/models.go b/internal/pkg/db/models.go index 95c477f..f6dcd38 100644 --- a/internal/pkg/db/models.go +++ b/internal/pkg/db/models.go @@ -122,7 +122,6 @@ type Bookmark struct { ContentID pgtype.UUID IsFavorite bool IsArchive bool - IsPublic bool Metadata []byte CreatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz diff --git a/internal/port/httpserver/handler_bookmark.go b/internal/port/httpserver/handler_bookmark.go index 0141a5f..3638e06 100644 --- a/internal/port/httpserver/handler_bookmark.go +++ b/internal/port/httpserver/handler_bookmark.go @@ -2,7 +2,6 @@ package httpserver import ( "context" - "errors" "fmt" "net/http" "recally/internal/core/bookmarks" @@ -23,7 +22,7 @@ type BookmarkService interface { ListBookmarks(ctx context.Context, tx db.DBTX, userID uuid.UUID, filters []string, query string, limit, offset int32) ([]bookmarks.BookmarkDTO, int64, error) CreateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, dto *bookmarks.BookmarkContentDTO) (*bookmarks.BookmarkDTO, error) GetBookmarkWithContent(ctx context.Context, tx db.DBTX, userId, id uuid.UUID) (*bookmarks.BookmarkDTO, error) - UpdateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, id uuid.UUID, content *bookmarks.BookmarkContentDTO) (*bookmarks.BookmarkDTO, error) + UpdateBookmark(ctx context.Context, tx db.DBTX, userId uuid.UUID, id uuid.UUID, bookmak *bookmarks.BookmarkDTO) (*bookmarks.BookmarkDTO, error) DeleteBookmark(ctx context.Context, tx db.DBTX, id, userID uuid.UUID) error DeleteBookmarksByUser(ctx context.Context, tx db.DBTX, userID uuid.UUID) error @@ -33,10 +32,10 @@ type BookmarkService interface { ListTags(ctx context.Context, tx db.DBTX, userID uuid.UUID) ([]bookmarks.TagDTO, error) ListDomains(ctx context.Context, tx db.DBTX, userID uuid.UUID) ([]bookmarks.DomainDTO, error) - GetBookmarkShareContent(ctx context.Context, tx db.DBTX, sharedID uuid.UUID) (*bookmarks.BookmarkContentDTO, error) - CreateBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID, expiresAt time.Time) (*bookmarks.BookmarkShareDTO, error) - UpdateSharedContent(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID, expiresAt time.Time) (*bookmarks.BookmarkShareDTO, error) - DeleteSharedContent(ctx context.Context, tx db.DBTX, userID uuid.UUID, contentID uuid.UUID) error + GetBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, bookmarkID uuid.UUID) (*bookmarks.BookmarkShareDTO, error) + CreateBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, bookmarkID uuid.UUID, expiresAt time.Time) (*bookmarks.BookmarkShareDTO, error) + UpdateBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, bookmarkID uuid.UUID, expiresAt time.Time) (*bookmarks.BookmarkShareDTO, error) + DeleteBookmarkShare(ctx context.Context, tx db.DBTX, userID uuid.UUID, bookmarkID uuid.UUID) error } // bookmarkServiceImpl implements BookmarkService @@ -59,10 +58,10 @@ func registerBookmarkHandlers(e *echo.Group, s *Service) { g.GET("/domains", h.listDomains) // Updated sharing endpoints - g.GET("/:bookmark-id/share", h.getShareBookmark) - g.POST("/:bookmark-id/share", h.shareBookmark) - g.PUT("/:bookmark-id/share", h.updateSharedBookmark) - g.DELETE("/:bookmark-id/share", h.deleteSharedBookmark) + g.GET("/:bookmark-id/share", h.getBookmarkShare) + g.POST("/:bookmark-id/share", h.createBookmarkShare) + g.PUT("/:bookmark-id/share", h.updateBookmarkShare) + g.DELETE("/:bookmark-id/share", h.deleteBookmarkShare) } type listBookmarksRequest struct { @@ -195,13 +194,13 @@ func (h *bookmarksHandler) listDomains(c echo.Context) error { } type createBookmarkRequest struct { - URL string `json:"url" validate:"required,url"` - Title string `json:"title"` - Description string `json:"description,omitempty"` - Tags []string `json:"tags,omitempty"` - Content string `json:"content,omitempty"` - HTML string `json:"html,omitempty"` - Metadata bookmarks.BookmarkContentMetadata `json:"metadata"` + URL string `json:"url" validate:"required,url"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` + Content string `json:"content,omitempty"` + HTML string `json:"html,omitempty"` + Metadata *bookmarks.BookmarkContentMetadata `json:"metadata"` } // createBookmark handles POST /bookmarks @@ -336,21 +335,25 @@ func (h *bookmarksHandler) updateBookmark(c echo.Context) error { return ErrorResponse(c, http.StatusInternalServerError, err) } - bookmark := &bookmarks.BookmarkContentDTO{ + bookmarkContent := &bookmarks.BookmarkContentDTO{ ID: req.BookmarkID, UserID: user.ID, } if req.Summary != "" { - bookmark.Summary = req.Summary + bookmarkContent.Summary = req.Summary } if req.Content != "" { - bookmark.Content = req.Content + bookmarkContent.Content = req.Content } if req.HTML != "" { - bookmark.Html = req.HTML + bookmarkContent.Html = req.HTML + } + + bookmark := &bookmarks.BookmarkDTO{ + Content: bookmarkContent, } updated, err := h.service.UpdateBookmark(ctx, tx, user.ID, req.BookmarkID, bookmark) @@ -495,155 +498,3 @@ func (h *bookmarksHandler) refreshBookmark(c echo.Context) error { return JsonResponse(c, http.StatusOK, content) } - -type shareBookmarkRequest struct { - BookmarkID uuid.UUID `param:"bookmark-id" validate:"required,uuid4"` - ExpiresAt time.Time `json:"expires_at"` -} - -// shareBookmark handles POST /bookmarks/:bookmark-id/share -// -// @Summary Share Bookmark -// @Description Creates a shareable link for a bookmark -// @Tags Bookmarks -// @Accept json -// @Produce json -// @Param bookmark-id path string true "Bookmark ID" -// @Param request body shareBookmarkRequest true "Share options" -// @Success 200 {object} JSONResult{data=bookmarks.BookmarkShareDTO} "Success" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 401 {object} JSONResult{data=nil} "Unauthorized" -// @Failure 404 {object} JSONResult{data=nil} "Not Found" -// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" -// @Router /bookmarks/{bookmark-id}/share [post] -func (h *bookmarksHandler) shareBookmark(c echo.Context) error { - ctx := c.Request().Context() - - req := new(shareBookmarkRequest) - if err := bindAndValidate(c, req); err != nil { - return err - } - - tx, user, err := initContext(ctx) - if err != nil { - return ErrorResponse(c, http.StatusInternalServerError, err) - } - - shared, err := h.service.CreateBookmarkShare(ctx, tx, user.ID, req.BookmarkID, req.ExpiresAt) - if err != nil { - return ErrorResponse(c, http.StatusInternalServerError, err) - } - - return JsonResponse(c, http.StatusOK, shared) -} - -type updateSharedBookmarkRequest struct { - BookmarkID uuid.UUID `param:"bookmark-id" validate:"required,uuid4"` - ExpiresAt time.Time `json:"expires_at" validate:"required"` -} - -// updateSharedBookmark handles PUT /bookmarks/:bookmark-id/share -// -// @Summary Update Shared Bookmark -// @Description Updates sharing settings for a bookmark -// @Tags Bookmarks -// @Accept json -// @Produce json -// @Param bookmark-id path string true "Bookmark ID" -// @Param request body updateSharedBookmarkRequest true "Update options" -// @Success 200 {object} JSONResult{data=bookmarks.BookmarkShareDTO} "Success" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 404 {object} JSONResult{data=nil} "Not Found" -// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" -// @Router /bookmarks/{bookmark-id}/share [put] -func (h *bookmarksHandler) updateSharedBookmark(c echo.Context) error { - ctx := c.Request().Context() - req := new(updateSharedBookmarkRequest) - if err := bindAndValidate(c, req); err != nil { - return err - } - - tx, user, err := initContext(ctx) - if err != nil { - return ErrorResponse(c, http.StatusInternalServerError, err) - } - - content, err := h.service.UpdateSharedContent(ctx, tx, user.ID, req.BookmarkID, req.ExpiresAt) - if err != nil { - return ErrorResponse(c, http.StatusInternalServerError, err) - } - - return JsonResponse(c, http.StatusOK, content) -} - -// deleteSharedBookmark handles DELETE /bookmarks/:bookmark-id/share -// -// @Summary Delete Shared Bookmark -// @Description Revokes sharing access for a bookmark -// @Tags Bookmarks -// @Accept json -// @Produce json -// @Param bookmark-id path string true "Bookmark ID" -// @Success 204 {object} JSONResult{data=nil} "No Content" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" -// @Router /bookmarks/{bookmark-id}/share [delete] -func (h *bookmarksHandler) deleteSharedBookmark(c echo.Context) error { - ctx := c.Request().Context() - - bookmarkID, err := uuid.Parse(c.Param("bookmark-id")) - if err != nil { - return ErrorResponse(c, http.StatusBadRequest, err) - } - - tx, user, err := initContext(ctx) - if err != nil { - return ErrorResponse(c, http.StatusInternalServerError, err) - } - - if err := h.service.DeleteSharedContent(ctx, tx, user.ID, bookmarkID); err != nil { - return ErrorResponse(c, http.StatusInternalServerError, err) - } - - return JsonResponse(c, http.StatusNoContent, nil) -} - -type getShareContentRequest struct { - BookmarkID uuid.UUID `param:"bookmark-id" validate:"required,uuid"` -} - -// getSharedBookmark handles GET /bookmarks/:bookmark-id/share -// -// @Summary Get Shared Bookmark -// @Description Gets sharing information for a bookmark -// @Tags Bookmarks -// @Accept json -// @Produce json -// @Param bookmark-id path string true "Bookmark ID" -// @Success 200 {object} JSONResult{data=bookmarks.BookmarkContentDTO} "Success" -// @Failure 400 {object} JSONResult{data=nil} "Bad Request" -// @Failure 404 {object} JSONResult{data=nil} "Not Found" -// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" -// @Router /bookmarks/{bookmark-id}/share [get] -func (h *bookmarksHandler) getShareBookmark(c echo.Context) error { - ctx := c.Request().Context() - - req := new(getShareContentRequest) - if err := bindAndValidate(c, req); err != nil { - return err - } - tx, err := loadTx(ctx) - if err != nil { - return errors.New("tx not found") - } - - cs, err := h.service.GetBookmarkShareContent(ctx, tx, req.BookmarkID) - if err != nil { - return ErrorResponse(c, http.StatusInternalServerError, err) - } - if cs == nil { - return ErrorResponse(c, http.StatusNotFound, fmt.Errorf("shared bookmark not found")) - } - - return JsonResponse(c, http.StatusOK, cs) -} diff --git a/internal/port/httpserver/handler_bookmark_share.go b/internal/port/httpserver/handler_bookmark_share.go new file mode 100644 index 0000000..4aed85b --- /dev/null +++ b/internal/port/httpserver/handler_bookmark_share.go @@ -0,0 +1,158 @@ +package httpserver + +import ( + "fmt" + "net/http" + "time" + + "github.com/google/uuid" + + "github.com/labstack/echo/v4" +) + +type createBookmarkShareRequest struct { + BookmarkID uuid.UUID `param:"bookmark-id" validate:"required,uuid4"` + ExpiresAt time.Time `json:"expires_at"` +} + +// createBookmarkShare handles POST /bookmarks/:bookmark-id/share +// +// @Summary Share Bookmark +// @Description Creates a shareable link for a bookmark +// @Tags Bookmarks +// @Accept json +// @Produce json +// @Param bookmark-id path string true "Bookmark ID" +// @Param request body createBookmarkShareRequest true "Share options" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkShareDTO} "Success" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 401 {object} JSONResult{data=nil} "Unauthorized" +// @Failure 404 {object} JSONResult{data=nil} "Not Found" +// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" +// @Router /bookmarks/{bookmark-id}/share [post] +func (h *bookmarksHandler) createBookmarkShare(c echo.Context) error { + ctx := c.Request().Context() + + req := new(createBookmarkShareRequest) + if err := bindAndValidate(c, req); err != nil { + return err + } + + tx, user, err := initContext(ctx) + if err != nil { + return ErrorResponse(c, http.StatusInternalServerError, err) + } + + shared, err := h.service.CreateBookmarkShare(ctx, tx, user.ID, req.BookmarkID, req.ExpiresAt) + if err != nil { + return ErrorResponse(c, http.StatusInternalServerError, err) + } + + return JsonResponse(c, http.StatusOK, shared) +} + +// updateBookmarkShare handles PUT /bookmarks/:bookmark-id/share +// +// @Summary Update Shared Bookmark +// @Description Updates sharing settings for a bookmark +// @Tags Bookmarks +// @Accept json +// @Produce json +// @Param bookmark-id path string true "Bookmark ID" +// @Param request body createBookmarkShareRequest true "Update options" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkShareDTO} "Success" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 404 {object} JSONResult{data=nil} "Not Found" +// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" +// @Router /bookmarks/{bookmark-id}/share [put] +func (h *bookmarksHandler) updateBookmarkShare(c echo.Context) error { + ctx := c.Request().Context() + req := new(createBookmarkShareRequest) + if err := bindAndValidate(c, req); err != nil { + return err + } + + tx, user, err := initContext(ctx) + if err != nil { + return ErrorResponse(c, http.StatusInternalServerError, err) + } + + content, err := h.service.UpdateBookmarkShare(ctx, tx, user.ID, req.BookmarkID, req.ExpiresAt) + if err != nil { + return ErrorResponse(c, http.StatusInternalServerError, err) + } + + return JsonResponse(c, http.StatusOK, content) +} + +type getBookmarkShareRequest struct { + BookmarkID uuid.UUID `param:"bookmark-id" validate:"required,uuid"` +} + +// deleteBookmarkShare handles DELETE /bookmarks/:bookmark-id/share +// +// @Summary Delete Shared Bookmark +// @Description Revokes sharing access for a bookmark +// @Tags Bookmarks +// @Accept json +// @Produce json +// @Param bookmark-id path string true "Bookmark ID" +// @Success 204 {object} JSONResult{data=nil} "No Content" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" +// @Router /bookmarks/{bookmark-id}/share [delete] +func (h *bookmarksHandler) deleteBookmarkShare(c echo.Context) error { + ctx := c.Request().Context() + + req := new(getBookmarkShareRequest) + if err := bindAndValidate(c, req); err != nil { + return err + } + + tx, user, err := initContext(ctx) + if err != nil { + return ErrorResponse(c, http.StatusInternalServerError, err) + } + + if err := h.service.DeleteBookmarkShare(ctx, tx, user.ID, req.BookmarkID); err != nil { + return ErrorResponse(c, http.StatusInternalServerError, err) + } + + return JsonResponse(c, http.StatusNoContent, nil) +} + +// getBookmarkShare handles GET /bookmarks/:bookmark-id/share +// +// @Summary Get Shared Bookmark +// @Description Gets sharing information for a bookmark +// @Tags Bookmarks +// @Accept json +// @Produce json +// @Param bookmark-id path string true "Bookmark ID" +// @Success 200 {object} JSONResult{data=bookmarks.BookmarkShareDTO} "Success" +// @Failure 400 {object} JSONResult{data=nil} "Bad Request" +// @Failure 404 {object} JSONResult{data=nil} "Not Found" +// @Failure 500 {object} JSONResult{data=nil} "Internal Server Error" +// @Router /bookmarks/{bookmark-id}/share [get] +func (h *bookmarksHandler) getBookmarkShare(c echo.Context) error { + ctx := c.Request().Context() + + req := new(getBookmarkShareRequest) + if err := bindAndValidate(c, req); err != nil { + return err + } + tx, user, err := initContext(ctx) + if err != nil { + return ErrorResponse(c, http.StatusInternalServerError, err) + } + + cs, err := h.service.GetBookmarkShare(ctx, tx, user.ID, req.BookmarkID) + if err != nil { + return ErrorResponse(c, http.StatusInternalServerError, err) + } + if cs == nil { + return ErrorResponse(c, http.StatusNotFound, fmt.Errorf("shared bookmark not found")) + } + + return JsonResponse(c, http.StatusOK, cs) +} diff --git a/web/src/components/bookmarks/bookmark-detail.tsx b/web/src/components/bookmarks/bookmark-detail.tsx index 8527250..0e1af21 100644 --- a/web/src/components/bookmarks/bookmark-detail.tsx +++ b/web/src/components/bookmarks/bookmark-detail.tsx @@ -161,7 +161,7 @@ export default function BookmarkDetailPage({ id }: { id: string }) { const handleCopyLink = async (id?: string) => { try { - const shareUrl = getShareUrl(id || bookmark.metadata?.share?.id); + const shareUrl = getShareUrl(id || bookmark.share?.id); if (shareUrl) { await navigator.clipboard.writeText(shareUrl); toast({ @@ -199,20 +199,21 @@ export default function BookmarkDetailPage({ id }: { id: string }) { } }; - const shareStatus = bookmark.metadata?.share - ? { - isShared: true, - isExpired: bookmark.metadata.share.expires_at - ? new Date(bookmark.metadata.share.expires_at) < new Date() - : false, - } - : { - isShared: false, - isExpired: false, - }; + const shareStatus = + bookmark.is_public && bookmark.share + ? { + isShared: true, + isExpired: bookmark.share.expires_at + ? new Date(bookmark.share.expires_at) < new Date() + : false, + } + : { + isShared: false, + isExpired: false, + }; - const shareExpireTime = bookmark.metadata?.share?.expires_at - ? new Date(bookmark.metadata.share.expires_at) + const shareExpireTime = bookmark.share?.expires_at + ? new Date(bookmark.share.expires_at) : undefined; return ( diff --git a/web/src/lib/apis/bookmarks.ts b/web/src/lib/apis/bookmarks.ts index b84158a..e43a73c 100644 --- a/web/src/lib/apis/bookmarks.ts +++ b/web/src/lib/apis/bookmarks.ts @@ -15,7 +15,6 @@ export interface BookmarkMetadata { last_read_at?: string; highlights?: Highlight[]; - share?: ShareContent; } export interface BookmarkContentMetadata { @@ -48,11 +47,12 @@ export interface Bookmark { is_favorite: boolean; is_archive: boolean; is_public: boolean; - metadata?: BookmarkMetadata; created_at: string; updated_at: string; content: BookmarkContent; + metadata?: BookmarkMetadata; tags?: string[]; + share?: ShareContent; } export interface ListBookmarksResponse {