From e4a8a4d708e3666714c03f1db2f0be46935e5275 Mon Sep 17 00:00:00 2001 From: boojack Date: Wed, 21 Dec 2022 19:22:32 +0800 Subject: [PATCH] feat: tag table (#811) * feat: tag table * chore: update * chore: update --- api/tag.go | 20 +++ store/db/migration/dev/LATEST__SCHEMA.sql | 7 + store/store.go | 3 + store/tag.go | 173 ++++++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 api/tag.go create mode 100644 store/tag.go diff --git a/api/tag.go b/api/tag.go new file mode 100644 index 00000000..77d6403c --- /dev/null +++ b/api/tag.go @@ -0,0 +1,20 @@ +package api + +type Tag struct { + Name string + CreatorID int +} + +type TagUpsert struct { + Name string + CreatorID int +} + +type TagFind struct { + CreatorID int +} + +type TagDelete struct { + Name string + CreatorID int +} diff --git a/store/db/migration/dev/LATEST__SCHEMA.sql b/store/db/migration/dev/LATEST__SCHEMA.sql index a5ac821a..3b7b4adb 100644 --- a/store/db/migration/dev/LATEST__SCHEMA.sql +++ b/store/db/migration/dev/LATEST__SCHEMA.sql @@ -86,3 +86,10 @@ CREATE TABLE memo_resource ( updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), UNIQUE(memo_id, resource_id) ); + +-- tag +CREATE TABLE tag ( + name TEXT NOT NULL, + creator_id INTEGER NOT NULL, + UNIQUE(name, creator_id) +); diff --git a/store/store.go b/store/store.go index 04580b3f..1f80736b 100644 --- a/store/store.go +++ b/store/store.go @@ -67,6 +67,9 @@ func vacuum(ctx context.Context, tx *sql.Tx) error { return err } if err := vacuumMemoResource(ctx, tx); err != nil { + return err + } + if err := vacuumTag(ctx, tx); err != nil { // Prevent revive warning. return err } diff --git a/store/tag.go b/store/tag.go new file mode 100644 index 00000000..4a44ab39 --- /dev/null +++ b/store/tag.go @@ -0,0 +1,173 @@ +package store + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/usememos/memos/api" + "github.com/usememos/memos/common" +) + +type tagRaw struct { + Name string + CreatorID int +} + +func (raw *tagRaw) toTag() *api.Tag { + return &api.Tag{ + Name: raw.Name, + CreatorID: raw.CreatorID, + } +} + +func (s *Store) UpsertTag(ctx context.Context, upsert *api.TagUpsert) (*api.Tag, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, FormatError(err) + } + defer tx.Rollback() + + tagRaw, err := upsertTag(ctx, tx, upsert) + if err != nil { + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + tag := tagRaw.toTag() + + return tag, nil +} + +func (s *Store) FindTagList(ctx context.Context, find *api.TagFind) ([]*api.Tag, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, FormatError(err) + } + defer tx.Rollback() + + tagRawList, err := findTagList(ctx, tx, find) + if err != nil { + return nil, err + } + + list := []*api.Tag{} + for _, raw := range tagRawList { + list = append(list, raw.toTag()) + } + + return list, nil +} + +func (s *Store) DeleteTag(ctx context.Context, delete *api.TagDelete) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return FormatError(err) + } + defer tx.Rollback() + + if err := deleteTag(ctx, tx, delete); err != nil { + return FormatError(err) + } + + if err := tx.Commit(); err != nil { + return FormatError(err) + } + + return nil +} + +func upsertTag(ctx context.Context, tx *sql.Tx, upsert *api.TagUpsert) (*tagRaw, error) { + query := ` + INSERT INTO tag ( + name, creator_id + ) + VALUES (?, ?) + ON CONFLICT(name, creator_id) DO NOTHING + RETURNING name, creator_id + ` + var tagRaw tagRaw + if err := tx.QueryRowContext(ctx, query, upsert.Name, upsert.CreatorID).Scan( + &tagRaw.Name, + &tagRaw.CreatorID, + ); err != nil { + return nil, FormatError(err) + } + + return &tagRaw, nil +} + +func findTagList(ctx context.Context, tx *sql.Tx, find *api.TagFind) ([]*tagRaw, error) { + where, args := []string{"creator_id = ?"}, []interface{}{find.CreatorID} + + query := ` + SELECT + name, + creator_id + FROM tag + WHERE ` + strings.Join(where, " AND ") + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + return nil, FormatError(err) + } + defer rows.Close() + + tagRawList := make([]*tagRaw, 0) + for rows.Next() { + var tagRaw tagRaw + if err := rows.Scan( + &tagRaw.Name, + &tagRaw.CreatorID, + ); err != nil { + return nil, FormatError(err) + } + + tagRawList = append(tagRawList, &tagRaw) + } + + if err := rows.Err(); err != nil { + return nil, FormatError(err) + } + + return tagRawList, nil +} + +func deleteTag(ctx context.Context, tx *sql.Tx, delete *api.TagDelete) error { + where, args := []string{"name = ?", "creator_id = ?"}, []interface{}{delete.Name, delete.CreatorID} + + stmt := `DELETE FROM tag WHERE ` + strings.Join(where, " AND ") + result, err := tx.ExecContext(ctx, stmt, args...) + if err != nil { + return FormatError(err) + } + + rows, _ := result.RowsAffected() + if rows == 0 { + return &common.Error{Code: common.NotFound, Err: fmt.Errorf("tag not found")} + } + + return nil +} + +func vacuumTag(ctx context.Context, tx *sql.Tx) error { + stmt := ` + DELETE FROM + tag + WHERE + creator_id NOT IN ( + SELECT + id + FROM + user + )` + _, err := tx.ExecContext(ctx, stmt) + if err != nil { + return FormatError(err) + } + + return nil +}