diff --git a/api/graphql.go b/api/graphql.go index 04ce446..2c54447 100644 --- a/api/graphql.go +++ b/api/graphql.go @@ -2,49 +2,20 @@ package api import ( "github.com/gorilla/mux" - "github.com/graphql-go/graphql" - "github.com/graphql-go/handler" gql "github.com/mylxsw/adanos-alert/api/graphql" + lib "github.com/mylxsw/adanos-alert/pkg/graphql" "github.com/mylxsw/go-toolkit/container" ) func graphqlRouters(cc *container.Container) func(router *mux.Router) { - var defaultField = graphql.Field{ - Type: graphql.String, - Resolve: func(p graphql.ResolveParams) (interface{}, error) { - return "Hello, world", nil - }, - } - - rootMutation := graphql.NewObject(graphql.ObjectConfig{ - Name: "RootMutation", - Fields: graphql.Fields{ - "hello": &defaultField, - }, - }) - rootQuery := graphql.NewObject(graphql.ObjectConfig{ - Name: "RootQuery", - Fields: graphql.Fields{ - "hello": &defaultField, - }, - }) + gqlib := lib.NewBuilder() - gql.NewWelcomeObject().Register(rootQuery, rootMutation) - gql.NewRuleObject(cc).Register(rootQuery, rootMutation) - - schemaConfig := graphql.SchemaConfig{Query: rootQuery, Mutation: rootMutation} - schema, err := graphql.NewSchema(schemaConfig) - if err != nil { - panic(err) - } + gql.NewWelcomeObject().Register(gqlib) + gql.NewRuleObject(cc).Register(gqlib) return func(router *mux.Router) { // router.Handle("/graphql", graphql.HTTPHandler(schema)) - router.Handle("/graphql", handler.New(&handler.Config{ - Schema: &schema, - Pretty: true, - GraphiQL: true, - })) + router.Handle("/graphql", gqlib.Build()) } } diff --git a/api/graphql/graphql.go b/api/graphql/graphql.go index 63a916d..d51474b 100644 --- a/api/graphql/graphql.go +++ b/api/graphql/graphql.go @@ -1,9 +1,9 @@ package graphql import ( - "github.com/graphql-go/graphql" + lib "github.com/mylxsw/adanos-alert/pkg/graphql" ) type Graphql interface { - Register(query *graphql.Object, mutation *graphql.Object) + Register(builder lib.GraphQL) } diff --git a/api/graphql/rule.go b/api/graphql/rule.go index dbc8c10..a4f1fa9 100644 --- a/api/graphql/rule.go +++ b/api/graphql/rule.go @@ -1,6 +1,8 @@ package graphql import ( + "fmt" + "github.com/graphql-go/graphql" "github.com/mylxsw/adanos-alert/api/view" "github.com/mylxsw/adanos-alert/internal/matcher" @@ -11,20 +13,17 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) -var ruleType = graphql.NewObject(graphql.ObjectConfig{ - Name: "Rule", - Fields: graphql.BindFields(view.Rule{}), -}) +var ruleType = gql.Object(view.Rule{}) type RuleObject struct { cc *container.Container } -func (r *RuleObject) Register(query *graphql.Object, mutation *graphql.Object) { +func (r *RuleObject) Register(builder gql.GraphQL) { r.cc.MustResolve(func(ruleRepo repository.RuleRepo) { - query.AddFieldConfig("rules", r.GetAllRules(ruleRepo)) - mutation.AddFieldConfig("addRule", r.AddRule(ruleRepo)) - mutation.AddFieldConfig("deleteRule", r.DeleteRule(ruleRepo)) + builder.Query("rules", r.GetAllRules(ruleRepo)) + builder.Mutation("addRule", r.AddRule(ruleRepo)) + builder.Mutation("deleteRule", r.DeleteRule(ruleRepo)) }) } @@ -35,7 +34,7 @@ func NewRuleObject(cc *container.Container) *RuleObject { func (r *RuleObject) GetAllRules(ruleRepo repository.RuleRepo) *graphql.Field { return gql.CreateField( graphql.NewList(ruleType), - gql.BindArgs(struct{ ID string `json:"id"` }{}, "id"), + gql.BindArgs(struct{ ID string `json:"id"` }{}), func(arg struct{ ID string `json:"id"` }) (interface{}, error) { filter := bson.M{} @@ -88,9 +87,13 @@ func (r *RuleObject) GetAllRules(ruleRepo repository.RuleRepo) *graphql.Field { // } func (r *RuleObject) AddRule(repo repository.RuleRepo) *graphql.Field { + args := gql.BindArgs(view.Rule{}, "name", "description", "interval", "threshold", "priority", "triggers", "rule", "template", "summary_template", "status") + for name, arg := range args { + fmt.Println(name, arg.Type, arg.Type.Name()) + } return gql.CreateField( ruleType, - gql.BindArgs(view.Rule{}, "name", "description", "interval", "threshold", "priority", "rule", "template", "summary_template", "status"), + gql.BindArgs(view.Rule{}, "name", "description", "interval", "threshold", "priority", "triggers", "rule", "template", "summary_template", "status"), func(rule view.Rule) (interface{}, error) { value := view.RuleToRepo(rule) @@ -168,7 +171,7 @@ func (r *RuleObject) AddRule(repo repository.RuleRepo) *graphql.Field { func (r *RuleObject) DeleteRule(repo repository.RuleRepo) *graphql.Field { return gql.CreateField( ruleType, - gql.BindArgs(struct{ ID string `json:"id"` }{}, "id"), + gql.BindArgs(struct{ ID string `json:"id"` }{}), func(arg struct{ ID string `json:"id"` }) (interface{}, error) { id, err := primitive.ObjectIDFromHex(arg.ID) if err != nil { diff --git a/api/graphql/welcome.go b/api/graphql/welcome.go index 87fed40..a213e2d 100644 --- a/api/graphql/welcome.go +++ b/api/graphql/welcome.go @@ -2,6 +2,7 @@ package graphql import ( "github.com/graphql-go/graphql" + lib "github.com/mylxsw/adanos-alert/pkg/graphql" ) type WelcomeObject struct { @@ -12,23 +13,16 @@ func NewWelcomeObject() *WelcomeObject { return &WelcomeObject{} } -func (w *WelcomeObject) Register(query *graphql.Object, mutation *graphql.Object) { - query.AddFieldConfig("welcome", &graphql.Field{ - Type: graphql.NewObject(graphql.ObjectConfig{ - Name: "WelcomeObject", - Fields: graphql.BindFields(WelcomeObject{}), - }), - Args: graphql.FieldConfigArgument{ - "name": &graphql.ArgumentConfig{ - Type: graphql.String, - DefaultValue: "Graphql", - }, - }, - Resolve: w.Hello, - }) +func (w *WelcomeObject) Register(builder lib.GraphQL) { + builder.Query("welcome", w.Hello()) } -func (w *WelcomeObject) Hello(p graphql.ResolveParams) (interface{}, error) { - name := p.Args["name"].(string) - return WelcomeObject{Message: name}, nil +func (w *WelcomeObject) Hello() *graphql.Field { + return lib.CreateField( + lib.Object(w), + lib.BindArgs(struct {Name string `json:"name"`}{}), + func(arg struct{ Name string `json:"name"` }) (interface{}, error) { + return WelcomeObject{Message: arg.Name}, nil + }, + ) } diff --git a/api/view/rule.go b/api/view/rule.go index e5bfaff..7f45332 100644 --- a/api/view/rule.go +++ b/api/view/rule.go @@ -4,6 +4,7 @@ import ( "time" "github.com/mylxsw/adanos-alert/internal/repository" + "github.com/mylxsw/asteria/log" "go.mongodb.org/mongo-driver/bson/primitive" ) @@ -28,15 +29,34 @@ type Rule struct { Threshold int64 `json:"threshold"` Priority int64 `json:"priority"` - Rule string `json:"rule"` - Template string `json:"template"` - SummaryTemplate string `json:"summary_template"` - Triggers []Trigger `json:"triggers"` + Rule string `json:"rule"` + Template string `json:"template"` + SummaryTemplate string `json:"summary_template"` + Triggers []*Trigger `json:"triggers"` Status repository.RuleStatus `json:"status"` - CreatedAt time.Time `json:"-"` - UpdatedAt time.Time `json:"-"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + CreatedTime time.Time `json:"created_time"` +} + +func (r Rule) TriggersResolver() ([]Trigger, error) { + return []Trigger{ + { + ID: "000001", + PreCondition: "a == b", + }, + { + ID: "000002", + PreCondition: "a == b and c == d", + }, + }, nil +} + +func (r Rule) CreatedTimeResolver() (string, error) { + log.Info("query created_time") + return r.CreatedTime.Format(time.RFC3339), nil } func RuleFromRepo(r repository.Rule) Rule { @@ -52,8 +72,9 @@ func RuleFromRepo(r repository.Rule) Rule { SummaryTemplate: r.SummaryTemplate, Triggers: TriggersFromRepos(r.Triggers), Status: r.Status, - CreatedAt: r.CreatedAt, - UpdatedAt: r.UpdatedAt, + CreatedAt: r.CreatedAt.Format(time.RFC3339), + UpdatedAt: r.UpdatedAt.Format(time.RFC3339), + CreatedTime: r.CreatedAt, } } @@ -68,6 +89,8 @@ func RulesFromRepos(rs []repository.Rule) []Rule { func RuleToRepo(r Rule) repository.Rule { id, _ := primitive.ObjectIDFromHex(r.ID) + createdAt, _ := time.Parse(time.RFC3339, r.CreatedAt) + updatedAt, _ := time.Parse(time.RFC3339, r.UpdatedAt) return repository.Rule{ ID: id, Name: r.Name, @@ -80,13 +103,13 @@ func RuleToRepo(r Rule) repository.Rule { SummaryTemplate: r.SummaryTemplate, Triggers: TriggersToRepo(r.Triggers), Status: r.Status, - CreatedAt: r.CreatedAt, - UpdatedAt: r.UpdatedAt, + CreatedAt: createdAt, + UpdatedAt: updatedAt, } } -func TriggerFromRepo(tr repository.Trigger) Trigger { - return Trigger{ +func TriggerFromRepo(tr repository.Trigger) *Trigger { + return &Trigger{ ID: tr.ID.Hex(), PreCondition: tr.PreCondition, Action: tr.Action, @@ -97,8 +120,8 @@ func TriggerFromRepo(tr repository.Trigger) Trigger { } } -func TriggersFromRepos(trs []repository.Trigger) []Trigger { - triggers := make([]Trigger, len(trs)) +func TriggersFromRepos(trs []repository.Trigger) []*Trigger { + triggers := make([]*Trigger, len(trs)) for i, tr := range trs { triggers[i] = TriggerFromRepo(tr) } @@ -119,10 +142,10 @@ func TriggerToRepo(tr Trigger) repository.Trigger { } } -func TriggersToRepo(trs []Trigger) []repository.Trigger { +func TriggersToRepo(trs []*Trigger) []repository.Trigger { var triggers = make([]repository.Trigger, len(trs)) for i, t := range trs { - triggers[i] = TriggerToRepo(t) + triggers[i] = TriggerToRepo(*t) } return triggers diff --git a/pkg/graphql/graphql.go b/pkg/graphql/graphql.go index 7fa47cc..bcf501b 100644 --- a/pkg/graphql/graphql.go +++ b/pkg/graphql/graphql.go @@ -1,13 +1,75 @@ package graphql import ( + "fmt" "reflect" "strings" "github.com/graphql-go/graphql" + "github.com/graphql-go/handler" ) -// CreateField create a GraphQL field +var defaultField = graphql.Fields{ + "hello": &graphql.Field{ + Type: graphql.String, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return "Hello, world", nil + }, + }, +} + +type GraphQL interface { + Query(name string, f *graphql.Field) + Mutation(name string, f *graphql.Field) + Build() *handler.Handler +} + +type Builder struct { + query *graphql.Object + mutation *graphql.Object +} + +// NewBuilder create a new Builder instance +func NewBuilder() GraphQL { + rootMutation := graphql.NewObject(graphql.ObjectConfig{ + Name: "Mutation", + Fields: defaultField, + }) + + rootQuery := graphql.NewObject(graphql.ObjectConfig{ + Name: "Query", + Fields: defaultField, + }) + + return &Builder{ + query: rootQuery, + mutation: rootMutation, + } +} + +func (gq *Builder) Query(name string, f *graphql.Field) { + gq.query.AddFieldConfig(name, f) +} + +func (gq *Builder) Mutation(name string, f *graphql.Field) { + gq.mutation.AddFieldConfig(name, f) +} + +func (gq *Builder) Build() *handler.Handler { + schemaConfig := graphql.SchemaConfig{Query: gq.query, Mutation: gq.mutation} + schema, err := graphql.NewSchema(schemaConfig) + if err != nil { + panic(err) + } + + return handler.New(&handler.Config{ + Schema: &schema, + Pretty: true, + GraphiQL: true, + }) +} + +// CreateField create a GraphQLBuilder field // resolver: func (obj T) (interface{}, error) func CreateField(typ graphql.Output, args graphql.FieldConfigArgument, resolver interface{}) *graphql.Field { resolverRefVal := reflect.ValueOf(resolver) @@ -36,7 +98,19 @@ func CreateField(typ graphql.Output, args graphql.FieldConfigArgument, resolver continue } - argRefVal.Elem().Field(i).Set(reflect.ValueOf(p.Args[tag])) + fmt.Println(p.Args[tag]) + + switch argRefVal.Elem().Field(i).Kind() { + case reflect.Slice: + s := reflect.MakeSlice(field.Type, 0, 0) + for _, e := range p.Args[tag].([]interface{}) { + s = reflect.AppendSlice(s, reflect.Indirect(reflect.ValueOf(e))) + } + + argRefVal.Elem().Field(i).Set(s) + default: + argRefVal.Elem().Field(i).Set(reflect.ValueOf(p.Args[tag])) + } } res := resolverRefVal.Call([]reflect.Value{argRefVal.Elem()}) @@ -49,9 +123,68 @@ func CreateField(typ graphql.Output, args graphql.FieldConfigArgument, resolver } } +// Object create a new graphql.Object +func Object(obj interface{}) *graphql.Object { + ref := reflect.TypeOf(obj) + var name string + if ref.Kind() == reflect.Ptr { + name = ref.Elem().Name() + } else { + name = ref.Name() + } + + return graphql.NewObject(graphql.ObjectConfig{ + Name: name, + Fields: BindFields(obj), + }) +} + // BindArgs create a FieldConfigArgument from object func BindArgs(obj interface{}, tags ...string) graphql.FieldConfigArgument { - return graphql.BindArg(obj, tags...) + if len(tags) == 0 { + objRef := reflect.TypeOf(obj) + for i := 0; i < objRef.NumField(); i++ { + tag := extractTag(objRef.Field(i).Tag) + if tag == "" || tag == "-" { + continue + } + + tags = append(tags, tag) + } + } + + return BindArg(obj, tags...) +} + +// lazy way of binding args +func BindArg(obj interface{}, tags ...string) graphql.FieldConfigArgument { + v := reflect.Indirect(reflect.ValueOf(obj)) + var config = make(graphql.FieldConfigArgument) + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i) + + mytag := extractTag(field.Tag) + if inArray(tags, mytag) { + config[mytag] = &graphql.ArgumentConfig{ + Type: getGraphType(field.Type), + } + } + } + return config +} + +func inArray(slice interface{}, item interface{}) bool { + s := reflect.ValueOf(slice) + if s.Kind() != reflect.Slice { + panic("inArray() given a non-slice type") + } + + for i := 0; i < s.Len(); i++ { + if reflect.DeepEqual(item, s.Index(i).Interface()) { + return true + } + } + return false } // ParseResolveParams parse ResolveParams to object @@ -81,3 +214,198 @@ func extractTag(tag reflect.StructTag) string { } return t } + +// can't take recursive slice type +// e.g +// type Person struct{ +// Friends []Person +// } +// it will throw panic stack-overflow +func BindFields(obj interface{}) graphql.Fields { + t := reflect.TypeOf(obj) + v := reflect.ValueOf(obj) + fields := make(map[string]*graphql.Field) + + if t.Kind() == reflect.Ptr { + t = t.Elem() + v = v.Elem() + } + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + tag := extractTag(field.Tag) + if tag == "-" || tag == "" { + continue + } + + fieldType := field.Type + + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + + var graphType graphql.Output + var graphArgs graphql.FieldConfigArgument + + // check whether there has a XxxResolver function + resolverName := field.Name + "Resolver" + method, ok := t.MethodByName(resolverName) + if ok { + ft := method.Func.Type() + if ft.NumOut() != 2 { + panic(fmt.Sprintf("resolver %s.%s must have two return value", t.Name(), resolverName)) + } + + // Parse return value + out0 := ft.Out(0) + if out0.Kind() == reflect.Struct { + structFields := BindFields(reflect.New(out0).Interface()) + graphType = graphql.NewObject(graphql.ObjectConfig{ + Name: tag, + Fields: structFields, + }) + } else { + graphType = getGraphType(out0) + } + + // Parse input args + if ft.NumIn() > 2 { + panic(fmt.Errorf("resolver %s.%s must have no more than 1 argument", t.Name(), resolverName)) + } + if ft.NumIn() == 2 { + arg0 := ft.In(1) + graphArgs = BindArgs(reflect.New(arg0).Interface()) + } + + fields[tag] = &graphql.Field{ + Type: graphType, + Args: graphArgs, + Resolve: func(p graphql.ResolveParams) (i interface{}, e error) { + var args = make([]reflect.Value, 0) + args = append(args, reflect.ValueOf(p.Source)) + + if ft.NumIn() > 1 { + argRefVal := reflect.New(ft.In(1)) + for i := 0; i < ft.In(1).NumField(); i++ { + field := ft.In(1).Field(i) + tag := extractTag(field.Tag) + if tag == "" || tag == "-" { + continue + } + + if p.Args[tag] == nil { + continue + } + + argRefVal.Elem().Field(i).Set(reflect.ValueOf(p.Args[tag])) + } + + args = append(args, argRefVal.Elem()) + } + + res := method.Func.Call(args) + if res[1].IsNil() { + return res[0].Interface(), nil + } + + return res[0].Interface(), res[1].Interface().(error) + }, + } + } else { + if fieldType.Kind() == reflect.Struct { + structFields := BindFields(v.Field(i).Interface()) + graphType = graphql.NewObject(graphql.ObjectConfig{ + Name: tag, + Fields: structFields, + }) + } else { + graphType = getGraphType(fieldType) + } + + fields[tag] = &graphql.Field{ + Type: graphType, + Args: graphArgs, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return extractValue(tag, p.Source), nil + }, + } + } + } + + return fields +} + +func getGraphType(tipe reflect.Type) graphql.Output { + kind := tipe.Kind() + switch kind { + case reflect.String: + return graphql.String + case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64: + return graphql.Int + case reflect.Float32, reflect.Float64: + return graphql.Float + case reflect.Bool: + return graphql.Boolean + case reflect.Slice: + return getGraphList(tipe) + case reflect.Struct: + return getGraphList(tipe) + case reflect.Ptr: + return getGraphList(tipe.Elem()) + } + + return graphql.String +} + +func getGraphList(tipe reflect.Type) *graphql.List { + if tipe.Kind() == reflect.Slice { + switch tipe.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64: + return graphql.NewList(graphql.Int) + case reflect.Bool: + return graphql.NewList(graphql.Boolean) + case reflect.Float32, reflect.Float64: + return graphql.NewList(graphql.Float) + case reflect.String: + return graphql.NewList(graphql.String) + } + } + // finally bind object + t := reflect.New(tipe.Elem()) + name := strings.Replace(fmt.Sprint(tipe.Elem()), ".", "_", -1) + obj := graphql.NewObject(graphql.ObjectConfig{ + Name: name, + Fields: BindFields(t.Elem().Interface()), + }) + + fmt.Println(name) + + return graphql.NewList(obj) +} + +func appendFields(dest, origin graphql.Fields) graphql.Fields { + for key, value := range origin { + dest[key] = value + } + return dest +} + +func extractValue(originTag string, obj interface{}) interface{} { + val := reflect.Indirect(reflect.ValueOf(obj)) + + for j := 0; j < val.NumField(); j++ { + field := val.Type().Field(j) + if field.Type.Kind() == reflect.Struct { + res := extractValue(originTag, val.Field(j).Interface()) + if res != nil { + return res + } + } + + if originTag == extractTag(field.Tag) { + return reflect.Indirect(val.Field(j)).Interface() + } + } + return nil +} diff --git a/pkg/graphql/scalar.go b/pkg/graphql/scalar.go index 8eb7edd..377e3c5 100644 --- a/pkg/graphql/scalar.go +++ b/pkg/graphql/scalar.go @@ -14,11 +14,11 @@ var Int64 = graphql.NewScalar(graphql.ScalarConfig{ Serialize: func(value interface{}) interface{} { return value.(int64) }, - // ParseValue parses GraphQL variables from `int64` to `Int64` + // ParseValue parses GraphQLBuilder variables from `int64` to `Int64` ParseValue: func(value interface{}) interface{} { return value.(int64) }, - // ParseLiteral parses GraphQL AST value to `Int64` + // ParseLiteral parses GraphQLBuilder AST value to `Int64` ParseLiteral: func(valueAST ast.Value) interface{} { switch valueAST := valueAST.(type) { case *ast.IntValue: