Documentation
¶
Overview ¶
Package appsync routes the invocations of AWS AppSync GraphQL direct Lambda resolvers (and of the Amplify GraphQL Transformer @function directive) to typed Go resolvers, one per type and field. It is the Go side of Powertools for AWS Lambda (Python)'s AppSyncResolver.
Getting started ¶
Register a resolver per field with the generic Resolver function. Its arguments type receives the GraphQL arguments of the field, decoded from JSON; its result is encoded back as JSON. Router.Resolve is the Lambda handler:
type GetTodoArgs struct {
ID string `json:"id"`
}
func main() {
app := appsync.New()
appsync.Resolver(app, "Query", "getTodo", func(ctx context.Context, args GetTodoArgs) (Todo, error) {
return todos.Get(ctx, args.ID)
})
appsync.Resolver(app, "Query", "listTodos", func(ctx context.Context, _ struct{}) ([]Todo, error) {
return todos.List(ctx)
})
lambda.Start(app.Resolve)
}
The type comes from info.parentTypeName and the field from info.fieldName (the top-level parentTypeName or typeName and fieldName of Amplify payloads). A resolver registered for AnyType answers the field under any type; one registered for the exact type wins over it. To resolve several fields with one function (Powertools Python's stacked decorators), register it once per field.
The event and the Lambda context ¶
Resolve keeps the invocation in the context it passes down. CurrentEvent returns the event of a single invocation, CurrentBatch the events of a batch, as Event values that promote every member of eventsource.AppSyncResolverEvent (Identity, Source, Request, Info, Stash, Prev). Event.DecodeSource decodes the parent object of the field into a Go type. DecodeEvent decodes the whole payload into a type of your own, the Go side of Powertools Python's data_model. The Lambda context comes from lambdacontext.FromContext(ctx).
Powertools Python's append_context, the data a handler shares with the resolvers of every router, is a value stored in the context before Resolve runs, by the handler or by a middleware:
lambda.Start(func(ctx context.Context, event jsontext.Value) (jsontext.Value, error) {
return app.Resolve(context.WithValue(ctx, tenantKey{}, tenantOf(event)), event)
})
Batch resolvers ¶
With batching enabled on the data source, AppSync sends many requests for the same field in one invocation, an array of events, which Resolve tells from a single event by the payload. BatchResolver registers a resolver that receives the whole batch and returns one result per event, in the same order: the answer to the N+1 problem. BatchItemResolver registers one called once per event instead; a failing event gets a null result, unless WithRaiseOnError makes the first failure fail the invocation. WithConcurrency resolves several events at the same time. A field may have a single resolver and a batch resolver at once: each serves its kind of invocation.
Errors ¶
An error from a resolver fails the invocation; AppSync turns it into a GraphQL error whose errorType is the name of the error's type and whose message is the error's message. Resolve returns errors as they are, never wrapped, so that the type is the resolver's own. To choose the errorType freely, return a messages.InvokeResponse_Error from github.com/aws/aws-lambda-go/lambda/messages, which aws-lambda-go reports as it is.
OnError registers a handler for a type of error: it turns the failure into the response of the invocation, as Powertools Python's exception_handler. Handlers are tried in registration order and the first whose type is in the error's chain wins.
The router's own failures are *ResolverNotFoundError (no resolver for the type and field), *ArgumentsError (arguments that do not decode), *InvalidBatchResponseError (a batch resolver that returned a different number of results) and *InvalidEventError (a payload that is not an AppSync event).
Routers ¶
Resolvers can be registered on several Routers, one per file or domain, and merged into the application with Include, Powertools Python's include_router.
Scalars ¶
eventsource.MakeID, AWSDate, AWSTime, AWSDateTime and AWSTimestamp return values of the AppSync scalars, as Powertools Python's scalar_types_utils.
Environment variables ¶
POWERTOOLS_DEBUG writes what the router does to standard error: the routing decisions and the events that failed. No other variable applies, as in Powertools Python.
Differences from Python ¶
- Arguments are a typed struct, not keyword arguments; results are encoded with encoding/json/v2.
- There are no async resolvers. BatchItemResolver with WithConcurrency is async_batch_resolver; a single resolver blocks as any Go function does. Python's precedence of a sync batch resolver over an async one for the same field does not arise.
- No shared mutable state: current_event, current_batch_event, lambda_context and the context dictionary are context values, so concurrent invocations (Lambda Managed Instances) are safe and nothing needs clearing.
- Registering a second resolver for the same type and field, directly or through Include, panics (Python lets the last one win silently), and so does a registration without a field name (Python accepts it and never matches it).
- A batch resolver must return exactly one result per event (Python only checks that it returns a list), and an empty batch resolves to an empty array (Python fails with an IndexError).
- An identity that is not an object (the string "None" in some sample events) reads as no identity instead of failing the invocation.
- Error handlers match by errors.AsType in registration order, not by the most specific class.
Index ¶
- Constants
- Variables
- func BatchItemResolver[A, R any](r *Router, typeName, fieldName string, ...)
- func BatchResolver[A, R any](r *Router, typeName, fieldName string, ...)
- func DecodeEvent[T any](ctx context.Context) (T, error)
- func OnError[E error, R any](r *Router, fn func(ctx context.Context, err E) (R, error))
- func Resolver[A, R any](r *Router, typeName, fieldName string, ...)
- type ArgumentsError
- type BatchOption
- type Event
- type InvalidBatchResponseError
- type InvalidEventError
- type ResolverNotFoundError
- type Router
Examples ¶
Constants ¶
const AnyType = "*"
AnyType registers a resolver for a field whatever its parent type, as Powertools Python's default type_name "*" does. A resolver registered for the exact type wins over it.
Variables ¶
var ErrNoEvent = errors.New("appsync: no AppSync resolver event in the context")
ErrNoEvent is returned by DecodeEvent when the context does not come from Router.Resolve.
Functions ¶
func BatchItemResolver ¶
func BatchItemResolver[A, R any](r *Router, typeName, fieldName string, fn func(ctx context.Context, event Event[A]) (R, error), opts ...BatchOption)
BatchItemResolver registers fn as the resolver of the field fieldName of the type typeName for batch invocations, called once per event: Powertools Python's batch_resolver with aggregate=False. The results are returned in the order of the batch. It moves the N+1 problem from AppSync into the function rather than solving it; prefer BatchResolver when the data source can fetch the whole batch at once.
By default a failing event (an error from fn, or arguments that do not decode into A) gets a null result and the others are resolved; see WithRaiseOnError and WithConcurrency. A panic in fn is not recovered.
It panics as Resolver does.
Example ¶
package main
import (
"context"
"encoding/json/jsontext"
"errors"
"fmt"
"github.com/alessandrolattao/powertools-lambda-go/eventhandler/appsync"
)
func main() {
type Args struct {
PostID string `json:"post_id"`
}
titles := map[string]string{"1": "post1"}
app := appsync.New()
appsync.BatchItemResolver(app, "Query", "relatedPosts", func(_ context.Context, event appsync.Event[Args]) (string, error) {
title, ok := titles[event.Arguments.PostID]
if !ok {
return "", errors.New("post not found") // a null result, unless WithRaiseOnError(true)
}
return title, nil
}, appsync.WithConcurrency(10))
event := jsontext.Value(`[
{"arguments": {"post_id": "1"}, "info": {"parentTypeName": "Query", "fieldName": "relatedPosts"}},
{"arguments": {"post_id": "9"}, "info": {"parentTypeName": "Query", "fieldName": "relatedPosts"}}
]`)
out, err := app.Resolve(context.Background(), event)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
Output: ["post1",null]
func BatchResolver ¶
func BatchResolver[A, R any](r *Router, typeName, fieldName string, fn func(ctx context.Context, events []Event[A]) ([]R, error))
BatchResolver registers fn as the resolver of the field fieldName of the type typeName for batch invocations (AppSync's batching of direct Lambda resolvers, the answer to the N+1 problem), in aggregate mode: fn receives the whole batch and returns one result per event, in the same order. This is Powertools Python's batch_resolver with aggregate=True.
A result list of a different length fails the invocation with an *InvalidBatchResponseError (Powertools Python only checks that it is a list). An error from fn, or from decoding the arguments of any event, fails the invocation unless an OnError handler turns it into a response.
It panics as Resolver does.
Example ¶
package main
import (
"context"
"encoding/json/jsontext"
"fmt"
"github.com/alessandrolattao/powertools-lambda-go/eventhandler/appsync"
)
func main() {
type Post struct {
PostID string `json:"post_id"`
}
titles := map[string]string{"1": "post1", "2": "post2"}
app := appsync.New()
// One invocation for the relatedPosts field of every post of the query.
appsync.BatchResolver(app, "Post", "relatedPosts", func(_ context.Context, events []appsync.Event[struct{}]) ([]string, error) {
results := make([]string, len(events))
for i, event := range events {
post, err := event.DecodeSource[Post]()
if err != nil {
return nil, err
}
results[i] = titles[post.PostID] // one batch query in real life
}
return results, nil
})
event := jsontext.Value(`[
{"source": {"post_id": "1"}, "info": {"parentTypeName": "Post", "fieldName": "relatedPosts"}},
{"source": {"post_id": "2"}, "info": {"parentTypeName": "Post", "fieldName": "relatedPosts"}}
]`)
out, err := app.Resolve(context.Background(), event)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
Output: ["post1","post2"]
func DecodeEvent ¶
DecodeEvent decodes the payload of the invocation, as AppSync sent it, into T: an object for a single invocation, an array for a batch. It is the Go side of Powertools Python's data_model: T is a type of your own that embeds eventsource.AppSyncResolverEvent (or a slice of them) and adds the members and methods your resolvers need.
func OnError ¶
OnError registers fn as the handler of the errors of type E: when resolving an invocation fails with an error that has an E in its chain (errors.AsType), what fn returns, encoded as JSON, becomes the response of the invocation instead, as with Powertools Python's exception_handler. An error from fn fails the invocation. The context fn receives carries the event (CurrentEvent, CurrentBatch) unless the payload itself was invalid.
Handlers are tried in the order they were registered and the first match wins, so register the most specific types first. They see every failure of Resolve: the resolver's own errors, *ResolverNotFoundError, *ArgumentsError, *InvalidBatchResponseError and *InvalidEventError, but not the item errors a BatchItemResolver turns into null results.
Example ¶
package main
import (
"context"
"encoding/json/jsontext"
"fmt"
"github.com/alessandrolattao/powertools-lambda-go/eventhandler/appsync"
)
// ValidationError is an error of the application's own.
type ValidationError struct{ Field string }
func (e *ValidationError) Error() string { return "invalid " + e.Field }
func main() {
app := appsync.New()
appsync.OnError(app, func(_ context.Context, err *ValidationError) (map[string]string, error) {
return map[string]string{"error": err.Error()}, nil
})
appsync.Resolver(app, appsync.AnyType, "createSomething", func(context.Context, struct{}) (string, error) {
return "", &ValidationError{Field: "name"}
})
out, err := app.Resolve(context.Background(), jsontext.Value(`{"fieldName": "createSomething"}`))
fmt.Println(string(out), err)
}
Output: {"error":"invalid name"} <nil>
func Resolver ¶
func Resolver[A, R any](r *Router, typeName, fieldName string, fn func(ctx context.Context, args A) (R, error))
Resolver registers fn as the resolver of the field fieldName of the type typeName (or of any type, with AnyType) for single invocations, the default of direct Lambda resolvers. The GraphQL arguments of the field are decoded into A (a struct with json tags, a map, or struct{} when the field takes none); the result is encoded as JSON. Registering the same function for several fields is how Powertools Python's stacked decorators read in Go.
The full event is available through CurrentEvent(ctx), the Lambda context through lambdacontext.FromContext(ctx). An error from fn fails the invocation unless an OnError handler turns it into a response; it is returned as it is, so that AppSync shows the name of its type as the errorType of the GraphQL error.
Resolver panics if typeName or fieldName is empty or a resolver is already registered for them: both are programming errors.
Example ¶
package main
import (
"context"
"encoding/json/jsontext"
"fmt"
"github.com/alessandrolattao/powertools-lambda-go/eventhandler/appsync"
)
type Todo struct {
ID string `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
type GetTodoArgs struct {
ID string `json:"id"`
}
func main() {
app := appsync.New()
appsync.Resolver(app, "Query", "getTodo", func(_ context.Context, args GetTodoArgs) (Todo, error) {
return Todo{ID: args.ID, Title: "Buy milk"}, nil
})
// In main: lambda.Start(app.Resolve)
event := jsontext.Value(`{
"arguments": {"id": "todo-1"},
"info": {"parentTypeName": "Query", "fieldName": "getTodo"}
}`)
out, err := app.Resolve(context.Background(), event)
fmt.Println(string(out), err)
}
Output: {"id":"todo-1","title":"Buy milk","completed":false} <nil>
Types ¶
type ArgumentsError ¶
ArgumentsError reports GraphQL arguments that do not decode into the arguments type of the resolver.
func (*ArgumentsError) Error ¶
func (e *ArgumentsError) Error() string
Error describes the arguments that failed.
func (*ArgumentsError) Unwrap ¶
func (e *ArgumentsError) Unwrap() error
Unwrap returns the decoding error.
type BatchOption ¶
type BatchOption func(*batchConfig)
BatchOption configures a BatchItemResolver.
func WithConcurrency ¶
func WithConcurrency(n int) BatchOption
WithConcurrency resolves up to n events of the batch at the same time, each in its own goroutine; the default, 1, resolves them one after the other. It is Powertools Python's async_batch_resolver, which gathers the events concurrently, with a bound. The results keep the order of the batch either way. With WithRaiseOnError, the first failure cancels the context of the events still running and no further event starts.
func WithRaiseOnError ¶
func WithRaiseOnError(raise bool) BatchOption
WithRaiseOnError decides what happens when the resolver fails for one event of the batch. Off (the default), the event's result is null, which AppSync reports as an error for that field only, and the other events are resolved. On, the first failure fails the whole invocation, through the OnError handlers, as Powertools Python's raise_on_error=True.
type Event ¶
type Event[A any] struct { eventsource.AppSyncResolverEvent // Arguments are the arguments of the field being resolved. Arguments A // contains filtered or unexported fields }
Event is one AppSync resolver event with its GraphQL arguments decoded into A. Batch resolvers receive it; CurrentEvent and CurrentBatch return it with the arguments as a map.
Every member of eventsource.AppSyncResolverEvent is promoted (Identity, Source, Request, Prev, Info, Stash, and the Field and ParentType methods). Arguments shadows the untyped map of the embedded event.
func CurrentBatch ¶
CurrentBatch returns the events of a batch invocation from a context that comes from Router.Resolve, as Powertools Python's app.current_batch_event. A single invocation has none: see CurrentEvent.
func CurrentEvent ¶
CurrentEvent returns the event of a single invocation from a context that comes from Router.Resolve, as Powertools Python's app.current_event. A batch invocation has none: see CurrentBatch.
Example ¶
package main
import (
"context"
"encoding/json/jsontext"
"fmt"
"github.com/alessandrolattao/powertools-lambda-go/eventhandler/appsync"
)
func main() {
type Merchant struct {
AccountType string `json:"accountType"`
}
app := appsync.New()
// A field of a non-root type: its parent object is the source.
appsync.Resolver(app, "Merchant", "extraInfo", func(ctx context.Context, _ struct{}) (map[string]string, error) {
event, _ := appsync.CurrentEvent(ctx)
merchant, err := event.DecodeSource[Merchant]()
if err != nil {
return nil, err
}
method := "USD"
if merchant.AccountType == "NEW" {
method = "BTC"
}
return map[string]string{"preferredPaymentMethod": method}, nil
})
event := jsontext.Value(`{
"source": {"accountType": "NEW"},
"info": {"parentTypeName": "Merchant", "fieldName": "extraInfo"}
}`)
out, err := app.Resolve(context.Background(), event)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
Output: {"preferredPaymentMethod":"BTC"}
func (Event[A]) DecodeSource ¶
DecodeSource decodes the parent object of the field (the source, null for a root field of Query or Mutation) into S. An event built by hand, without the raw source, decodes its Source map instead.
type InvalidBatchResponseError ¶
type InvalidBatchResponseError struct {
TypeName string
FieldName string
// Items is the size of the batch, Results the number of results the
// resolver returned.
Items, Results int
}
InvalidBatchResponseError reports a batch resolver whose results do not match its batch one to one: AppSync pairs results and requests by position, so a result missing or in excess would answer the wrong field.
func (*InvalidBatchResponseError) Error ¶
func (e *InvalidBatchResponseError) Error() string
Error describes the mismatch.
type InvalidEventError ¶
type InvalidEventError struct {
Err error
}
InvalidEventError reports an invocation payload that is not an AppSync resolver event: neither an object (a single invocation) nor an array of objects (a batch).
func (*InvalidEventError) Error ¶
func (e *InvalidEventError) Error() string
Error describes the payload problem.
func (*InvalidEventError) Unwrap ¶
func (e *InvalidEventError) Unwrap() error
Unwrap returns the decoding error.
type ResolverNotFoundError ¶
ResolverNotFoundError reports an invocation for a type and field that no resolver is registered for (Powertools Python raises ValueError for a single invocation and ResolverNotFoundError for a batch). Batch tells which registry was searched: a single resolver does not resolve a batch, nor a batch resolver a single event.
func (*ResolverNotFoundError) Error ¶
func (e *ResolverNotFoundError) Error() string
Error returns the message Powertools Python uses.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router routes the invocations of AppSync direct Lambda resolvers to the resolvers registered for their type and field. Resolve is its Lambda handler. The same type serves as the application and as a group of resolvers merged into it with Include.
Register every resolver before the first invocation: registration is not safe concurrently with Resolve, which only reads, and so serves concurrent invocations safely. The zero Router is ready to use.
func (*Router) Include ¶
Include adds the resolvers and error handlers of other to r, as Powertools Python's include_router. Resolvers are copied: register them on other before including it. Error handlers of other come after those of r.
Include panics if r and other both have a resolver for the same type and field (Powertools Python lets other overwrite it silently).
Example ¶
package main
import (
"context"
"encoding/json/jsontext"
"fmt"
"github.com/alessandrolattao/powertools-lambda-go/eventhandler/appsync"
)
func main() {
// In a file of its own, the resolvers of one domain.
locations := appsync.New()
appsync.Resolver(locations, appsync.AnyType, "listLocations", func(context.Context, struct{}) ([]string, error) {
return []string{"Perry, James and Carroll"}, nil
})
app := appsync.New()
app.Include(locations)
out, err := app.Resolve(context.Background(), jsontext.Value(`{"typeName": "Query", "fieldName": "listLocations"}`))
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
Output: ["Perry, James and Carroll"]
func (*Router) Resolve ¶
Resolve is the Lambda handler: it resolves the invocation event with the registered resolvers, telling a single invocation (an object) from a batch (an array) by the payload, and returns the JSON response for AppSync.
lambda.Start(app.Resolve)
It composes with middlewares as any handler does. Values a middleware stores in ctx reach the resolvers: this is how Powertools Python's append_context reads in Go. An empty batch resolves to an empty array.
Example (NotFound) ¶
package main
import (
"context"
"encoding/json/jsontext"
"errors"
"fmt"
"github.com/alessandrolattao/powertools-lambda-go/eventhandler/appsync"
)
func main() {
app := appsync.New()
_, err := app.Resolve(context.Background(), jsontext.Value(`{"typeName": "Query", "fieldName": "getTodo"}`))
// aws-lambda-go reports the type of the error as the errorType that
// AppSync shows in the GraphQL error.
if notFound, ok := errors.AsType[*appsync.ResolverNotFoundError](err); ok {
fmt.Println(notFound.TypeName, notFound.FieldName)
}
fmt.Println(err)
}
Output: Query getTodo No resolver found for 'Query.getTodo'