From 254fe8a96118d38ed84fcf24a1ca4f234dcd9699 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sat, 13 Aug 2022 21:50:59 -0400 Subject: [PATCH 1/2] GraphQL query --- go.mod | 4 +- go.sum | 4 ++ internal/app/app.go | 4 ++ internal/app/cors/cors.go | 47 +++++++++++++++++++ internal/app/graphql/config.go | 20 ++++++++ internal/app/graphql/graphql.go | 82 +++++++++++++++++++++++++++++++++ internal/app/graphql/schema.go | 27 +++++++++++ internal/app/grpc/connect_go.go | 38 +-------------- 8 files changed, 189 insertions(+), 37 deletions(-) create mode 100644 internal/app/cors/cors.go create mode 100644 internal/app/graphql/config.go create mode 100644 internal/app/graphql/graphql.go create mode 100644 internal/app/graphql/schema.go diff --git a/go.mod b/go.mod index 81b29c2..3e451e7 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( go.uber.org/fx v1.17.1 go.uber.org/zap v1.21.0 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 + google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e google.golang.org/grpc v1.46.0 google.golang.org/protobuf v1.28.0 ) @@ -41,6 +42,8 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/graphql-go/graphql v0.8.0 // indirect + github.com/graphql-go/handler v0.2.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.5 // indirect @@ -63,7 +66,6 @@ require ( golang.org/x/sys v0.0.0-20220429233432-b5fbb4746d32 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e // indirect gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 11f52a3..c62de95 100644 --- a/go.sum +++ b/go.sum @@ -221,6 +221,10 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/graphql-go/graphql v0.8.0 h1:JHRQMeQjofwqVvGwYnr8JnPTY0AxgVy1HpHSGPLdH0I= +github.com/graphql-go/graphql v0.8.0/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= +github.com/graphql-go/handler v0.2.3 h1:CANh8WPnl5M9uA25c2GBhPqJhE53Fg0Iue/fRNla71E= +github.com/graphql-go/handler v0.2.3/go.mod h1:leLF6RpV5uZMN1CdImAxuiayrYYhOk33bZciaUGaXeU= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= diff --git a/internal/app/app.go b/internal/app/app.go index c791ac4..95c2647 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,6 +2,8 @@ package app import ( "github.com/kevinmichaelchen/api-go-template/internal/app/config" + "github.com/kevinmichaelchen/api-go-template/internal/app/cors" + "github.com/kevinmichaelchen/api-go-template/internal/app/graphql" "github.com/kevinmichaelchen/api-go-template/internal/app/grpc" "github.com/kevinmichaelchen/api-go-template/internal/app/logging" "github.com/kevinmichaelchen/api-go-template/internal/app/metrics" @@ -13,6 +15,8 @@ import ( var Module = fx.Options( config.Module, + cors.Module, + graphql.Module, grpc.Module, logging.Module, metrics.Module, diff --git a/internal/app/cors/cors.go b/internal/app/cors/cors.go new file mode 100644 index 0000000..cb9d2a8 --- /dev/null +++ b/internal/app/cors/cors.go @@ -0,0 +1,47 @@ +package cors + +import ( + "github.com/rs/cors" + "go.uber.org/fx" + "net/http" +) + +var Module = fx.Module("cors", + fx.Provide( + NewCORS, + ), +) + +func NewCORS() *cors.Cors { + // To let web developers play with the demo service from browsers, we need a + // very permissive CORS setup. + return cors.New(cors.Options{ + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowOriginFunc: func(origin string) bool { + // Allow all origins, which effectively disables CORS. + return true + }, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{ + // Content-Type is in the default safelist. + "Accept", + "Accept-Encoding", + "Accept-Post", + "Connect-Accept-Encoding", + "Connect-Content-Encoding", + "Content-Encoding", + "Grpc-Accept-Encoding", + "Grpc-Encoding", + "Grpc-Message", + "Grpc-Status", + "Grpc-Status-Details-Bin", + }, + }) +} diff --git a/internal/app/graphql/config.go b/internal/app/graphql/config.go new file mode 100644 index 0000000..6c14599 --- /dev/null +++ b/internal/app/graphql/config.go @@ -0,0 +1,20 @@ +package graphql + +import ( + "context" + "github.com/sethvargo/go-envconfig" +) + +func NewConfig() (cfg Config, err error) { + err = envconfig.Process(context.Background(), &cfg) + return +} + +type Config struct { + GraphQLConfig *NestedConfig `env:",prefix=GRAPHQL_"` +} + +type NestedConfig struct { + Host string `env:"HOST,default=localhost"` + Port int `env:"PORT,default=8082"` +} diff --git a/internal/app/graphql/graphql.go b/internal/app/graphql/graphql.go new file mode 100644 index 0000000..7c92314 --- /dev/null +++ b/internal/app/graphql/graphql.go @@ -0,0 +1,82 @@ +package graphql + +import ( + "context" + "errors" + "fmt" + "github.com/graphql-go/graphql" + "github.com/graphql-go/handler" + "github.com/rs/cors" + "go.uber.org/fx" + "go.uber.org/zap" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "net/http" +) + +var Module = fx.Module("graphql", + fx.Provide( + NewConfig, + NewGraphQL, + NewSchema, + ), + fx.Invoke( + RegisterGraphQL, + ), +) + +type registerGraphQLInput struct { + fx.In + + Schema *graphql.Schema + Logger *zap.Logger + Mux *http.ServeMux `name:"graphqlMux"` +} + +type NewGraphQLOutput struct { + fx.Out + + Mux *http.ServeMux `name:"graphqlMux"` +} + +func NewGraphQL(lc fx.Lifecycle, logger *zap.Logger, cfg Config, crs *cors.Cors) NewGraphQLOutput { + mux := http.NewServeMux() + address := fmt.Sprintf("%s:%d", cfg.GraphQLConfig.Host, cfg.GraphQLConfig.Port) + srv := &http.Server{ + Addr: address, + // Use h2c so we can serve HTTP/2 without TLS. + Handler: h2c.NewHandler( + crs.Handler(mux), + &http2.Server{}, + ), + } + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + // In production, we'd want to separate the Listen and Serve phases for + // better error-handling. + go func() { + err := srv.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Error("connect-go ListenAndServe failed", zap.Error(err)) + } + }() + logger.Sugar().Infof("Listening for connect-go on: %s", address) + return nil + }, + OnStop: func(ctx context.Context) error { + return srv.Shutdown(ctx) + }, + }) + return NewGraphQLOutput{ + Mux: mux, + } +} + +func RegisterGraphQL(in registerGraphQLInput) { + h := handler.New(&handler.Config{ + Schema: in.Schema, + Pretty: true, + GraphiQL: true, + }) + in.Mux.Handle("/graphql", h) +} diff --git a/internal/app/graphql/schema.go b/internal/app/graphql/schema.go new file mode 100644 index 0000000..3a8b69b --- /dev/null +++ b/internal/app/graphql/schema.go @@ -0,0 +1,27 @@ +package graphql + +import "github.com/graphql-go/graphql" + +func NewSchema() (graphql.Schema, error) { + q := graphql.NewObject(graphql.ObjectConfig{ + Name: "Query", + Fields: graphql.Fields{ + "name": &graphql.Field{ + Name: "name", + Type: graphql.String, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return p.Context.Value("name"), nil + }, + }, + }, + }) + + return graphql.NewSchema(graphql.SchemaConfig{ + Query: q, + Mutation: nil, + Subscription: nil, + Types: nil, + Directives: nil, + Extensions: nil, + }) +} diff --git a/internal/app/grpc/connect_go.go b/internal/app/grpc/connect_go.go index 7a99725..959cf53 100644 --- a/internal/app/grpc/connect_go.go +++ b/internal/app/grpc/connect_go.go @@ -42,14 +42,14 @@ type NewConnectGoServerOutput struct { Mux *http.ServeMux `name:"connectGoMux"` } -func NewConnectGoServer(lc fx.Lifecycle, logger *zap.Logger, cfg Config) NewConnectGoServerOutput { +func NewConnectGoServer(lc fx.Lifecycle, logger *zap.Logger, cfg Config, crs *cors.Cors) NewConnectGoServerOutput { mux := http.NewServeMux() address := fmt.Sprintf("%s:%d", cfg.ConnectConfig.Host, cfg.ConnectConfig.Port) srv := &http.Server{ Addr: address, // Use h2c so we can serve HTTP/2 without TLS. Handler: h2c.NewHandler( - newCORS().Handler(mux), + crs.Handler(mux), &http2.Server{}, ), } @@ -74,37 +74,3 @@ func NewConnectGoServer(lc fx.Lifecycle, logger *zap.Logger, cfg Config) NewConn Mux: mux, } } - -func newCORS() *cors.Cors { - // To let web developers play with the demo service from browsers, we need a - // very permissive CORS setup. - return cors.New(cors.Options{ - AllowedMethods: []string{ - http.MethodHead, - http.MethodGet, - http.MethodPost, - http.MethodPut, - http.MethodPatch, - http.MethodDelete, - }, - AllowOriginFunc: func(origin string) bool { - // Allow all origins, which effectively disables CORS. - return true - }, - AllowedHeaders: []string{"*"}, - ExposedHeaders: []string{ - // Content-Type is in the default safelist. - "Accept", - "Accept-Encoding", - "Accept-Post", - "Connect-Accept-Encoding", - "Connect-Content-Encoding", - "Content-Encoding", - "Grpc-Accept-Encoding", - "Grpc-Encoding", - "Grpc-Message", - "Grpc-Status", - "Grpc-Status-Details-Bin", - }, - }) -} From eefbfd9fa20babf2dc7535c46dc9d847b6b09da3 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sat, 13 Aug 2022 22:01:10 -0400 Subject: [PATCH 2/2] more --- internal/app/graphql/graphql.go | 7 ++++--- internal/app/graphql/schema.go | 12 ++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/app/graphql/graphql.go b/internal/app/graphql/graphql.go index 7c92314..9eff515 100644 --- a/internal/app/graphql/graphql.go +++ b/internal/app/graphql/graphql.go @@ -74,9 +74,10 @@ func NewGraphQL(lc fx.Lifecycle, logger *zap.Logger, cfg Config, crs *cors.Cors) func RegisterGraphQL(in registerGraphQLInput) { h := handler.New(&handler.Config{ - Schema: in.Schema, - Pretty: true, - GraphiQL: true, + Schema: in.Schema, + Pretty: true, + GraphiQL: true, + Playground: true, }) in.Mux.Handle("/graphql", h) } diff --git a/internal/app/graphql/schema.go b/internal/app/graphql/schema.go index 3a8b69b..56dd0af 100644 --- a/internal/app/graphql/schema.go +++ b/internal/app/graphql/schema.go @@ -2,21 +2,21 @@ package graphql import "github.com/graphql-go/graphql" -func NewSchema() (graphql.Schema, error) { +func NewSchema() (*graphql.Schema, error) { q := graphql.NewObject(graphql.ObjectConfig{ - Name: "Query", + Name: "RootQuery", Fields: graphql.Fields{ "name": &graphql.Field{ Name: "name", Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - return p.Context.Value("name"), nil + return "Kevin", nil }, }, }, }) - return graphql.NewSchema(graphql.SchemaConfig{ + s, err := graphql.NewSchema(graphql.SchemaConfig{ Query: q, Mutation: nil, Subscription: nil, @@ -24,4 +24,8 @@ func NewSchema() (graphql.Schema, error) { Directives: nil, Extensions: nil, }) + if err != nil { + return nil, err + } + return &s, nil }