More GraphQL notes

This commit is contained in:
Thomas Bishop 2022-11-16 17:57:44 +00:00
parent fa64450549
commit 81d9d3ea90
9 changed files with 181 additions and 29 deletions

View file

@ -9,9 +9,11 @@ tags: [graph-ql, apollo]
Apollo Client is the client-side counterpart to [Apollo Server](/Databases/GraphQL/Apollo/Apollo_Server.md). We use it for managing queries and mutations from the frontend to our Apollo GraphQL server. It is specifically designed to work with React.
> We will be working with the [schema](/Databases/GraphQL/Apollo/Apollo_Server.md#example-schema) we defined when working on the server
## Initializing the client
We initialise the client and set-up in memory caching to reduce network requests:
We initialise the client and set-up in memory kcaching to reduce network requests:
```js
const client = new ApolloClient({
@ -62,6 +64,7 @@ const TRACKS = gql`
```
The convention is to name the query constant in `ALL_CAPS`.
> Note that the name of the query on the client doesn't have to match the query type defined in the schema however it should reference it on the second line (`tracksFormHome)
### `useQuery` hook
@ -87,4 +90,3 @@ const Tracks = () => {
- We destructure the `loading, error, data` variables that are returned from the hook
- We pass in our query constant as an argument.
- In the example we just render the serialized data but we could of course pass the data as a prop and map through it in an embedded child component.

View file

@ -7,19 +7,21 @@ tags: [graph-ql, apollo]
# Apollo Server
> Apollo Server is the part of the Apollo suite that we use to create the backend of a GraphQL project; a GraphQL server.
> Apollo Server is the part of the Apollo suite that we use to create the backend of a GraphQL project: a GraphQL server.
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 fields
- Return the fields as a response
- Populate the queried schema fieldsj
- Return the fields as a JSON response object
## Example schema
We will use the following schema in the examples
//
```js
const typeDefs = gql`
type Query {
@ -27,6 +29,7 @@ const typeDefs = gql`
tracksForHome: [Track!]!
}
// TODO, rewrite this so I can get syntax highlighting
"A track is a group of Modules that teaches about a specific topic"
type Track {
id: ID!
@ -56,6 +59,8 @@ module.exports = typeDefs;
## Setting up the server
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
const { ApolloServer } = require("apollo-server");
const typeDefs = require("./schema");
@ -111,31 +116,68 @@ const mocks = {
const server = new ApolloServer({ typeDefs, mocks });
```
We can now [run queries](/Databases/GraphQL/Apollo/Apollo_Client.md#running-a-query) against our server.
## Implementing resolvers
A resolver is a [function](/Databases/GraphQL/Creating_a_GraphQL_server.md#resolvers)
A resolver is a [function](/Databases/GraphQL/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`.
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.
As per the [generic example](/Databases/GraphQL/Creating_a_GraphQL_server.md#resolvers), you write write your resolvers as keys on a `resolvers` object, e.g:
```js
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:
```js
const resolvers = {
Query: {
tracksForHome: () => {},
},
};
```
### Resolver parameters
The resolver function has standard parameters that you draw on when implementing the resolution:
- `parent`
- something to do with resolver chains //TODO: return to
- `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
- `context`
- shared state between different resolvers that contains essential connection parameters such as authentication, a database connection, or a `RESTDataSource` (see below)
- `info`
- least essential, used for 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.
REST APIs fall victim to the "n + 1" problem: say you want to get one resource, then for each element returned of this resource you need to send another request using a property of this resource to get another resource. For each of the first requests you need to send another request once they resolve.
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.
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:
```js
const {RESTDataSource} = require('apollo-datasource-rest');
const { RESTDataSource } = require("apollo-datasource-rest");
class TrackAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://odyssey-lift-off-rest-api.herokuapp.com/';
this.baseURL = "https://odyssey-lift-off-rest-api.herokuapp.com/";
}
getTracksForHome() {
return this.get('tracks');
return this.get("tracks");
}
getAuthor(authorId) {
@ -143,3 +185,62 @@ class TrackAPI extends RESTDataSource {
}
}
```
### Using our `RESTDataSource` in our resolver
As our GraphQL server is sourcing data from a REST API, we can now integrate the `RESTDataSource` class with our resolver.
First thing, we need to instantiate an instance of our `TrackApi` class, otherwise we won't be able to use any of its methods in the resolver.
We will create an instance of this class and pass it into `ApolloServer` object we established at the beginning. We will pass it to the `dataSources` key. **This will allow us to access it from within the `context` parameter in our resolver function**
We can also get rid of the `mocks` object since we don't need it any more. We will replace it with our `resolvers` constant:
```diff
const server = new ApolloServer({
typeDefs,
- mocks,
+ resolvers,
+ dataSources: () => {
+ return {
+ trackApi: new TrackApi()
+ }
}
})
```
Now we can complete our resolver:
```js
const resolvers = {
Query: {
tracksForHome: (_, __, {dataSources}) => {},
return dataSources.trackApi.getTracksForHome()
},
};
```
So we destructure the `dataSources` object from the parent Apollo Server instance (in the place of the `context` parameter) which gives us access to our `trackApi` class. This resolver will now make the API request and return the tracks.
The `tracksForHome` query returns `Track` objects and these have a required `author` key that returns an `Author` type. So we are also going to need a resolver that can return the author data that will be populated along with `Track`.
We already have this functionality in our class: `getAuthor` so we just need to integrate it:
```js
const resolvers = {
Query: {
tracksForHome: (_, __, { dataSources }) => {
return dataSources.trackApi.getTracksForHome();
},
},
Track: {
author: ({ authorId }, _, { dataSources }) => {
return dataSources.trackApi.getAuthor(authorId);
},
},
};
```
- We keep `Track` outside of `Query` because it has no corresponding query in the schema and we must always match the schema. `Track` is a self-standing field so the resolver must **match this schema shape**. The query `getTracksForHome` references `Track` but it is a separate field.
- 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 from the client to return a specific author.

View file

@ -0,0 +1,44 @@
---
title: Using arguments with Apollo Client
categories:
- Databases
tags: [graph-ql, apollo]
---
# Using arguments with Apollo Client
In order to demonstrate arguments we need to expand the [original schema](/Databases/GraphQL/Apollo/Apollo_Server.md#example-schema).
Remember a Track is a group of Modules that teaches about a specific topic. We are going to add:
- `description` and `numberOfViews` fields to the original `Track` type
- A new `Module` type
- A field `modules` to the `Track` type that will be an array of type `Module`
## Updated schema
```js
type Track {
id: ID!
title: String!
author: Author!
thumbnail: String
length: Int
modulesCount: Int
description: String
numberOfViews: Int
modules: [Module!]!
}
type Module {
id: ID!
title: String!
length: Int
}
type Author {
id: ID!
name: String!
photo: String
}
```

View file

@ -50,8 +50,7 @@ app.use(
## Resolvers
We will specify our resolvers in a dedicate resolver file. In GraphQL you need to define resolvers for both your queries and your mutations.
We will specify our resolvers in a dedicated resolver file. In GraphQL you need to define resolvers for both your queries and your mutations.
To achieve this we will have a dummy object as the database containing our products and a class working as a generator function that will create a product object with certain properties, individuated by an id. We will invoke this class to create new products for the database and to retrieve existing products from the database.

View file

@ -1,7 +1,7 @@
---
categories:
- Databases
tags: [mongo_db, node_js, mongoose]
tags: [mongo_db, node-js, mongoose]
---
# Adding documents to a collection

View file

@ -1,7 +1,7 @@
---
categories:
- Databases
tags: [mongo_db, node_js, mongoose]
tags: [mongo_db, node-js, mongoose]
---
# Creating a schema and model

View file

@ -1,7 +1,7 @@
---
categories:
- Databases
tags: [mongo_db, node_js, mongoose]
tags: [mongo_db, node-js, mongoose]
---
# Validating Mongoose schemas

View file

@ -7,7 +7,7 @@ tags:
- node-modules
---
# `events` module
# Node.js `events` module
In most cases you won't interact with the `events` module directly since other modules and third-party modules are abstractions on top of it. For instance the `http` module is using events under the hood to handle requests and responses.
@ -23,14 +23,14 @@ Because Node's runtime is [event-driven](/Programming_Languages/NodeJS/Architect
## Basic syntax
```js
const EventEmitter = require('events'); // import the module
const EventEmitter = require("events"); // import the module
// Raise an event
const emitter = new EventEmitter('messageLogged');
const emitter = new EventEmitter("messageLogged");
// Register a listener
emitter.on('messagedLogged', function () {
console.log('The listener was called.');
emitter.on("messagedLogged", function () {
console.log("The listener was called.");
});
```
@ -45,12 +45,12 @@ emitter.on('messagedLogged', function () {
```js
// Raise an event
const emitter = new EventEmitter('messageLogged', function (eventArg) {
console.log('Listener called', eventArg);
const emitter = new EventEmitter("messageLogged", function (eventArg) {
console.log("Listener called", eventArg);
});
// Register a listener
emitter.on('messagedLogged', {id: 1, url: 'http://www.example.com'});
emitter.on("messagedLogged", { id: 1, url: "http://www.example.com" });
```
## Extending the `EventEmitter` class
@ -61,12 +61,12 @@ emitter.on('messagedLogged', {id: 1, url: 'http://www.example.com'});
```js
// File: Logger.js
const EventEmitter = require('events');
const EventEmitter = require("events");
class Logger extends EventEmitter {
log(message) {
console.log(message);
this.emit('messageLogged', {id: 1, url: 'http://www.example.com'});
this.emit("messageLogged", { id: 1, url: "http://www.example.com" });
}
}
```

View file

@ -1 +1,7 @@
https://runestone.academy/ns/books/published/welcomecs/index.html
## Computer Science modules
[Welcome to CS](https://runestone.academy/ns/books/published/welcomecs/index.html)
## Python
[Tiny Python Projects (O'Reilly)](https://learning.oreilly.com/library/view/tiny-python-projects/9781617297519/)