More GraphQL notes
This commit is contained in:
parent
fa64450549
commit
81d9d3ea90
9 changed files with 181 additions and 29 deletions
|
@ -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.
|
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
|
## 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
|
```js
|
||||||
const client = new ApolloClient({
|
const client = new ApolloClient({
|
||||||
|
@ -62,6 +64,7 @@ const TRACKS = gql`
|
||||||
```
|
```
|
||||||
|
|
||||||
The convention is to name the query constant in `ALL_CAPS`.
|
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)
|
> 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
|
### `useQuery` hook
|
||||||
|
@ -87,4 +90,3 @@ const Tracks = () => {
|
||||||
- We destructure the `loading, error, data` variables that are returned from the hook
|
- We destructure the `loading, error, data` variables that are returned from the hook
|
||||||
- We pass in our query constant as an argument.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|
|
@ -7,19 +7,21 @@ tags: [graph-ql, apollo]
|
||||||
|
|
||||||
# Apollo Server
|
# 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:
|
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 fields
|
- Populate the queried schema fieldsj
|
||||||
- Return the fields as a response
|
- Return the fields as a JSON response object
|
||||||
|
|
||||||
## Example schema
|
## Example schema
|
||||||
|
|
||||||
We will use the following schema in the examples
|
We will use the following schema in the examples
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const typeDefs = gql`
|
const typeDefs = gql`
|
||||||
type Query {
|
type Query {
|
||||||
|
@ -27,6 +29,7 @@ const typeDefs = gql`
|
||||||
tracksForHome: [Track!]!
|
tracksForHome: [Track!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO, rewrite this so I can get syntax highlighting
|
||||||
"A track is a group of Modules that teaches about a specific topic"
|
"A track is a group of Modules that teaches about a specific topic"
|
||||||
type Track {
|
type Track {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
@ -56,6 +59,8 @@ module.exports = typeDefs;
|
||||||
|
|
||||||
## Setting up the server
|
## 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
|
```js
|
||||||
const { ApolloServer } = require("apollo-server");
|
const { ApolloServer } = require("apollo-server");
|
||||||
const typeDefs = require("./schema");
|
const typeDefs = require("./schema");
|
||||||
|
@ -111,35 +116,131 @@ const mocks = {
|
||||||
|
|
||||||
const server = new ApolloServer({ typeDefs, mocks });
|
const server = new ApolloServer({ typeDefs, mocks });
|
||||||
```
|
```
|
||||||
We can now [run queries](/Databases/GraphQL/Apollo/Apollo_Client.md#running-a-query) against our server.
|
|
||||||
|
We can now [run queries](/Databases/GraphQL/Apollo/Apollo_Client.md#running-a-query) against our server.
|
||||||
|
|
||||||
## Implementing resolvers
|
## 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`
|
### `RESTDataSource`
|
||||||
|
|
||||||
This is something you can apply to your server to improve the efficiency of working with REST APIs in your resolvers.
|
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:
|
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
|
```js
|
||||||
const {RESTDataSource} = require('apollo-datasource-rest');
|
const { RESTDataSource } = require("apollo-datasource-rest");
|
||||||
|
|
||||||
class TrackAPI extends RESTDataSource {
|
class TrackAPI extends RESTDataSource {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.baseURL = 'https://odyssey-lift-off-rest-api.herokuapp.com/';
|
this.baseURL = "https://odyssey-lift-off-rest-api.herokuapp.com/";
|
||||||
}
|
}
|
||||||
|
|
||||||
getTracksForHome() {
|
getTracksForHome() {
|
||||||
return this.get('tracks');
|
return this.get("tracks");
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthor(authorId) {
|
getAuthor(authorId) {
|
||||||
return this.get(`author/${authorId}`);
|
return this.get(`author/${authorId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
```
|
|
@ -50,8 +50,7 @@ app.use(
|
||||||
|
|
||||||
## Resolvers
|
## Resolvers
|
||||||
|
|
||||||
|
We will specify our resolvers in a dedicated resolver file. In GraphQL you need to define resolvers for both your queries and your mutations.
|
||||||
We will specify our resolvers in a dedicate 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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
categories:
|
categories:
|
||||||
- Databases
|
- Databases
|
||||||
tags: [mongo_db, node_js, mongoose]
|
tags: [mongo_db, node-js, mongoose]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Adding documents to a collection
|
# Adding documents to a collection
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
categories:
|
categories:
|
||||||
- Databases
|
- Databases
|
||||||
tags: [mongo_db, node_js, mongoose]
|
tags: [mongo_db, node-js, mongoose]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Creating a schema and model
|
# Creating a schema and model
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
categories:
|
categories:
|
||||||
- Databases
|
- Databases
|
||||||
tags: [mongo_db, node_js, mongoose]
|
tags: [mongo_db, node-js, mongoose]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Validating Mongoose schemas
|
# Validating Mongoose schemas
|
||||||
|
|
|
@ -7,7 +7,7 @@ tags:
|
||||||
- node-modules
|
- 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.
|
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
|
## Basic syntax
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const EventEmitter = require('events'); // import the module
|
const EventEmitter = require("events"); // import the module
|
||||||
|
|
||||||
// Raise an event
|
// Raise an event
|
||||||
const emitter = new EventEmitter('messageLogged');
|
const emitter = new EventEmitter("messageLogged");
|
||||||
|
|
||||||
// Register a listener
|
// Register a listener
|
||||||
emitter.on('messagedLogged', function () {
|
emitter.on("messagedLogged", function () {
|
||||||
console.log('The listener was called.');
|
console.log("The listener was called.");
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -45,12 +45,12 @@ emitter.on('messagedLogged', function () {
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Raise an event
|
// Raise an event
|
||||||
const emitter = new EventEmitter('messageLogged', function (eventArg) {
|
const emitter = new EventEmitter("messageLogged", function (eventArg) {
|
||||||
console.log('Listener called', eventArg);
|
console.log("Listener called", eventArg);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register a listener
|
// 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
|
## Extending the `EventEmitter` class
|
||||||
|
@ -61,12 +61,12 @@ emitter.on('messagedLogged', {id: 1, url: 'http://www.example.com'});
|
||||||
```js
|
```js
|
||||||
// File: Logger.js
|
// File: Logger.js
|
||||||
|
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require("events");
|
||||||
|
|
||||||
class Logger extends EventEmitter {
|
class Logger extends EventEmitter {
|
||||||
log(message) {
|
log(message) {
|
||||||
console.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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
|
@ -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/)
|
||||||
|
|
Loading…
Add table
Reference in a new issue