Skip to content

Add support for null literals #401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions examples/crud-null/Readme.md
Original file line number Diff line number Diff line change
@@ -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}}`
233 changes: 233 additions & 0 deletions examples/crud-null/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions language/ast/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions language/ast/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions language/kinds/kinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
FloatValue = "FloatValue"
StringValue = "StringValue"
BooleanValue = "BooleanValue"
NullValue = "NullValue"
EnumValue = "EnumValue"
ListValue = "ListValue"
ObjectValue = "ObjectValue"
Expand Down
10 changes: 9 additions & 1 deletion language/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
10 changes: 1 addition & 9 deletions language/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := `
Expand Down Expand Up @@ -367,6 +358,7 @@ func TestAllowsNonKeywordsAnywhereNameIsAllowed(t *testing.T) {
"subscription",
"true",
"false",
"null",
}
for _, keyword := range nonKeywords {
fragmentName := keyword
Expand Down
9 changes: 9 additions & 0 deletions language/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading