diff --git a/internal/app/application/services/room.go b/internal/app/application/services/room.go index fe9479f..a12bf6d 100644 --- a/internal/app/application/services/room.go +++ b/internal/app/application/services/room.go @@ -30,3 +30,79 @@ func (s *RoomService) Create(name string, description *string, userID string) (* return room, nil } + +func (s *RoomService) GetAll( + search string, + userID string, + pageFilter PageFilter, +) ([]*entities.Room, error) { + queryFilter := s.makeGetAllQueryFilter(search, userID) + + rooms, err := s.roomRepository.GetByQueryFilters(*queryFilter, &repositories.PageFilter{ + Offset: (pageFilter.Page - 1) * pageFilter.Size, + Limit: pageFilter.Size, + }) + if err != nil { + return nil, err + } + + return rooms, nil +} + +func (s *RoomService) CountAll( + search string, + userID string, +) (int64, error) { + queryFilter := s.makeGetAllQueryFilter(search, userID) + + count, err := s.roomRepository.CountByQueryFilters(*queryFilter) + if err != nil { + return 0, err + } + + return count, nil +} + +func (s *RoomService) makeGetAllQueryFilter( + search string, + userID string, +) *repositories.QueryFilter { + queryFilter := &repositories.QueryFilter{ + ConditionGroups: []repositories.ConditionGroup{ + { + Operator: repositories.AndLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: entities.RoomUserIDField, + Operator: repositories.EqualComparisonOperator, + Value: userID, + }, + }, + }, + }, + } + + if search != "" { + searchConditionGroup := repositories.ConditionGroup{ + Operator: repositories.OrLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: entities.RoomNameField, + Operator: repositories.LikeComparisonOperator, + Value: "%" + search + "%", + }, + { + Field: entities.RoomDescriptionField, + Operator: repositories.LikeComparisonOperator, + Value: "%" + search + "%", + }, + }, + } + queryFilter.ConditionGroups = append( + queryFilter.ConditionGroups, + searchConditionGroup, + ) + } + + return queryFilter +} diff --git a/internal/app/application/services/room_test.go b/internal/app/application/services/room_test.go index efb284e..f12481b 100644 --- a/internal/app/application/services/room_test.go +++ b/internal/app/application/services/room_test.go @@ -3,6 +3,7 @@ package services import ( "errors" "github.com/google/uuid" + "github.com/jibaru/home-inventory-api/m/internal/app/domain/entities" "github.com/jibaru/home-inventory-api/m/internal/app/infrastructure/repositories/stub" "github.com/labstack/gommon/random" "github.com/stretchr/testify/assert" @@ -48,3 +49,94 @@ func TestRoomServiceCreateRoomErrorInRepository(t *testing.T) { assert.EqualError(t, err, mockError.Error()) roomRepository.AssertExpectations(t) } + +func TestRoomServiceGetAll(t *testing.T) { + roomRepository := new(stub.RoomRepositoryMock) + roomService := NewRoomService(roomRepository) + + search := random.String(100, random.Alphanumeric) + userID := uuid.NewString() + pageFilter := PageFilter{ + Page: 1, + Size: 10, + } + + rooms := []*entities.Room{ + { + ID: uuid.NewString(), + Name: random.String(100, random.Alphanumeric), + Description: nil, + UserID: userID, + }, + } + roomRepository.On("GetByQueryFilters", mock.AnythingOfType("repositories.QueryFilter"), mock.AnythingOfType("*repositories.PageFilter")). + Return(rooms, nil) + + result, err := roomService.GetAll(search, userID, pageFilter) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, rooms, result) + roomRepository.AssertExpectations(t) +} + +func TestRoomServiceGetAllErrorInRepository(t *testing.T) { + roomRepository := new(stub.RoomRepositoryMock) + roomService := NewRoomService(roomRepository) + + search := random.String(100, random.Alphanumeric) + userID := uuid.NewString() + pageFilter := PageFilter{ + Page: 1, + Size: 10, + } + + mockError := errors.New("repository error") + roomRepository.On( + "GetByQueryFilters", + mock.AnythingOfType("repositories.QueryFilter"), + mock.AnythingOfType("*repositories.PageFilter"), + ).Return(nil, mockError) + + result, err := roomService.GetAll(search, userID, pageFilter) + + assert.Error(t, err) + assert.Nil(t, result) + assert.EqualError(t, err, mockError.Error()) + roomRepository.AssertExpectations(t) +} + +func TestRoomServiceCountAll(t *testing.T) { + roomRepository := new(stub.RoomRepositoryMock) + roomService := NewRoomService(roomRepository) + + search := random.String(100, random.Alphanumeric) + userID := uuid.NewString() + + count := int64(10) + roomRepository.On("CountByQueryFilters", mock.AnythingOfType("repositories.QueryFilter")).Return(count, nil) + + result, err := roomService.CountAll(search, userID) + + assert.NoError(t, err) + assert.Equal(t, count, result) + roomRepository.AssertExpectations(t) +} + +func TestRoomServiceCountAllErrorInRepository(t *testing.T) { + roomRepository := new(stub.RoomRepositoryMock) + roomService := NewRoomService(roomRepository) + + search := random.String(100, random.Alphanumeric) + userID := uuid.NewString() + + mockError := errors.New("repository error") + roomRepository.On("CountByQueryFilters", mock.AnythingOfType("repositories.QueryFilter")).Return(int64(0), mockError) + + result, err := roomService.CountAll(search, userID) + + assert.Error(t, err) + assert.Equal(t, int64(0), result) + assert.EqualError(t, err, mockError.Error()) + roomRepository.AssertExpectations(t) +} diff --git a/internal/app/domain/entities/room.go b/internal/app/domain/entities/room.go index 7de29a6..4081679 100644 --- a/internal/app/domain/entities/room.go +++ b/internal/app/domain/entities/room.go @@ -15,6 +15,12 @@ var ( ErrUserIDShouldNotBeEmpty = errors.New("user id should not be empty") ) +const ( + RoomNameField = "name" + RoomDescriptionField = "description" + RoomUserIDField = "user_id" +) + type Room struct { ID string Name string diff --git a/internal/app/domain/repositories/arguments.go b/internal/app/domain/repositories/arguments.go index c7c78f1..7a4c195 100644 --- a/internal/app/domain/repositories/arguments.go +++ b/internal/app/domain/repositories/arguments.go @@ -1,6 +1,34 @@ package repositories +const ( + AndLogicalOperator LogicalOperator = "AND" + OrLogicalOperator LogicalOperator = "OR" +) + +const ( + LikeComparisonOperator ComparisonOperator = "LIKE" + EqualComparisonOperator ComparisonOperator = "=" +) + +type ComparisonOperator string +type LogicalOperator string + type PageFilter struct { Offset int Limit int } + +type QueryFilter struct { + ConditionGroups []ConditionGroup +} + +type ConditionGroup struct { + Operator LogicalOperator + Conditions []Condition +} + +type Condition struct { + Field string + Operator ComparisonOperator + Value interface{} +} diff --git a/internal/app/domain/repositories/room.go b/internal/app/domain/repositories/room.go index 24eb1c9..6cefd27 100644 --- a/internal/app/domain/repositories/room.go +++ b/internal/app/domain/repositories/room.go @@ -6,11 +6,15 @@ import ( ) var ( - ErrCanNotCreateRoom = errors.New("can not create room") - ErrCanNotCheckIfRoomExistsByID = errors.New("can not check if room exists by id") + ErrCanNotCreateRoom = errors.New("can not create room") + ErrCanNotCheckIfRoomExistsByID = errors.New("can not check if room exists by id") + ErrRoomRepositoryCanNotGetRooms = errors.New("can not get rooms") + ErrRoomRepositoryCanNotCountRooms = errors.New("can not count rooms") ) type RoomRepository interface { Create(room *entities.Room) error ExistsByID(id string) (bool, error) + GetByQueryFilters(queryFilter QueryFilter, pageFilter *PageFilter) ([]*entities.Room, error) + CountByQueryFilters(queryFilter QueryFilter) (int64, error) } diff --git a/internal/app/infrastructure/controllers/get_rooms.go b/internal/app/infrastructure/controllers/get_rooms.go new file mode 100644 index 0000000..f56d1ca --- /dev/null +++ b/internal/app/infrastructure/controllers/get_rooms.go @@ -0,0 +1,78 @@ +package controllers + +import ( + "github.com/jibaru/home-inventory-api/m/internal/app/application/services" + "github.com/jibaru/home-inventory-api/m/internal/app/infrastructure/responses" + "github.com/labstack/echo/v4" + "net/http" +) + +type GetRoomsController struct { + roomService *services.RoomService +} + +type GetRoomsRequest struct { + Search string `query:"search"` + Page int `query:"page"` + PerPage int `query:"per_page"` +} + +type GetRoomsResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` +} + +func NewGetRoomsController( + roomService *services.RoomService, +) *GetRoomsController { + return &GetRoomsController{roomService: roomService} +} + +func (c *GetRoomsController) Handle(ctx echo.Context) error { + userID := ctx.Get("auth_id").(string) + request := GetRoomsRequest{} + + err := (&echo.DefaultBinder{}).BindQueryParams(ctx, &request) + if err != nil { + return ctx.JSON(http.StatusBadRequest, responses.NewMessageResponse(err.Error())) + } + + rooms, err := c.roomService.GetAll( + request.Search, + userID, + services.PageFilter{ + Page: request.Page, + Size: request.PerPage, + }, + ) + if err != nil { + return ctx.JSON(http.StatusBadRequest, responses.NewMessageResponse(err.Error())) + } + + total, err := c.roomService.CountAll( + request.Search, + userID, + ) + if err != nil { + return ctx.JSON(http.StatusBadRequest, responses.NewMessageResponse(err.Error())) + } + + responseRooms := make([]*GetRoomsResponse, 0) + for _, room := range rooms { + responseRooms = append(responseRooms, &GetRoomsResponse{ + ID: room.ID, + Name: room.Name, + Description: room.Description, + }) + } + + return ctx.JSON(http.StatusOK, responses.NewPaginatedResponse( + responseRooms, + total, + request.Page, + request.PerPage, + len(rooms), + ctx.Request().URL.Path, + )) +} diff --git a/internal/app/infrastructure/http/server.go b/internal/app/infrastructure/http/server.go index 2013bf1..3f7def8 100644 --- a/internal/app/infrastructure/http/server.go +++ b/internal/app/infrastructure/http/server.go @@ -52,6 +52,7 @@ func RunServer( createItemController := controllers.NewCreateItemController(itemService) addItemIntoBoxController := controllers.NewAddItemIntoBoxController(boxService) removeItemFromBoxController := controllers.NewRemoveItemFromBoxController(boxService) + getRoomsController := controllers.NewGetRoomsController(roomService) needsAuthMiddleware := middlewares.NewNeedsAuthMiddleware(authService) @@ -70,6 +71,7 @@ func RunServer( authApi.POST("/items", createItemController.Handle) authApi.POST("/boxes/:boxID/items", addItemIntoBoxController.Handle) authApi.DELETE("/boxes/:boxID/items/:itemID", removeItemFromBoxController.Handle) + authApi.GET("/rooms", getRoomsController.Handle) e.Logger.Fatal(e.Start(host + ":" + port)) } diff --git a/internal/app/infrastructure/repositories/gorm/helpers.go b/internal/app/infrastructure/repositories/gorm/helpers.go new file mode 100644 index 0000000..65724b1 --- /dev/null +++ b/internal/app/infrastructure/repositories/gorm/helpers.go @@ -0,0 +1,30 @@ +package gorm + +import ( + "github.com/jibaru/home-inventory-api/m/internal/app/domain/repositories" + "gorm.io/gorm" + "strings" +) + +func applyFilters(db *gorm.DB, filter repositories.QueryFilter) *gorm.DB { + for _, group := range filter.ConditionGroups { + var groupConditions []string + var args []interface{} + + for _, condition := range group.Conditions { + switch condition.Operator { + case repositories.LikeComparisonOperator: + groupConditions = append(groupConditions, condition.Field+" LIKE ?") + args = append(args, condition.Value.(string)) + default: + groupConditions = append(groupConditions, condition.Field+" "+string(condition.Operator)+" ?") + args = append(args, condition.Value) + } + } + + groupQuery := strings.Join(groupConditions, " "+string(group.Operator)+" ") + db = db.Where(groupQuery, args...) + } + + return db +} diff --git a/internal/app/infrastructure/repositories/gorm/helpers_test.go b/internal/app/infrastructure/repositories/gorm/helpers_test.go new file mode 100644 index 0000000..d783362 --- /dev/null +++ b/internal/app/infrastructure/repositories/gorm/helpers_test.go @@ -0,0 +1,136 @@ +package gorm + +import ( + "github.com/jibaru/home-inventory-api/m/internal/app/domain/repositories" + "github.com/stretchr/testify/assert" + "regexp" + "testing" +) + +type FakeTable struct{} + +func TestApplyFiltersWithEmptyQueryFilters(t *testing.T) { + db, dbMock := makeDBMock() + + queryFilter := repositories.QueryFilter{} + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fake_tables`")) + + db = applyFilters(db.Model(&FakeTable{}), queryFilter) + db.Find(&FakeTable{}) + + err := dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} + +func TestApplyFiltersWithSingleQueryFilters(t *testing.T) { + db, dbMock := makeDBMock() + + queryFilter := repositories.QueryFilter{ + ConditionGroups: []repositories.ConditionGroup{ + { + Conditions: []repositories.Condition{ + { + Field: "name", + Operator: repositories.LikeComparisonOperator, + Value: "test", + }, + }, + }, + }, + } + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fake_tables` WHERE name LIKE ?")). + WithArgs("test") + + db = applyFilters(db.Model(&FakeTable{}), queryFilter) + db.Find(&FakeTable{}) + + err := dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} + +func TestApplyFiltersWithMultipleQueryFilters(t *testing.T) { + db, dbMock := makeDBMock() + + queryFilter := repositories.QueryFilter{ + ConditionGroups: []repositories.ConditionGroup{ + { + Conditions: []repositories.Condition{ + { + Field: "name", + Operator: repositories.LikeComparisonOperator, + Value: "test", + }, + }, + }, + { + Operator: repositories.AndLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: "age", + Operator: repositories.EqualComparisonOperator, + Value: 20, + }, + }, + }, + }, + } + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fake_tables` WHERE name LIKE ? AND age = ?")). + WithArgs("test", 20) + + db = applyFilters(db.Model(&FakeTable{}), queryFilter) + db.Find(&FakeTable{}) + + err := dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} + +func TestApplyFiltersWithMultipleQueryFiltersAndOrLogicalOperator(t *testing.T) { + db, dbMock := makeDBMock() + + queryFilter := repositories.QueryFilter{ + ConditionGroups: []repositories.ConditionGroup{ + { + Operator: repositories.AndLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: "name", + Operator: repositories.LikeComparisonOperator, + Value: "test", + }, + { + Field: "is_ok", + Operator: repositories.EqualComparisonOperator, + Value: true, + }, + }, + }, + { + Operator: repositories.OrLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: "age", + Operator: repositories.EqualComparisonOperator, + Value: 20, + }, + { + Field: "stars", + Operator: repositories.EqualComparisonOperator, + Value: 10, + }, + }, + }, + }, + } + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fake_tables` WHERE (name LIKE ? AND is_ok = ?) AND (age = ? OR stars = ?)")). + WithArgs("test", true, 20, 10) + + db = applyFilters(db.Model(&FakeTable{}), queryFilter) + db.Find(&FakeTable{}) + + err := dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} diff --git a/internal/app/infrastructure/repositories/gorm/room.go b/internal/app/infrastructure/repositories/gorm/room.go index ab4170c..81d029b 100644 --- a/internal/app/infrastructure/repositories/gorm/room.go +++ b/internal/app/infrastructure/repositories/gorm/room.go @@ -36,3 +36,31 @@ func (r *RoomRepository) ExistsByID(id string) (bool, error) { return count > 0, nil } + +func (r *RoomRepository) GetByQueryFilters(queryFilter repositories.QueryFilter, pageFilter *repositories.PageFilter) ([]*entities.Room, error) { + var rooms []*entities.Room + err := applyFilters(r.db, queryFilter). + Offset(pageFilter.Offset). + Limit(pageFilter.Limit). + Find(&rooms). + Error + + if err != nil { + return nil, repositories.ErrRoomRepositoryCanNotGetRooms + } + + return rooms, nil +} + +func (r *RoomRepository) CountByQueryFilters(queryFilter repositories.QueryFilter) (int64, error) { + var count int64 + err := applyFilters(r.db.Model(&entities.Room{}), queryFilter). + Count(&count). + Error + + if err != nil { + return 0, repositories.ErrRoomRepositoryCanNotCountRooms + } + + return count, nil +} diff --git a/internal/app/infrastructure/repositories/gorm/room_test.go b/internal/app/infrastructure/repositories/gorm/room_test.go index 2945097..edb2b54 100644 --- a/internal/app/infrastructure/repositories/gorm/room_test.go +++ b/internal/app/infrastructure/repositories/gorm/room_test.go @@ -102,3 +102,168 @@ func TestRoomRepositoryExistsByIDErrorCanNotCheckIfRoomExistsByID(t *testing.T) err = dbMock.ExpectationsWereMet() assert.NoError(t, err) } + +func TestRoomRepositoryGetByQueryFilters(t *testing.T) { + db, dbMock := makeDBMock() + roomRepository := NewRoomRepository(db) + + roomID := uuid.NewString() + roomName := random.String(100, random.Alphanumeric) + roomDescription := random.String(255, random.Alphanumeric) + userID := uuid.NewString() + createdAt := time.Now() + updatedAt := time.Now() + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `rooms` WHERE (name LIKE ? OR description LIKE ?) AND user_id = ? LIMIT 1 OFFSET 1")). + WithArgs("%search%", "%search%", userID). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "description", "user_id", "created_at", "updated_at"}). + AddRow(roomID, roomName, roomDescription, userID, createdAt, updatedAt)) + + queryFilter := repositories.QueryFilter{ + ConditionGroups: []repositories.ConditionGroup{ + { + Operator: repositories.OrLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: entities.RoomNameField, + Operator: repositories.LikeComparisonOperator, + Value: "%search%", + }, + { + Field: entities.RoomDescriptionField, + Operator: repositories.LikeComparisonOperator, + Value: "%search%", + }, + }, + }, + { + Operator: repositories.AndLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: entities.RoomUserIDField, + Operator: repositories.EqualComparisonOperator, + Value: userID, + }, + }, + }, + }, + } + pageFilter := &repositories.PageFilter{ + Offset: 1, + Limit: 1, + } + rooms, err := roomRepository.GetByQueryFilters(queryFilter, pageFilter) + + assert.NoError(t, err) + assert.Len(t, rooms, 1) + assert.Equal(t, roomID, rooms[0].ID) + assert.Equal(t, roomName, rooms[0].Name) + assert.Equal(t, roomDescription, *rooms[0].Description) + assert.Equal(t, userID, rooms[0].UserID) + assert.Equal(t, createdAt, rooms[0].CreatedAt) + assert.Equal(t, updatedAt, rooms[0].UpdatedAt) + err = dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} + +func TestRoomRepositoryGetByQueryFiltersErrorCanNotGetRooms(t *testing.T) { + db, dbMock := makeDBMock() + roomRepository := NewRoomRepository(db) + + userID := uuid.NewString() + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `rooms` WHERE user_id = ? LIMIT 1 OFFSET 1")). + WithArgs(userID). + WillReturnError(errors.New("database error")) + + queryFilter := repositories.QueryFilter{ + ConditionGroups: []repositories.ConditionGroup{ + { + Operator: repositories.AndLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: entities.RoomUserIDField, + Operator: repositories.EqualComparisonOperator, + Value: userID, + }, + }, + }, + }, + } + pageFilter := &repositories.PageFilter{ + Offset: 1, + Limit: 1, + } + rooms, err := roomRepository.GetByQueryFilters(queryFilter, pageFilter) + + assert.Error(t, err) + assert.ErrorIs(t, err, repositories.ErrRoomRepositoryCanNotGetRooms) + assert.Nil(t, rooms) + err = dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} + +func TestRoomRepositoryCountByQueryFilters(t *testing.T) { + db, dbMock := makeDBMock() + roomRepository := NewRoomRepository(db) + + userID := uuid.NewString() + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `rooms` WHERE user_id = ?")). + WithArgs(userID). + WillReturnRows(sqlmock.NewRows([]string{"count(*)"}).AddRow(1)) + + queryFilter := repositories.QueryFilter{ + ConditionGroups: []repositories.ConditionGroup{ + { + Operator: repositories.AndLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: entities.RoomUserIDField, + Operator: repositories.EqualComparisonOperator, + Value: userID, + }, + }, + }, + }, + } + count, err := roomRepository.CountByQueryFilters(queryFilter) + + assert.NoError(t, err) + assert.Equal(t, int64(1), count) + err = dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} + +func TestRoomRepositoryCountByQueryFiltersErrorCanNotCountRooms(t *testing.T) { + db, dbMock := makeDBMock() + roomRepository := NewRoomRepository(db) + + userID := uuid.NewString() + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `rooms` WHERE user_id = ?")). + WithArgs(userID). + WillReturnError(errors.New("database error")) + + queryFilter := repositories.QueryFilter{ + ConditionGroups: []repositories.ConditionGroup{ + { + Operator: repositories.AndLogicalOperator, + Conditions: []repositories.Condition{ + { + Field: entities.RoomUserIDField, + Operator: repositories.EqualComparisonOperator, + Value: userID, + }, + }, + }, + }, + } + count, err := roomRepository.CountByQueryFilters(queryFilter) + + assert.Error(t, err) + assert.ErrorIs(t, err, repositories.ErrRoomRepositoryCanNotCountRooms) + assert.Equal(t, int64(0), count) + err = dbMock.ExpectationsWereMet() + assert.NoError(t, err) +} diff --git a/internal/app/infrastructure/repositories/stub/room.go b/internal/app/infrastructure/repositories/stub/room.go index 471b279..49049ec 100644 --- a/internal/app/infrastructure/repositories/stub/room.go +++ b/internal/app/infrastructure/repositories/stub/room.go @@ -2,6 +2,7 @@ package stub import ( "github.com/jibaru/home-inventory-api/m/internal/app/domain/entities" + "github.com/jibaru/home-inventory-api/m/internal/app/domain/repositories" "github.com/stretchr/testify/mock" ) @@ -18,3 +19,23 @@ func (m *RoomRepositoryMock) ExistsByID(id string) (bool, error) { args := m.Called(id) return args.Bool(0), args.Error(1) } + +func (m *RoomRepositoryMock) GetByQueryFilters( + queryFilter repositories.QueryFilter, + pageFilter *repositories.PageFilter, +) ([]*entities.Room, error) { + args := m.Called(queryFilter, pageFilter) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).([]*entities.Room), args.Error(1) +} + +func (m *RoomRepositoryMock) CountByQueryFilters( + queryFilter repositories.QueryFilter, +) (int64, error) { + args := m.Called(queryFilter) + return args.Get(0).(int64), args.Error(1) +} diff --git a/internal/app/infrastructure/responses/paginated.go b/internal/app/infrastructure/responses/paginated.go new file mode 100644 index 0000000..7e9c5db --- /dev/null +++ b/internal/app/infrastructure/responses/paginated.go @@ -0,0 +1,58 @@ +package responses + +import "strconv" + +type PaginatedResponse[T comparable] struct { + Data []T `json:"data"` + Meta struct { + Total int64 `json:"total"` + Page int `json:"page"` + PerPage int `json:"per_page"` + PageCount int `json:"page_count"` + Links struct { + First string `json:"first"` + Last string `json:"last"` + Prev string `json:"prev"` + Next string `json:"next"` + } `json:"links"` + } `json:"meta"` +} + +func NewPaginatedResponse[T comparable]( + data []T, + total int64, + page, perPage, pageCount int, + path string, +) *PaginatedResponse[T] { + return &PaginatedResponse[T]{ + Data: data, + Meta: struct { + Total int64 `json:"total"` + Page int `json:"page"` + PerPage int `json:"per_page"` + PageCount int `json:"page_count"` + Links struct { + First string `json:"first"` + Last string `json:"last"` + Prev string `json:"prev"` + Next string `json:"next"` + } `json:"links"` + }{ + Total: total, + Page: page, + PerPage: perPage, + PageCount: pageCount, + Links: struct { + First string `json:"first"` + Last string `json:"last"` + Prev string `json:"prev"` + Next string `json:"next"` + }{ + First: path + "?page=1&per_page=" + strconv.Itoa(perPage), + Last: path + "?page=" + strconv.Itoa(pageCount) + "&per_page=" + strconv.Itoa(perPage), + Prev: path + "?page=" + strconv.Itoa(page-1) + "&per_page=" + strconv.Itoa(perPage), + Next: path + "?page=" + strconv.Itoa(page+1) + "&per_page=" + strconv.Itoa(perPage), + }, + }, + } +} diff --git a/internal/app/infrastructure/responses/paginated_test.go b/internal/app/infrastructure/responses/paginated_test.go new file mode 100644 index 0000000..e461367 --- /dev/null +++ b/internal/app/infrastructure/responses/paginated_test.go @@ -0,0 +1,32 @@ +package responses + +import ( + "github.com/stretchr/testify/assert" + "strconv" + "testing" +) + +func TestNewPaginatedResponse(t *testing.T) { + data := []string{"some test", "another test"} + total := int64(1000) + page := 1 + perPage := 2 + pageCount := len(data) + path := "/tests" + + response := NewPaginatedResponse(data, total, page, perPage, pageCount, path) + + assert.Equal(t, data, response.Data) + assert.Equal(t, total, response.Meta.Total) + assert.Equal(t, page, response.Meta.Page) + assert.Equal(t, perPage, response.Meta.PerPage) + assert.Equal(t, pageCount, response.Meta.PageCount) + assert.Equal(t, path+"?page="+strconv.Itoa(page)+"&per_page="+strconv.Itoa(perPage), response.Meta.Links.First) + assert.Equal(t, path+"?page="+strconv.Itoa(pageCount)+"&per_page="+strconv.Itoa(perPage), response.Meta.Links.Last) + assert.Equal(t, path+"?page="+strconv.Itoa(page-1)+"&per_page="+strconv.Itoa(perPage), response.Meta.Links.Prev) + assert.Equal(t, path+"?page="+strconv.Itoa(page+1)+"&per_page="+strconv.Itoa(perPage), response.Meta.Links.Next) + + for _, v := range response.Data { + assert.Contains(t, data, v) + } +}