├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── examples ├── pubsub │ └── pubsub.go └── simple.go ├── pubsub.go ├── subscription_manager.go └── values_helper.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vendor/ -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/graphql-go/graphql" 6 | packages = [".","gqlerrors","language/ast","language/kinds","language/lexer","language/location","language/parser","language/printer","language/source","language/typeInfo","language/visitor"] 7 | revision = "d114382b5e9f369f253062f18b5033cfff025f82" 8 | version = "v7.1.0" 9 | 10 | [solve-meta] 11 | analyzer-name = "dep" 12 | analyzer-version = 1 13 | inputs-digest = "87c4c3824f95f17cbba4c26801c35975ef1594639f713e0db2f426ff95c2c830" 14 | solver-name = "gps-cdcl" 15 | solver-version = 1 16 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/graphql-go/graphql" 26 | version = "7.1.0" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Niklas Voss 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/pubsub/pubsub.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "errors" 5 | "github.com/trevex/graphql-go-subscription" 6 | ) 7 | 8 | type operation int 9 | 10 | const ( 11 | SUBSCRIBE operation = iota 12 | UNSUBSCRIBE 13 | PUBLISH 14 | SHUTDOWN 15 | ) 16 | 17 | type command struct { 18 | op operation 19 | topic string 20 | ch chan interface{} 21 | payload interface{} 22 | } 23 | 24 | type Subscription struct { 25 | ch chan interface{} 26 | } 27 | 28 | type PubSub struct { 29 | cmds chan command 30 | capacity int 31 | topics map[string]map[chan interface{}]bool 32 | subscribers map[chan interface{}]map[string]bool 33 | } 34 | 35 | func New(capacity int) *PubSub { 36 | ps := &PubSub{ 37 | make(chan command), 38 | capacity, 39 | make(map[string]map[chan interface{}]bool), 40 | make(map[chan interface{}]map[string]bool), 41 | } 42 | go ps.run() 43 | return ps 44 | } 45 | 46 | func (ps *PubSub) Subscribe(topic string, options interface{}, callback func(interface{}) error) (subscription.Subscription, error) { 47 | sub := &Subscription{ 48 | make(chan interface{}, ps.capacity), 49 | } 50 | ps.cmds <- command{op: SUBSCRIBE, topic: topic, ch: sub.ch} 51 | go (func() { 52 | for payload := range sub.ch { 53 | err := callback(payload) 54 | if err != nil { 55 | return 56 | } 57 | } 58 | })() 59 | return sub, nil 60 | } 61 | 62 | func (ps *PubSub) Unsubscribe(sub subscription.Subscription) error { 63 | if s, ok := sub.(*Subscription); ok { 64 | ps.cmds <- command{op: UNSUBSCRIBE, ch: s.ch} 65 | return nil 66 | } 67 | return errors.New("Subscription has wrong type.") 68 | } 69 | 70 | func (ps *PubSub) Publish(topic string, payload interface{}) { 71 | ps.cmds <- command{op: PUBLISH, topic: topic, payload: payload} 72 | } 73 | 74 | func (ps *PubSub) Shutdown() { 75 | ps.cmds <- command{op: SHUTDOWN} 76 | } 77 | 78 | func (ps *PubSub) run() { 79 | for cmd := range ps.cmds { 80 | switch cmd.op { 81 | case SHUTDOWN: 82 | break 83 | case SUBSCRIBE: 84 | ps.subscribe(cmd.topic, cmd.ch) 85 | case UNSUBSCRIBE: 86 | for topic, _ := range ps.subscribers[cmd.ch] { 87 | ps.unsubscribe(topic, cmd.ch) 88 | } 89 | case PUBLISH: 90 | ps.publish(cmd.topic, cmd.payload) 91 | } 92 | } 93 | } 94 | 95 | func (ps *PubSub) subscribe(topic string, ch chan interface{}) { 96 | if _, ok := ps.topics[topic]; !ok { 97 | ps.topics[topic] = make(map[chan interface{}]bool) 98 | } 99 | ps.topics[topic][ch] = true 100 | if _, ok := ps.subscribers[ch]; !ok { 101 | ps.subscribers[ch] = make(map[string]bool) 102 | } 103 | ps.subscribers[ch][topic] = true 104 | } 105 | 106 | func (ps *PubSub) unsubscribe(topic string, ch chan interface{}) { 107 | if _, ok := ps.topics[topic]; !ok { 108 | return 109 | } 110 | if _, ok := ps.topics[topic][ch]; !ok { 111 | return 112 | } 113 | delete(ps.topics[topic], ch) 114 | delete(ps.subscribers[ch], topic) 115 | if len(ps.topics[topic]) == 0 { 116 | delete(ps.topics, topic) 117 | } 118 | if len(ps.subscribers[ch]) == 0 { 119 | close(ch) 120 | delete(ps.subscribers, ch) 121 | } 122 | } 123 | 124 | func (ps *PubSub) publish(topic string, payload interface{}) { 125 | for ch, _ := range ps.topics[topic] { 126 | ch <- payload 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /examples/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/graphql-go/graphql" 7 | "github.com/trevex/graphql-go-subscription" 8 | "github.com/trevex/graphql-go-subscription/examples/pubsub" 9 | "time" 10 | ) 11 | 12 | var messages []string 13 | 14 | var rootQuery = graphql.NewObject(graphql.ObjectConfig{ 15 | Name: "RootQuery", 16 | Fields: graphql.Fields{ 17 | "messages": &graphql.Field{ 18 | Type: graphql.NewList(graphql.String), 19 | }, 20 | }, 21 | }) 22 | 23 | var rootSubscription = graphql.NewObject(graphql.ObjectConfig{ 24 | Name: "RootSubscription", 25 | Fields: graphql.Fields{ 26 | "newMessage": &graphql.Field{ 27 | Type: graphql.String, 28 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 29 | return p.Info.RootValue, nil 30 | }, 31 | }, 32 | }, 33 | }) 34 | 35 | var schema, _ = graphql.NewSchema(graphql.SchemaConfig{ 36 | Query: rootQuery, 37 | Subscription: rootSubscription, 38 | }) 39 | 40 | var ps = pubsub.New(4) 41 | 42 | var subscriptionManager = subscription.NewSubscriptionManager(subscription.SubscriptionManagerConfig{ 43 | Schema: schema, 44 | PubSub: ps, 45 | }) 46 | 47 | func main() { 48 | query := ` 49 | subscription { 50 | newMessage 51 | } 52 | ` 53 | 54 | subId, _ := subscriptionManager.Subscribe(subscription.SubscriptionConfig{ 55 | Query: query, 56 | Callback: func(result *graphql.Result) error { 57 | str, _ := json.Marshal(result) 58 | fmt.Printf("%s", str) 59 | return nil 60 | }, 61 | }) 62 | 63 | // Add a new message 64 | newMsg := "Hello, world!" 65 | // To the store 66 | messages = append(messages, newMsg) 67 | // And additionally publish it as well 68 | ps.Publish("newMessage", newMsg) 69 | 70 | // Dirty way to wait for goroutines 71 | time.Sleep(2 * time.Second) 72 | 73 | // To clean up a subscription unsubscribe 74 | subscriptionManager.Unsubscribe(subId) 75 | 76 | // Shutdown pubsub routines 77 | ps.Shutdown() 78 | } 79 | -------------------------------------------------------------------------------- /pubsub.go: -------------------------------------------------------------------------------- 1 | package subscription 2 | 3 | type Subscription interface{} 4 | 5 | type PubSub interface { 6 | Subscribe(topic string, options interface{}, callback func(interface{}) error) (Subscription, error) 7 | Unsubscribe(sub Subscription) error 8 | } 9 | -------------------------------------------------------------------------------- /subscription_manager.go: -------------------------------------------------------------------------------- 1 | package subscription 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/graphql-go/graphql" 7 | "github.com/graphql-go/graphql/language/ast" 8 | "github.com/graphql-go/graphql/language/parser" 9 | ) 10 | 11 | type FilterFunc func(ctx context.Context, payload interface{}) bool 12 | 13 | type TriggerConfig struct { 14 | Options interface{} 15 | Filter FilterFunc 16 | } 17 | 18 | type TriggerMap map[string]*TriggerConfig 19 | 20 | var defaultTriggerConfig = TriggerConfig{ 21 | Options: nil, 22 | Filter: func(ctx context.Context, payload interface{}) bool { 23 | return true 24 | }, 25 | } 26 | 27 | type SetupFunction func(config *SubscriptionConfig, args map[string]interface{}, subscriptionName string) TriggerMap 28 | 29 | type SetupFunctionMap map[string]SetupFunction 30 | 31 | type SubscriptionId uint64 32 | 33 | type SubscriptionManagerConfig struct { 34 | Schema graphql.Schema 35 | PubSub PubSub 36 | SetupFunctions SetupFunctionMap 37 | } 38 | 39 | type SubscriptionManager struct { 40 | schema graphql.Schema 41 | pubsub PubSub 42 | setupFunctions SetupFunctionMap 43 | subscriptions map[SubscriptionId][]Subscription 44 | maxId SubscriptionId 45 | } 46 | 47 | type SubscriptionConfig struct { 48 | Query string 49 | Context context.Context 50 | VariableValues map[string]interface{} 51 | OperationName string 52 | Callback func(*graphql.Result) error 53 | } 54 | 55 | func NewSubscriptionManager(config SubscriptionManagerConfig) *SubscriptionManager { 56 | sm := &SubscriptionManager{ 57 | config.Schema, 58 | config.PubSub, 59 | config.SetupFunctions, 60 | make(map[SubscriptionId][]Subscription), 61 | 0, 62 | } 63 | if sm.setupFunctions == nil { 64 | sm.setupFunctions = SetupFunctionMap{} 65 | } 66 | return sm 67 | } 68 | 69 | func (sm *SubscriptionManager) Subscribe(config SubscriptionConfig) (SubscriptionId, error) { 70 | if config.VariableValues == nil { 71 | config.VariableValues = make(map[string]interface{}) 72 | } 73 | doc, err := parser.Parse(parser.ParseParams{Source: config.Query}) 74 | if err != nil { 75 | return 0, fmt.Errorf("Failed to parse query: %v", err) 76 | } 77 | result := graphql.ValidateDocument(&sm.schema, doc, graphql.SpecifiedRules) // TODO: add single root subscription rule 78 | if !result.IsValid || len(result.Errors) > 0 { 79 | return 0, fmt.Errorf("Validation failed, errors: %+v", result.Errors) 80 | } 81 | 82 | var subscriptionName string 83 | var args map[string]interface{} 84 | for _, node := range doc.Definitions { 85 | if node.GetKind() == "OperationDefinition" { 86 | def, _ := node.(*ast.OperationDefinition) 87 | rootField, _ := def.GetSelectionSet().Selections[0].(*ast.Field) 88 | subscriptionName = rootField.Name.Value 89 | 90 | fields := sm.schema.SubscriptionType().Fields() 91 | args, err = getArgumentValues(fields[subscriptionName].Args, rootField.Arguments, config.VariableValues) 92 | break 93 | } 94 | } 95 | 96 | var triggerMap TriggerMap 97 | if setupFunc, ok := sm.setupFunctions[subscriptionName]; ok { 98 | triggerMap = setupFunc(&config, args, subscriptionName) 99 | } else { 100 | triggerMap = TriggerMap{ 101 | subscriptionName: &defaultTriggerConfig, 102 | } 103 | } 104 | sm.maxId++ 105 | subscriptionId := sm.maxId 106 | sm.subscriptions[subscriptionId] = []Subscription{} 107 | 108 | for triggerName, triggerConfig := range triggerMap { 109 | sub, err := sm.pubsub.Subscribe(triggerName, triggerConfig.Options, func(payload interface{}) error { 110 | if triggerConfig.Filter(config.Context, payload) { 111 | result := graphql.Execute(graphql.ExecuteParams{ 112 | Schema: sm.schema, 113 | Root: payload, 114 | AST: doc, 115 | OperationName: config.OperationName, 116 | Args: args, 117 | Context: config.Context, 118 | }) 119 | err := config.Callback(result) 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | return nil 125 | }) 126 | if err != nil { 127 | return 0, fmt.Errorf("Subscription of trigger %v failed, error: %v", triggerName, err) 128 | } 129 | sm.subscriptions[subscriptionId] = append(sm.subscriptions[subscriptionId], sub) 130 | } 131 | 132 | return subscriptionId, nil 133 | } 134 | 135 | func (sm *SubscriptionManager) Unsubscribe(id SubscriptionId) { 136 | for _, sub := range sm.subscriptions[id] { 137 | sm.pubsub.Unsubscribe(sub) 138 | } 139 | delete(sm.subscriptions, id) 140 | } 141 | -------------------------------------------------------------------------------- /values_helper.go: -------------------------------------------------------------------------------- 1 | package subscription 2 | 3 | import ( 4 | "github.com/graphql-go/graphql" 5 | "github.com/graphql-go/graphql/language/ast" 6 | "github.com/graphql-go/graphql/language/kinds" 7 | "math" 8 | ) 9 | 10 | // Taken from graphql-go/values.go, because API is not public: 11 | 12 | // The MIT License (MIT) 13 | // 14 | // Copyright (c) 2015 Chris Ramón 15 | // 16 | // Permission is hereby granted, free of charge, to any person obtaining a copy 17 | // of this software and associated documentation files (the "Software"), to deal 18 | // in the Software without restriction, including without limitation the rights 19 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | // copies of the Software, and to permit persons to whom the Software is 21 | // furnished to do so, subject to the following conditions: 22 | // 23 | // The above copyright notice and this permission notice shall be included in all 24 | // copies or substantial portions of the Software. 25 | // 26 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | // SOFTWARE. 33 | 34 | func isNullish(value interface{}) bool { 35 | if value, ok := value.(string); ok { 36 | return value == "" 37 | } 38 | if value, ok := value.(*string); ok { 39 | if value == nil { 40 | return true 41 | } 42 | return *value == "" 43 | } 44 | if value, ok := value.(int); ok { 45 | return math.IsNaN(float64(value)) 46 | } 47 | if value, ok := value.(*int); ok { 48 | if value == nil { 49 | return true 50 | } 51 | return math.IsNaN(float64(*value)) 52 | } 53 | if value, ok := value.(float32); ok { 54 | return math.IsNaN(float64(value)) 55 | } 56 | if value, ok := value.(*float32); ok { 57 | if value == nil { 58 | return true 59 | } 60 | return math.IsNaN(float64(*value)) 61 | } 62 | if value, ok := value.(float64); ok { 63 | return math.IsNaN(value) 64 | } 65 | if value, ok := value.(*float64); ok { 66 | if value == nil { 67 | return true 68 | } 69 | return math.IsNaN(*value) 70 | } 71 | return value == nil 72 | } 73 | 74 | func valueFromAST(valueAST ast.Value, ttype graphql.Input, variables map[string]interface{}) interface{} { 75 | 76 | if ttype, ok := ttype.(*graphql.NonNull); ok { 77 | val := valueFromAST(valueAST, ttype.OfType, variables) 78 | return val 79 | } 80 | 81 | if valueAST == nil { 82 | return nil 83 | } 84 | 85 | if valueAST, ok := valueAST.(*ast.Variable); ok && valueAST.Kind == kinds.Variable { 86 | if valueAST.Name == nil { 87 | return nil 88 | } 89 | if variables == nil { 90 | return nil 91 | } 92 | variableName := valueAST.Name.Value 93 | variableVal, ok := variables[variableName] 94 | if !ok { 95 | return nil 96 | } 97 | return variableVal 98 | } 99 | 100 | if ttype, ok := ttype.(*graphql.List); ok { 101 | itemType := ttype.OfType 102 | if valueAST, ok := valueAST.(*ast.ListValue); ok && valueAST.Kind == kinds.ListValue { 103 | values := []interface{}{} 104 | for _, itemAST := range valueAST.Values { 105 | v := valueFromAST(itemAST, itemType, variables) 106 | values = append(values, v) 107 | } 108 | return values 109 | } 110 | v := valueFromAST(valueAST, itemType, variables) 111 | return []interface{}{v} 112 | } 113 | 114 | if ttype, ok := ttype.(*graphql.InputObject); ok { 115 | valueAST, ok := valueAST.(*ast.ObjectValue) 116 | if !ok { 117 | return nil 118 | } 119 | fieldASTs := map[string]*ast.ObjectField{} 120 | for _, fieldAST := range valueAST.Fields { 121 | if fieldAST.Name == nil { 122 | continue 123 | } 124 | fieldName := fieldAST.Name.Value 125 | fieldASTs[fieldName] = fieldAST 126 | 127 | } 128 | obj := map[string]interface{}{} 129 | for fieldName, field := range ttype.Fields() { 130 | fieldAST, ok := fieldASTs[fieldName] 131 | if !ok || fieldAST == nil { 132 | continue 133 | } 134 | fieldValue := valueFromAST(fieldAST.Value, field.Type, variables) 135 | if isNullish(fieldValue) { 136 | fieldValue = field.DefaultValue 137 | } 138 | if !isNullish(fieldValue) { 139 | obj[fieldName] = fieldValue 140 | } 141 | } 142 | return obj 143 | } 144 | 145 | switch ttype := ttype.(type) { 146 | case *graphql.Scalar: 147 | parsed := ttype.ParseLiteral(valueAST) 148 | if !isNullish(parsed) { 149 | return parsed 150 | } 151 | case *graphql.Enum: 152 | parsed := ttype.ParseLiteral(valueAST) 153 | if !isNullish(parsed) { 154 | return parsed 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | func getArgumentValues(argDefs []*graphql.Argument, argASTs []*ast.Argument, variableVariables map[string]interface{}) (map[string]interface{}, error) { 161 | argASTMap := map[string]*ast.Argument{} 162 | for _, argAST := range argASTs { 163 | if argAST.Name != nil { 164 | argASTMap[argAST.Name.Value] = argAST 165 | } 166 | } 167 | results := map[string]interface{}{} 168 | for _, argDef := range argDefs { 169 | 170 | name := argDef.PrivateName 171 | var valueAST ast.Value 172 | if argAST, ok := argASTMap[name]; ok { 173 | valueAST = argAST.Value 174 | } 175 | value := valueFromAST(valueAST, argDef.Type, variableVariables) 176 | if isNullish(value) { 177 | value = argDef.DefaultValue 178 | } 179 | if !isNullish(value) { 180 | results[name] = value 181 | } 182 | } 183 | return results, nil 184 | } 185 | --------------------------------------------------------------------------------