Skip to content

Commit

Permalink
Add database methods to add and remove bookmarks (#821)
Browse files Browse the repository at this point in the history
For now: since all the searches are public, we don't do any access checking.
  • Loading branch information
jcscottiii authored Oct 29, 2024
1 parent 5ad5de5 commit 0da9899
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 4 deletions.
4 changes: 0 additions & 4 deletions lib/gcpspanner/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,14 +471,12 @@ func (c *entityWriter[M, ExternalStruct, SpannerStruct, ExternalKey]) updateWith
}

// removableEntityMapper extends writeableEntityMapper with the ability to remove an entity.
// nolint:unused // TODO: Remove nolint directive once the interface is used.
type removableEntityMapper[ExternalStruct any, SpannerStruct any, ExternalKey any] interface {
writeableEntityMapper[ExternalStruct, SpannerStruct, ExternalKey]
DeleteKey(ExternalKey) spanner.Key
}

// entityRemover is a basic client for removing any row from the database.
// nolint:unused // TODO: Remove nolint directive once the type is used.
type entityRemover[
M removableEntityMapper[ExternalStruct, SpannerStruct, ExternalKey],
ExternalStruct any,
Expand All @@ -488,7 +486,6 @@ type entityRemover[
}

// remove performs an delete operation on an entity.
// nolint:unused // TODO: Remove nolint directive once the method is used.
func (c *entityRemover[M, ExternalStruct, SpannerStruct, ExternalKey]) remove(ctx context.Context,
input ExternalStruct) error {
_, err := c.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
Expand Down Expand Up @@ -559,7 +556,6 @@ func newEntityReader[
return &entityReader[M, ExternalStruct, SpannerStruct, ExternalKey]{c}
}

// nolint:unused // TODO: Remove nolint directive once the method is used.
func newEntityRemover[
M removableEntityMapper[ExternalStruct, SpannerStruct, ExternalKey],
SpannerStruct any,
Expand Down
60 changes: 60 additions & 0 deletions lib/gcpspanner/user_search_bookmarks.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,70 @@

package gcpspanner

import (
"context"
"fmt"

"cloud.google.com/go/spanner"
)

// UserSavedSearchBookmark represents a user's bookmark for a saved search.
type UserSavedSearchBookmark struct {
UserID string `spanner:"UserID"`
SavedSearchID string `spanner:"SavedSearchID"`
}

const userSavedSearchBookmarksTable = "UserSavedSearchBookmarks"

// Implements the entityMapper interface for UserSavedSearchBookmark.
type userSavedSearchBookmarkMapper struct{}

func (m userSavedSearchBookmarkMapper) Table() string {
return userSavedSearchBookmarksTable
}

type userSavedSearchBookmarkKey struct {
UserSavedSearchBookmark
}

func (m userSavedSearchBookmarkMapper) GetKey(
in UserSavedSearchBookmark) userSavedSearchBookmarkKey {
return userSavedSearchBookmarkKey{
UserSavedSearchBookmark: in,
}
}

func (m userSavedSearchBookmarkMapper) Merge(
_ UserSavedSearchBookmark, existing UserSavedSearchBookmark) UserSavedSearchBookmark {
return existing
}

func (m userSavedSearchBookmarkMapper) SelectOne(
key userSavedSearchBookmarkKey) spanner.Statement {
stmt := spanner.NewStatement(fmt.Sprintf(`
SELECT
SavedSearchID, UserID
FROM %s
WHERE UserID = @userID AND SavedSearchID = @savedSearchID
LIMIT 1`,
m.Table()))
parameters := map[string]interface{}{
"userID": key.UserID,
"savedSearchID": key.SavedSearchID,
}
stmt.Params = parameters

return stmt
}

func (m userSavedSearchBookmarkMapper) DeleteKey(key userSavedSearchBookmarkKey) spanner.Key {
return spanner.Key{key.UserID, key.SavedSearchID}
}

func (c *Client) AddUserSearchBookmark(ctx context.Context, req UserSavedSearchBookmark) error {
return newEntityWriter[userSavedSearchBookmarkMapper](c).upsert(ctx, req)
}

func (c *Client) DeleteUserSearchBookmark(ctx context.Context, req UserSavedSearchBookmark) error {
return newEntityRemover[userSavedSearchBookmarkMapper](c).remove(ctx, req)
}
111 changes: 111 additions & 0 deletions lib/gcpspanner/user_search_bookmarks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gcpspanner

import (
"context"
"testing"

"cloud.google.com/go/spanner"
)

func TestUserSearchBookmark(t *testing.T) {
restartDatabaseContainer(t)
ctx := context.Background()

savedSearchID, err := spannerClient.CreateNewUserSavedSearch(ctx, CreateUserSavedSearchRequest{
Name: "my little search",
Query: "group:css",
OwnerUserID: "userID1",
})
if err != nil {
t.Errorf("expected nil error. received %s", err)
}
if savedSearchID == nil {
t.Fatal("expected non-nil id.")
}

const testUser = "test-user"

// user initially does not have a bookmark
expectedSavedSearch := &UserSavedSearch{
IsBookmarked: valuePtr(false),
Role: nil,
SavedSearch: SavedSearch{
ID: *savedSearchID,
Name: "my little search",
Query: "group:css",
Scope: "USER_PUBLIC",
AuthorID: "userID1",
// Don't actually compare the last two values.
CreatedAt: spanner.CommitTimestamp,
UpdatedAt: spanner.CommitTimestamp,
},
}
actual, err := spannerClient.GetUserSavedSearch(ctx, *savedSearchID, valuePtr(testUser))
if err != nil {
t.Errorf("expected nil error. received %s", err)
}
if !userSavedSearchEquality(expectedSavedSearch, actual) {
t.Errorf("different saved searches\nexpected: %+v\nreceived: %v", expectedSavedSearch, actual)
}

// user can successfully have a bookmark added
expectedSavedSearchAfter := &UserSavedSearch{
IsBookmarked: valuePtr(true),
Role: nil,
SavedSearch: SavedSearch{
ID: *savedSearchID,
Name: "my little search",
Query: "group:css",
Scope: "USER_PUBLIC",
AuthorID: "userID1",
// Don't actually compare the last two values.
CreatedAt: spanner.CommitTimestamp,
UpdatedAt: spanner.CommitTimestamp,
},
}
err = spannerClient.AddUserSearchBookmark(ctx, UserSavedSearchBookmark{
UserID: testUser,
SavedSearchID: *savedSearchID,
})
if err != nil {
t.Errorf("expected nil error. received %s", err)
}
actual, err = spannerClient.GetUserSavedSearch(ctx, *savedSearchID, valuePtr(testUser))
if err != nil {
t.Errorf("expected nil error. received %s", err)
}
if !userSavedSearchEquality(expectedSavedSearchAfter, actual) {
t.Errorf("different saved searches\nexpected: %+v\nreceived: %v", expectedSavedSearchAfter, actual)
}

// user can successfully have a bookmark removed
err = spannerClient.DeleteUserSearchBookmark(ctx, UserSavedSearchBookmark{
UserID: testUser,
SavedSearchID: *savedSearchID,
})
if err != nil {
t.Errorf("expected nil error. received %s", err)
}
actual, err = spannerClient.GetUserSavedSearch(ctx, *savedSearchID, valuePtr(testUser))
if err != nil {
t.Errorf("expected nil error. received %s", err)
}
if !userSavedSearchEquality(expectedSavedSearch, actual) {
t.Errorf("different saved searches\nexpected: %+v\nreceived: %v", expectedSavedSearch, actual)
}

}

0 comments on commit 0da9899

Please sign in to comment.