mirror of
https://github.com/zhigang1992/apollo.git
synced 2026-04-28 20:05:38 +08:00
Clean up data source section
Minor changes
This commit is contained in:
@@ -1,28 +1,25 @@
|
||||
---
|
||||
title: "2. Hook up your data sources"
|
||||
description: Start here for the Apollo fullstack tutorial
|
||||
description: Connect REST and SQL data to your graph
|
||||
---
|
||||
|
||||
Apollo data sources provide the best experience for fetching and caching data from REST endpoints, web services, and databases. It's a new pattern for loading data from various sources, with built-in support for deduplication, caching, and error handling.
|
||||
Now that we've constructed our schema, we need to hook up our data sources to our graph API. Graph APIs are extremely flexible because you can layer them on top of any service, including REST APIs, databases, or gRPC services.
|
||||
|
||||
When layering GraphQL over your REST endpoints, Apollo data sources enable **partial query caching**-––a policy that provides autom atic caching of HTTP requests based on the caching headers returned from the backend and also allows you override the backend cache policy, by providing the option of setting an explicit `ttl` value in your data source.
|
||||
Apollo makes connecting these services to your graph simple with our data source API. An **Apollo data source** is a class that encapsulates all of the data fetching logic, as well as caching and deduplication, for a particular service. By using Apollo data sources to hook up your services to your graph API, you're also following best practices for organizing your code.
|
||||
|
||||
In addition, Apollo data sources grants your app a well-structured way of organizing and encapsulating data fetching logic. Rather than bloating your resolver functions with logic, you can wrap access to a particular backend data source inside data source classes.
|
||||
In the next sections, we'll build data sources for a REST API and a SQL database and connect them to Apollo Server. Don't worry if you're not familiar with either of those technologies, you won't need to understand them deeply in order to follow the examples. 😀
|
||||
|
||||
<h2 id="rest-api">Connect a REST API</h2>
|
||||
|
||||
To get started, install the `apollo-datasource` and `apollo-datasource-rest` packages:
|
||||
First, let's connect the [Space-X v2 REST API](https://github.com/r-spacex/SpaceX-API) to our graph. To get started, install the `apollo-datasource-rest` package:
|
||||
|
||||
```bash
|
||||
npm install apollo-datasource apollo-datasource-rest --save
|
||||
npm install apollo-datasource-rest --save
|
||||
```
|
||||
|
||||
* **apollo-datasource**: This is the generic data source package. It's good for connecting to non-REST data sources.
|
||||
* **apollo-datasource-rest**: This package exposes the `RESTDataSource` class that is responsible for fetching data from a given REST API. To define a data source for the REST endpoint, extend the `RESTDataSource` class and implement the data fetching methods that your resolvers require.
|
||||
This package exposes the `RESTDataSource` class that is responsible for fetching data from a REST API. To build a data source for a REST API, extend the `RESTDataSource` class and define `this.baseURL`.
|
||||
|
||||
Create a new `datasources` folder inside the `src` directory. This folder will contain our data source files. Now, create `launch.js` within the `datasources` directory.
|
||||
|
||||
The REST API endpoint we'll use for our app is `https://api.spacexdata.com/v2/`. Go ahead and add the endpoint as the base URL as shown in the code below:
|
||||
In our example, the `baseURL` for our API is `https://api.spacexdata.com/v2/`. Let's create our `LaunchAPI` data source by adding the code below to `src/datasources/launch.js`:
|
||||
|
||||
_src/datasources/launch.js_
|
||||
|
||||
@@ -39,29 +36,43 @@ class LaunchAPI extends RESTDataSource {
|
||||
module.exports = LaunchAPI;
|
||||
```
|
||||
|
||||
In the code above, we required the `RESTDataSource` class and extended it with our custom `LaunchAPI` class. We then set up the base URL in the class constructor to the endpoint that fetches the data needed for our application.
|
||||
The Apollo `RESTDataSource` also sets up an in-memory cache that caches responses from our REST resources with no additional setup. We call this **partial query caching**. What's great about this cache is that you can reuse existing caching logic that your REST API exposes. If you're curious to learn more about partial query caching with Apollo data sources, please check out [our blog post](https://blog.apollographql.com/easy-and-performant-graphql-over-rest-e02796993b2b).
|
||||
|
||||
The next step is to add methods to the `LaunchAPI` class that corresponds to the type of queries our UI will fetch from the server. According to our app specifications, we'll need to get all the launches, and get a specific launch. So, let's take care of the former immediately.
|
||||
<h3 id="fetching">Write data fetching methods</h3>
|
||||
|
||||
The next step is to add methods to the `LaunchAPI` data source that correspond to the queries our graph API needs to fetch. According to our schema, we'll need a method to get all of the launches. Let's add a `getAllLaunches` method to our `LaunchAPI` class now:
|
||||
|
||||
_src/datasources/launch.js_
|
||||
|
||||
```js
|
||||
...
|
||||
async getAllLaunches() {
|
||||
const res = await this.get('launches');
|
||||
|
||||
return res && res.length ? res.map(l => this.launchReducer(l)) : [];
|
||||
}
|
||||
```
|
||||
|
||||
In the code above, `this.get('launches')`, makes a `GET` request to `https://api.spacexdata.com/v2/launches` and stores the returned data in the `res` variable. If the `res` variable is not empty, then the `getAllLaunches` method maps through the launches and returns an object that corresponds with the schema fields of the `Launch` schema type via the `launchReducer` method. If there are no launches, an empty array is returned.
|
||||
The Apollo REST data sources has helper methods that correspond to HTTP verbs like `GET` and `POST`. In the code above, `this.get('launches')`, makes a `GET` request to `https://api.spacexdata.com/v2/launches` and stores the returned launches in the `res` variable. Then, the `getAllLaunches` method maps over the launches and transforms the response from our REST endpoint with `this.launchReducer`. If there are no launches, an empty array is returned.
|
||||
|
||||
What object is returned via the `launchReducer` method? Copy the `launchReducer` method below and add to the file.
|
||||
Now, we need to write our `launchReducer` method in order to transform our launch data into the shape our schema expects. We recommend this approach in order to decouple your graph API from business logic specific to your REST API. First, let's recall what our `Launch` type looks like in our schema. You don't have to copy this code:
|
||||
|
||||
_src/schema.js_
|
||||
|
||||
```graphql
|
||||
type Launch {
|
||||
id: ID!
|
||||
year: String
|
||||
mission: Mission!
|
||||
rocket: Rocket
|
||||
launchSuccess: Boolean
|
||||
isBooked: Boolean
|
||||
}
|
||||
```
|
||||
|
||||
Next, let's write a `launchReducer` function to transform the data into that shape. Copy the following code into your `LaunchAPI` class:
|
||||
|
||||
_src/datasources/launch.js_
|
||||
|
||||
```js
|
||||
...
|
||||
launchReducer(launch) {
|
||||
return {
|
||||
id: launch.flight_number || 0,
|
||||
@@ -82,252 +93,76 @@ launchReducer(launch) {
|
||||
}
|
||||
```
|
||||
|
||||
With the above changes, we can easily make changes to the `launchReducer` method while the `getAllLaunches` method stays lean and concise. The `launchReducer` method also makes testing the `LaunchAPI` data source class easier. The later part of this tutorial covers testing!
|
||||
With the above changes, we can easily make changes to the `launchReducer` method while the `getAllLaunches` method stays lean and concise. The `launchReducer` method also makes testing the `LaunchAPI` data source class easier, which we'll cover later.
|
||||
|
||||
Next, let's take care of getting a specific launch. Add the method, `getLaunchById`, and `getLaunchesByIds` to the `LaunchAPI` class.
|
||||
Next, let's take care of fetching a specific launch by its ID. Let's add two methods, `getLaunchById`, and `getLaunchesByIds` to the `LaunchAPI` class.
|
||||
|
||||
_src/datasources/launch.js_
|
||||
|
||||
```js
|
||||
...
|
||||
async getLaunchById({ launchId }) {
|
||||
const res = await this.get('launches', { flight_number: launchId });
|
||||
return this.launchReducer(res[0]);
|
||||
}
|
||||
|
||||
async getLaunchesByIds({ launchIds }) {
|
||||
getLaunchesByIds({ launchIds }) {
|
||||
return Promise.all(
|
||||
launchIds.map(launchId => this.getLaunchById({ launchId })),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The `getLaunchById` method takes in a flight number and returns the data for a particular launch, while `getLaunchesByIds` returns several launches based on their respective `launchIds`. `Promise.all()` takes an array of promises and returns a single promise that resolves when all the promises in the array have been resolved with their fulfilled values.
|
||||
The `getLaunchById` method takes in a flight number and returns the data for a particular launch, while `getLaunchesByIds` returns several launches based on their respective `launchIds`.
|
||||
|
||||
Now that we've connected our REST API successfully, let's connect our database!
|
||||
|
||||
<h2 id="database">Connect a database</h2>
|
||||
|
||||
A data store is needed for saving and fetching user information. It's also important for user trips. We'll make use of [SQLite](https://www.sqlite.org) for our app's database. SQLite is a self-contained, light-weight, zero-configuration and embedded SQL database engine.
|
||||
Our REST API is read-only, so we need to connect our graph API to a database for saving and fetching user data. This tutorial uses SQLite for our SQL database, and Sequelize for our ORM. Since this section contains some SQL-specific code that isn't necessary to understanding Apollo data sources, we've already built a `UserAPI` data source for you in `src/datasources/user.js`. Please navigate to that file so we can explain the overall concepts.
|
||||
|
||||
Before connecting to SQLite, go ahead and install the `sequelize` package from npm:
|
||||
<h3 id="custom-data-source">Build a custom data source</h3>
|
||||
|
||||
```bash
|
||||
npm install sequelize --save
|
||||
```
|
||||
Apollo doesn't have support for a SQL data source yet (although we'd love to help guide you if you're interested in contributing), so we will need to create a custom data source for our database by extending the generic Apollo data source class. You can create your own with the `apollo-datasource` package.
|
||||
|
||||
**Sequelize** is an ORM for Node.js that supports several relational database management systems such as MySQL, MariaDB, PostgreSQL, SQLite and MSSQL.
|
||||
Here are some of the core concepts for creating your own data source:
|
||||
|
||||
Now, create a `store.sqlite` file in the root directory. Once you have done that, change from the root directory to the `src/datasources` directory:
|
||||
- The `initialize` method: You'll need to implement this method if you want to pass in any configuration options to your class. Here, we're using this method to access our graph API's context.
|
||||
- `this.context`: A graph API's context is an object that's shared among every resolver in a GraphQL request. We're going to explain this in more detail in the next section. Right now, all you need to know is that the context is useful for storing user information.
|
||||
- Caching: While the REST data source comes with its own built in cache, the generic data source does not. You can use [our cache primitives](/docs/apollo-server/features/data-sources.html#Using-Memcached-Redis-as-a-cache-storage-backend) to build your own, however!
|
||||
|
||||
```bash
|
||||
cd src/datasources
|
||||
```
|
||||
Let's go over some of the methods we created in `src/datasources/user.js` to fetch and update data in our database. You will want to reference these in the next section:
|
||||
|
||||
Next, create a `user.js` file inside the `src/datasources` directory. We'll connect to the SQLite database and set up the methods for interacting with the SQL data source within the `src/datasources/user.js` file. Time to set that up!
|
||||
- `findOrCreateUser({ email })`: Finds or creates a user with a given `email` in the database
|
||||
- `bookTrips({ launchIds })`: Takes an object with an array of `launchIds` and books them for the logged in user
|
||||
- `cancelTrip({ launchId })`: Takes an object with a `launchId` and cancels that launch for the logged in user
|
||||
- `getLaunchIdsByUser()`: Returns all booked launches for the logged in user.
|
||||
- `isBookedOnLaunch({ launchId })`: Determines whether the logged in user booked a certain launch
|
||||
|
||||
Copy the code below and add it to the `src/datasources/user.js` file.
|
||||
<h2 id="apollo-server">Add data sources to Apollo Server</h2>
|
||||
|
||||
_src/datasources/user.js_
|
||||
Now that we've built our `LaunchAPI` data source to connect our REST API and our `UserAPI` data source to connect our SQL database, we need to add them to our graph API.
|
||||
|
||||
```js
|
||||
const { DataSource } = require('apollo-datasource');
|
||||
const isEmail = require('isemail');
|
||||
|
||||
class UserAPI extends DataSource {
|
||||
constructor({ store }) {
|
||||
super();
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
initialize(config) {
|
||||
this.context = config.context;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the code above, we passed an instance of `store`, which is the function for interacting with the SQLite database into the constructor.
|
||||
|
||||
The `initialize` method is a `DataSource` class method that allows for setting up config within the `UserAPI` class. In this scenario, we assign `context` manually to the class variable, `this.context`, because custom data sources don't automatically get the request context from the `ApolloServer` constructor.
|
||||
|
||||
Let's add more methods to the `UserAPI` class.
|
||||
|
||||
<h4 id="create-user">Create a User</h4>
|
||||
|
||||
Head over to your terminal and install the `isemail` package:
|
||||
|
||||
```bash
|
||||
npm install isemail --save
|
||||
```
|
||||
|
||||
The `isemail` package is an npm module that validates emails. Now, write the code to find or create a user within the `UserAPI` class below:
|
||||
|
||||
_src/datasources/user.js_
|
||||
|
||||
```js
|
||||
...
|
||||
|
||||
async findOrCreateUser() {
|
||||
const email = this.context && this.context.user ? this.context.user.email : null;
|
||||
|
||||
if (!email || !isEmail.validate(email)) return null;
|
||||
|
||||
const users = await this.store.users.findOrCreate({ where: { email } });
|
||||
return users && users[0] ? users[0] : null;
|
||||
}
|
||||
```
|
||||
|
||||
The `findOrCreateUser` method checks the context object whether a user's email is present or not. It then checks whether the email is a valid email address. If it's not valid, null is returned, else it runs a check within the `users` table in the SQLite database. If the email exists in the database, then the user already exists, else a new user is created, and stored in the database.
|
||||
|
||||
It's worthy to note that the user object in `this.context` is extracted from the token gotten from the request headers during authentication. The value is then passed to the `context`. This is why we can access the user via `this.context.user` and `this.context.user.email`.
|
||||
|
||||
<h4 id="book-and-cancel-trip">Book and Cancel a Trip</h4>
|
||||
|
||||
Add a `bookTrip` and `cancelTrip` method to the `UserAPI` data source class.
|
||||
|
||||
_src/datasources/user.js_
|
||||
|
||||
```js
|
||||
...
|
||||
class UserAPI extends DataSource {
|
||||
constructor() {
|
||||
...
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
async bookTrip({ launchId }) {
|
||||
const userId = this.context.user.id;
|
||||
return !!this.store.trips.findOrCreate({ where: { userId, launchId } });
|
||||
}
|
||||
|
||||
async cancelTrip({ launchId }) {
|
||||
const userId = this.context.user.id;
|
||||
return !!this.store.trips.destroy({ where: { userId, launchId } });
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
A user selects a particular launch and books a trip. The `userId` and `launchId` values are needed to book the trip successfully. Therefore, the `bookTrip` method accepts a `launchId` via its arguments, obtains the `userId` via the `context` object and invokes the `findOrCreate` method on the `trips` table to book the trip.
|
||||
|
||||
The `cancelTrip` method requires a `userId` and `launchId` to delete a trip from the `trips` table successfully. Therefore, the `cancelTrip` method performs almost the same operation as the `bookTrip` method except that it invokes the `destroy` method on the `trips` table and deletes the trip.
|
||||
|
||||
<h4 id="get-launches">Get All Launches By User</h4>
|
||||
|
||||
We need to get all the launches reserved by a user. This calls for a method, `getLaunchIdsByUser`. Copy the method below and add it to the file.
|
||||
|
||||
_src/datasources/user.js_
|
||||
|
||||
```js
|
||||
...
|
||||
class UserAPI extends DataSource {
|
||||
constructor() {
|
||||
...
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
async getLaunchIdsByUser() {
|
||||
const userId = this.context.user.id;
|
||||
const found = await this.store.trips.findAll({
|
||||
where: { userId },
|
||||
});
|
||||
return found && found.length
|
||||
? found.map(l => l.dataValues.launchId).filter(l => !!l)
|
||||
: [];
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
Let's analyze the code above.
|
||||
|
||||
In the `getLaunchIdsByUser` method, a `userId` is accepted via the `context` object. All the trips booked by a user with a particular `userId` are fetched and stored in the `found` variable. If there are trips found, then an array of launch ids are returned else an empty array is returned.
|
||||
|
||||
<h4 id="get-launches">Get Booked Status on a Launch</h4>
|
||||
|
||||
We need to add a method that can return the booked status of a launch for a particular user. Copy the method, `isBookedOnLaunch` below and add it to the `UserAPI` class.
|
||||
|
||||
_src/datasources/user.js_
|
||||
|
||||
```js
|
||||
...
|
||||
async isBookedOnLaunch({ launchId }) {
|
||||
const userId = this.context.user.id;
|
||||
const found = await this.store.trips.findAll({
|
||||
where: { userId, launchId },
|
||||
});
|
||||
return found && found.length > 0;
|
||||
}
|
||||
```
|
||||
|
||||
In the `isBookedOnLaunch` method, we invoke the `findAll` method of the `trips` table to find out if the particular launch passed to the method via `launchId` has been booked by the logged-in user.
|
||||
|
||||
In the various methods that we created and copied to the `UserAPI` class, you must have noticed `this.store.users` and `this.store.trips`. These are two tables from our potential SQLite data store. Let's create the store!
|
||||
|
||||
<h4 id="create-the-store">Create the Store</h4>
|
||||
|
||||
Fork the [GitHub repo--Create A Store](insert-repo-link-here) for the store.
|
||||
|
||||
In `src/utils.js` file, the `createStore` function sets up a new SQL instance that connects to the SQLite database. A `database`, `username`, and `password` values are passed as arguments. And an object specifying the `dialect`, location of the SQLite database and operator aliases is also passed as an argument to the SQL instance.
|
||||
|
||||
The `users` and `trips` tables have now been defined with their respective fields. And an object containing `users` and `trips`is returned within the `createStore` function to enable us access the ORM methods later on, in the body of our data source.
|
||||
|
||||
<h2 id="database">Connect Data Sources and Store to Server</h2>
|
||||
|
||||
Now that we have defined our data sources, they need to be passed as options to the `ApolloServer` constructor so that our resolvers can access them.
|
||||
|
||||
Copy the code below and add it to the `src/index.js` file.
|
||||
Adding our data sources is simple, just create a `dataSources` property on your `ApolloServer` that corresponds to a function that returns an object with your instantiated data sources. Let's see what that looks like by navigating to `src/index.js` and adding the code below:
|
||||
|
||||
_src/index.js_
|
||||
|
||||
```js
|
||||
...
|
||||
...
|
||||
const typeDefs = require('./schema');
|
||||
const { createStore } = require('./utils');
|
||||
|
||||
const LaunchAPI = require('./datasources/launch');
|
||||
const UserAPI = require('./datasources/user');
|
||||
|
||||
const store = createStore();
|
||||
|
||||
// Set up Apollo Server
|
||||
const server = new ApolloServer({
|
||||
typeDefs,
|
||||
dataSources: () => ({
|
||||
launchAPI: new LaunchAPI(),
|
||||
userAPI: new UserAPI({ store }),
|
||||
}),
|
||||
context: async ({ req }) => {
|
||||
|
||||
const email = 'johndoe@apollo.com';
|
||||
|
||||
// if the email isn't formatted validly, return null for user
|
||||
if (!isEmail.validate(email)) return { user: null };
|
||||
|
||||
// find a user by their email
|
||||
const users = await store.users.findOrCreate({ where: { email } });
|
||||
|
||||
const user = users && users[0] ? users[0] : null;
|
||||
|
||||
return { user: { ...user.dataValues } };
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
In the code above, we required the `launch` and `user` data source files, created an instance of both classes and passed them as objects to the `dataSources` function in `ApolloServer`'s constructor.
|
||||
First, we import our `createStore` function to set up our database, as well as our data sources: `LaunchAPI` and `UserAPI`. Then, we create our database by calling `createStore`. Finally, we add the `dataSources` function to our `ApolloServer` to connect `LaunchAPI` and `UserAPI` to our graph. We also pass in our database we created to the `UserAPI` data source.
|
||||
|
||||
We also required the `utils.js` file, assigned the `createStore()` method to a `store` variable and passed it to the `UserAPI` constructor. The `createStore()` method is responsible for making sure the `users` and `trips` tables exist.
|
||||
|
||||
```js
|
||||
dataSources: () => ({
|
||||
launchAPI: new LaunchAPI(),
|
||||
userAPI: new UserAPI({ store }),
|
||||
}),
|
||||
```
|
||||
|
||||
The block of code above ensures that when Apollo Server boots up, the server puts the data sources on the `context` for every request, so you can access them from your resolvers.
|
||||
|
||||
Now, what about the `context` function defined explicitly in the `ApolloServer` constructor?
|
||||
|
||||
As shown in the code above, If the email presented is not valid, a null user is returned as the `context` object's value.
|
||||
|
||||
If the email is valid, we simply look up the email in the `users` store. If the user exists, we return the user's details as the `context` object's value, else we return null. The user's details is what we access as `this.context.user` via the `initialize` method in the `UserAPI` data source class.
|
||||
|
||||
In the next section of this tutorial, we'll write the resolvers for our app and you'll learn more about `context` and how to access the data sources on them.
|
||||
Now that we've hooked up our data sources to Apollo Server, it's time to move onto the next section and learn how to call our data sources from within our resolvers.
|
||||
Reference in New Issue
Block a user