GraphQL Part 2
Table of Contents
- Recap
- Mutations
- Subscriptions
- Authentication Versus Authorisation
- Auth in GraphQL
Recap
1 query NameAndId {
2 allUsers {
3 name
4 id
5 }
6 }
Last LT we discussed:
- Queries
- Forming our own complex queries
- A comparison to an equivalent RESTful API Query
- The structure of a query (fields, arguments, aliases, fragments, operation type/name and variables)
- A live demo
Above is a simple example of a query as seen from last talk. Under users, we are requesting both name and id back as seen on live 3 and 4 respectively. The server once this request is received, resolves the query to some actual values which typically would come from some other database or repository. The result of the query is then returned back to the client, with a very similar shape to that of the request.
1 const resolvers = {
2 Query: {
3 allGrads: () => grads,
4 grad(obj, args, context, info) {
5 return grads.find((grad) => grad.id === args.id);
6 },
7 },
8 };
- This is a simple resolver taken from the last talk as a reminder
- There are resolvers for two different query types
- We have one for allGrads (which we can infer returns a list of all the grads)
- And we have one for a single Grad
- Resolvers can take up to 4 arguments as shown in the grad resolver
- Obj/Parent: which is the return value of the parent resolver
- Args: Which contains the arguments provided for that field
- Context: Which is shared across all resolvers
- Info: Contains data about an operations execution state (like field name or the path to the resolver)
Tables of Contents
- Recap
- Mutations
- Subscriptions
- Authentication Versus Authorisation
- Authorisation in GraphQL
Mutations
- Provide a way to create/update data
- Query/Mutate fields in a single request
- Updates existing data or creates new data
- Typically we would want permissions on who can update/create data
- We also get data back when we use a mutation, so a second request is not required to fetch the data which we just manipulated
# Schema
1 type Mutation {
2 createUser(input: UserInput): User
3 }
- Before we had a query type, we now have a schema of type: Mutation
- As a reminder, the schema definition language (SDL) acts like the contract between the server and the client, it defines what GraphQL APIs can and can't do
// Resolver
1 Mutation: {
2 createUser: (parent, args, context, info) => {
3 const newUser = {
4 name: args.input.name,
5 age: args.input.age
6 }
7 users.push(newUser); // Simulate adding to DB
8 return newUser
9 }
10 }
- This is a mutation resolver
- It essentially has the same structure to the resolver we saw earlier
- The main difference being that we are creating some data on a store within the resolver (lines 3-7)
- Typically we would see some kind of authorisation request as mutations can be a destructive operation
Table of Contents
- Recap
- Mutations
- Subscriptions
- Authentication Versus Authorisation
- Authorisation in GraphQL
Subscriptions
- Subscriptions, like queries, fetch data
- Long lasting connection
- Most commonly utilise WebSocket
- Best used to make small, incremental changes to large objects
- Or to receive low-latency real-time updates (like for a chat application)
How it works (Very basic level):
- A client subscribes to specific events
- The server publishes these events when they occur
- The client receives these updates in real time
# Schema
1 type Subscription {
2 messageSent: Message
3 }
- The messageSent field will update its value whenever a new Post is created on the backend
- This will push the message to the subscribing clients
// Resolver
1 Subscription: {
2 messageSent: {
3 subscribe: () => pubsub.asyncIterator([MESSAGE_SENT]);
4 }
5 }
- This is a subscription resolver
- This is what is called when we make a request to messageSent
- PubSub is used in this example (an import from apollo-server)
- Subscribe is a function which returns an asyncIterator from pubsub
- This is listening for a MESSAGE_SET event, when received the iterator notifies subscribed clients
1 const { PubSub } = require("apollo-server");
2 const pubSub = new PubSub();
- Messaging Service
- Decouples producers and receivers
- Allows for services to communicate asynchronously
- Publisher communicate asynchronously to subscribers by broadcasting events
- The subscribers listen for these events
- PubSub is just one example of a service, there are many different libraries which can be used instead
Subscriptions
pubSub.asyncIterator([MESSAGE_SENT])
- Standard interface for iterating over asynchronous results
- Listens for events associated with a label (or set of labels)
- Adds these events to a queue for processing
- Don't know when/in what order we will receive these events, so we need the asyncIterator
Table of Contents
- Recap
- Mutations
- Subscriptions
- Authentication Versus Authorisation
- Authorisation in GraphQL
Authentication
- Verifying the true identity of a user/entity
- Drivers license to authenticate your identity
- Ensures system security
- Proving you are who you say you are
Authentication Types
- Passwords, Tokens, SSO, Biometrics, MFA
- Requires credentials or some other information to prove identity
Authorisation
- What a user/identity is allowed to do
- Occurs after authentication
- Use policies/rules to determine authorisation
- Example: Airline needs to determine who is allowed on board
Combined
- Would need both ID (Passport) and a plane ticket to board a flight
- Required for most APIs
- Prevents request spams
- Limits access to sensitive data
RESTful API: Auth
- A user (with an API token perhaps), may or may not have access to an endpoint
- RESTful APIs have many endpoints
- Easy to simply allow or deny at each endpoint
Table of Contents
- Recap
- Mutations
- Subscriptions
- Authentication Versus Authorisation
- Authorisation in GraphQL
Authorisation in GraphQL
- All users hit one endpoint
- How can we implement access controls?
- GraphQL queries will always hit the same endpoint, this is different to the behaviour of RESTful APIs with many different endpoints, key to note
Authorisation in GraphQL
- We can use the context object in our resolvers!
1 Query: {
2 allPeople: (parent, args, context, info) => {
3 if (!context.user) throw new AuthenticationError("You must be logged in!");
4 return allPeople; // Simulate from DB
5 };
6 }
- Working backwards a little bit here, pretending that a users information is already storied in the context object
- Either throw an error, or return the requested data based on if a user exists
- The context object is shared amongst all resolvers, so similar checks can be made in other resolvers
- Could also be role-based
Authorisation in GraphQL
1 const server = new ApolloServer({
2 typeDefs,
3 resolvers,
4 context,
5 });
6
7 server.listen().then(({ url }) => {
8 console.log(`Server ready at ${url}`);
9 });
- Just to quickly show how we setup the apolloServer from our last talk
- Notice we pass in the context object which will be passed to our resolvers
Authorisation in GraphQL
1 const context = ({ req }) => {
2 const token = req.headers.authorisation || "";
3 const user = jwt.verify(token, SECRET);
4 return { user };
5 };
- An example of what our context object may lok like, using JWT tokens here
Final Considerations
- Where to put authentication/authorisation
- Throwing errors
- We could perform auth at the server level, or resolver level etc.
Final Considerations
query allUsersAndBankDetails(userId: $userId) {
allUsers{
name
age
}
userBankDetails(userId: $userID) {
accountNumber
password
}
}
- Hypothetically a user might have permission to query allUsers, but not a specific userBankDetails which are not their own
- If we throw an error saying they are un-authenticated, we would lose all data (including allUsers)
- This may not be behaviour we want
- Perhaps we just want to return nothing for userBankDetails to signify a lack of permissions, but still return allUsers data