feat: [CODE-3883]: Add controller and handler layer for favorites (#3806)

* feat: [CODE-3833]: [CODE-3844]: merged handler layer code to controller layer
* feat: [CODE-3884]: Add handler layer for favorites (#3808)

* format router file
* Merge branch 'CODE-3883' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into CODE-3884
* feat: [CODE-3884]: moved favorite handler under user handler
* merge develop
* feat: [CODE-3900]: Add and populate is_favorite field in RepositoryOutput (#3821)

* addressed review comments
* feat: [CODE-3900]: Add and populate is_favorite field in RepositoryOutput
* feat: [CODE-3884]: Add handler layer for favorites
* format code
* feat: [CODE-3833]: moved favorite controller files under user controller directory
* removed unwanted comment
* Merge branch 'main' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into CODE-3883
* addressed review comments
* merge develop
* feat: [CODE-3883]: Add controller layer for favorites
* feat: [CODE-3870]: Add only_favorites query param t
This commit is contained in:
Karan Saraswat 2025-05-26 10:49:37 +00:00 committed by Harness
parent 812acb6b53
commit 6315cecfbd
11 changed files with 237 additions and 12 deletions

View File

@ -19,6 +19,7 @@ import (
"github.com/harness/gitness/app/auth/authz"
userevents "github.com/harness/gitness/app/events/user"
"github.com/harness/gitness/app/services/refcache"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types"
@ -37,6 +38,8 @@ type Controller struct {
membershipStore store.MembershipStore
publicKeyStore store.PublicKeyStore
eventReporter *userevents.Reporter
repoFinder refcache.RepoFinder
favoriteStore store.FavoriteStore
}
func NewController(
@ -48,6 +51,8 @@ func NewController(
membershipStore store.MembershipStore,
publicKeyStore store.PublicKeyStore,
eventReporter *userevents.Reporter,
repoFinder refcache.RepoFinder,
favoriteStore store.FavoriteStore,
) *Controller {
return &Controller{
tx: tx,
@ -58,6 +63,8 @@ func NewController(
membershipStore: membershipStore,
publicKeyStore: publicKeyStore,
eventReporter: eventReporter,
repoFinder: repoFinder,
favoriteStore: favoriteStore,
}
}

View File

@ -0,0 +1,56 @@
// Copyright 2023 Harness, Inc.
//
// 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 user
import (
"context"
"fmt"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
func (c *Controller) CreateFavorite(
ctx context.Context,
session *auth.Session,
in *types.FavoriteResource,
) (*types.FavoriteResource, error) {
switch in.Type { // nolint:exhaustive
case enum.ResourceTypeRepo:
repo, err := c.repoFinder.FindByID(ctx, in.ID)
if err != nil {
return nil, fmt.Errorf("couldn't fetch repo for the user: %w", err)
}
if err = apiauth.CheckRepo(
ctx,
c.authorizer,
session,
repo,
enum.PermissionRepoView); err != nil {
return nil, err
}
in.ID = repo.ID
default:
return nil, fmt.Errorf("resource not onboarded to favorites: %s", in.Type)
}
if err := c.favoriteStore.Create(ctx, session.Principal.ID, in); err != nil {
return nil, fmt.Errorf("failed to mark %s %d as favorite: %w", in.Type, in.ID, err)
}
return in, nil
}

View File

@ -0,0 +1,56 @@
// Copyright 2023 Harness, Inc.
//
// 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 user
import (
"context"
"fmt"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
func (c *Controller) DeleteFavorite(
ctx context.Context,
session *auth.Session,
in *types.FavoriteResource,
) error {
switch in.Type { // nolint:exhaustive
case enum.ResourceTypeRepo:
repo, err := c.repoFinder.FindByID(ctx, in.ID)
if err != nil {
return fmt.Errorf("couldn't fetch repo for the user: %w", err)
}
if err = apiauth.CheckRepo(
ctx,
c.authorizer,
session,
repo,
enum.PermissionRepoView); err != nil {
return err
}
in.ID = repo.ID
default:
return fmt.Errorf("resource not onboarded to favorites: %s", in.Type)
}
if err := c.favoriteStore.Delete(ctx, session.Principal.ID, in); err != nil {
return err
}
return nil
}

View File

@ -17,6 +17,7 @@ package user
import (
"github.com/harness/gitness/app/auth/authz"
userevents "github.com/harness/gitness/app/events/user"
"github.com/harness/gitness/app/services/refcache"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types/check"
@ -24,7 +25,6 @@ import (
"github.com/google/wire"
)
// WireSet provides a wire set for this package.
var WireSet = wire.NewSet(
ProvideController,
)
@ -38,6 +38,8 @@ func ProvideController(
membershipStore store.MembershipStore,
publicKeyStore store.PublicKeyStore,
eventReporter *userevents.Reporter,
repoFinder refcache.RepoFinder,
favoriteStore store.FavoriteStore,
) *Controller {
return NewController(
tx,
@ -47,5 +49,7 @@ func ProvideController(
tokenStore,
membershipStore,
publicKeyStore,
eventReporter)
eventReporter,
repoFinder,
favoriteStore)
}

View File

@ -0,0 +1,48 @@
// Copyright 2023 Harness, Inc.
//
// 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 user
import (
"encoding/json"
"net/http"
"github.com/harness/gitness/app/api/controller/user"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/types"
)
// HandleCreateFavorite returns a http.HandlerFunc that creates a new favorite.
func HandleCreateFavorite(userCtrl *user.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
in := new(types.FavoriteResource)
err := json.NewDecoder(r.Body).Decode(in)
if err != nil {
render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err)
return
}
favoriteResource, err := userCtrl.CreateFavorite(ctx, session, in)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.JSON(w, http.StatusCreated, favoriteResource)
}
}

View File

@ -0,0 +1,48 @@
// Copyright 2023 Harness, Inc.
//
// 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 user
import (
"encoding/json"
"net/http"
"github.com/harness/gitness/app/api/controller/user"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/types"
)
// HandleDeleteFavorite returns a http.HandlerFunc that delete a favorite.
func HandleDeleteFavorite(userCtrl *user.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
in := new(types.FavoriteResource)
err := json.NewDecoder(r.Body).Decode(in)
if err != nil {
render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err)
return
}
err = userCtrl.DeleteFavorite(ctx, session, in)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.DeleteSuccessful(w)
}
}

View File

@ -806,6 +806,12 @@ func setupUser(r chi.Router, userCtrl *user.Controller) {
r.Delete(fmt.Sprintf("/{%s}", request.PathParamPublicKeyIdentifier),
handleruser.HandleDeletePublicKey(userCtrl))
})
// Favorites
r.Route("/favorite", func(r chi.Router) {
r.Post("/", handleruser.HandleCreateFavorite(userCtrl))
r.Delete("/", handleruser.HandleDeleteFavorite(userCtrl))
})
})
}

View File

@ -1377,13 +1377,13 @@ type (
}
FavoriteStore interface {
Create(ctx context.Context, in *types.FavoriteResource) error
Create(ctx context.Context, principalID int64, in *types.FavoriteResource) error
Map(
ctx context.Context,
principalID int64,
resourceType enum.ResourceType,
resourceIDs []int64,
) (map[int64]bool, error)
Delete(ctx context.Context, in *types.FavoriteResource) error
Delete(ctx context.Context, principalID int64, in *types.FavoriteResource) error
}
)

View File

@ -49,7 +49,7 @@ type favorite struct {
}
// Create marks the resource as favorite.
func (s *FavoriteStore) Create(ctx context.Context, in *types.FavoriteResource) error {
func (s *FavoriteStore) Create(ctx context.Context, principalID int64, in *types.FavoriteResource) error {
tableName, resourceColumnName, err := getTableAndColumnName(in.Type)
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "failed to fetch table and column name for favorite resource")
@ -61,7 +61,7 @@ func (s *FavoriteStore) Create(ctx context.Context, in *types.FavoriteResource)
query, arg, err := db.BindNamed(favoriteResourceInsert, favorite{
ResourceID: in.ID,
PrincipalID: in.PrincipalID,
PrincipalID: principalID,
Created: time.Now().UnixMilli(),
})
if err != nil {
@ -122,7 +122,7 @@ func (s *FavoriteStore) Map(
}
// Delete unfavorites the resource.
func (s *FavoriteStore) Delete(ctx context.Context, in *types.FavoriteResource) error {
func (s *FavoriteStore) Delete(ctx context.Context, principalID int64, in *types.FavoriteResource) error {
tableName, resourceColumnName, err := getTableAndColumnName(in.Type)
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "failed to fetch table and column name for favorite resource")
@ -132,7 +132,7 @@ func (s *FavoriteStore) Delete(ctx context.Context, in *types.FavoriteResource)
db := dbtx.GetAccessor(ctx, s.db)
if _, err := db.ExecContext(ctx, favoriteResourceDelete, in.ID, in.PrincipalID); err != nil {
if _, err := db.ExecContext(ctx, favoriteResourceDelete, in.ID, principalID); err != nil {
return database.ProcessSQLErrorf(ctx, err, "delete query failed for %s", tableName)
}

View File

@ -206,7 +206,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil {
return nil, err
}
controller := user.ProvideController(transactor, principalUID, authorizer, principalStore, tokenStore, membershipStore, publicKeyStore, reporter)
favoriteStore := database.ProvideFavoriteStore(db)
controller := user.ProvideController(transactor, principalUID, authorizer, principalStore, tokenStore, membershipStore, publicKeyStore, reporter, repoFinder, favoriteStore)
serviceController := service.NewController(principalUID, authorizer, principalStore)
bootstrapBootstrap := bootstrap.ProvideBootstrap(config, controller, serviceController)
authenticator := authn.ProvideAuthenticator(config, principalStore, tokenStore)

View File

@ -17,7 +17,6 @@ package types
import "github.com/harness/gitness/types/enum"
type FavoriteResource struct {
ID int64 `json:"resource_id"`
Type enum.ResourceType `json:"resource_type"`
PrincipalID int64
ID int64 `json:"resource_id"`
Type enum.ResourceType `json:"resource_type"`
}