Skip to content

Commit 94080dc

Browse files
committed
implement likes
1 parent 98965db commit 94080dc

File tree

6 files changed

+252
-10
lines changed

6 files changed

+252
-10
lines changed
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package v1
2+
3+
import (
4+
"github.com/labstack/echo/v4"
5+
"github.com/neatplex/nightel-core/internal/models"
6+
"github.com/neatplex/nightel-core/internal/services/container"
7+
"net/http"
8+
)
9+
10+
type LikesStoreRequest struct {
11+
StoryID uint64 `json:"story_id" validate:"required"`
12+
}
13+
14+
func LikesStore(ctr *container.Container) echo.HandlerFunc {
15+
return func(ctx echo.Context) error {
16+
user := ctx.Get("user").(*models.User)
17+
18+
var r LikesStoreRequest
19+
if err := ctx.Bind(&r); err != nil {
20+
return ctx.JSON(http.StatusBadRequest, map[string]string{
21+
"message": "Cannot parse the request body.",
22+
})
23+
}
24+
if err := ctx.Validate(r); err != nil {
25+
return ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
26+
"message": err.Error(),
27+
})
28+
}
29+
30+
story, err := ctr.StoryService.FindById(r.StoryID)
31+
if err != nil {
32+
return err
33+
}
34+
35+
like, err := ctr.LikeService.Create(user, story)
36+
if err != nil {
37+
return err
38+
}
39+
40+
return ctx.JSON(http.StatusCreated, map[string]*models.Like{
41+
"like": like,
42+
})
43+
}
44+
}
45+
46+
type LikesDeleteRequest struct {
47+
LikeID uint64 `json:"like_id" validate:"required"`
48+
}
49+
50+
func LikesDelete(ctr *container.Container) echo.HandlerFunc {
51+
return func(ctx echo.Context) error {
52+
user := ctx.Get("user").(*models.User)
53+
54+
var r LikesDeleteRequest
55+
if err := ctx.Bind(&r); err != nil {
56+
return ctx.JSON(http.StatusBadRequest, map[string]string{
57+
"message": "Cannot parse the request body.",
58+
})
59+
}
60+
if err := ctx.Validate(r); err != nil {
61+
return ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
62+
"message": err.Error(),
63+
})
64+
}
65+
66+
like, err := ctr.LikeService.FindById(r.LikeID)
67+
if err != nil {
68+
return err
69+
}
70+
71+
if like.UserID != user.ID {
72+
return ctx.JSON(http.StatusForbidden, map[string]string{
73+
"message": "You do not have permission to perform this action.",
74+
})
75+
}
76+
77+
if err = ctr.LikeService.Delete(like.ID); err != nil {
78+
return err
79+
}
80+
81+
return ctx.NoContent(http.StatusNoContent)
82+
}
83+
}

internal/http/server/handlers/v1/stories.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func StoriesStore(ctr *container.Container) echo.HandlerFunc {
117117
}
118118

119119
type StoriesUpdateCaptionRequest struct {
120+
StoryID uint64 `json:"story_id" validate:"required"`
120121
Caption string `json:"caption" validate:"required"`
121122
}
122123

@@ -136,8 +137,7 @@ func StoriesUpdateCaption(ctr *container.Container) echo.HandlerFunc {
136137
})
137138
}
138139

139-
id := utils.StringToID(ctx.Param("id"))
140-
story, err := ctr.StoryService.FindById(id)
140+
story, err := ctr.StoryService.FindById(r.StoryID)
141141
if err != nil {
142142
return err
143143
}
@@ -159,11 +159,15 @@ func StoriesUpdateCaption(ctr *container.Container) echo.HandlerFunc {
159159
}
160160
}
161161

162+
type StoriesDeleteRequest struct {
163+
StoryID uint64 `json:"story_id" validate:"required"`
164+
}
165+
162166
func StoriesDelete(ctr *container.Container) echo.HandlerFunc {
163167
return func(ctx echo.Context) error {
164168
user := ctx.Get("user").(*models.User)
165169

166-
var r StoriesUpdateCaptionRequest
170+
var r StoriesDeleteRequest
167171
if err := ctx.Bind(&r); err != nil {
168172
return ctx.JSON(http.StatusBadRequest, map[string]string{
169173
"message": "Cannot parse the request body.",

internal/http/server/routes.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ func (s *Server) registerRoutes() {
3131
private.POST("/stories", v1.StoriesIndex(s.container))
3232
private.GET("/stories", v1.StoriesIndex(s.container))
3333
private.POST("/stories", v1.StoriesStore(s.container))
34-
private.PATCH("/stories/:id/caption", v1.StoriesUpdateCaption(s.container))
35-
private.DELETE("/stories/:id", v1.StoriesDelete(s.container))
34+
private.PATCH("/stories/caption", v1.StoriesUpdateCaption(s.container))
35+
private.DELETE("/stories", v1.StoriesDelete(s.container))
36+
// likes
37+
private.POST("/likes", v1.LikesStore(s.container))
38+
private.DELETE("/likes", v1.LikesDelete(s.container))
3639
// files
3740
private.POST("/files", v1.FilesStore(s.container, s.l))
3841
// feed

internal/services/container/container.go

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/neatplex/nightel-core/internal/database"
55
"github.com/neatplex/nightel-core/internal/s3"
66
"github.com/neatplex/nightel-core/internal/services/file"
7+
"github.com/neatplex/nightel-core/internal/services/like"
78
"github.com/neatplex/nightel-core/internal/services/story"
89
"github.com/neatplex/nightel-core/internal/services/token"
910
"github.com/neatplex/nightel-core/internal/services/user"
@@ -14,6 +15,7 @@ type Container struct {
1415
TokenService *token.Service
1516
StoryService *story.Service
1617
FileService *file.Service
18+
LikeService *like.Service
1719
}
1820

1921
func New(database *database.Database, s3 *s3.S3) *Container {
@@ -22,5 +24,6 @@ func New(database *database.Database, s3 *s3.S3) *Container {
2224
TokenService: token.New(database),
2325
StoryService: story.New(database),
2426
FileService: file.New(database, s3),
27+
LikeService: like.New(database),
2528
}
2629
}

internal/services/like/service.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package like
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"github.com/neatplex/nightel-core/internal/database"
7+
"github.com/neatplex/nightel-core/internal/models"
8+
"gorm.io/gorm"
9+
)
10+
11+
type Service struct {
12+
database *database.Database
13+
}
14+
15+
func (s *Service) Create(user *models.User, story *models.Story) (*models.Like, error) {
16+
var like models.Like
17+
r := s.database.Handler().FirstOrCreate(&like, &models.Like{UserID: user.ID, StoryID: story.ID})
18+
if r.Error != nil {
19+
return nil, fmt.Errorf("services: like: Create: %v, err: %v", like, r.Error)
20+
}
21+
return &like, r.Error
22+
}
23+
24+
func (s *Service) FindById(id uint64) (*models.Like, error) {
25+
var model models.Like
26+
r := s.database.Handler().Where("id = ?", id).First(&model)
27+
if r.Error != nil {
28+
if errors.Is(r.Error, gorm.ErrRecordNotFound) {
29+
return nil, nil
30+
}
31+
return nil, fmt.Errorf("services: like: FindById: `%d`, err: %v", id, r.Error)
32+
}
33+
return &model, nil
34+
}
35+
36+
func (s *Service) Delete(id uint64) error {
37+
r := s.database.Handler().Delete(&models.Like{}, id)
38+
if r.Error != nil {
39+
return fmt.Errorf("services: like: Delete #%v, err: %v", id, r.Error)
40+
}
41+
return r.Error
42+
}
43+
44+
func New(database *database.Database) *Service {
45+
return &Service{database: database}
46+
}

web/swagger.json

+108-5
Original file line numberDiff line numberDiff line change
@@ -528,9 +528,51 @@
528528
}
529529
}
530530
}
531+
},
532+
"delete": {
533+
"operationId": "stories-delete",
534+
"description": "Delete the story",
535+
"tags": [
536+
"Stories"
537+
],
538+
"requestBody": {
539+
"required": true,
540+
"content": {
541+
"application/json": {
542+
"schema": {
543+
"type": "object",
544+
"properties": {
545+
"story_id": {
546+
"type": "integer",
547+
"example": 13,
548+
"description": "Unique id of story (uint64)"
549+
}
550+
}
551+
}
552+
}
553+
}
554+
},
555+
"responses": {
556+
"201": {
557+
"description": "The story deleted."
558+
},
559+
"403": {
560+
"description": "You do not have permission to perform this action."
561+
},
562+
"default": {
563+
"description": "Error!",
564+
"content": {
565+
"application/json": {
566+
"schema": {
567+
"$ref": "#/components/schemas/Error"
568+
}
569+
}
570+
}
571+
}
572+
}
531573
}
532574
},
533-
"/stories/:id/caption": {
575+
"/stories/caption": {
534576
"patch": {
535577
"operationId": "stories-update-caption",
536578
"description": "Update the story caption",
@@ -544,6 +586,11 @@
544586
"schema": {
545587
"type": "object",
546588
"properties": {
589+
"story_id": {
590+
"type": "integer",
591+
"example": 13,
592+
"description": "Unique id of story (uint64)"
593+
},
547594
"caption": {
548595
"type": "string",
549596
"example": "This is my first story! #first_story",
@@ -586,13 +633,69 @@
586633
}
587634
}
588635
},
589-
"/stories/:id": {
636+
"/likes": {
637+
"post": {
638+
"operationId": "likes-store",
639+
"description": "Store a new like action",
640+
"tags": [
641+
"Likes"
642+
],
643+
"requestBody": {
644+
"required": true,
645+
"content": {
646+
"application/json": {
647+
"schema": {
648+
"type": "object",
649+
"properties": {
650+
"story_id": {
651+
"type": "integer",
652+
"example": 13,
653+
"description": "Unique id of story (uint64)"
654+
}
655+
}
656+
}
657+
}
658+
}
659+
},
660+
"responses": {
661+
"201": {
662+
"description": "The new like action created."
663+
},
664+
"default": {
665+
"description": "Error!",
666+
"content": {
667+
"application/json": {
668+
"schema": {
669+
"$ref": "#/components/schemas/Error"
670+
}
671+
}
672+
}
673+
}
674+
}
675+
},
590676
"delete": {
591-
"operationId": "stories-delete",
592-
"description": "Delete the story",
677+
"operationId": "likes-delete",
678+
"description": "Delete the like action",
593679
"tags": [
594-
"Stories"
680+
"Likes"
595681
],
682+
"requestBody": {
683+
"required": true,
684+
"content": {
685+
"application/json": {
686+
"schema": {
687+
"type": "object",
688+
"properties": {
689+
"like_id": {
690+
"type": "integer",
691+
"example": 13,
692+
"description": "Unique id of like action (uint64)"
693+
}
694+
}
695+
}
696+
}
697+
}
698+
},
596699
"responses": {
597700
"201": {
598701
"description": "The story deleted."

0 commit comments

Comments
 (0)