From 62d914ed840f8dc05e1a3c66385daf4773b87714 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 23 Sep 2018 20:28:22 +1000 Subject: [PATCH 1/2] Add support for null literals --- definition.go | 3 +++ language/ast/node.go | 1 + language/ast/values.go | 29 +++++++++++++++++++++++++++++ language/kinds/kinds.go | 1 + language/parser/parser.go | 10 +++++++++- language/parser/parser_test.go | 10 +--------- language/printer/printer.go | 9 +++++++++ rules.go | 6 ++++++ scalars.go | 13 +++++++++++++ 9 files changed, 72 insertions(+), 10 deletions(-) diff --git a/definition.go b/definition.go index 759ab7d2..788e3d28 100644 --- a/definition.go +++ b/definition.go @@ -158,6 +158,9 @@ func GetNullable(ttype Type) Nullable { return ttype } +// NullValue to be able to detect if a value is set to null or if it is omitted +type NullValue struct {} + // Named interface for types that do not include modifiers like List or NonNull. type Named interface { String() string diff --git a/language/ast/node.go b/language/ast/node.go index cd63a0fc..d7cc3a80 100644 --- a/language/ast/node.go +++ b/language/ast/node.go @@ -22,6 +22,7 @@ var _ Node = (*IntValue)(nil) var _ Node = (*FloatValue)(nil) var _ Node = (*StringValue)(nil) var _ Node = (*BooleanValue)(nil) +var _ Node = (*NullValue)(nil) var _ Node = (*EnumValue)(nil) var _ Node = (*ListValue)(nil) var _ Node = (*ObjectValue)(nil) diff --git a/language/ast/values.go b/language/ast/values.go index 6c3c8864..54fa1861 100644 --- a/language/ast/values.go +++ b/language/ast/values.go @@ -16,6 +16,7 @@ var _ Value = (*IntValue)(nil) var _ Value = (*FloatValue)(nil) var _ Value = (*StringValue)(nil) var _ Value = (*BooleanValue)(nil) +var _ Value = (*NullValue)(nil) var _ Value = (*EnumValue)(nil) var _ Value = (*ListValue)(nil) var _ Value = (*ObjectValue)(nil) @@ -172,6 +173,34 @@ func (v *BooleanValue) GetValue() interface{} { return v.Value } +// NullValue implements Node, Value +type NullValue struct { + Kind string + Loc *Location + Value interface{} +} + +func NewNullValue(v *NullValue) *NullValue { + + return &NullValue{ + Kind: kinds.NullValue, + Loc: v.Loc, + Value: v.Value, + } +} + +func (v *NullValue) GetKind() string { + return v.Kind +} + +func (v *NullValue) GetLoc() *Location { + return v.Loc +} + +func (v *NullValue) GetValue() interface{} { + return nil +} + // EnumValue implements Node, Value type EnumValue struct { Kind string diff --git a/language/kinds/kinds.go b/language/kinds/kinds.go index 40bc994e..f70b1a3c 100644 --- a/language/kinds/kinds.go +++ b/language/kinds/kinds.go @@ -23,6 +23,7 @@ const ( FloatValue = "FloatValue" StringValue = "StringValue" BooleanValue = "BooleanValue" + NullValue = "NullValue" EnumValue = "EnumValue" ListValue = "ListValue" ObjectValue = "ObjectValue" diff --git a/language/parser/parser.go b/language/parser/parser.go index 70c814d2..c8f8b5e8 100644 --- a/language/parser/parser.go +++ b/language/parser/parser.go @@ -616,7 +616,15 @@ func parseValueLiteral(parser *Parser, isConst bool) (ast.Value, error) { Value: value, Loc: loc(parser, token.Start), }), nil - } else if token.Value != "null" { + } else if token.Value == "null" { + if err := advance(parser); err != nil { + return nil, err + } + return ast.NewNullValue(&ast.NullValue{ + Value: nil, + Loc: loc(parser, token.Start), + }), nil + } else { if err := advance(parser); err != nil { return nil, err } diff --git a/language/parser/parser_test.go b/language/parser/parser_test.go index 3cc4253a..ec260ea2 100644 --- a/language/parser/parser_test.go +++ b/language/parser/parser_test.go @@ -183,15 +183,6 @@ func TestDoesNotAcceptFragmentsSpreadOfOn(t *testing.T) { testErrorMessage(t, test) } -func TestDoesNotAllowNullAsValue(t *testing.T) { - test := errorMessageTest{ - `{ fieldWithNullableStringInput(input: null) }'`, - `Syntax Error GraphQL (1:39) Unexpected Name "null"`, - false, - } - testErrorMessage(t, test) -} - func TestParsesMultiByteCharacters_Unicode(t *testing.T) { doc := ` @@ -367,6 +358,7 @@ func TestAllowsNonKeywordsAnywhereNameIsAllowed(t *testing.T) { "subscription", "true", "false", + "null", } for _, keyword := range nonKeywords { fragmentName := keyword diff --git a/language/printer/printer.go b/language/printer/printer.go index eb4d5ec1..aaf23833 100644 --- a/language/printer/printer.go +++ b/language/printer/printer.go @@ -367,6 +367,15 @@ var printDocASTReducer = map[string]visitor.VisitFunc{ } return visitor.ActionNoChange, nil }, + "NullValue": func(p visitor.VisitFuncParams) (string, interface{}) { + switch node := p.Node.(type) { + case *ast.NullValue: + return visitor.ActionUpdate, fmt.Sprintf("%v", node.Value) + case map[string]interface{}: + return visitor.ActionUpdate, getMapValueString(node, "Value") + } + return visitor.ActionNoChange, nil + }, "EnumValue": func(p visitor.VisitFuncParams) (string, interface{}) { switch node := p.Node.(type) { case *ast.EnumValue: diff --git a/rules.go b/rules.go index ae0c75b9..4d9ae78c 100644 --- a/rules.go +++ b/rules.go @@ -1730,6 +1730,12 @@ func isValidLiteralValue(ttype Input, valueAST ast.Value) (bool, []string) { return true, nil } + // This function only tests literals, and assumes variables will provide + // values of the correct type. + if valueAST.GetKind() == kinds.NullValue { + return true, nil + } + // This function only tests literals, and assumes variables will provide // values of the correct type. if valueAST.GetKind() == kinds.Variable { diff --git a/scalars.go b/scalars.go index 9131f802..5e9d942a 100644 --- a/scalars.go +++ b/scalars.go @@ -162,6 +162,8 @@ var Int = NewScalar(ScalarConfig{ if intValue, err := strconv.Atoi(valueAST.Value); err == nil { return intValue } + case *ast.NullValue: + return NullValue{} } return nil }, @@ -299,6 +301,8 @@ var Float = NewScalar(ScalarConfig{ if floatValue, err := strconv.ParseFloat(valueAST.Value, 32); err == nil { return floatValue } + case *ast.NullValue: + return NullValue{} } return nil }, @@ -326,7 +330,10 @@ var String = NewScalar(ScalarConfig{ switch valueAST := valueAST.(type) { case *ast.StringValue: return valueAST.Value + case *ast.NullValue: + return NullValue{} } + return nil }, }) @@ -485,6 +492,8 @@ var Boolean = NewScalar(ScalarConfig{ switch valueAST := valueAST.(type) { case *ast.BooleanValue: return valueAST.Value + case *ast.NullValue: + return NullValue{} } return nil }, @@ -506,6 +515,8 @@ var ID = NewScalar(ScalarConfig{ return valueAST.Value case *ast.StringValue: return valueAST.Value + case *ast.NullValue: + return NullValue{} } return nil }, @@ -562,6 +573,8 @@ var DateTime = NewScalar(ScalarConfig{ switch valueAST := valueAST.(type) { case *ast.StringValue: return valueAST.Value + case *ast.NullValue: + return NullValue{} } return nil }, From 2c170a1c791de3b25d0c348b18a34bafa198e6fa Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 23 Sep 2018 21:01:40 +1000 Subject: [PATCH 2/2] Add example usage of null literal --- examples/crud-null/Readme.md | 25 ++++ examples/crud-null/main.go | 233 +++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 examples/crud-null/Readme.md create mode 100644 examples/crud-null/main.go diff --git a/examples/crud-null/Readme.md b/examples/crud-null/Readme.md new file mode 100644 index 00000000..9b5abb94 --- /dev/null +++ b/examples/crud-null/Readme.md @@ -0,0 +1,25 @@ +# Go GraphQL CRUD example + +Implementation create, read, update and delete on Go + +To run the program, go to the directory +`cd examples/crud` + +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}}` + +## 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-null/main.go b/examples/crud-null/main.go new file mode 100644 index 00000000..589a8964 --- /dev/null +++ b/examples/crud-null/main.go @@ -0,0 +1,233 @@ +package main + +import ( + "github.com/graphql-go/graphql" + "github.com/graphql-go/handler" + "math/rand" + "net/http" + "time" +) + +type Product struct { + ID int64 `json:"id"` + Name string `json:"name"` + Info graphql.Nullable `json:"info,omitempty"` + Price graphql.Nullable `json:"price"` +} + +var products []Product + +var productType = graphql.NewObject( + graphql.ObjectConfig{ + Name: "Product", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.Int, + }, + "name": &graphql.Field{ + Type: graphql.String, + }, + "info": &graphql.Field{ + Type: graphql.String, + }, + "price": &graphql.Field{ + Type: graphql.Float, + }, + }, + }, +) + +var queryType = graphql.NewObject( + graphql.ObjectConfig{ + Name: "Query", + Fields: graphql.Fields{ + /* Get (read) single product by id + http://localhost:8080/product?query={product(id:1){name,info,price}} + */ + "product": &graphql.Field{ + Type: productType, + Description: "Get product by id", + Args: graphql.FieldConfigArgument{ + "id": &graphql.ArgumentConfig{ + Type: graphql.Int, + }, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + id, ok := p.Args["id"].(int) + if ok { + // Find product + for _, product := range products { + if int(product.ID) == id { + return product, nil + } + } + } + return nil, nil + }, + }, + /* Get (read) product list + http://localhost:8080/product?query={list{id,name,info,price}} + */ + "list": &graphql.Field{ + Type: graphql.NewList(productType), + Description: "Get product list", + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + return products, nil + }, + }, + }, + }) + +var mutationType = graphql.NewObject(graphql.ObjectConfig{ + Name: "Mutation", + Fields: graphql.Fields{ + /* Create new product item + 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}} + */ + "create": &graphql.Field{ + Type: productType, + Description: "Create new product", + Args: graphql.FieldConfigArgument{ + "name": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + "info": &graphql.ArgumentConfig{ + Type: graphql.String, + }, + "price": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.Float), + }, + }, + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + rand.Seed(time.Now().UnixNano()) + product := Product{ + ID: int64(rand.Intn(100000)), // generate random ID + Name: params.Args["name"].(string), + Info: params.Args["info"].(string), + Price: params.Args["price"].(float64), + } + products = append(products, product) + return product, nil + }, + }, + + /* Update product by id + http://localhost:8080/product?query=mutation+_{update(id:1,price:3.95){id,name,info,price}} + */ + "update": &graphql.Field{ + Type: productType, + Description: "Update product by id", + Args: graphql.FieldConfigArgument{ + "id": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + "name": &graphql.ArgumentConfig{ + Type: graphql.String, + }, + "info": &graphql.ArgumentConfig{ + Type: graphql.String, + }, + "price": &graphql.ArgumentConfig{ + Type: graphql.Float, + }, + }, + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + id, _ := params.Args["id"].(int) + name, nameOk := params.Args["name"].(string) + + // Handling null with type switch and nullable type + info, infoOk := graphql.GetNullable(graphql.String), false + + switch v := params.Args["info"].(type) { + case string: + info, infoOk = v, true + case graphql.NullValue: + info, infoOk = nil, true + } + + // Handling null with boolean and handling separately later + price, priceOk := params.Args["price"].(float64) + _, priceNull := params.Args["price"].(graphql.NullValue) + + + product := Product{} + for i, p := range products { + if int64(id) == p.ID { + if nameOk { + products[i].Name = name + } + if infoOk{ + products[i].Info = info + } + if priceOk || priceNull { + if priceOk { + products[i].Price = price + } else { + products[i].Price = nil + } + + } + product = products[i] + break + } + } + return product, nil + }, + }, + + /* Delete product by id + http://localhost:8080/product?query=mutation+_{delete(id:1){id,name,info,price}} + */ + "delete": &graphql.Field{ + Type: productType, + Description: "Delete product by id", + Args: graphql.FieldConfigArgument{ + "id": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + }, + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + id, _ := params.Args["id"].(int) + product := Product{} + for i, p := range products { + if int64(id) == p.ID { + product = products[i] + // Remove from product list + products = append(products[:i], products[i+1:]...) + } + } + + return product, nil + }, + }, + }, +}) + +var schema, _ = graphql.NewSchema( + graphql.SchemaConfig{ + Query: queryType, + Mutation: mutationType, + }, +) + +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) + + h := handler.New(&handler.Config{ + Schema: &schema, + Pretty: true, + GraphiQL: true, + }) + + http.Handle("/graphql", h) + http.ListenAndServe(":8080", nil) +} \ No newline at end of file