diff --git a/README.md b/README.md index a1f5ae56..4bb203ae 100644 --- a/README.md +++ b/README.md @@ -71,5 +71,5 @@ For more complex examples, refer to the [examples/](https://github.com/graphql-g | [dataloader](https://github.com/nicksrandall/dataloader) | [Nick Randall](https://github.com/nicksrandall) | [DataLoader](https://github.com/facebook/dataloader) implementation in Go. | ### Blog Posts -- [Golang + GraphQL + Relay](http://wehavefaces.net/) +- [Golang + GraphQL + Relay](https://wehavefaces.net/learn-golang-graphql-relay-1-e59ea174a902) diff --git a/examples/crud/Readme.md b/examples/crud/Readme.md index 9b5abb94..ab657516 100644 --- a/examples/crud/Readme.md +++ b/examples/crud/Readme.md @@ -1,25 +1,25 @@ # Go GraphQL CRUD example -Implementation create, read, update and delete on Go +Implement create, read, update and delete on Go. -To run the program, go to the directory -`cd examples/crud` +To run the program: -Run the example -`go run main.go` +1. go to the directory: `cd examples/crud` +2. Run the example: `go run main.go` ## Create + `http://localhost:8080/product?query=mutation+_{create(name:"Inca Kola",info:"Inca Kola is a soft drink that was created in Peru in 1935 by British immigrant Joseph Robinson Lindley using lemon verbena (wiki)",price:1.99){id,name,info,price}}` ## Read -Get single product by id -`http://localhost:8080/product?query={product(id:1){name,info,price}}` -Get product list -`http://localhost:8080/product?query={list{id,name,info,price}}` +* Get single product by id: `http://localhost:8080/product?query={product(id:1){name,info,price}}` +* Get product list: `http://localhost:8080/product?query={list{id,name,info,price}}` ## Update + `http://localhost:8080/product?query=mutation+_{update(id:1,price:3.95){id,name,info,price}}` ## Delete + `http://localhost:8080/product?query=mutation+_{delete(id:1){id,name,info,price}}` diff --git a/examples/crud/main.go b/examples/crud/main.go index bdd83504..dac20bcd 100644 --- a/examples/crud/main.go +++ b/examples/crud/main.go @@ -10,6 +10,7 @@ import ( "github.com/graphql-go/graphql" ) +// Product contains information about one product type Product struct { ID int64 `json:"id"` Name string `json:"name"` @@ -17,7 +18,26 @@ type Product struct { Price float64 `json:"price"` } -var products []Product +var products = []Product{ + { + ID: 1, + Name: "Chicha Morada", + Info: "Chicha morada is a beverage originated in the Andean regions of Perú but is actually consumed at a national level (wiki)", + Price: 7.99, + }, + { + ID: 2, + Name: "Chicha de jora", + Info: "Chicha de jora is a corn beer chicha prepared by germinating maize, extracting the malt sugars, boiling the wort, and fermenting it in large vessels (traditionally huge earthenware vats) for several days (wiki)", + Price: 5.95, + }, + { + ID: 3, + Name: "Pisco", + Info: "Pisco is a colorless or yellowish-to-amber colored brandy produced in winemaking regions of Peru and Chile (wiki)", + Price: 9.95, + }, +} var productType = graphql.NewObject( graphql.ObjectConfig{ @@ -204,17 +224,7 @@ func executeQuery(query string, schema graphql.Schema) *graphql.Result { return result } -func initProductsData(p *[]Product) { - product1 := Product{ID: 1, Name: "Chicha Morada", Info: "Chicha morada is a beverage originated in the Andean regions of Perú but is actually consumed at a national level (wiki)", Price: 7.99} - product2 := Product{ID: 2, Name: "Chicha de jora", Info: "Chicha de jora is a corn beer chicha prepared by germinating maize, extracting the malt sugars, boiling the wort, and fermenting it in large vessels (traditionally huge earthenware vats) for several days (wiki)", Price: 5.95} - product3 := Product{ID: 3, Name: "Pisco", Info: "Pisco is a colorless or yellowish-to-amber colored brandy produced in winemaking regions of Peru and Chile (wiki)", Price: 9.95} - *p = append(*p, product1, product2, product3) -} - func main() { - // Primary data initialization - initProductsData(&products) - http.HandleFunc("/product", func(w http.ResponseWriter, r *http.Request) { result := executeQuery(r.URL.Query().Get("query"), schema) json.NewEncoder(w).Encode(result) diff --git a/examples/sql-nullstring/README.md b/examples/sql-nullstring/README.md new file mode 100644 index 00000000..4e4e6e2a --- /dev/null +++ b/examples/sql-nullstring/README.md @@ -0,0 +1,39 @@ +# Go GraphQL SQL null string example + +database/sql Nullstring implementation, with JSON marshalling interfaces. + +To run the program, go to the directory +`cd examples/sql-nullstring` + +Run the example +`go run main.go` + +## sql.NullString + +On occasion you will encounter sql fields that are nullable, as in + +```sql +CREATE TABLE persons ( + id INT PRIMARY KEY, + name TEXT NOT NULL, + favorite_dog TEXT -- this field can have a NULL value +) +``` + +For the struct + +```golang +import "database/sql" + +type Person struct { + ID int `json:"id" sql:"id"` + Name string `json:"name" sql:"name"` + FavoriteDog sql.NullString `json:"favorite_dog" sql:"favorite_dog"` +} +``` + +But `graphql` would render said field as an object `{{ false}}` or `{{Bulldog true}}`, depending on their validity. + +With this implementation, `graphql` would render the null items as an empty string (`""`), but would be saved in the database as `NULL`, appropriately. + +The pattern can be extended to include other `database/sql` null types. diff --git a/examples/sql-nullstring/main.go b/examples/sql-nullstring/main.go new file mode 100644 index 00000000..cfcf6390 --- /dev/null +++ b/examples/sql-nullstring/main.go @@ -0,0 +1,252 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/language/ast" + "log" +) + +// NullString to be used in place of sql.NullString +type NullString struct { + sql.NullString +} + +// MarshalJSON from the json.Marshaler interface +func (v NullString) MarshalJSON() ([]byte, error) { + if v.Valid { + return json.Marshal(v.String) + } + return json.Marshal(nil) +} + +// UnmarshalJSON from the json.Unmarshaler interface +func (v *NullString) UnmarshalJSON(data []byte) error { + var x *string + if err := json.Unmarshal(data, &x); err != nil { + return err + } + if x != nil { + v.String = *x + v.Valid = true + } else { + v.Valid = false + } + return nil +} + +// NewNullString create a new null string. Empty string evaluates to an +// "invalid" NullString +func NewNullString(value string) *NullString { + var null NullString + if value != "" { + null.String = value + null.Valid = true + return &null + } + null.Valid = false + return &null +} + +// SerializeNullString serializes `NullString` to a string +func SerializeNullString(value interface{}) interface{} { + switch value := value.(type) { + case NullString: + return value.String + case *NullString: + v := *value + return v.String + default: + return nil + } +} + +// ParseNullString parses GraphQL variables from `string` to `CustomID` +func ParseNullString(value interface{}) interface{} { + switch value := value.(type) { + case string: + return NewNullString(value) + case *string: + return NewNullString(*value) + default: + return nil + } +} + +// ParseLiteralNullString parses GraphQL AST value to `NullString`. +func ParseLiteralNullString(valueAST ast.Value) interface{} { + switch valueAST := valueAST.(type) { + case *ast.StringValue: + return NewNullString(valueAST.Value) + default: + return nil + } +} + +// NullableString graphql *Scalar type based of NullString +var NullableString = graphql.NewScalar(graphql.ScalarConfig{ + Name: "NullableString", + Description: "The `NullableString` type repesents a nullable SQL string.", + Serialize: SerializeNullString, + ParseValue: ParseNullString, + ParseLiteral: ParseLiteralNullString, +}) + +/* +CREATE TABLE persons ( + favorite_dog TEXT -- is a nullable field + ); + +*/ + +// Person noqa +type Person struct { + Name string `json:"name"` + FavoriteDog *NullString `json:"favorite_dog"` // Some people don't like dogs ¯\_(ツ)_/¯ +} + +// PersonType noqa +var PersonType = graphql.NewObject(graphql.ObjectConfig{ + Name: "Person", + Fields: graphql.Fields{ + "name": &graphql.Field{ + Type: graphql.String, + }, + "favorite_dog": &graphql.Field{ + Type: NullableString, + }, + }, +}) + +func main() { + schema, err := graphql.NewSchema(graphql.SchemaConfig{ + Query: graphql.NewObject(graphql.ObjectConfig{ + Name: "Query", + Fields: graphql.Fields{ + "people": &graphql.Field{ + Type: graphql.NewList(PersonType), + Args: graphql.FieldConfigArgument{ + "favorite_dog": &graphql.ArgumentConfig{ + Type: NullableString, + }, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + dog, dogOk := p.Args["favorite_dog"].(*NullString) + people := []Person{ + Person{Name: "Alice", FavoriteDog: NewNullString("Yorkshire Terrier")}, + // `Bob`'s favorite dog will be saved as null in the database + Person{Name: "Bob", FavoriteDog: NewNullString("")}, + Person{Name: "Chris", FavoriteDog: NewNullString("French Bulldog")}, + } + switch { + case dogOk: + log.Printf("favorite_dog from arguments: %+v", dog) + dogPeople := make([]Person, 0) + for _, p := range people { + if p.FavoriteDog.Valid { + if p.FavoriteDog.String == dog.String { + dogPeople = append(dogPeople, p) + } + } + } + return dogPeople, nil + default: + return people, nil + } + }, + }, + }, + }), + }) + if err != nil { + log.Fatal(err) + } + query := ` +query { + people { + name + favorite_dog + } +}` + queryWithArgument := ` +query { + people(favorite_dog: "Yorkshire Terrier") { + name + favorite_dog + } +}` + r1 := graphql.Do(graphql.Params{ + Schema: schema, + RequestString: query, + }) + r2 := graphql.Do(graphql.Params{ + Schema: schema, + RequestString: queryWithArgument, + }) + if len(r1.Errors) > 0 { + log.Fatal(r1) + } + if len(r2.Errors) > 0 { + log.Fatal(r1) + } + b1, err := json.MarshalIndent(r1, "", " ") + b2, err := json.MarshalIndent(r2, "", " ") + if err != nil { + log.Fatal(err) + + } + fmt.Printf("\nQuery: %+v\n", string(query)) + fmt.Printf("\nResult: %+v\n", string(b1)) + fmt.Printf("\nQuery (with arguments): %+v\n", string(queryWithArgument)) + fmt.Printf("\nResult (with arguments): %+v\n", string(b2)) +} + +/* Output: +Query: +query { + people { + name + favorite_dog + } +} + +Result: { + "data": { + "people": [ + { + "favorite_dog": "Yorkshire Terrier", + "name": "Alice" + }, + { + "favorite_dog": "", + "name": "Bob" + }, + { + "favorite_dog": "French Bulldog", + "name": "Chris" + } + ] + } +} + +Query (with arguments): +query { + people(favorite_dog: "Yorkshire Terrier") { + name + favorite_dog + } +} + +Result (with arguments): { + "data": { + "people": [ + { + "favorite_dog": "Yorkshire Terrier", + "name": "Alice" + } + ] + } +} +*/ diff --git a/executor.go b/executor.go index d8477140..3c8441d2 100644 --- a/executor.go +++ b/executor.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "sort" "strings" "github.com/graphql-go/graphql/gqlerrors" @@ -254,7 +255,9 @@ func executeFieldsSerially(p executeFieldsParams) *Result { } finalResults := make(map[string]interface{}, len(p.Fields)) - for responseName, fieldASTs := range p.Fields { + for _, orderedField := range orderedFields(p.Fields) { + responseName := orderedField.responseName + fieldASTs := orderedField.fieldASTs fieldPath := p.Path.WithKey(responseName) resolved, state := resolveField(p.ExecutionContext, p.ParentType, p.Source, fieldASTs, fieldPath) if state.hasNoFieldDefs { @@ -650,15 +653,15 @@ func resolveField(eCtx *executionContext, parentType *Object, source interface{} Context: eCtx.Context, }) - if resolveFnError != nil { - panic(resolveFnError) - } - extErrs = resolveFieldFinishFn(result, resolveFnError) if len(extErrs) != 0 { eCtx.Errors = append(eCtx.Errors, extErrs...) } + if resolveFnError != nil { + panic(resolveFnError) + } + completed := completeValueCatchingError(eCtx, returnType, fieldASTs, info, path, result) return completed, resultState } @@ -1038,3 +1041,39 @@ func getFieldDef(schema Schema, parentType *Object, fieldName string) *FieldDefi } return parentType.Fields()[fieldName] } + +// contains field information that will be placed in an ordered slice +type orderedField struct { + responseName string + fieldASTs []*ast.Field +} + +// orders fields from a fields map by location in the source +func orderedFields(fields map[string][]*ast.Field) []*orderedField { + orderedFields := []*orderedField{} + fieldMap := map[int]*orderedField{} + startLocs := []int{} + + for responseName, fieldASTs := range fields { + // find the lowest location in the current fieldASTs + lowest := -1 + for _, fieldAST := range fieldASTs { + loc := fieldAST.GetLoc().Start + if lowest == -1 || loc < lowest { + lowest = loc + } + } + startLocs = append(startLocs, lowest) + fieldMap[lowest] = &orderedField{ + responseName: responseName, + fieldASTs: fieldASTs, + } + } + + sort.Ints(startLocs) + for _, startLoc := range startLocs { + orderedFields = append(orderedFields, fieldMap[startLoc]) + } + + return orderedFields +} diff --git a/extensions_test.go b/extensions_test.go index db7551ec..ea23f752 100644 --- a/extensions_test.go +++ b/extensions_test.go @@ -23,6 +23,12 @@ func tinit(t *testing.T) graphql.Schema { return "foo", nil }, }, + "erred": &graphql.Field{ + Type: graphql.String, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return "", errors.New("ooops") + }, + }, }, }), }) @@ -306,6 +312,35 @@ func TestExtensionResolveFieldFinishFuncPanic(t *testing.T) { } } +func TestExtensionResolveFieldFinishFuncAfterError(t *testing.T) { + var fnErrs int + ext := newtestExt("testExt") + ext.resolveFieldDidStartFn = func(ctx context.Context, i *graphql.ResolveInfo) (context.Context, graphql.ResolveFieldFinishFunc) { + return ctx, func(v interface{}, err error) { + if err != nil { + fnErrs++ + } + } + } + + schema := tinit(t) + query := `query Example { erred }` + schema.AddExtensions(ext) + + result := graphql.Do(graphql.Params{ + Schema: schema, + RequestString: query, + }) + + if resErrs := len(result.Errors); resErrs != 1 { + t.Errorf("Incorrect number of returned result errors: %d", resErrs) + } + + if fnErrs != 1 { + t.Errorf("Incorrect number of errors captured: %d", fnErrs) + } +} + func TestExtensionGetResultPanic(t *testing.T) { ext := newtestExt("testExt") ext.getResultFn = func(context.Context) interface{} { diff --git a/graphql_test.go b/graphql_test.go index 4b24c5f7..8b06a7b1 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -10,9 +10,10 @@ import ( ) type T struct { - Query string - Schema graphql.Schema - Expected interface{} + Query string + Schema graphql.Schema + Expected interface{} + Variables map[string]interface{} } var Tests = []T{} @@ -69,14 +70,35 @@ func init() { }, }, }, + { + Query: ` + query HumanByIdQuery($id: String!) { + human(id: $id) { + name + } + } + `, + Schema: testutil.StarWarsSchema, + Expected: &graphql.Result{ + Data: map[string]interface{}{ + "human": map[string]interface{}{ + "name": "Darth Vader", + }, + }, + }, + Variables: map[string]interface{}{ + "id": "1001", + }, + }, } } func TestQuery(t *testing.T) { for _, test := range Tests { params := graphql.Params{ - Schema: test.Schema, - RequestString: test.Query, + Schema: test.Schema, + RequestString: test.Query, + VariableValues: test.Variables, } testGraphql(test, params, t) } diff --git a/introspection.go b/introspection.go index a184f85d..51feb42d 100644 --- a/introspection.go +++ b/introspection.go @@ -312,6 +312,14 @@ func init() { }, "deprecationReason": &Field{ Type: String, + Resolve: func(p ResolveParams) (interface{}, error) { + if field, ok := p.Source.(*FieldDefinition); ok { + if field.DeprecationReason != "" { + return field.DeprecationReason, nil + } + } + return nil, nil + }, }, }, }) @@ -497,6 +505,14 @@ func init() { }, "deprecationReason": &Field{ Type: String, + Resolve: func(p ResolveParams) (interface{}, error) { + if field, ok := p.Source.(*EnumValueDefinition); ok { + if field.DeprecationReason != "" { + return field.DeprecationReason, nil + } + } + return nil, nil + }, }, }, }) diff --git a/introspection_test.go b/introspection_test.go index 71d47d9f..c0e62bf1 100644 --- a/introspection_test.go +++ b/introspection_test.go @@ -67,7 +67,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "queryType", @@ -81,7 +81,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "mutationType", @@ -91,7 +91,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "name": "__Type", }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "subscriptionType", @@ -101,7 +101,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "name": "__Type", }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "directives", @@ -123,7 +123,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, }, "inputFields": nil, @@ -148,7 +148,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "name", @@ -159,7 +159,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "description", @@ -170,7 +170,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "fields", @@ -199,7 +199,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "interfaces", @@ -218,7 +218,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "possibleTypes", @@ -237,7 +237,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "enumValues", @@ -266,7 +266,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "inputFields", @@ -285,7 +285,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "ofType", @@ -296,7 +296,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, }, "inputFields": nil, @@ -314,42 +314,42 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { map[string]interface{}{ "name": "SCALAR", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "OBJECT", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "INTERFACE", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "UNION", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "ENUM", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "INPUT_OBJECT", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "LIST", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "NON_NULL", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, }, "possibleTypes": nil, @@ -389,7 +389,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "description", @@ -400,7 +400,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "args", @@ -422,7 +422,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "type", @@ -437,7 +437,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "isDeprecated", @@ -452,7 +452,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "deprecationReason", @@ -463,7 +463,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, }, "inputFields": nil, @@ -488,7 +488,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "description", @@ -499,7 +499,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "type", @@ -514,7 +514,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "defaultValue", @@ -525,7 +525,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, }, "inputFields": nil, @@ -550,7 +550,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "description", @@ -561,7 +561,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "isDeprecated", @@ -576,7 +576,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "deprecationReason", @@ -587,7 +587,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, }, "inputFields": nil, @@ -612,7 +612,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "description", @@ -623,7 +623,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { "ofType": nil, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "locations", @@ -645,7 +645,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "args", @@ -667,7 +667,7 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { }, }, "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "onOperation", @@ -730,37 +730,37 @@ func TestIntrospection_ExecutesAnIntrospectionQuery(t *testing.T) { map[string]interface{}{ "name": "QUERY", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "MUTATION", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "SUBSCRIPTION", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "FIELD", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "FRAGMENT_DEFINITION", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "FRAGMENT_SPREAD", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "INLINE_FRAGMENT", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, }, "possibleTypes": nil, @@ -1023,7 +1023,7 @@ func TestIntrospection_IdentifiesDeprecatedFields(t *testing.T) { map[string]interface{}{ "name": "nonDeprecated", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "deprecated", @@ -1162,7 +1162,7 @@ func TestIntrospection_IdentifiesDeprecatedEnumValues(t *testing.T) { map[string]interface{}{ "name": "NONDEPRECATED", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, map[string]interface{}{ "name": "DEPRECATED", @@ -1172,7 +1172,7 @@ func TestIntrospection_IdentifiesDeprecatedEnumValues(t *testing.T) { map[string]interface{}{ "name": "ALSONONDEPRECATED", "isDeprecated": false, - "deprecationReason": "", + "deprecationReason": nil, }, }, }, diff --git a/rules_known_argument_names_test.go b/rules_known_argument_names_test.go index 574a0037..332cfd88 100644 --- a/rules_known_argument_names_test.go +++ b/rules_known_argument_names_test.go @@ -111,7 +111,7 @@ func TestValidate_KnownArgumentNames_UnknownArgsAmongstKnownArgsWithSuggestions( } `, []gqlerrors.FormattedError{ testutil.RuleError(`Unknown argument "ddogCommand" on field "doesKnowCommand" of type "Dog". `+ - `Did you mean "dogCommand"?`, 3, 25), + `Did you mean "dogCommand" or "nextDogCommand"?`, 3, 25), }) } func TestValidate_KnownArgumentNames_UnknownArgsDeeply(t *testing.T) { diff --git a/rules_overlapping_fields_can_be_merged.go b/rules_overlapping_fields_can_be_merged.go index 836fb43f..ccb769d0 100644 --- a/rules_overlapping_fields_can_be_merged.go +++ b/rules_overlapping_fields_can_be_merged.go @@ -623,8 +623,8 @@ func sameArguments(args1 []*ast.Argument, args2 []*ast.Argument) bool { } if arg1Name == arg2Name { foundArgs2 = arg2 + break } - break } if foundArgs2 == nil { return false diff --git a/rules_overlapping_fields_can_be_merged_test.go b/rules_overlapping_fields_can_be_merged_test.go index 97648ba2..bf36bae8 100644 --- a/rules_overlapping_fields_can_be_merged_test.go +++ b/rules_overlapping_fields_can_be_merged_test.go @@ -32,6 +32,14 @@ func TestValidate_OverlappingFieldsCanBeMerged_IdenticalFieldsWithIdenticalArgs( } `) } +func TestValidate_OverlappingFieldsCanBeMerged_IdenticalFieldsWithMultipleIdenticalArgs(t *testing.T) { + testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` + fragment mergeIdenticalFieldsWithIdenticalArgs on Dog { + doesKnowCommand(dogCommand: SIT nextDogCommand: DOWN) + doesKnowCommand(dogCommand: SIT nextDogCommand: DOWN) + } + `) +} func TestValidate_OverlappingFieldsCanBeMerged_IdenticalFieldsWithIdenticalDirectives(t *testing.T) { testutil.ExpectPassesRule(t, graphql.OverlappingFieldsCanBeMergedRule, ` fragment mergeSameFieldsWithSameDirectives on Dog { diff --git a/scalars.go b/scalars.go index 94c1943a..45479b54 100644 --- a/scalars.go +++ b/scalars.go @@ -296,7 +296,7 @@ var Float = NewScalar(ScalarConfig{ return floatValue } case *ast.IntValue: - if floatValue, err := strconv.ParseFloat(valueAST.Value, 32); err == nil { + if floatValue, err := strconv.ParseFloat(valueAST.Value, 64); err == nil { return floatValue } } @@ -547,6 +547,8 @@ func unserializeDateTime(value interface{}) interface{} { return nil } return unserializeDateTime([]byte(*value)) + case time.Time: + return value default: return nil } diff --git a/testutil/rules_test_harness.go b/testutil/rules_test_harness.go index a359d82b..384f447e 100644 --- a/testutil/rules_test_harness.go +++ b/testutil/rules_test_harness.go @@ -96,6 +96,9 @@ func init() { "dogCommand": &graphql.ArgumentConfig{ Type: dogCommandEnum, }, + "nextDogCommand": &graphql.ArgumentConfig{ + Type: dogCommandEnum, + }, }, }, "isHousetrained": &graphql.Field{ diff --git a/testutil/testutil.go b/testutil/testutil.go index 658928b1..0d905542 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -80,7 +80,7 @@ func init() { PrimaryFunction: "Astromech", } Luke.Friends = append(Luke.Friends, []StarWarsChar{Han, Leia, Threepio, Artoo}...) - Vader.Friends = append(Luke.Friends, []StarWarsChar{Tarkin}...) + Vader.Friends = append(Vader.Friends, []StarWarsChar{Tarkin}...) Han.Friends = append(Han.Friends, []StarWarsChar{Luke, Leia, Artoo}...) Leia.Friends = append(Leia.Friends, []StarWarsChar{Luke, Han, Threepio, Artoo}...) Tarkin.Friends = append(Tarkin.Friends, []StarWarsChar{Vader}...)