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
|
## 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
|
```js
|
||||||
const client = new ApolloClient({
|
const client = new ApolloClient({
|
||||||
|
@ -26,7 +26,7 @@ const client = new ApolloClient({
|
||||||
|
|
||||||
## Utilising the provider
|
## 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
|
```jsx
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
|
|
@ -13,7 +13,7 @@ It is able to do the following:
|
||||||
|
|
||||||
- Receive an incoming GraphQL query from a client
|
- Receive an incoming GraphQL query from a client
|
||||||
- Validate that query against the server schema
|
- 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
|
- Return the fields as a JSON response object
|
||||||
|
|
||||||
## Example schema
|
## Example schema
|
||||||
|
@ -21,6 +21,8 @@ It is able to do the following:
|
||||||
We will use the following schema in the examples.
|
We will use the following schema in the examples.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
// schema.js
|
||||||
|
|
||||||
const typeDefs = gql`
|
const typeDefs = gql`
|
||||||
" Our schema types will be nested here
|
" 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).
|
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
|
```js
|
||||||
|
// index.js
|
||||||
|
|
||||||
const { ApolloServer } = require("apollo-server");
|
const { ApolloServer } = require("apollo-server");
|
||||||
const typeDefs = require("./schema");
|
const typeDefs = require("./schema");
|
||||||
const server = new ApolloServer({ typeDefs });
|
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
|
## 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:
|
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 = {};
|
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
|
```js
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
|
@ -134,27 +138,26 @@ const resolvers = {
|
||||||
|
|
||||||
### Resolver parameters
|
### 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`
|
- `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`
|
- `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`
|
- `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`
|
- `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.
|
Typically you won't use every parameter with every resolver. You can ommit them with `_, __`; the number of dashes indicating the argument placement.
|
||||||
|
|
||||||
### `RESTDataSource`
|
### `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.
|
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
|
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.
|
||||||
a separate end point for each `Track` returned.
|
|
||||||
|
|
||||||
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:
|
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.
|
- 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`.
|
|
||||||
- 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.
|
* 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.
|
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.
|
||||||
|
|
||||||
> 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`
|
|
||||||
|
|
||||||
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.
|
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 trackId = "xyz";
|
||||||
|
|
||||||
const { loading, error, data } = useQuery(GET_TRACK, {
|
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.
|
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.
|
||||||
|
|
||||||
// TODO: Find examples of using more than one variable.
|
|
||||||
|
|
|
@ -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 |
|
| PUT | /api/customers/guid | Update an existing customer | Yes |
|
||||||
| DELETE | /api/customers/1 | Delete a customer | No, data comes from GUID |
|
| DELETE | /api/customers/1 | Delete a customer | No, data comes from GUID |
|
||||||
| POST | /api/customers | Create a new customer | Yes |
|
| POST | /api/customers | Create a new customer | Yes |
|
||||||
|
|
||||||
|
// TODO: Add PATCH and explain differenct from PUT
|
||||||
|
|
Loading…
Add table
Reference in a new issue