apifu

package module
Version: v0.0.0-...-f795d1e Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2021 License: MIT Imports: 23 Imported by: 1

README

api-fu GitHub Actions Go Report Card codecov Documentation Mentioned in Awesome Go

api-fu (noun)

  1. (informal) Mastery of APIs. 💪

Packages

  • The top level apifu package is an opinionated library that aims to make it as easy as possible to build APIs that conform to API-fu's ideals. See the examples directory for example usage.
  • The graphql package is an unopinionated library for building GraphQL APIs. If you agree with API-fu's ideals, you should use apifu instead, but if you want something lower level, the graphql package is still an excellent standalone GraphQL library. It fully supports all features of the June 2018 spec.
  • The graphqlws package is an unopinionated library for using the Apollo graphql-ws protocol. This allows you to serve your GraphQL API via WebSockets and provide subscription functionality.

Usage

API-fu builds GraphQL APIs with code. To begin, you need a config that at least defines a query field:

var fuCfg apifu.Config

fuCfg.AddQueryField("foo", &graphql.FieldDefinition{
    Type: graphql.StringType,
    Resolve: func(ctx *graphql.FieldContext) (interface{}, error) {
        return "bar", nil
    },
})

From there, you can build the API:

fu, err := apifu.NewAPI(&fuCfg)
if err != nil {
    panic(err)
}

And serve it:

fu.ServeGraphQL(w, r)

API-fu also has first-class support for common patterns such as nodes that are queryable using global ids. See the examples directory for more complete example code.

Features

✅ Supports all features of the latest GraphQL spec.

This includes null literals, error extensions, subscriptions, and directives.

🚅 Fast!

The graphql package is over twice as fast and several times more memory efficient than its inspiration (graphql-go/graphql).

pkg: github.com/ccbrown/api-fu/graphql/benchmarks
BenchmarkAPIFu
BenchmarkAPIFu-16        	     765	   1553517 ns/op	  890575 B/op	   22587 allocs/op
BenchmarkGraphQLGo
BenchmarkGraphQLGo-16    	     315	   3753681 ns/op	 3990220 B/op	   45952 allocs/op
⚡️ Supports efficient batching and concurrency without the use of goroutines.

The graphql package supports virtually any batching or concurrency pattern using low level primitives.

The apifu package provides high level ways to use them.

For example, you can define a resolver like this to do work in a goroutine:

fuCfg.AddQueryField("myField", &graphql.FieldDefinition{
    Type: graphql.IntType,
    Resolve: func(ctx *graphql.FieldContext) (interface{}, error) {
        return Go(ctx.Context, func() (interface{}, error) {
            return doSomethingComplex(), nil
        }), nil
    },
})

Or you can define a resolver like this to batch up queries, allowing you to minimize round trips to your database:

fuCfg.AddQueryField("myField", &graphql.FieldDefinition{
    Type: graphql.IntType,
    Resolve: Batch(func(ctx []*graphql.FieldContext) []graphql.ResolveResult {
        return resolveABunchOfTheseAtOnce(ctx)
    },
})
💡 Provides implementations for commonly used scalar types.

For example, the apifu package provides date-time and long (but JavaScript safe) integers.

📡 Implements handlers for HTTP and the Apollo graphql-ws protocol.

Once you've built your API, all you have to do is:

fu.ServeGraphQL(w, r)

Or:

fu.ServeGraphQLWS(w, r)
📖 Provides easy-to-use helpers for creating connections adhering to the Relay Cursor Connections Specification.

Just provide a name, cursor constructor, edge fields, and edge getter:

{
    "messagesConnection": apifu.TimeBasedConnection(&apifu.TimeBasedConnectionConfig{
        NamePrefix: "ChannelMessages",
        EdgeCursor: func(edge interface{}) apifu.TimeBasedCursor {
            message := edge.(*model.Message)
            return apifu.NewTimeBasedCursor(message.Time, string(message.Id))
        },
        EdgeFields: map[string]*graphql.FieldDefinition{
            "node": &graphql.FieldDefinition{
                Type: graphql.NewNonNullType(messageType),
                Resolve: func(ctx *graphql.FieldContext) (interface{}, error) {
                    return ctx.Object, nil
                },
            },
        },
        EdgeGetter: func(ctx *graphql.FieldContext, minTime time.Time, maxTime time.Time, limit int) (interface{}, error) {
            return ctxSession(ctx.Context).GetMessagesByChannelIdAndTimeRange(ctx.Object.(*model.Channel).Id, minTime, maxTime, limit)
        },
    }),
}
🛠 Can generate Apollo-like client-side type definitions and validate queries in source code.

The gql-client-gen tool can be used to generate types for use in client-side code as well as validate queries at compile-time. The generated types intelligently unmarshal inline fragments and fragment spreads based on __typename values.

See cmd/gql-client-gen for details.

🚔 Calculates operation costs during validation for rate limiting and metering

During validation, you can specify a max operation cost or get the actual cost of an operation using customizable cost definitions:

doc, errs := graphql.ParseAndValidate(req.Query, req.Schema, req.ValidateCost(maxCost, &actualCost))

API Design Guidelines

The following are guidelines that are recommended for all new GraphQL APIs. API-fu aims to make it easy to conform to these for robust and future-proof APIs:

  • All mutations should resolve to result types. No mutations should simply resolve to a node. For example, a createUser mutation should resolve to a CreateUserResult object with a user field rather than simply resolving to a User. This is necessary to keep mutations extensible. Likewise, subscriptions should not resolve directly to node types. For example, a subscription for messages in a chat room (chatRoomMessages) should resolve to a ChatRoomMessagesEvent type.
  • Nodes with 1-to-many relationships should make related nodes available via Relay Cursor Connections. Nodes should not have fields that simply resolve to lists of related nodes. Additionally, all connections must require a first or last argument that specifies the upper bound on the number of nodes returned by that connection. This makes it possible to determine an upper bound on the number of nodes returned by a query before that query begins execution, e.g. using rules similar to GitHub's.
  • Mutations that modify nodes should always include the updated version of that node in the result. This makes it easy for clients to maintain up-to-date state and tolerate eventual consistency (If a client updates a resource, then immediately requests it in a subsequent query, the server may provide a version of the resource that was cached before the update.).
  • Nodes should provide revision numbers. Each time a node is modified, the revision number must increment. This helps clients maintain up-to-date state and enables simultaneous change detection.
  • It should be easy for clients to query historical data and subscribe to real-time data without missing anything due to race conditions. The most transparent and fool-proof way to facilitate this is to make subscriptions immediately push a small history of events to clients as soon as they're started. The pushed history should generally only need to cover a few seconds' worth of events. If queries use eventual consistency, the pushed history should be at least as large as the query cache's TTL.

Versioning and Compatibility Guarantees

This library is not versioned. However, one guarantee is made: Any backwards-incompatible changes made will break your build at compile-time. If your application compiles after updating API-fu, you're good to go.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DateTimeType = &graphql.ScalarType{
	Name:        "DateTime",
	Description: "DateTime represents an RFC-3339 datetime.",
	LiteralCoercion: func(v ast.Value) interface{} {
		switch v := v.(type) {
		case *ast.StringValue:
			return parseDateTime(v.Value)
		}
		return nil
	},
	VariableValueCoercion: parseDateTime,
	ResultCoercion: func(v interface{}) interface{} {
		switch v := v.(type) {
		case time.Time:
			if b, err := v.MarshalText(); err == nil {
				return string(b)
			}
		}
		return nil
	},
}

DateTimeType provides a DateTime implementation that serializing to and from RFC-3339 datetimes.

View Source
var LongIntType = &graphql.ScalarType{
	Name:        "LongInt",
	Description: "LongInt represents a signed integer that may be longer than 32 bits, but still within JavaScript / IEEE-654's \"safe\" range.",
	LiteralCoercion: func(v ast.Value) interface{} {
		switch v := v.(type) {
		case *ast.IntValue:
			if n, err := strconv.ParseInt(v.Value, 10, 64); err == nil && n >= minSafeInteger && n <= maxSafeInteger {
				return n
			}
		}
		return nil
	},
	VariableValueCoercion: coerceLongInt,
	ResultCoercion:        coerceLongInt,
}

LongIntType provides a scalar implementation for integers that may be larger than 32 bits, but can still be represented by JavaScript numbers.

View Source
var PageInfoType = &graphql.ObjectType{
	Name: "PageInfo",
	Fields: map[string]*graphql.FieldDefinition{
		"hasPreviousPage": NonNull(graphql.BooleanType, "HasPreviousPage"),
		"hasNextPage":     NonNull(graphql.BooleanType, "HasNextPage"),
		"startCursor":     NonNull(graphql.StringType, "StartCursor"),
		"endCursor":       NonNull(graphql.StringType, "EndCursor"),
	},
}

PageInfoType implements the GraphQL type for the page info of a GraphQL Cursor Connection.

Functions

func Batch

func Batch(f func([]*graphql.FieldContext) []graphql.ResolveResult) func(*graphql.FieldContext) (interface{}, error)

Batch batches up the resolver invocations into a single call. As queries are executed, whenever resolution gets "stuck", all pending batch resolvers will be triggered concurrently. Batch resolvers must return one result for every field context it receives.

func Connection

func Connection(config *ConnectionConfig) *graphql.FieldDefinition

Connection is used to create a connection field that adheres to the GraphQL Cursor Connections Specification.

func Go

func Go(ctx context.Context, f func() (interface{}, error)) graphql.ResolvePromise

Go completes resolution asynchronously and concurrently with any other asynchronous resolutions.

func Node

func Node(nodeType *graphql.ObjectType, idFieldName string) *graphql.FieldDefinition

Node returns a field that resolves to the node of the given type, whose id is the value of the specified field.

func NonEmptyString

func NonEmptyString(fieldName string) *graphql.FieldDefinition

NonEmptyString returns a field that resolves to a string if the field's value is non-empty. Otherwise, the field resolves to nil.

func NonNull

func NonNull(t graphql.Type, fieldName string) *graphql.FieldDefinition

NonNull returns a non-null field that resolves to the given type.

func NonNullNodeID

func NonNullNodeID(modelType reflect.Type, fieldName string) *graphql.FieldDefinition

NonNullNodeID returns a field that resolves to an ID for an object of the given type.

func NonZeroDateTime

func NonZeroDateTime(fieldName string) *graphql.FieldDefinition

NonZeroDateTime returns a field definition that resolves to the value of the field with the given name. If the field's value is the zero time, the field resolves to nil instead.

func OwnID

func OwnID(fieldName string) *graphql.FieldDefinition

OwnID returns a field that resolves to an ID for the current Object.

func PersistedQueryExtension

func PersistedQueryExtension(storage PersistedQueryStorage, execute func(*graphql.Request) *graphql.Response) func(*graphql.Request) *graphql.Response

PersistedQueryExtension implements Apollo persisted queries: https://www.apollographql.com/docs/react/api/link/persisted-queries/

Typically this shouldn't be invoked directly. Instead, set the PersistedQueryStorage Config field.

func TimeBasedConnection

func TimeBasedConnection(config *TimeBasedConnectionConfig) *graphql.FieldDefinition

TimeBasedConnection creates a new connection for edges sorted by time. In addition to the standard first, last, after, and before fields, the connection will have atOrAfterTime and beforeTime fields, which can be used to query a specific time range.

Types

type API

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

API is responsible for serving your API traffic. Construct an API by creating a Config, then calling NewAPI.

func NewAPI

func NewAPI(cfg *Config) (*API, error)

NewAPI validates your schema and builds an API ready to serve requests.

func (*API) CloseHijackedConnections

func (api *API) CloseHijackedConnections()

CloseHijackedConnections closes connections hijacked by ServeGraphQLWS.

func (*API) ServeGraphQL

func (api *API) ServeGraphQL(w http.ResponseWriter, r *http.Request)

ServeGraphQL serves GraphQL HTTP requests. Requests may be GET requests using query string parameters or POST requests with either the application/json or application/graphql content type.

func (*API) ServeGraphQLWS

func (api *API) ServeGraphQLWS(w http.ResponseWriter, r *http.Request)

ServeGraphQLWS serves a graphql-ws WebSocket connection. This method hijacks connections. To gracefully close them, use CloseHijackedConnections.

type Config

type Config struct {
	Logger               logrus.FieldLogger
	WebSocketOriginCheck func(r *http.Request) bool
	SerializeNodeId      func(typeId int, id interface{}) string
	DeserializeNodeId    func(string) (typeId int, id interface{})
	AdditionalNodeFields map[string]*graphql.FieldDefinition

	// If given, Apollo persisted queries are supported by the API:
	// https://www.apollographql.com/docs/react/api/link/persisted-queries/
	PersistedQueryStorage PersistedQueryStorage

	// When calculating field costs, this is used as the default. This is typically either
	// `graphql.FieldCost{Resolver: 1}` or left as zero.
	DefaultFieldCost graphql.FieldCost

	// Execute is invoked to execute a GraphQL request. If not given, this is simply
	// graphql.Execute. You may wish to provide this to perform request logging or
	// pre/post-processing.
	Execute func(*graphql.Request, *RequestInfo) *graphql.Response

	// If given, this function is invoked when the servers receives the graphql-ws connection init
	// payload. If an error is returned, it will be sent to the client and the connection will be
	// closed. Otherwise the returned context will become associated with the connection.
	//
	// This is commonly used for authentication.
	HandleGraphQLWSInit func(ctx context.Context, parameters json.RawMessage) (context.Context, error)

	// Explicitly adds named types to the schema. This is generally only required for interface
	// implementations that aren't explicitly referenced elsewhere in the schema.
	AdditionalTypes map[string]graphql.NamedType
	// contains filtered or unexported fields
}

Config defines the schema and other parameters for an API.

func (*Config) AddMutation

func (cfg *Config) AddMutation(name string, def *graphql.FieldDefinition)

AddMutation adds a mutation to your schema.

func (*Config) AddNamedType

func (cfg *Config) AddNamedType(t graphql.NamedType)

AddNamedType adds a named type to the schema. This is generally only required for interface implementations that aren't explicitly referenced elsewhere in the schema.

func (*Config) AddNodeType

func (cfg *Config) AddNodeType(t *NodeType) *graphql.ObjectType

AddNodeType registers the given node type and returned the object type created for the node.

func (*Config) AddQueryField

func (cfg *Config) AddQueryField(name string, def *graphql.FieldDefinition)

AddQueryField adds a field to your schema's query object.

func (*Config) AddSubscription

func (cfg *Config) AddSubscription(name string, def *graphql.FieldDefinition)

AddSubscription adds a subscription operation to your schema.

When a subscription is started, your resolver will be invoked with ctx.IsSubscribe set to true. When this happens, you should return a pointer to a SubscriptionSourceStream (or an error). For example:

Resolve: func(ctx *graphql.FieldContext) (interface{}, error) {
    if ctx.IsSubscribe {
        ticker := time.NewTicker(time.Second)
        return &apifu.SubscriptionSourceStream{
            EventChannel: ticker.C,
            Stop:         ticker.Stop,
        }, nil
    } else if ctx.Object != nil {
        return ctx.Object, nil
    } else {
        return nil, fmt.Errorf("Subscriptions are not supported using this protocol.")
    }
},

func (*Config) MutationType

func (cfg *Config) MutationType() *graphql.ObjectType

MutationType returns the root mutation type.

func (*Config) NodeObjectType

func (cfg *Config) NodeObjectType(name string) *graphql.ObjectType

NodeObjectType returns the object type for a node type previously added via AddNodeType.

func (*Config) QueryType

func (cfg *Config) QueryType() *graphql.ObjectType

QueryType returns the root query type.

type ConnectionConfig

type ConnectionConfig struct {
	// A prefix to use for the connection and edge type names. For example, if you provide
	// "Example", the connection type will be named "ExampleConnection" and the edge type will be
	// "ExampleEdge".
	NamePrefix string

	// An optional description for the connection.
	Description string

	// An optional map of additional arguments to add to the connection.
	Arguments map[string]*graphql.InputValueDefinition

	// If getting all edges for the connection is cheap, you can just provide ResolveAllEdges.
	// ResolveAllEdges should return a slice value, with one item for each edge, and a function that
	// can be used to sort the cursors produced by EdgeCursor.
	ResolveAllEdges func(ctx *graphql.FieldContext) (edgeSlice interface{}, cursorLess func(a, b interface{}) bool, err error)

	// If getting all edges for the connection is too expensive for ResolveAllEdges, you can provide
	// ResolveEdges. ResolveEdges is just like ResolveAllEdges, but is only required to return edges
	// within the range defined by the given cursors and is only required to return up to `limit`
	// edges. If limit is negative, the last edges within the range should be returned instead of
	// the first.
	//
	// Returning extra edges or out-of-order edges is fine. They will be sorted and filtered
	// automatically. However, you should ensure that no duplicate edges are returned.
	//
	// If desired, edges outside of the given range may be returned to indicate the presence of more
	// pages before or after the given range. This is completely optional, and the connection's
	// behavior will be fully compliant with the Relay Pagination spec regardless. However,
	// providing these additional edges will allow hasNextPage and hasPreviousPage to be true in
	// scenarios where the spec allows them to be false for performance reasons.
	ResolveEdges func(ctx *graphql.FieldContext, after, before interface{}, limit int) (edgeSlice interface{}, cursorLess func(a, b interface{}) bool, err error)

	// If you use ResolveEdges, you can optionally provide ResolveTotalCount to add a totalCount
	// field to the connection. If you use ResolveAllEdges, there is no need to provide this.
	ResolveTotalCount func(ctx *graphql.FieldContext) (interface{}, error)

	// CursorType allows the connection to deserialize cursors. It is required for all connections.
	CursorType reflect.Type

	// EdgeCursor should return a value that can be used to determine the edge's relative ordering.
	// For example, this might be a struct with a name and id for a connection whose edges are
	// sorted by name. The value must be able to be marshaled to and from binary. This function
	// should return the type of cursor assigned to CursorType.
	EdgeCursor func(edge interface{}) interface{}

	// EdgeFields should provide definitions for the fields of each node. You must provide the
	// "node" field, but the "cursor" field will be provided for you.
	EdgeFields map[string]*graphql.FieldDefinition
}

ConnectionConfig defines the configuration for a connection that adheres to the GraphQL Cursor Connections Specification.

type NodeType

type NodeType struct {
	// Id should be an integer that uniquely identifies the node type. Once set, it should never
	// change and no other nodes should ever use the same id.
	Id int

	Name string

	Model reflect.Type

	// GetByIds should be a function that accepts a context and slice of ids and returns a slice of
	// models.
	GetByIds func(ctx context.Context, ids interface{}) (models interface{}, err error)

	Fields map[string]*graphql.FieldDefinition
}

NodeType defines the configuration for a node type.

type PageInfo

type PageInfo struct {
	HasPreviousPage bool
	HasNextPage     bool
	StartCursor     string
	EndCursor       string
}

PageInfo represents the page info of a GraphQL Cursor Connection.

type PersistedQueryStorage

type PersistedQueryStorage interface {
	// GetPersistedQuery should return the query if it's available or an empty string otherwise.
	GetPersistedQuery(ctx context.Context, hash []byte) string

	// PersistQuery should persist the query with the given hash.
	PersistQuery(ctx context.Context, query string, hash []byte)
}

PersistedQueryStorage represents the storage backend for persisted queries. Storage operations are done on a best effort basis and cannot return errors – any errors that happen internally will not prevent the execution of a query (though it might force clients to make additional requests).

type RequestInfo

type RequestInfo struct {
	Cost int
}

type SubscriptionSourceStream

type SubscriptionSourceStream struct {
	// A channel of events. The channel can be of any type.
	EventChannel interface{}

	// Stop is invoked when the subscription should be stopped and the event channel should be
	// closed.
	Stop func()
}

SubscriptionSourceStream defines the source stream for a subscription.

func (*SubscriptionSourceStream) Run

func (s *SubscriptionSourceStream) Run(ctx context.Context, onEvent func(interface{})) error

Run drives the stream until it's closed or until the given context is cancelled.

type TimeBasedConnectionConfig

type TimeBasedConnectionConfig struct {
	// An optional description for the connection.
	Description string

	// A required prefix for the type names. For a field named "friendsConnection" on a User type,
	// the recommended prefix would be "UserFriends". This will result in types named
	// "UserFriendsConnection" and "UserFriendsEdge".
	NamePrefix string

	// This function should return a TimeBasedCursor for the given edge.
	EdgeCursor func(edge interface{}) TimeBasedCursor

	// Returns the fields for the edge. This should always at least include a "node" field.
	EdgeFields map[string]*graphql.FieldDefinition

	// The getter for the edges. If limit is zero, all edges within the given range should be
	// returned. If limit is greater than zero, up to limit edges at the start of the range should
	// be returned. If limit is less than zero, up to -limit edge at the end of the range should be
	// returned.
	EdgeGetter func(ctx *graphql.FieldContext, minTime time.Time, maxTime time.Time, limit int) (interface{}, error)

	// An optional map of additional arguments to add to the connection.
	Arguments map[string]*graphql.InputValueDefinition

	// To support the "totalCount" connection field, you can provide this method.
	ResolveTotalCount func(ctx *graphql.FieldContext) (interface{}, error)
}

TimeBasedConnectionConfig defines the configuration for a time-based connection that adheres to the GraphQL Cursor Connections Specification.

type TimeBasedCursor

type TimeBasedCursor struct {
	Nano int64
	Id   string
}

TimeBasedCursor represents the data embedded in cursors for time-based connections.

func NewTimeBasedCursor

func NewTimeBasedCursor(t time.Time, id string) TimeBasedCursor

NewTimeBasedCursor constructs a TimeBasedCursor.

Directories

Path Synopsis
cmd
examples
chat Module
ast

Jump to

Keyboard shortcuts

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