Skip to content

Commit 21165aa

Browse files
authored
feat: allow tag filtering and count retrieval via api v1 (#1079)
* fix: frontend url to retrieve bookmark count * chore: unneeded type in generic * feat: allow tag filtering and count retrieval * fix: make styles * fix: make swagger * fix: make swag * tests: refactored gettags tests * fix: initialise tags empty slice
1 parent cdc13ed commit 21165aa

26 files changed

Lines changed: 734 additions & 236 deletions

docs/swagger/docs.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,20 @@ const docTemplate = `{
417417
"Tags"
418418
],
419419
"summary": "List tags",
420+
"parameters": [
421+
{
422+
"type": "boolean",
423+
"description": "Include bookmark count for each tag",
424+
"name": "with_bookmark_count",
425+
"in": "query"
426+
},
427+
{
428+
"type": "integer",
429+
"description": "Filter tags by bookmark ID",
430+
"name": "bookmark_id",
431+
"in": "query"
432+
}
433+
],
420434
"responses": {
421435
"200": {
422436
"description": "OK",

docs/swagger/swagger.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,20 @@
406406
"Tags"
407407
],
408408
"summary": "List tags",
409+
"parameters": [
410+
{
411+
"type": "boolean",
412+
"description": "Include bookmark count for each tag",
413+
"name": "with_bookmark_count",
414+
"in": "query"
415+
},
416+
{
417+
"type": "integer",
418+
"description": "Filter tags by bookmark ID",
419+
"name": "bookmark_id",
420+
"in": "query"
421+
}
422+
],
409423
"responses": {
410424
"200": {
411425
"description": "OK",

docs/swagger/swagger.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,15 @@ paths:
441441
/api/v1/tags:
442442
get:
443443
description: List all tags
444+
parameters:
445+
- description: Include bookmark count for each tag
446+
in: query
447+
name: with_bookmark_count
448+
type: boolean
449+
- description: Filter tags by bookmark ID
450+
in: query
451+
name: bookmark_id
452+
type: integer
444453
produces:
445454
- application/json
446455
responses:

internal/cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func initShiori(ctx context.Context, cmd *cobra.Command) (*config.Config, *depen
124124
account := model.AccountDTO{
125125
Username: "shiori",
126126
Password: "gopher",
127-
Owner: model.Ptr[bool](true),
127+
Owner: model.Ptr(true),
128128
}
129129

130130
if _, err := dependencies.Domains().Accounts().CreateAccount(cmd.Context(), account); err != nil {

internal/database/database.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package database
22

33
import (
44
"context"
5+
"database/sql"
56
"fmt"
67
"log"
78
"net/url"
89
"strings"
910

1011
"github.com/go-shiori/shiori/internal/model"
12+
"github.com/huandu/go-sqlbuilder"
1113
"github.com/jmoiron/sqlx"
1214
"github.com/pkg/errors"
1315
)
@@ -39,11 +41,25 @@ func Connect(ctx context.Context, dbURL string) (model.DB, error) {
3941
}
4042

4143
type dbbase struct {
42-
*sqlx.DB
44+
flavor sqlbuilder.Flavor
45+
reader *sqlx.DB
46+
writer *sqlx.DB
47+
}
48+
49+
func (db *dbbase) Flavor() sqlbuilder.Flavor {
50+
return db.flavor
51+
}
52+
53+
func (db *dbbase) ReaderDB() *sqlx.DB {
54+
return db.reader
55+
}
56+
57+
func (db *dbbase) WriterDB() *sqlx.DB {
58+
return db.writer
4359
}
4460

4561
func (db *dbbase) withTx(ctx context.Context, fn func(tx *sqlx.Tx) error) error {
46-
tx, err := db.BeginTxx(ctx, nil)
62+
tx, err := db.writer.BeginTxx(ctx, nil)
4763
if err != nil {
4864
return errors.WithStack(err)
4965
}
@@ -64,3 +80,32 @@ func (db *dbbase) withTx(ctx context.Context, fn func(tx *sqlx.Tx) error) error
6480

6581
return err
6682
}
83+
84+
func (db *dbbase) GetContext(ctx context.Context, dest any, query string, args ...any) error {
85+
return db.reader.GetContext(ctx, dest, query, args...)
86+
}
87+
88+
// Deprecated: Use SelectContext instead.
89+
func (db *dbbase) Select(dest any, query string, args ...any) error {
90+
return db.reader.Select(dest, query, args...)
91+
}
92+
93+
func (db *dbbase) SelectContext(ctx context.Context, dest any, query string, args ...any) error {
94+
return db.reader.SelectContext(ctx, dest, query, args...)
95+
}
96+
97+
func (db *dbbase) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
98+
return db.writer.ExecContext(ctx, query, args...)
99+
}
100+
101+
func (db *dbbase) MustBegin() *sqlx.Tx {
102+
return db.writer.MustBegin()
103+
}
104+
105+
func NewDBBase(reader, writer *sqlx.DB, flavor sqlbuilder.Flavor) dbbase {
106+
return dbbase{
107+
reader: reader,
108+
writer: writer,
109+
flavor: flavor,
110+
}
111+
}

internal/database/database_tags.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package database
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"log/slog"
8+
9+
"github.com/go-shiori/shiori/internal/model"
10+
"github.com/huandu/go-sqlbuilder"
11+
)
12+
13+
// GetTags returns a list of tags from the database.
14+
// If opts.WithBookmarkCount is true, the result will include the number of bookmarks for each tag.
15+
// If opts.BookmarkID is not 0, the result will include only the tags for the specified bookmark.
16+
// If opts.OrderBy is set, the result will be ordered by the specified column.
17+
func (db *dbbase) GetTags(ctx context.Context, opts model.DBListTagsOptions) ([]model.TagDTO, error) {
18+
sb := db.Flavor().NewSelectBuilder()
19+
20+
sb.Select("t.id", "t.name")
21+
sb.From("tag t")
22+
23+
// Treat the case where we want the bookmark count and filter by bookmark ID as a special case:
24+
// If we only want one of them, we can use a JOIN and GROUP BY.
25+
// If we want both, we need to use a subquery to get the count of bookmarks for each tag filtered
26+
// by bookmark ID.
27+
if opts.WithBookmarkCount && opts.BookmarkID == 0 {
28+
// Join with bookmark_tag and group by tag ID to get the count of bookmarks for each tag
29+
sb.JoinWithOption(sqlbuilder.LeftJoin, "bookmark_tag bt", "bt.tag_id = t.id")
30+
sb.SelectMore("COUNT(bt.tag_id) AS bookmark_count")
31+
sb.GroupBy("t.id")
32+
} else if opts.BookmarkID > 0 {
33+
// If we want the bookmark count, we need to use a subquery to get the count of bookmarks for each tag
34+
if opts.WithBookmarkCount {
35+
sb.SelectMore(
36+
sb.BuilderAs(
37+
db.Flavor().NewSelectBuilder().Select("COUNT(bt2.tag_id)").From("bookmark_tag bt2").Where("bt2.tag_id = t.id"),
38+
"bookmark_count",
39+
),
40+
)
41+
}
42+
43+
// Join with bookmark_tag and filter by bookmark ID to get the tags for a specific bookmark
44+
sb.JoinWithOption(sqlbuilder.RightJoin, "bookmark_tag bt",
45+
sb.And(
46+
"bt.tag_id = t.id",
47+
sb.Equal("bt.bookmark_id", opts.BookmarkID),
48+
),
49+
)
50+
sb.Where(sb.IsNotNull("t.id"))
51+
}
52+
53+
if opts.OrderBy == model.DBTagOrderByTagName {
54+
sb.OrderBy("t.name")
55+
}
56+
57+
query, args := sb.Build()
58+
query = db.ReaderDB().Rebind(query)
59+
60+
slog.Info("GetTags query", "query", query, "args", args)
61+
62+
tags := []model.TagDTO{}
63+
err := db.ReaderDB().SelectContext(ctx, &tags, query, args...)
64+
if err != nil && err != sql.ErrNoRows {
65+
return nil, fmt.Errorf("failed to get tags: %w", err)
66+
}
67+
68+
return tags, nil
69+
}

0 commit comments

Comments
 (0)