goql

package module
v1.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 25, 2024 License: Apache-2.0 Imports: 15 Imported by: 2

README

goql

go.dev reference Generated via Bootstrap Coverage Status

A GraphQL client package written in Go.

Contributing

Please read the CONTRIBUTING.md document for guidelines on developing and contributing changes.

High-level Overview

goql is a GraphQL client library with built-in two-way marshaling support via struct tags. This is key because it allows for strongly typed GraphQL queries as opposed to variables containing a string representation of the query. This also facilitates more advanced features, such as sparse field sets.

For complete documentation see the generated pkg.go documentation. For a complete guide on the struct tag syntax, see the documentation found below under Defining GraphQL Operations.

Installation

In the root of your project repository (same directory as your go.mod and go.sum files):

go get github.com/getoutreach/goql

After that you should be able to import it anywhere within your project.

Defining GraphQL Operations

GraphQL operations can be defined by using normal Go struct types along with the help of struct tags. For example:

type QueryUserCollection struct {
	UserCollection struct {
		Collection []struct {
			ID   string
			Name string
		} `goql:"keep"`
	} `goql:"userCollection(filter:$filter<[Filter]>,sort:$sort<[String]>,size:$size<Int>,before:$before<String>,after:$after<String>)"`
}

when passed through the GraphQL query marshaller renders the following string:

query (
  $filter: [Filter]
  $sort: [String]
  $size: Int
  $before: String
  $after: String
) {
  userCollection(
    filter: $filter
    sort: $sort
    size: $size
    before: $before
    after: $after
  ) {
    collection {
      id
      name
    }
  }
}

Here's the high-level steps to go through when first defining a GraphQL operation:

  1. Create a struct that will act as a wrapper for the entire operation. The top-level model will be the only immediate child struct field of this wrapper struct (e.g. QueryUserCollection's only immediate child is UserCollection which together represents the query($filter: [Filter], ...) { userCollection(filter: $filter, ...) { ... } } part of the output).
  2. Define all of the fields and sub-models of the top-level model as struct fields within the top-level model (e.g. UserCollection contains children fields []Collection, ID, and Name). All types should match the types described in the schema of the query. - ID in GraphQL is a string in Go. - Any type with the non-null (!) restriction in GraphQL should be a non-pointer type in Go. Conversely, any type in GraphQL without this restriction should be nullable (a pointer type) in Go. - If the field is an integral part of the operation, e.g. UserCollection, and Collection fields in the struct above, add the goql:"keep" tag to them to tell the marshaler to always include these fields. This is necessary in order for sparse field sets to work. However, in the example above the keep tag can actually be omitted from the UserCollection part of the query as it already defines an operation declaration, which the marshaler already sees as an integral part of the operation and implicitly marks it to be kept (that is why the keep tag is left off of that portion, but on Collection still).
  3. Iterate through the fields and add goql struct tags to further define the structure of the operation by modifying declarations, adding aliases, variables, or directives to each field. See the immediately proceeding section, GraphQL Struct Tag Syntax, for more information on these struct tags and how to define them.
GraphQL Struct Tag Syntax

The following components can be used alone or together, separated by a comma within in the tag, to define a goql struct tag for a field or model on an operation:

  • modelName(arg:$var<Type>, arg2:$var2<Type2!>, ...)
    • Defines the name and argument list for a model. This is close to what you would see in a normal GraphQL operation, with a little syntactic sugar added to define the types of variables since they're needed in the wrapper of the operation when defining the variables used throughout it. This component implicitly defines the keep tag for the field as well, given that operation declarations are necessary regardless of sparse fieldset instructions.
    • MyModel struct `goql:"myModel(page:$page<Int!>)"` -> query($page: Int!) { myModel(page: $page) { ...
  • fieldNameOverride
    • Overrides the name of a field, by default the lower camel-case version of the name of the struct field is used.
    • Name string `goql:"username"` -> username
  • @alias(desiredAlias)
    • Adds an alias for a field or model, which will change the returned key in the JSON response from the GraphQL server. See the GraphQL documentation on aliases for more information.
    • An alias is required when an operation name set by a goql tag diverges from the struct field name. Without an alias in that situation the data would not be able to be marshaled back into the struct field after the operation succeeds, resulting in a silent "error". As an example, Role *Role `goql:"createRole(...)"` would need an alias since createRole (operation name) != Role (struct field name).
    • Name string `goql:"@alias(username)"` -> username: name
  • @include($flag)
    • Adds an include directive to the field or model. See the GraphQL documentation on directives for more information. Note that the variable passed to this directive in the struct tag does not have a type proceeding it in square brackets. This is because these directive variables always have the type of Boolean!, so it is implied and therefore not necessary.
    • Name string `goql:"@include($withName)"` -> name @include(if: $withName)
  • @skip($flag)
    • Adds a skip directive to the field or model. See the GraphQL documentation on directives for more information. Note that the variable passed to this directive in the struct tag does not have a type proceeding it in square brackets. This is because these directive variables always have the type of Boolean!, so it is implied and therefore not necessary.
    • Name string `goql:"@skip($withoutName)"` -> name @skip(if: $withoutName)
  • keep
    • Tells the marshaler to keep this field regardless of what is requested in terms of sparse field sets.

Here is an example of using multiple struct tags together:

Name string `goql:"@alias(username),@include($withName)"` -> username: name @include(if: $withName)

Rules:

  • The same component cannot be defined more than once in a singular struct tag.
    • Name string `goql:"@include($withName),@include($withName2)"` would result in an error because an include directive was defined twice on the same struct tag.
  • All defined variables must only have one type each associated with them.
    • MyModel struct `goql:"myModel(page:$page<Int!>,pageSize:$page<Int>)"` would result in an error, since $page is defined to have both the type of Int! and Int.
    • MyModel struct `goql:"myModel(page:$page<Int!>),@include($page)"` would also result in an error, since $page is defined to have the type of both Int! and Boolean! (implicit when used in the include directive).

Documentation

Overview

Package goql is a low-level client interface that can communicate with a running GraphQL server.

Index

Constants

This section is empty.

Variables

View Source
var DefaultClientOptions = ClientOptions{
	HTTPClient:               nil,
	ErrorMapper:              nil,
	UseJSONTagNameAsFallback: false,
}

DefaultClientOptions is a variable that can be passed for the ClientOptions when calling NewClient that will trigger use of all of the default options.

Functions

func MarshalMutation

func MarshalMutation(q interface{}, fields Fields) (string, error)

MarshalMutation takes a variable that must be a struct type and constructs a GraphQL operation using it's fields and graphql struct tags that can be used as a GraphQL mutation operation.

func MarshalMutationWithOptions added in v1.12.0

func MarshalMutationWithOptions(q interface{}, fields Fields, opts ...marshalOption) (string, error)

MarshalMutationWithOptions takes a variable that must be a struct type and constructs a GraphQL operation using it's fields and graphql struct tags that can be used as a GraphQL mutation operation. Additionally, MarshalMutationWithOptions accepts an array of functional options to change the marshalling behavior.

func MarshalQuery

func MarshalQuery(q interface{}, fields Fields) (string, error)

MarshalQuery takes a variable that must be a struct type and constructs a GraphQL operation using it's fields and graphql struct tags that can be used as a GraphQL query operation.

func MarshalQueryWithOptions added in v1.12.0

func MarshalQueryWithOptions(q interface{}, fields Fields, opts ...marshalOption) (string, error)

MarshalQueryWithOptions takes a variable that must be a struct type and constructs a GraphQL operation using it's fields and graphql struct tags that can be used as a GraphQL query operation. Additionally, MarshalQueryWithOptions accepts an array of functional options to change the marshalling behavior.

func OptFallbackJSONTag added in v1.12.0

func OptFallbackJSONTag(opt *optStruct)

OptFallbackJSONTag causes the marshalling of structs to queries to still respect goql struct tags in the same way as default, but if no `goql` struct tag is present, marshalling will try to derive the name-in-GQL of that struct field from a `json` struct tag, if a `json` struct tag is present. If no `goql` struct tag AND no `json` struct tag are present, then marshalling defaults to the same toLowerCamelCase approach as always.

func OptGoqlTagsOnly added in v1.12.0

func OptGoqlTagsOnly(opt *optStruct)

OptGoqlTagsOnly will cause the marshalling procedure of goql to *only* look at the `goql` struct tags when marshalling a struct into a GQL operation. The behavior caused by this option is the default behavior of goql, this option is provided for explicitness sake. If you would like marshalling to use `goql` struct tags and then to _fall back_ on JSON struct tags if no goql tags are present on a struct, then use the OptFallbackJSONTag option.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client contains all of the necessary fields and receiver functions to carry out requests to a GraphQL server in an idiomatic way.

func NewClient

func NewClient(clientURL string, options ClientOptions) *Client

NewClient returns a configured pointer to a Client. If httpClient is nil, http.DefaultClient will be used in place of it. If errorMapper is omitted the Errors type will be returned in the case of any errors that came from the GraphQL server.

func (*Client) CustomOperation

func (c *Client) CustomOperation(ctx context.Context, query string, variables map[string]interface{}, resp interface{}) error

CustomOperation is a wrapper around CustomOperationWithHeaders that passes no headers.

func (*Client) CustomOperationWithHeaders

func (c *Client) CustomOperationWithHeaders(ctx context.Context, query string, variables map[string]interface{},
	resp interface{}, headers http.Header) error

CustomOperationWithHeaders takes a query in the form of a string and attempts to marshal the response into the resp parameter, which should be passed by reference (as a pointer). If nil is passed as the actual parameter for the formal parameter resp, the response is discarded.

func (*Client) Mutate

func (c *Client) Mutate(ctx context.Context, operation *Operation) error

Mutate is a wrapper around MutateWithHeaders that passes no headers.

func (*Client) MutateWithHeaders

func (c *Client) MutateWithHeaders(ctx context.Context, operation *Operation, headers http.Header) error

MutateWithHeaders performs a mutate type of request to mutate and retrieve data from a GraphQL server. q should be passed by reference and all variables defined in the struct tag of q should exist within the variables map as well.

func (*Client) Query

func (c *Client) Query(ctx context.Context, operation *Operation) error

Query is a wrapper around QueryWithHeaders that passes no headers.

func (*Client) QueryWithHeaders

func (c *Client) QueryWithHeaders(ctx context.Context, operation *Operation, headers http.Header) error

QueryWithHeaders performs a query type of request to retrieve data from a GraphQL server. q should be passed by reference and all variables defined in the struct tag of q should exist within the variables map as well.

type ClientOptions

type ClientOptions struct {
	HTTPClient               *http.Client
	ErrorMapper              ErrorMapper
	UseJSONTagNameAsFallback bool
}

ClientOptions is the type passed to NewClient that allows for configuration of the client.

HTTPClient is an optional http.Client that the GraphQL client will use underneath the hood. If this field is omitted or nil then the client will use http.DefaultClient.

ErrorMapper allows the Errors type potentially returned from the GraphQL server to be mapped to a different type that implements the error interface, optionally. The status code of the response from the GraphQL server is also passed to this function to attempt to give more context to the callee. If omitted or nil the Errors type will be returned in the case of any errors that came from the GraphQL server. See the documentation for the Errors type for more information as to what can be done with this mapping function.

UseJSONTagNameAsFallback indicates whether goql should fall back on using the `json` struct tags if there's no `goql` struct tags when marshaling a struct into a query. If true, only the name of the field is inferred from the JSON struct tag, not any other attribute such as alias, include, or keep. Default value is false.

type Error

type Error struct {
	Message    string          `json:"message"`
	Path       []string        `json:"path"`
	Extensions json.RawMessage `json:"extensions"`
}

Error is the type that contains the structure of an error returned from a GraphQL server. The Extensions key is intentionally left as a json.RawMessage so that it can optionally be handled and marshaled into whatever type necessary by the ErrorMapper passed to the client.

type ErrorMapper

type ErrorMapper func(int, Errors) error

ErrorMapper is a type that is used for error mapping functions. The status code and Errors are sent as parameters and it is the functions responsibility to map the Errors into a type that implements the error interface.

type Errors

type Errors []Error

Errors is a type alias for a slice of Error, which is what is returned in the response of a request to a GraphQL server. More information is available on the Error type's documentation.

func (Errors) Error

func (e Errors) Error() string

Error is a value receiver function on the Errors type which implements the error interface for its receiver. This allows the type to be returned as a normal error, but it can also be asserted to it's original type if desired.

type Fields

type Fields map[string]interface{}

Fields is a type that is intended to be used to allow sparse field sets when rendering by specifying the fields within the underlying map. Take the following desired GraphQL operation for example:

query {
	roleCollection {
		collection {
			id
			name
			parentRole {
				id
			}
		}
	}
}

The following variable would denote those fields being requested, given that the "keep" tag was specified on roleCollection and collection (since they're always necessary in the above query):

f := graphql.Fields{
	"id": true,
	"name": true,
	"parentRole": graphql.Fields{
		"id": true,
	},
}

Any omitted fields or fields explicitly set to false will not be included in the resulting query. If fields is passed as nil, all fields will be rendered on the operation.

func FieldsFromDelimitedList

func FieldsFromDelimitedList(list, fieldDelimiter, subFieldDelimiter string) Fields

FieldsFromDelimitedList is meant to be used to transform a URL query parameter in a Fields type variable to get the sparse fieldset functionality from an HTTP API.

The fieldDelimiter is the delimiter that separates each individual field entry, and usually would be a comma (,). The subFieldDelimiter is the delimiter that separates nested field entries, and usually would be a period. Take the following as an example of how the inputs of this function correlate to the output:

list: id,name,parent.id,parent.parentOfParent.id fieldDelimiter: , subFieldDelimiter: . --- Output:

graphql.Fields{
	"id": true,
	"name": true,
	"parent": graphql.Fields{
		"id": true,
		"parentOfParent": graphql.Fields{
			"id": true,
		},
	},
}

func FieldsFromURLQueryParam

func FieldsFromURLQueryParam(raw string) Fields

FieldsFromURLQueryParam uses FieldsFromDelimitedList in an opinionated fashion, assuming your fields are separated by a comma and the subfields are separated by a period. See the documentation for FieldsFromDelimitedList for a more granular description on how this process works.

func Union

func Union(x, y Fields) Fields

Union is a function that takes the union (as in the union of two sets) of two Fields types. If performance is a worry, it is advantageous to pass the larger of the two Fields types as the first parameter (formal parameter x).

type Operation

type Operation struct {
	OperationType interface{}
	Fields        Fields
	Variables     map[string]interface{}
}

Operation is an encapsulation of all of the elements that are used to compose an operation using struct tags. The OperationType field should always be passed by reference in order for the data to be able to be marshaled back into it.

Directories

Path Synopsis
Package graphql_test exports a Server that facilitates the testing of client integrations of GraphQL by mocking a GraphQL server.
Package graphql_test exports a Server that facilitates the testing of client integrations of GraphQL by mocking a GraphQL server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL