diff --git a/example/chat/.gitignore b/example/chat/.gitignore new file mode 100644 index 0000000000..d30f40ef44 --- /dev/null +++ b/example/chat/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/example/chat/generated.go b/example/chat/generated.go new file mode 100644 index 0000000000..e5cc874710 --- /dev/null +++ b/example/chat/generated.go @@ -0,0 +1,964 @@ +// This file was generated by github.com/vektah/gqlgen, DO NOT EDIT + +package chat + +import ( + context "context" + fmt "fmt" + strconv "strconv" + sync "sync" + time "time" + + graphql "github.com/vektah/gqlgen/graphql" + errors "github.com/vektah/gqlgen/neelance/errors" + introspection "github.com/vektah/gqlgen/neelance/introspection" + query "github.com/vektah/gqlgen/neelance/query" + schema "github.com/vektah/gqlgen/neelance/schema" +) + +type Resolvers interface { + Mutation_post(ctx context.Context, text string, username string, roomName string) (Message, error) + Query_room(ctx context.Context, name string) (*Chatroom, error) + + Subscription_messageAdded(ctx context.Context, roomName string) (<-chan Message, error) +} + +func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { + return &executableSchema{resolvers} +} + +type executableSchema struct { + resolvers Resolvers +} + +func (e *executableSchema) Schema() *schema.Schema { + return parsedSchema +} + +func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { + ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} + + data := ec._query(op.Selections, nil) + ec.wg.Wait() + + return &graphql.Response{ + Data: data, + Errors: ec.Errors, + } +} + +func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { + ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} + + data := ec._mutation(op.Selections, nil) + ec.wg.Wait() + + return &graphql.Response{ + Data: data, + Errors: ec.Errors, + } +} + +func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) <-chan *graphql.Response { + events := make(chan *graphql.Response, 10) + + ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} + + eventData := ec._subscription(op.Selections, nil) + if ec.Errors != nil { + events <- &graphql.Response{ + Data: graphql.Null, + Errors: ec.Errors, + } + close(events) + } else { + go func() { + for data := range eventData { + ec.wg.Wait() + events <- &graphql.Response{ + Data: data, + Errors: ec.Errors, + } + time.Sleep(20 * time.Millisecond) + } + }() + } + return events +} + +type executionContext struct { + errors.Builder + resolvers Resolvers + variables map[string]interface{} + doc *query.Document + ctx context.Context + wg sync.WaitGroup +} + +var chatroomImplementors = []string{"Chatroom"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) _chatroom(sel []query.Selection, it *Chatroom) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, chatroomImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Chatroom") + case "id": + badArgs := false + if badArgs { + continue + } + res := it.ID() + + out.Values[i] = graphql.MarshalID(res) + case "name": + badArgs := false + if badArgs { + continue + } + res := it.Name + + out.Values[i] = graphql.MarshalString(res) + case "messages": + badArgs := false + if badArgs { + continue + } + res := it.Messages + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + tmp1 = ec._message(field.Selections, &res[idx1]) + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var messageImplementors = []string{"Message"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) _message(sel []query.Selection, it *Message) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, messageImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Message") + case "id": + badArgs := false + if badArgs { + continue + } + res := it.ID + + out.Values[i] = graphql.MarshalID(res) + case "text": + badArgs := false + if badArgs { + continue + } + res := it.Text + + out.Values[i] = graphql.MarshalString(res) + case "createdBy": + badArgs := false + if badArgs { + continue + } + res := it.CreatedBy + + out.Values[i] = graphql.MarshalString(res) + case "createdAt": + badArgs := false + if badArgs { + continue + } + res := it.CreatedAt + + out.Values[i] = graphql.MarshalTime(res) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var mutationImplementors = []string{"Mutation"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) _mutation(sel []query.Selection, it *interface{}) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, mutationImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Mutation") + case "post": + badArgs := false + var arg0 string + if tmp, ok := field.Args["text"]; ok { + tmp2, err := graphql.UnmarshalString(tmp) + if err != nil { + badArgs = true + } + arg0 = tmp2 + } + var arg1 string + if tmp, ok := field.Args["username"]; ok { + tmp2, err := graphql.UnmarshalString(tmp) + if err != nil { + badArgs = true + } + arg1 = tmp2 + } + var arg2 string + if tmp, ok := field.Args["roomName"]; ok { + tmp2, err := graphql.UnmarshalString(tmp) + if err != nil { + badArgs = true + } + arg2 = tmp2 + } + if badArgs { + continue + } + res, err := ec.resolvers.Mutation_post(ec.ctx, arg0, arg1, arg2) + if err != nil { + ec.Error(err) + continue + } + + out.Values[i] = ec._message(field.Selections, &res) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var queryImplementors = []string{"Query"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) _query(sel []query.Selection, it *interface{}) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, queryImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Query") + case "room": + badArgs := false + var arg0 string + if tmp, ok := field.Args["name"]; ok { + tmp2, err := graphql.UnmarshalString(tmp) + if err != nil { + badArgs = true + } + arg0 = tmp2 + } + if badArgs { + continue + } + ec.wg.Add(1) + go func(i int, field graphql.CollectedField) { + defer ec.wg.Done() + res, err := ec.resolvers.Query_room(ec.ctx, arg0) + if err != nil { + ec.Error(err) + return + } + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = ec._chatroom(field.Selections, res) + } + }(i, field) + case "__schema": + badArgs := false + if badArgs { + continue + } + res := ec.introspectSchema() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = ec.___Schema(field.Selections, res) + } + case "__type": + badArgs := false + var arg0 string + if tmp, ok := field.Args["name"]; ok { + tmp2, err := graphql.UnmarshalString(tmp) + if err != nil { + badArgs = true + } + arg0 = tmp2 + } + if badArgs { + continue + } + res := ec.introspectType(arg0) + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = ec.___Type(field.Selections, res) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var subscriptionImplementors = []string{"Subscription"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) _subscription(sel []query.Selection, it *interface{}) <-chan graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, subscriptionImplementors, ec.variables) + + if len(fields) != 1 { + fmt.Errorf("must subscribe to exactly one stream") + return nil + } + + var field = fields[0] + channel := make(chan graphql.Marshaler, 1) + switch field.Name { + case "messageAdded": + badArgs := false + var arg0 string + if tmp, ok := field.Args["roomName"]; ok { + tmp2, err := graphql.UnmarshalString(tmp) + if err != nil { + badArgs = true + } + arg0 = tmp2 + } + if badArgs { + return nil + } + results, err := ec.resolvers.Subscription_messageAdded(ec.ctx, arg0) + if err != nil { + ec.Error(err) + return nil + } + + go func() { + for res := range results { + var out graphql.OrderedMap + var messageRes graphql.Marshaler + messageRes = ec._message(field.Selections, &res) + out.Add(field.Alias, messageRes) + channel <- &out + } + }() + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + + return channel +} + +var __DirectiveImplementors = []string{"__Directive"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) ___Directive(sel []query.Selection, it *introspection.Directive) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, __DirectiveImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Directive") + case "name": + badArgs := false + if badArgs { + continue + } + res := it.Name() + + out.Values[i] = graphql.MarshalString(res) + case "description": + badArgs := false + if badArgs { + continue + } + res := it.Description() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = graphql.MarshalString(*res) + } + case "locations": + badArgs := false + if badArgs { + continue + } + res := it.Locations() + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + tmp1 = graphql.MarshalString(res[idx1]) + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + case "args": + badArgs := false + if badArgs { + continue + } + res := it.Args() + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + + if res[idx1] == nil { + tmp1 = graphql.Null + } else { + tmp1 = ec.___InputValue(field.Selections, res[idx1]) + } + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var __EnumValueImplementors = []string{"__EnumValue"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) ___EnumValue(sel []query.Selection, it *introspection.EnumValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, __EnumValueImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__EnumValue") + case "name": + badArgs := false + if badArgs { + continue + } + res := it.Name() + + out.Values[i] = graphql.MarshalString(res) + case "description": + badArgs := false + if badArgs { + continue + } + res := it.Description() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = graphql.MarshalString(*res) + } + case "isDeprecated": + badArgs := false + if badArgs { + continue + } + res := it.IsDeprecated() + + out.Values[i] = graphql.MarshalBoolean(res) + case "deprecationReason": + badArgs := false + if badArgs { + continue + } + res := it.DeprecationReason() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = graphql.MarshalString(*res) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var __FieldImplementors = []string{"__Field"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) ___Field(sel []query.Selection, it *introspection.Field) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, __FieldImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Field") + case "name": + badArgs := false + if badArgs { + continue + } + res := it.Name() + + out.Values[i] = graphql.MarshalString(res) + case "description": + badArgs := false + if badArgs { + continue + } + res := it.Description() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = graphql.MarshalString(*res) + } + case "args": + badArgs := false + if badArgs { + continue + } + res := it.Args() + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + + if res[idx1] == nil { + tmp1 = graphql.Null + } else { + tmp1 = ec.___InputValue(field.Selections, res[idx1]) + } + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + case "type": + badArgs := false + if badArgs { + continue + } + res := it.Type() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = ec.___Type(field.Selections, res) + } + case "isDeprecated": + badArgs := false + if badArgs { + continue + } + res := it.IsDeprecated() + + out.Values[i] = graphql.MarshalBoolean(res) + case "deprecationReason": + badArgs := false + if badArgs { + continue + } + res := it.DeprecationReason() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = graphql.MarshalString(*res) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var __InputValueImplementors = []string{"__InputValue"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) ___InputValue(sel []query.Selection, it *introspection.InputValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, __InputValueImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__InputValue") + case "name": + badArgs := false + if badArgs { + continue + } + res := it.Name() + + out.Values[i] = graphql.MarshalString(res) + case "description": + badArgs := false + if badArgs { + continue + } + res := it.Description() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = graphql.MarshalString(*res) + } + case "type": + badArgs := false + if badArgs { + continue + } + res := it.Type() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = ec.___Type(field.Selections, res) + } + case "defaultValue": + badArgs := false + if badArgs { + continue + } + res := it.DefaultValue() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = graphql.MarshalString(*res) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var __SchemaImplementors = []string{"__Schema"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) ___Schema(sel []query.Selection, it *introspection.Schema) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, __SchemaImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Schema") + case "types": + badArgs := false + if badArgs { + continue + } + res := it.Types() + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + + if res[idx1] == nil { + tmp1 = graphql.Null + } else { + tmp1 = ec.___Type(field.Selections, res[idx1]) + } + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + case "queryType": + badArgs := false + if badArgs { + continue + } + res := it.QueryType() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = ec.___Type(field.Selections, res) + } + case "mutationType": + badArgs := false + if badArgs { + continue + } + res := it.MutationType() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = ec.___Type(field.Selections, res) + } + case "subscriptionType": + badArgs := false + if badArgs { + continue + } + res := it.SubscriptionType() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = ec.___Type(field.Selections, res) + } + case "directives": + badArgs := false + if badArgs { + continue + } + res := it.Directives() + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + + if res[idx1] == nil { + tmp1 = graphql.Null + } else { + tmp1 = ec.___Directive(field.Selections, res[idx1]) + } + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var __TypeImplementors = []string{"__Type"} + +// nolint: gocyclo, errcheck, gas, goconst +func (ec *executionContext) ___Type(sel []query.Selection, it *introspection.Type) graphql.Marshaler { + fields := graphql.CollectFields(ec.doc, sel, __TypeImplementors, ec.variables) + out := graphql.NewOrderedMap(len(fields)) + for i, field := range fields { + out.Keys[i] = field.Alias + out.Values[i] = graphql.Null + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Type") + case "kind": + badArgs := false + if badArgs { + continue + } + res := it.Kind() + + out.Values[i] = graphql.MarshalString(res) + case "name": + badArgs := false + if badArgs { + continue + } + res := it.Name() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = graphql.MarshalString(*res) + } + case "description": + badArgs := false + if badArgs { + continue + } + res := it.Description() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = graphql.MarshalString(*res) + } + case "fields": + badArgs := false + var arg0 bool + if tmp, ok := field.Args["includeDeprecated"]; ok { + tmp2, err := graphql.UnmarshalBoolean(tmp) + if err != nil { + badArgs = true + } + arg0 = tmp2 + } + if badArgs { + continue + } + res := it.Fields(arg0) + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + + if res[idx1] == nil { + tmp1 = graphql.Null + } else { + tmp1 = ec.___Field(field.Selections, res[idx1]) + } + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + case "interfaces": + badArgs := false + if badArgs { + continue + } + res := it.Interfaces() + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + + if res[idx1] == nil { + tmp1 = graphql.Null + } else { + tmp1 = ec.___Type(field.Selections, res[idx1]) + } + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + case "possibleTypes": + badArgs := false + if badArgs { + continue + } + res := it.PossibleTypes() + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + + if res[idx1] == nil { + tmp1 = graphql.Null + } else { + tmp1 = ec.___Type(field.Selections, res[idx1]) + } + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + case "enumValues": + badArgs := false + var arg0 bool + if tmp, ok := field.Args["includeDeprecated"]; ok { + tmp2, err := graphql.UnmarshalBoolean(tmp) + if err != nil { + badArgs = true + } + arg0 = tmp2 + } + if badArgs { + continue + } + res := it.EnumValues(arg0) + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + + if res[idx1] == nil { + tmp1 = graphql.Null + } else { + tmp1 = ec.___EnumValue(field.Selections, res[idx1]) + } + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + case "inputFields": + badArgs := false + if badArgs { + continue + } + res := it.InputFields() + + arr1 := graphql.Array{} + for idx1 := range res { + var tmp1 graphql.Marshaler + + if res[idx1] == nil { + tmp1 = graphql.Null + } else { + tmp1 = ec.___InputValue(field.Selections, res[idx1]) + } + arr1 = append(arr1, tmp1) + } + out.Values[i] = arr1 + case "ofType": + badArgs := false + if badArgs { + continue + } + res := it.OfType() + + if res == nil { + out.Values[i] = graphql.Null + } else { + out.Values[i] = ec.___Type(field.Selections, res) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + + return out +} + +var parsedSchema = schema.MustParse("type Chatroom {\n id: ID!\n name: String!\n messages: [Message!]!\n}\n\ntype Message {\n id: ID!\n text: String!\n createdBy: String!\n createdAt: Time!\n}\n\ntype Query {\n room(name:String!): Chatroom\n}\n\ntype Mutation {\n post(text: String!, username: String!, roomName: String!): Message!\n}\n\ntype Subscription {\n messageAdded(roomName: String!): Message!\n}\n\nschema {\n query: Query\n mutation: Mutation\n subscription: Subscription\n}\n\nscalar Time\n") + +func (ec *executionContext) introspectSchema() *introspection.Schema { + return introspection.WrapSchema(parsedSchema) +} + +func (ec *executionContext) introspectType(name string) *introspection.Type { + t := parsedSchema.Resolve(name) + if t == nil { + return nil + } + return introspection.WrapType(t) +} diff --git a/example/chat/models.go b/example/chat/models.go new file mode 100644 index 0000000000..9397a0cfcd --- /dev/null +++ b/example/chat/models.go @@ -0,0 +1,22 @@ +package chat + +import ( + "sync" + "time" +) + +type Chatroom struct { + Name string + Messages []Message + Observers map[string]chan Message + mu sync.Mutex +} + +func (c Chatroom) ID() string { return "C" + c.Name } + +type Message struct { + ID string + Text string + CreatedBy string + CreatedAt time.Time +} diff --git a/example/chat/package.json b/example/chat/package.json new file mode 100644 index 0000000000..6b3c0346d9 --- /dev/null +++ b/example/chat/package.json @@ -0,0 +1,20 @@ +{ + "name": "chat", + "version": "0.1.0", + "private": true, + "dependencies": { + "apollo-cache-inmemory": "^1.1.9", + "apollo-client": "^2.2.5", + "react": "^16.2.0", + "react-apollo": "^2.1.0-beta.2", + "react-dom": "^16.2.0", + "react-scripts": "1.1.1", + "subscriptions-transport-ws": "^0.9.5" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} diff --git a/example/chat/public/index.html b/example/chat/public/index.html new file mode 100644 index 0000000000..831ac82599 --- /dev/null +++ b/example/chat/public/index.html @@ -0,0 +1,15 @@ + + + + + + + React App + + + +
+ + diff --git a/example/chat/readme.md b/example/chat/readme.md new file mode 100644 index 0000000000..be1925cbb4 --- /dev/null +++ b/example/chat/readme.md @@ -0,0 +1,15 @@ +### chat app + +Example app using subscriptions to build a chat room. + +to run this server +```bash +go run ./example/chat/server/server.go +``` + +to run the react app +```bash +cd ./example/chat +npm install +npm run start +``` diff --git a/example/chat/resolvers.go b/example/chat/resolvers.go new file mode 100644 index 0000000000..9fee0076bb --- /dev/null +++ b/example/chat/resolvers.go @@ -0,0 +1,86 @@ +//go:generate gorunpkg github.com/vektah/gqlgen -out generated.go + +package chat + +import ( + context "context" + "math/rand" + "time" +) + +type resolvers struct { + Rooms map[string]*Chatroom +} + +func New() *resolvers { + return &resolvers{ + Rooms: map[string]*Chatroom{}, + } +} + +func (r *resolvers) Mutation_post(ctx context.Context, text string, userName string, roomName string) (Message, error) { + room := r.Rooms[roomName] + if room == nil { + room = &Chatroom{Name: roomName, Observers: map[string]chan Message{}} + r.Rooms[roomName] = room + } + + message := Message{ + ID: randString(8), + CreatedAt: time.Now(), + Text: text, + CreatedBy: userName, + } + + room.Messages = append(room.Messages, message) + room.mu.Lock() + for _, observer := range room.Observers { + observer <- message + } + room.mu.Unlock() + return message, nil +} + +func (r *resolvers) Query_room(ctx context.Context, name string) (*Chatroom, error) { + room := r.Rooms[name] + if room == nil { + room = &Chatroom{Name: name, Observers: map[string]chan Message{}} + r.Rooms[name] = room + } + + return room, nil +} + +func (r *resolvers) Subscription_messageAdded(ctx context.Context, roomName string) (<-chan Message, error) { + room := r.Rooms[roomName] + if room == nil { + room = &Chatroom{Name: roomName, Observers: map[string]chan Message{}} + r.Rooms[roomName] = room + } + + id := randString(8) + events := make(chan Message, 1) + + go func() { + <-ctx.Done() + room.mu.Lock() + delete(room.Observers, id) + room.mu.Unlock() + }() + + room.mu.Lock() + room.Observers[id] = events + room.mu.Unlock() + + return events, nil +} + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func randString(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} diff --git a/example/chat/schema.graphql b/example/chat/schema.graphql new file mode 100644 index 0000000000..3507aa503a --- /dev/null +++ b/example/chat/schema.graphql @@ -0,0 +1,32 @@ +type Chatroom { + id: ID! + name: String! + messages: [Message!]! +} + +type Message { + id: ID! + text: String! + createdBy: String! + createdAt: Time! +} + +type Query { + room(name:String!): Chatroom +} + +type Mutation { + post(text: String!, username: String!, roomName: String!): Message! +} + +type Subscription { + messageAdded(roomName: String!): Message! +} + +schema { + query: Query + mutation: Mutation + subscription: Subscription +} + +scalar Time diff --git a/example/chat/server/server.go b/example/chat/server/server.go new file mode 100644 index 0000000000..2bab154b85 --- /dev/null +++ b/example/chat/server/server.go @@ -0,0 +1,22 @@ +package main + +import ( + "log" + "net/http" + + "github.com/gorilla/websocket" + "github.com/vektah/gqlgen/example/chat" + "github.com/vektah/gqlgen/handler" +) + +func main() { + http.Handle("/", handler.Playground("Todo", "/query")) + http.Handle("/query", handler.GraphQL(chat.MakeExecutableSchema(chat.New()), + handler.WebsocketUpgrader(websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + })), + ) + log.Fatal(http.ListenAndServe(":8085", nil)) +} diff --git a/example/chat/src/App.js b/example/chat/src/App.js new file mode 100644 index 0000000000..8e4b9a87cf --- /dev/null +++ b/example/chat/src/App.js @@ -0,0 +1,28 @@ +import React, { Component } from 'react'; +import Room from './Room'; + +class App extends Component { + constructor(props) { + super(props); + + this.state = { + name: 'tester', + channel: '#gophers', + } + } + render() { + return (
+ name:
+ this.setState({name: e.target.value })} />
+ + channel:
+ this.setState({channel: e.target.value })}/>
+ + +
+ ); + } +} + + +export default App; diff --git a/example/chat/src/Room.js b/example/chat/src/Room.js new file mode 100644 index 0000000000..5d3e382920 --- /dev/null +++ b/example/chat/src/Room.js @@ -0,0 +1,85 @@ +import React , {Component} from 'react'; +import { graphql, compose } from 'react-apollo'; +import gql from 'graphql-tag'; + +class Room extends Component { + constructor(props) { + super(props) + + this.state = {text: ''} + } + + componentWillMount() { + this.props.data.subscribeToMore({ + document: Subscription, + variables: { + channel: this.props.channel, + }, + updateQuery: (prev, {subscriptionData}) => { + if (!subscriptionData.data) { + return prev; + } + const newMessage = subscriptionData.data.messageAdded; + if (prev.room.messages.find((msg) => msg.id === newMessage.id)) { + return prev + } + return Object.assign({}, prev, { + room: Object.assign({}, prev.room, { + messages: [...prev.room.messages, newMessage], + }) + }); + } + }); + } + + render() { + const data = this.props.data; + + if (data.loading) { + return
loading
+ } + + return
+
+ {data.room.messages.map((msg) => +
{msg.createdBy}: {msg.text}
+ )} +
+ this.setState({text: e.target.value})}/> + +
; + } +} + +const Subscription = gql` + subscription MoreMessages($channel: String!) { + messageAdded(roomName:$channel) { + id + text + createdBy + } + } +`; + +const Query = gql` + query Room($channel: String!) { + room(name: $channel) { + messages { id text createdBy } + } + } +`; + +const Mutation = gql` + mutation sendMessage($text: String!, $channel: String!, $name: String!) { + post(text:$text, roomName:$channel, username:$name) { id } + } +`; + + +export default compose(graphql(Mutation), graphql(Query))(Room); diff --git a/example/chat/src/index.js b/example/chat/src/index.js new file mode 100644 index 0000000000..7f412654dc --- /dev/null +++ b/example/chat/src/index.js @@ -0,0 +1,32 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { ApolloProvider } from 'react-apollo'; +import ApolloClient from 'apollo-client'; +import App from './App'; +import { SubscriptionClient } from 'subscriptions-transport-ws'; +import { InMemoryCache } from 'apollo-cache-inmemory'; + + +const client = new SubscriptionClient('ws://localhost:8085/query', { + reconnect: true, +}); + +const apolloClient = new ApolloClient({ + link: client, + cache: new InMemoryCache(), +}); + +if (module.hot) { + module.hot.accept('./App', () => { + const NextApp = require('./App').default; + render(); + }) +} + +function render(component) { + ReactDOM.render( + {component} + , document.getElementById('root')); +} + +render(); diff --git a/example/chat/types.json b/example/chat/types.json new file mode 100644 index 0000000000..c523c6ba5c --- /dev/null +++ b/example/chat/types.json @@ -0,0 +1,5 @@ +{ + "Chatroom": "github.com/vektah/gqlgen/example/chat.Chatroom", + "User": "github.com/vektah/gqlgen/example/chat.User", + "Message": "github.com/vektah/gqlgen/example/chat.Message" +} diff --git a/handler/graphql.go b/handler/graphql.go index 0e7716b872..67c6ed8ecd 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -7,6 +7,7 @@ import ( "strings" + "github.com/gorilla/websocket" "github.com/vektah/gqlgen/graphql" "github.com/vektah/gqlgen/neelance/errors" "github.com/vektah/gqlgen/neelance/query" @@ -19,10 +20,33 @@ type params struct { Variables map[string]interface{} `json:"variables"` } -func GraphQL(exec graphql.ExecutableSchema) http.HandlerFunc { +type Config struct { + upgrader websocket.Upgrader +} + +type Option func(cfg *Config) + +func WebsocketUpgrader(upgrader websocket.Upgrader) Option { + return func(cfg *Config) { + cfg.upgrader = upgrader + } +} + +func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc { + cfg := Config{ + upgrader: websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }, + } + + for _, option := range options { + option(&cfg) + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.Header.Get("Upgrade"), "websocket") { - connectWs(exec, w, r) + connectWs(exec, w, r, cfg.upgrader) return } diff --git a/handler/websocket.go b/handler/websocket.go index 91613bc34c..e43eeaf2b6 100644 --- a/handler/websocket.go +++ b/handler/websocket.go @@ -35,11 +35,6 @@ type operationMessage struct { Type string `json:"type"` } -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - type wsConnection struct { ctx context.Context conn *websocket.Conn @@ -48,7 +43,7 @@ type wsConnection struct { mu sync.Mutex } -func connectWs(exec graphql.ExecutableSchema, w http.ResponseWriter, r *http.Request) { +func connectWs(exec graphql.ExecutableSchema, w http.ResponseWriter, r *http.Request, upgrader websocket.Upgrader) { ws, err := upgrader.Upgrade(w, r, http.Header{ "Sec-Websocket-Protocol": []string{"graphql-ws"}, }) @@ -142,24 +137,32 @@ func (c *wsConnection) subscribe(message *operationMessage) bool { doc, qErr := query.Parse(params.Query) if qErr != nil { - c.sendError(params.OperationName, qErr) + c.sendError(message.ID, qErr) return true } errs := validation.Validate(c.exec.Schema(), doc) if len(errs) != 0 { - c.sendError(params.OperationName, errs...) + c.sendError(message.ID, errs...) return true } op, err := doc.GetOperation(params.OperationName) if err != nil { - c.sendError(params.OperationName, errors.Errorf("%s", err.Error())) + c.sendError(message.ID, errors.Errorf("%s", err.Error())) return true } if op.Type != query.Subscription { - c.sendError(params.OperationName, errors.Errorf("only subscriptions are currently supported over websockets")) + var result *graphql.Response + if op.Type == query.Query { + result = c.exec.Query(c.ctx, doc, params.Variables, op) + } else { + result = c.exec.Mutation(c.ctx, doc, params.Variables, op) + } + + c.sendData(message.ID, result) + c.write(&operationMessage{ID: message.ID, Type: completeMsg}) return true }