And here we are with part two of our series on AWS Amplify! if you missed part one, you can check it out here.
GraphQL API Directives
As promised we are going to briefly talk about the set of directives that if correctly used should help a dev to save a bunch of time while setting an API.
@model
This directive indicates that the type defined in the schema should also be treated as resource exposed by the API, that means that it needs to have a database to store its values as well as rules defined to access it. Besides that this directive also defines a set of operations by default that generate a bunch of logic to handle the creating, updating, deleting of records as well as the fetching of that data from the DB, all that while complying with the set of rules defined to access that specific resource. We can override this default operations or even disable them if we want.
@key
This directive defines a way of ordering and searching a collection or a model based on one or a couple of columns. Additionally under the hood this also defines an index to help improve the search times in case they ever get slow.
@auth
We briefly touched on this while talking about the model directive, but this directive helps us to define a set of rules that apply while accessing the defined model. This can be while accessing one operation or a set of them so only the owner of the post can delete it or update it, while everybody else in the platform can view it and vote on it as an example. There is a wide variety of combinations of scopes that we can define, I strongly recommend checking out the official docs for a specific breakdown on them.
@connection
This directive helps us to connect multiple models so whenever you are pulling an entity that is tightly coupled with another you can fetch both of them at the same time. To properly achieve this the connection directive relies on the @key directive to help it sort out what records should it bring or not.
@function
This directive allows us to define our own set of rules and logic that a request must complete to get the desired response, basically this directive it’s tied to a Mutation or Query to go over the default resolvers and define our own to handle the logic in that case. It’s really useful when the default operations are not enough or we have another operation that does not necessarily match with the CRUD of a resource.
Here's an example of a complete schema with examples of each of the directives.
type Restaurant @model
@auth(rules: [
# allow all authenticated users to read this type
{ allow: private, operations: [read] },
# only the system admins can create and delete the restaurants
{ allow: groups, groups: ["Admins"], operations: [create, delete] },
# only the owner of the restaurant to update it
{ allow: owner, ownerField: "owner", operations: [update] },
])
{
id: ID!
name: String!
owner: String!
location: Location!
address: String
description: String
profilePicture: S3Object
menu: [MenuItem] @connection(keyName: "byRestaurant", fields: ["id"])
}
type MenuItem @model
@auth(rules: [
# allow all authenticated users to read this type
{ allow: private, operations: [read] },
])
@key(name: "byRestaurant", fields: ["restaurantId", "price"])
{
id: ID!
restaurantId: ID!
name: String!
price: Int!
ingredients: [String]
serves: Int
}
type S3Object {
bucket: String!
region: String!
key: String!
}
type UserProfile @model
@auth(rules: [
# allow all authenticated users to read this type
{ allow: private, operations: [read] },
])
{
id: ID!
username: String!
email: String!
profilePicture: S3Object
}
type FriendRequest @model
@auth(rules: [
# allow every user to create, update, delete
{ allow: owner },
# Authorize the update mutation and both queries.
{ allow: owner, ownerField: "relates", operations: [update, read] }
])
{
id: ID!
owner: String!
relates: [String!]
fromId: ID!
toId: ID!
status: FriendRequestStatus!
}
type Reservation @model {
id: ID!
restaurantId: ID!
attendants: [ID!]
arrivalDate: AWSDateTime!
dishes: [MenuItem] @connection(keyName: "byRestaurant", fields: ["restaurantId"])
}
enum FriendRequestStatus {
pending
accepted
rejected
}
type Location {
lat: Float!
lon: Float!
}
input CreateReservationInput {
restaurantId: ID!
attendants: [ID!]
arrivalDate: AWSDateTime!
dishes: [ID!]
}
# Operations: Queries, mutations and subscriptions
type Query {
topDishes(ofRestaurant: ID!): [MenuItem] @function(name: "topDishes-${env}")
}
type Mutation {
bookReservation(input: CreateReservationInput): Reservation @function(name: "addReservation-${env}")
}
Honorable mentions / Unsung heroes
As mentioned previously the aim of Amplify it’s to help simplify the challenge that can be to understand and use AWS, to do so it relies on a set of services, some of them visible to you as a dev and some other a tad hidden. The following are services that are essential for AWS to work, without them it wouldn’t be possible to achieve what Amplify set out to do.
Cloudformation
It's a service that based on some configuration files (JSON and/or YMAL) triggers the creation, deletion or updating of resources in the cloud. This is vital for Amplify to work the way it does.
IAM
It’s a service that restricts the access of the different components to match our input, if we respect the law of minimum required access for each part, the one that makes sure that it gets enforced its IAM.
SNS
It’s a service that handles the sending of different simple notifications, each time a Cognito or other service sends a SMS or email it’s relying on SNS to achieve that.