mirror of
https://github.com/zhigang1992/apollo.git
synced 2026-05-26 22:57:34 +08:00
Connect to database
This commit is contained in:
committed by
Peggy Rayzis
parent
d1400b5659
commit
05e0bd88c1
@@ -7,18 +7,19 @@ Apollo data sources provide the best experience for fetching and caching data fr
|
||||
|
||||
<h2 id="rest-api">Connect a REST API</h2>
|
||||
|
||||
To get started connecting to a REST API, install the `apollo-datasource` and `apollo-datasource-rest` packages:
|
||||
To get started, install the `apollo-datasource` and `apollo-datasource-rest` packages:
|
||||
|
||||
```bash
|
||||
npm install apollo-datasource-rest
|
||||
npm install apollo-datasource apollo-datasource-rest --save
|
||||
```
|
||||
|
||||
The `apollo-datasource-rest` 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. Let's look at a simple example to understand how data sources work.
|
||||
* **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. Let's look at a simple example to understand how data sources work.
|
||||
|
||||
```js
|
||||
import { RESTDataSource } from 'apollo-datasource-rest';
|
||||
const { RESTDataSource } = require('apollo-datasource-rest');
|
||||
|
||||
export class MvrpAPI extends RESTDataSource {
|
||||
class MvrpAPI extends RESTDataSource {
|
||||
constructor() {
|
||||
super();
|
||||
this.baseURL = 'https://mvrp.herokuapp.com/api/';
|
||||
@@ -36,6 +37,8 @@ export class MvrpAPI extends RESTDataSource {
|
||||
return result[0];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = MvrpAPI;
|
||||
```
|
||||
|
||||
The `https://mvrp.herokuapp.com/api/` endpoint is a simple REST API that returns data for cars. Furthermore, the `MvrpAPI` class implementation in the code above contains a `getAllCars` and `getACar` functions that wrap convenience methods provided by the `RESTDataSource` class for performing HTTP requests. In this example, the built-in `get` method used is responsible for `GET` requests.
|
||||
@@ -142,4 +145,239 @@ async getLaunchesByIds({ launchIds }) {
|
||||
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.
|
||||
|
||||
|
||||
<h2 id="database">Connect a database</h2>
|
||||
<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. Let's 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.
|
||||
|
||||
Before connecting to SQLite, go ahead and install `sequelize`:
|
||||
|
||||
```bash
|
||||
npm install sequelize --save
|
||||
```
|
||||
|
||||
**Sequelize** is an ORM for Node.js that supports several relational database management systems such as MySQL, MariaDB, PostgreSQL, SQLite and MSSQL. In this tutorial, we'll make use of it for the SQLite database.
|
||||
|
||||
Now, create a `store.sqlite` file in the root directory. Once you have done that, change the directory from root to `src/datasources`:
|
||||
|
||||
```bash
|
||||
cd src/datasources
|
||||
```
|
||||
|
||||
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 up!
|
||||
|
||||
_src/datasources/user.js_
|
||||
|
||||
```js
|
||||
const { DataSource } = require('apollo-datasource');
|
||||
const SQL = require('sequelize');
|
||||
|
||||
class UserAPI extends DataSource {
|
||||
constructor() {
|
||||
super();
|
||||
this.store = createStore();
|
||||
}
|
||||
}
|
||||
|
||||
const createStore = () => {
|
||||
const Op = SQL.Op;
|
||||
const operatorsAliases = {
|
||||
$in: Op.in,
|
||||
};
|
||||
|
||||
const db = new SQL('rocket', null, null, {
|
||||
dialect: 'sqlite',
|
||||
storage: './store.sqlite',
|
||||
operatorsAliases,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = UserAPI;
|
||||
```
|
||||
|
||||
In the code above, the `createStore` function sets up a new SQL instance that connects to the SQLite database with the name of the database specified which is `rocket`. Username and password are null, the location and operator aliases are also required.
|
||||
|
||||
The `createStore` function is also invoked in the constructor and stored in a class variable called `store`.
|
||||
|
||||
Let's extend the `createStore` function to create a `users` and `trips` table.
|
||||
|
||||
```js
|
||||
...
|
||||
...
|
||||
const createStore = () => {
|
||||
...
|
||||
|
||||
const users = db.define('user', {
|
||||
id: {
|
||||
type: SQL.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
createdAt: SQL.DATE,
|
||||
updatedAt: SQL.DATE,
|
||||
email: SQL.STRING,
|
||||
token: SQL.STRING,
|
||||
});
|
||||
|
||||
const trips = db.define('trip', {
|
||||
id: {
|
||||
type: SQL.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
createdAt: SQL.DATE,
|
||||
updatedAt: SQL.DATE,
|
||||
launchId: SQL.INTEGER,
|
||||
userId: SQL.INTEGER,
|
||||
});
|
||||
|
||||
return { users, trips};
|
||||
|
||||
};
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Now that we are done with the table creation, let's set up methods in the `UserAPI` class to:
|
||||
|
||||
* Create a user.
|
||||
* Book a trip.
|
||||
* Cancel a trip.
|
||||
* Get launches reserved by a user.
|
||||
* Get all the users that have reserved a particular launch.
|
||||
|
||||
|
||||
### Create a User
|
||||
|
||||
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 below:
|
||||
|
||||
_src/datasources/user.js_
|
||||
|
||||
```js
|
||||
const { DataSource } = require('apollo-datasource');
|
||||
const SQL = require('sequelize');
|
||||
const isEmail = require('isemail');
|
||||
|
||||
class UserAPI extends DataSource {
|
||||
constructor() {
|
||||
super();
|
||||
this.store = createStore();
|
||||
}
|
||||
|
||||
userReducer(user) {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
avatar: user.avatar,
|
||||
};
|
||||
}
|
||||
|
||||
async findOrCreateUser({ email }) {
|
||||
if (!isEmail.validate(email)) return null;
|
||||
const users = await this.store.users.findOrCreate({ where: { email } });
|
||||
return users && users[0] ? this.userReducer(users[0]) : null;
|
||||
}
|
||||
}
|
||||
|
||||
....
|
||||
// the createStore function is here
|
||||
....
|
||||
|
||||
module.exports = UserAPI;
|
||||
```
|
||||
|
||||
The `userReducer` method makes the `UserAPI` class easier to test because it abstracts the user object been returned from the `findOrCreateUser` method into a different method.
|
||||
|
||||
The `findOrCreateUser` method takes in a user's email and checks whether the email argument 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, stored in the database.
|
||||
|
||||
### Book and Cancel a Trip
|
||||
|
||||
Add a `bookTrip` and `cancelTrip` method to the `UserAPI` data source class.
|
||||
|
||||
_src/datasources/user.js_
|
||||
|
||||
```js
|
||||
...
|
||||
class UserAPI extends DataSource {
|
||||
constructor() {
|
||||
...
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
async bookTrip({ userId, launchId }) {
|
||||
return this.store.trips.findOrCreate({ where: { userId, launchId } });
|
||||
}
|
||||
|
||||
async cancelTrip({ userId, launchId }) {
|
||||
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 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.
|
||||
|
||||
|
||||
### Get Launches and Users
|
||||
|
||||
We need to get all the launches reserved by a user and also obtain all the users that signed up for a particular launch. This calls for two methods, `getLaunchIdsByUser`, and `getUsersByLaunch`.
|
||||
|
||||
_src/datasources/user.js_
|
||||
|
||||
```js
|
||||
...
|
||||
class UserAPI extends DataSource {
|
||||
constructor() {
|
||||
...
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
async getLaunchIdsByUser({ userId }) {
|
||||
const found = await this.store.trips.findAll({
|
||||
where: { userId: userId },
|
||||
});
|
||||
|
||||
return found && found.length
|
||||
? found.map(l => l.dataValues.launchId).filter(l => !!l)
|
||||
: [];
|
||||
}
|
||||
|
||||
async getUsersByLaunch({ launchId }) {
|
||||
const found = await this.store.trips.findAll({
|
||||
where: { launchId: launchId },
|
||||
});
|
||||
|
||||
const userIds = found && found.length ? found.map(l => l.dataValues.userId) : [];
|
||||
|
||||
if (!userIds || !userIds.length) return [];
|
||||
|
||||
const foundUsers = await this.store.users.findAll({
|
||||
where: { id: { $in: userIds } },
|
||||
});
|
||||
|
||||
if (!foundUsers || !foundUsers.length) return [];
|
||||
|
||||
return foundUsers.map(u => this.userReducer(u));
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
Let's analyze the code above.
|
||||
|
||||
In the `getLaunchIdsByUser` method, a `userId` is accepted via the method argument. 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.
|
||||
|
||||
In the `getUsersByLaunch` method, a `launchId` is accepted via the method argument. All the trips booked for a particular launch with a specific `launchId` are fetched and stored in the `found` variable. Next, an array of user ids for trips in the `found` variable are obtained and stored in the `userIds` variable. If there are no user ids, then the `userIds` variable becomes an empty array.
|
||||
|
||||
So, if the `userIds` variable is empty, then the `getUsersByLaunch` method returns an empty array. However, if the `userIds` variable is not empty, then users with those ids are fetched from the users table.
|
||||
|
||||
If no users were found, the `getUsersByLaunch` method returns an empty array, else an array of users with their respective `id`, `email` and `avatar` is returned!
|
||||
Reference in New Issue
Block a user