Skip to content

Commit

Permalink
Limit and Offset for pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
henrod committed May 8, 2017
1 parent 85d9b96 commit f026452
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 28 deletions.
41 changes: 33 additions & 8 deletions api/offer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"

"github.com/Sirupsen/logrus"
"github.com/topfreegames/offers/errors"
Expand Down Expand Up @@ -160,7 +161,12 @@ func (g *OfferHandler) updateOffer(w http.ResponseWriter, r *http.Request) {

func (g *OfferHandler) list(w http.ResponseWriter, r *http.Request) {
mr := metricsReporterFromCtx(r.Context())
gameID := r.URL.Query().Get("game-id")

vars := r.URL.Query()
gameID := vars.Get("game-id")
limitStr := vars.Get("limit")
offsetStr := vars.Get("offset")
var err error
userEmail := userEmailFromContext(r.Context())

logger := g.App.Logger.WithFields(logrus.Fields{
Expand All @@ -177,10 +183,28 @@ func (g *OfferHandler) list(w http.ResponseWriter, r *http.Request) {
return
}

var err error
var limit uint64
if limitStr == "" {
limit = 50
} else if limit, err = strconv.ParseUint(limitStr, 10, 64); err != nil {
logger.WithError(err).Error("List game offers failed.")
g.App.HandleError(w, http.StatusBadRequest, "The limit parameter must be an uint.", err)
return
}

var offset uint64
if offsetStr == "" {
offset = 0
} else if offset, err = strconv.ParseUint(offsetStr, 10, 64); err != nil {
logger.WithError(err).Error("List game offers failed.")
g.App.HandleError(w, http.StatusBadRequest, "The offset parameter must be an uint.", err)
return
}

var offers []*models.Offer
var pages int
err = mr.WithSegment(models.SegmentModel, func() error {
offers, err = models.ListOffers(g.App.DB, gameID, mr)
offers, pages, err = models.ListOffers(g.App.DB, gameID, limit, offset, mr)
return err
})

Expand All @@ -191,10 +215,11 @@ func (g *OfferHandler) list(w http.ResponseWriter, r *http.Request) {
}

logger.Info("Listed game offers successfully.")
if len(offers) == 0 {
Write(w, http.StatusOK, "[]")
return
responseObj := map[string]interface{}{
"offers": offers,
"pages": pages,
}
bytes, _ := json.Marshal(offers)
WriteBytes(w, http.StatusOK, bytes)

bts, _ := json.Marshal(responseObj)
WriteBytes(w, http.StatusOK, bts)
}
173 changes: 158 additions & 15 deletions api/offer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1046,23 +1046,157 @@ var _ = Describe("Offer Template Handler", func() {
Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json"))

Expect(recorder.Code).To(Equal(http.StatusOK))
var obj []map[string]interface{}
err := json.Unmarshal([]byte(recorder.Body.String()), &obj)
var obj map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &obj)
Expect(err).NotTo(HaveOccurred())
Expect(obj).To(HaveLen(5))

offers := obj["offers"].([]interface{})
Expect(offers).To(HaveLen(5))
for i := 0; i < len(obj); i++ {
Expect(obj[i]).To(HaveKey("id"))
Expect(obj[i]).To(HaveKey("name"))
Expect(obj[i]).To(HaveKey("productId"))
Expect(obj[i]).To(HaveKey("gameId"))
Expect(obj[i]).To(HaveKey("contents"))
Expect(obj[i]).To(HaveKey("metadata"))
Expect(obj[i]).To(HaveKey("enabled"))
Expect(obj[i]).To(HaveKey("placement"))
Expect(obj[i]).To(HaveKey("period"))
Expect(obj[i]).To(HaveKey("frequency"))
Expect(obj[i]).To(HaveKey("trigger"))
offer := offers[i].(map[string]interface{})
Expect(offer).To(HaveKey("id"))
Expect(offer).To(HaveKey("name"))
Expect(offer).To(HaveKey("productId"))
Expect(offer).To(HaveKey("gameId"))
Expect(offer).To(HaveKey("contents"))
Expect(offer).To(HaveKey("metadata"))
Expect(offer).To(HaveKey("enabled"))
Expect(offer).To(HaveKey("placement"))
Expect(offer).To(HaveKey("period"))
Expect(offer).To(HaveKey("frequency"))
Expect(offer).To(HaveKey("trigger"))
}

pages := obj["pages"].(float64)
Expect(pages).To(Equal(float64(1)))
})

It("should return two offers with limit 2 and no offset", func() {
limit := 2
url := fmt.Sprintf("/offers?game-id=offers-game&limit=%d", limit)
request, _ := http.NewRequest("GET", url, nil)

app.Router.ServeHTTP(recorder, request)
Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json"))

Expect(recorder.Code).To(Equal(http.StatusOK))
var obj map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &obj)
Expect(err).NotTo(HaveOccurred())

offers := obj["offers"].([]interface{})
Expect(offers).To(HaveLen(2))

pages := obj["pages"].(float64)
Expect(pages).To(Equal(float64(3)))
})

It("should return three offers with no limit and offset 2", func() {
offset := 2
url := fmt.Sprintf("/offers?game-id=offers-game&offset=%d", offset)
request, _ := http.NewRequest("GET", url, nil)

app.Router.ServeHTTP(recorder, request)
Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json"))

Expect(recorder.Code).To(Equal(http.StatusOK))
var obj map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &obj)
Expect(err).NotTo(HaveOccurred())

offers := obj["offers"].([]interface{})
Expect(offers).To(HaveLen(3))

pages := obj["pages"].(float64)
Expect(pages).To(Equal(float64(1)))
})

It("should return three offers with no limit and offset 2", func() {
offset := 2
url := fmt.Sprintf("/offers?game-id=offers-game&offset=%d", offset)
request, _ := http.NewRequest("GET", url, nil)

app.Router.ServeHTTP(recorder, request)
Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json"))

Expect(recorder.Code).To(Equal(http.StatusOK))
var obj map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &obj)
Expect(err).NotTo(HaveOccurred())

offers := obj["offers"].([]interface{})
Expect(offers).To(HaveLen(3))

pages := obj["pages"].(float64)
Expect(pages).To(Equal(float64(1)))
})

It("should return error if limit is negative", func() {
limit := -1
url := fmt.Sprintf("/offers?game-id=offers-game&limit=%d", limit)
request, _ := http.NewRequest("GET", url, nil)

app.Router.ServeHTTP(recorder, request)
Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json"))

Expect(recorder.Code).To(Equal(http.StatusBadRequest))
var obj map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &obj)
Expect(err).NotTo(HaveOccurred())
Expect(obj["code"]).To(Equal("OFF-004"))
Expect(obj["error"]).To(Equal("The limit parameter must be an uint."))
Expect(obj["description"]).To(Equal("strconv.ParseUint: parsing \"-1\": invalid syntax"))
})

It("should return error if limit is not a number", func() {
limit := "qwerty"
url := fmt.Sprintf("/offers?game-id=offers-game&limit=%s", limit)
request, _ := http.NewRequest("GET", url, nil)

app.Router.ServeHTTP(recorder, request)
Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json"))

Expect(recorder.Code).To(Equal(http.StatusBadRequest))
var obj map[string]interface{}
err := json.Unmarshal([]byte(recorder.Body.String()), &obj)
Expect(err).NotTo(HaveOccurred())
Expect(obj["code"]).To(Equal("OFF-004"))
Expect(obj["error"]).To(Equal("The limit parameter must be an uint."))
Expect(obj["description"]).To(Equal("strconv.ParseUint: parsing \"qwerty\": invalid syntax"))
})

It("should return error if offset is negative", func() {
offset := -1
url := fmt.Sprintf("/offers?game-id=offers-game&offset=%d", offset)
request, _ := http.NewRequest("GET", url, nil)

app.Router.ServeHTTP(recorder, request)
Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json"))

Expect(recorder.Code).To(Equal(http.StatusBadRequest))
var obj map[string]interface{}
err := json.Unmarshal([]byte(recorder.Body.String()), &obj)
Expect(err).NotTo(HaveOccurred())
Expect(obj["code"]).To(Equal("OFF-004"))
Expect(obj["error"]).To(Equal("The offset parameter must be an uint."))
Expect(obj["description"]).To(Equal("strconv.ParseUint: parsing \"-1\": invalid syntax"))
})

It("should return error if offset is not a number", func() {
offset := "qwerty"
url := fmt.Sprintf("/offers?game-id=offers-game&offset=%s", offset)
request, _ := http.NewRequest("GET", url, nil)

app.Router.ServeHTTP(recorder, request)
Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json"))

Expect(recorder.Code).To(Equal(http.StatusBadRequest))
var obj map[string]interface{}
err := json.Unmarshal([]byte(recorder.Body.String()), &obj)
Expect(err).NotTo(HaveOccurred())
Expect(obj["code"]).To(Equal("OFF-004"))
Expect(obj["error"]).To(Equal("The offset parameter must be an uint."))
Expect(obj["description"]).To(Equal("strconv.ParseUint: parsing \"qwerty\": invalid syntax"))
})

It("should return empty list if no offers", func() {
Expand All @@ -1071,7 +1205,16 @@ var _ = Describe("Offer Template Handler", func() {
app.Router.ServeHTTP(recorder, request)
Expect(recorder.Header().Get("Content-Type")).To(Equal("application/json"))
Expect(recorder.Code).To(Equal(http.StatusOK))
Expect(recorder.Body.String()).To(Equal("[]"))

var obj map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &obj)
Expect(err).NotTo(HaveOccurred())

offers := obj["offers"].([]interface{})
Expect(offers).To(HaveLen(0))

pages := obj["pages"].(float64)
Expect(pages).To(Equal(float64(0)))
})

It("should return status code of 400 if game-id is not provided", func() {
Expand Down
39 changes: 36 additions & 3 deletions models/offer.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,49 @@ func GetEnabledOffers(db runner.Connection, gameID string, offersCache *cache.Ca
}

//ListOffers returns all the offer templates for a given game
func ListOffers(db runner.Connection, gameID string, mr *MixedMetricsReporter) ([]*Offer, error) {
var offers []*Offer
//return the number of pages using the number of offers and given the limit for each page
func ListOffers(
db runner.Connection,
gameID string,
limit, offset uint64,
mr *MixedMetricsReporter,
) ([]*Offer, int, error) {
offers := []*Offer{}
err := mr.WithDatastoreSegment("offers", SegmentSelect, func() error {
return db.
Select("*").
From("offers").
Where("game_id = $1", gameID).
OrderBy("created_at").
Limit(limit).
Offset(offset).
QueryStructs(&offers)
})
return offers, err
if err != nil {
return offers, 0, err
}

var numberOffers int
err = mr.WithDatastoreSegment("offers", SegmentSelect, func() error {
return db.
Select("COUNT(*)").
From("offers").
Where("game_id = $1", gameID).
QueryScalar(&numberOffers)
})
if err != nil {
return offers, 0, err
}

var pages int
if limit != 0 {
pages = numberOffers / int(limit)
if numberOffers%int(limit) != 0 {
pages = pages + 1
}
}

return offers, pages, nil
}

// InsertOffer inserts a new offer template into DB
Expand Down
53 changes: 51 additions & 2 deletions models/offer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/satori/go.uuid"
"github.com/topfreegames/offers/models"
. "github.com/topfreegames/offers/testing"
oTesting "github.com/topfreegames/offers/testing"
"gopkg.in/mgutz/dat.v2/dat"
runner "gopkg.in/mgutz/dat.v2/sqlx-runner"
"time"
Expand Down Expand Up @@ -173,15 +174,63 @@ var _ = Describe("Offer Models", func() {

Describe("List offers", func() {
It("Should return the full list of offers for a game", func() {
games, err := models.ListOffers(db, "offers-game", nil)
var limit uint64 = 5
var offset uint64 = 0
games, pages, err := models.ListOffers(db, "offers-game", limit, offset, nil)
Expect(err).NotTo(HaveOccurred())
Expect(games).To(HaveLen(5))
Expect(pages).To(Equal(1))
})

It("should return empty list if non-existing game id", func() {
games, err := models.ListOffers(db, "non-existing-game", nil)
var limit uint64 = 5
var offset uint64 = 0
games, pages, err := models.ListOffers(db, "non-existing-game", limit, offset, nil)
Expect(err).NotTo(HaveOccurred())
Expect(games).To(HaveLen(0))
Expect(pages).To(Equal(0))
})

It("should return two offers with limit 2 and offset 0", func() {
var limit uint64 = 2
var offset uint64 = 0
var pages int = 5/2 + 1
games, pages, err := models.ListOffers(db, "offers-game", limit, offset, nil)
Expect(err).NotTo(HaveOccurred())
Expect(games).To(HaveLen(2))
Expect(pages).To(Equal(pages))
})

It("should return one offer with limit 2 and offset 4", func() {
var limit uint64 = 2
var offset uint64 = 4
var pages int = 5/2 + 1
games, pages, err := models.ListOffers(db, "offers-game", limit, offset, nil)
Expect(err).NotTo(HaveOccurred())
Expect(games).To(HaveLen(1))
Expect(pages).To(Equal(pages))
})

It("should return 0 pages if limit is 0", func() {
var limit uint64 = 0
var offset uint64 = 0
var pages int = 0
games, pages, err := models.ListOffers(db, "offers-game", limit, offset, nil)
Expect(err).NotTo(HaveOccurred())
Expect(games).To(HaveLen(0))
Expect(pages).To(Equal(pages))
})

It("should return error if db isn't connected", func() {
db, err := oTesting.GetTestDB()
Expect(err).NotTo(HaveOccurred())
err = db.(*runner.DB).DB.Close()
Expect(err).NotTo(HaveOccurred())

var limit uint64 = 10
var offset uint64 = 0
_, _, err = models.ListOffers(db, "offers-game", limit, offset, nil)
Expect(err).To(HaveOccurred())
})
})

Expand Down

0 comments on commit f026452

Please sign in to comment.