Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 551b6ee

Browse files
committed
Remove "viewer" field, add DataLoader.create() factory
- The "viewer" wrapper field is no longer needed with Relay Modern. - DataLoader instances were moved to /src/DataLoader.js, it's better to initialize data loaders separately for each web request.
1 parent 791f388 commit 551b6ee

File tree

15 files changed

+357
-325
lines changed

15 files changed

+357
-325
lines changed

README.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ meant to be paired with a web and/or mobile application project such as [React S
3333
│ ├── /types/ # GraphQL types with resolve functions
3434
│ │ ├── /Node.js # Relay's "node" definitions
3535
│ │ ├── /UserType.js # User account (id, email, etc.)
36-
│ │ ├── /ViewerType.js # The top-level GraphQL object type
3736
│ │ └── /... # etc.
3837
│ ├── /app.js # Express.js application
38+
│ ├── /DataLoader.js # Data access utility for GraphQL /w batching and caching
3939
│ ├── /db.js # Database access and connection pooling (via Knex)
40+
│ ├── /email.js # Client utility for sending transactional email
4041
│ ├── /passport.js # Passport.js authentication strategies
4142
│ ├── /redis.js # Redis client
4243
│ ├── /schema.js # GraphQL schema
@@ -152,19 +153,15 @@ However, if you decide to get involved, please take a moment to review the [guid
152153

153154
## Support
154155

155-
* [#nodejs-api-starter](http://stackoverflow.com/questions/tagged/nodejs-api-starter) on Stack Overflow — Questions and answers
156156
* [#nodejs-api-starter](https://gitter.im/kriasoft/nodejs-api-starter) on Gitter — Watch announcements, share ideas and feedback
157-
* [GitHub Issues](https://github.com/kriasoft/nodejs-api-starter/issues) — Check open issues, send feature requests
158-
* [@koistya](https://twitter.com/koistya) on [Codementor](https://www.codementor.io/koistya), [HackHands](https://hackhands.com/koistya/) or [Skype][skype] — Private consulting and customization requests
157+
* [GitHub Issues](https://github.com/kriasoft/nodejs-api-starter/issues) — Check open issues, send bug reports feature requests
158+
* [@koistya](https://twitter.com/koistya) on [Codementor](https://www.codementor.io/koistya) or [Skype][skype] — Private consulting and customization requests
159159

160160

161161
## License
162162

163-
Copyright © 2016-present Kriasoft, LLC. This source code is licensed under the MIT
164-
license found in the [LICENSE.txt](https://github.com/kriasoft/nodejs-api-starter/blob/master/LICENSE.txt)
165-
file. The documentation to the project is licensed under the
166-
[CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/) license.
167-
163+
Copyright © 2016-present Kriasoft. This source code is licensed under the MIT license found in the
164+
[LICENSE.txt](https://github.com/kriasoft/nodejs-api-starter/blob/master/LICENSE.txt) file.
168165

169166
---
170167
Made with ♥ by Konstantin Tarkus ([@koistya](https://twitter.com/koistya), [blog](https://medium.com/@tarkus)) and [contributors](https://github.com/kriasoft/nodejs-api-starter/graphs/contributors)
@@ -190,4 +187,4 @@ Made with ♥ by Konstantin Tarkus ([@koistya](https://twitter.com/koistya), [bl
190187
[redis]: https://redis.io/
191188
[loader]: https://github.com/facebook/dataloader
192189
[gitter]: https://gitter.im/kriasoft/nodejs-api-starter
193-
[skype]: https://hatscripts.com/addskype?koistya
190+
[skype]: https://calendly.com/koistya

package.json

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,35 @@
88
},
99
"dependencies": {
1010
"bluebird": "^3.5.0",
11-
"body-parser": "^1.17.1",
12-
"connect-redis": "^3.2.0",
11+
"body-parser": "^1.17.2",
12+
"connect-redis": "^3.3.0",
1313
"cookie-parser": "^1.4.3",
1414
"cors": "^2.8.3",
1515
"dataloader": "^1.3.0",
16-
"express": "^4.15.2",
16+
"express": "^4.15.3",
1717
"express-flash": "^0.0.2",
18-
"express-graphql": "^0.6.4",
19-
"express-session": "^1.15.2",
18+
"express-graphql": "^0.6.5",
19+
"express-session": "^1.15.3",
2020
"faker": "^4.1.0",
21-
"graphql": "^0.9.3",
21+
"graphql": "^0.9.6",
2222
"graphql-relay": "^0.5.1",
23-
"handlebars": "^4.0.6",
23+
"handlebars": "^4.0.8",
2424
"handlebars-layouts": "^3.1.4",
25-
"i18next": "^8.0.0",
26-
"i18next-express-middleware": "^1.0.4",
27-
"i18next-node-fs-backend": "^0.1.3",
28-
"juice": "^4.0.2",
29-
"knex": "^0.12.9",
25+
"i18next": "^8.2.1",
26+
"i18next-express-middleware": "^1.0.5",
27+
"i18next-node-fs-backend": "^1.0.0",
28+
"juice": "^4.1.0",
29+
"knex": "^0.13.0",
3030
"node-fetch": "^1.6.3",
3131
"nodemailer": "^4.0.1",
3232
"passport": "^0.3.2",
3333
"passport-facebook": "^2.1.1",
3434
"passport-google-oauth": "^1.0.0",
3535
"passport-twitter": "^1.0.4",
36-
"pg": "^6.1.5",
36+
"pg": "^6.2.2",
3737
"pretty-error": "^2.1.0",
3838
"redis": "^2.7.1",
39-
"source-map-support": "^0.4.14",
39+
"source-map-support": "^0.4.15",
4040
"validator": "^7.0.0"
4141
},
4242
"devDependencies": {
@@ -52,20 +52,20 @@
5252
"babel-register": "^6.24.1",
5353
"chai": "^3.5.0",
5454
"chai-http": "^3.0.0",
55-
"chokidar": "^1.6.1",
55+
"chokidar": "^1.7.0",
5656
"eslint": "^3.19.0",
57-
"eslint-config-airbnb-base": "^11.1.3",
58-
"eslint-plugin-flowtype": "^2.32.1",
57+
"eslint-config-airbnb-base": "^11.2.0",
58+
"eslint-plugin-flowtype": "^2.33.0",
5959
"eslint-plugin-import": "^2.2.0",
60-
"flow-bin": "^0.44.2",
61-
"mocha": "^3.2.0",
60+
"flow-bin": "^0.46.0",
61+
"mocha": "^3.4.1",
6262
"rimraf": "^2.6.1"
6363
},
6464
"scripts": {
6565
"lint": "eslint src test tools migrations",
6666
"check": "flow check",
67-
"test": "mocha test/unit --compilers js:babel-register",
68-
"test:watch": "mocha test/unit --compilers js:babel-register --reporter min --watch",
67+
"test": "NODE_ENV=test mocha test/unit --compilers js:babel-register",
68+
"test:watch": "NODE_ENV=test mocha test/unit --compilers js:babel-register --reporter min --watch",
6969
"build": "node tools/build.js",
7070
"build:watch": "node tools/build.js --watch",
7171
"db:version": "node tools/db.js version",

src/DataLoader.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Node.js API Starter Kit (https://reactstarter.com/nodejs)
3+
*
4+
* Copyright © 2016-present Kriasoft, LLC. All rights reserved.
5+
*
6+
* This source code is licensed under the MIT license found in the
7+
* LICENSE.txt file in the root directory of this source tree.
8+
*/
9+
10+
/* @flow */
11+
/* eslint-disable global-require */
12+
13+
import DataLoader from 'dataloader';
14+
import Article from './models/Article';
15+
import User from './models/User';
16+
17+
/**
18+
* Data access utility to be used with GraphQL resolve() functions. For example:
19+
*
20+
* new GraphQLObjectType({
21+
* ...
22+
* resolve(post, args, { loader }) {
23+
* return loader.users.load(post.authorId);
24+
* }
25+
* })
26+
*
27+
* For more information visit https://github.com/facebook/dataloader
28+
*/
29+
export default {
30+
create: () => ({
31+
users: new DataLoader(keys => User.findByIds(keys)),
32+
articles: new DataLoader(keys => Article.findByIds(keys)),
33+
}),
34+
};

src/app.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import i18nextBackend from 'i18next-node-fs-backend';
2323
import expressGraphQL from 'express-graphql';
2424
import PrettyError from 'pretty-error';
2525
import { printSchema } from 'graphql';
26+
import DataLoader from './DataLoader';
2627
import email from './email';
2728
import redis from './redis';
2829
import passport from './passport';
@@ -81,7 +82,9 @@ app.get('/graphql/schema', (req, res) => {
8182
app.use('/graphql', expressGraphQL(req => ({
8283
schema,
8384
context: {
85+
t: req.t,
8486
user: req.user,
87+
loader: DataLoader.create(),
8588
},
8689
graphiql: process.env.NODE_ENV !== 'production',
8790
pretty: process.env.NODE_ENV !== 'production',

src/email.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@ function loadTemplate(filename) {
3838
return handlebars.template(m.exports);
3939
}
4040

41-
fs.readdirSync(baseDir).forEach((name) => {
42-
if (fs.statSync(`${baseDir}/${name}`).isDirectory()) {
43-
templates.set(name, {
44-
subject: loadTemplate(`${baseDir}/${name}/subject.js`),
45-
html: loadTemplate(`${baseDir}/${name}/html.js`),
46-
});
47-
}
48-
});
41+
if (process.env.NODE_ENV !== 'test') {
42+
fs.readdirSync(baseDir).forEach((name) => {
43+
if (fs.statSync(`${baseDir}/${name}`).isDirectory()) {
44+
templates.set(name, {
45+
subject: loadTemplate(`${baseDir}/${name}/subject.js`),
46+
html: loadTemplate(`${baseDir}/${name}/html.js`),
47+
});
48+
}
49+
});
50+
}
4951

5052
/**
5153
* Usage example:

src/models/Article.js

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,19 @@
99

1010
/* @flow */
1111

12-
import DataLoader from 'dataloader';
1312
import fetch from 'node-fetch';
1413
import redis from '../redis';
1514

16-
const DATA_URI = 'https://gist.githubusercontent.com/koistya/a32919e847531320675764e7308b796a/raw/articles.json';
17-
18-
let loader;
1915
let lastFetchTime = 0;
16+
const DATA_URI = 'https://gist.githubusercontent.com/koistya/a32919e847531320675764e7308b796a/raw/articles.json';
2017

2118
// returns [ { id: 1, title: '...', author: '...', url: '...' }, ... ]
2219
async function fetchArticles() {
2320
lastFetchTime = Date.now();
2421
const data = await fetch(DATA_URI).then(x => x.json());
2522
await redis.msetAsync(data.reduce((acc, val, idx) =>
26-
[...acc, `articles:${data.length - idx}`, JSON.stringify(val)], []));
27-
return data.map((x, i) => ({ id: data.length - i, ...x }));
23+
[...acc, `articles:${data.length - idx}`, JSON.stringify({ id: data.length - idx, ...val })], []));
24+
return data;
2825
}
2926

3027
class Article {
@@ -33,29 +30,34 @@ class Article {
3330
author: string;
3431
url: string;
3532

36-
static async find() {
33+
constructor(props: any) {
34+
Object.assign(this, props);
35+
}
36+
37+
static async find(): Promise<Article[]> {
3738
const keys = await redis.keysAsync('articles:*');
3839
const data = keys.length ?
39-
(await redis.mgetAsync(keys)).map((x, i) => ({ id: keys[i], ...JSON.parse(x) })) :
40+
(await redis.mgetAsync(keys)).map(x => JSON.parse(x)) :
4041
(await fetchArticles());
4142

4243
// Update cache in the background if it's older than 10 minutes
4344
if (Date.now() - lastFetchTime > 600000) fetchArticles();
4445

45-
return data.map(x => Object.assign(new Article(), x));
46+
return data.map(x => new Article(x));
4647
}
4748

48-
static load(keys) {
49-
return loader.load(keys);
49+
static findOneById(id: number): Promise<Article> {
50+
return redis.getAsync(`articles:${id}`).then(x => x && new Article(JSON.parse(x)));
5051
}
51-
}
5252

53-
loader = new DataLoader(keys => Promise.resolve()
54-
.then(() => lastFetchTime ? null : fetchArticles()) // eslint-disable-line no-confusing-arrow
55-
.then(() => redis.mgetAsync(keys.map(x => `articles:${x}`)))
56-
.then(data => data.map((x, i) => {
57-
if (x) return Object.assign(new Article(), x, { id: keys[i] });
58-
throw new Error(`Cannot find an article with ID ${keys[i]}`);
59-
})));
53+
static async findByIds(ids: number[]): Promise<Article[]> {
54+
if (!lastFetchTime) await fetchArticles();
55+
return redis.mgetAsync(ids.map(id => `articles:${id}`))
56+
.then(data => data.map((x, i) => {
57+
if (!x) throw new Error(`Cannot find an article with ID ${ids[i]}`);
58+
return new Article(JSON.parse(x));
59+
}));
60+
}
61+
}
6062

6163
export default Article;

src/models/User.js

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,59 @@
1111

1212
import db from '../db';
1313

14+
const fields = [
15+
'id',
16+
'email',
17+
];
18+
1419
class User {
20+
id: string;
21+
email: string;
22+
23+
constructor(props: any) {
24+
Object.assign(this, props);
25+
}
26+
27+
static find(...args) {
28+
return db.table('users')
29+
.where(...(args.length ? args : [{}]))
30+
.select(...fields)
31+
.then(rows => rows.map(x => new User(x)));
32+
}
1533

16-
static findOne(...args) {
17-
return db.table('users').where(...args).first('id', 'email');
34+
static findByIds(ids: string[]): Promise<User[]> {
35+
return db.table('users')
36+
.whereIn('id', ids)
37+
.select(...fields)
38+
.then(rows => ids.map((id) => {
39+
const row = rows.find(x => x.id === id);
40+
return row && new User(row);
41+
}));
42+
}
43+
44+
static findOne(...args): Promise<User> {
45+
return db.table('users')
46+
.where(...(args.length ? args : [{}]))
47+
.first(...fields)
48+
.then(x => x && new User(x));
1849
}
1950

2051
static findOneByLogin(provider: string, key: string) {
2152
return db.table('users')
2253
.leftJoin('user_logins', 'users.id', 'user_logins.user_id')
2354
.where({ 'user_logins.name': provider, 'user_logins.key': key })
24-
.first('id', 'email');
55+
.first(...fields)
56+
.then(x => x && new User(x));
2557
}
2658

27-
static any(...args) {
28-
return db.raw('SELECT EXISTS ?', db.table('users').where(...args).select(db.raw('1')))
59+
static any(...args): boolean {
60+
return db.raw('SELECT EXISTS ?', db.table('users').where(...(args.length ? args : [{}])).select(db.raw('1')))
2961
.then(x => x.rows[0].exists);
3062
}
3163

3264
static create(user) {
3365
return db.table('users')
34-
.insert(user, ['id', 'email']).then(x => x[0]);
66+
.insert(user, fields).then(x => new User(x[0]));
3567
}
3668

3769
static setClaims(userId, provider, providerKey, claims) {

src/models/index.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/schema.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,36 @@
1010
/* @flow */
1111

1212
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
13-
import Node from './types/Node';
14-
import ViewerType from './types/ViewerType';
13+
import { connectionArgs, connectionDefinitions, connectionFromPromisedArray } from 'graphql-relay';
14+
import { nodeField, nodesField } from './types/Node';
15+
import Article from './models/Article';
16+
import ArticleType from './types/ArticleType';
17+
import UserType from './types/UserType';
1518

16-
// In order to make it work with Relay 0.x, all the top-level
17-
// fields are placed inside the "viewer" field
1819
export default new GraphQLSchema({
1920
query: new GraphQLObjectType({
2021
name: 'Query',
2122
fields: {
22-
node: Node.nodeField,
23-
nodes: Node.nodesField,
24-
viewer: {
25-
type: ViewerType,
26-
resolve() {
27-
return Object.create(null);
23+
node: nodeField,
24+
nodes: nodesField,
25+
me: {
26+
type: UserType,
27+
resolve(root, args, { user }) {
28+
return user;
29+
},
30+
},
31+
articles: {
32+
type: connectionDefinitions({
33+
name: 'ArticleConnection',
34+
nodeType: ArticleType,
35+
}).connectionType,
36+
description: 'Featured articles',
37+
args: connectionArgs,
38+
resolve(root, args, { loader }) {
39+
return connectionFromPromisedArray(Article.find().then(items => items.map((x) => {
40+
loader.articles.prime(x.id, x);
41+
return x;
42+
})), args);
2843
},
2944
},
3045
},

0 commit comments

Comments
 (0)