From 0d89fdc0a83b1fa3f52e1ddd61a8c9899cd91652 Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Thu, 7 Dec 2023 12:07:43 +0530 Subject: [PATCH 1/2] fix: Add missing body close calls after reading from request Signed-off-by: jay-dee7 --- auth/server/webauthn_server.go | 5 ++++- auth/signup.go | 2 +- orgmode/admin.go | 1 + registry/v2/repository.go | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/auth/server/webauthn_server.go b/auth/server/webauthn_server.go index 63736a91..a8fc789a 100644 --- a/auth/server/webauthn_server.go +++ b/auth/server/webauthn_server.go @@ -348,6 +348,10 @@ func (wa *webauthn_server) BeginLogin(ctx echo.Context) error { }, } + defer func() { + ctx.Request().Body.Close() + }() + credentialAssertion, err := wa.webauthn.BeginLogin(ctx.Request().Context(), opts) if err != nil { if werr := wa.webauthn.RemoveSessionData(ctx.Request().Context(), user.ID); werr != nil { @@ -366,7 +370,6 @@ func (wa *webauthn_server) BeginLogin(ctx echo.Context) error { wa.logger.Log(ctx, err).Send() return echoErr } - defer ctx.Request().Body.Close() echoErr := ctx.JSON(http.StatusOK, echo.Map{ "options": credentialAssertion, diff --git a/auth/signup.go b/auth/signup.go index d57627e0..407fb130 100644 --- a/auth/signup.go +++ b/auth/signup.go @@ -19,7 +19,7 @@ import ( func (a *auth) parseSignUpRequest(ctx echo.Context) (*types.User, error) { var user types.User if err := json.NewDecoder(ctx.Request().Body).Decode(&user); err != nil { - return nil, err + return nil, fmt.Errorf("error parsing signup request: %w", err) } defer ctx.Request().Body.Close() diff --git a/orgmode/admin.go b/orgmode/admin.go index 3b9ea8ab..13039372 100644 --- a/orgmode/admin.go +++ b/orgmode/admin.go @@ -35,6 +35,7 @@ func (o *orgMode) AllowOrgAdmin() echo.MiddlewareFunc { o.logger.Log(ctx, err).Send() return echoErr } + defer ctx.Request().Body.Close() // only allow self-migrate if !strings.EqualFold(user.ID.String(), body.UserID.String()) { diff --git a/registry/v2/repository.go b/registry/v2/repository.go index 5c7a4894..b2bfcbd8 100644 --- a/registry/v2/repository.go +++ b/registry/v2/repository.go @@ -36,6 +36,7 @@ func (r *registry) CreateRepository(ctx echo.Context) error { "message": "error parsing request input", }) } + defer ctx.Request().Body.Close() if err = body.Validate(); err != nil { return ctx.JSON(http.StatusBadRequest, echo.Map{ From 084b9d306cd3d81dae6ca41ba7404d74acd22fc1 Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Thu, 7 Dec 2023 12:12:16 +0530 Subject: [PATCH 2/2] feat: APIs to add repositories to favorites and store repo pull count Signed-off-by: jay-dee7 --- registry/v2/extensions/analytics.go | 73 ++++++++++++ registry/v2/extensions/catalog_detail.go | 2 + registry/v2/registry.go | 11 +- router/middlewares.go | 2 +- router/registry.go | 2 + router/route_names.go | 1 + router/router.go | 10 +- store/v1/registry/registry_impl.go | 140 ++++++++++++++++++----- store/v1/registry/store.go | 3 + store/v1/types/registry.go | 2 + store/v1/types/users.go | 24 ++-- 11 files changed, 226 insertions(+), 44 deletions(-) create mode 100644 registry/v2/extensions/analytics.go diff --git a/registry/v2/extensions/analytics.go b/registry/v2/extensions/analytics.go new file mode 100644 index 00000000..828077e8 --- /dev/null +++ b/registry/v2/extensions/analytics.go @@ -0,0 +1,73 @@ +package extensions + +import ( + "net/http" + "time" + + "github.com/containerish/OpenRegistry/store/v1/types" + "github.com/google/uuid" + "github.com/labstack/echo/v4" +) + +type FavoriteRepositoryRequest struct { + RepositoryID uuid.UUID `json:"repository_id" query:"repository_id"` + UserID uuid.UUID `json:"user_id" query:"user_id"` +} + +func (ext *extension) AddRepositoryToFavorites(ctx echo.Context) error { + ctx.Set(types.HandlerStartTime, time.Now()) + + var body FavoriteRepositoryRequest + if err := ctx.Bind(&body); err != nil { + echoErr := ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + }) + ext.logger.Log(ctx, err).Send() + return echoErr + } + defer ctx.Request().Body.Close() + + err := ext.store.AddRepositoryToFavorites(ctx.Request().Context(), body.RepositoryID, body.UserID) + if err != nil { + echoErr := ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + }) + ext.logger.Log(ctx, err).Send() + return echoErr + } + + echoErr := ctx.JSON(http.StatusOK, echo.Map{ + "message": "repository added to favorites", + }) + ext.logger.Log(ctx, nil).Send() + return echoErr +} + +func (ext *extension) RemoveRepositoryFromFavorites(ctx echo.Context) error { + ctx.Set(types.HandlerStartTime, time.Now()) + + var body FavoriteRepositoryRequest + if err := ctx.Bind(&body); err != nil { + echoErr := ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + }) + ext.logger.Log(ctx, err).Send() + return echoErr + } + defer ctx.Request().Body.Close() + + err := ext.store.RemoveRepositoryFromFavorites(ctx.Request().Context(), body.RepositoryID, body.UserID) + if err != nil { + echoErr := ctx.JSON(http.StatusBadRequest, echo.Map{ + "error": err.Error(), + }) + ext.logger.Log(ctx, err).Send() + return echoErr + } + + echoErr := ctx.JSON(http.StatusOK, echo.Map{ + "message": "repository removed from favorites", + }) + ext.logger.Log(ctx, nil).Send() + return echoErr +} diff --git a/registry/v2/extensions/catalog_detail.go b/registry/v2/extensions/catalog_detail.go index 23f74599..0b884f97 100644 --- a/registry/v2/extensions/catalog_detail.go +++ b/registry/v2/extensions/catalog_detail.go @@ -18,6 +18,8 @@ type Extenion interface { ChangeContainerImageVisibility(ctx echo.Context) error PublicCatalog(ctx echo.Context) error GetUserCatalog(ctx echo.Context) error + AddRepositoryToFavorites(ctx echo.Context) error + RemoveRepositoryFromFavorites(ctx echo.Context) error } type extension struct { diff --git a/registry/v2/registry.go b/registry/v2/registry.go index 63179642..23fabb76 100644 --- a/registry/v2/registry.go +++ b/registry/v2/registry.go @@ -226,7 +226,8 @@ func (r *registry) PullManifest(ctx echo.Context) error { manifest, err := r.store.GetManifestByReference(ctx.Request().Context(), namespace, ref) if err != nil { - errMsg := common.RegistryErrorResponse(RegistryErrorCodeManifestUnknown, err.Error(), echo.Map{ + errMsg := common.RegistryErrorResponse(RegistryErrorCodeManifestUnknown, "manifest not found", echo.Map{ + "error": err.Error(), "namespace": namespace, "ref": ref, }) @@ -235,6 +236,14 @@ func (r *registry) PullManifest(ctx echo.Context) error { return echoErr } + defer func() { + err = r.store.IncrementRepositoryPullCounter(ctx.Request().Context(), manifest.RepositoryID) + // silently fail + if err != nil { + r.logger.DebugWithContext(ctx).Err(err).Send() + } + }() + trimmedMf := manifest.ToOCISubject() ctx.Response().Header().Set("Docker-Content-Digest", manifest.Digest) ctx.Response().Header().Set("Content-Type", manifest.MediaType) diff --git a/router/middlewares.go b/router/middlewares.go index afa34797..55fd2ec7 100644 --- a/router/middlewares.go +++ b/router/middlewares.go @@ -25,7 +25,7 @@ func registryNamespaceValidator(logger telemetry.Logger) echo.MiddlewareFunc { } namespace := ctx.Param("username") + "/" + ctx.Param("imagename") - if !nsRegex.MatchString(namespace) { + if namespace != "/" && !nsRegex.MatchString(namespace) { registryErr := common.RegistryErrorResponse( registry.RegistryErrorCodeNameInvalid, "invalid user namespace", diff --git a/router/registry.go b/router/registry.go index c06c3066..8352baea 100644 --- a/router/registry.go +++ b/router/registry.go @@ -104,4 +104,6 @@ func RegisterExtensionsRoutes( group.Add(http.MethodGet, UserCatalog, ext.GetUserCatalog, middlewares...) group.Add(http.MethodPost, ChangeRepositoryVisibility, ext.ChangeContainerImageVisibility, middlewares...) group.Add(http.MethodPost, CreateRepository, reg.CreateRepository, middlewares...) + group.Add(http.MethodPost, RepositoryFavorites, ext.AddRepositoryToFavorites, middlewares...) + group.Add(http.MethodDelete, RepositoryFavorites, ext.RemoveRepositoryFromFavorites, middlewares...) } diff --git a/router/route_names.go b/router/route_names.go index 1b61f30e..bdba115a 100644 --- a/router/route_names.go +++ b/router/route_names.go @@ -74,4 +74,5 @@ const ( ChangeRepositoryVisibility = Ext + "/repository/visibility" CreateRepository = Ext + "/repository/create" + RepositoryFavorites = Ext + "/repository/favorites" ) diff --git a/router/router.go b/router/router.go index 818ff560..abb5ba4f 100644 --- a/router/router.go +++ b/router/router.go @@ -38,9 +38,7 @@ func Register( usersStore users_store.UserStore, automationStore automation.BuildAutomationStore, ) *echo.Echo { - e := echo.New() - - setDefaultEchoOptions(e, cfg.WebAppConfig, healthCheckApi) + e := setDefaultEchoOptions(cfg.WebAppConfig, healthCheckApi) baseAPIRouter := e.Group("/api") githubRouter := e.Group("/github") @@ -63,6 +61,7 @@ func Register( RegisterExtensionsRoutes(ociRouter, registryApi, extensionsApi) RegisterWebauthnRoutes(webauthnRouter, webauthnApi) RegisterOrgModeRoutes(orgModeRouter, orgModeApi) + if cfg.Integrations.GetGithubConfig() != nil && cfg.Integrations.GetGithubConfig().Enabled { RegisterGitHubRoutes( githubRouter, @@ -96,7 +95,8 @@ func Register( return e } -func setDefaultEchoOptions(e *echo.Echo, webConfig config.WebAppConfig, healthCheck http.HandlerFunc) { +func setDefaultEchoOptions(webConfig config.WebAppConfig, healthCheck http.HandlerFunc) *echo.Echo { + e := echo.New() e.HideBanner = true e.Use(middleware.Recover()) @@ -123,4 +123,6 @@ func setDefaultEchoOptions(e *echo.Echo, webConfig config.WebAppConfig, healthCh p.Use(e) e.Add(http.MethodGet, "/health", echo.WrapHandler(healthCheck)) + + return e } diff --git a/store/v1/registry/registry_impl.go b/store/v1/registry/registry_impl.go index d3fabe83..5d3a9c31 100644 --- a/store/v1/registry/registry_impl.go +++ b/store/v1/registry/registry_impl.go @@ -7,7 +7,7 @@ import ( "strings" "time" - v2 "github.com/containerish/OpenRegistry/store/v1" + v1 "github.com/containerish/OpenRegistry/store/v1" "github.com/containerish/OpenRegistry/store/v1/types" "github.com/google/uuid" oci_digest "github.com/opencontainers/go-digest" @@ -52,7 +52,7 @@ func (s *registryStore) CreateRepository(ctx context.Context, repository *types. if _, err := s.db.NewInsert().Model(repository).Exec(ctx); err != nil { logEvent.Err(err).Send() - return v2.WrapDatabaseError(err, v2.DatabaseOperationWrite) + return v1.WrapDatabaseError(err, v1.DatabaseOperationWrite) } logEvent.Bool("success", true).Send() @@ -65,7 +65,7 @@ func (s *registryStore) GetRepositoryByID(ctx context.Context, ID uuid.UUID) (*t repository := &types.ContainerImageRepository{ID: ID} if err := s.db.NewSelect().Model(repository).WherePK().Scan(ctx); err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -95,7 +95,7 @@ func (s *registryStore) GetRepositoryByNamespace( Scan(ctx) if err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -128,7 +128,7 @@ func (s *registryStore) GetRepositoryByName( Scan(ctx) if err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -141,7 +141,7 @@ func (s *registryStore) DeleteLayerByDigestWithTxn(ctx context.Context, txn *bun _, err := txn.NewDelete().Model(&types.ContainerImageLayer{}).Where("digest = ?", digest).Exec(ctx) if err != nil { logEvent.Err(err).Send() - return v2.WrapDatabaseError(err, v2.DatabaseOperationDelete) + return v1.WrapDatabaseError(err, v1.DatabaseOperationDelete) } return nil @@ -153,7 +153,7 @@ func (s *registryStore) DeleteLayerByDigest(ctx context.Context, digest string) _, err := s.db.NewDelete().Model(&types.ContainerImageLayer{}).Where("digest = ?", digest).Exec(ctx) if err != nil { logEvent.Err(err).Send() - return v2.WrapDatabaseError(err, v2.DatabaseOperationDelete) + return v1.WrapDatabaseError(err, v1.DatabaseOperationDelete) } logEvent.Bool("success", true).Send() @@ -174,7 +174,7 @@ func (s *registryStore) DeleteManifestOrTag(ctx context.Context, reference strin if err != nil { logEvent.Err(err).Send() - return v2.WrapDatabaseError(err, v2.DatabaseOperationDelete) + return v1.WrapDatabaseError(err, v1.DatabaseOperationDelete) } logEvent.Bool("success", true).Send() @@ -193,7 +193,7 @@ func (s *registryStore) DeleteManifestOrTagWithTxn(ctx context.Context, txn *bun if err != nil { logEvent.Err(err).Send() - return v2.WrapDatabaseError(err, v2.DatabaseOperationDelete) + return v1.WrapDatabaseError(err, v1.DatabaseOperationDelete) } logEvent.Bool("success", true).Send() @@ -218,7 +218,7 @@ func (s *registryStore) GetCatalog( Where("name = ? and visibility = ?", repositoryName, types.RepositoryVisibilityPublic). Scan(ctx) if err != nil { - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } namespaceList := make([]string, len(catalog)) @@ -243,7 +243,7 @@ func (s *registryStore) GetPublicRepositories( Where("visibility = ?", types.RepositoryVisibilityPublic). ScanAndCount(ctx) if err != nil { - return nil, 0, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, 0, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } return repositories, total, nil @@ -276,7 +276,7 @@ func (s *registryStore) GetUserRepositories( }). ScanAndCount(ctx) if err != nil { - return nil, 0, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, 0, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } return repositories, total, nil @@ -305,7 +305,7 @@ func (s *registryStore) GetCatalogCount(ctx context.Context, namespace string) ( count, err := stmnt.Count(ctx) if err != nil { logEvent.Err(err).Send() - return 0, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return 0, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -344,7 +344,7 @@ func (s *registryStore) GetCatalogDetail( err := stmnt.Scan(ctx) if err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -358,7 +358,7 @@ func (s *registryStore) GetContentHashById(ctx context.Context, uuid string) (st err := s.db.NewSelect().Model(&types.ContainerImageLayer{}).Column("dfs_link").WherePK(uuid).Scan(ctx, &dfsLink) if err != nil { logEvent.Err(err).Send() - return "", v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return "", v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -373,7 +373,7 @@ func (s *registryStore) GetImageNamespace(ctx context.Context, search string) ([ err := s.db.NewSelect().Model(&manifests).Where("substr(namespace, 1, 50) LIKE ?", bun.Ident(search)).Scan(ctx) if err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -395,7 +395,7 @@ func (s *registryStore) GetImageTags(ctx context.Context, namespace string) ([]s Scan(ctx) if err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -414,7 +414,7 @@ func (s *registryStore) GetLayer(ctx context.Context, digest string) (*types.Con var layer types.ContainerImageLayer if err := s.db.NewSelect().Model(&layer).Where("digest = ?", digest).Scan(ctx); err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -427,7 +427,7 @@ func (s *registryStore) GetManifest(ctx context.Context, id string) (*types.Imag var manifest types.ImageManifest if err := s.db.NewSelect().Model(&manifest).Where("id = ?", id).Scan(ctx); err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -470,7 +470,7 @@ func (s *registryStore) GetManifestByReference( if err := q.Scan(ctx); err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -500,7 +500,7 @@ func (s *registryStore) GetRepoDetail( Scan(ctx) if err != nil { logEvent.Err(err).Send() - return nil, v2.WrapDatabaseError(err, v2.DatabaseOperationRead) + return nil, v1.WrapDatabaseError(err, v1.DatabaseOperationRead) } logEvent.Bool("success", true).Send() @@ -526,7 +526,7 @@ func (s *registryStore) SetContainerImageVisibility( if err != nil { logEvent.Err(err).Send() - return v2.WrapDatabaseError(err, v2.DatabaseOperationUpdate) + return v1.WrapDatabaseError(err, v1.DatabaseOperationUpdate) } logEvent.Bool("success", true).Send() @@ -539,7 +539,7 @@ func (s *registryStore) SetLayer(ctx context.Context, txn *bun.Tx, l *types.Cont _, err := txn.NewInsert().Model(l).On("conflict (digest) do update").Set("updated_at = ?", time.Now()).Exec(ctx) if err != nil { logEvent.Err(err).Send() - return v2.WrapDatabaseError(err, v2.DatabaseOperationWrite) + return v1.WrapDatabaseError(err, v1.DatabaseOperationWrite) } logEvent.Bool("success", true).Send() @@ -562,7 +562,7 @@ func (s *registryStore) SetManifest(ctx context.Context, txn *bun.Tx, im *types. Exec(ctx) if err != nil { logEvent.Err(err).Send() - return v2.WrapDatabaseError(err, v2.DatabaseOperationWrite) + return v1.WrapDatabaseError(err, v1.DatabaseOperationWrite) } logEvent.Bool("success", true).Send() @@ -573,16 +573,16 @@ func (s *registryStore) SetManifest(ctx context.Context, txn *bun.Tx, im *types. _, err := txn.NewInsert().Model(im).Exec(ctx) if err != nil { logEvent.Err(err).Send() - return v2.WrapDatabaseError(err, v2.DatabaseOperationWrite) + return v1.WrapDatabaseError(err, v1.DatabaseOperationWrite) } logEvent.Bool("success", true).Send() return nil } - return v2.WrapDatabaseError( + return v1.WrapDatabaseError( fmt.Errorf("DB_ERR: InsertOnUpdate feature not available"), - v2.DatabaseOperationWrite, + v1.DatabaseOperationWrite, ) } @@ -700,8 +700,92 @@ func (s *registryStore) GetImageSizeByLayerIds(ctx context.Context, layerIDs []s Where("digest in (?)", bun.In(layerIDs)). Scan(ctx, &size) if err != nil { - return 0, err + return 0, v1.WrapDatabaseError(err, v1.DatabaseOperationUpdate) } return size, nil } + +func (s *registryStore) IncrementRepositoryPullCounter(ctx context.Context, repoID uuid.UUID) error { + repo := types.ContainerImageRepository{ + ID: repoID, + } + + _, err := s.db.NewUpdate().Model(&repo).WherePK().Set("pull_count = pull_count + 1").Exec(ctx) + if err != nil { + return v1.WrapDatabaseError(err, v1.DatabaseOperationUpdate) + } + + return nil +} + +func (s *registryStore) AddRepositoryToFavorites(ctx context.Context, repoID uuid.UUID, userID uuid.UUID) error { + user := types.User{} + + q := s. + db. + NewUpdate(). + Model(&user). + Set("favorite_repositories = array_append(favorite_repositories, ?)", repoID). + Where("id = ?", userID). + Where("NOT (? = ANY(favorite_repositories))", repoID) + + result, err := q.Exec(ctx) + if err != nil { + return v1.WrapDatabaseError(err, v1.DatabaseOperationUpdate) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return v1.WrapDatabaseError(err, v1.DatabaseOperationUpdate) + } + + if rowsAffected == 1 { + repo := types.ContainerImageRepository{ + ID: repoID, + } + + _, err = s.db.NewUpdate().Model(&repo).WherePK().Set("favorite_count = favorite_count + 1").Exec(ctx) + if err != nil { + return v1.WrapDatabaseError(err, v1.DatabaseOperationUpdate) + } + + return nil + } + + return v1.WrapDatabaseError(fmt.Errorf("repository is already in favorites list"), v1.DatabaseOperationUpdate) +} + +func (s *registryStore) RemoveRepositoryFromFavorites(ctx context.Context, repoID uuid.UUID, userID uuid.UUID) error { + user := types.User{} + q := s. + db. + NewUpdate(). + Model(&user). + Set("favorite_repositories = array_remove(favorite_repositories, ?)", repoID). + Where("id = ?", userID). + Where("? = ANY(favorite_repositories)", repoID) + + result, err := q.Exec(ctx) + if err != nil { + return v1.WrapDatabaseError(err, v1.DatabaseOperationUpdate) + } + + rowsAffected, err := result.RowsAffected() + if err == nil && rowsAffected == 1 { + repo := types.ContainerImageRepository{ + ID: repoID, + } + + _, err = s.db.NewUpdate().Model(&repo).WherePK().Set("favorite_count = favorite_count - 1").Exec(ctx) + if err != nil { + return v1.WrapDatabaseError(err, v1.DatabaseOperationUpdate) + } + return nil + } + + return v1.WrapDatabaseError( + fmt.Errorf("repository is not in favorites list"), + v1.DatabaseOperationUpdate, + ) +} diff --git a/store/v1/registry/store.go b/store/v1/registry/store.go index 9463598b..4550643c 100644 --- a/store/v1/registry/store.go +++ b/store/v1/registry/store.go @@ -83,4 +83,7 @@ type RegistryStore interface { GetRepositoryByNamespace(ctx context.Context, namespace string) (*types.ContainerImageRepository, error) RepositoryExists(ctx context.Context, namespace string) bool GetRepositoryByName(ctx context.Context, userId uuid.UUID, name string) (*types.ContainerImageRepository, error) + IncrementRepositoryPullCounter(ctx context.Context, repoID uuid.UUID) error + AddRepositoryToFavorites(ctx context.Context, repoID uuid.UUID, userID uuid.UUID) error + RemoveRepositoryFromFavorites(ctx context.Context, repoID uuid.UUID, userID uuid.UUID) error } diff --git a/store/v1/types/registry.go b/store/v1/types/registry.go index 7639da40..c14da12f 100644 --- a/store/v1/types/registry.go +++ b/store/v1/types/registry.go @@ -102,6 +102,8 @@ type ( Builds []*RepositoryBuild `bun:"rel:has-many,join:id=repository_id" json:"-"` ID uuid.UUID `bun:"id,pk,type:uuid,default:gen_random_uuid()" json:"id"` OwnerID uuid.UUID `bun:"owner_id,type:uuid" json:"owner_id"` + PullCount uint64 `bun:"pull_count" json:"pull_count"` + FavoriteCount uint64 `bun:"favorite_count" json:"favorite_count"` } RepositoryVisibility string diff --git a/store/v1/types/users.go b/store/v1/types/users.go index 39c44889..9443fdcb 100644 --- a/store/v1/types/users.go +++ b/store/v1/types/users.go @@ -43,11 +43,13 @@ type ( User struct { bun.BaseModel `bun:"table:users,alias:u" json:"-"` - UpdatedAt time.Time `bun:"updated_at" json:"updated_at,omitempty" validate:"-"` - CreatedAt time.Time `bun:"created_at" json:"created_at,omitempty" validate:"-"` - Identities Identities `bun:"identities" json:"identities,omitempty"` - Username string `bun:"username,notnull,unique" json:"username,omitempty" validate:"-"` - Password string `bun:"password" json:"password,omitempty"` + UpdatedAt time.Time `bun:"updated_at" json:"updated_at,omitempty" validate:"-"` + CreatedAt time.Time `bun:"created_at" json:"created_at,omitempty" validate:"-"` + Identities Identities `bun:"identities" json:"identities,omitempty"` + // nolint:lll + Username string `bun:"username,notnull,unique" json:"username,omitempty" validate:"-"` + Password string `bun:"password" json:"password,omitempty"` + // nolint:lll Email string `bun:"email,notnull,unique" json:"email,omitempty" validate:"email"` UserType string `bun:"user_type" json:"user_type"` Sessions []*Session `bun:"rel:has-many,join:id=owner_id" json:"-"` @@ -55,11 +57,13 @@ type ( WebauthnCredentials []*WebauthnCredential `bun:"rel:has-many,join:id=credential_owner_id" json:"-"` Permissions []*Permissions `bun:"rel:has-many,join:id=user_id" json:"-"` Repositories []*ContainerImageRepository `bun:"rel:has-many,join:id=owner_id" json:"-"` - ID uuid.UUID `bun:"id,type:uuid,pk" json:"id,omitempty" validate:"-"` - IsActive bool `bun:"is_active" json:"is_active,omitempty" validate:"-"` - WebauthnConnected bool `bun:"webauthn_connected" json:"webauthn_connected,omitempty"` - GithubConnected bool `bun:"github_connected" json:"github_connected,omitempty"` - IsOrgOwner bool `bun:"is_org_owner" json:"is_org_owner,omitempty"` + // nolint:lll + FavoriteRepositories []uuid.UUID `bun:"favorite_repositories,type:uuid[],default:'{}'" json:"favorite_repositories"` + ID uuid.UUID `bun:"id,type:uuid,pk" json:"id,omitempty" validate:"-"` + IsActive bool `bun:"is_active" json:"is_active,omitempty" validate:"-"` + WebauthnConnected bool `bun:"webauthn_connected" json:"webauthn_connected"` + GithubConnected bool `bun:"github_connected" json:"github_connected"` + IsOrgOwner bool `bun:"is_org_owner" json:"is_org_owner,omitempty"` } // type here is string so that we can use it with echo.Context & std context.Context