Skip to content

Commit

Permalink
Merge pull request #24 from Linesmerrill/update/paginated-api-calls
Browse files Browse the repository at this point in the history
Update/paginated api calls
  • Loading branch information
Linesmerrill authored Oct 31, 2023
2 parents 47e0672 + 174f202 commit 93de263
Show file tree
Hide file tree
Showing 46 changed files with 4,837 additions and 578 deletions.
21 changes: 16 additions & 5 deletions api/handlers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"io"
"net/http"

"github.com/linesmerrill/police-cad-api/api/handlers/search"

"github.com/linesmerrill/police-cad-api/models"

"github.com/gorilla/mux"
Expand All @@ -17,7 +15,7 @@ import (
"github.com/linesmerrill/police-cad-api/databases"
)

// App stores the router and db connection so it can be reused
// App stores the router and db connection, so it can be reused
type App struct {
Router *mux.Router
DB databases.CollectionHelper
Expand All @@ -31,12 +29,13 @@ func (a *App) New() *mux.Router {

u := User{DB: databases.NewUserDatabase(a.dbHelper)}
c := Community{DB: databases.NewCommunityDatabase(a.dbHelper)}
n := search.NameSearch{DB: databases.NewCivilianDatabase(a.dbHelper)}
civ := Civilian{DB: databases.NewCivilianDatabase(a.dbHelper)}
v := Vehicle{DB: databases.NewVehicleDatabase(a.dbHelper)}
f := Firearm{DB: databases.NewFirearmDatabase(a.dbHelper)}
l := License{DB: databases.NewLicenseDatabase(a.dbHelper)}
e := Ems{DB: databases.NewEmsDatabase(a.dbHelper)}
ev := EmsVehicle{DB: databases.NewEmsVehicleDatabase(a.dbHelper)}
w := Warrant{DB: databases.NewWarrantDatabase(a.dbHelper)}
call := Call{DB: databases.NewCallDatabase(a.dbHelper)}

// healthchex
Expand All @@ -52,13 +51,25 @@ func (a *App) New() *mux.Router {
apiCreate.Handle("/civilian/{civilian_id}", api.Middleware(http.HandlerFunc(civ.CivilianByIDHandler))).Methods("GET")
apiCreate.Handle("/civilians", api.Middleware(http.HandlerFunc(civ.CivilianHandler))).Methods("GET")
apiCreate.Handle("/civilians/user/{user_id}", api.Middleware(http.HandlerFunc(civ.CiviliansByUserIDHandler))).Methods("GET")
apiCreate.Handle("/name-search", api.Middleware(http.HandlerFunc(n.NameSearchHandler))).Methods("GET")
apiCreate.Handle("/civilians/search", api.Middleware(http.HandlerFunc(civ.CiviliansByNameSearchHandler))).Methods("GET")
apiCreate.Handle("/vehicle/{vehicle_id}", api.Middleware(http.HandlerFunc(v.VehicleByIDHandler))).Methods("GET")
apiCreate.Handle("/vehicles", api.Middleware(http.HandlerFunc(v.VehicleHandler))).Methods("GET")
apiCreate.Handle("/vehicles/user/{user_id}", api.Middleware(http.HandlerFunc(v.VehiclesByUserIDHandler))).Methods("GET")
apiCreate.Handle("/vehicles/registered-owner/{registered_owner_id}", api.Middleware(http.HandlerFunc(v.VehiclesByRegisteredOwnerIDHandler))).Methods("GET")
apiCreate.Handle("/vehicles/search", api.Middleware(http.HandlerFunc(v.VehiclesByPlateSearchHandler))).Methods("GET")
apiCreate.Handle("/firearm/{firearm_id}", api.Middleware(http.HandlerFunc(f.FirearmByIDHandler))).Methods("GET")
apiCreate.Handle("/firearms", api.Middleware(http.HandlerFunc(f.FirearmHandler))).Methods("GET")
apiCreate.Handle("/firearms/user/{user_id}", api.Middleware(http.HandlerFunc(f.FirearmsByUserIDHandler))).Methods("GET")
apiCreate.Handle("/firearms/registered-owner/{registered_owner_id}", api.Middleware(http.HandlerFunc(f.FirearmsByRegisteredOwnerIDHandler))).Methods("GET")
apiCreate.Handle("/license/{license_id}", api.Middleware(http.HandlerFunc(l.LicenseByIDHandler))).Methods("GET")
apiCreate.Handle("/licenses", api.Middleware(http.HandlerFunc(l.LicenseHandler))).Methods("GET")
apiCreate.Handle("/licenses/user/{user_id}", api.Middleware(http.HandlerFunc(l.LicensesByUserIDHandler))).Methods("GET")
apiCreate.Handle("/licenses/owner/{owner_id}", api.Middleware(http.HandlerFunc(l.LicensesByOwnerIDHandler))).Methods("GET")

apiCreate.Handle("/warrant/{warrant_id}", api.Middleware(http.HandlerFunc(w.WarrantByIDHandler))).Methods("GET")
apiCreate.Handle("/warrants", api.Middleware(http.HandlerFunc(w.WarrantHandler))).Methods("GET")
apiCreate.Handle("/warrants/user/{user_id}", api.Middleware(http.HandlerFunc(w.WarrantsByUserIDHandler))).Methods("GET")

apiCreate.Handle("/ems/{ems_id}", api.Middleware(http.HandlerFunc(e.EmsByIDHandler))).Methods("GET")
apiCreate.Handle("/ems", api.Middleware(http.HandlerFunc(e.EmsHandler))).Methods("GET")
apiCreate.Handle("/ems/user/{user_id}", api.Middleware(http.HandlerFunc(e.EmsByUserIDHandler))).Methods("GET")
Expand Down
11 changes: 10 additions & 1 deletion api/handlers/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"

"github.com/gorilla/mux"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"

"github.com/linesmerrill/police-cad-api/config"
Expand All @@ -23,7 +25,14 @@ type Call struct {

// CallHandler returns all calls
func (c Call) CallHandler(w http.ResponseWriter, r *http.Request) {
dbResp, err := c.DB.Find(context.TODO(), bson.M{})
Limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
zap.S().Warnf(fmt.Sprintf("limit not set, using default of %v, err: %v", Limit|10, err))
}
limit64 := int64(Limit)
Page = getPage(Page, r)
skip64 := int64(Page * Limit)
dbResp, err := c.DB.Find(context.TODO(), bson.D{}, &options.FindOptions{Limit: &limit64, Skip: &skip64})
if err != nil {
config.ErrorStatus("failed to get calls", http.StatusNotFound, w, err)
return
Expand Down
22 changes: 11 additions & 11 deletions api/handlers/call_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func TestCall_CallHandlerJsonMarshalError(t *testing.T) {
arg := args.Get(0).(*[]models.Call)
*arg = []models.Call{{Details: models.CallDetails{CreatedAt: x}}}
})
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(singleResultHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(singleResultHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -284,7 +284,7 @@ func TestCall_CallHandlerFailedToFindOne(t *testing.T) {
client.(*mocks.ClientHelper).On("StartSession").Return(nil, errors.New("mocked-error"))
db.(*MockDatabaseHelper).On("Client").Return(client)
singleResultHelper.(*mocks.SingleResultHelper).On("Decode", mock.Anything).Return(errors.New("mongo: no documents in result"))
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(singleResultHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(singleResultHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -333,7 +333,7 @@ func TestCall_CallHandlerSuccess(t *testing.T) {
*arg = []models.Call{{ID: "5fc51f36c72ff10004dca381"}}

})
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(singleResultHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(singleResultHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -380,7 +380,7 @@ func TestCall_CallHandlerEmptyResponse(t *testing.T) {
arg := args.Get(0).(*[]models.Call)
*arg = nil
})
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(cursorHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(cursorHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -433,7 +433,7 @@ func TestCall_CallsByCommunityIDHandlerJsonMarshalError(t *testing.T) {
arg := args.Get(0).(*[]models.Call)
*arg = []models.Call{{Details: models.CallDetails{CreatedAt: x}}}
})
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(singleResultHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(singleResultHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -480,7 +480,7 @@ func TestCall_CallsByCommunityIDHandlerFailedToFindOne(t *testing.T) {
client.(*mocks.ClientHelper).On("StartSession").Return(nil, errors.New("mocked-error"))
db.(*MockDatabaseHelper).On("Client").Return(client)
singleResultHelper.(*mocks.SingleResultHelper).On("Decode", mock.Anything).Return(errors.New("mongo: no documents in result"))
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(singleResultHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(singleResultHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -527,7 +527,7 @@ func TestCall_CallsByCommunityIDHandlerActiveCommunityIDFailedToFindOne(t *testi
client.(*mocks.ClientHelper).On("StartSession").Return(nil, errors.New("mocked-error"))
db.(*MockDatabaseHelper).On("Client").Return(client)
singleResultHelper.(*mocks.SingleResultHelper).On("Decode", mock.Anything).Return(errors.New("mongo: no documents in result"))
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(singleResultHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(singleResultHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -579,7 +579,7 @@ func TestCall_CallsByCommunityIDHandlerSuccess(t *testing.T) {
*arg = []models.Call{{ID: "5fc51f36c72ff10004dca381"}}

})
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(singleResultHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(singleResultHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -630,7 +630,7 @@ func TestCall_CallsByCommunityIDHandlerSuccessWithActiveCommunityID(t *testing.T
*arg = []models.Call{{ID: "5fc51f36c72ff10004dca381"}}

})
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(singleResultHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(singleResultHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -681,7 +681,7 @@ func TestCall_CallsByCommunityIDHandlerSuccessWithNullCommunityID(t *testing.T)
*arg = []models.Call{{ID: "5fc51f36c72ff10004dca381"}}

})
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(singleResultHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(singleResultHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down Expand Up @@ -731,7 +731,7 @@ func TestCall_CallsByCommunityIDHandlerEmptyResponse(t *testing.T) {
arg := args.Get(0).(*[]models.Call)
*arg = nil
})
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything).Return(cursorHelper)
conn.(*mocks.CollectionHelper).On("Find", mock.Anything, mock.Anything, mock.Anything).Return(cursorHelper)
db.(*MockDatabaseHelper).On("Collection", "calls").Return(conn)

callDatabase := databases.NewCallDatabase(db)
Expand Down
97 changes: 93 additions & 4 deletions api/handlers/civilian.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,41 @@ package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"

"github.com/gorilla/mux"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"

"github.com/linesmerrill/police-cad-api/config"
"github.com/linesmerrill/police-cad-api/databases"
"github.com/linesmerrill/police-cad-api/models"
)

var (
// Page denotes the starting Page for pagination results
Page = 0
)

// Civilian exported for testing purposes
type Civilian struct {
DB databases.CivilianDatabase
}

// CivilianHandler returns all civilians
func (c Civilian) CivilianHandler(w http.ResponseWriter, r *http.Request) {
dbResp, err := c.DB.Find(context.TODO(), bson.M{})
Limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
zap.S().Warnf(fmt.Sprintf("limit not set, using default of %v, err: %v", Limit|10, err))
}
limit64 := int64(Limit)
Page = getPage(Page, r)
skip64 := int64(Page * Limit)
dbResp, err := c.DB.Find(context.TODO(), bson.D{}, &options.FindOptions{Limit: &limit64, Skip: &skip64})
if err != nil {
config.ErrorStatus("failed to get civilians", http.StatusNotFound, w, err)
return
Expand Down Expand Up @@ -72,6 +87,13 @@ func (c Civilian) CivilianByIDHandler(w http.ResponseWriter, r *http.Request) {
func (c Civilian) CiviliansByUserIDHandler(w http.ResponseWriter, r *http.Request) {
userID := mux.Vars(r)["user_id"]
activeCommunityID := r.URL.Query().Get("active_community_id")
Limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
zap.S().Warnf(fmt.Sprintf("limit not set, using default of %v", Limit|10))
}
limit64 := int64(Limit)
Page = getPage(Page, r)
skip64 := int64(Page * Limit)

zap.S().Debugf("user_id: '%v'", userID)
zap.S().Debugf("active_community: '%v'", activeCommunityID)
Expand All @@ -84,12 +106,12 @@ func (c Civilian) CiviliansByUserIDHandler(w http.ResponseWriter, r *http.Reques
//
// Likewise, if the user is not in a community, then we will display only the civilians
// that are not in a community
var err error
err = nil
if activeCommunityID != "" && activeCommunityID != "null" && activeCommunityID != "undefined" {
dbResp, err = c.DB.Find(context.TODO(), bson.M{
"civilian.userID": userID,
"civilian.activeCommunityID": activeCommunityID,
})
}, &options.FindOptions{Limit: &limit64, Skip: &skip64})
if err != nil {
config.ErrorStatus("failed to get civilians with active community id", http.StatusNotFound, w, err)
return
Expand All @@ -101,7 +123,7 @@ func (c Civilian) CiviliansByUserIDHandler(w http.ResponseWriter, r *http.Reques
{"civilian.activeCommunityID": nil},
{"civilian.activeCommunityID": ""},
},
})
}, &options.FindOptions{Limit: &limit64, Skip: &skip64})
if err != nil {
config.ErrorStatus("failed to get civilians with empty active community id", http.StatusNotFound, w, err)
return
Expand All @@ -121,3 +143,70 @@ func (c Civilian) CiviliansByUserIDHandler(w http.ResponseWriter, r *http.Reques
w.WriteHeader(http.StatusOK)
w.Write(b)
}

// CiviliansByNameSearchHandler returns paginated list of civilians that match the give name
func (c Civilian) CiviliansByNameSearchHandler(w http.ResponseWriter, r *http.Request) {
firstName := r.URL.Query().Get("first_name")
lastName := r.URL.Query().Get("last_name")
activeCommunityID := r.URL.Query().Get("active_community_id") // optional
Limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
zap.S().Warnf(fmt.Sprintf("limit not set, using default of %v", Limit|10))
}
limit64 := int64(Limit)
Page = getPage(Page, r)
skip64 := int64(Page * Limit)

zap.S().Debugf("first_name: '%v', last_name: '%v'", firstName, lastName)
zap.S().Debugf("active_community: '%v'", activeCommunityID)

var dbResp []models.Civilian

// If the user is in a community then we want to search for civilians that
// are in that same community. This way each user can have different civilians
// across different communities.
//
// Likewise, if the user is not in a community, then we will display only the civilians
// that are not in a community
err = nil
dbResp, err = c.DB.Find(context.TODO(), bson.M{
"$text": bson.M{
"$search": fmt.Sprintf("%s %s", firstName, lastName),
},
"civilian.activeCommunityID": activeCommunityID,
}, &options.FindOptions{Limit: &limit64, Skip: &skip64})
if err != nil {
config.ErrorStatus("failed to get civilian name search", http.StatusNotFound, w, err)
return
}

// Because the frontend requires that the data elements inside models.Civilians exist, if
// len == 0 then we will just return an empty data object
if len(dbResp) == 0 {
dbResp = []models.Civilian{}
}
b, err := json.Marshal(dbResp)
if err != nil {
config.ErrorStatus("failed to marshal response", http.StatusInternalServerError, w, err)
return
}
w.WriteHeader(http.StatusOK)
w.Write(b)
}

func getPage(Page int, r *http.Request) int {
if r.URL.Query().Get("page") == "" {
zap.S().Warnf("page not set, using default of %v", Page)
} else {
var err error
Page, err = strconv.Atoi(r.URL.Query().Get("page"))
if err != nil {
zap.S().Errorf(fmt.Sprintf("error parsing page number: %v", err))
}
if Page < 0 {
zap.S().Warnf(fmt.Sprintf("cannot process page number less than 1. Got: %v", Page))
return 0
}
}
return Page
}
Loading

0 comments on commit 93de263

Please sign in to comment.