apifu

package module
v0.0.0-...-3478735 Latest Latest
Warning

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

Go to latest
Published: Mar 19, 2024 License: MIT Imports: 27 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 graphql/transport directory contains unopinionated libraries for using various transport protocols. For example, it contains transport implementations that allow you to serve your GraphQL API via WebSockets and provide subscription functionality.
  • The cmd/gql-client-gen package provides a CLI tool that can be used to generate types for use by client code. THis allows your client's queries to be type-safe and fully validated at compile time.
Experimental Packages

The above packages are mature and have been thoroughly proven in real-world, production deployments. The following packages have not yet seen such rigorous real-world testing and are thus considered experimental. They are fully functional and well unit tested, but may change at any time and are not yet subject to any compatibility guarantees.

  • The jsonapi package is a library for building JSON:API APIs. It's somewhat high level, but is no more opinionated than JSON:API itself is. However, it does hold some of those opinions more strongly (i.e. it doesn't support violating many of the JSON:API spec's recommendations and "SHOULD"s).

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, the Apollo graphql-ws protocol, and the newer graphql-transport-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: Unless otherwise noted, 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 ConnectionFieldDefinition

func ConnectionFieldDefinition(config *ConnectionFieldDefinitionConfig) *graphql.FieldDefinition

Returns a minimal connection field definition, with default arguments and cost function defined.

func ConnectionInterface

func ConnectionInterface(config *ConnectionInterfaceConfig) *graphql.InterfaceType

Returns an interface for a connection.

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 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 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 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() error

CloseHijackedConnections closes connections hijacked by ServeGraphQLWS.

func (*API) Schema

func (api *API) Schema() *graphql.Schema

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 WebSocket connection. It will serve connections for both the deprecated graphql-ws subprotocol and the newer graphql-transport-ws subprotocol.

This method hijacks connections. To gracefully close them, use CloseHijackedConnections.

type Config

type Config struct {
	Logger               logrus.FieldLogger
	WebSocketOriginCheck func(r *http.Request) bool

	// If given, these fields will be added to the Node interface.
	AdditionalNodeFields map[string]*graphql.FieldDefinition

	// Invoked to get nodes by their global ids.
	ResolveNodesByGlobalIds func(ctx context.Context, ids []string) ([]interface{}, error)

	// 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

	// If given, these function will be executed as the schema is built. It is executed on a clone
	// of the schema and can be used to make last minute modifications to types, such as injecting
	// documentation.
	PreprocessGraphQLSchemaDefinition func(schema *graphql.SchemaDefinition) error

	// If given, this function will be invoked to get the feature set for a request.
	Features func(ctx context.Context) graphql.FeatureSet
	// 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) 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) NodeInterface

func (cfg *Config) NodeInterface() *graphql.InterfaceType

NodeInterface returns the node interface.

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 deprecation reason for the connection.
	DeprecationReason string

	// The direction of the connection. This determines which of the first/last/before/after
	// arguments are defined on the connection.
	Direction ConnectionDirection

	// 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

	// The connection will implement these interfaces. If any of the interfaces define an edge
	// field as an interface, this connection's edges will also implement that interface.
	ImplementedInterfaces []*graphql.InterfaceType

	// This connection is only available for introspection and use when the given features are enabled.
	RequiredFeatures graphql.FeatureSet
}

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

type ConnectionDirection

type ConnectionDirection int
const (
	ConnectionDirectionBidirectional ConnectionDirection = iota
	ConnectionDirectionForwardOnly
	ConnectionDirectionBackwardOnly
)

type ConnectionFieldDefinitionConfig

type ConnectionFieldDefinitionConfig struct {
	// The type of the connection.
	Type graphql.Type

	// The direction of the connection.
	Direction ConnectionDirection

	// An optional description for the connection field.
	Description string

	// An optional deprecation reason for the connection field.
	DeprecationReason string

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

	// This connection is only available for introspection and use when the given features are enabled.
	RequiredFeatures graphql.FeatureSet
}

Defines the configuration for a connection interface.

type ConnectionInterfaceConfig

type ConnectionInterfaceConfig 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

	// 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

	// If true, implementations must provide the "totalCount" field.
	HasTotalCount bool

	// This connection is only available for introspection and use when the given features are enabled.
	RequiredFeatures graphql.FeatureSet
}

Defines the configuration for a connection interface.

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

	// An optional deprecation reason for the connection.
	DeprecationReason 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)

	// The connection will implement these interfaces. If any of the interfaces define an edge
	// field as an interface, this connection's edges will also implement that interface.
	ImplementedInterfaces []*graphql.InterfaceType

	// This connection is only available for introspection and use when the given features are enabled.
	RequiredFeatures graphql.FeatureSet
}

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
This is a package for creating [JSON:API](https://jsonapi.org) APIs.
This is a package for creating [JSON:API](https://jsonapi.org) APIs.

Jump to

Keyboard shortcuts

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