New stuff
This commit is contained in:
parent
3ca32b980e
commit
e8f3d355b3
6 changed files with 207 additions and 26 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.DS_Store
|
|
@ -13,7 +13,7 @@ Apollo Client is the client-side counterpart to [Apollo Server](/Databases/Graph
|
|||
|
||||
## Initializing the client
|
||||
|
||||
We initialise the client and set-up in memory kcaching to reduce network requests:
|
||||
We initialise the client and set-up in memory caching to reduce network requests:
|
||||
|
||||
```js
|
||||
const client = new ApolloClient({
|
||||
|
@ -26,7 +26,7 @@ const client = new ApolloClient({
|
|||
|
||||
## Utilising the provider
|
||||
|
||||
Apollo Provides a top level application context that we can wrap our React app in. This will provide access to the client object from anywhere within the app, eg:
|
||||
Apollo provides a top level application context that we can wrap our React app in. This will provide access to the client object from anywhere within the app, eg:
|
||||
|
||||
```jsx
|
||||
ReactDOM.render(
|
||||
|
|
|
@ -13,7 +13,7 @@ It is able to do the following:
|
|||
|
||||
- Receive an incoming GraphQL query from a client
|
||||
- Validate that query against the server schema
|
||||
- Populate the queried schema fieldsj
|
||||
- Populate the queried schema fields
|
||||
- Return the fields as a JSON response object
|
||||
|
||||
## Example schema
|
||||
|
@ -21,6 +21,8 @@ It is able to do the following:
|
|||
We will use the following schema in the examples.
|
||||
|
||||
```js
|
||||
// schema.js
|
||||
|
||||
const typeDefs = gql`
|
||||
" Our schema types will be nested here
|
||||
`;
|
||||
|
@ -53,6 +55,8 @@ type Author {
|
|||
We instantiate an `ApolloServer` instance and pass our schema to it. We then subscribe to it with a [listener](/Programming_Languages/Node/Modules/Core/Node_JS_events_module.md#extending-the-eventemitter-class).
|
||||
|
||||
```js
|
||||
// index.js
|
||||
|
||||
const { ApolloServer } = require("apollo-server");
|
||||
const typeDefs = require("./schema");
|
||||
const server = new ApolloServer({ typeDefs });
|
||||
|
@ -112,9 +116,9 @@ We can now [run queries](/Databases/GraphQL/Apollo/Apollo_Client.md#running-a-qu
|
|||
|
||||
## Implementing resolvers
|
||||
|
||||
A resolver is a [function](/Trash/Creating_a_GraphQL_server.md#resolvers) that populates data for a given query. It should have **the same name as the field for the query**. So far we have one query in our schema: `tracksForHome` which returns the tracks to be listed on the home page. We must therefore we must also name our resolver for this query `tracksForHome`.
|
||||
A resolver is a [function](/Trash/Creating_a_GraphQL_server.md#resolvers) that populates data for a given query. It should have **the same name as the field for the query**. So far we have one query in our schema: `tracksForHome` which returns the tracks to be listed on the home page. We must therefore also name our resolver for this query `tracksForHome`.
|
||||
|
||||
It can fetch data from any data source or multiple data sources (other servers, databases, REST APIs) and then presents this as a single integrated resouce to the client, matching the shape requested.
|
||||
It can fetch data from a single data source or multiple data sources (other servers, databases, REST APIs) and present this as a single integrated resource to the client, matching the shape requested.
|
||||
|
||||
As per the [generic example](/Trash/Creating_a_GraphQL_server.md#resolvers), you write write your resolvers as keys on a `resolvers` object, e.g:
|
||||
|
||||
|
@ -122,7 +126,7 @@ As per the [generic example](/Trash/Creating_a_GraphQL_server.md#resolvers), you
|
|||
const resolvers = {};
|
||||
```
|
||||
|
||||
The `resolvers` object's keys will correspond to the schema's types and fields. For some reason Apollo requires extra scaffolding around the keys, you have to wrap the key in `Query` like so:
|
||||
The `resolvers` object's keys will correspond to the schema's types and fields. You distinguish resolves which directly correspond to a query in the schema from other resolver types by wraping them in `Query {}`.
|
||||
|
||||
```js
|
||||
const resolvers = {
|
||||
|
@ -134,27 +138,26 @@ const resolvers = {
|
|||
|
||||
### Resolver parameters
|
||||
|
||||
The resolver function has standard parameters that you draw on when implementing the resolution:
|
||||
Each resolver function has the same standard parameters that you can invoke when implementing the resolution: `resolverFunction(parent, args, context, info)`.
|
||||
|
||||
- `parent`
|
||||
- something to do with resolver chains //TODO: return to
|
||||
- Used with [resolver chains](/Databases/GraphQL/Apollo/Using_arguments_with_Apollo_Client.md#resolver-chains) ---add example
|
||||
- `args`
|
||||
- an object containing the argments that were provided for the field by the client. For instance if the client requests a field with an accompanying `id` argument, `id` will show up in the `args` object
|
||||
- an object comprising arguments provided for the given field by the client. For instance if the client requests a field with an accompanying `id` argument, `id` can be parsed via the `args` object
|
||||
- `context`
|
||||
- shared state between different resolvers that contains essential connection parameters such as authentication, a database connection, or a `RESTDataSource` (see below)
|
||||
- shared state between different resolvers that contains essential connection parameters such as authentication, a database connection, or a `RESTDataSource` (see below). This will be typically instantiated via a class which is then invoked within the `ApolloServer` instance under the `dataSources` key.
|
||||
- `info`
|
||||
- least essential, used for caching
|
||||
- not used so frequently but employed as part of caching
|
||||
|
||||
Typically you won't use every parameter with every resolver. You can ommit them with `_, __`; the number of dashes indicating the argument placement.
|
||||
|
||||
### `RESTDataSource`
|
||||
|
||||
This is something you can apply to your server to improve the efficiency of working with REST APIs in your resolvers.
|
||||
A resolver can return data from multiple sources. One of the most common sources is a RESTful endpoint. Apollo provides a specific class for handling REST endpoints in your resolvers: `RESTDataSource`.
|
||||
|
||||
REST APIs fall victim to the "n + 1" problem: say you want to get an array of one resource type, then for each element returned you need to send another request using one of its properties to fetch a related resource.
|
||||
|
||||
This is implicit in the case of the `Track` type in the schema. Each `Track` has an `author` key but the `Author` type isn't embedded in `Track` it has to be fetched using an `id`. In a REST API, this would require a request to
|
||||
a separate end point for each `Track` returned.
|
||||
This is implicit in the case of the `Track` type in the schema. Each `Track` has an `author` key but the `Author` type isn't embedded in `Track` it has to be fetched using an `id`. In a REST API, this would require a request to a separate end point for each `Track` returned, increasing the time complexity of the request.
|
||||
|
||||
Here is an example of `RESTDataSource` being used. It is just a class that can be extended and which provides inbuilt methods for running fetches against a REST API:
|
||||
|
||||
|
@ -232,6 +235,13 @@ const resolvers = {
|
|||
};
|
||||
```
|
||||
|
||||
- We keep `Track` outside of `Query` because it has no corresponding query in the schema and we must always match the schema.
|
||||
- We invoke the `context` again when we destructure `dataSources`.
|
||||
- This time we utilise the `args` parameter in the resolver since an `id` will be provided as a client-side [argument](/Databases/GraphQL/Apollo/Using_arguments_with_Apollo_Client.md) to return a specific author.
|
||||
- Just as we nest the `tracksForHome` resolver under `Query`, we must nest `author` under `Track` to match the structure of the schema. This resolver doesn't respond to a query that is exposed to the client so it shouldn't go under `Query`.
|
||||
|
||||
* We invoke the `context` again when we destructure `dataSources` from the `ApolloServer` instance.
|
||||
* This time we utilise the `args` parameter in the resolver since an `id` will be provided as a client-side [argument](/Databases/GraphQL/Apollo/Using_arguments_with_Apollo_Client.md) to return a specific author.
|
||||
|
||||
## The `useMutation` hook
|
||||
|
||||
We invoke the `useMutation` hook to issue mutations from the client-side.
|
||||
|
||||
As with queries and [query constants](/Databases/GraphQL/Apollo/Apollo_Client.md#query-constants)
|
||||
|
|
172
Databases/GraphQL/Apollo/Mutations_with_Apollo_Client.md
Normal file
172
Databases/GraphQL/Apollo/Mutations_with_Apollo_Client.md
Normal file
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
title: Mutations with Apollo Client
|
||||
categories:
|
||||
- Databases
|
||||
tags: [graphql]
|
||||
---
|
||||
|
||||
# Mutations with Apollo Client
|
||||
|
||||
Queries are read-only operations. Mutations are write-only operations.
|
||||
|
||||
Just like the `Query` type, the `Mutation` type serves as an entrypoint to the schema.
|
||||
|
||||
## Naming convention
|
||||
|
||||
- Use verb: `add`, `create`, `delete`
|
||||
- Refer to the datatype
|
||||
|
||||
For example `addSpaceCat(){}`
|
||||
|
||||
## Demonstration mutation
|
||||
|
||||
We are going to create a mutation that increments the `numberOfViews` field on the `Track` type:
|
||||
|
||||
## Updating the schema
|
||||
|
||||
```js
|
||||
type Mutation {
|
||||
incrementTrackViews(id: ID!): IncrementTrackViewsResponse!
|
||||
}
|
||||
|
||||
|
||||
// Define a specific response type that specifically matches our needs
|
||||
type IncrementTrackViewsResponse {
|
||||
code: Int! // status code
|
||||
success: Boolean! // whether mutation was successful
|
||||
message: String! // what to say if mutation successful
|
||||
track: Track // not nullable because might error
|
||||
}
|
||||
```
|
||||
|
||||
Based on this schema, the mutation will recieve a `Track` id and increment the specified `Track`. It will return an object comprising the newly updated `Track` and a bundle of properties that provide feedback on the status of the operations: a status code, whether it succeeded, and a message.
|
||||
|
||||
## Updating the data source
|
||||
|
||||
Remember that our sole data source in the demonstration project is a REST API. We handle it within GraphQL using Apollos `RESTDataSource` class. We need to add a method to this class that will increment the track views. We wil use the `PATCH` REST method:
|
||||
|
||||
```js
|
||||
class TrackAPI extends RESTDataSource {
|
||||
constructor() {...}
|
||||
getTracksForHome() {...}
|
||||
getAuthor(authorId) {...}
|
||||
getTrack(trackId){...};
|
||||
|
||||
incrementTrackViews(trackId) {
|
||||
return this.patch(`track/${trackId}/numberOfViews`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `patch()` method is procided by the `RESTDataSouce` class that `TrackAPI` inherits from
|
||||
|
||||
## Adding resolver
|
||||
|
||||
Next we need a resolver that corresponds to the mutation we have defined in the schema. We will need to handle successful responses as well as errors.
|
||||
|
||||
### Success case
|
||||
|
||||
As always we match the shape of the schema:
|
||||
|
||||
```js
|
||||
const resolvers = {
|
||||
Query: {
|
||||
// ...query resolvers
|
||||
},
|
||||
Mutation: {
|
||||
// increments a track's numberOfViews property
|
||||
incrementTrackViews: async (_, { id }, { dataSources }) => {
|
||||
const track = await dataSources.trackAPI.incrementTrackViews(id);
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: `Successfully incremented number of views for track ${id}`,
|
||||
track,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
There's more going on with this resolver than the previous one. As is standard, we call the API using the `TrackAPI` class. However we don't just immediately return this when it executes. This is because the schema specifies that the retrun type `IncrementTrackViewsResponse` requires more than just the updated `Track`. So we wait this and return it with the cluster of metadata about the mutation response (`code`, `success`, and `message`).
|
||||
|
||||
### Error case
|
||||
|
||||
We can extend the Mutation resolver to allow for errors. We'll do this by refactoring the resolver into a `try...catch` block and adding the error handling in the `catch`.
|
||||
|
||||
We'll harness the details that are provided by Apollos' own `err` object which is returned by the `RESTDataSource` class that our resolver ultimately traces back to:
|
||||
|
||||
```js
|
||||
const resolvers = {
|
||||
|
||||
Query: {
|
||||
// ...query resolvers
|
||||
}
|
||||
|
||||
Mutation: {
|
||||
incrementTrackViews: async (_, {id}, {dataSources}) => {
|
||||
try {
|
||||
const track = await dataSources.trackAPI.incrementTrackViews(id);
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: `Successfully incremented number of views for track ${id}`,
|
||||
track
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
code: err.extensions.response.status,
|
||||
success: false,
|
||||
message: err.extensions.response.body,
|
||||
track: null
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## The `useMutation` hook
|
||||
|
||||
We invoke the `useMutation` hook to issue mutations from the client-side.
|
||||
|
||||
As with queries and [query constants](/Databases/GraphQL/Apollo/Apollo_Client.md#query-constants) we wrap our mutation in a `gql` template string:
|
||||
|
||||
```js
|
||||
const INCREMENT_TRACK_VIEWS = gql`
|
||||
mutation IncrementTrackViews($incrementTrackViewsId: ID!) {
|
||||
incrementTrackViews(id: $incrementTrackViewsId) {
|
||||
code
|
||||
success
|
||||
message
|
||||
track {
|
||||
id
|
||||
numberOfViews
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
We then pass it to the `useMutation` hook including an options object with our variables. (This time the specific variable is named):
|
||||
|
||||
```js
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
|
||||
useMutation(INCREMENT_TRACK_VIEWS, {
|
||||
variables: { incrementTrackViewsId: id },
|
||||
});
|
||||
```
|
||||
|
||||
`useMutation` returns an array of two elements:
|
||||
|
||||
1. The mutation function that actually executes
|
||||
2. An object comprising (`loading`, `error`, `data`) - this is the same as is the return value of `useQuery`.
|
||||
|
||||
So we can destructure like so (we don't always need the second element);
|
||||
|
||||
```js
|
||||
const [incrementTrackViews, dataObject] = useMutation(INCREMENT_TRACK_VIEWS...)
|
||||
```
|
||||
|
||||
Given that we can isolate the mutation function as the first destructured element of the array, we could then attach `incrementTrackViews` to a button or other frontend interaction.
|
|
@ -167,11 +167,7 @@ Track: {
|
|||
},
|
||||
```
|
||||
|
||||
Notice that `authorId` is used in the place of the `parent` parameter. It already exists on the `Track` type that wraps. So this can be invoked to fulfill `author` and thereby access the author name from the graph.
|
||||
|
||||
> I don't really understand this but the general point seems to be that the resolvers outside of the main `Query` block in the resolver are tied to a data type and can be used to magically populate query requests for nested fields providing a key is on the main datatype returned.
|
||||
|
||||
Now repeat this example with `modules`
|
||||
Notice that `authorId` is used in the place of the `parent` parameter. It already exists on the `Track` type that wraps the resolver. So this can be invoked to fulfill `author` and thereby access the author name from the graph.
|
||||
|
||||
This process is also required for our extended schema. The `Track` type now has a `modules` field that comprises an array of the `Module` type.
|
||||
|
||||
|
@ -277,10 +273,10 @@ Then to employ in React:
|
|||
const trackId = "xyz";
|
||||
|
||||
const { loading, error, data } = useQuery(GET_TRACK, {
|
||||
variables: trackId,
|
||||
variables: {
|
||||
id: trackId,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Note that in contrast to the [simple example](/Databases/GraphQL/Apollo/Apollo_Client.md#query-constants) because we are using variables, we have to pass-in an additional object with the query constant that specifies our variables.
|
||||
|
||||
// TODO: Find examples of using more than one variable.
|
||||
Note that in contrast to the [simple example](/Databases/GraphQL/Apollo/Apollo_Client.md#query-constants) because we are using variables, we have to pass-in an additional options object with the query constant that specifies our variables.
|
||||
|
|
|
@ -42,3 +42,5 @@ A basic example of a REST API would be a series of methods corresponding to the
|
|||
| PUT | /api/customers/guid | Update an existing customer | Yes |
|
||||
| DELETE | /api/customers/1 | Delete a customer | No, data comes from GUID |
|
||||
| POST | /api/customers | Create a new customer | Yes |
|
||||
|
||||
// TODO: Add PATCH and explain differenct from PUT
|
||||
|
|
Loading…
Add table
Reference in a new issue