From e8f3d355b38011af08709b55c06152a0bfb77e1c Mon Sep 17 00:00:00 2001 From: Thomas Bishop Date: Mon, 28 Nov 2022 13:12:07 +0000 Subject: [PATCH] New stuff --- .gitignore | 1 + Databases/GraphQL/Apollo/Apollo_Client.md | 4 +- Databases/GraphQL/Apollo/Apollo_Server.md | 40 ++-- .../Apollo/Mutations_with_Apollo_Client.md | 172 ++++++++++++++++++ .../Using_arguments_with_Apollo_Client.md | 14 +- Databases/REST/RESTful_APIs.md | 2 + 6 files changed, 207 insertions(+), 26 deletions(-) create mode 100644 .gitignore create mode 100644 Databases/GraphQL/Apollo/Mutations_with_Apollo_Client.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/Databases/GraphQL/Apollo/Apollo_Client.md b/Databases/GraphQL/Apollo/Apollo_Client.md index aae3aa8..1d5d2f7 100644 --- a/Databases/GraphQL/Apollo/Apollo_Client.md +++ b/Databases/GraphQL/Apollo/Apollo_Client.md @@ -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( diff --git a/Databases/GraphQL/Apollo/Apollo_Server.md b/Databases/GraphQL/Apollo/Apollo_Server.md index 4037e0e..84f130c 100644 --- a/Databases/GraphQL/Apollo/Apollo_Server.md +++ b/Databases/GraphQL/Apollo/Apollo_Server.md @@ -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) diff --git a/Databases/GraphQL/Apollo/Mutations_with_Apollo_Client.md b/Databases/GraphQL/Apollo/Mutations_with_Apollo_Client.md new file mode 100644 index 0000000..7f1ff8f --- /dev/null +++ b/Databases/GraphQL/Apollo/Mutations_with_Apollo_Client.md @@ -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. diff --git a/Databases/GraphQL/Apollo/Using_arguments_with_Apollo_Client.md b/Databases/GraphQL/Apollo/Using_arguments_with_Apollo_Client.md index 88c81cc..17d0fd0 100644 --- a/Databases/GraphQL/Apollo/Using_arguments_with_Apollo_Client.md +++ b/Databases/GraphQL/Apollo/Using_arguments_with_Apollo_Client.md @@ -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. diff --git a/Databases/REST/RESTful_APIs.md b/Databases/REST/RESTful_APIs.md index eea9907..6374645 100644 --- a/Databases/REST/RESTful_APIs.md +++ b/Databases/REST/RESTful_APIs.md @@ -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