Partial implementation of multi-receipt querys
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
This commit is contained in:
parent
a22bbbf5ab
commit
47ff895b26
@ -5,20 +5,42 @@ import (
|
||||
"net/http"
|
||||
|
||||
"git.ewellenr.ca/receipt_indexer/backend/internal/storage"
|
||||
"git.ewellenr.ca/receipt_indexer/backend/internal/storage/cache"
|
||||
)
|
||||
|
||||
type receiptKey string
|
||||
|
||||
const receiptCtx receiptKey = "receipt"
|
||||
|
||||
type receiptsQuery struct {
|
||||
Length int `json:"length"`
|
||||
List []*storage.Receipt `json:"list"`
|
||||
}
|
||||
|
||||
func (app *application) getReceiptsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// get the page and size from context
|
||||
// default them to something
|
||||
receipts, err := app.store.Receipts.GetForUser(r.Context(), getUserFromContext(r).ID)
|
||||
if err != nil {
|
||||
app.internalServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.jsonResponse(w, http.StatusOK, &receiptsQuery{Length: len(receipts), List: receipts}); err != nil {
|
||||
app.internalServerError(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) getReceiptHandler(w http.ResponseWriter, r *http.Request) {
|
||||
receipt, err := app.store.Receipts.GetByID(r.Context())
|
||||
if err != nil {
|
||||
app.internalServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.jsonResponse(w, http.StatusOK, &receiptsQuery{Length: len(receipts), List: receipts}); err != nil {
|
||||
app.internalServerError(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) createReceiptHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@ -41,7 +63,7 @@ func (app *application) deleteReceiptHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
func (app *application) getReceipt(ctx context.Context, receiptID int64) (*storage.Receipt, error) {
|
||||
if !app.config.redisCfg.enabled {
|
||||
if !app.config.cacheCfg.enabled {
|
||||
return app.store.Receipts.GetByID(ctx, receiptID)
|
||||
}
|
||||
|
||||
@ -64,6 +86,36 @@ func (app *application) getReceipt(ctx context.Context, receiptID int64) (*stora
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
func (app *application) getReceipts(ctx context.Context, userID int64) ([]*storage.Receipt, error) {
|
||||
if !app.config.cacheCfg.enabled {
|
||||
return app.store.Receipts.GetForUser(ctx, userID)
|
||||
}
|
||||
|
||||
var queryID int64 = 0 //encode it here
|
||||
receiptIDs, err := app.cacheStorage.ReceiptList.Get(ctx, queryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if receiptIDs == nil {
|
||||
receipts, err := app.store.Receipts.GetForUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receiptIDs = []int64{}
|
||||
for _, val := range receipts {
|
||||
receiptIDs = append(receiptIDs, val.ID)
|
||||
}
|
||||
|
||||
if err := app.cacheStorage.ReceiptList.Set(ctx, cache.ReceiptListKeyVal{ID: queryID, List: receiptIDs}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
func getReceiptFromContext(r *http.Request) *storage.Receipt {
|
||||
receipt, _ := r.Context().Value(receiptCtx).(*storage.Receipt)
|
||||
return receipt
|
||||
|
||||
18
backend/internal/storage/cache/cache.go
vendored
18
backend/internal/storage/cache/cache.go
vendored
@ -9,12 +9,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
UserExpTime = time.Minute
|
||||
RoleExpTime = time.Minute
|
||||
GroupExpTime = time.Minute
|
||||
UserGroupsExpTime = time.Minute
|
||||
ReceiptExpTime = time.Minute
|
||||
ReceiptImageExpTime = time.Minute
|
||||
UserExpTime = time.Minute
|
||||
RoleExpTime = time.Minute
|
||||
GroupExpTime = time.Minute
|
||||
UserGroupsExpTime = time.Minute
|
||||
ReceiptExpTime = time.Minute * 5
|
||||
ReceiptsQueryExpTime = time.Minute
|
||||
ReceiptImageExpTime = time.Minute
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
@ -43,6 +44,11 @@ type Storage struct {
|
||||
Set(ctx context.Context, receipt *storage.Receipt) error
|
||||
Delete(ctx context.Context, id int64)
|
||||
}
|
||||
ReceiptList interface {
|
||||
Get(ctx context.Context, id int64) ([]int64, error)
|
||||
Set(ctx context.Context, receiptidList ReceiptListKeyVal) error
|
||||
Delete(ctx context.Context, id int64)
|
||||
}
|
||||
ReceiptImage interface {
|
||||
Get(ctx context.Context, id int64) (*storage.Image, error)
|
||||
Set(ctx context.Context, image *storage.Image) error
|
||||
|
||||
59
backend/internal/storage/cache/redis-receipts.go
vendored
Normal file
59
backend/internal/storage/cache/redis-receipts.go
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type ReceiptListKeyVal struct {
|
||||
ID int64 `json:"id"`
|
||||
List []int64 `json:"list"`
|
||||
}
|
||||
|
||||
type ReceiptListStore struct {
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
func (s *ReceiptListStore) generateCacheKey(id int64) string {
|
||||
return fmt.Sprintf("receiptList-%d", id)
|
||||
}
|
||||
|
||||
func (s *ReceiptListStore) Get(ctx context.Context, id int64) ([]int64, error) {
|
||||
cacheKey := s.generateCacheKey(id)
|
||||
|
||||
data, err := s.rdb.Get(ctx, cacheKey).Result()
|
||||
if err == redis.Nil {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var receipts ReceiptListKeyVal
|
||||
if data != "" {
|
||||
err := json.Unmarshal([]byte(data), &receipts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return receipts.List, nil
|
||||
}
|
||||
|
||||
func (s *ReceiptListStore) Set(ctx context.Context, receiptidList ReceiptListKeyVal) error {
|
||||
cacheKey := s.generateCacheKey(receiptidList.ID)
|
||||
|
||||
json, err := json.Marshal(receiptidList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.rdb.Set(ctx, cacheKey, json, ReceiptsQueryExpTime).Err()
|
||||
}
|
||||
|
||||
func (s *ReceiptListStore) Delete(ctx context.Context, id int64) {
|
||||
cacheKey := s.generateCacheKey(id)
|
||||
s.rdb.Del(ctx, cacheKey)
|
||||
}
|
||||
@ -1,6 +1,15 @@
|
||||
package storage
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
l_context "git.ewellenr.ca/receipt_indexer/backend/internal/context"
|
||||
)
|
||||
|
||||
type Receipt struct {
|
||||
ID int64 `json:"id"`
|
||||
@ -19,3 +28,123 @@ type ReceiptData struct {
|
||||
Items map[string]float64 `json:"items"`
|
||||
// Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type ReceiptsQuery struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
SearchTerms []string `json:"search_terms"`
|
||||
MinTotal int `json:"min_amount"`
|
||||
MaxTotal int `json:"max_amount"`
|
||||
}
|
||||
|
||||
var (
|
||||
pageNumberKey = "page"
|
||||
pageSizeKey = "size"
|
||||
timeStartKey = "time_start"
|
||||
timeEndKey = "time_end"
|
||||
searchTermsKey = "search_terms"
|
||||
totalMinKey = "total_min"
|
||||
totalMaxKey = "total_max"
|
||||
)
|
||||
|
||||
const stringifiedQueryMaxLength = 524288000 // 500MiB
|
||||
|
||||
func newReceiptQuery() ReceiptsQuery {
|
||||
return ReceiptsQuery{
|
||||
UserID: -1,
|
||||
StartDate: time.Time{},
|
||||
EndDate: time.Time{},
|
||||
Page: 1,
|
||||
Size: 20,
|
||||
SearchTerms: nil,
|
||||
MinTotal: -1,
|
||||
MaxTotal: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeReceiptQuery(ctx context.Context, userID int64) (string, error) {
|
||||
|
||||
receiptQuery := newReceiptQuery()
|
||||
|
||||
receiptQuery.UserID = userID
|
||||
|
||||
var urlParams url.Values = nil
|
||||
if ctx.Value(l_context.QueryParamsCtx) != nil {
|
||||
urlParams = ctx.Value(l_context.QueryParamsCtx).(url.Values)
|
||||
}
|
||||
|
||||
if urlParams == nil {
|
||||
return "", ErrInvalidReceiptsQuery
|
||||
}
|
||||
|
||||
// Extract the search terms
|
||||
if urlParams.Has(pageNumberKey) {
|
||||
temp, err := strconv.Atoi(urlParams.Get(pageNumberKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
receiptQuery.Page = temp
|
||||
}
|
||||
|
||||
if urlParams.Has(pageSizeKey) {
|
||||
temp, err := strconv.Atoi(urlParams.Get(pageSizeKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
receiptQuery.Page = temp
|
||||
}
|
||||
|
||||
if urlParams.Has(timeStartKey) {
|
||||
temp, err := time.Parse(time.DateTime, urlParams.Get(timeStartKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
receiptQuery.StartDate = temp
|
||||
}
|
||||
|
||||
if urlParams.Has(timeEndKey) {
|
||||
temp, err := time.Parse(time.DateTime, urlParams.Get(timeEndKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
receiptQuery.EndDate = temp
|
||||
}
|
||||
|
||||
if urlParams.Has(totalMinKey) {
|
||||
temp, err := strconv.Atoi(urlParams.Get(totalMinKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
receiptQuery.MinTotal = temp
|
||||
}
|
||||
|
||||
if urlParams.Has(totalMaxKey) {
|
||||
temp, err := strconv.Atoi(urlParams.Get(totalMaxKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
receiptQuery.MaxTotal = temp
|
||||
}
|
||||
|
||||
if urlParams.Has(searchTermsKey) {
|
||||
temp := urlParams[searchTermsKey]
|
||||
receiptQuery.SearchTerms = temp
|
||||
}
|
||||
|
||||
// encoding the query into a string so that it can be used as a query term
|
||||
jsonEncoding, err := json.Marshal(receiptQuery)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out := base64.StdEncoding.EncodeToString(jsonEncoding)
|
||||
// convert userID to base64 encoding of the int (so bitwise of the int and then into base64)
|
||||
|
||||
if len(out) > stringifiedQueryMaxLength { // Makes the assumption that none of the other filters would cause the overflow of 500MiB
|
||||
return "", ErrSearchTermTooLong
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@ -8,10 +8,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrConflict = errors.New("resource already exists")
|
||||
ErrDuplicateEmail = errors.New("a user with that email already exists")
|
||||
ErrDuplicateUsername = errors.New("a user with that username already exists")
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrConflict = errors.New("resource already exists")
|
||||
ErrDuplicateEmail = errors.New("a user with that email already exists")
|
||||
ErrDuplicateUsername = errors.New("a user with that username already exists")
|
||||
ErrInvalidReceiptsQuery = errors.New("invalid receipts query")
|
||||
ErrSearchTermTooLong = errors.New("search term too long")
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
@ -62,6 +64,7 @@ type Storage struct {
|
||||
GetByID(ctx context.Context, id int64) (*Receipt, error)
|
||||
Create(ctx context.Context, receipt *Receipt) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
GetForUser(ctx context.Context, userid int64) ([]*Receipt, error)
|
||||
}
|
||||
Images interface {
|
||||
GetByID(context.Context, int64) (*Image, error)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user