diff --git a/.golangci.yml b/.golangci.yml index e9739e1..a68fea1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -83,7 +83,6 @@ linters: issues: new: false fix: false - new-from-rev: 2b6063ac3c145007726044a5555b6d2a40d3b089 exclude-rules: - path: _test\.go linters: @@ -113,5 +112,6 @@ linters-settings: ignore-names: - err - id + - to ignore-decls: - i int diff --git a/command.go b/command.go index 089709a..9f80eb3 100644 --- a/command.go +++ b/command.go @@ -24,6 +24,9 @@ import ( const ( basePath = "graphql" schemaBasePath = "graphql/schema" + + filePermission755 = 0755 + filePermission644 = 0644 ) var skipGoModTidy = true @@ -56,28 +59,18 @@ func Generate(services []Service, basePath string, schemaBasePath string) error schemaPath := path.Join(schemaBasePath, "schema.graphql") cfg := config.DefaultConfig() + err := config.CompleteConfig(cfg) if err != nil { - return err + return fmt.Errorf("failed to complete the config: %w", err) } cfg.SchemaFilename = []string{schemaPath} cfg.Models = make(map[string]config.TypeMapEntry) - if err := os.MkdirAll(schemaBasePath, 0755); err != nil && !os.IsExist(err) { - return fmt.Errorf("mkdir %q failed: %w", schemaBasePath, err) - } - - if err := os.WriteFile(schemaPath, schema, 0644); err != nil { - return fmt.Errorf("writefile %q failed: %w", schemaPath, err) - } - - if err := os.WriteFile(path.Join(basePath, "module.go"), module, 0644); err != nil { - return fmt.Errorf("writefile %q/module.go failed: %w", basePath, err) - } - - if err := os.WriteFile(path.Join(basePath, "emptymodule.go"), emptyModule, 0644); err != nil { - return fmt.Errorf("writefile %q/emptymodule.go failed: %w", basePath, err) + err = createFiles(schemaBasePath, schemaPath, basePath) + if err != nil { + return err } types := new(Types) @@ -91,40 +84,17 @@ func Generate(services []Service, basePath string, schemaBasePath string) error fpath := path.Join(schemaBasePath, fname) log.Printf("Writing %s", fname) - if err := os.WriteFile(fpath, service.Schema(), 0644); err != nil { + + if err := os.WriteFile(fpath, service.Schema(), filePermission644); err != nil { return fmt.Errorf("writefile %q failed: %w", fpath, err) } + cfg.SchemaFilename = append(cfg.SchemaFilename, fpath) service.Types(types) } - // merge models into config models - for graphqlObject, goType := range types.names { - cfg.Models[graphqlObject] = config.TypeMapEntry{Model: []string{goType}} - } - - for graphqlObject, fields := range types.fields { - for graphqlField, goType := range fields { - model := cfg.Models[graphqlObject] - if cfg.Models[graphqlObject].Fields == nil { - model.Fields = make(map[string]config.TypeMapField) - } - model.Fields[graphqlField] = config.TypeMapField{FieldName: goType} - cfg.Models[graphqlObject] = model - } - } - - for graphqlObject, resolver := range types.resolver { - for graphqlField := range resolver { - model := cfg.Models[graphqlObject] - if cfg.Models[graphqlObject].Fields == nil { - model.Fields = make(map[string]config.TypeMapField) - } - model.Fields[graphqlField] = config.TypeMapField{Resolver: true} - cfg.Models[graphqlObject] = model - } - } + mergeModels(types, cfg) float := cfg.Models["Float"] float.Model = append(float.Model, "flamingo.me/graphql.Float", "github.com/99designs/gqlgen/graphql.Float") @@ -150,9 +120,61 @@ func Generate(services []Service, basePath string, schemaBasePath string) error if err := api.Generate(cfg, api.AddPlugin(&plugin{types: types})); err != nil { return fmt.Errorf("gqlgen/api.Generate failed: %w", err) } + return nil } +func createFiles(schemaBasePath string, schemaPath string, basePath string) error { + if err := os.MkdirAll(schemaBasePath, filePermission755); err != nil && !os.IsExist(err) { + return fmt.Errorf("mkdir %q failed: %w", schemaBasePath, err) + } + + if err := os.WriteFile(schemaPath, schema, filePermission644); err != nil { + return fmt.Errorf("writefile %q failed: %w", schemaPath, err) + } + + if err := os.WriteFile(path.Join(basePath, "module.go"), module, filePermission644); err != nil { + return fmt.Errorf("writefile %q/module.go failed: %w", basePath, err) + } + + if err := os.WriteFile(path.Join(basePath, "emptymodule.go"), emptyModule, filePermission644); err != nil { + return fmt.Errorf("writefile %q/emptymodule.go failed: %w", basePath, err) + } + + return nil +} + +// merge models into config models +func mergeModels(types *Types, cfg *config.Config) { + for graphqlObject, goType := range types.names { + cfg.Models[graphqlObject] = config.TypeMapEntry{Model: []string{goType}} + } + + for graphqlObject, fields := range types.fields { + for graphqlField, goType := range fields { + model := cfg.Models[graphqlObject] + if cfg.Models[graphqlObject].Fields == nil { + model.Fields = make(map[string]config.TypeMapField) + } + + model.Fields[graphqlField] = config.TypeMapField{FieldName: goType} + cfg.Models[graphqlObject] = model + } + } + + for graphqlObject, resolver := range types.resolver { + for graphqlField := range resolver { + model := cfg.Models[graphqlObject] + if cfg.Models[graphqlObject].Fields == nil { + model.Fields = make(map[string]config.TypeMapField) + } + + model.Fields[graphqlField] = config.TypeMapField{Resolver: true} + cfg.Models[graphqlObject] = model + } + } +} + var _ plugin2.CodeGenerator = &plugin{} var _ plugin2.ConfigMutator = &plugin{} var _ plugin2.Plugin = &plugin{} @@ -184,7 +206,8 @@ func (m *plugin) GenerateCode(data *codegen.Data) error { panic("code generation failed") } }() - return gqltemplates.Render(gqltemplates.Options{ + + err := gqltemplates.Render(gqltemplates.Options{ PackageName: "graphql", Filename: "graphql/resolver.go", Data: &resolverBuild{ @@ -196,6 +219,7 @@ func (m *plugin) GenerateCode(data *codegen.Data) error { Funcs: template.FuncMap{ "gpkg": func(from, to string, field codegen.Field) string { if m.types.resolver[from][to][0] == "" { + //nolint: forbidigo // special case of error output fmt.Printf( "\nmissing resolver for %q.%q:\n\tfunc (r *%sResolver) %s%s\n\n\ttypes.Resolve(\"%s\", \"%s\", %sResolver{}, \"%s\")\n\n", from, to, @@ -346,6 +370,12 @@ func direct(root *{{$root.TypeName}}) map[string]interface{} { } `, }) + + if err != nil { + return fmt.Errorf("failed to render generation template: %w", err) + } + + return nil } type resolverBuild struct { diff --git a/corshandler.go b/corshandler.go index 92189c5..7db61b2 100644 --- a/corshandler.go +++ b/corshandler.go @@ -14,6 +14,7 @@ func (h *corsHandler) validateOrigin(origin string) bool { return true } } + return false } diff --git a/example/todo/infrastructure/todo.go b/example/todo/infrastructure/todo.go index ea7ff14..6bdee68 100644 --- a/example/todo/infrastructure/todo.go +++ b/example/todo/infrastructure/todo.go @@ -17,6 +17,11 @@ var todos = []*domain.Todo{ {ID: "task-2", Task: "task c"}, } +var ( + ErrNoTodoGiven = errors.New("no todo given") + ErrTodoNotFound = errors.New("todo not found") +) + // Todos returns a list of mocked todos func (ts *TodoService) Todos(_ context.Context, _ string) ([]*domain.Todo, error) { return todos, nil @@ -25,13 +30,15 @@ func (ts *TodoService) Todos(_ context.Context, _ string) ([]*domain.Todo, error // AddTodo mutation adds an entry to the list func (ts *TodoService) AddTodo(_ context.Context, _ string, task string) (*domain.Todo, error) { if task == "" { - return nil, errors.New("no todo given") + return nil, ErrNoTodoGiven } + todo := &domain.Todo{ ID: "task-" + strconv.Itoa(len(todos)), Task: task, } todos = append(todos, todo) + return todo, nil } @@ -43,5 +50,6 @@ func (ts *TodoService) TodoDone(_ context.Context, todoID string, done bool) (*d return todos[i], nil } } - return nil, errors.New("todo not found") + + return nil, ErrTodoNotFound } diff --git a/example/todo/resolver.go b/example/todo/resolver.go index 89ec0f3..ea78bd4 100644 --- a/example/todo/resolver.go +++ b/example/todo/resolver.go @@ -2,6 +2,7 @@ package todo import ( "context" + "fmt" "flamingo.me/graphql/example/todo/domain" "flamingo.me/graphql/example/todo/infrastructure" @@ -21,7 +22,12 @@ func (r *UserResolver) Inject(todosBackend *infrastructure.TodoService) *UserRes // Todos getter func (r *UserResolver) Todos(ctx context.Context, obj *userDomain.User) ([]*domain.Todo, error) { - return r.todosBackend.Todos(ctx, obj.Name) + todos, err := r.todosBackend.Todos(ctx, obj.Name) + if err != nil { + return nil, fmt.Errorf("can not load todos: %w", err) + } + + return todos, nil } // MutationResolver maps mutations @@ -37,10 +43,20 @@ func (r *MutationResolver) Inject(resolver *UserResolver) *MutationResolver { // TodoAdd mutation func (r *MutationResolver) TodoAdd(ctx context.Context, user string, task string) (*domain.Todo, error) { - return r.resolver.todosBackend.AddTodo(ctx, user, task) + todo, err := r.resolver.todosBackend.AddTodo(ctx, user, task) + if err != nil { + return nil, fmt.Errorf("can not add todo: %w", err) + } + + return todo, nil } // TodoDone mutation func (r *MutationResolver) TodoDone(ctx context.Context, todo string, done bool) (*domain.Todo, error) { - return r.resolver.todosBackend.TodoDone(ctx, todo, done) + todoDone, err := r.resolver.todosBackend.TodoDone(ctx, todo, done) + if err != nil { + return nil, fmt.Errorf("can not update todo: %w", err) + } + + return todoDone, nil } diff --git a/example/user/domain/user.go b/example/user/domain/user.go index 3890482..649d71c 100644 --- a/example/user/domain/user.go +++ b/example/user/domain/user.go @@ -19,16 +19,22 @@ type UserService interface { // Get returns an attribute by its key func (a Attributes) Get(key string) string { - return a[key].(string) + if s, ok := a[key].(string); ok { + return s + } + + return "" } // Keys lists all attribute keys func (a Attributes) Keys() []string { keys := make([]string, len(a)) i := 0 + for k := range a { keys[i] = k i++ } + return keys } diff --git a/example/user/interfaces/graphql/resolver.go b/example/user/interfaces/graphql/resolver.go index 0a96b76..accfdcc 100644 --- a/example/user/interfaces/graphql/resolver.go +++ b/example/user/interfaces/graphql/resolver.go @@ -3,6 +3,7 @@ package graphql import ( "context" "errors" + "fmt" "strings" "flamingo.me/graphql/example/user/domain" @@ -24,7 +25,12 @@ func (r *UserQueryResolver) Inject(userService domain.UserService) *UserQueryRes // User getter func (r *UserQueryResolver) User(ctx context.Context, id string) (*domain.User, error) { - return r.userService.UserByID(ctx, id) + user, err := r.userService.UserByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("can not load user: %w", err) + } + + return user, nil } // UserAttributeFilter directive diff --git a/graphqlhandler.go b/graphqlhandler.go index fc891c1..3bdc70b 100644 --- a/graphqlhandler.go +++ b/graphqlhandler.go @@ -2,6 +2,7 @@ package graphql import ( "context" + "fmt" "io" "net/http" "net/http/httptest" @@ -21,6 +22,7 @@ func wrapGqlHandler(handler http.Handler) web.Action { return func(ctx context.Context, req *web.Request) web.Result { rw := httptest.NewRecorder() handler.ServeHTTP(rw, req.Request().WithContext(ctx)) + return &gqlHandler{ request: req, recorder: rw, @@ -28,12 +30,17 @@ func wrapGqlHandler(handler http.Handler) web.Action { } } -func (h *gqlHandler) Apply(ctx context.Context, rw http.ResponseWriter) error { +func (h *gqlHandler) Apply(_ context.Context, rw http.ResponseWriter) error { for k, vs := range h.recorder.Header() { for _, v := range vs { rw.Header().Add(k, v) } } + _, err := io.Copy(rw, h.recorder.Body) - return err + if err != nil { + return fmt.Errorf("failed to write body: %w", err) + } + + return nil } diff --git a/helper.go b/helper.go index 4d12b98..e3097ff 100644 --- a/helper.go +++ b/helper.go @@ -32,6 +32,7 @@ func (tc *Types) Resolve(graphqlType, graphqlField string, typ interface{}, meth if tc.resolver == nil { tc.resolver = make(map[string]map[string][3]string) } + if tc.resolver[graphqlType] == nil { tc.resolver[graphqlType] = make(map[string][3]string) } @@ -40,6 +41,7 @@ func (tc *Types) Resolve(graphqlType, graphqlField string, typ interface{}, meth for t.Kind() == reflect.Ptr { t = t.Elem() } + tc.resolver[graphqlType][graphqlField] = [3]string{t.PkgPath(), t.Name(), method} } @@ -48,9 +50,11 @@ func (tc *Types) GoField(graphqlType, graphqlField, goField string) { if tc.fields == nil { tc.fields = make(map[string]map[string]string) } + if tc.fields[graphqlType] == nil { tc.fields[graphqlType] = make(map[string]string) } + tc.fields[graphqlType][graphqlField] = goField } diff --git a/marshaller.go b/marshaller.go index 8ba0baa..f52b437 100644 --- a/marshaller.go +++ b/marshaller.go @@ -12,6 +12,8 @@ import ( "github.com/99designs/gqlgen/graphql" ) +var ErrUnexpectedValue = errors.New("unexpected value") + // MarshalFloat for graphql Float scalars to be compatible with big.Float func MarshalFloat(f big.Float) graphql.Marshaler { return graphql.WriterFunc(func(w io.Writer) { @@ -21,31 +23,37 @@ func MarshalFloat(f big.Float) graphql.Marshaler { // UnmarshalFloat for graphql Float scalars to be compatible with big.Float func UnmarshalFloat(v interface{}) (big.Float, error) { - switch v := v.(type) { + switch floatValue := v.(type) { case string: - f, _, err := big.ParseFloat(v, 10, 64, big.ToNearestEven) - if f == nil { - return big.Float{}, err - } - return *f, err + return parseString(floatValue) case int: - return *big.NewFloat(float64(v)), nil + return *big.NewFloat(float64(floatValue)), nil case int64: - return *big.NewFloat(float64(v)), nil + return *big.NewFloat(float64(floatValue)), nil case float64: - return *big.NewFloat(v), nil + return *big.NewFloat(floatValue), nil case json.Number: - f, _, err := big.ParseFloat(string(v), 10, 64, big.ToNearestEven) - if f == nil { - return big.Float{}, err - } - return *f, err + return parseString(string(floatValue)) default: - return big.Float{}, fmt.Errorf("%T is not a float", v) + return big.Float{}, fmt.Errorf("%w: %T is not a float", ErrUnexpectedValue, floatValue) } } -// MarshalDate marshals time.Time inf form of YYYY-MM-DD +//nolint:gomnd // options for float parsing are clear +func parseString(floatValue string) (big.Float, error) { + f, _, err := big.ParseFloat(floatValue, 10, 64, big.ToNearestEven) + if f == nil { + f = &big.Float{} + } + + if err != nil { + return *f, fmt.Errorf("can not parse string %q to float: %w", floatValue, err) + } + + return *f, nil +} + +// MarshalDate marshals time.Time in form of YYYY-MM-DD func MarshalDate(t time.Time) graphql.Marshaler { if t.IsZero() { return graphql.Null @@ -62,7 +70,14 @@ func UnmarshalDate(v interface{}) (time.Time, error) { if len(tmpStr) == 0 { return time.Time{}, nil } - return time.Parse("2006-01-02", tmpStr) + + parse, err := time.Parse(time.DateOnly, tmpStr) + if err != nil { + return time.Time{}, fmt.Errorf("date must be in format %q: %w", time.DateOnly, err) + } + + return parse, nil } - return time.Time{}, errors.New("date should be a string") + + return time.Time{}, fmt.Errorf("%w: date should be a string", ErrUnexpectedValue) } diff --git a/marshaller_test.go b/marshaller_test.go index b9fd9d7..b3e9345 100644 --- a/marshaller_test.go +++ b/marshaller_test.go @@ -1,4 +1,4 @@ -package graphql +package graphql_test import ( "bytes" @@ -6,12 +6,16 @@ import ( "math/big" "testing" "time" + + "flamingo.me/graphql" ) -func TestMarshaller(t *testing.T) { +func TestMarshalFloats(t *testing.T) { + t.Parallel() + i := new(big.Float).SetFloat64(1) - v, err := UnmarshalFloat("1") + v, err := graphql.UnmarshalFloat("1") if err != nil { t.Error(err) } @@ -20,7 +24,7 @@ func TestMarshaller(t *testing.T) { t.Error("string unmatch") } - v, err = UnmarshalFloat(1) + v, err = graphql.UnmarshalFloat(1) if err != nil { t.Error(err) } @@ -29,7 +33,7 @@ func TestMarshaller(t *testing.T) { t.Error("int unmatch") } - v, err = UnmarshalFloat(int64(1)) + v, err = graphql.UnmarshalFloat(int64(1)) if err != nil { t.Error(err) } @@ -38,7 +42,7 @@ func TestMarshaller(t *testing.T) { t.Error("int64 unmatch") } - v, err = UnmarshalFloat(float64(1.0)) + v, err = graphql.UnmarshalFloat(float64(1.0)) if err != nil { t.Error(err) } @@ -47,29 +51,35 @@ func TestMarshaller(t *testing.T) { t.Error("float64 unmatch") } - _, err = UnmarshalFloat("test") + _, err = graphql.UnmarshalFloat("test") if err == nil { t.Error("invalid float error fails") } +} - writer := MarshalDate(time.Time{}) +func TestMarshalDates(t *testing.T) { + t.Parallel() + + writer := graphql.MarshalDate(time.Time{}) b := bytes.NewBufferString("") writer.MarshalGQL(b) + if b.String() != "null" { t.Error("zero date should be null") } now := time.Now() - writer = MarshalDate(now) + writer = graphql.MarshalDate(now) b = bytes.NewBufferString("") writer.MarshalGQL(b) + if b.String() != fmt.Sprintf("%q", now.Format("2006-01-02")) { t.Error("date should be marshalled to format YYYY-MM-DD") } - date, err := UnmarshalDate("2011-08-12") + date, err := graphql.UnmarshalDate("2011-08-12") if err != nil { t.Fatal("Unmarshal of correct date string should work") } @@ -79,17 +89,17 @@ func TestMarshaller(t *testing.T) { t.Error("Umarshalled date is wrong") } - _, err = UnmarshalDate("foobar") + _, err = graphql.UnmarshalDate("foobar") if err == nil { t.Error("Unmarshal of invalid date string should lead to an error") } - _, err = UnmarshalDate(42) + _, err = graphql.UnmarshalDate(42) if err == nil { t.Error("Unmarshal of invalid date type should lead to an error") } - date, err = UnmarshalDate("") + date, err = graphql.UnmarshalDate("") if err != nil { t.Fatal("Unmarshal of empty string should work") } diff --git a/module.go b/module.go index b43decf..bfb0650 100644 --- a/module.go +++ b/module.go @@ -69,12 +69,15 @@ func (r *routes) Inject( } // Routes definition for flamingo router +// +//nolint:gomnd // number usage clear together with the function names func (r *routes) Routes(registry *web.RouterRegistry) { if r.exec == nil { panic("Please register/generate a schema module before running the server!") } var origins []string + err := r.origins.MapInto(&origins) if err != nil { panic(err) @@ -119,6 +122,7 @@ func (r *routes) Routes(registry *web.RouterRegistry) { registry.HandleAny("graphql", wrapGqlHandler(corsHandler.gqlMiddleware(gqlHandler))) registry.MustRoute("/graphql-console", "graphql.console") + u, _ := r.reverseRouter.Relative("graphql", nil) registry.HandleAny("graphql.console", web.WrapHTTPHandler(playground.Handler("Flamingo GraphQL Console", u.String()))) } diff --git a/module_test.go b/module_test.go index c8ab39f..73838f2 100644 --- a/module_test.go +++ b/module_test.go @@ -3,13 +3,14 @@ package graphql_test import ( "testing" - "flamingo.me/dingo" + "flamingo.me/flamingo/v3/framework/config" "flamingo.me/graphql" ) func TestModule_Configure(t *testing.T) { - r := dingo.TryModule(new(graphql.Module)) - if r != nil { - t.Error(r) + t.Parallel() + + if err := config.TryModules(nil, new(graphql.Module)); err != nil { + t.Error(err) } }