GraphQL, real time (MQTT) & offline programming. Integrate with DynamoDB (schema -> resource, import from existing DynamoDB table)
Wizard : (1) define a model with an editor (2) configure table (3) create table & automatically generated gateway (with schema & resolver)
Examples: some template gateway & tables, good for study & a tour
GraphQL Proxy: runs GraphQL engine, process requests, map to logical functions, then data resolution process do a batching process (Data Loader) to data source. Also manages conflict detection & resolution strategies.
Operation: supports query, mutation, subscription
Action: AWS defined one action, notification to connected subscribers (resulted from mutation).
Data Source: persistent storage or a trigger together with credentials.
Resolver: converts payload to storage system protocol & executions. Comprised of request/response mapping templates & transformation / execution logic.
Identity: caller represented by credentials. Must be sent with each request. Pass (as context) to resolver & conflict handler
AppSync client
One schema, one API
Structure:
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
type Query {
getEvent(id: ID!): Event
...
}
type Mutation {
createEvent(...): Event
...
}
type Subscription {
subscribeToEventComments(eventId: String!): Comment
@aws_subscribe(mutations: ["commentOnEvent"])
...
}
type Event {
...
}
Console can:
ID, String, Int, Float, Boolean
AWSDate - ISO 8601 Date string YYYY-MM-DD, YYYY-MM-DDZ, YYYY-MM-DD±HH:MM
AWSTime - extended ISO 8601, e.g. 12:30:24.500+05:30
AWSDateTime - YYYY-MM-DDThh:mm:ss.sssZ
AWSTimeStamp - number of seconds elapsed since epoch
AWSEmail - email
AWSJSON
AWSURL
AWSPhone
AWSIPAddress
Type resolution: __typename
Various types (see below) of data sources are provided. AppSync data source comprises (1) reference to the resource behind (like DynamoDB, Lambda) and (2) roles for AppSync to call the resource.
Resolvers are how the resource get called. Resolver is comprised of (1) the data source, so end point is provided (2) request mapping template, so request get generated, (3) response mapping template, so the fetched data get mapped. Then, since AppSync schema is strong typed, the mapped result will be checked against the schema. If, for example, a required field in the return type of the schema is missing from the mapped response, the request fails.
Automatic support provided to DynamoDB resource. It's a good idea to look at the generated resolver and start from there.
Where to use resolver and how it works.
Start from the root of the request - query & mutation. They need resolvers to work. Check generated resolver there to get the ideas. Note: the name appeared under "Resolver" in AppSync console is actually name of the resource. New user may become confused and think the resolvers are the same. No they are not. The request/response templates would be different.
NOTE: if a query get no resolver attached, the result would be null, and NO ERROR reported.
As for fields of types, they usually don't have resolver attached, so long the resolved result contains a matched property. However if it does not (like the connection type of a child, see "comments" of "Event" type in the example), a provider is needed. In the templates, the $context.source would become available, which is the parent's resolved result.
Resolver for subscriptions work at the time user subscribe to a subscription. Resolver can do the same things with request / response template & back-end data source, do fine-grained authorization, return initial result (which match the defined return type) .
Uses Mapping Templates, see: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-programming-guide.html#aws-appsync-resolver-mapping-template-reference-programming-guide & https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-programming-guide.html
Testing & Debugging: https://docs.aws.amazon.com/appsync/latest/devguide/test-debug-resolvers.html#aws-appsync-test-debug-resolvers
Mapping Template translates GraphQL request to backend request & translates backend response to GraphQL response. Written in Velocity VTL (provides condition, looping, etc. for complex instructions).
Able to do: validation, conversion, etc. with VTL. can do filtering & authentication with caller information in the context
To start: use a lambda to return everything, set the lambda as resolver, and check what's provided in the request as a starting point.
About VTL & AppSync:
Auto generated queries:
And input type for filtering "listYourTypes" TableYourTypeFilterInput:
Auto generate YourTypeConnection type as result of "listYourTypes" query:
Auto generated mutations & input for mutations:
onCreateYourType, onUpdateYourType, onDeleteYourType
able to do queries
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Subscriptions are invoked as a response to a mutation.
Define in schema like:
type Subscription {
updatedPost(id:ID! author:String): Post
@aws_subscribe(mutations: ["updatePost"])
}
Subscrube from client like:
subscription UpdatedPostSub {
updatedPost(id:"XYZ", author:"ABC") {
title
content
}
}
Client connection is handled by SDK using MQTT over WebSocket. Authentication with IAM, Cognito ID pool, Cognito user pools for field-level fine-grained authorization. Can attach resolvers to subscription fields.
Subscriptions are triggered from mutations. @aws_subscribe takes an array of mutation as inputs so one subscription can be triggered by multiple mutations.
Mutation selection set is sent to subscribers. Client can choose what fields are sent back.
Filtering: Current behavior is to use one or more argument to affect when & how clients are notified. Arguments can be required or optional. Logic is matching. There's a discussion here: https://stackoverflow.com/questions/50037650/custom-filtering-on-subscription-in-aws-appsync in which Michael, who is software engineer at AWS AppSync, answered that custom filter is not supported and "may be considered in the future".
[specific API] / Setting : require a role to log to cloud watch
[specific API] / [specific data source] : require a role to access back-end data source
The role needs an IAM policy like above
API_KEY: issued from console, can be used to control throttling, hard coded in application, not very secure, on client specified in header "x-api-key"
AWS_IAM: use IAM to control access, resource can be fine-grained to certain query/mutation/subscription (see code example in document)
OPENID_CONNECT: configure with OIDC URL, AppSync service will fetch configuration from that URL (with some added path).
AMAZON_COGNITO_USER_POOLS: provide cognito user pool token in header, can use cognito group to limit access with @aws_auth directive.
type Query {
posts:[Post!]!
@aws_auth(cognito_groups: ["Bloggers", "Readers"])
}
type Mutation {
addPost(id:ID!, title:String!):Post!
@aws_auth(cognito_groups: ["Bloggers"])
}
...
Can use CLI command to specify grant-or-deny strategy
$ aws appsync --region us-west-2 create-graphql-api --authentication-type AMAZON_COGNITO_USER_POOLS --name userpoolstest --user-pool-config '{ "userPoolId":"test", "defaultEffect":"ALLOW", "awsRegion":"us-west-2"}'
Use mapping templates & filtering.
Mapping template: for example, the add mutation store the user identity information in table for comparison, then update mutation's request template would has a condition to compare or look for the identity information.
Filtering information: use a response template to decide what to RETURN to user (not the response of back-end, which is controlled by the request to back-end)
Some complex examples are given in user guide that is worth studying.
One technique is user a resolver with a permission data source (a permission dynamoDB table for example).
Control subscription: can make decision at time client makes a subscription with a resolver. Resolver can query data from data source, perform conditional logic in request / response mapping template. Can return additional data to client such as initial results from a subscription.
See "Event" example, may do with DynamoDB next token mechanism. That means user cannot jump to any page. Also client need to keep the tokens for "prev" navigation. See https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/57
CLI available (for management not for invoking, it seems):
Mobile & web client:
Node.js client:
For a newbie, start from playing with the examples and look into the resolvers.
To build:
{
"version" : "2017-02-28",
"payload": $util.toJson($context)
}
Query with pagination. Support range key condition.
type Query {
querySubjectAccount(
account: String!,
minSk: String,
maxSk: String,
limit: Int,
exclusiveStartKey: String,
forward: Boolean = true
): AWSJSON
}
#if($context.args.minSk)
#if($context.args.maxSk) #set( $skExpression = "AND sk BETWEEN :minSk AND :maxSk")
#else #set( $skExpression = "AND sk >= :minSk")
#end
#elseif ($context.args.maxSk) #set( $skExpression = "AND sk <= :maxSk")
#else #set( $skExpression = "")
#end
{
"version" : "2017-02-28",
"operation" : "Query",
"query" : {
"expression": "pk = :pk ${skExpression}",
"expressionValues" : {
":pk" : {
"S" : "${context.identity.sub}:${ctx.args.account}"
},
#if($context.args.minSk)
":minSk" : {
"S" : "${ctx.args.minSk}"
},
#end
#if($context.args.maxSk)
":maxSk" : {
"S" : "${ctx.args.maxSk}"
},
#end
},
},
## Add 'limit' and 'nextToken' arguments to this field in your schema to implement pagination. **
"limit": $util.defaultIfNull(${ctx.args.limit}, 20),
"scanIndexForward": #if($ctx.args.forward) true #else false #end,
"nextToken": $util.toJson($util.defaultIfNullOrBlank($ctx.args.nextToken, null))
}