Documentation
¶
Overview ¶
Package gql provides tooling for building and using GraphQL to serve content. Most notably it includes ObjectBuilder which enables easy building of GraphQL schema from Golang structs.
The tooling in this package wraps the GraphQL server implementation from github.com/graphql-go/graphql.
When paired with https://github.com/GannettDigital/jstransform/ this tooling allows for building Go based GraphQL servers from JSONschema quickly and then evolving the schema easily.
Index ¶
- Constants
- Variables
- func DeepExtractField(s interface{}, key string) interface{}
- func ExtractField(s interface{}, key string) interface{}
- func ResolveByField(name string, parent string) graphql.FieldResolveFn
- func ResolveListField(name string, parent string) graphql.FieldResolveFn
- func ResolveTotalCount(totalFieldName, listFieldName, parent string) graphql.FieldResolveFn
- type Comparator
- func NewEqualComparator(arg map[string]interface{}) (Comparator, error)
- func NewInComparator(arg map[string]interface{}) (Comparator, error)
- func NewLimitLengthComparator(arg map[string]interface{}) (Comparator, error)
- func NewNotEqualComparator(arg map[string]interface{}) (Comparator, error)
- func NewNotInComparator(arg map[string]interface{}) (Comparator, error)
- type ListFunctions
- type NewListOperation
- type NotComparator
- type ObjectBuilder
- type QueryFunctionReporter
- type QueryReporter
Examples ¶
Constants ¶
const ( // FieldPathSeparator is '_' because it is the only allowed non-alphanumeric character. // http://facebook.github.io/graphql/October2016/#sec-Names // Any _ occurences will be gracefully removed e.g. _my_odd_name => myoddname FieldPathSeparator = "_" // QueryReporterContextKey is the key used with context.WithValue to locate the QueryReporter. QueryReporterContextKey = "GraphQLQueryReporter" )
Variables ¶
var ( // ListOperations is the set of list filters operations available for use by ResolveListField. // The string is the operation, ie '==' which matches a particular NewListOperation function. // The default implementations are pre-populated and additional operations can be added before running the // ObjectBuilder. ListOperations = map[string]NewListOperation{ "==": NewEqualComparator, "!=": NewNotEqualComparator, ">": NewIntegerComparator(">"), ">=": NewIntegerComparator(">="), "<": NewIntegerComparator("<"), "<=": NewIntegerComparator("<="), "LIMIT": NewLimitLengthComparator, "IN": NewInComparator, "NOT IN": NewNotInComparator, } )
Functions ¶
func DeepExtractField ¶ added in v0.4.0
func DeepExtractField(s interface{}, key string) interface{}
DeepExtractField executed the deepExtractFieldWithError method and swallows the error, if any.
func ExtractField ¶
func ExtractField(s interface{}, key string) interface{}
ExtractField returns the value of a field from a struct, the key is the field name, which is matched to the output from the fieldName function. This function also handles searching any root level embedded structs. If the key does not match a field in the struct or the provided interface is not a struct nil is returned.
func ResolveByField ¶
func ResolveByField(name string, parent string) graphql.FieldResolveFn
ResolveByField returns a FieldResolveFn that leverages ExtractFields for the given field name to resolve the data. The resolve function assumes the entire object is available in the ResolveParams source. It will also report the queried field to the QueryReporter if one is found in the context. This is default resolve function used by the objectbuilder.
func ResolveListField ¶ added in v0.3.0
func ResolveListField(name string, parent string) graphql.FieldResolveFn
ResolveListField returns a FieldResolveFn that leverages ResolveByField to get the field value then applies an optional filter to the returned list of results.
The optional filter is specified by a 'filter' argument which is a JSON object with a required string field named 'Operation' and optional object field 'Argument' and string field named 'Field'. Certain operations may require a valid 'Field' and/or 'Argument'. When provided 'Argument' should be a JSON object whose value will be the argument to NewListOperation functions. The value of 'Field' is the name of a field in the list items, if the list contains objects within it child field keys can be added using FieldPathSeparator.
In addition to the filter argument as sort argument can be specified. The sort argument takes a string parameter Field which is the same as that for the filter, the field to be compared or the list itself if unspecified. It also takes an optional order parameter which is either "ASC" or "DESC", "ASC" is default.
Sorting occurs before filtering as some filters limit the total returned size of the list.
Example:
{ query(id: "blah") { modules(filter: {Field: "moduleName", Operation: "==", Argument: {Value: "foo"}}, sort: {Field: "module_type", Order: "ASC"}) { moduleName } } }
func ResolveTotalCount ¶ added in v0.4.6
func ResolveTotalCount(totalFieldName, listFieldName, parent string) graphql.FieldResolveFn
ResolveTotalCount accepts a total count field name, a name of the list field, and a parent name. It leverages ExtractField for the given list field name and will return the count of items in the extract field if it is an array or a slice. It will also report the queried field to the QueryReporter if one is found in the context.
Types ¶
type Comparator ¶ added in v0.3.0
type Comparator interface {
Match(interface{}) bool
}
A Comparator checks whether a value Matches. Each NewListOperation returns a comparator to be used in List filtering.
func NewEqualComparator ¶ added in v0.3.0
func NewEqualComparator(arg map[string]interface{}) (Comparator, error)
NewEqualComparator returns a comparator for equality of both strings and ints. The comparator expects either a string or int with the key "Value" as part of the given argument.
func NewInComparator ¶ added in v0.3.0
func NewInComparator(arg map[string]interface{}) (Comparator, error)
NewInComparator returns a comparator which returns true if the operand matches any of the given values. The comparator expects a list of strings under the key "Values" as part of the given argument.
func NewLimitLengthComparator ¶ added in v0.3.0
func NewLimitLengthComparator(arg map[string]interface{}) (Comparator, error)
NewLimitLengthComparator returns a comparator which simply limits the length of the results to the integer given with the key "Value" in the argument.
func NewNotEqualComparator ¶ added in v0.3.0
func NewNotEqualComparator(arg map[string]interface{}) (Comparator, error)
NewNotEqualComparator returns a comparator for not equality of both strings and ints. The comparator expects either a string or int with the key "Value" as part of the given argument.
func NewNotInComparator ¶ added in v0.3.0
func NewNotInComparator(arg map[string]interface{}) (Comparator, error)
NewNotInComparator wraps NewInComparator returning the opposite boolean.
type ListFunctions ¶ added in v0.4.7
type NewListOperation ¶ added in v0.3.0
type NewListOperation func(argument map[string]interface{}) (Comparator, error)
NewListOperation is a function which returns a Comparator given a set of list filter arguments.
func NewIntegerComparator ¶ added in v0.3.0
func NewIntegerComparator(op string) NewListOperation
NewIntegerComparator returns a NewListOperator for integer operations supported by integerComparator, specifically <, <=, > and >=. The returned NewListOperation expects an int with the key "Value" as part of the given argument.
type NotComparator ¶ added in v0.3.0
type NotComparator struct {
Child Comparator
}
NotComparator simply does a logical not on the Match result of its child.
func (NotComparator) Match ¶ added in v0.3.0
func (c NotComparator) Match(raw interface{}) bool
type ObjectBuilder ¶
type ObjectBuilder struct {
// contains filtered or unexported fields
}
ObjectBuilder is used to build GraphGL Objects based on the fields within a set of structs. A graphql.Schema is built with the parameters defined at https://godoc.org/github.com/graphql-go/graphql#SchemaConfig this code creates Objects which implement the graphql.Type interface and also graphql.Interface interface. These types and interfaces can be leveraged to build the Query and the other components of a schema.
Fields from the structs are setup as GraphQL fields only if they are exported. In the GraphQL these fields are named to match the JSON struct tag name or if none is found the lowercase field name. If the JSON struct tag for a field specifies "omitempty" the field is nullable otherwise it is NonNullable.
Interfaces for a type are built whenever the underlying struct has an embedded struct within it. The embedded struct is built as an interface for the type. Only root level embedded structs are handled this way.
GraphQL objects contain fields and for each field GraphQL utilizes resolve functions to populate the data. The default resolve function (ResolveByField) is setup for auto-generated fields. This resolve function assumes that the resolve function used for the Query retrieved an entire object matching the given struct and the resolve for fields is simply to pull the correct field from that object. The default resolve function also looks for a QueryReporter in the context and if it exists reports the QueriedFields. If the field is a List the default function is ResolveListField which works the same way but adds a filter parameter optionally used to filter the list items.
It is also possible to specify custom fields which can be setup with custom resolve functions. See fieldAdditions on the NewObjectBulider function and the AddCustomFields method.
The go proverbs, "Clear is better than clever." and "Reflection is never clear." both apply here as the reflection is not the easiest to follow. It was chosen specifically because adding this complexity here makes projects utilizing GraphQL simpler as it allows the complicated types to update and change without any of the implementation code being impacted.
Example (Full) ¶
The full example expands on the simple example showing custom fields, GraphQL interfaces and an interface including itself as a way of pulling related items in a single query.
type ExampleStruct struct { fieldA string Links []struct { ID string `json:"id,omitempty"` } `json:"links,omitempty"` } type exampleStruct2 struct { ExampleStruct fieldB string } type exampleStruct3 struct { ExampleStruct fieldB string } exampleData2 := make(map[string]exampleStruct2) exampleData3 := make(map[string]exampleStruct3) exampleStructResolver := func(p graphql.ResolveParams) (interface{}, error) { id, ok := p.Args["id"].(string) if !ok { return nil, errors.New("failed to extract ID from argument") } // replace with DB implementation if example, ok := exampleData2[id]; ok { return example, nil } example := exampleData3[id] return example, nil } ob, err := NewObjectBuilder([]interface{}{exampleStruct2{}, exampleStruct3{}}, "", nil) if err != nil { log.Fatal(err) } // First create the interface so the interface can be used in adding a custom field ifaces := ob.BuildInterfaces() exampleInterface := ifaces["ExampleStruct"] // This add a new field in the ExampleStruct interface that allows resolving additional structs recursively. ob.AddCustomFields(map[string][]*graphql.Field{ strings.Join([]string{"ExampleStruct", "links"}, FieldPathSeparator): { { Name: "examplestruct", Type: exampleInterface, Resolve: exampleStructResolver, }, }, }) types := ob.BuildTypes() queryCfg := graphql.ObjectConfig{ Name: "Query", Fields: graphql.Fields{ "example": &graphql.Field{ Type: exampleInterface, // The Query returns the interface so either type matching it can be returned Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Description: "ID of the object to retrieve", Type: graphql.NewNonNull(graphql.String), }, }, Resolve: exampleStructResolver, }, }, } query := graphql.NewObject(queryCfg) schema, err := graphql.NewSchema(graphql.SchemaConfig{ Query: query, Types: types, }) if err != nil { log.Fatal(err) } // schema is now ready to use for resolving queries params := graphql.Params{ Context: context.Background(), Schema: schema, RequestString: "", // replace with real query } graphql.Do(params)
Output:
Example (Prefix) ¶
The prefix example shows how a prefix could be used to create GraphQL schema distinctions between similar or identical types.
type exampleStruct struct { fieldA string } exampleData := make(map[string]exampleStruct) ob, err := NewObjectBuilder([]interface{}{exampleStruct{}}, "", nil) if err != nil { log.Fatal(err) } types := ob.BuildTypes() // A second object builder adds a prefix to the naming. This example is a contrived but should demonstrate how // naming collisions in the GraphQL schema are avoided by adding the prefix. sob, err := NewObjectBuilder([]interface{}{exampleStruct{}}, "staging", nil) if err != nil { log.Fatal(err) } stypes := sob.BuildTypes() queryCfg := graphql.ObjectConfig{ Name: "Query", Fields: graphql.Fields{ "example": &graphql.Field{ Type: types[0], Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Description: "ID of the object to retrieve", Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { id, ok := p.Args["id"].(string) if !ok { return nil, errors.New("failed to extract ID from argument.") } // replace with DB implementation example := exampleData[id] return example, nil }, }, "stagingExample": &graphql.Field{ Type: stypes[0], Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Description: "ID of the object to retrieve", Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { id, ok := p.Args["id"].(string) if !ok { return nil, errors.New("failed to extract ID from argument.") } // replace with DB implementation, this one coming from staging example := exampleData[id] return example, nil }, }, }, } query := graphql.NewObject(queryCfg) schema, err := graphql.NewSchema(graphql.SchemaConfig{ Query: query, Types: append(types, stypes...), }) if err != nil { log.Fatal(err) } // schema is now ready to use for resolving queries params := graphql.Params{ Context: context.Background(), Schema: schema, RequestString: "", } graphql.Do(params)
Output:
Example (Simple) ¶
The simple example shows how to use the object builder with a struct with no embedded fields. The main value of the object builder comes with many structs each with many fields as it greatly minimizes the amount of code needed to setup these scenarios. This example shows the bare minimum additional pieces needed from the graphql-go library to setup a working schema.
type exampleStruct struct { fieldA string } exampleData := make(map[string]exampleStruct) ob, err := NewObjectBuilder([]interface{}{exampleStruct{}}, "", nil) if err != nil { log.Fatal(err) } types := ob.BuildTypes() queryCfg := graphql.ObjectConfig{ Name: "Query", Fields: graphql.Fields{ "example": &graphql.Field{ Type: types[0], Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Description: "ID of the object to retrieve", Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { id, ok := p.Args["id"].(string) if !ok { return nil, errors.New("failed to extract ID from argument.") } // replace with DB implementation example := exampleData[id] return example, nil }, }, }, } query := graphql.NewObject(queryCfg) schema, err := graphql.NewSchema(graphql.SchemaConfig{ Query: query, Types: types, }) if err != nil { log.Fatal(err) } // schema is now ready to use for resolving queries params := graphql.Params{ Context: context.Background(), Schema: schema, RequestString: "", } resp := graphql.Do(params) out, err := json.Marshal(resp) if err != nil { log.Fatal(err) } fmt.Println(out)
Output:
func NewObjectBuilder ¶
func NewObjectBuilder(structs []interface{}, namePrefix string, fieldAdditions map[string][]*graphql.Field) (*ObjectBuilder, error)
NewObjectBuilder creates an ObjectBuilder for the given structs and fieldAdditions.
namePrefix is an optional string to prefix the name of each generated type and interface with, it becomes part of the parent name of a field name but otherwise does not affect field names.
fieldAdditions allows for adding into GraphQL objects fields which don't show up in the underlying structs. The key to the map is a path for the field parent, this starts with the FieldPathRoot and adds the struct name for any embedded structs joined with FieldPathSeparator. Each nested object within the GraphQL object has its own path name. For example `fmt.Sprintf("%surl%ssitename", FieldPathRoot, FieldPathSeparator)` for fields added to the sitename object which is within the url object at the root. Be aware that these fields are added to all structs that have a matching path, this includes any interfaces build from embedded structs as well.
func (*ObjectBuilder) AddCustomFields ¶
func (ob *ObjectBuilder) AddCustomFields(fieldAdditions map[string][]*graphql.Field)
AddCustomFields configures more custom fields that will be used when building the types. This will overwrite custom fields with the same name that already exist in the object builder. This function is especially useful for adding fields that utilize an existing interface when run after BuildInterfaces but before BuildTypes.
func (*ObjectBuilder) BuildInterfaces ¶
func (ob *ObjectBuilder) BuildInterfaces() map[string]*graphql.Interface
BuildInterfaces will create GraphQL interfaces out of embedded structs for source object builder structs. If not previously run this will be run automatically part of the BuildTypes method. It can be run independently before running BuildTypes to support using the returned interfaces in additional fields that will be added when BuildTypes creates the GraphQL types.
func (*ObjectBuilder) BuildTypes ¶
func (ob *ObjectBuilder) BuildTypes() []graphql.Type
BuildTypes creates the GraphQL types from the sources structs. The output of this method is suitable for directly including in graphql.SchemaConfig which when coupled with a graphql.Query can be built into the a GraphQL schema.
type QueryFunctionReporter ¶ added in v0.4.7
type QueryFunctionReporter interface { // QueriedListFunctions is called by the list resolve function used for filtered fields within the GraphQL query. QueriedListFunctions(string, ListFunctions) error }
QueryFunctionReporter defines the interface used to report details on the GraphQL queries functions being performed. Query functions include list filtering and list sorting. Implementations must be concurrency safe and added to the request context using QueryFunctionReporterContextKey as the context value key.
type QueryReporter ¶
type QueryReporter interface { // QueriedField is called by the default resolve function used for fields within the GraphQL query. QueriedField(string) error }
QueryReporter defines the interface used to report details on the GraphQL queries being performed. Implementations must be concurrency safe and added to the request context using QueryReporterContextKey as the context value key.