├── graph ├── rest │ ├── item.go │ ├── site.go │ ├── retailer.go │ ├── client.go │ ├── alert.go │ ├── cart.go │ ├── event.go │ └── session.go ├── .graphql-schema-linterrc ├── model │ ├── pagination.go │ ├── timestamp.go │ ├── int64.go │ ├── session.go │ ├── retailer.go │ ├── item.go │ ├── site.go │ ├── alert.go │ ├── event.go │ ├── cart.go │ └── models_gen.go ├── resolver │ ├── resolver.go │ ├── item.resolver.go │ ├── helper │ │ └── session.helper.go │ ├── schema.resolver.go │ ├── retailer.resolver.go │ ├── alert.resolver.go │ ├── site.resolver.go │ ├── cart.resolver.go │ ├── event.resolver.go │ └── session.resolver.go ├── dataloader │ ├── cartloader.go │ ├── siteloader.go │ ├── alertloader.go │ ├── sessionloader.go │ ├── retailerloader.go │ ├── unifiedsessionloader.go │ ├── cartregistrationloader.go │ ├── dataloaders.go │ ├── cartloader_gen.go │ ├── siteloader_gen.go │ ├── alertloader_gen.go │ ├── sessionloader_gen.go │ ├── retailerloader_gen.go │ ├── unifiedsessionloader_gen.go │ └── cartregistrationloader_gen.go └── schema │ ├── schema.graphql │ ├── retailer.graphql │ ├── item.graphql │ ├── site.graphql │ ├── alert.graphql │ ├── session.graphql │ ├── cart.graphql │ └── event.graphql ├── go.mod ├── tools.go ├── Dockerfile ├── etc └── .env ├── docker-compose.yml ├── router └── router.go ├── .gitignore ├── Makefile ├── main.go ├── handler └── handler.go ├── internal ├── middleware │ └── context.go └── config │ └── config.go ├── README.md └── gqlgen.yml /graph/rest/item.go: -------------------------------------------------------------------------------- 1 | package rest 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/s3ndd/sen-graphql-go 2 | 3 | go 1.23.5 4 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package main 5 | 6 | import ( 7 | _ "github.com/99designs/gqlgen" 8 | _ "github.com/vektah/dataloaden" 9 | ) 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.12 2 | 3 | RUN apk --no-cache add ca-certificates openssl && update-ca-certificates 4 | 5 | WORKDIR /opt/sen 6 | 7 | COPY sen-graphql-go ./bin/sen-graphql-go 8 | COPY ./etc ./etc 9 | 10 | ENTRYPOINT ["/opt/sen/bin/sen-graphql-go"] 11 | -------------------------------------------------------------------------------- /etc/.env: -------------------------------------------------------------------------------- 1 | ENV=prod 2 | SOURCE_PROGRAM=github.com/s3ndd/sen-graphql-go 3 | LOG_FORMAT=json 4 | LOG_LEVEL=info 5 | PORT=60000 6 | 7 | API_GATEWAY_BASEURL=api-staging.sen.live 8 | HTTP_CLIENT_USE_SECURE=true 9 | HTTP_CLIENT_TIMEOUT=5s 10 | SERVICE_API_KEY=dy8lmdfvwuh!glvh0&yp@e7lv$tg0^kv -------------------------------------------------------------------------------- /graph/.graphql-schema-linterrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | "defined-types-are-used", 4 | "deprecations-have-a-reason", 5 | "enum-values-all-caps", 6 | "fields-are-camel-cased", 7 | "input-object-values-are-camel-cased", 8 | "types-are-capitalized" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /graph/model/pagination.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Pagination structure that includes json representation 4 | type Pagination struct { 5 | PageIndex int `json:"page_index"` 6 | PageSize int `json:"page_size"` 7 | TotalRecordCount int64 `json:"total_record_count"` 8 | } 9 | -------------------------------------------------------------------------------- /graph/resolver/resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | //go:generate go run github.com/99designs/gqlgen 4 | 5 | // This file will not be regenerated automatically. 6 | // 7 | // It serves as dependency injection for your app, add any dependencies you require here. 8 | 9 | type Resolver struct{} 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | networks: 3 | default: 4 | driver: bridge 5 | 6 | services: 7 | github.com/s3ndd/sen-graphql-go: 8 | image: sen-graphql-go:master 9 | build: 10 | context: . 11 | dockerfile: Dockerfile 12 | container_name: github.com/s3ndd/sen-graphql-go 13 | restart: always 14 | ports: 15 | - '60999:60000' 16 | expose: 17 | - '60000' 18 | networks: 19 | - default -------------------------------------------------------------------------------- /graph/dataloader/cartloader.go: -------------------------------------------------------------------------------- 1 | package dataloader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/s3ndd/sen-graphql-go/graph/model" 7 | "github.com/s3ndd/sen-graphql-go/graph/rest" 8 | ) 9 | 10 | func newCartLoader(ctx context.Context) *CartLoader { 11 | return &CartLoader{ 12 | wait: waitTime, 13 | maxBatch: maxBatch, 14 | fetch: func(cartIDs []string) ([]*model.Cart, []error) { 15 | carts, err := rest.GetCartByIDs(ctx, cartIDs) 16 | return carts, errors(err) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graph/dataloader/siteloader.go: -------------------------------------------------------------------------------- 1 | package dataloader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/s3ndd/sen-graphql-go/graph/model" 7 | "github.com/s3ndd/sen-graphql-go/graph/rest" 8 | ) 9 | 10 | func newSiteLoader(ctx context.Context) *SiteLoader { 11 | return &SiteLoader{ 12 | wait: waitTime, 13 | maxBatch: maxBatch, 14 | fetch: func(siteIDs []string) ([]*model.Site, []error) { 15 | sites, err := rest.GetSiteByIDs(ctx, siteIDs) 16 | return sites, errors(err) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graph/dataloader/alertloader.go: -------------------------------------------------------------------------------- 1 | package dataloader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/s3ndd/sen-graphql-go/graph/model" 7 | "github.com/s3ndd/sen-graphql-go/graph/rest" 8 | ) 9 | 10 | func newAlertLoader(ctx context.Context) *AlertLoader { 11 | return &AlertLoader{ 12 | wait: waitTime, 13 | maxBatch: maxBatch, 14 | fetch: func(cartIDs []string) ([]*model.Alert, []error) { 15 | alerts, err := rest.GetAlertByIDs(ctx, cartIDs) 16 | return alerts, errors(err) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graph/dataloader/sessionloader.go: -------------------------------------------------------------------------------- 1 | package dataloader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/s3ndd/sen-graphql-go/graph/model" 7 | "github.com/s3ndd/sen-graphql-go/graph/rest" 8 | ) 9 | 10 | func newSessionLoader(ctx context.Context) *SessionLoader { 11 | return &SessionLoader{ 12 | wait: waitTime, 13 | maxBatch: maxBatch, 14 | fetch: func(sessionIDs []string) ([]*model.Session, []error) { 15 | sessions, err := rest.GetSessionByIDs(ctx, sessionIDs) 16 | return sessions, errors(err) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graph/dataloader/retailerloader.go: -------------------------------------------------------------------------------- 1 | package dataloader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/s3ndd/sen-graphql-go/graph/model" 7 | "github.com/s3ndd/sen-graphql-go/graph/rest" 8 | ) 9 | 10 | func newRetailerLoader(ctx context.Context) *RetailerLoader { 11 | return &RetailerLoader{ 12 | wait: waitTime, 13 | maxBatch: maxBatch, 14 | fetch: func(retailerIDs []string) ([]*model.Retailer, []error) { 15 | retailers, err := rest.GetRetailerByIDs(ctx, retailerIDs) 16 | return retailers, errors(err) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graph/dataloader/unifiedsessionloader.go: -------------------------------------------------------------------------------- 1 | package dataloader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/s3ndd/sen-graphql-go/graph/model" 7 | "github.com/s3ndd/sen-graphql-go/graph/rest" 8 | ) 9 | 10 | func newUnifiedSessionLoader(ctx context.Context) *UnifiedSessionLoader { 11 | return &UnifiedSessionLoader{ 12 | wait: waitTime, 13 | maxBatch: maxBatch, 14 | fetch: func(sessionIDs []string) ([]*model.Session, []error) { 15 | sessions, err := rest.GetUnifiedSessionByIDs(ctx, sessionIDs) 16 | return sessions, errors(err) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graph/model/timestamp.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | "github.com/99designs/gqlgen/graphql" 10 | ) 11 | 12 | func MarshalTimestamp(t time.Time) graphql.Marshaler { 13 | return graphql.WriterFunc(func(w io.Writer) { 14 | fmt.Fprintf(w, `"%s"`, t.Format(time.RFC3339Nano)) 15 | }) 16 | } 17 | 18 | func UnmarshalTimestamp(v interface{}) (time.Time, error) { 19 | if s, ok := v.(string); ok { 20 | return time.Parse(time.RFC3339Nano, s) 21 | } 22 | return time.Time{}, errors.New("timestamp format error") 23 | } 24 | -------------------------------------------------------------------------------- /graph/dataloader/cartregistrationloader.go: -------------------------------------------------------------------------------- 1 | package dataloader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/s3ndd/sen-graphql-go/graph/model" 7 | "github.com/s3ndd/sen-graphql-go/graph/rest" 8 | ) 9 | 10 | func newCartRegistrationLoader(ctx context.Context) *CartRegistrationLoader { 11 | return &CartRegistrationLoader{ 12 | wait: waitTime, 13 | maxBatch: maxBatch, 14 | fetch: func(cartIDs []string) ([]*model.CartRegistration, []error) { 15 | carts, err := rest.GetCartRegistrationByIDs(ctx, cartIDs) 16 | return carts, errors(err) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graph/model/int64.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "strconv" 7 | 8 | "github.com/99designs/gqlgen/graphql" 9 | ) 10 | 11 | func MarshalInt64(i int64) graphql.Marshaler { 12 | return graphql.WriterFunc(func(w io.Writer) { 13 | b := strconv.AppendInt(nil, i, 10) 14 | _, _ = w.Write(b) 15 | }) 16 | } 17 | 18 | func UnmarshalInt64(v interface{}) (int64, error) { 19 | switch v := v.(type) { 20 | case string: 21 | i, err := strconv.ParseInt(v, 10, 64) 22 | if err == nil { 23 | return i, nil 24 | } 25 | case int64: 26 | return v, nil 27 | case int: 28 | return int64(v), nil 29 | } 30 | return 0, errors.New("not Int64") 31 | } 32 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/s3ndd/sen-graphql-go/graph/dataloader" 7 | "github.com/s3ndd/sen-graphql-go/handler" 8 | "github.com/s3ndd/sen-graphql-go/internal/middleware" 9 | ) 10 | 11 | // InitRouter initialize routing information 12 | func InitRouter() *gin.Engine { 13 | r := gin.New() 14 | r.Use(gin.Recovery()).Use(middleware.GinContextToContextMiddleware()).Use(dataloader.LoaderMiddleware()) 15 | 16 | apiV1 := r.Group("/api/v1") 17 | apiV1.GET("/healthcheck", handler.HealthCheck()) 18 | aipGraphql := r.Group("/graphql") 19 | aipGraphql.POST("/query", handler.GraphqlHandler()) 20 | aipGraphql.GET("/graphiql", handler.PlaygroundHandler()) 21 | 22 | return r 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | 10 | # Compiled Java class files 11 | *.class 12 | 13 | # Compiled Python bytecode 14 | *.py[cod] 15 | 16 | # Log files 17 | *.log 18 | 19 | # Package files 20 | *.jar 21 | 22 | # Maven 23 | target/ 24 | dist/ 25 | 26 | # JetBrains IDE 27 | .idea/ 28 | 29 | # Unit test reports 30 | TEST*.xml 31 | 32 | # Generated by MacOS 33 | .DS_Store 34 | 35 | # Generated by Windows 36 | Thumbs.db 37 | 38 | # Applications 39 | *.app 40 | *.exe 41 | *.war 42 | 43 | # Large media files 44 | *.mp4 45 | *.tiff 46 | *.avi 47 | *.flv 48 | *.mov 49 | *.wmv 50 | 51 | -------------------------------------------------------------------------------- /graph/resolver/item.resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "github.com/s3ndd/sen-graphql-go/graph/generated" 10 | "github.com/s3ndd/sen-graphql-go/graph/model" 11 | ) 12 | 13 | func (r *itemResolver) DiscountType(ctx context.Context, obj *model.Item) (string, error) { 14 | return obj.DiscountType.String(), nil 15 | } 16 | 17 | func (r *queryResolver) Items(ctx context.Context, sessionID string) ([]*model.Item, error) { 18 | panic(fmt.Errorf("not implemented")) 19 | } 20 | 21 | // Item returns generated.ItemResolver implementation. 22 | func (r *Resolver) Item() generated.ItemResolver { return &itemResolver{r} } 23 | 24 | type itemResolver struct{ *Resolver } 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DIR := ${CURDIR} 2 | 3 | lint: 4 | ./ci/lint.sh 5 | 6 | fmt: 7 | ./ci/fmt.sh 8 | 9 | test: 10 | ./ci/test.sh 11 | 12 | run: 13 | go run main.go 14 | 15 | run-doc: 16 | swagger serve --port=63000 --flavor=swagger swagger.yml 17 | 18 | validate-doc: 19 | swagger validate swagger.yml 20 | 21 | go-build: 22 | CGO_ENABLED=0 GOOS=linux go build -o github.com/s3ndd/sen-graphql-go ./main.go 23 | 24 | build: 25 | CGO_ENABLED=0 GOOS=linux go build -o github.com/s3ndd/sen-graphql-go ./main.go && docker-compose build github.com/s3ndd/sen-graphql-go 26 | 27 | up: 28 | docker-compose up -d $(filter-out $@,$(MAKECMDGOALS)) 29 | 30 | stop: 31 | docker-compose stop $(filter-out $@,$(MAKECMDGOALS)) 32 | 33 | down: 34 | docker-compose down -v 35 | 36 | logs: 37 | docker-compose logs -f --tail=100 $(filter-out $@,$(MAKECMDGOALS)) 38 | 39 | 40 | %: 41 | @: 42 | 43 | .PHONY: all test clean 44 | -------------------------------------------------------------------------------- /graph/resolver/helper/session.helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "context" 5 | "github.com/s3ndd/sen-go/log" 6 | "github.com/s3ndd/sen-graphql-go/graph/dataloader" 7 | "github.com/s3ndd/sen-graphql-go/graph/model" 8 | ) 9 | 10 | func LoadSessionByID(ctx context.Context, id string) (*model.Session, error) { 11 | logger := log.ForRequest(ctx).WithField("session_id", id) 12 | loaders := dataloader.ContextLoaders(ctx) 13 | session, err := loaders.Sessions.Load(id) 14 | if err != nil { 15 | logger.WithError(err).Error("failed to get the session from the session loader") 16 | return nil, err 17 | } 18 | if session != nil { 19 | return session, nil 20 | } 21 | 22 | session, err = loaders.UnifiedSessions.Load(id) 23 | if err != nil { 24 | logger.WithError(err).Error("failed to get the session from the unified session loader") 25 | return nil, err 26 | } 27 | return session, nil 28 | } 29 | -------------------------------------------------------------------------------- /graph/model/session.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Session struct { 8 | ID string `json:"id"` 9 | RetailerID *string `json:"retailer_id,omitempty"` 10 | CartID string `json:"cart_id"` 11 | Cart *Cart `json:"cart,omitempty"` 12 | UserID *string `json:"user_id"` 13 | SiteID string `json:"site_id"` 14 | Status string `json:"status"` 15 | IntegrationType 16 | ExternalToken *string `json:"external_token"` 17 | DealBarcode *string `json:"deal_barcode"` 18 | Items []Item `json:"items"` 19 | ItemsPrePos []Item `json:"items_pre_pos"` 20 | Total float64 `json:"total"` 21 | TotalSavings float64 `json:"total_savings"` 22 | TotalTax []Tax `json:"tax"` 23 | Created time.Time `json:"created"` 24 | Updated time.Time `json:"updated"` 25 | } 26 | 27 | type Tax struct { 28 | Rate float64 `json:"rate"` 29 | Amount float64 `json:"amount"` 30 | } 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | internalConfig "github.com/s3ndd/sen-graphql-go/internal/config" 8 | "github.com/s3ndd/sen-graphql-go/router" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/s3ndd/sen-go/config" 12 | "github.com/s3ndd/sen-go/log" 13 | ) 14 | 15 | func main() { 16 | config.Load() 17 | logger := log.NewLogger(internalConfig.Logger()) 18 | logger.Info("Starting...") 19 | if env := config.String("ENV", "dev"); env == "prod" || env == "production" { 20 | gin.SetMode(gin.ReleaseMode) 21 | } 22 | 23 | routerInit := router.InitRouter() 24 | 25 | errs := make(chan error, 1) 26 | go func() { 27 | server := &http.Server{ 28 | Addr: ":" + config.String("PORT", "60000"), 29 | Handler: routerInit, 30 | ReadTimeout: time.Second * 5, 31 | WriteTimeout: time.Second * 60, 32 | } 33 | errs <- server.ListenAndServe() 34 | }() 35 | logger.WithError(<-errs).Info("terminated") 36 | } 37 | -------------------------------------------------------------------------------- /graph/schema/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | } 5 | 6 | """ 7 | Int64 is a signed 64‐bit integer. 8 | GraphQL Int is a signed 32‐bit integer. 9 | """ 10 | scalar Int64 11 | 12 | """ 13 | Timestamp is a date and time. 14 | It is serialized as a String in RFC3339 date and time format. 15 | """ 16 | scalar Timestamp 17 | 18 | """ 19 | Decimal is a number where precision is important. 20 | """ 21 | scalar Decimal 22 | 23 | type Query { 24 | """ 25 | version is just a sample field. Since it needs at least one field, put a version here. 26 | """ 27 | version: String! 28 | } 29 | 30 | type Mutation { 31 | """ 32 | setVersion is just a sample field. Since it needs at least one field, put a version here. 33 | """ 34 | setVersion(input: String!): String! 35 | } 36 | 37 | 38 | # Information for paginating this connection 39 | type PageInfo { 40 | startCursor: ID! 41 | endCursor: ID! 42 | hasNextPage: Boolean! 43 | } -------------------------------------------------------------------------------- /graph/resolver/schema.resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "github.com/s3ndd/sen-graphql-go/graph/generated" 10 | ) 11 | 12 | func (r *mutationResolver) SetVersion(ctx context.Context, input string) (string, error) { 13 | return input, nil 14 | } 15 | 16 | func (r *queryResolver) Version(ctx context.Context) (string, error) { 17 | panic(fmt.Errorf("not implemented")) 18 | } 19 | 20 | // Mutation returns generated.MutationResolver implementation. 21 | func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } 22 | 23 | // Query returns generated.QueryResolver implementation. 24 | func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } 25 | 26 | type mutationResolver struct{ *Resolver } 27 | type queryResolver struct{ *Resolver } 28 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/99designs/gqlgen/graphql/handler" 7 | "github.com/99designs/gqlgen/graphql/playground" 8 | "github.com/gin-gonic/gin" 9 | 10 | "github.com/s3ndd/sen-graphql-go/graph/generated" 11 | "github.com/s3ndd/sen-graphql-go/graph/resolver" 12 | ) 13 | 14 | // GraphqlHandler defines the Graphql handler 15 | func GraphqlHandler() gin.HandlerFunc { 16 | srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &resolver.Resolver{}})) 17 | 18 | return func(ctx *gin.Context) { 19 | 20 | srv.ServeHTTP(ctx.Writer, ctx.Request) 21 | } 22 | } 23 | 24 | // PlaygroundHandler defines the Playground handler 25 | func PlaygroundHandler() gin.HandlerFunc { 26 | return func(ctx *gin.Context) { 27 | playground.Handler("GraphQL playground", "/graphql/query").ServeHTTP(ctx.Writer, ctx.Request) 28 | } 29 | } 30 | 31 | // HealthCheck returns 200 and success 32 | func HealthCheck() gin.HandlerFunc { 33 | return func(ctx *gin.Context) { 34 | ctx.JSON(http.StatusOK, "success") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /graph/model/retailer.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // RetailerStatus indicates the status of a retailer 6 | type RetailerStatus string 7 | 8 | const ( 9 | // ActiveRetailerStatus indicates retailer is an active customer 10 | ActiveRetailerStatus RetailerStatus = "ACTIVE" 11 | // PrePilotRetailerStatus indicates retailer is a prepilot customer 12 | PrePilotRetailerStatus RetailerStatus = "PREPILOT" 13 | // PilotRetailerStatus indicates retailer is a pilot customer 14 | PilotRetailerStatus RetailerStatus = "PILOT" 15 | // DemoRetailerStatus indicates retailer is a demo customer 16 | DemoRetailerStatus RetailerStatus = "DEMO" 17 | // TrialRetailerStatus indicates retailer is a trial customer 18 | TrialRetailerStatus RetailerStatus = "TRIAL" 19 | ) 20 | 21 | // Retailer contains information of a retailer 22 | type Retailer struct { 23 | ID string `json:"id"` 24 | Name string `json:"name"` 25 | Status RetailerStatus `json:"status"` 26 | PdfURL string `json:"pdf_url,omitempty"` 27 | Created time.Time `json:"created"` 28 | Updated time.Time `json:"updated"` 29 | Deleted bool `json:"deleted"` 30 | } 31 | -------------------------------------------------------------------------------- /internal/middleware/context.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type ctxKeyType struct{} 11 | 12 | var ginCtxKey interface{} = ctxKeyType{} 13 | 14 | // GinContextToContextMiddleware adds gin context to the context.Context 15 | func GinContextToContextMiddleware() gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | ctx := context.WithValue(c.Request.Context(), ginCtxKey, c) 18 | c.Request = c.Request.WithContext(ctx) 19 | c.Next() 20 | } 21 | } 22 | 23 | // GinContextFromContext recovers the gin.Context from the context.Context struct 24 | func GinContextFromContext(ctx context.Context) (*gin.Context, error) { 25 | ginContext := ctx.Value(ginCtxKey) 26 | if ginContext == nil { 27 | err := fmt.Errorf("could not retrieve gin.Context") 28 | return nil, err 29 | } 30 | 31 | gc, ok := ginContext.(*gin.Context) 32 | if !ok { 33 | err := fmt.Errorf("gin.Context has wrong type") 34 | return nil, err 35 | } 36 | return gc, nil 37 | } 38 | 39 | func GetAuthorizationHeader(ctx context.Context) string { 40 | if gc, err := GinContextFromContext(ctx); err == nil { 41 | return gc.GetHeader("Authorization") 42 | } 43 | return "" 44 | 45 | } 46 | -------------------------------------------------------------------------------- /graph/resolver/retailer.resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "github.com/s3ndd/sen-graphql-go/graph/dataloader" 9 | "github.com/s3ndd/sen-graphql-go/graph/generated" 10 | "github.com/s3ndd/sen-graphql-go/graph/model" 11 | "github.com/s3ndd/sen-graphql-go/graph/rest" 12 | ) 13 | 14 | func (r *queryResolver) Retailer(ctx context.Context, id string) (*model.Retailer, error) { 15 | loaders := dataloader.ContextLoaders(ctx) 16 | return loaders.Retailers.Load(id) 17 | } 18 | 19 | func (r *queryResolver) Retailers(ctx context.Context) ([]*model.Retailer, error) { 20 | retailers, err := rest.GetRetailers(ctx) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return retailers, nil 25 | } 26 | 27 | func (r *retailerResolver) Sites(ctx context.Context, obj *model.Retailer) ([]*model.Site, error) { 28 | sites, err := rest.GetSitesByRetailerID(ctx, obj.ID) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return sites, nil 33 | } 34 | 35 | // Retailer returns generated.RetailerResolver implementation. 36 | func (r *Resolver) Retailer() generated.RetailerResolver { return &retailerResolver{r} } 37 | 38 | type retailerResolver struct{ *Resolver } 39 | -------------------------------------------------------------------------------- /graph/model/item.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type DiscountType string 4 | 5 | const ( 6 | BogoDiscountType DiscountType = "BOGO" 7 | PercentageDiscountType DiscountType = "PERCENTAGE" 8 | NoneDiscountType DiscountType = "NONE" 9 | DollarsOffDiscountType DiscountType = "DOLLARS_OFF" 10 | FixedPriceDiscountType DiscountType = "FIXED_PRICE" 11 | ) 12 | 13 | func (dt DiscountType) String() string { 14 | return string(dt) 15 | } 16 | 17 | type Item struct { 18 | Code string `json:"code"` 19 | Price float64 `json:"price"` 20 | Weight *float64 `json:"weight,omitempty"` 21 | Savings float64 `json:"savings"` 22 | Quantity int `json:"quantity"` 23 | ExtraInfo *string `json:"extra_info,omitempty"` 24 | Restricted bool `json:"restricted"` 25 | Description string `json:"description"` 26 | ProductKey string `json:"product_key"` 27 | TotalPrice float64 `json:"total_price"` 28 | PriceWeight *float64 `json:"price_weight,omitempty"` 29 | TaxRate *float64 `json:"tax_rate,omitempty"` 30 | DiscountType DiscountType `json:"discount_type"` 31 | Discount *string `json:"discount,omitempty"` 32 | InternalKey string `json:"internal_key"` 33 | BogoID *int `json:"bogo_id,omitempty"` 34 | Category *string `json:"category,omitempty"` 35 | } 36 | -------------------------------------------------------------------------------- /graph/resolver/alert.resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "github.com/s3ndd/sen-graphql-go/graph/dataloader" 9 | "github.com/s3ndd/sen-graphql-go/graph/generated" 10 | "github.com/s3ndd/sen-graphql-go/graph/model" 11 | "github.com/s3ndd/sen-graphql-go/graph/rest" 12 | ) 13 | 14 | func (r *alertResolver) Session(ctx context.Context, obj *model.Alert) (*model.Session, error) { 15 | if obj.SessionID == nil { 16 | return nil, nil 17 | } 18 | loaders := dataloader.ContextLoaders(ctx) 19 | return loaders.Sessions.Load(*obj.SessionID) 20 | } 21 | 22 | func (r *queryResolver) Alert(ctx context.Context, id string) (*model.Alert, error) { 23 | loaders := dataloader.ContextLoaders(ctx) 24 | return loaders.Alerts.Load(id) 25 | } 26 | 27 | func (r *queryResolver) Alerts(ctx context.Context, siteID *string, sessionID *string, cartID *string, status []model.AlertStatus, types []model.AlertType) (*model.AlertConnection, error) { 28 | alerts, err := rest.GetAlerts(ctx, siteID, sessionID, cartID, status, types) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return alerts, nil 33 | } 34 | 35 | // Alert returns generated.AlertResolver implementation. 36 | func (r *Resolver) Alert() generated.AlertResolver { return &alertResolver{r} } 37 | 38 | type alertResolver struct{ *Resolver } 39 | -------------------------------------------------------------------------------- /graph/schema/retailer.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | """ 3 | retailer returns the retailer with the given id. It will return null if the retailer can cannot be found. 4 | """ 5 | retailer(id: ID!): Retailer 6 | 7 | """ 8 | retailers returns the list of retailers. 9 | """ 10 | retailers: [Retailer!]! 11 | 12 | } 13 | type Retailer { 14 | """ 15 | id is the unique identifier for the retailer. 16 | """ 17 | id: ID! 18 | """ 19 | name is the retailer's name. 20 | """ 21 | name: String! 22 | """ 23 | status is the status of the retailer. 24 | """ 25 | status: RetailerStatus! 26 | """ 27 | pdfURL is the url of the terms. 28 | """ 29 | pdfURL: String! 30 | """ 31 | sites is the list of sites belong to the retailer 32 | """ 33 | sites: [Site!]! 34 | """ 35 | created is when the retailer was created. 36 | """ 37 | created: Timestamp! 38 | """ 39 | updated is when the retailer was last updated. 40 | """ 41 | updated: Timestamp! 42 | } 43 | 44 | enum RetailerStatus { 45 | """ 46 | ActiveRetailerStatus indicates retailer is an active customer 47 | """ 48 | ACTIVE 49 | """ 50 | PREPILOT indicates retailer is a prepilot customer 51 | """ 52 | PREPILOT 53 | """ 54 | PILOT indicates retailer is a pilot customer 55 | """ 56 | PILOT 57 | """ 58 | DEMO indicates retailer is a demo customer 59 | """ 60 | DEMO 61 | """ 62 | TRIAL indicates retailer is a trial customer 63 | """ 64 | TRIAL 65 | } -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/s3ndd/sen-go/client" 7 | "github.com/s3ndd/sen-go/config" 8 | "github.com/s3ndd/sen-go/log" 9 | ) 10 | 11 | // Logger builds the log config from the config file and envs. 12 | func Logger() *log.Config { 13 | return &log.Config{ 14 | LogLevel: config.String("LOGGER_LEVEL", string(log.LevelInfo)), 15 | LogFormat: config.String("LOGGER_FORMAT", "json"), 16 | Program: config.String("SOURCE_PROGRAM", "sen-monitoring"), 17 | Env: config.String("ENV", "dev"), 18 | SentryLogLevel: config.String("SENTRY_LOGGER_LEVEL", string(log.LevelError)), 19 | SentryDsn: config.String("SENTRY_DSN", ""), 20 | SentryDebug: config.Bool("SENTRY_DEBUG", true), 21 | SentryAttachStacktrace: config.Bool("SENTRY_ATTACH_STACK_TRACE", true), 22 | SentryFlushTimeout: config.Duration("SENTRY_FLUSH_TIMEOUT", 3*time.Second), 23 | } 24 | } 25 | 26 | // HTTPClient returns a http client config for integration services 27 | func HTTPClient() *client.Config { 28 | return &client.Config{ 29 | EndpointURL: config.String("API_GATEWAY_BASEURL", "localhost:8080"), 30 | UseSecure: config.Bool("HTTP_CLIENT_USE_SECURE", true), 31 | IgnoreCertificateErrors: true, 32 | Timeout: config.Duration("HTTP_CLIENT_TIMEOUT", 5*time.Second), 33 | } 34 | } 35 | 36 | // ApiKey retrieves and returns the value of the api key. 37 | func ApiKey(key string) string { 38 | return config.String(key, "") 39 | } 40 | -------------------------------------------------------------------------------- /graph/model/site.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // SiteStatus indicates the status of a site 6 | type SiteStatus string 7 | 8 | const ( 9 | // ActiveSiteStatus indicates the site is active 10 | ActiveSiteStatus SiteStatus = "ACTIVE" 11 | ) 12 | 13 | // SiteRegion indicates the region a site is located in 14 | type SiteRegion string 15 | 16 | const ( 17 | // AsiaSiteRegion indicates the site is located in Asia 18 | AsiaSiteRegion SiteRegion = "ASIA" 19 | // AustraliaSiteRegion indicates the site is located in Australia 20 | AustraliaSiteRegion SiteRegion = "AUSTRALIA" 21 | // EuropeSiteRegion indicates the site is located in Europe 22 | EuropeSiteRegion SiteRegion = "EUROPE" 23 | // USSiteRegion indicates the site is located in US 24 | USSiteRegion SiteRegion = "US" 25 | ) 26 | 27 | // Site contains information of a site 28 | type Site struct { 29 | ID string `json:"id"` 30 | Name string `json:"name"` 31 | Status SiteStatus `json:"status"` 32 | Region SiteRegion `json:"region"` 33 | Currency string `json:"currency"` 34 | WorkflowType string `json:"workflow_type"` 35 | IntegrationType IntegrationType `json:"integration_type"` 36 | SecretKey *string `json:"secret_key,omitempty"` 37 | AlertNotificationType string `json:"alert_notification_type"` 38 | AlertNotificationURL *string `json:"alert_notification_url,omitempty"` 39 | RetailerID string `json:"retailer_id"` 40 | Retailer *Retailer `json:"retailer"` 41 | LogoURL string `json:"logo_url,omitempty"` 42 | Created time.Time `json:"created"` 43 | Updated time.Time `json:"updated"` 44 | Deleted bool `json:"deleted"` 45 | } 46 | -------------------------------------------------------------------------------- /graph/schema/item.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | items(sessionID: ID!): [Item]! 3 | } 4 | 5 | type Item { 6 | """ 7 | code is the unique identifier for the item. 8 | """ 9 | code: String! 10 | """ 11 | price is the price of the item. 12 | """ 13 | price: Float! 14 | """ 15 | weight is the weight of the item if the item is sold by weight. 16 | """ 17 | weight: Float 18 | """ 19 | savings is the savings of the item. 20 | """ 21 | savings: Float! 22 | """ 23 | quantity is the quantity of the item. 24 | """ 25 | quantity:Int! 26 | """ 27 | extraInfo is the extra information of the item. 28 | """ 29 | extraInfo:String 30 | """ 31 | restricted indicates whether the item is restricted or not. 32 | """ 33 | restricted: Boolean! 34 | """ 35 | description is the name of the item. 36 | """ 37 | description:String! 38 | """ 39 | productKey is the unique identifier for the item. 40 | """ 41 | productKey: String! 42 | """ 43 | totalPrice is the total price of the item. 44 | """ 45 | totalPrice: Float! 46 | """ 47 | priceWeight is the price of item by weight+. 48 | """ 49 | priceWeight: Float 50 | """ 51 | taxRate is the tax rate of the item. 52 | """ 53 | taxRate: Float 54 | """ 55 | discountType is the discount applied to the item. If there is no discount, it will be NONE. 56 | """ 57 | discountType: String! 58 | """ 59 | discount is the discount code applied to the item. 60 | """ 61 | discount: String 62 | """ 63 | internalKey is the internal unique identifier for the item. 64 | """ 65 | internalKey: String! 66 | """ 67 | bogoID is the id of bogo. 68 | """ 69 | bogoID:Int 70 | """ 71 | category denotes the item is a special item. 72 | """ 73 | category: String 74 | } -------------------------------------------------------------------------------- /graph/resolver/site.resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "github.com/s3ndd/sen-graphql-go/graph/dataloader" 10 | "github.com/s3ndd/sen-graphql-go/graph/generated" 11 | "github.com/s3ndd/sen-graphql-go/graph/model" 12 | "github.com/s3ndd/sen-graphql-go/graph/rest" 13 | ) 14 | 15 | func (r *queryResolver) Site(ctx context.Context, retailerID string, id string) (*model.Site, error) { 16 | loaders := dataloader.ContextLoaders(ctx) 17 | site, err := loaders.Sites.Load(id) 18 | if err != nil { 19 | return nil, err 20 | } 21 | if site != nil && site.RetailerID != retailerID { 22 | return nil, errors.New("site not found") 23 | } 24 | return site, nil 25 | } 26 | 27 | func (r *queryResolver) Sites(ctx context.Context, retailerID string) ([]*model.Site, error) { 28 | sites, err := rest.GetSitesByRetailerID(ctx, retailerID) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return sites, nil 33 | } 34 | 35 | func (r *siteResolver) IntegrationType(ctx context.Context, obj *model.Site) (string, error) { 36 | return obj.IntegrationType.String(), nil 37 | } 38 | 39 | func (r *siteResolver) Sessions(ctx context.Context, obj *model.Site, status []model.SessionStatus) (*model.SessionConnection, error) { 40 | sessions, err := rest.GetSessionsBySiteID(ctx, obj.ID, status) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return sessions, nil 45 | } 46 | 47 | func (r *siteResolver) Alerts(ctx context.Context, obj *model.Site, status []model.AlertStatus, types []model.AlertType) (*model.AlertConnection, error) { 48 | alerts, err := rest.GetAlerts(ctx, &obj.ID, nil, nil, status, types) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return alerts, nil 53 | } 54 | 55 | // Site returns generated.SiteResolver implementation. 56 | func (r *Resolver) Site() generated.SiteResolver { return &siteResolver{r} } 57 | 58 | type siteResolver struct{ *Resolver } 59 | -------------------------------------------------------------------------------- /graph/model/alert.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // AlertStatus is the type of alert status 6 | type AlertStatus string 7 | 8 | const ( 9 | // OpenAlertStatus is the initial status that means the alert has to be acknowledged, resolved or self solved. 10 | OpenAlertStatus AlertStatus = "OPEN" 11 | // AcknowledgedAlertStatus is the status that means the alert has been claimed by staff. 12 | AcknowledgedAlertStatus AlertStatus = "ACKNOWLEDGED" 13 | // ResolvedAlertStatus is the final status that means the alert has been resolved. 14 | ResolvedAlertStatus AlertStatus = "RESOLVED" 15 | // SelfSolvedAlertStatus is the final status that means the alert has been solved by the user. 16 | SelfSolvedAlertStatus AlertStatus = "SELF_SOLVED" 17 | // NaAlertStatus is not an actual status in the database, which is only used in the code. 18 | NaAlertStatus AlertStatus = "NA" 19 | ) 20 | 21 | // AlertType is the type of alert 22 | type AlertType string 23 | 24 | const ( 25 | HelpAlertType AlertType = "HELP" 26 | LatchAlertType AlertType = "LATCH" 27 | POSErrorAlertType AlertType = "POS_ERROR" 28 | MultipleItemsAlertType AlertType = "MULTIPLE_ITEMS" 29 | MissedLabelAlertType AlertType = "MISSED_LABEL" 30 | ) 31 | 32 | // Alert defines the alert body. 33 | type Alert struct { 34 | ID string `json:"id"` 35 | SiteID string `json:"site_id"` 36 | CartID string `json:"cart_id"` 37 | CartQRCode string `json:"cart_qr_code"` 38 | SessionID *string `json:"session_id"` 39 | Status AlertStatus `json:"status"` 40 | Type AlertType `json:"type"` 41 | Responder *string `json:"responder"` 42 | Message *string `json:"message"` 43 | TriggeredAt time.Time `json:"triggered_at"` 44 | AcknowledgedAt *time.Time `json:"acknowledged_at"` 45 | ResolvedAt *time.Time `json:"resolved_at"` 46 | Created time.Time `json:"created"` 47 | Updated time.Time `json:"updated"` 48 | Deleted bool `json:"deleted"` 49 | } 50 | 51 | type AlertConnection struct { 52 | Alerts []*Alert `json:"alerts"` 53 | } 54 | -------------------------------------------------------------------------------- /graph/resolver/cart.resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "github.com/s3ndd/sen-graphql-go/graph/dataloader" 9 | "github.com/s3ndd/sen-graphql-go/graph/generated" 10 | "github.com/s3ndd/sen-graphql-go/graph/model" 11 | "github.com/s3ndd/sen-graphql-go/graph/resolver/helper" 12 | "github.com/s3ndd/sen-graphql-go/graph/rest" 13 | ) 14 | 15 | func (r *cartResolver) Site(ctx context.Context, obj *model.Cart) (*model.Site, error) { 16 | loaders := dataloader.ContextLoaders(ctx) 17 | cart, err := loaders.CartRegistrations.Load(obj.ID) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return cart.Site, nil 23 | } 24 | 25 | func (r *cartResolver) Session(ctx context.Context, obj *model.Cart) (*model.Session, error) { 26 | if obj.SessionID == nil { 27 | return nil, nil 28 | } 29 | 30 | return helper.LoadSessionByID(ctx, *obj.SessionID) 31 | } 32 | 33 | func (r *cartResolver) Sessions(ctx context.Context, obj *model.Cart, status []model.SessionStatus) ([]*model.Session, error) { 34 | sessions, err := rest.GetSessionsBySiteID(ctx, obj.SiteID, status) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return sessions.Sessions, nil 39 | } 40 | 41 | func (r *cartResolver) Alerts(ctx context.Context, obj *model.Cart, status []model.AlertStatus, types []model.AlertType) ([]*model.Alert, error) { 42 | alerts, err := rest.GetAlerts(ctx, &obj.SiteID, obj.SessionID, &obj.ID, status, types) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return alerts.Alerts, nil 48 | } 49 | 50 | func (r *queryResolver) Cart(ctx context.Context, id string) (*model.Cart, error) { 51 | loaders := dataloader.ContextLoaders(ctx) 52 | return loaders.Carts.Load(id) 53 | } 54 | 55 | func (r *queryResolver) Carts(ctx context.Context, siteID string) (*model.CartConnection, error) { 56 | cartConnection, err := rest.GetCartsBySiteID(ctx, siteID) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return cartConnection, nil 62 | } 63 | 64 | // Cart returns generated.CartResolver implementation. 65 | func (r *Resolver) Cart() generated.CartResolver { return &cartResolver{r} } 66 | 67 | type cartResolver struct{ *Resolver } 68 | -------------------------------------------------------------------------------- /graph/rest/site.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/s3ndd/sen-go/log" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | func GetSitesByRetailerID(ctx context.Context, retailerID string) ([]*model.Site, error) { 13 | var sites struct { 14 | Sites []*model.Site `json:"sites"` 15 | } 16 | resp, err := HttpClient().Get(ctx, 17 | Uri(RegistryServicePrefix, "v1", fmt.Sprintf("retailers/%s/sites", retailerID)), 18 | GenerateHeaders(), &sites) 19 | if err != nil { 20 | log.ForRequest(ctx).WithError(err).WithField("retailer_id", retailerID). 21 | Error("failed to get the sites with the given retailer id") 22 | return nil, err 23 | } 24 | 25 | if err := CheckStatus(resp.StatusCode()); err != nil { 26 | log.ForRequest(ctx).WithFields(log.LogFields{"response": sites, "status_code": resp.StatusCode()}).WithError(err). 27 | Error("failed to get the sites from registry service") 28 | return nil, err 29 | } 30 | return sites.Sites, nil 31 | } 32 | 33 | func GetSiteByID(ctx context.Context, siteID, retailerID string) (*model.Site, error) { 34 | var site model.Site 35 | resp, err := HttpClient().Get(ctx, 36 | Uri(RegistryServicePrefix, "v1", fmt.Sprintf("retailers/%s/sites/%s?hide_sensitive=false", retailerID, siteID)), 37 | GenerateHeaders(), &site) 38 | if err != nil { 39 | log.ForRequest(ctx).WithError(err).WithField("retailer_id", retailerID). 40 | Error("failed to get the sites with the given retailer id") 41 | return nil, err 42 | } 43 | 44 | if err := CheckStatus(resp.StatusCode()); err != nil { 45 | log.ForRequest(ctx).WithFields(log.LogFields{"response": site, "status_code": resp.StatusCode()}).WithError(err). 46 | Error("failed to get the sites from registry service") 47 | return nil, err 48 | } 49 | return &site, nil 50 | } 51 | 52 | func GetSiteByIDs(ctx context.Context, siteIDs []string) ([]*model.Site, error) { 53 | path := Uri(RegistryServicePrefix, "v1", "sites/bulk") 54 | var response map[string]*model.Site 55 | if err := batchQuery(ctx, path, siteIDs, &response); err != nil { 56 | log.ForRequest(ctx).WithError(err). 57 | Error("failed to get the sites by ids from registry service") 58 | return nil, err 59 | } 60 | sites := make([]*model.Site, len(siteIDs)) 61 | for i := range siteIDs { 62 | if site, ok := response[siteIDs[i]]; ok { 63 | sites[i] = site 64 | } 65 | } 66 | 67 | return sites, nil 68 | } 69 | -------------------------------------------------------------------------------- /graph/rest/retailer.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/s3ndd/sen-go/log" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | func GetRetailerByID(ctx context.Context, retailerID string) (*model.Retailer, error) { 13 | var retailer model.Retailer 14 | resp, err := HttpClient().Get(ctx, 15 | Uri(RegistryServicePrefix, "v1", fmt.Sprintf("retailers/%s?include_sites=true", retailerID)), 16 | GenerateHeaders(), &retailer) 17 | if err != nil { 18 | log.ForRequest(ctx).WithError(err).WithField("retailer_id", retailerID). 19 | Error("failed to get the retailer with the given id") 20 | return nil, err 21 | } 22 | 23 | if err := CheckStatus(resp.StatusCode()); err != nil { 24 | log.ForRequest(ctx).WithFields(log.LogFields{"response": retailer, "status_code": resp.StatusCode()}).WithError(err). 25 | Error("failed to get the retailer from registry service") 26 | return nil, err 27 | } 28 | return &retailer, nil 29 | } 30 | 31 | func GetRetailerByIDs(ctx context.Context, retailerIDs []string) ([]*model.Retailer, error) { 32 | log.Global().WithField("retailer_ids", retailerIDs).Info("get retailers") 33 | path := Uri(RegistryServicePrefix, "v1", "retailers/bulk") 34 | var response map[string]*model.Retailer 35 | if err := batchQuery(ctx, path, retailerIDs, &response); err != nil { 36 | log.ForRequest(ctx).WithError(err). 37 | Error("failed to get the retailers by ids from registry service") 38 | return nil, err 39 | } 40 | retailers := make([]*model.Retailer, len(response)) 41 | for i := range retailerIDs { 42 | if retailer, ok := response[retailerIDs[i]]; ok { 43 | retailers[i] = retailer 44 | } 45 | } 46 | return retailers, nil 47 | } 48 | 49 | func GetRetailers(ctx context.Context) ([]*model.Retailer, error) { 50 | var retailers struct { 51 | Retailers []*model.Retailer `json:"retailers"` 52 | } 53 | resp, err := HttpClient().Get(ctx, 54 | Uri(RegistryServicePrefix, "v1", "retailers"), 55 | GenerateHeaders(), &retailers) 56 | if err != nil { 57 | log.ForRequest(ctx).WithError(err).Error("failed to get the retailer with the given id") 58 | return nil, err 59 | } 60 | 61 | if err := CheckStatus(resp.StatusCode()); err != nil { 62 | log.ForRequest(ctx).WithFields(log.LogFields{"response": retailers, "status_code": resp.StatusCode()}).WithError(err). 63 | Error("failed to get the retailer from registry service") 64 | return nil, err 65 | } 66 | return retailers.Retailers, nil 67 | } 68 | -------------------------------------------------------------------------------- /graph/dataloader/dataloaders.go: -------------------------------------------------------------------------------- 1 | package dataloader 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | "github.com/s3ndd/sen-graphql-go/internal/middleware" 10 | ) 11 | 12 | //go:generate go run github.com/vektah/dataloaden RetailerLoader string *github.com/s3ndd/sen-graphql-go/graph/model.Retailer 13 | //go:generate go run github.com/vektah/dataloaden SiteLoader string *github.com/s3ndd/sen-graphql-go/graph/model.Site 14 | //go:generate go run github.com/vektah/dataloaden CartLoader string *github.com/s3ndd/sen-graphql-go/graph/model.Cart 15 | //go:generate go run github.com/vektah/dataloaden CartRegistrationLoader string *github.com/s3ndd/sen-graphql-go/graph/model.CartRegistration 16 | //go:generate go run github.com/vektah/dataloaden AlertLoader string *github.com/s3ndd/sen-graphql-go/graph/model.Alert 17 | //go:generate go run github.com/vektah/dataloaden SessionLoader string *github.com/s3ndd/sen-graphql-go/graph/model.Session 18 | //go:generate go run github.com/vektah/dataloaden UnifiedSessionLoader string *github.com/s3ndd/sen-graphql-go/graph/model.Session 19 | 20 | const ( 21 | maxBatch = 1000 22 | waitTime = 1 * time.Millisecond 23 | ) 24 | 25 | const ctxKey = "data_loaders" 26 | 27 | type Loaders struct { 28 | Retailers *RetailerLoader 29 | Sites *SiteLoader 30 | Carts *CartLoader 31 | CartRegistrations *CartRegistrationLoader 32 | Alerts *AlertLoader 33 | Sessions *SessionLoader 34 | UnifiedSessions *UnifiedSessionLoader 35 | } 36 | 37 | func LoaderMiddleware() gin.HandlerFunc { 38 | return func(ctx *gin.Context) { 39 | var loaders Loaders 40 | 41 | loaders.Retailers = newRetailerLoader(ctx) 42 | loaders.Sites = newSiteLoader(ctx) 43 | loaders.Carts = newCartLoader(ctx) 44 | loaders.CartRegistrations = newCartRegistrationLoader(ctx) 45 | loaders.Alerts = newAlertLoader(ctx) 46 | loaders.Sessions = newSessionLoader(ctx) 47 | loaders.UnifiedSessions = newUnifiedSessionLoader(ctx) 48 | ctx.Set(ctxKey, &loaders) 49 | ctx.Next() 50 | } 51 | } 52 | 53 | func ContextLoaders(ctx context.Context) *Loaders { 54 | // both context switch and loader will be used as a middleware, so it must be in the context 55 | // ignore any error here. 56 | gc, _ := middleware.GinContextFromContext(ctx) 57 | value, _ := gc.Get(ctxKey) 58 | return value.(*Loaders) 59 | } 60 | 61 | func errors(err error) []error { 62 | if err != nil { 63 | return []error{err} 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /graph/schema/site.graphql: -------------------------------------------------------------------------------- 1 | 2 | extend type Query { 3 | site(retailerID: ID!, id: ID!): Site 4 | sites(retailerID: ID!): [Site!]! 5 | } 6 | 7 | type Site { 8 | """ 9 | id is the unique identifier for the site. 10 | """ 11 | id: ID! 12 | """ 13 | name is the site's name. 14 | """ 15 | name: String! 16 | """ 17 | status is the status of the site. 18 | """ 19 | status: SiteStatus! 20 | """ 21 | retailerID is the UUID for the retailer. 22 | """ 23 | retailerID: String! 24 | """ 25 | retailer is the detail of the retailer. 26 | """ 27 | retailer: Retailer 28 | """ 29 | logoURL is the url of the site's logo 30 | """ 31 | logoURL: String 32 | """ 33 | region is the location where the site is. 34 | """ 35 | region: SiteRegion! 36 | """ 37 | currency is the currency supported by the site. 38 | """ 39 | currency: String! 40 | """ 41 | workflowType is the workflow the site is using. 42 | """ 43 | workflowType: String! 44 | """ 45 | integrationType indicates how to integrate with the pos. 46 | """ 47 | integrationType : String! 48 | """ 49 | alertNotificationType is the type used to notify the alert. 50 | """ 51 | alertNotificationType: String! 52 | """ 53 | alertNotificationURL is the alert notification callback url. 54 | """ 55 | alertNotificationURL:String 56 | """ 57 | created is when the site was created. 58 | """ 59 | created: Timestamp! 60 | """ 61 | updated is when the site was last updated. 62 | """ 63 | updated: Timestamp! 64 | 65 | """ 66 | sessions is a list of sessions of this site 67 | """ 68 | sessions(status: [SessionStatus!]): SessionConnection! 69 | """ 70 | alerts is a list of alerts of this site 71 | """ 72 | alerts(status: [AlertStatus!], types: [AlertType!]): AlertConnection! 73 | } 74 | 75 | enum SiteStatus { 76 | # ActiveSiteStatus indicates the site is active 77 | ACTIVE 78 | } 79 | 80 | enum SiteRegion { 81 | """ 82 | ASIA indicates the site is located in Asia 83 | """ 84 | ASIA 85 | """ 86 | AUSTRALIA indicates the site is located in Australia 87 | """ 88 | AUSTRALIA 89 | """ 90 | EUROPE indicates the site is located in Europe 91 | """ 92 | EUROPE 93 | """ 94 | US indicates the site is located in US 95 | """ 96 | US 97 | } 98 | 99 | enum IntegrationType{ 100 | POS 101 | POS_LESS 102 | PRODUCT_ONLY 103 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README # 2 | 3 | github.com/s3ndd/sen-graphql-go service is a service that handles GraphQL queries and mutations. 4 | 5 | ## Dev Config 6 | Development configuration values are taken from etc/.env and environment variables. 7 | 8 | | Key | Type | Description | Default | 9 | | ------------- |---------------|-------------------------------------------------------------|------------------| 10 | | `ENV` | string | Is it running in Development or Production. | dev | 11 | | `SOURCE_PROGRAM` | string | The service name | github.com/s3ndd/sen-graphql-go | 12 | | `PORT` | int | The HTTP port that the routing service API should serve on. | 60000 | 13 | | `LOG_FORMAT` | string | Log format. | json | 14 | | `LOG_LEVEL` | string | Log level. | debug | 15 | | `API_GATEWAY_BASEURL` | string | The URL of the API Gateway. | localhost:36000 | 16 | | `HTTP_CLIENT_USE_SECURE` | bool | Indicate use https or http. | false | 17 | | `HTTP_CLIENT_TIMEOUT` | time.Duration | The http request timeout. | 5s | 18 | | `SERVICE_API_KEY` | string | The api_key used to send request other services. | | 19 | 20 | ### Running in LocalDev 21 | 1. Clone the `github.com/s3ndd/sen-graphql-go` repository to your workspace 22 | 2. Navigate to the github.com/s3ndd/sen-graphql-go directory 23 | 3. Run `go run main.go` or `make run` 24 | 25 | ### Running Test 26 | 1. Run command `make test`. It will run `./ci/test.sh`. 27 | 28 | 29 | ### Structure 30 | All the Graphql related code is under the `graph` directory. 31 | - `downloader`: Downloaders used to query by batching and caching. The files with `_gen.go` suffix are generated by [dataloaden](https://github.com/vektah/dataloaden) 32 | - `generated`: Code under this directory is generated by [gqlgen](https://github.com/99designs/gqlgen). Do not change manually. 33 | - `model`: These are used to serve GraphQL responses. `models_gen.go` is generated by `gqlgen` 34 | - `resolver`: The GraphQL queries are resolved under this directory. 35 | - `rest`: Helper functions to query from the REST APIs. 36 | - `schema`: The GraphQL schema files. 37 | 38 | ### Development 39 | 1. Add or update a schema 40 | 2. Add related model if required 41 | 3. Regenerate the schema 42 | - `$ go generate ./...` 43 | 4. Add the related `rest` helper 44 | 5. Update the resolver 45 | 46 | ### Playground 47 | `http://localhost:60000/graphql/graphiql` is the Graphql playground. 48 | 49 | -------------------------------------------------------------------------------- /graph/schema/alert.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | alert(id: ID!): Alert 3 | alerts(siteID:ID, sessionID: ID, cartID: ID, status:[AlertStatus!], types: [AlertType!]): AlertConnection 4 | } 5 | 6 | type Alert { 7 | """ 8 | id is the unique identifier for the alert. 9 | """ 10 | id: ID! 11 | """ 12 | siteID is the unique identifier for the site where the alert belongs to. 13 | """ 14 | siteID: ID! 15 | """ 16 | cartID is the unique identifier for the cart where the alert belongs to. 17 | """ 18 | cartID: ID! 19 | """ 20 | cartQRCode is the unique identifier for the cart where the alert belongs to. 21 | """ 22 | cartQRCode: ID! 23 | """ 24 | sessionID is the unique identifier for the session where the alert belongs to. 25 | """ 26 | sessionID: ID 27 | """ 28 | session is the detail of the session which is associated with the cart. 29 | """ 30 | session: Session 31 | """ 32 | status is the status of the alert. 33 | """ 34 | status: AlertStatus! 35 | """ 36 | type is the type of the cart. 37 | """ 38 | type: AlertType! 39 | """ 40 | responder is unique identifier for the user who claimed the alert. 41 | """ 42 | responder: String 43 | """ 44 | message is the more information of the alert. 45 | """ 46 | message: String 47 | """ 48 | triggeredAt is the time when the alert is triggered. 49 | """ 50 | triggeredAt: Timestamp 51 | """ 52 | acknowledgedAt is the time when the alert is claimed. 53 | """ 54 | acknowledgedAt: Timestamp 55 | """ 56 | resolvedAt is the time when the alert is resolved. 57 | """ 58 | resolvedAt: Timestamp 59 | """ 60 | created is when the session was created. 61 | """ 62 | created: Timestamp! 63 | """ 64 | updated is when the session was last updated. 65 | """ 66 | updated: Timestamp! 67 | """ 68 | deleted indicates whether the cart has been removed or not. 69 | """ 70 | deleted: Boolean! 71 | } 72 | 73 | type AlertConnection { 74 | alerts: [Alert!]! 75 | } 76 | 77 | enum AlertType { 78 | HELP 79 | LATCH 80 | POS_ERROR 81 | MULTIPLE_ITEMS 82 | MISSED_LABEL 83 | } 84 | 85 | enum AlertStatus { 86 | """ 87 | OPEN is the initial status that means the alert has to be acknowledged, resolved or self solved. 88 | """ 89 | OPEN 90 | """ 91 | ACKNOWLEDGED is the status that means the alert has been claimed by staff. 92 | """ 93 | ACKNOWLEDGED 94 | """ 95 | RESOLVED is the final status that means the alert has been resolved. 96 | """ 97 | RESOLVED 98 | """ 99 | SELF_SOLVED is the final status that means the alert has been solved by the user. 100 | """ 101 | SELF_SOLVED 102 | """ 103 | NA is not an actual status in the database, which is only used in the code. 104 | """ 105 | NA 106 | } 107 | -------------------------------------------------------------------------------- /graph/schema/session.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | session(id: ID!, siteID: ID!, retailerID:ID!): Session 3 | sessions(siteID: ID!, retailerID:ID!, status: [SessionStatus!]): SessionConnection! 4 | } 5 | 6 | extend type Mutation { 7 | updateSessionStatus(input: UpdateSessionStatusRequest): Session! 8 | } 9 | 10 | type Session { 11 | """ 12 | id is the unique identifier for the session. 13 | """ 14 | id: ID! 15 | """ 16 | cartID is the unique identifier for the cart associated with the session. 17 | """ 18 | cartID: ID! 19 | """ 20 | userID is the unique identifier for the user who created the session. 21 | """ 22 | userID: ID 23 | """ 24 | repeatUser indicates the session was created by a repeat user or a new user. 25 | """ 26 | repeatUser: Boolean 27 | """ 28 | retailerID is the unique identifier for the retailer. 29 | """ 30 | retailerID: ID 31 | """ 32 | siteID is the unique identifier for the site. 33 | """ 34 | siteID: ID! 35 | """ 36 | site is the detail of the site where the session belongs to. 37 | """ 38 | site: Site! 39 | """ 40 | status is the status of the session. 41 | """ 42 | status: SessionStatus! 43 | """ 44 | externalToken is the token from the third party. 45 | """ 46 | externalToken: String 47 | """ 48 | dealBarcode is the unique identifier generated to transfer the session to pos. 49 | """ 50 | dealBarcode: String 51 | """ 52 | total is the total price of the session. 53 | """ 54 | total: Float! 55 | """ 56 | totalSavings is the total saving of the session. 57 | """ 58 | totalSavings: Float! 59 | """ 60 | totalTax is the total tax information of the session. 61 | """ 62 | totalTax: [Tax!]! 63 | """ 64 | items is the final item list. 65 | """ 66 | items: [Item!]! 67 | """ 68 | itemsPrePos is the iteme pre handed over to the pos . 69 | """ 70 | itemsPrePos: [Item!]! 71 | """ 72 | alerts is the alerts triggered during the session. 73 | """ 74 | alerts(status:[AlertStatus!], types: [AlertType!]): [Alert]! 75 | """ 76 | events is the event list process for the session. 77 | """ 78 | events(eventType: EventType, eventSubTypes: [EventSubType!]): [Event!]! 79 | """ 80 | created is when the session was created. 81 | """ 82 | created: Timestamp! 83 | """ 84 | updated is when the session was last updated. 85 | """ 86 | updated: Timestamp! 87 | } 88 | 89 | type Tax { 90 | rate: Float! 91 | amount: Float! 92 | } 93 | 94 | type SessionConnection { 95 | sessions: [Session!]! 96 | } 97 | 98 | input UpdateSessionStatusRequest { 99 | sessionID: ID! 100 | retailerID: ID! 101 | siteID: ID! 102 | status: SessionStatus! 103 | } 104 | 105 | enum SessionStatus { 106 | UNSPECIFIED 107 | SHOPPING 108 | PAUSED 109 | HELD 110 | PRECHECKOUT 111 | CHECKOUT 112 | PAID 113 | FINISHED 114 | CANCELLED 115 | } -------------------------------------------------------------------------------- /graph/model/event.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | type EventResponse struct { 6 | ID string `json:"id"` 7 | SessionID string `json:"session_id"` 8 | EventType EventType `json:"event_type"` 9 | Flagged bool `json:"flagged"` 10 | Skipped bool `json:"skipped"` 11 | Message EventMessage `json:"message"` 12 | Timestamp *time.Time `json:"timestamp,omitempty"` 13 | Created time.Time `json:"created"` 14 | Updated time.Time `json:"updated"` 15 | } 16 | 17 | type Event struct { 18 | ID string `json:"id"` 19 | SessionID string `json:"sessionID"` 20 | EventType EventType `json:"eventType"` 21 | EventSubType EventSubType `json:"eventSubType"` 22 | ProductKeyList []ProductKeyList `json:"productKeyList"` 23 | Flagged bool `json:"flagged"` 24 | Skipped bool `json:"skipped"` 25 | Created time.Time `json:"created"` 26 | Updated time.Time `json:"updated"` 27 | } 28 | 29 | type EventMessage struct { 30 | EventSubType EventSubType `json:"event_sub_type"` 31 | WebhookNotify bool `json:"webhook_notify"` 32 | ProductKeyList []ProductKeyList `json:"product_key_list,omitempty"` 33 | // Items filed is used to send request to the retail service 34 | Items []ProductKeyList `json:"items,omitempty"` 35 | } 36 | 37 | type ActionType string 38 | 39 | const ( 40 | AddActionType ActionType = "ADD" 41 | RemoveActionType ActionType = "REMOVE" 42 | ReplaceActionType ActionType = "REPLACE" 43 | ) 44 | 45 | func (a ActionType) String() string { 46 | return string(a) 47 | } 48 | 49 | func (a ActionType) ToEventSubType() EventSubType { 50 | return EventSubType(a) 51 | } 52 | 53 | type ProductKeyList struct { 54 | ProductKey string `json:"product_key" binding:"required"` 55 | Labelled bool `json:"labelled"` 56 | CandidateClasses []string `json:"candidate_classes,omitempty"` 57 | BarcodeReader *string `json:"barcode_reader_product_key,omitempty"` 58 | Discount *string `json:"discount,omitempty"` 59 | Quantity *int `json:"quantity"` 60 | } 61 | 62 | type ItemsRequest struct { 63 | ID string `json:"id"` 64 | SessionID string `json:"sessionID"` 65 | SiteID string `json:"siteID"` 66 | RetailerID string `json:"retailerID"` 67 | Flagged bool `json:"flagged"` 68 | Skipped bool `json:"skipped"` 69 | Items []ProductKeyList `json:"item"` 70 | WebhookNotify bool `json:"webhook_notify"` 71 | } 72 | 73 | type ReplaceItemRequest struct { 74 | ID string `json:"id"` 75 | SessionID string `json:"sessionID"` 76 | SiteID string `json:"siteID"` 77 | RetailerID string `json:"retailerID"` 78 | Flagged bool `json:"flagged"` 79 | Skipped bool `json:"skipped"` 80 | FromItem ProductKeyList `json:"fromItem"` 81 | ToItem ProductKeyList `json:"toItem"` 82 | WebhookNotify bool `json:"webhook_notify"` 83 | } 84 | -------------------------------------------------------------------------------- /graph/schema/cart.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | cart(id: ID!): Cart 3 | carts(siteID: ID!): CartConnection 4 | } 5 | 6 | type Cart { 7 | """ 8 | id is the unique identifier for the cart. 9 | """ 10 | id: ID! 11 | """ 12 | code is the unique QR code for the cart. 13 | """ 14 | code: String! 15 | """ 16 | name is the name for the cart. 17 | """ 18 | name: String! 19 | """ 20 | status is the status of the cart. 21 | """ 22 | status: CartStatus! 23 | """ 24 | languagePack is the language configured for the cart. 25 | """ 26 | languagePack: String! 27 | """ 28 | softwareVersion is the software version of the cart. 29 | """ 30 | softwareVersion: String 31 | """ 32 | battery is the battery metric of the cart. 33 | """ 34 | batteries: [Battery!]! 35 | """ 36 | temperature is the temperature metric of the cart. 37 | """ 38 | temperatures: [Temperature!]! 39 | """ 40 | wifi is the wifi metric of the cart. 41 | """ 42 | wifis: [Wifi!]! 43 | """ 44 | siteID is the unique identifier for the site where the cart belongs to. 45 | """ 46 | siteID: String! 47 | """ 48 | site is the detail of the site where the cart belongs to. 49 | """ 50 | site: Site! 51 | """ 52 | sessionID is the unique identifier for the session which is associated with the cart. 53 | """ 54 | sessionID: ID 55 | """ 56 | sessionURL is the URL where the events will be sent to. 57 | """ 58 | sessionURL: String 59 | """ 60 | session is the detail of the session which is associated with the cart. 61 | """ 62 | session: Session 63 | """ 64 | sessions is a list of sessions which are associated with the cart. 65 | """ 66 | sessions(status: [SessionStatus!]): [Session!]! 67 | """ 68 | alerts is a list of alerts which are associated with the cart. 69 | """ 70 | alerts(status: [AlertStatus!], types: [AlertType!]): [Alert!]! 71 | 72 | """ 73 | created is when the session was created. 74 | """ 75 | created: Timestamp! 76 | """ 77 | updated is when the session was last updated. 78 | """ 79 | updated: Timestamp! 80 | """ 81 | deleted indicates whether the cart has been removed or not. 82 | """ 83 | deleted: Timestamp 84 | } 85 | 86 | enum CartStatus { 87 | CartState_UNSPECIFIED 88 | CartState_SUSPENDED 89 | CartState_AVAILABLE 90 | CartState_ACTIVE 91 | CartState_PAUSED 92 | } 93 | 94 | type CartConnection { 95 | carts: [Cart!]! 96 | # pageInfo: PageInfo! 97 | } 98 | 99 | 100 | type Wifi { 101 | cartID: ID! 102 | wifiIdx: Int! 103 | currentRssi: Float! 104 | averageRssi: Float! 105 | deleted: Timestamp 106 | } 107 | 108 | type Battery { 109 | cartID: ID! 110 | batteryIdx: Int! 111 | charging: Boolean! 112 | percentage: Int! 113 | cellMillivolts: Int64! 114 | inputMillivolts: Int64! 115 | temperature: Int64! 116 | deleted: Timestamp 117 | } 118 | 119 | type Temperature { 120 | cartID: ID! 121 | temperatureIdx: Int! 122 | measurement: Float! 123 | code: String! 124 | deleted: Timestamp 125 | } 126 | -------------------------------------------------------------------------------- /graph/rest/client.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "sync" 12 | 13 | "github.com/s3ndd/sen-go/client" 14 | "github.com/s3ndd/sen-go/log" 15 | 16 | "github.com/s3ndd/sen-graphql-go/internal/config" 17 | ) 18 | 19 | var ( 20 | httpClient client.ClientInterface 21 | httpClientSync sync.Once 22 | ) 23 | 24 | type ServicePrefix string 25 | 26 | const ( 27 | RegistryServicePrefix ServicePrefix = "registry" 28 | AlertNotificationServicePrefix ServicePrefix = "alert-notification" 29 | PaymentServicePrefix ServicePrefix = "payment" 30 | CartServicePrefix ServicePrefix = "css" 31 | UserServicePrefix ServicePrefix = "user" 32 | RetailServicePrefix ServicePrefix = "retail" 33 | UnifiedSessionServicePrefix ServicePrefix = "unified_session" 34 | ) 35 | 36 | // HttpClient returns a singleton rest client 37 | func HttpClient() client.ClientInterface { 38 | httpClientSync.Do(func() { 39 | httpClient = client.NewClient(config.HTTPClient()) 40 | }) 41 | 42 | return httpClient 43 | } 44 | 45 | // Uri returns the full uri. 46 | func Uri(servicePrefix ServicePrefix, version, path string) string { 47 | return fmt.Sprintf("/%s/%s/%s", servicePrefix, version, path) 48 | } 49 | 50 | // GenerateHeaders attaches the api-key to the http header 51 | func GenerateHeaders() map[string]string { 52 | return map[string]string{ 53 | "api-key": config.ApiKey("SERVICE_API_KEY"), 54 | } 55 | } 56 | 57 | // GenerateSignatureHeaders attaches retailer_id and site_id to the http header 58 | func GenerateSignatureHeaders(retailerID, siteID string) map[string]string { 59 | return map[string]string{ 60 | "retailer_id": retailerID, 61 | "site_id": siteID, 62 | } 63 | } 64 | 65 | // CheckStatus checks the http status and returns the related error message 66 | func CheckStatus(statusCode int) error { 67 | if statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices { 68 | return nil 69 | } 70 | 71 | switch statusCode { 72 | case http.StatusUnauthorized: 73 | return errors.New("invalid request credentials error") 74 | case http.StatusForbidden: 75 | return errors.New("forbidden error") 76 | case http.StatusNotFound: 77 | return errors.New("not found error") 78 | case http.StatusBadRequest: 79 | return errors.New("bad request error") 80 | default: 81 | return errors.New("internal server error") 82 | } 83 | } 84 | 85 | // batchQuery queries the data via the bulk endpoint. 86 | func batchQuery(ctx context.Context, path string, ids []string, response interface{}) error { 87 | logger := log.ForRequest(ctx).WithFields(log.LogFields{ 88 | "path": path, 89 | "ids": ids, 90 | }) 91 | body, _ := json.Marshal(struct { 92 | IDs []string `json:"ids"` 93 | }{ 94 | ids, 95 | }) 96 | 97 | resp, err := HttpClient().Post(ctx, path, GenerateHeaders(), ioutil.NopCloser(bytes.NewBuffer(body)), response) 98 | if err != nil { 99 | logger.WithError(err).Error("failed to send the batch query request") 100 | return err 101 | } 102 | if err := CheckStatus(resp.StatusCode()); err != nil { 103 | logger.WithField("status_code", resp.StatusCode()).WithError(err). 104 | Error("failed to query the data in batch") 105 | return err 106 | } 107 | logger.WithField("response", response).Info("batch query response") 108 | 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /graph/resolver/event.resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | "github.com/s3ndd/sen-graphql-go/graph/resolver/helper" 11 | "github.com/s3ndd/sen-graphql-go/graph/rest" 12 | 13 | "github.com/s3ndd/sen-go/log" 14 | ) 15 | 16 | func (r *mutationResolver) AddItems(ctx context.Context, input *model.ItemsRequest) (*model.EventResponse, error) { 17 | logger := log.ForRequest(ctx).WithField("input", input) 18 | // verify session id, and obtain site id and retailer id 19 | session, err := helper.LoadSessionByID(ctx, input.SessionID) 20 | if err != nil { 21 | logger.WithError(err).Error("failed to get the session from the session loader") 22 | return nil, err 23 | } 24 | if session == nil { 25 | err = fmt.Errorf("failed to get the session by id") 26 | logger.WithError(err).Error(err.Error()) 27 | return nil, err 28 | } 29 | if session.SiteID != input.SiteID { 30 | err = fmt.Errorf("session not found with the given site id") 31 | logger.WithError(err).Error(err.Error()) 32 | return nil, err 33 | } 34 | event, err := rest.ProcessItems(ctx, input, model.AddActionType) 35 | if err != nil { 36 | logger.WithError(err).Error("failed to process add item event") 37 | return nil, err 38 | } 39 | return event, nil 40 | } 41 | 42 | func (r *mutationResolver) RemoveItems(ctx context.Context, input *model.ItemsRequest) (*model.EventResponse, error) { 43 | logger := log.ForRequest(ctx).WithField("input", input) 44 | // verify session id, and obtain site id and retailer id 45 | session, err := helper.LoadSessionByID(ctx, input.SessionID) 46 | if err != nil { 47 | logger.WithError(err).Error("failed to get the session from the session loader") 48 | return nil, err 49 | } 50 | if session == nil { 51 | err = fmt.Errorf("failed to get the session by id") 52 | logger.WithError(err).Error(err.Error()) 53 | return nil, err 54 | } 55 | if session.SiteID != input.SiteID { 56 | err = fmt.Errorf("session not found with the given site id") 57 | logger.WithError(err).Error(err.Error()) 58 | return nil, err 59 | } 60 | event, err := rest.ProcessItems(ctx, input, model.RemoveActionType) 61 | if err != nil { 62 | logger.WithError(err).Error("failed to process remove item event") 63 | return nil, err 64 | } 65 | return event, nil 66 | } 67 | 68 | func (r *mutationResolver) ReplaceItem(ctx context.Context, input *model.ReplaceItemRequest) (*model.EventResponse, error) { 69 | logger := log.ForRequest(ctx).WithField("input", input) 70 | session, err := helper.LoadSessionByID(ctx, input.SessionID) 71 | if err != nil { 72 | logger.WithError(err).Error("failed to get the session from the session loader") 73 | return nil, err 74 | } 75 | if session == nil { 76 | err = fmt.Errorf("failed to get the session by id") 77 | logger.WithError(err).Error(err.Error()) 78 | return nil, err 79 | } 80 | if session.SiteID != input.SiteID { 81 | err = fmt.Errorf("session not found with the given site id") 82 | logger.WithError(err).Error(err.Error()) 83 | return nil, err 84 | } 85 | fromKey, toKey := input.FromItem.ProductKey, input.ToItem.ProductKey 86 | if input.FromItem.Discount != nil { 87 | fromKey += *input.FromItem.Discount 88 | } 89 | if input.ToItem.Discount != nil { 90 | toKey += *input.ToItem.Discount 91 | } 92 | if fromKey == toKey { 93 | err = fmt.Errorf("replacement item cannot be identical") 94 | logger.WithError(err).Error(err.Error()) 95 | return nil, err 96 | } 97 | event, err := rest.ReplaceItem(ctx, input) 98 | return event, err 99 | } 100 | -------------------------------------------------------------------------------- /gqlgen.yml: -------------------------------------------------------------------------------- 1 | # Where are all the schema files located? globs are supported eg src/**/*.graphqls 2 | schema: 3 | - graph/schema/*.graphql 4 | 5 | # Where should the generated server code go? 6 | exec: 7 | filename: graph/generated/generated.go 8 | package: generated 9 | 10 | # Uncomment to enable federation 11 | # federation: 12 | # filename: graph/generated/federation.go 13 | # package: generated 14 | 15 | # Where should any generated models go? 16 | model: 17 | filename: graph/model/models_gen.go 18 | package: model 19 | 20 | # Where should the resolver implementations go? 21 | resolver: 22 | # filename: graph/resolver/resolver.go 23 | # type: Resolver 24 | layout: follow-schema 25 | dir: graph/resolver 26 | package: resolver 27 | filename_template: "{name}.resolver.go" 28 | 29 | # Optional: turn on use `gqlgen:"fieldName"` tags in your models 30 | # struct_tag: json 31 | 32 | # Optional: turn on to use []Thing instead of []*Thing 33 | # omit_slice_element_pointers: false 34 | 35 | # Optional: set to speed up generation time by not performing a final validation pass. 36 | # skip_validation: true 37 | 38 | # gqlgen will search for any type names in the schema in these go packages 39 | # if they match it will use them, otherwise it will generate them. 40 | autobind: 41 | - "github.com/s3ndd/sen-graphql-go/graph/model" 42 | 43 | # This section declares type mapping between the GraphQL and go type systems 44 | # 45 | # The first line in each type will be used as defaults for resolver arguments and 46 | # modelgen, the others will be allowed when binding to fields. Configure them to 47 | # your liking 48 | models: 49 | Retailer: 50 | model: github.com/s3ndd/sen-graphql-go/graph/model.Retailer 51 | Site: 52 | model: github.com/s3ndd/sen-graphql-go/graph/model.Site 53 | Session: 54 | model: github.com/s3ndd/sen-graphql-go/graph/model.Session 55 | Item: 56 | model: github.com/s3ndd/sen-graphql-go/graph/model.Item 57 | DiscountType: 58 | model: github.com/s3ndd/sen-graphql-go/graph/model.DiscountType 59 | Event: 60 | model: github.com/s3ndd/sen-graphql-go/graph/model.Event 61 | EventResponse: 62 | model: github.com/s3ndd/sen-graphql-go/graph/model.EventResponse 63 | ProcessItemsRequest: 64 | model: github.com/s3ndd/sen-graphql-go/graph/model.ItemsRequest 65 | ReplaceItemRequest: 66 | model: github.com/s3ndd/sen-graphql-go/graph/model.ReplaceItemRequest 67 | InputItems: 68 | model: github.com/s3ndd/sen-graphql-go/graph/model.ProductKeyList 69 | InputSingleItem: 70 | model: github.com/s3ndd/sen-graphql-go/graph/model.ProductKeyList 71 | Cart: 72 | model: github.com/s3ndd/sen-graphql-go/graph/model.Cart 73 | Battery: 74 | model: github.com/s3ndd/sen-graphql-go/graph/model.Battery 75 | Wifi: 76 | model: github.com/s3ndd/sen-graphql-go/graph/model.Wifi 77 | Temperature: 78 | model: github.com/s3ndd/sen-graphql-go/graph/model.Temperature 79 | Camera: 80 | model: github.com/s3ndd/sen-graphql-go/graph/model.Camera 81 | CartConnection: 82 | model: github.com/s3ndd/sen-graphql-go/graph/model.CartConnection 83 | Alert: 84 | model: github.com/s3ndd/sen-graphql-go/graph/model.Alert 85 | AlertConnection: 86 | model: github.com/s3ndd/sen-graphql-go/graph/model.AlertConnection 87 | Timestamp: 88 | model: github.com/s3ndd/sen-graphql-go/graph/model.Timestamp 89 | Int64: 90 | model: github.com/s3ndd/sen-graphql-go/graph/model.Int64 91 | ID: 92 | model: 93 | - github.com/99designs/gqlgen/graphql.ID 94 | - github.com/99designs/gqlgen/graphql.Int 95 | - github.com/99designs/gqlgen/graphql.Int64 96 | - github.com/99designs/gqlgen/graphql.Int32 97 | Int: 98 | model: 99 | - github.com/99designs/gqlgen/graphql.Int 100 | - github.com/99designs/gqlgen/graphql.Int64 101 | - github.com/99designs/gqlgen/graphql.Int32 102 | -------------------------------------------------------------------------------- /graph/schema/event.graphql: -------------------------------------------------------------------------------- 1 | extend type Mutation{ 2 | addItems(input: ProcessItemsRequest): EventResponse! 3 | removeItems(input: ProcessItemsRequest): EventResponse! 4 | replaceItem(input: ReplaceItemRequest): EventResponse! 5 | } 6 | 7 | input ProcessItemsRequest { 8 | """ 9 | id is the unique identifier for the event. 10 | """ 11 | id: ID! 12 | """ 13 | sessionID is the unique identifier for the session associated with the event. 14 | """ 15 | sessionID: ID! 16 | """ 17 | siteID is the unique identifier for the site associated with the event. 18 | """ 19 | siteID: ID! 20 | """ 21 | retailerID is the unique identifier for the retailer associated with the event. 22 | """ 23 | retailerID: ID! 24 | """ 25 | if flagged is true, this event will be flagged for review 26 | """ 27 | flagged: Boolean 28 | """ 29 | if skipped is true, this event will not be processed 30 | """ 31 | skipped: Boolean 32 | """ 33 | items is the list of item objects that needs to be processed in this event 34 | """ 35 | items: [InputItems!]! 36 | """ 37 | if webhookNotify is true, this event will send a processing result and updated session list to configured webhook 38 | """ 39 | webhookNotify: Boolean 40 | } 41 | 42 | input ReplaceItemRequest{ 43 | """ 44 | id is the unique identifier for the event. 45 | """ 46 | id: ID! 47 | """ 48 | sessionID is the unique identifier for the session associated with the event. 49 | """ 50 | sessionID: ID! 51 | """ 52 | siteID is the unique identifier for the site associated with the event. 53 | """ 54 | siteID: ID! 55 | """ 56 | retailerID is the unique identifier for the retailer associated with the event. 57 | """ 58 | retailerID: ID! 59 | """ 60 | if flagged is true, this event will be flagged for review 61 | """ 62 | flagged: Boolean 63 | """ 64 | if skipped is true, this event will not be processed 65 | """ 66 | skipped: Boolean 67 | """ 68 | fromItem is the item object that needs to be replaced 69 | """ 70 | fromItem: InputSingleItem! 71 | """ 72 | toItem is the item object that needs to be replaced by 73 | """ 74 | toItem: InputSingleItem! 75 | """ 76 | if webhookNotify is true, this event will send a processing result and updated session list to configured webhook 77 | """ 78 | webhookNotify: Boolean 79 | } 80 | 81 | input InputItems{ 82 | """ 83 | productKey is the item barcode 84 | """ 85 | productKey: String! 86 | """ 87 | if labelled is true, this item is a labelled item 88 | """ 89 | labelled: Boolean 90 | """ 91 | discount is the discount code for this item 92 | """ 93 | discount: String 94 | """ 95 | quantity is the quantity of this item 96 | """ 97 | quantity: Int! 98 | } 99 | 100 | input InputSingleItem{ 101 | """ 102 | productKey is the item barcode 103 | """ 104 | productKey: String! 105 | """ 106 | if labelled is true, this item is a labelled item 107 | """ 108 | labelled: Boolean 109 | """ 110 | discount is the discount code for this item 111 | """ 112 | discount: String 113 | } 114 | 115 | type EventResponse { 116 | id: ID! 117 | sessionID: ID! 118 | flagged: Boolean 119 | skipped: Boolean 120 | created: Timestamp! 121 | updated: Timestamp! 122 | } 123 | 124 | type ProductKeyList { 125 | productKey: String! 126 | labelled: Boolean! 127 | discount: String 128 | quantity: Int! 129 | } 130 | 131 | type Event { 132 | id: ID! 133 | sessionID: ID! 134 | eventType: EventType! 135 | eventSubType: EventSubType! 136 | productKeyList: [ProductKeyList!]! 137 | flagged: Boolean! 138 | skipped: Boolean! 139 | created: Timestamp! 140 | updated: Timestamp! 141 | } 142 | 143 | enum EventType { 144 | SHOPPING 145 | INFERENCE 146 | } 147 | 148 | enum EventSubType { 149 | IN 150 | OUT 151 | ADD 152 | REMOVE 153 | REPLACE 154 | } -------------------------------------------------------------------------------- /graph/model/cart.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type CartStatus string 8 | 9 | // CartStatus string values 10 | const ( 11 | UnspecifiedCartStatus CartStatus = "CartState_UNSPECIFIED" 12 | SuspendedCartStatus CartStatus = "CartState_SUSPENDED" 13 | AvailableCartStatus CartStatus = "CartState_AVAILABLE" 14 | ActiveCartStatus CartStatus = "CartState_ACTIVE" 15 | PausedCartStatus CartStatus = "CartState_PAUSED" 16 | ) 17 | 18 | // Battery contains the battery details of a cart 19 | type Battery struct { 20 | CartID string `json:"cart_id"` 21 | BatteryIdx int `json:"battery_idx"` 22 | Charging bool `gorm:"not null;type:tinyint;default:0" json:"charging"` 23 | Percentage int `json:"percentage"` 24 | CellMillivolts int64 `json:"cell_millivolts"` 25 | InputMillivolts int64 `json:"input_millivolts"` 26 | Temperature int64 `json:"temperature"` 27 | Created time.Time `json:"created"` 28 | Updated time.Time `json:"updated"` 29 | Deleted *time.Time `json:"deleted"` 30 | } 31 | 32 | // Wifi contains the wifi details generated by a cart 33 | type Wifi struct { 34 | CartID string `json:"cart_id"` 35 | WifiIdx int `json:"wifi_idx"` 36 | CurrentRssi float64 `json:"current_rssi"` 37 | AverageRssi float64 `json:"average_rssi"` 38 | Created time.Time `json:"created"` 39 | Updated time.Time `json:"updated"` 40 | Deleted *time.Time `json:"deleted"` 41 | } 42 | 43 | // Temperature contains the temperature details generated by a cart 44 | type Temperature struct { 45 | CartID string `json:"cart_id"` 46 | TemperatureIdx int `json:"temperature_idx"` 47 | Measurement float64 `json:"measurement"` 48 | Code string `json:"code"` 49 | Created time.Time `json:"created"` 50 | Updated time.Time `json:"updated"` 51 | Deleted *time.Time `json:"deleted"` 52 | } 53 | 54 | // Camera contains the camera details of a cart 55 | type Camera struct { 56 | CartID string `json:"cart_id"` 57 | CameraIdx int `json:"camera_idx"` 58 | CameraKey string `json:"camera_key"` 59 | CameraFps int `json:"camera_fps"` 60 | CameraTemp int `json:"camera_temp"` 61 | CameraRunning bool `json:"camera_running"` 62 | Created time.Time `json:"created"` 63 | Updated time.Time `json:"updated"` 64 | Deleted *time.Time `json:"deleted"` 65 | } 66 | 67 | // Cart contains the details about a cart 68 | type Cart struct { 69 | ID string `json:"id"` 70 | SiteID string `json:"site_id"` 71 | Code string `json:"code"` 72 | Name string `json:"name"` 73 | SessionID *string `json:"session_id"` 74 | SessionURL string `json:"session_url"` 75 | LanguagePack string `json:"language_pack"` 76 | Status CartStatus `json:"status"` 77 | SoftwareVersion *string `json:"software_version"` 78 | Batteries []*Battery `json:"batteries"` 79 | Cameras []*Camera `json:"cameras"` 80 | Temperatures []*Temperature `json:"temperatures"` 81 | Wifis []*Wifi `json:"wifis"` 82 | Created time.Time `json:"created"` 83 | Updated time.Time `json:"updated"` 84 | Deleted *time.Time `json:"deleted"` 85 | } 86 | 87 | type CartConnection struct { 88 | Carts []Cart `json:"carts"` 89 | } 90 | 91 | // CartRegistration represents the internal structure of a cart 92 | type CartRegistration struct { 93 | ID string `json:"id"` 94 | QRCode string `json:"qr_code"` 95 | SiteID string `json:"site_id"` 96 | Site *Site `json:"site,omitempty"` 97 | RetailerID string `json:"retailer_id"` 98 | Retailer *Retailer `json:"retailer,omitempty"` 99 | Created time.Time `json:"created"` 100 | Updated time.Time `json:"updated"` 101 | Deleted bool `json:"deleted"` 102 | } 103 | 104 | // CartsRegistration contains the model of pagination and stores slices of carts 105 | type CartsRegistration struct { 106 | Pagination Pagination `json:"pagination"` 107 | Carts []CartRegistration `json:"carts"` 108 | } 109 | -------------------------------------------------------------------------------- /graph/rest/alert.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/s3ndd/sen-go/log" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | func GetAlertByID(ctx context.Context, id string) (*model.Alert, error) { 13 | var alert model.Alert 14 | resp, err := HttpClient().Get(ctx, 15 | Uri(AlertNotificationServicePrefix, "v1", fmt.Sprintf("alerts/%s", id)), 16 | GenerateHeaders(), &alert) 17 | if err != nil { 18 | log.ForRequest(ctx).WithError(err).WithField("alert_id", id). 19 | Error("failed to get the alert with the given id") 20 | return nil, err 21 | } 22 | 23 | if err := CheckStatus(resp.StatusCode()); err != nil { 24 | log.ForRequest(ctx).WithFields(log.LogFields{ 25 | "response": alert, 26 | "status_code": resp.StatusCode(), 27 | }).WithError(err).Error("failed to get the alert from alert notification service") 28 | return nil, err 29 | } 30 | return &alert, nil 31 | } 32 | 33 | func GetAlertByIDs(ctx context.Context, alertIDs []string) ([]*model.Alert, error) { 34 | path := Uri(AlertNotificationServicePrefix, "v1", "alerts/bulk") 35 | var response map[string]*model.Alert 36 | if err := batchQuery(ctx, path, alertIDs, &response); err != nil { 37 | log.ForRequest(ctx).WithError(err). 38 | Error("failed to get the alerts by ids from cart service") 39 | return nil, err 40 | } 41 | alerts := make([]*model.Alert, len(response)) 42 | for i := range alertIDs { 43 | if alert, ok := response[alertIDs[i]]; ok { 44 | alerts[i] = alert 45 | } 46 | } 47 | 48 | return alerts, nil 49 | } 50 | 51 | func GetAlerts(ctx context.Context, siteID, sessionID, cartID *string, 52 | status []model.AlertStatus, types []model.AlertType) (*model.AlertConnection, error) { 53 | var alerts model.AlertConnection 54 | queryString := "" 55 | if siteID != nil { 56 | queryString = fmt.Sprintf("?site_id=%s", *siteID) 57 | } 58 | if sessionID != nil { 59 | symbol := "?" 60 | if queryString != "" { 61 | symbol = "&" 62 | } 63 | queryString += fmt.Sprintf("%ssession_id=%s", symbol, *sessionID) 64 | } 65 | if cartID != nil { 66 | symbol := "?" 67 | if queryString != "" { 68 | symbol = "&" 69 | } 70 | queryString += fmt.Sprintf("%scart_id=%s", symbol, *cartID) 71 | } 72 | 73 | statusString, typesString := "", "" 74 | for i := range status { 75 | statusString += string(status[i]) 76 | if i < len(status)-1 { 77 | statusString += "," 78 | } 79 | } 80 | if statusString != "" { 81 | symbol := "?" 82 | if queryString != "" { 83 | symbol = "&" 84 | } 85 | queryString += fmt.Sprintf("%sstatus=%s", symbol, statusString) 86 | } 87 | for i := range types { 88 | typesString += string(types[i]) 89 | if i < len(types)-1 { 90 | typesString += "," 91 | } 92 | } 93 | if typesString != "" { 94 | symbol := "?" 95 | if queryString != "" { 96 | symbol = "&" 97 | } 98 | 99 | queryString += fmt.Sprintf("%stypes=%s", symbol, typesString) 100 | } 101 | 102 | resp, err := HttpClient().Get(ctx, 103 | Uri(AlertNotificationServicePrefix, "v1", 104 | fmt.Sprintf("alerts%s", queryString)), 105 | GenerateHeaders(), &alerts) 106 | if err != nil { 107 | log.ForRequest(ctx).WithError(err).WithField("query_string", queryString). 108 | Error("failed to get the alerts with the given parameters") 109 | return nil, err 110 | } 111 | 112 | if err := CheckStatus(resp.StatusCode()); err != nil { 113 | log.ForRequest(ctx).WithFields(log.LogFields{ 114 | "response": alerts, 115 | "status_code": resp.StatusCode(), 116 | }).WithError(err).Error("failed to get the alerts from alert notification service") 117 | return nil, err 118 | } 119 | return &alerts, nil 120 | } 121 | 122 | func GetAlertsBySessionID(ctx context.Context, sessionID string, status []model.AlertStatus, types []model.AlertType) ([]*model.Alert, error) { 123 | var alerts struct { 124 | Alerts []*model.Alert `json:"alerts"` 125 | } 126 | statusString, typesString := "", "" 127 | for i := range status { 128 | statusString += string(status[i]) 129 | if i < len(status)-1 { 130 | statusString += "," 131 | } 132 | } 133 | if statusString != "" { 134 | statusString = fmt.Sprintf("&status=%s", statusString) 135 | } 136 | for i := range types { 137 | typesString += string(types[i]) 138 | if i < len(types)-1 { 139 | typesString += "," 140 | } 141 | } 142 | if typesString != "" { 143 | typesString = fmt.Sprintf("&types=%s", typesString) 144 | } 145 | 146 | resp, err := HttpClient().Get(ctx, 147 | Uri(AlertNotificationServicePrefix, "v1", 148 | fmt.Sprintf("alerts?session_id=%s%s%s", sessionID, statusString, typesString)), 149 | GenerateHeaders(), &alerts) 150 | if err != nil { 151 | log.ForRequest(ctx).WithError(err).WithField("session_id", sessionID). 152 | Error("failed to get the alerts with the given session id") 153 | return nil, err 154 | } 155 | 156 | if err := CheckStatus(resp.StatusCode()); err != nil { 157 | log.ForRequest(ctx).WithFields(log.LogFields{ 158 | "response": alerts, 159 | "status_code": resp.StatusCode(), 160 | }).WithError(err).Error("failed to get the alerts from alert notification service") 161 | return nil, err 162 | } 163 | return alerts.Alerts, nil 164 | } 165 | 166 | func GetAlertsBySiteID(ctx context.Context, siteID string) ([]*model.Alert, error) { 167 | return nil, nil 168 | } 169 | -------------------------------------------------------------------------------- /graph/model/models_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package model 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "strconv" 9 | ) 10 | 11 | type PageInfo struct { 12 | StartCursor string `json:"startCursor"` 13 | EndCursor string `json:"endCursor"` 14 | HasNextPage bool `json:"hasNextPage"` 15 | } 16 | 17 | type SessionConnection struct { 18 | Sessions []*Session `json:"sessions"` 19 | } 20 | 21 | type UpdateSessionStatusRequest struct { 22 | SessionID string `json:"sessionID"` 23 | RetailerID string `json:"retailerID"` 24 | SiteID string `json:"siteID"` 25 | Status SessionStatus `json:"status"` 26 | } 27 | 28 | type EventSubType string 29 | 30 | const ( 31 | EventSubTypeIn EventSubType = "IN" 32 | EventSubTypeOut EventSubType = "OUT" 33 | EventSubTypeAdd EventSubType = "ADD" 34 | EventSubTypeRemove EventSubType = "REMOVE" 35 | EventSubTypeReplace EventSubType = "REPLACE" 36 | ) 37 | 38 | var AllEventSubType = []EventSubType{ 39 | EventSubTypeIn, 40 | EventSubTypeOut, 41 | EventSubTypeAdd, 42 | EventSubTypeRemove, 43 | EventSubTypeReplace, 44 | } 45 | 46 | func (e EventSubType) IsValid() bool { 47 | switch e { 48 | case EventSubTypeIn, EventSubTypeOut, EventSubTypeAdd, EventSubTypeRemove, EventSubTypeReplace: 49 | return true 50 | } 51 | return false 52 | } 53 | 54 | func (e EventSubType) String() string { 55 | return string(e) 56 | } 57 | 58 | func (e *EventSubType) UnmarshalGQL(v interface{}) error { 59 | str, ok := v.(string) 60 | if !ok { 61 | return fmt.Errorf("enums must be strings") 62 | } 63 | 64 | *e = EventSubType(str) 65 | if !e.IsValid() { 66 | return fmt.Errorf("%s is not a valid EventSubType", str) 67 | } 68 | return nil 69 | } 70 | 71 | func (e EventSubType) MarshalGQL(w io.Writer) { 72 | fmt.Fprint(w, strconv.Quote(e.String())) 73 | } 74 | 75 | type EventType string 76 | 77 | const ( 78 | EventTypeShopping EventType = "SHOPPING" 79 | EventTypeInference EventType = "INFERENCE" 80 | ) 81 | 82 | var AllEventType = []EventType{ 83 | EventTypeShopping, 84 | EventTypeInference, 85 | } 86 | 87 | func (e EventType) IsValid() bool { 88 | switch e { 89 | case EventTypeShopping, EventTypeInference: 90 | return true 91 | } 92 | return false 93 | } 94 | 95 | func (e EventType) String() string { 96 | return string(e) 97 | } 98 | 99 | func (e *EventType) UnmarshalGQL(v interface{}) error { 100 | str, ok := v.(string) 101 | if !ok { 102 | return fmt.Errorf("enums must be strings") 103 | } 104 | 105 | *e = EventType(str) 106 | if !e.IsValid() { 107 | return fmt.Errorf("%s is not a valid EventType", str) 108 | } 109 | return nil 110 | } 111 | 112 | func (e EventType) MarshalGQL(w io.Writer) { 113 | fmt.Fprint(w, strconv.Quote(e.String())) 114 | } 115 | 116 | type IntegrationType string 117 | 118 | const ( 119 | IntegrationTypePos IntegrationType = "POS" 120 | IntegrationTypePosLess IntegrationType = "POS_LESS" 121 | IntegrationTypeProductOnly IntegrationType = "PRODUCT_ONLY" 122 | ) 123 | 124 | var AllIntegrationType = []IntegrationType{ 125 | IntegrationTypePos, 126 | IntegrationTypePosLess, 127 | IntegrationTypeProductOnly, 128 | } 129 | 130 | func (e IntegrationType) IsValid() bool { 131 | switch e { 132 | case IntegrationTypePos, IntegrationTypePosLess, IntegrationTypeProductOnly: 133 | return true 134 | } 135 | return false 136 | } 137 | 138 | func (e IntegrationType) String() string { 139 | return string(e) 140 | } 141 | 142 | func (e *IntegrationType) UnmarshalGQL(v interface{}) error { 143 | str, ok := v.(string) 144 | if !ok { 145 | return fmt.Errorf("enums must be strings") 146 | } 147 | 148 | *e = IntegrationType(str) 149 | if !e.IsValid() { 150 | return fmt.Errorf("%s is not a valid IntegrationType", str) 151 | } 152 | return nil 153 | } 154 | 155 | func (e IntegrationType) MarshalGQL(w io.Writer) { 156 | fmt.Fprint(w, strconv.Quote(e.String())) 157 | } 158 | 159 | type SessionStatus string 160 | 161 | const ( 162 | SessionStatusUnspecified SessionStatus = "UNSPECIFIED" 163 | SessionStatusShopping SessionStatus = "SHOPPING" 164 | SessionStatusPaused SessionStatus = "PAUSED" 165 | SessionStatusHeld SessionStatus = "HELD" 166 | SessionStatusPrecheckout SessionStatus = "PRECHECKOUT" 167 | SessionStatusCheckout SessionStatus = "CHECKOUT" 168 | SessionStatusPaid SessionStatus = "PAID" 169 | SessionStatusFinished SessionStatus = "FINISHED" 170 | SessionStatusCancelled SessionStatus = "CANCELLED" 171 | ) 172 | 173 | var AllSessionStatus = []SessionStatus{ 174 | SessionStatusUnspecified, 175 | SessionStatusShopping, 176 | SessionStatusPaused, 177 | SessionStatusHeld, 178 | SessionStatusPrecheckout, 179 | SessionStatusCheckout, 180 | SessionStatusPaid, 181 | SessionStatusFinished, 182 | SessionStatusCancelled, 183 | } 184 | 185 | func (e SessionStatus) IsValid() bool { 186 | switch e { 187 | case SessionStatusUnspecified, SessionStatusShopping, SessionStatusPaused, SessionStatusHeld, SessionStatusPrecheckout, SessionStatusCheckout, SessionStatusPaid, SessionStatusFinished, SessionStatusCancelled: 188 | return true 189 | } 190 | return false 191 | } 192 | 193 | func (e SessionStatus) String() string { 194 | return string(e) 195 | } 196 | 197 | func (e *SessionStatus) UnmarshalGQL(v interface{}) error { 198 | str, ok := v.(string) 199 | if !ok { 200 | return fmt.Errorf("enums must be strings") 201 | } 202 | 203 | *e = SessionStatus(str) 204 | if !e.IsValid() { 205 | return fmt.Errorf("%s is not a valid SessionStatus", str) 206 | } 207 | return nil 208 | } 209 | 210 | func (e SessionStatus) MarshalGQL(w io.Writer) { 211 | fmt.Fprint(w, strconv.Quote(e.String())) 212 | } 213 | -------------------------------------------------------------------------------- /graph/dataloader/cartloader_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/vektah/dataloaden, DO NOT EDIT. 2 | 3 | package dataloader 4 | 5 | import ( 6 | "sync" 7 | "time" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | // CartLoaderConfig captures the config to create a new CartLoader 13 | type CartLoaderConfig struct { 14 | // Fetch is a method that provides the data for the loader 15 | Fetch func(keys []string) ([]*model.Cart, []error) 16 | 17 | // Wait is how long wait before sending a batch 18 | Wait time.Duration 19 | 20 | // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit 21 | MaxBatch int 22 | } 23 | 24 | // NewCartLoader creates a new CartLoader given a fetch, wait, and maxBatch 25 | func NewCartLoader(config CartLoaderConfig) *CartLoader { 26 | return &CartLoader{ 27 | fetch: config.Fetch, 28 | wait: config.Wait, 29 | maxBatch: config.MaxBatch, 30 | } 31 | } 32 | 33 | // CartLoader batches and caches requests 34 | type CartLoader struct { 35 | // this method provides the data for the loader 36 | fetch func(keys []string) ([]*model.Cart, []error) 37 | 38 | // how long to done before sending a batch 39 | wait time.Duration 40 | 41 | // this will limit the maximum number of keys to send in one batch, 0 = no limit 42 | maxBatch int 43 | 44 | // INTERNAL 45 | 46 | // lazily created cache 47 | cache map[string]*model.Cart 48 | 49 | // the current batch. keys will continue to be collected until timeout is hit, 50 | // then everything will be sent to the fetch method and out to the listeners 51 | batch *cartLoaderBatch 52 | 53 | // mutex to prevent races 54 | mu sync.Mutex 55 | } 56 | 57 | type cartLoaderBatch struct { 58 | keys []string 59 | data []*model.Cart 60 | error []error 61 | closing bool 62 | done chan struct{} 63 | } 64 | 65 | // Load a Cart by key, batching and caching will be applied automatically 66 | func (l *CartLoader) Load(key string) (*model.Cart, error) { 67 | return l.LoadThunk(key)() 68 | } 69 | 70 | // LoadThunk returns a function that when called will block waiting for a Cart. 71 | // This method should be used if you want one goroutine to make requests to many 72 | // different data loaders without blocking until the thunk is called. 73 | func (l *CartLoader) LoadThunk(key string) func() (*model.Cart, error) { 74 | l.mu.Lock() 75 | if it, ok := l.cache[key]; ok { 76 | l.mu.Unlock() 77 | return func() (*model.Cart, error) { 78 | return it, nil 79 | } 80 | } 81 | if l.batch == nil { 82 | l.batch = &cartLoaderBatch{done: make(chan struct{})} 83 | } 84 | batch := l.batch 85 | pos := batch.keyIndex(l, key) 86 | l.mu.Unlock() 87 | 88 | return func() (*model.Cart, error) { 89 | <-batch.done 90 | 91 | var data *model.Cart 92 | if pos < len(batch.data) { 93 | data = batch.data[pos] 94 | } 95 | 96 | var err error 97 | // its convenient to be able to return a single error for everything 98 | if len(batch.error) == 1 { 99 | err = batch.error[0] 100 | } else if batch.error != nil { 101 | err = batch.error[pos] 102 | } 103 | 104 | if err == nil { 105 | l.mu.Lock() 106 | l.unsafeSet(key, data) 107 | l.mu.Unlock() 108 | } 109 | 110 | return data, err 111 | } 112 | } 113 | 114 | // LoadAll fetches many keys at once. It will be broken into appropriate sized 115 | // sub batches depending on how the loader is configured 116 | func (l *CartLoader) LoadAll(keys []string) ([]*model.Cart, []error) { 117 | results := make([]func() (*model.Cart, error), len(keys)) 118 | 119 | for i, key := range keys { 120 | results[i] = l.LoadThunk(key) 121 | } 122 | 123 | carts := make([]*model.Cart, len(keys)) 124 | errors := make([]error, len(keys)) 125 | for i, thunk := range results { 126 | carts[i], errors[i] = thunk() 127 | } 128 | return carts, errors 129 | } 130 | 131 | // LoadAllThunk returns a function that when called will block waiting for a Carts. 132 | // This method should be used if you want one goroutine to make requests to many 133 | // different data loaders without blocking until the thunk is called. 134 | func (l *CartLoader) LoadAllThunk(keys []string) func() ([]*model.Cart, []error) { 135 | results := make([]func() (*model.Cart, error), len(keys)) 136 | for i, key := range keys { 137 | results[i] = l.LoadThunk(key) 138 | } 139 | return func() ([]*model.Cart, []error) { 140 | carts := make([]*model.Cart, len(keys)) 141 | errors := make([]error, len(keys)) 142 | for i, thunk := range results { 143 | carts[i], errors[i] = thunk() 144 | } 145 | return carts, errors 146 | } 147 | } 148 | 149 | // Prime the cache with the provided key and value. If the key already exists, no change is made 150 | // and false is returned. 151 | // (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) 152 | func (l *CartLoader) Prime(key string, value *model.Cart) bool { 153 | l.mu.Lock() 154 | var found bool 155 | if _, found = l.cache[key]; !found { 156 | // make a copy when writing to the cache, its easy to pass a pointer in from a loop var 157 | // and end up with the whole cache pointing to the same value. 158 | cpy := *value 159 | l.unsafeSet(key, &cpy) 160 | } 161 | l.mu.Unlock() 162 | return !found 163 | } 164 | 165 | // Clear the value at key from the cache, if it exists 166 | func (l *CartLoader) Clear(key string) { 167 | l.mu.Lock() 168 | delete(l.cache, key) 169 | l.mu.Unlock() 170 | } 171 | 172 | func (l *CartLoader) unsafeSet(key string, value *model.Cart) { 173 | if l.cache == nil { 174 | l.cache = map[string]*model.Cart{} 175 | } 176 | l.cache[key] = value 177 | } 178 | 179 | // keyIndex will return the location of the key in the batch, if its not found 180 | // it will add the key to the batch 181 | func (b *cartLoaderBatch) keyIndex(l *CartLoader, key string) int { 182 | for i, existingKey := range b.keys { 183 | if key == existingKey { 184 | return i 185 | } 186 | } 187 | 188 | pos := len(b.keys) 189 | b.keys = append(b.keys, key) 190 | if pos == 0 { 191 | go b.startTimer(l) 192 | } 193 | 194 | if l.maxBatch != 0 && pos >= l.maxBatch-1 { 195 | if !b.closing { 196 | b.closing = true 197 | l.batch = nil 198 | go b.end(l) 199 | } 200 | } 201 | 202 | return pos 203 | } 204 | 205 | func (b *cartLoaderBatch) startTimer(l *CartLoader) { 206 | time.Sleep(l.wait) 207 | l.mu.Lock() 208 | 209 | // we must have hit a batch limit and are already finalizing this batch 210 | if b.closing { 211 | l.mu.Unlock() 212 | return 213 | } 214 | 215 | l.batch = nil 216 | l.mu.Unlock() 217 | 218 | b.end(l) 219 | } 220 | 221 | func (b *cartLoaderBatch) end(l *CartLoader) { 222 | b.data, b.error = l.fetch(b.keys) 223 | close(b.done) 224 | } 225 | -------------------------------------------------------------------------------- /graph/dataloader/siteloader_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/vektah/dataloaden, DO NOT EDIT. 2 | 3 | package dataloader 4 | 5 | import ( 6 | "sync" 7 | "time" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | // SiteLoaderConfig captures the config to create a new SiteLoader 13 | type SiteLoaderConfig struct { 14 | // Fetch is a method that provides the data for the loader 15 | Fetch func(keys []string) ([]*model.Site, []error) 16 | 17 | // Wait is how long wait before sending a batch 18 | Wait time.Duration 19 | 20 | // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit 21 | MaxBatch int 22 | } 23 | 24 | // NewSiteLoader creates a new SiteLoader given a fetch, wait, and maxBatch 25 | func NewSiteLoader(config SiteLoaderConfig) *SiteLoader { 26 | return &SiteLoader{ 27 | fetch: config.Fetch, 28 | wait: config.Wait, 29 | maxBatch: config.MaxBatch, 30 | } 31 | } 32 | 33 | // SiteLoader batches and caches requests 34 | type SiteLoader struct { 35 | // this method provides the data for the loader 36 | fetch func(keys []string) ([]*model.Site, []error) 37 | 38 | // how long to done before sending a batch 39 | wait time.Duration 40 | 41 | // this will limit the maximum number of keys to send in one batch, 0 = no limit 42 | maxBatch int 43 | 44 | // INTERNAL 45 | 46 | // lazily created cache 47 | cache map[string]*model.Site 48 | 49 | // the current batch. keys will continue to be collected until timeout is hit, 50 | // then everything will be sent to the fetch method and out to the listeners 51 | batch *siteLoaderBatch 52 | 53 | // mutex to prevent races 54 | mu sync.Mutex 55 | } 56 | 57 | type siteLoaderBatch struct { 58 | keys []string 59 | data []*model.Site 60 | error []error 61 | closing bool 62 | done chan struct{} 63 | } 64 | 65 | // Load a Site by key, batching and caching will be applied automatically 66 | func (l *SiteLoader) Load(key string) (*model.Site, error) { 67 | return l.LoadThunk(key)() 68 | } 69 | 70 | // LoadThunk returns a function that when called will block waiting for a Site. 71 | // This method should be used if you want one goroutine to make requests to many 72 | // different data loaders without blocking until the thunk is called. 73 | func (l *SiteLoader) LoadThunk(key string) func() (*model.Site, error) { 74 | l.mu.Lock() 75 | if it, ok := l.cache[key]; ok { 76 | l.mu.Unlock() 77 | return func() (*model.Site, error) { 78 | return it, nil 79 | } 80 | } 81 | if l.batch == nil { 82 | l.batch = &siteLoaderBatch{done: make(chan struct{})} 83 | } 84 | batch := l.batch 85 | pos := batch.keyIndex(l, key) 86 | l.mu.Unlock() 87 | 88 | return func() (*model.Site, error) { 89 | <-batch.done 90 | 91 | var data *model.Site 92 | if pos < len(batch.data) { 93 | data = batch.data[pos] 94 | } 95 | 96 | var err error 97 | // its convenient to be able to return a single error for everything 98 | if len(batch.error) == 1 { 99 | err = batch.error[0] 100 | } else if batch.error != nil { 101 | err = batch.error[pos] 102 | } 103 | 104 | if err == nil { 105 | l.mu.Lock() 106 | l.unsafeSet(key, data) 107 | l.mu.Unlock() 108 | } 109 | 110 | return data, err 111 | } 112 | } 113 | 114 | // LoadAll fetches many keys at once. It will be broken into appropriate sized 115 | // sub batches depending on how the loader is configured 116 | func (l *SiteLoader) LoadAll(keys []string) ([]*model.Site, []error) { 117 | results := make([]func() (*model.Site, error), len(keys)) 118 | 119 | for i, key := range keys { 120 | results[i] = l.LoadThunk(key) 121 | } 122 | 123 | sites := make([]*model.Site, len(keys)) 124 | errors := make([]error, len(keys)) 125 | for i, thunk := range results { 126 | sites[i], errors[i] = thunk() 127 | } 128 | return sites, errors 129 | } 130 | 131 | // LoadAllThunk returns a function that when called will block waiting for a Sites. 132 | // This method should be used if you want one goroutine to make requests to many 133 | // different data loaders without blocking until the thunk is called. 134 | func (l *SiteLoader) LoadAllThunk(keys []string) func() ([]*model.Site, []error) { 135 | results := make([]func() (*model.Site, error), len(keys)) 136 | for i, key := range keys { 137 | results[i] = l.LoadThunk(key) 138 | } 139 | return func() ([]*model.Site, []error) { 140 | sites := make([]*model.Site, len(keys)) 141 | errors := make([]error, len(keys)) 142 | for i, thunk := range results { 143 | sites[i], errors[i] = thunk() 144 | } 145 | return sites, errors 146 | } 147 | } 148 | 149 | // Prime the cache with the provided key and value. If the key already exists, no change is made 150 | // and false is returned. 151 | // (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) 152 | func (l *SiteLoader) Prime(key string, value *model.Site) bool { 153 | l.mu.Lock() 154 | var found bool 155 | if _, found = l.cache[key]; !found { 156 | // make a copy when writing to the cache, its easy to pass a pointer in from a loop var 157 | // and end up with the whole cache pointing to the same value. 158 | cpy := *value 159 | l.unsafeSet(key, &cpy) 160 | } 161 | l.mu.Unlock() 162 | return !found 163 | } 164 | 165 | // Clear the value at key from the cache, if it exists 166 | func (l *SiteLoader) Clear(key string) { 167 | l.mu.Lock() 168 | delete(l.cache, key) 169 | l.mu.Unlock() 170 | } 171 | 172 | func (l *SiteLoader) unsafeSet(key string, value *model.Site) { 173 | if l.cache == nil { 174 | l.cache = map[string]*model.Site{} 175 | } 176 | l.cache[key] = value 177 | } 178 | 179 | // keyIndex will return the location of the key in the batch, if its not found 180 | // it will add the key to the batch 181 | func (b *siteLoaderBatch) keyIndex(l *SiteLoader, key string) int { 182 | for i, existingKey := range b.keys { 183 | if key == existingKey { 184 | return i 185 | } 186 | } 187 | 188 | pos := len(b.keys) 189 | b.keys = append(b.keys, key) 190 | if pos == 0 { 191 | go b.startTimer(l) 192 | } 193 | 194 | if l.maxBatch != 0 && pos >= l.maxBatch-1 { 195 | if !b.closing { 196 | b.closing = true 197 | l.batch = nil 198 | go b.end(l) 199 | } 200 | } 201 | 202 | return pos 203 | } 204 | 205 | func (b *siteLoaderBatch) startTimer(l *SiteLoader) { 206 | time.Sleep(l.wait) 207 | l.mu.Lock() 208 | 209 | // we must have hit a batch limit and are already finalizing this batch 210 | if b.closing { 211 | l.mu.Unlock() 212 | return 213 | } 214 | 215 | l.batch = nil 216 | l.mu.Unlock() 217 | 218 | b.end(l) 219 | } 220 | 221 | func (b *siteLoaderBatch) end(l *SiteLoader) { 222 | b.data, b.error = l.fetch(b.keys) 223 | close(b.done) 224 | } 225 | -------------------------------------------------------------------------------- /graph/dataloader/alertloader_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/vektah/dataloaden, DO NOT EDIT. 2 | 3 | package dataloader 4 | 5 | import ( 6 | "sync" 7 | "time" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | // AlertLoaderConfig captures the config to create a new AlertLoader 13 | type AlertLoaderConfig struct { 14 | // Fetch is a method that provides the data for the loader 15 | Fetch func(keys []string) ([]*model.Alert, []error) 16 | 17 | // Wait is how long wait before sending a batch 18 | Wait time.Duration 19 | 20 | // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit 21 | MaxBatch int 22 | } 23 | 24 | // NewAlertLoader creates a new AlertLoader given a fetch, wait, and maxBatch 25 | func NewAlertLoader(config AlertLoaderConfig) *AlertLoader { 26 | return &AlertLoader{ 27 | fetch: config.Fetch, 28 | wait: config.Wait, 29 | maxBatch: config.MaxBatch, 30 | } 31 | } 32 | 33 | // AlertLoader batches and caches requests 34 | type AlertLoader struct { 35 | // this method provides the data for the loader 36 | fetch func(keys []string) ([]*model.Alert, []error) 37 | 38 | // how long to done before sending a batch 39 | wait time.Duration 40 | 41 | // this will limit the maximum number of keys to send in one batch, 0 = no limit 42 | maxBatch int 43 | 44 | // INTERNAL 45 | 46 | // lazily created cache 47 | cache map[string]*model.Alert 48 | 49 | // the current batch. keys will continue to be collected until timeout is hit, 50 | // then everything will be sent to the fetch method and out to the listeners 51 | batch *alertLoaderBatch 52 | 53 | // mutex to prevent races 54 | mu sync.Mutex 55 | } 56 | 57 | type alertLoaderBatch struct { 58 | keys []string 59 | data []*model.Alert 60 | error []error 61 | closing bool 62 | done chan struct{} 63 | } 64 | 65 | // Load a Alert by key, batching and caching will be applied automatically 66 | func (l *AlertLoader) Load(key string) (*model.Alert, error) { 67 | return l.LoadThunk(key)() 68 | } 69 | 70 | // LoadThunk returns a function that when called will block waiting for a Alert. 71 | // This method should be used if you want one goroutine to make requests to many 72 | // different data loaders without blocking until the thunk is called. 73 | func (l *AlertLoader) LoadThunk(key string) func() (*model.Alert, error) { 74 | l.mu.Lock() 75 | if it, ok := l.cache[key]; ok { 76 | l.mu.Unlock() 77 | return func() (*model.Alert, error) { 78 | return it, nil 79 | } 80 | } 81 | if l.batch == nil { 82 | l.batch = &alertLoaderBatch{done: make(chan struct{})} 83 | } 84 | batch := l.batch 85 | pos := batch.keyIndex(l, key) 86 | l.mu.Unlock() 87 | 88 | return func() (*model.Alert, error) { 89 | <-batch.done 90 | 91 | var data *model.Alert 92 | if pos < len(batch.data) { 93 | data = batch.data[pos] 94 | } 95 | 96 | var err error 97 | // its convenient to be able to return a single error for everything 98 | if len(batch.error) == 1 { 99 | err = batch.error[0] 100 | } else if batch.error != nil { 101 | err = batch.error[pos] 102 | } 103 | 104 | if err == nil { 105 | l.mu.Lock() 106 | l.unsafeSet(key, data) 107 | l.mu.Unlock() 108 | } 109 | 110 | return data, err 111 | } 112 | } 113 | 114 | // LoadAll fetches many keys at once. It will be broken into appropriate sized 115 | // sub batches depending on how the loader is configured 116 | func (l *AlertLoader) LoadAll(keys []string) ([]*model.Alert, []error) { 117 | results := make([]func() (*model.Alert, error), len(keys)) 118 | 119 | for i, key := range keys { 120 | results[i] = l.LoadThunk(key) 121 | } 122 | 123 | alerts := make([]*model.Alert, len(keys)) 124 | errors := make([]error, len(keys)) 125 | for i, thunk := range results { 126 | alerts[i], errors[i] = thunk() 127 | } 128 | return alerts, errors 129 | } 130 | 131 | // LoadAllThunk returns a function that when called will block waiting for a Alerts. 132 | // This method should be used if you want one goroutine to make requests to many 133 | // different data loaders without blocking until the thunk is called. 134 | func (l *AlertLoader) LoadAllThunk(keys []string) func() ([]*model.Alert, []error) { 135 | results := make([]func() (*model.Alert, error), len(keys)) 136 | for i, key := range keys { 137 | results[i] = l.LoadThunk(key) 138 | } 139 | return func() ([]*model.Alert, []error) { 140 | alerts := make([]*model.Alert, len(keys)) 141 | errors := make([]error, len(keys)) 142 | for i, thunk := range results { 143 | alerts[i], errors[i] = thunk() 144 | } 145 | return alerts, errors 146 | } 147 | } 148 | 149 | // Prime the cache with the provided key and value. If the key already exists, no change is made 150 | // and false is returned. 151 | // (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) 152 | func (l *AlertLoader) Prime(key string, value *model.Alert) bool { 153 | l.mu.Lock() 154 | var found bool 155 | if _, found = l.cache[key]; !found { 156 | // make a copy when writing to the cache, its easy to pass a pointer in from a loop var 157 | // and end up with the whole cache pointing to the same value. 158 | cpy := *value 159 | l.unsafeSet(key, &cpy) 160 | } 161 | l.mu.Unlock() 162 | return !found 163 | } 164 | 165 | // Clear the value at key from the cache, if it exists 166 | func (l *AlertLoader) Clear(key string) { 167 | l.mu.Lock() 168 | delete(l.cache, key) 169 | l.mu.Unlock() 170 | } 171 | 172 | func (l *AlertLoader) unsafeSet(key string, value *model.Alert) { 173 | if l.cache == nil { 174 | l.cache = map[string]*model.Alert{} 175 | } 176 | l.cache[key] = value 177 | } 178 | 179 | // keyIndex will return the location of the key in the batch, if its not found 180 | // it will add the key to the batch 181 | func (b *alertLoaderBatch) keyIndex(l *AlertLoader, key string) int { 182 | for i, existingKey := range b.keys { 183 | if key == existingKey { 184 | return i 185 | } 186 | } 187 | 188 | pos := len(b.keys) 189 | b.keys = append(b.keys, key) 190 | if pos == 0 { 191 | go b.startTimer(l) 192 | } 193 | 194 | if l.maxBatch != 0 && pos >= l.maxBatch-1 { 195 | if !b.closing { 196 | b.closing = true 197 | l.batch = nil 198 | go b.end(l) 199 | } 200 | } 201 | 202 | return pos 203 | } 204 | 205 | func (b *alertLoaderBatch) startTimer(l *AlertLoader) { 206 | time.Sleep(l.wait) 207 | l.mu.Lock() 208 | 209 | // we must have hit a batch limit and are already finalizing this batch 210 | if b.closing { 211 | l.mu.Unlock() 212 | return 213 | } 214 | 215 | l.batch = nil 216 | l.mu.Unlock() 217 | 218 | b.end(l) 219 | } 220 | 221 | func (b *alertLoaderBatch) end(l *AlertLoader) { 222 | b.data, b.error = l.fetch(b.keys) 223 | close(b.done) 224 | } 225 | -------------------------------------------------------------------------------- /graph/dataloader/sessionloader_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/vektah/dataloaden, DO NOT EDIT. 2 | 3 | package dataloader 4 | 5 | import ( 6 | "sync" 7 | "time" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | // SessionLoaderConfig captures the config to create a new SessionLoader 13 | type SessionLoaderConfig struct { 14 | // Fetch is a method that provides the data for the loader 15 | Fetch func(keys []string) ([]*model.Session, []error) 16 | 17 | // Wait is how long wait before sending a batch 18 | Wait time.Duration 19 | 20 | // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit 21 | MaxBatch int 22 | } 23 | 24 | // NewSessionLoader creates a new SessionLoader given a fetch, wait, and maxBatch 25 | func NewSessionLoader(config SessionLoaderConfig) *SessionLoader { 26 | return &SessionLoader{ 27 | fetch: config.Fetch, 28 | wait: config.Wait, 29 | maxBatch: config.MaxBatch, 30 | } 31 | } 32 | 33 | // SessionLoader batches and caches requests 34 | type SessionLoader struct { 35 | // this method provides the data for the loader 36 | fetch func(keys []string) ([]*model.Session, []error) 37 | 38 | // how long to done before sending a batch 39 | wait time.Duration 40 | 41 | // this will limit the maximum number of keys to send in one batch, 0 = no limit 42 | maxBatch int 43 | 44 | // INTERNAL 45 | 46 | // lazily created cache 47 | cache map[string]*model.Session 48 | 49 | // the current batch. keys will continue to be collected until timeout is hit, 50 | // then everything will be sent to the fetch method and out to the listeners 51 | batch *sessionLoaderBatch 52 | 53 | // mutex to prevent races 54 | mu sync.Mutex 55 | } 56 | 57 | type sessionLoaderBatch struct { 58 | keys []string 59 | data []*model.Session 60 | error []error 61 | closing bool 62 | done chan struct{} 63 | } 64 | 65 | // Load a Session by key, batching and caching will be applied automatically 66 | func (l *SessionLoader) Load(key string) (*model.Session, error) { 67 | return l.LoadThunk(key)() 68 | } 69 | 70 | // LoadThunk returns a function that when called will block waiting for a Session. 71 | // This method should be used if you want one goroutine to make requests to many 72 | // different data loaders without blocking until the thunk is called. 73 | func (l *SessionLoader) LoadThunk(key string) func() (*model.Session, error) { 74 | l.mu.Lock() 75 | if it, ok := l.cache[key]; ok { 76 | l.mu.Unlock() 77 | return func() (*model.Session, error) { 78 | return it, nil 79 | } 80 | } 81 | if l.batch == nil { 82 | l.batch = &sessionLoaderBatch{done: make(chan struct{})} 83 | } 84 | batch := l.batch 85 | pos := batch.keyIndex(l, key) 86 | l.mu.Unlock() 87 | 88 | return func() (*model.Session, error) { 89 | <-batch.done 90 | 91 | var data *model.Session 92 | if pos < len(batch.data) { 93 | data = batch.data[pos] 94 | } 95 | 96 | var err error 97 | // its convenient to be able to return a single error for everything 98 | if len(batch.error) == 1 { 99 | err = batch.error[0] 100 | } else if batch.error != nil { 101 | err = batch.error[pos] 102 | } 103 | 104 | if err == nil { 105 | l.mu.Lock() 106 | l.unsafeSet(key, data) 107 | l.mu.Unlock() 108 | } 109 | 110 | return data, err 111 | } 112 | } 113 | 114 | // LoadAll fetches many keys at once. It will be broken into appropriate sized 115 | // sub batches depending on how the loader is configured 116 | func (l *SessionLoader) LoadAll(keys []string) ([]*model.Session, []error) { 117 | results := make([]func() (*model.Session, error), len(keys)) 118 | 119 | for i, key := range keys { 120 | results[i] = l.LoadThunk(key) 121 | } 122 | 123 | sessions := make([]*model.Session, len(keys)) 124 | errors := make([]error, len(keys)) 125 | for i, thunk := range results { 126 | sessions[i], errors[i] = thunk() 127 | } 128 | return sessions, errors 129 | } 130 | 131 | // LoadAllThunk returns a function that when called will block waiting for a Sessions. 132 | // This method should be used if you want one goroutine to make requests to many 133 | // different data loaders without blocking until the thunk is called. 134 | func (l *SessionLoader) LoadAllThunk(keys []string) func() ([]*model.Session, []error) { 135 | results := make([]func() (*model.Session, error), len(keys)) 136 | for i, key := range keys { 137 | results[i] = l.LoadThunk(key) 138 | } 139 | return func() ([]*model.Session, []error) { 140 | sessions := make([]*model.Session, len(keys)) 141 | errors := make([]error, len(keys)) 142 | for i, thunk := range results { 143 | sessions[i], errors[i] = thunk() 144 | } 145 | return sessions, errors 146 | } 147 | } 148 | 149 | // Prime the cache with the provided key and value. If the key already exists, no change is made 150 | // and false is returned. 151 | // (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) 152 | func (l *SessionLoader) Prime(key string, value *model.Session) bool { 153 | l.mu.Lock() 154 | var found bool 155 | if _, found = l.cache[key]; !found { 156 | // make a copy when writing to the cache, its easy to pass a pointer in from a loop var 157 | // and end up with the whole cache pointing to the same value. 158 | cpy := *value 159 | l.unsafeSet(key, &cpy) 160 | } 161 | l.mu.Unlock() 162 | return !found 163 | } 164 | 165 | // Clear the value at key from the cache, if it exists 166 | func (l *SessionLoader) Clear(key string) { 167 | l.mu.Lock() 168 | delete(l.cache, key) 169 | l.mu.Unlock() 170 | } 171 | 172 | func (l *SessionLoader) unsafeSet(key string, value *model.Session) { 173 | if l.cache == nil { 174 | l.cache = map[string]*model.Session{} 175 | } 176 | l.cache[key] = value 177 | } 178 | 179 | // keyIndex will return the location of the key in the batch, if its not found 180 | // it will add the key to the batch 181 | func (b *sessionLoaderBatch) keyIndex(l *SessionLoader, key string) int { 182 | for i, existingKey := range b.keys { 183 | if key == existingKey { 184 | return i 185 | } 186 | } 187 | 188 | pos := len(b.keys) 189 | b.keys = append(b.keys, key) 190 | if pos == 0 { 191 | go b.startTimer(l) 192 | } 193 | 194 | if l.maxBatch != 0 && pos >= l.maxBatch-1 { 195 | if !b.closing { 196 | b.closing = true 197 | l.batch = nil 198 | go b.end(l) 199 | } 200 | } 201 | 202 | return pos 203 | } 204 | 205 | func (b *sessionLoaderBatch) startTimer(l *SessionLoader) { 206 | time.Sleep(l.wait) 207 | l.mu.Lock() 208 | 209 | // we must have hit a batch limit and are already finalizing this batch 210 | if b.closing { 211 | l.mu.Unlock() 212 | return 213 | } 214 | 215 | l.batch = nil 216 | l.mu.Unlock() 217 | 218 | b.end(l) 219 | } 220 | 221 | func (b *sessionLoaderBatch) end(l *SessionLoader) { 222 | b.data, b.error = l.fetch(b.keys) 223 | close(b.done) 224 | } 225 | -------------------------------------------------------------------------------- /graph/dataloader/retailerloader_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/vektah/dataloaden, DO NOT EDIT. 2 | 3 | package dataloader 4 | 5 | import ( 6 | "sync" 7 | "time" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | // RetailerLoaderConfig captures the config to create a new RetailerLoader 13 | type RetailerLoaderConfig struct { 14 | // Fetch is a method that provides the data for the loader 15 | Fetch func(keys []string) ([]*model.Retailer, []error) 16 | 17 | // Wait is how long wait before sending a batch 18 | Wait time.Duration 19 | 20 | // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit 21 | MaxBatch int 22 | } 23 | 24 | // NewRetailerLoader creates a new RetailerLoader given a fetch, wait, and maxBatch 25 | func NewRetailerLoader(config RetailerLoaderConfig) *RetailerLoader { 26 | return &RetailerLoader{ 27 | fetch: config.Fetch, 28 | wait: config.Wait, 29 | maxBatch: config.MaxBatch, 30 | } 31 | } 32 | 33 | // RetailerLoader batches and caches requests 34 | type RetailerLoader struct { 35 | // this method provides the data for the loader 36 | fetch func(keys []string) ([]*model.Retailer, []error) 37 | 38 | // how long to done before sending a batch 39 | wait time.Duration 40 | 41 | // this will limit the maximum number of keys to send in one batch, 0 = no limit 42 | maxBatch int 43 | 44 | // INTERNAL 45 | 46 | // lazily created cache 47 | cache map[string]*model.Retailer 48 | 49 | // the current batch. keys will continue to be collected until timeout is hit, 50 | // then everything will be sent to the fetch method and out to the listeners 51 | batch *retailerLoaderBatch 52 | 53 | // mutex to prevent races 54 | mu sync.Mutex 55 | } 56 | 57 | type retailerLoaderBatch struct { 58 | keys []string 59 | data []*model.Retailer 60 | error []error 61 | closing bool 62 | done chan struct{} 63 | } 64 | 65 | // Load a Retailer by key, batching and caching will be applied automatically 66 | func (l *RetailerLoader) Load(key string) (*model.Retailer, error) { 67 | return l.LoadThunk(key)() 68 | } 69 | 70 | // LoadThunk returns a function that when called will block waiting for a Retailer. 71 | // This method should be used if you want one goroutine to make requests to many 72 | // different data loaders without blocking until the thunk is called. 73 | func (l *RetailerLoader) LoadThunk(key string) func() (*model.Retailer, error) { 74 | l.mu.Lock() 75 | if it, ok := l.cache[key]; ok { 76 | l.mu.Unlock() 77 | return func() (*model.Retailer, error) { 78 | return it, nil 79 | } 80 | } 81 | if l.batch == nil { 82 | l.batch = &retailerLoaderBatch{done: make(chan struct{})} 83 | } 84 | batch := l.batch 85 | pos := batch.keyIndex(l, key) 86 | l.mu.Unlock() 87 | 88 | return func() (*model.Retailer, error) { 89 | <-batch.done 90 | 91 | var data *model.Retailer 92 | if pos < len(batch.data) { 93 | data = batch.data[pos] 94 | } 95 | 96 | var err error 97 | // its convenient to be able to return a single error for everything 98 | if len(batch.error) == 1 { 99 | err = batch.error[0] 100 | } else if batch.error != nil { 101 | err = batch.error[pos] 102 | } 103 | 104 | if err == nil { 105 | l.mu.Lock() 106 | l.unsafeSet(key, data) 107 | l.mu.Unlock() 108 | } 109 | 110 | return data, err 111 | } 112 | } 113 | 114 | // LoadAll fetches many keys at once. It will be broken into appropriate sized 115 | // sub batches depending on how the loader is configured 116 | func (l *RetailerLoader) LoadAll(keys []string) ([]*model.Retailer, []error) { 117 | results := make([]func() (*model.Retailer, error), len(keys)) 118 | 119 | for i, key := range keys { 120 | results[i] = l.LoadThunk(key) 121 | } 122 | 123 | retailers := make([]*model.Retailer, len(keys)) 124 | errors := make([]error, len(keys)) 125 | for i, thunk := range results { 126 | retailers[i], errors[i] = thunk() 127 | } 128 | return retailers, errors 129 | } 130 | 131 | // LoadAllThunk returns a function that when called will block waiting for a Retailers. 132 | // This method should be used if you want one goroutine to make requests to many 133 | // different data loaders without blocking until the thunk is called. 134 | func (l *RetailerLoader) LoadAllThunk(keys []string) func() ([]*model.Retailer, []error) { 135 | results := make([]func() (*model.Retailer, error), len(keys)) 136 | for i, key := range keys { 137 | results[i] = l.LoadThunk(key) 138 | } 139 | return func() ([]*model.Retailer, []error) { 140 | retailers := make([]*model.Retailer, len(keys)) 141 | errors := make([]error, len(keys)) 142 | for i, thunk := range results { 143 | retailers[i], errors[i] = thunk() 144 | } 145 | return retailers, errors 146 | } 147 | } 148 | 149 | // Prime the cache with the provided key and value. If the key already exists, no change is made 150 | // and false is returned. 151 | // (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) 152 | func (l *RetailerLoader) Prime(key string, value *model.Retailer) bool { 153 | l.mu.Lock() 154 | var found bool 155 | if _, found = l.cache[key]; !found { 156 | // make a copy when writing to the cache, its easy to pass a pointer in from a loop var 157 | // and end up with the whole cache pointing to the same value. 158 | cpy := *value 159 | l.unsafeSet(key, &cpy) 160 | } 161 | l.mu.Unlock() 162 | return !found 163 | } 164 | 165 | // Clear the value at key from the cache, if it exists 166 | func (l *RetailerLoader) Clear(key string) { 167 | l.mu.Lock() 168 | delete(l.cache, key) 169 | l.mu.Unlock() 170 | } 171 | 172 | func (l *RetailerLoader) unsafeSet(key string, value *model.Retailer) { 173 | if l.cache == nil { 174 | l.cache = map[string]*model.Retailer{} 175 | } 176 | l.cache[key] = value 177 | } 178 | 179 | // keyIndex will return the location of the key in the batch, if its not found 180 | // it will add the key to the batch 181 | func (b *retailerLoaderBatch) keyIndex(l *RetailerLoader, key string) int { 182 | for i, existingKey := range b.keys { 183 | if key == existingKey { 184 | return i 185 | } 186 | } 187 | 188 | pos := len(b.keys) 189 | b.keys = append(b.keys, key) 190 | if pos == 0 { 191 | go b.startTimer(l) 192 | } 193 | 194 | if l.maxBatch != 0 && pos >= l.maxBatch-1 { 195 | if !b.closing { 196 | b.closing = true 197 | l.batch = nil 198 | go b.end(l) 199 | } 200 | } 201 | 202 | return pos 203 | } 204 | 205 | func (b *retailerLoaderBatch) startTimer(l *RetailerLoader) { 206 | time.Sleep(l.wait) 207 | l.mu.Lock() 208 | 209 | // we must have hit a batch limit and are already finalizing this batch 210 | if b.closing { 211 | l.mu.Unlock() 212 | return 213 | } 214 | 215 | l.batch = nil 216 | l.mu.Unlock() 217 | 218 | b.end(l) 219 | } 220 | 221 | func (b *retailerLoaderBatch) end(l *RetailerLoader) { 222 | b.data, b.error = l.fetch(b.keys) 223 | close(b.done) 224 | } 225 | -------------------------------------------------------------------------------- /graph/resolver/session.resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "github.com/s3ndd/sen-graphql-go/graph/dataloader" 11 | "github.com/s3ndd/sen-graphql-go/graph/generated" 12 | "github.com/s3ndd/sen-graphql-go/graph/model" 13 | "github.com/s3ndd/sen-graphql-go/graph/resolver/helper" 14 | "github.com/s3ndd/sen-graphql-go/graph/rest" 15 | "strings" 16 | 17 | "github.com/s3ndd/sen-go/log" 18 | ) 19 | 20 | func (r *mutationResolver) UpdateSessionStatus(ctx context.Context, input *model.UpdateSessionStatusRequest) (*model.Session, error) { 21 | logger := log.ForRequest(ctx).WithFields(log.LogFields{ 22 | "session_id": input.SessionID, 23 | "status": input.Status, 24 | }) 25 | session, err := helper.LoadSessionByID(ctx, input.SessionID) 26 | if err != nil { 27 | logger.WithError(err).Error("failed to get the session from the session loader") 28 | return nil, err 29 | } 30 | if session == nil { 31 | err = fmt.Errorf("failed to get session by id") 32 | logger.WithError(err).Error(err.Error()) 33 | return nil, err 34 | } 35 | session, err = rest.UpdateSessionById(ctx, input) 36 | if err != nil { 37 | logger.WithError(err).Error("failed to update session by id") 38 | return nil, err 39 | } 40 | return session, nil 41 | } 42 | 43 | func (r *queryResolver) Session(ctx context.Context, id string, siteID string, retailerID string) (*model.Session, error) { 44 | logger := log.ForRequest(ctx).WithFields(log.LogFields{ 45 | "session_id": id, 46 | "site_id": siteID, 47 | "retailer_id": retailerID, 48 | }) 49 | loaders := dataloader.ContextLoaders(ctx) 50 | session, err := loaders.Sessions.Load(id) 51 | if err != nil { 52 | logger.WithError(err).Error("failed to get the session from the session loader") 53 | return nil, err 54 | } 55 | if session != nil { 56 | if session.SiteID != siteID || (session.RetailerID != nil && *session.RetailerID != retailerID) { 57 | logger.Warn("session with the given id is not found") 58 | return nil, errors.New("session with the given id is not found") 59 | } 60 | // attach the retailer id to session 61 | session.RetailerID = &retailerID 62 | return session, nil 63 | } 64 | 65 | session, err = loaders.UnifiedSessions.Load(id) 66 | if err != nil { 67 | logger.WithError(err).Error("failed to get the session from the unified session loader") 68 | return nil, err 69 | } 70 | if session == nil || session.SiteID != siteID || (session.RetailerID != nil && *session.RetailerID != retailerID) { 71 | return nil, errors.New("session with the given id is not found") 72 | } 73 | return session, nil 74 | } 75 | 76 | func (r *queryResolver) Sessions(ctx context.Context, siteID string, retailerID string, status []model.SessionStatus) (*model.SessionConnection, error) { 77 | sessions, err := rest.GetSessionsBySiteID(ctx, siteID, status) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return sessions, nil 82 | } 83 | 84 | func (r *sessionResolver) RepeatUser(ctx context.Context, obj *model.Session) (*bool, error) { 85 | repeatUser := false 86 | if obj.UserID == nil { 87 | return &repeatUser, nil 88 | } 89 | sessionConnection, err := rest.GetSessionsByUserID(ctx, *obj.UserID, 1, 2) 90 | if err != nil { 91 | log.ForRequest(ctx).WithField("session", obj).WithError(err). 92 | Warn("failed to retrieve sessions by user id, so return false by default") 93 | return &repeatUser, nil 94 | } 95 | repeatUser = len(sessionConnection.Sessions) > 1 96 | return &repeatUser, nil 97 | } 98 | 99 | func (r *sessionResolver) Site(ctx context.Context, obj *model.Session) (*model.Site, error) { 100 | loaders := dataloader.ContextLoaders(ctx) 101 | site, err := loaders.Sites.Load(obj.SiteID) 102 | if err != nil || site == nil { 103 | return nil, err 104 | } 105 | return site, nil 106 | } 107 | 108 | func (r *sessionResolver) Status(ctx context.Context, obj *model.Session) (model.SessionStatus, error) { 109 | if strings.HasPrefix(obj.Status, "SessionStatus_") { 110 | return model.SessionStatus(strings.ReplaceAll(obj.Status, "SessionStatus_", "")), nil 111 | } 112 | return model.SessionStatus(obj.Status), nil 113 | } 114 | 115 | func (r *sessionResolver) Alerts(ctx context.Context, obj *model.Session, status []model.AlertStatus, types []model.AlertType) ([]*model.Alert, error) { 116 | alerts, err := rest.GetAlertsBySessionID(ctx, obj.ID, status, types) 117 | if err != nil { 118 | return nil, err 119 | } 120 | return alerts, nil 121 | } 122 | 123 | func (r *sessionResolver) Events(ctx context.Context, obj *model.Session, eventType *model.EventType, eventSubTypes []model.EventSubType) ([]*model.Event, error) { 124 | if obj.RetailerID == nil { 125 | loaders := dataloader.ContextLoaders(ctx) 126 | site, err := loaders.Sites.Load(obj.SiteID) 127 | if err != nil || site == nil { 128 | return nil, err 129 | } 130 | obj.RetailerID = &site.RetailerID 131 | } 132 | eventsResponse, err := rest.GetEventsBySessionID(ctx, obj.ID, obj.SiteID, *obj.RetailerID, eventType, eventSubTypes) 133 | if err != nil { 134 | return nil, err 135 | } 136 | log.ForRequest(ctx).WithFields(log.LogFields{ 137 | "session_id": obj.ID, 138 | "retailer_id": obj.RetailerID, 139 | "site_id": obj.SiteID, 140 | "event_type": eventType, 141 | "event_sub_types": eventSubTypes, 142 | }).WithField("events", eventsResponse).Debug("get events by session id response") 143 | 144 | events := make([]*model.Event, 0, len(eventsResponse)) 145 | 146 | for _, eventResponse := range eventsResponse { 147 | event := &model.Event{ 148 | ID: eventResponse.ID, 149 | SessionID: eventResponse.SessionID, 150 | EventType: eventResponse.EventType, 151 | EventSubType: eventResponse.Message.EventSubType, 152 | Flagged: eventResponse.Flagged, 153 | Skipped: eventResponse.Skipped, 154 | Created: eventResponse.Created, 155 | Updated: eventResponse.Updated, 156 | } 157 | // compatible with retail service response 158 | if len(eventResponse.Message.Items) > 0 { 159 | event.ProductKeyList = eventResponse.Message.Items 160 | } 161 | if len(event.ProductKeyList) == 0 { 162 | event.ProductKeyList = eventResponse.Message.ProductKeyList 163 | quantityOne := 1 164 | // this quantity adjust is for the retail service only 165 | for i := range event.ProductKeyList { 166 | if event.ProductKeyList[i].Quantity == nil { 167 | event.ProductKeyList[i].Quantity = &quantityOne 168 | } 169 | } 170 | } 171 | 172 | events = append(events, event) 173 | } 174 | 175 | return events, nil 176 | } 177 | 178 | // Session returns generated.SessionResolver implementation. 179 | func (r *Resolver) Session() generated.SessionResolver { return &sessionResolver{r} } 180 | 181 | type sessionResolver struct{ *Resolver } 182 | -------------------------------------------------------------------------------- /graph/rest/cart.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/s3ndd/sen-go/log" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | // GetCartsRegistrationBySiteID returns the cart from registry service 13 | func GetCartsRegistrationBySiteID(ctx context.Context, siteID string) (*model.CartsRegistration, error) { 14 | var carts model.CartsRegistration 15 | resp, err := HttpClient().Get(ctx, 16 | // TODO: include_deleted=false&page_size=100 will be updated later 17 | Uri(RegistryServicePrefix, "v1", fmt.Sprintf("carts?site_id=%s&include_deleted=false&page_size=100", siteID)), 18 | GenerateHeaders(), &carts) 19 | if err != nil { 20 | log.ForRequest(ctx).WithError(err).WithField("site_id", siteID). 21 | Error("failed to get the carts with the given site id") 22 | return nil, err 23 | } 24 | 25 | if err := CheckStatus(resp.StatusCode()); err != nil { 26 | log.ForRequest(ctx).WithFields(log.LogFields{ 27 | "response": carts, 28 | "status_code": resp.StatusCode(), 29 | }).WithError(err).Error("failed to get the carts with the given site id from registry service") 30 | return nil, err 31 | } 32 | return &carts, nil 33 | } 34 | 35 | // GetCartRegistrationByID returns the cart from registry service 36 | func GetCartRegistrationByID(ctx context.Context, id string) (*model.CartRegistration, error) { 37 | var cart model.CartRegistration 38 | resp, err := HttpClient().Get(ctx, 39 | Uri(RegistryServicePrefix, "v1", fmt.Sprintf("carts/id/%s", id)), 40 | GenerateHeaders(), &cart) 41 | if err != nil { 42 | log.ForRequest(ctx).WithError(err).WithField("cart_id", id). 43 | Error("failed to get the cart with the given id") 44 | return nil, err 45 | } 46 | 47 | if err := CheckStatus(resp.StatusCode()); err != nil { 48 | log.ForRequest(ctx).WithFields(log.LogFields{ 49 | "response": cart, 50 | "status_code": resp.StatusCode(), 51 | }).WithError(err).Error("failed to get the cart with the given id from registry service") 52 | return nil, err 53 | } 54 | return &cart, nil 55 | } 56 | 57 | func GetCartRegistrationByIDs(ctx context.Context, cartIDs []string) ([]*model.CartRegistration, error) { 58 | path := Uri(RegistryServicePrefix, "v1", "carts/bulk") 59 | var response map[string]*model.CartRegistration 60 | if err := batchQuery(ctx, path, cartIDs, &response); err != nil { 61 | log.ForRequest(ctx).WithError(err). 62 | Error("failed to get the carts by ids from registry service") 63 | return nil, err 64 | } 65 | carts := make([]*model.CartRegistration, len(response)) 66 | for i := range cartIDs { 67 | if cart, ok := response[cartIDs[i]]; ok { 68 | carts[i] = cart 69 | } 70 | } 71 | 72 | return carts, nil 73 | } 74 | 75 | // GetCartRegistrationByQRCode returns the cart from registry service 76 | func GetCartRegistrationByQRCode(ctx context.Context, qrCode string) (*model.CartRegistration, error) { 77 | var cart model.CartRegistration 78 | resp, err := HttpClient().Get(ctx, 79 | Uri(RegistryServicePrefix, "v1", fmt.Sprintf("carts/qr_code/%s", qrCode)), 80 | GenerateHeaders(), &cart) 81 | if err != nil { 82 | log.ForRequest(ctx).WithError(err).WithField("cart_qr_code", qrCode). 83 | Error("failed to get the cart with the given qr code") 84 | return nil, err 85 | } 86 | 87 | if err := CheckStatus(resp.StatusCode()); err != nil { 88 | log.ForRequest(ctx).WithFields(log.LogFields{ 89 | "response": cart, 90 | "status_code": resp.StatusCode(), 91 | }).WithError(err).Error("failed to get the cart with the given qr code from registry service") 92 | return nil, err 93 | } 94 | return &cart, nil 95 | } 96 | 97 | func GetCartByID(ctx context.Context, id, siteID string) (*model.Cart, error) { 98 | var cart model.Cart 99 | resp, err := HttpClient().Get(ctx, 100 | Uri(CartServicePrefix, 101 | "v1", 102 | fmt.Sprintf("%s/carts/id/%s?embed=%s&embed=%s&embed=%s", siteID, id, "wifi", "temperature", "battery")), 103 | GenerateHeaders(), &cart) 104 | if err != nil { 105 | log.ForRequest(ctx).WithError(err).WithFields(log.LogFields{ 106 | "cart_id": id, 107 | "site_id": siteID, 108 | }).Error("failed to get the cart with the given id from css") 109 | return nil, err 110 | } 111 | 112 | if err := CheckStatus(resp.StatusCode()); err != nil { 113 | log.ForRequest(ctx).WithFields(log.LogFields{ 114 | "response": cart, 115 | "status_code": resp.StatusCode(), 116 | }).WithError(err).Error("failed to get the cart with the given id from css") 117 | return nil, err 118 | } 119 | return &cart, nil 120 | } 121 | 122 | func GetCartByIDs(ctx context.Context, cartIDs []string) ([]*model.Cart, error) { 123 | path := Uri(CartServicePrefix, "v1", "carts/bulk") 124 | var response map[string]*model.Cart 125 | if err := batchQuery(ctx, path, cartIDs, &response); err != nil { 126 | log.ForRequest(ctx).WithError(err). 127 | Error("failed to get the carts by ids from cart service") 128 | return nil, err 129 | } 130 | carts := make([]*model.Cart, len(response)) 131 | for i := range cartIDs { 132 | if cart, ok := response[cartIDs[i]]; ok { 133 | carts[i] = cart 134 | } 135 | } 136 | 137 | return carts, nil 138 | } 139 | 140 | func GetCartByQRCode(ctx context.Context, qrCode, siteID string) (*model.Cart, error) { 141 | var cart model.Cart 142 | resp, err := HttpClient().Get(ctx, 143 | Uri(CartServicePrefix, 144 | "v1", 145 | fmt.Sprintf("%s/carts/code/%s?embed=%s&embed=%s&embed=%s", siteID, qrCode, "wifi", "temperature", "battery")), 146 | GenerateHeaders(), &cart) 147 | if err != nil { 148 | log.ForRequest(ctx).WithError(err).WithFields(log.LogFields{ 149 | "cart_qr_code": qrCode, 150 | "site_id": siteID, 151 | }).Error("failed to get the cart with the given qr code from css") 152 | return nil, err 153 | } 154 | 155 | if err := CheckStatus(resp.StatusCode()); err != nil { 156 | log.ForRequest(ctx).WithFields(log.LogFields{ 157 | "response": cart, 158 | "status_code": resp.StatusCode(), 159 | }).WithError(err).Error("failed to get the cart with the given qr code from css") 160 | return nil, err 161 | } 162 | return &cart, nil 163 | } 164 | 165 | func GetCartsBySiteID(ctx context.Context, siteID string) (*model.CartConnection, error) { 166 | var carts model.CartConnection 167 | resp, err := HttpClient().Get(ctx, 168 | Uri(CartServicePrefix, 169 | "v1", 170 | fmt.Sprintf("%s/carts?embed=%s&embed=%s&embed=%s", siteID, "wifi", "temperature", "battery")), 171 | GenerateHeaders(), &carts) 172 | if err != nil { 173 | log.ForRequest(ctx).WithError(err).WithField("site_id", siteID). 174 | Error("failed to get the carts with the given site id from css") 175 | return nil, err 176 | } 177 | 178 | if err := CheckStatus(resp.StatusCode()); err != nil { 179 | log.ForRequest(ctx).WithFields(log.LogFields{ 180 | "response": carts, 181 | "status_code": resp.StatusCode(), 182 | }).WithError(err).Error("failed to get the carts with the given site id from css") 183 | return nil, err 184 | } 185 | return &carts, nil 186 | } 187 | -------------------------------------------------------------------------------- /graph/dataloader/unifiedsessionloader_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/vektah/dataloaden, DO NOT EDIT. 2 | 3 | package dataloader 4 | 5 | import ( 6 | "sync" 7 | "time" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | // UnifiedSessionLoaderConfig captures the config to create a new UnifiedSessionLoader 13 | type UnifiedSessionLoaderConfig struct { 14 | // Fetch is a method that provides the data for the loader 15 | Fetch func(keys []string) ([]*model.Session, []error) 16 | 17 | // Wait is how long wait before sending a batch 18 | Wait time.Duration 19 | 20 | // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit 21 | MaxBatch int 22 | } 23 | 24 | // NewUnifiedSessionLoader creates a new UnifiedSessionLoader given a fetch, wait, and maxBatch 25 | func NewUnifiedSessionLoader(config UnifiedSessionLoaderConfig) *UnifiedSessionLoader { 26 | return &UnifiedSessionLoader{ 27 | fetch: config.Fetch, 28 | wait: config.Wait, 29 | maxBatch: config.MaxBatch, 30 | } 31 | } 32 | 33 | // UnifiedSessionLoader batches and caches requests 34 | type UnifiedSessionLoader struct { 35 | // this method provides the data for the loader 36 | fetch func(keys []string) ([]*model.Session, []error) 37 | 38 | // how long to done before sending a batch 39 | wait time.Duration 40 | 41 | // this will limit the maximum number of keys to send in one batch, 0 = no limit 42 | maxBatch int 43 | 44 | // INTERNAL 45 | 46 | // lazily created cache 47 | cache map[string]*model.Session 48 | 49 | // the current batch. keys will continue to be collected until timeout is hit, 50 | // then everything will be sent to the fetch method and out to the listeners 51 | batch *unifiedSessionLoaderBatch 52 | 53 | // mutex to prevent races 54 | mu sync.Mutex 55 | } 56 | 57 | type unifiedSessionLoaderBatch struct { 58 | keys []string 59 | data []*model.Session 60 | error []error 61 | closing bool 62 | done chan struct{} 63 | } 64 | 65 | // Load a Session by key, batching and caching will be applied automatically 66 | func (l *UnifiedSessionLoader) Load(key string) (*model.Session, error) { 67 | return l.LoadThunk(key)() 68 | } 69 | 70 | // LoadThunk returns a function that when called will block waiting for a Session. 71 | // This method should be used if you want one goroutine to make requests to many 72 | // different data loaders without blocking until the thunk is called. 73 | func (l *UnifiedSessionLoader) LoadThunk(key string) func() (*model.Session, error) { 74 | l.mu.Lock() 75 | if it, ok := l.cache[key]; ok { 76 | l.mu.Unlock() 77 | return func() (*model.Session, error) { 78 | return it, nil 79 | } 80 | } 81 | if l.batch == nil { 82 | l.batch = &unifiedSessionLoaderBatch{done: make(chan struct{})} 83 | } 84 | batch := l.batch 85 | pos := batch.keyIndex(l, key) 86 | l.mu.Unlock() 87 | 88 | return func() (*model.Session, error) { 89 | <-batch.done 90 | 91 | var data *model.Session 92 | if pos < len(batch.data) { 93 | data = batch.data[pos] 94 | } 95 | 96 | var err error 97 | // its convenient to be able to return a single error for everything 98 | if len(batch.error) == 1 { 99 | err = batch.error[0] 100 | } else if batch.error != nil { 101 | err = batch.error[pos] 102 | } 103 | 104 | if err == nil { 105 | l.mu.Lock() 106 | l.unsafeSet(key, data) 107 | l.mu.Unlock() 108 | } 109 | 110 | return data, err 111 | } 112 | } 113 | 114 | // LoadAll fetches many keys at once. It will be broken into appropriate sized 115 | // sub batches depending on how the loader is configured 116 | func (l *UnifiedSessionLoader) LoadAll(keys []string) ([]*model.Session, []error) { 117 | results := make([]func() (*model.Session, error), len(keys)) 118 | 119 | for i, key := range keys { 120 | results[i] = l.LoadThunk(key) 121 | } 122 | 123 | sessions := make([]*model.Session, len(keys)) 124 | errors := make([]error, len(keys)) 125 | for i, thunk := range results { 126 | sessions[i], errors[i] = thunk() 127 | } 128 | return sessions, errors 129 | } 130 | 131 | // LoadAllThunk returns a function that when called will block waiting for a Sessions. 132 | // This method should be used if you want one goroutine to make requests to many 133 | // different data loaders without blocking until the thunk is called. 134 | func (l *UnifiedSessionLoader) LoadAllThunk(keys []string) func() ([]*model.Session, []error) { 135 | results := make([]func() (*model.Session, error), len(keys)) 136 | for i, key := range keys { 137 | results[i] = l.LoadThunk(key) 138 | } 139 | return func() ([]*model.Session, []error) { 140 | sessions := make([]*model.Session, len(keys)) 141 | errors := make([]error, len(keys)) 142 | for i, thunk := range results { 143 | sessions[i], errors[i] = thunk() 144 | } 145 | return sessions, errors 146 | } 147 | } 148 | 149 | // Prime the cache with the provided key and value. If the key already exists, no change is made 150 | // and false is returned. 151 | // (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) 152 | func (l *UnifiedSessionLoader) Prime(key string, value *model.Session) bool { 153 | l.mu.Lock() 154 | var found bool 155 | if _, found = l.cache[key]; !found { 156 | // make a copy when writing to the cache, its easy to pass a pointer in from a loop var 157 | // and end up with the whole cache pointing to the same value. 158 | cpy := *value 159 | l.unsafeSet(key, &cpy) 160 | } 161 | l.mu.Unlock() 162 | return !found 163 | } 164 | 165 | // Clear the value at key from the cache, if it exists 166 | func (l *UnifiedSessionLoader) Clear(key string) { 167 | l.mu.Lock() 168 | delete(l.cache, key) 169 | l.mu.Unlock() 170 | } 171 | 172 | func (l *UnifiedSessionLoader) unsafeSet(key string, value *model.Session) { 173 | if l.cache == nil { 174 | l.cache = map[string]*model.Session{} 175 | } 176 | l.cache[key] = value 177 | } 178 | 179 | // keyIndex will return the location of the key in the batch, if its not found 180 | // it will add the key to the batch 181 | func (b *unifiedSessionLoaderBatch) keyIndex(l *UnifiedSessionLoader, key string) int { 182 | for i, existingKey := range b.keys { 183 | if key == existingKey { 184 | return i 185 | } 186 | } 187 | 188 | pos := len(b.keys) 189 | b.keys = append(b.keys, key) 190 | if pos == 0 { 191 | go b.startTimer(l) 192 | } 193 | 194 | if l.maxBatch != 0 && pos >= l.maxBatch-1 { 195 | if !b.closing { 196 | b.closing = true 197 | l.batch = nil 198 | go b.end(l) 199 | } 200 | } 201 | 202 | return pos 203 | } 204 | 205 | func (b *unifiedSessionLoaderBatch) startTimer(l *UnifiedSessionLoader) { 206 | time.Sleep(l.wait) 207 | l.mu.Lock() 208 | 209 | // we must have hit a batch limit and are already finalizing this batch 210 | if b.closing { 211 | l.mu.Unlock() 212 | return 213 | } 214 | 215 | l.batch = nil 216 | l.mu.Unlock() 217 | 218 | b.end(l) 219 | } 220 | 221 | func (b *unifiedSessionLoaderBatch) end(l *UnifiedSessionLoader) { 222 | b.data, b.error = l.fetch(b.keys) 223 | close(b.done) 224 | } 225 | -------------------------------------------------------------------------------- /graph/dataloader/cartregistrationloader_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/vektah/dataloaden, DO NOT EDIT. 2 | 3 | package dataloader 4 | 5 | import ( 6 | "sync" 7 | "time" 8 | 9 | "github.com/s3ndd/sen-graphql-go/graph/model" 10 | ) 11 | 12 | // CartRegistrationLoaderConfig captures the config to create a new CartRegistrationLoader 13 | type CartRegistrationLoaderConfig struct { 14 | // Fetch is a method that provides the data for the loader 15 | Fetch func(keys []string) ([]*model.CartRegistration, []error) 16 | 17 | // Wait is how long wait before sending a batch 18 | Wait time.Duration 19 | 20 | // MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit 21 | MaxBatch int 22 | } 23 | 24 | // NewCartRegistrationLoader creates a new CartRegistrationLoader given a fetch, wait, and maxBatch 25 | func NewCartRegistrationLoader(config CartRegistrationLoaderConfig) *CartRegistrationLoader { 26 | return &CartRegistrationLoader{ 27 | fetch: config.Fetch, 28 | wait: config.Wait, 29 | maxBatch: config.MaxBatch, 30 | } 31 | } 32 | 33 | // CartRegistrationLoader batches and caches requests 34 | type CartRegistrationLoader struct { 35 | // this method provides the data for the loader 36 | fetch func(keys []string) ([]*model.CartRegistration, []error) 37 | 38 | // how long to done before sending a batch 39 | wait time.Duration 40 | 41 | // this will limit the maximum number of keys to send in one batch, 0 = no limit 42 | maxBatch int 43 | 44 | // INTERNAL 45 | 46 | // lazily created cache 47 | cache map[string]*model.CartRegistration 48 | 49 | // the current batch. keys will continue to be collected until timeout is hit, 50 | // then everything will be sent to the fetch method and out to the listeners 51 | batch *cartRegistrationLoaderBatch 52 | 53 | // mutex to prevent races 54 | mu sync.Mutex 55 | } 56 | 57 | type cartRegistrationLoaderBatch struct { 58 | keys []string 59 | data []*model.CartRegistration 60 | error []error 61 | closing bool 62 | done chan struct{} 63 | } 64 | 65 | // Load a CartRegistration by key, batching and caching will be applied automatically 66 | func (l *CartRegistrationLoader) Load(key string) (*model.CartRegistration, error) { 67 | return l.LoadThunk(key)() 68 | } 69 | 70 | // LoadThunk returns a function that when called will block waiting for a CartRegistration. 71 | // This method should be used if you want one goroutine to make requests to many 72 | // different data loaders without blocking until the thunk is called. 73 | func (l *CartRegistrationLoader) LoadThunk(key string) func() (*model.CartRegistration, error) { 74 | l.mu.Lock() 75 | if it, ok := l.cache[key]; ok { 76 | l.mu.Unlock() 77 | return func() (*model.CartRegistration, error) { 78 | return it, nil 79 | } 80 | } 81 | if l.batch == nil { 82 | l.batch = &cartRegistrationLoaderBatch{done: make(chan struct{})} 83 | } 84 | batch := l.batch 85 | pos := batch.keyIndex(l, key) 86 | l.mu.Unlock() 87 | 88 | return func() (*model.CartRegistration, error) { 89 | <-batch.done 90 | 91 | var data *model.CartRegistration 92 | if pos < len(batch.data) { 93 | data = batch.data[pos] 94 | } 95 | 96 | var err error 97 | // its convenient to be able to return a single error for everything 98 | if len(batch.error) == 1 { 99 | err = batch.error[0] 100 | } else if batch.error != nil { 101 | err = batch.error[pos] 102 | } 103 | 104 | if err == nil { 105 | l.mu.Lock() 106 | l.unsafeSet(key, data) 107 | l.mu.Unlock() 108 | } 109 | 110 | return data, err 111 | } 112 | } 113 | 114 | // LoadAll fetches many keys at once. It will be broken into appropriate sized 115 | // sub batches depending on how the loader is configured 116 | func (l *CartRegistrationLoader) LoadAll(keys []string) ([]*model.CartRegistration, []error) { 117 | results := make([]func() (*model.CartRegistration, error), len(keys)) 118 | 119 | for i, key := range keys { 120 | results[i] = l.LoadThunk(key) 121 | } 122 | 123 | cartRegistrations := make([]*model.CartRegistration, len(keys)) 124 | errors := make([]error, len(keys)) 125 | for i, thunk := range results { 126 | cartRegistrations[i], errors[i] = thunk() 127 | } 128 | return cartRegistrations, errors 129 | } 130 | 131 | // LoadAllThunk returns a function that when called will block waiting for a CartRegistrations. 132 | // This method should be used if you want one goroutine to make requests to many 133 | // different data loaders without blocking until the thunk is called. 134 | func (l *CartRegistrationLoader) LoadAllThunk(keys []string) func() ([]*model.CartRegistration, []error) { 135 | results := make([]func() (*model.CartRegistration, error), len(keys)) 136 | for i, key := range keys { 137 | results[i] = l.LoadThunk(key) 138 | } 139 | return func() ([]*model.CartRegistration, []error) { 140 | cartRegistrations := make([]*model.CartRegistration, len(keys)) 141 | errors := make([]error, len(keys)) 142 | for i, thunk := range results { 143 | cartRegistrations[i], errors[i] = thunk() 144 | } 145 | return cartRegistrations, errors 146 | } 147 | } 148 | 149 | // Prime the cache with the provided key and value. If the key already exists, no change is made 150 | // and false is returned. 151 | // (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).) 152 | func (l *CartRegistrationLoader) Prime(key string, value *model.CartRegistration) bool { 153 | l.mu.Lock() 154 | var found bool 155 | if _, found = l.cache[key]; !found { 156 | // make a copy when writing to the cache, its easy to pass a pointer in from a loop var 157 | // and end up with the whole cache pointing to the same value. 158 | cpy := *value 159 | l.unsafeSet(key, &cpy) 160 | } 161 | l.mu.Unlock() 162 | return !found 163 | } 164 | 165 | // Clear the value at key from the cache, if it exists 166 | func (l *CartRegistrationLoader) Clear(key string) { 167 | l.mu.Lock() 168 | delete(l.cache, key) 169 | l.mu.Unlock() 170 | } 171 | 172 | func (l *CartRegistrationLoader) unsafeSet(key string, value *model.CartRegistration) { 173 | if l.cache == nil { 174 | l.cache = map[string]*model.CartRegistration{} 175 | } 176 | l.cache[key] = value 177 | } 178 | 179 | // keyIndex will return the location of the key in the batch, if its not found 180 | // it will add the key to the batch 181 | func (b *cartRegistrationLoaderBatch) keyIndex(l *CartRegistrationLoader, key string) int { 182 | for i, existingKey := range b.keys { 183 | if key == existingKey { 184 | return i 185 | } 186 | } 187 | 188 | pos := len(b.keys) 189 | b.keys = append(b.keys, key) 190 | if pos == 0 { 191 | go b.startTimer(l) 192 | } 193 | 194 | if l.maxBatch != 0 && pos >= l.maxBatch-1 { 195 | if !b.closing { 196 | b.closing = true 197 | l.batch = nil 198 | go b.end(l) 199 | } 200 | } 201 | 202 | return pos 203 | } 204 | 205 | func (b *cartRegistrationLoaderBatch) startTimer(l *CartRegistrationLoader) { 206 | time.Sleep(l.wait) 207 | l.mu.Lock() 208 | 209 | // we must have hit a batch limit and are already finalizing this batch 210 | if b.closing { 211 | l.mu.Unlock() 212 | return 213 | } 214 | 215 | l.batch = nil 216 | l.mu.Unlock() 217 | 218 | b.end(l) 219 | } 220 | 221 | func (b *cartRegistrationLoaderBatch) end(l *CartRegistrationLoader) { 222 | b.data, b.error = l.fetch(b.keys) 223 | close(b.done) 224 | } 225 | -------------------------------------------------------------------------------- /graph/rest/event.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "strings" 11 | 12 | "github.com/s3ndd/sen-go/auth" 13 | "github.com/s3ndd/sen-go/log" 14 | 15 | "github.com/s3ndd/sen-graphql-go/graph/model" 16 | "github.com/s3ndd/sen-graphql-go/internal/middleware" 17 | ) 18 | 19 | type ShoppingEventRequest struct { 20 | path string 21 | body []byte 22 | headers map[string]string 23 | } 24 | 25 | type CreateEventBody struct { 26 | ID string `json:"id"` 27 | SessionID string `json:"session_id"` 28 | EventType model.EventType `json:"event_type"` 29 | Message model.EventMessage `json:"message"` 30 | Flagged bool `json:"flagged"` 31 | } 32 | 33 | func ProcessItems(ctx context.Context, input *model.ItemsRequest, actionType model.ActionType) (*model.EventResponse, error) { 34 | logger := log.ForRequest(ctx).WithFields(log.LogFields{"input": input, "action": actionType}) 35 | site, err := GetSiteByID(ctx, input.SiteID, input.RetailerID) 36 | if err != nil { 37 | logger.WithError(err).Error("failed to get site by id") 38 | return nil, err 39 | } 40 | body := &CreateEventBody{ 41 | ID: input.ID, 42 | SessionID: input.SessionID, 43 | EventType: model.EventTypeShopping, 44 | Flagged: input.Flagged, 45 | Message: model.EventMessage{ 46 | EventSubType: actionType.ToEventSubType(), 47 | WebhookNotify: input.WebhookNotify, 48 | ProductKeyList: input.Items, 49 | }, 50 | } 51 | 52 | request := buildShoppingEventRequest(ctx, site, body) 53 | 54 | var event *model.EventResponse 55 | resp, err := HttpClient().Post(ctx, request.path, request.headers, ioutil.NopCloser(bytes.NewBuffer(request.body)), &event) 56 | if err != nil { 57 | logger.WithError(err).Error(fmt.Sprintf("failed to post %s item event request", actionType)) 58 | return nil, err 59 | } 60 | logger.WithField("resp_status_code", resp.StatusCode()).Info("response_status_code") 61 | if err := CheckStatus(resp.StatusCode()); err != nil { 62 | logger.WithField("response", event).WithError(err).Error(fmt.Sprintf("failed to %s item to session", actionType)) 63 | return nil, err 64 | } 65 | return event, nil 66 | } 67 | 68 | func ReplaceItem(ctx context.Context, input *model.ReplaceItemRequest) (*model.EventResponse, error) { 69 | logger := log.ForRequest(ctx) 70 | site, err := GetSiteByID(ctx, input.SiteID, input.RetailerID) 71 | if err != nil { 72 | logger.WithError(err).Error("failed to get site by id") 73 | return nil, err 74 | } 75 | productKeyList := make([]model.ProductKeyList, 2) 76 | quantityOne, quantityMinusOne := 1, -1 77 | productKeyList[0] = model.ProductKeyList{ 78 | ProductKey: input.FromItem.ProductKey, 79 | Labelled: input.FromItem.Labelled, 80 | Quantity: &quantityMinusOne, 81 | } 82 | productKeyList[1] = model.ProductKeyList{ 83 | ProductKey: input.ToItem.ProductKey, 84 | Labelled: input.ToItem.Labelled, 85 | Quantity: &quantityOne, 86 | } 87 | 88 | body := &CreateEventBody{ 89 | ID: input.ID, 90 | SessionID: input.SessionID, 91 | EventType: model.EventTypeShopping, 92 | Flagged: input.Flagged, 93 | Message: model.EventMessage{ 94 | EventSubType: model.ReplaceActionType.ToEventSubType(), 95 | ProductKeyList: productKeyList, 96 | WebhookNotify: input.WebhookNotify, 97 | }, 98 | } 99 | request := buildShoppingEventRequest(ctx, site, body) 100 | 101 | var event *model.EventResponse 102 | resp, err := HttpClient().Post(ctx, request.path, request.headers, ioutil.NopCloser(bytes.NewBuffer(request.body)), &event) 103 | if err != nil { 104 | logger.WithError(err).Error( 105 | fmt.Sprintf("failed to post %s item event request", strings.ToLower(model.ReplaceActionType.String()))) 106 | return nil, err 107 | } 108 | logger.WithField("resp_status_code", resp.StatusCode()).Info("response_status_code") 109 | if err := CheckStatus(resp.StatusCode()); err != nil { 110 | logger.WithField("response", event).WithError(err).Error( 111 | fmt.Sprintf("failed to %s item to session", strings.ToLower(model.ReplaceActionType.String()))) 112 | return nil, err 113 | } 114 | return event, nil 115 | } 116 | 117 | func buildShoppingEventRequest(ctx context.Context, site *model.Site, createEventBody *CreateEventBody) ShoppingEventRequest { 118 | logger := log.ForRequest(ctx) 119 | request := ShoppingEventRequest{} 120 | 121 | if site.IntegrationType == model.IntegrationTypePos { 122 | // a tricky part, since the request body differs from the unified session service 123 | createEventBody.Message.Items = createEventBody.Message.ProductKeyList 124 | body, _ := json.Marshal(createEventBody) 125 | request.body = body 126 | // path 127 | request.path = Uri(RetailServicePrefix, "v2", "events") 128 | // headers 129 | headers := GenerateHeaders() 130 | headers["Authorization"] = middleware.GetAuthorizationHeader(ctx) 131 | request.headers = headers 132 | } else { 133 | body, _ := json.Marshal(createEventBody) 134 | request.body = body 135 | // path 136 | request.path = Uri(UnifiedSessionServicePrefix, "v1", fmt.Sprintf("sessions/%s/items", createEventBody.SessionID)) 137 | // headers 138 | headers := GenerateSignatureHeaders(site.RetailerID, site.ID) 139 | signature, err := auth.Sign(http.MethodPost, request.path, site.RetailerID, site.ID, string(request.body), *site.SecretKey) 140 | if err != nil { 141 | logger.WithError(err).Error("failed to generate header's signature") 142 | } 143 | headers["signature"] = fmt.Sprintf("algorithm=%s, signature=%s", "HMAC-SHA256", signature) 144 | request.headers = headers 145 | } 146 | 147 | return request 148 | } 149 | 150 | func GetEventsBySessionID(ctx context.Context, sessionID, siteID, retailerID string, 151 | eventType *model.EventType, eventSubTypes []model.EventSubType) ([]*model.EventResponse, error) { 152 | // get events from the retail service first, if failed, get from the session service 153 | logger := log.ForRequest(ctx).WithFields(log.LogFields{ 154 | "session_id": sessionID, 155 | "site_id": siteID, 156 | "retailer_id": retailerID, 157 | }) 158 | 159 | queryParameter := "" 160 | if eventType != nil { 161 | queryParameter = fmt.Sprintf("?event_type=%s", *eventType) 162 | } 163 | if len(eventSubTypes) > 0 { 164 | for _, eventSubType := range eventSubTypes { 165 | if queryParameter == "" { 166 | queryParameter = "?" 167 | } else { 168 | queryParameter += "&" 169 | } 170 | queryParameter += fmt.Sprintf("event_sub_type=%s", eventSubType) 171 | } 172 | } 173 | 174 | var events []*model.EventResponse 175 | resp, err := HttpClient().Get(ctx, 176 | Uri(RetailServicePrefix, "v2", fmt.Sprintf("sessions/%s/events%s", sessionID, queryParameter)), 177 | GenerateHeaders(), &events) 178 | if err != nil { 179 | logger.WithError(err). 180 | Error("failed to get the events with the given session id from retail service") 181 | return nil, err 182 | } 183 | 184 | if err := CheckStatus(resp.StatusCode()); err != nil { 185 | log.ForRequest(ctx).WithFields(log.LogFields{"response": events, "status_code": resp.StatusCode()}).WithError(err). 186 | Error("failed to get the events from retail service") 187 | return nil, err 188 | } 189 | 190 | if len(events) > 0 { 191 | return events, nil 192 | } 193 | 194 | // normally, if there is no timeout and network issue, err should be nil and the status should be 200 195 | // look up from the session service one more time 196 | // when the retail integration service is ready, it just needs to query from the session service 197 | var eventsResponse struct { 198 | Events []*model.EventResponse `json:"events"` 199 | Error *string `json:"error,omitempty"` 200 | Code *int `json:"code,omitempty"` 201 | } 202 | resp, err = HttpClient().Get(ctx, 203 | Uri(UnifiedSessionServicePrefix, "v1", 204 | fmt.Sprintf("retailers/%s/sites/%s/sessions/%s/events%s", 205 | retailerID, siteID, sessionID, queryParameter)), 206 | GenerateHeaders(), &eventsResponse) 207 | if err != nil { 208 | logger.WithError(err). 209 | Error("failed to get the events with the given session id from session service") 210 | return nil, err 211 | } 212 | 213 | if err := CheckStatus(resp.StatusCode()); err != nil { 214 | if resp.StatusCode() == http.StatusNotFound { 215 | return nil, nil 216 | } 217 | log.ForRequest(ctx).WithFields(log.LogFields{"response": eventsResponse, "status_code": resp.StatusCode()}).WithError(err). 218 | Error("failed to get the events from session service") 219 | return nil, err 220 | } 221 | 222 | return eventsResponse.Events, nil 223 | } 224 | -------------------------------------------------------------------------------- /graph/rest/session.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | 11 | "github.com/s3ndd/sen-go/auth" 12 | "github.com/s3ndd/sen-go/log" 13 | 14 | "github.com/s3ndd/sen-graphql-go/graph/model" 15 | "github.com/s3ndd/sen-graphql-go/internal/middleware" 16 | ) 17 | 18 | // SessionRequest is the body request structure for updating a session status 19 | type SessionRequest struct { 20 | Paused bool `json:"paused,omitempty"` 21 | Cancelled bool `json:"cancelled,omitempty"` 22 | } 23 | 24 | // SessionIntegrationRequest is the REST request info to session services 25 | type SessionIntegrationRequest struct { 26 | path string 27 | body []byte 28 | headers map[string]string 29 | } 30 | 31 | func GetSessionByID(ctx context.Context, id string) (*model.Session, error) { 32 | var session model.Session 33 | resp, err := HttpClient().Get(ctx, 34 | Uri(RetailServicePrefix, "v2", fmt.Sprintf("sessions/%s", id)), 35 | GenerateHeaders(), &session) 36 | if err != nil { 37 | log.ForRequest(ctx).WithError(err).WithField("session_id", id). 38 | Error("failed to get the session with the given id") 39 | return nil, err 40 | } 41 | 42 | if err := CheckStatus(resp.StatusCode()); err != nil { 43 | log.ForRequest(ctx).WithFields(log.LogFields{ 44 | "response": session, 45 | "status_code": resp.StatusCode(), 46 | }).WithError(err).Error("failed to get the session with the given id from retail service") 47 | return nil, err 48 | } 49 | return &session, nil 50 | } 51 | 52 | func GetSessionByIDs(ctx context.Context, sessionIDs []string) ([]*model.Session, error) { 53 | path := Uri(RetailServicePrefix, "v2", "sessions/bulk?include_unrecognised=true") 54 | var response map[string]*model.Session 55 | if err := batchQuery(ctx, path, sessionIDs, &response); err != nil { 56 | log.ForRequest(ctx).WithError(err). 57 | Error("failed to get the session by ids from retail service") 58 | return nil, err 59 | } 60 | sessions := make([]*model.Session, len(sessionIDs)) 61 | for i := range sessionIDs { 62 | if session, ok := response[sessionIDs[i]]; ok { 63 | sessions[i] = session 64 | } 65 | } 66 | 67 | return sessions, nil 68 | } 69 | 70 | func GetSessionsBySiteID(ctx context.Context, siteID string, status []model.SessionStatus) (*model.SessionConnection, error) { 71 | queryString := fmt.Sprintf("site_id=%s", siteID) 72 | for i := range status { 73 | queryString += fmt.Sprintf("&status=%s", status[i]) 74 | } 75 | var session model.SessionConnection 76 | resp, err := HttpClient().Get(ctx, 77 | Uri(RetailServicePrefix, "v2", fmt.Sprintf("sessions?%s", queryString)), 78 | GenerateHeaders(), &session) 79 | if err != nil { 80 | log.ForRequest(ctx).WithError(err).WithFields(log.LogFields{ 81 | "site_id": siteID, 82 | "status": status, 83 | }).Error("failed to get the sessions with the given site id") 84 | return nil, err 85 | } 86 | 87 | if err := CheckStatus(resp.StatusCode()); err != nil { 88 | log.ForRequest(ctx).WithFields(log.LogFields{ 89 | "response": session, 90 | "status_code": resp.StatusCode(), 91 | }).WithError(err).Error("failed to get the sessions with the give site id from retail service") 92 | return nil, err 93 | } 94 | return &session, nil 95 | } 96 | 97 | // GetSessionsByUserID returns the sessions of the five user id. 98 | // This function is used to conduct whether the user is a repeat customer or a new customer. 99 | func GetSessionsByUserID(ctx context.Context, userID string, pageIndex, pageSize int) (*model.SessionConnection, error) { 100 | var session model.SessionConnection 101 | resp, err := HttpClient().Get(ctx, 102 | Uri(RetailServicePrefix, 103 | "v2", 104 | fmt.Sprintf("sessions?user_id=%s&page_index=%d&page_size=%d", userID, pageIndex, pageSize)), 105 | GenerateHeaders(), &session) 106 | if err != nil { 107 | log.ForRequest(ctx).WithError(err).WithFields(log.LogFields{ 108 | "user_id": userID, 109 | "page_index": pageIndex, 110 | "page_size": pageSize, 111 | }).Error("failed to get the sessions with the given site id") 112 | return nil, err 113 | } 114 | 115 | if err := CheckStatus(resp.StatusCode()); err != nil { 116 | log.ForRequest(ctx).WithFields(log.LogFields{ 117 | "response": session, 118 | "status_code": resp.StatusCode(), 119 | }).WithError(err).Error("failed to get the sessions with the give site id from retail service") 120 | return nil, err 121 | } 122 | return &session, nil 123 | } 124 | 125 | func GetUnifiedSessionByIDs(ctx context.Context, sessionIDs []string) ([]*model.Session, error) { 126 | path := Uri(UnifiedSessionServicePrefix, "v1", "sessions/bulk?include_unrecognised=true") 127 | var response map[string]*model.Session 128 | if err := batchQuery(ctx, path, sessionIDs, &response); err != nil { 129 | log.ForRequest(ctx).WithError(err). 130 | Error("failed to get the session by ids from unified session service") 131 | return nil, err 132 | } 133 | sessions := make([]*model.Session, len(sessionIDs)) 134 | for i := range sessionIDs { 135 | if session, ok := response[sessionIDs[i]]; ok { 136 | sessions[i] = session 137 | } 138 | } 139 | 140 | return sessions, nil 141 | } 142 | 143 | // UpdateSessionById returns the session of the given session id. 144 | // This function is used to update the status of a session and routes the request respective service 145 | func UpdateSessionById(ctx context.Context, input *model.UpdateSessionStatusRequest) (*model.Session, error) { 146 | logger := log.ForRequest(ctx).WithField("input", input) 147 | request := updateSessionStatusHelper(ctx, input) 148 | logger = logger.WithField("request", request) 149 | if request == nil { 150 | err := fmt.Errorf("invalid update session status request") 151 | logger.WithError(err).Error(err.Error()) 152 | return nil, err 153 | } 154 | var session *model.Session 155 | resp, err := HttpClient().Post(ctx, request.path, request.headers, ioutil.NopCloser(bytes.NewBuffer(request.body)), &session) 156 | if err != nil { 157 | logger.WithError(err).Error("failed to post session status update request") 158 | return nil, err 159 | } 160 | logger.WithField("resp_status_code", resp.StatusCode()).Info("response_status_code") 161 | if err := CheckStatus(resp.StatusCode()); err != nil { 162 | logger.WithField("response", session).WithError(err).Error("failed to update session status with the given id and status") 163 | return nil, err 164 | } 165 | return session, nil 166 | } 167 | 168 | // A helper function to construct a REST request's headers, path and body 169 | func updateSessionStatusHelper(ctx context.Context, input *model.UpdateSessionStatusRequest) *SessionIntegrationRequest { 170 | logger := log.ForRequest(ctx).WithField("input", input) 171 | site, err := GetSiteByID(ctx, input.SiteID, input.RetailerID) 172 | if err != nil { 173 | logger.WithError(err).Error("failed to get site by id") 174 | return nil 175 | } 176 | 177 | useRetailService := site.IntegrationType == model.IntegrationTypePos 178 | request := &SessionIntegrationRequest{} 179 | defer func(useRetailService bool, request *SessionIntegrationRequest) { 180 | if useRetailService { 181 | headers := GenerateHeaders() 182 | headers["Authorization"] = middleware.GetAuthorizationHeader(ctx) 183 | request.headers = headers 184 | } else { 185 | headers := GenerateSignatureHeaders(input.RetailerID, input.SiteID) 186 | signature, err := auth.Sign(http.MethodPost, request.path, input.RetailerID, input.SiteID, string(request.body), *site.SecretKey) 187 | if err != nil { 188 | logger.WithError(err).Error("failed to generate header's signature") 189 | } 190 | headers["signature"] = fmt.Sprintf("algorithm=%s, signature=%s", "HMAC-SHA256", signature) 191 | request.headers = headers 192 | } 193 | }(useRetailService, request) 194 | 195 | switch input.Status { 196 | case model.SessionStatusPaused: 197 | request.body, _ = json.Marshal(&SessionRequest{ 198 | Paused: true, 199 | }) 200 | if useRetailService { 201 | request.path = Uri(RetailServicePrefix, "v2", fmt.Sprintf("sessions/%s/paused", input.SessionID)) 202 | } else { 203 | request.path = Uri(UnifiedSessionServicePrefix, "v1", fmt.Sprintf("sessions/%s/pause", input.SessionID)) 204 | } 205 | case model.SessionStatusShopping: 206 | request.body, _ = json.Marshal(&SessionRequest{ 207 | Paused: false, 208 | }) 209 | if useRetailService { 210 | request.path = Uri(RetailServicePrefix, "v2", fmt.Sprintf("sessions/%s/paused", input.SessionID)) 211 | } else { 212 | request.path = Uri(UnifiedSessionServicePrefix, "v1", fmt.Sprintf("sessions/%s/resume", input.SessionID)) 213 | } 214 | case model.SessionStatusCancelled: 215 | request.body, _ = json.Marshal(&SessionRequest{ 216 | Cancelled: true, 217 | }) 218 | if useRetailService { 219 | request.path = Uri(RetailServicePrefix, "v2", fmt.Sprintf("sessions/%s/cancelled", input.SessionID)) 220 | } else { 221 | request.path = Uri(UnifiedSessionServicePrefix, "v1", fmt.Sprintf("sessions/%s/cancel", input.SessionID)) 222 | } 223 | default: 224 | return nil 225 | } 226 | 227 | return request 228 | } 229 | --------------------------------------------------------------------------------