-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add database method to create user saved search (#815)
For creating: - In the same transaction it will check: - If the user does not owned too many saved searches. - Add the user as an admin - Add the new saved search as a bookmark We also introduce the search config struct which holds an app wide configuration for the max number of saved searches a user can have. Fixes: #804
- Loading branch information
1 parent
0ccb2a5
commit 187bbc2
Showing
6 changed files
with
233 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
// 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" | ||
"errors" | ||
"fmt" | ||
|
||
"cloud.google.com/go/spanner" | ||
"github.com/google/uuid" | ||
) | ||
|
||
// CreateUserSavedSearchRequest is the request to create a new user saved search. | ||
type CreateUserSavedSearchRequest struct { | ||
Name string | ||
Query string | ||
OwnerUserID string | ||
} | ||
|
||
var ( | ||
// ErrOwnerSavedSearchLimitExceeded indicates that the user already has | ||
// reached the limit of saved searches that a given user can own. | ||
ErrOwnerSavedSearchLimitExceeded = errors.New("saved search limit reached") | ||
) | ||
|
||
// CreateNewUserSavedSearch creates a new user-owned saved search. | ||
// It returns the ID of the newly created saved search if successful. | ||
func (c *Client) CreateNewUserSavedSearch( | ||
ctx context.Context, | ||
newSearch CreateUserSavedSearchRequest) (*string, error) { | ||
id := uuid.NewString() | ||
_, err := c.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { | ||
// 1. Read the current count of owned searches | ||
var count int64 | ||
stmt := spanner.Statement{ | ||
SQL: fmt.Sprintf(`SELECT COUNT(*) | ||
FROM %s | ||
WHERE UserID = @OwnerID AND UserRole = @Role`, savedSearchUserRolesTable), | ||
Params: map[string]interface{}{ | ||
"OwnerID": newSearch.OwnerUserID, | ||
"Role": SavedSearchOwner, | ||
}, | ||
} | ||
row, err := txn.Query(ctx, stmt).Next() | ||
if err != nil { | ||
return err | ||
} | ||
if err := row.Columns(&count); err != nil { | ||
return err | ||
} | ||
|
||
// 2. Check against the limit | ||
if count >= int64(c.searchCfg.maxOwnedSearchesPerUser) { | ||
return ErrOwnerSavedSearchLimitExceeded | ||
} | ||
|
||
var mutations []*spanner.Mutation | ||
// TODO: In the future, look into using an entityMapper for SavedSearch. | ||
// Then, we can use createInsertMutation. | ||
m1, err := spanner.InsertStruct(savedSearchesTable, SavedSearch{ | ||
ID: id, | ||
Name: newSearch.Name, | ||
Query: newSearch.Query, | ||
Scope: UserPublicScope, | ||
AuthorID: newSearch.OwnerUserID, | ||
CreatedAt: spanner.CommitTimestamp, | ||
UpdatedAt: spanner.CommitTimestamp, | ||
}) | ||
if err != nil { | ||
return errors.Join(ErrInternalQueryFailure, err) | ||
} | ||
mutations = append(mutations, m1) | ||
|
||
// TODO: In the future, look into using an entityMapper for SavedSearchUserRole. | ||
// Then, we can use createInsertMutation. | ||
m2, err := spanner.InsertStruct(savedSearchUserRolesTable, SavedSearchUserRole{ | ||
SavedSearchID: id, | ||
UserID: newSearch.OwnerUserID, | ||
UserRole: SavedSearchOwner, | ||
}) | ||
if err != nil { | ||
return errors.Join(ErrInternalQueryFailure, err) | ||
} | ||
mutations = append(mutations, m2) | ||
|
||
// TODO: In the future, look into using an entityMapper for UserSavedSearchBookmark. | ||
// Then, we can use createInsertMutation. | ||
m3, err := spanner.InsertStruct(userSavedSearchBookmarksTable, UserSavedSearchBookmark{ | ||
SavedSearchID: id, | ||
UserID: newSearch.OwnerUserID, | ||
}) | ||
if err != nil { | ||
return errors.Join(ErrInternalQueryFailure, err) | ||
} | ||
mutations = append(mutations, m3) | ||
|
||
err = txn.BufferWrite(mutations) | ||
if err != nil { | ||
return errors.Join(ErrInternalQueryFailure, err) | ||
} | ||
|
||
return nil | ||
}) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &id, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// 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" | ||
"errors" | ||
"testing" | ||
) | ||
|
||
func TestCreateNewUserSavedSearch(t *testing.T) { | ||
restartDatabaseContainer(t) | ||
ctx := context.Background() | ||
// Reset the max to 2. | ||
spannerClient.searchCfg.maxOwnedSearchesPerUser = 2 | ||
|
||
t.Run("create fails after reaching limit", func(t *testing.T) { | ||
savedSearchID1, 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 savedSearchID1 == nil { | ||
t.Error("expected non-nil id.") | ||
} | ||
|
||
savedSearchID2, err := spannerClient.CreateNewUserSavedSearch(ctx, CreateUserSavedSearchRequest{ | ||
Name: "my little search part 2", | ||
Query: "group:avif", | ||
OwnerUserID: "userID1", | ||
}) | ||
if err != nil { | ||
t.Errorf("expected nil error. received %s", err) | ||
} | ||
if savedSearchID2 == nil { | ||
t.Error("expected non-nil id.") | ||
} | ||
|
||
savedSearchID3, err := spannerClient.CreateNewUserSavedSearch(ctx, CreateUserSavedSearchRequest{ | ||
Name: "my little search part 3", | ||
Query: "name:subgrid", | ||
OwnerUserID: "userID1", | ||
}) | ||
if !errors.Is(err, ErrOwnerSavedSearchLimitExceeded) { | ||
t.Errorf("unexpected error. received %v", err) | ||
} | ||
if savedSearchID3 != nil { | ||
t.Error("expected nil id.") | ||
} | ||
}) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters