├── .babelrc
├── .eslintrc.yml
├── .gitignore
├── .nvmrc
├── LICENSE
├── README.md
├── graphql-demo.gif
├── knexfile.js
├── migrations
└── 20160411194216_blog.js
├── mysql
├── package.json
├── presentation.pdf
├── seeds
└── blog.js
├── src
├── browser.js
├── components
│ ├── Blog.js
│ ├── Editor.css
│ ├── Editor.js
│ ├── Footer.js
│ ├── Header.js
│ ├── Notification.js
│ ├── Post.js
│ ├── PostCreator.js
│ ├── PostEditor.js
│ ├── Posts.js
│ ├── Presentation
│ │ ├── author.error.png
│ │ ├── author.fragment.png
│ │ ├── author.graphql
│ │ ├── author.inline.png
│ │ ├── author.json
│ │ ├── author.variables.png
│ │ ├── example.graphql
│ │ ├── example.js
│ │ ├── graphiql.png
│ │ ├── index.js
│ │ └── presentation.css
│ └── Users.js
├── middleware
│ ├── api.js
│ ├── files.js
│ └── view.js
├── presentation.js
├── schema
│ ├── AuthorType.js
│ ├── MutationType.js
│ ├── PostType.js
│ ├── QueryType.js
│ └── index.js
└── server.js
├── webpack.config.browser.babel.js
└── webpack.config.server.babel.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "react-html-attrs"
4 | ],
5 | "presets": ["es2015", "react", "react-hmre", "stage-1"]
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: airbnb
2 | parser: babel-eslint
3 | plugins:
4 | - react
5 | rules:
6 | # No "best" practice
7 | "arrow-body-style": 0
8 |
9 | "array-bracket-spacing": [2, "always"]
10 |
11 | # There's no "best" value
12 | "max-len": 0
13 |
14 | # express.Router()
15 | "new-cap": [2, { newIsCap: true, properties: false }]
16 |
17 | # Every express server starts with it
18 | "no-console": 0
19 |
20 | # Normalizing args shouldn't require a secondary variable
21 | "no-param-reassign": 0
22 |
23 | # ...args is common, get over it
24 | "no-shadow": 0
25 |
26 | # Documenting function signatures is useful
27 | "no-unused-vars": [2, { vars: local, args: none }]
28 |
29 | # No "best practice"
30 | "prefer-arrow-callback": 0
31 |
32 | # 'I can't help but make this mistake!'
33 | "quotes": [0, double, avoid-escape]
34 |
35 | # https://github.com/insin/babel-plugin-react-html-attrs
36 | "react/no-unknown-property": 0
37 |
38 | # I know what I'm doing
39 | "react/no-deprecated": 0
40 |
41 | # HMR works best with React.Component
42 | "react/prefer-stateless-function": 0
43 |
44 | # Warn, but don't break build
45 | "react/prop-types": 0
46 |
47 | "react/sort-comp": 0
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v5.10.1
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Eric Clemmons
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GraphQL Demo
2 |
3 | > A functioning introduction to GraphQL for [Node.js Houston][0]
4 | > –
5 |
6 | This [presentation][1] & [demo](#demo) covers the following concepts:
7 |
8 | - Benefits of GraphQL over REST.
9 | - GraphQL structure vs. JSON.
10 | - Adding `express-graphql` to `express` as a middleware.
11 | - Using GraphiQL's interface.
12 | - Querying a DB via GraphQL (i.e. queries).
13 | - Writing to a DB via GraphQL (i.e. mutations).
14 | - Loading & caching data with `dataloader`.
15 | - Resources for diving deeper with GraphQL.
16 |
17 | - - -
18 |
19 | ## Demo
20 |
21 | > 
22 |
23 | ### Running the Demo
24 |
25 | First, ensure you have the following dependencies installed:
26 |
27 | - MySQL (`brew install mysql`)
28 | - Node
29 |
30 | ```shell
31 | $ nvm use
32 | $ npm install
33 | $ npm start
34 | ```
35 |
36 | Open .
37 |
38 | ### Special Thanks
39 |
40 | - [Spectacle](https://github.com/FormidableLabs/spectacle)
41 | by
42 | [@kenwheeler](https://github.com/kenwheeler).
43 | - [spectacle-code-slide](https://github.com/thejameskyle/spectacle-code-slide)
44 | by
45 | [@thejameskyle](https://github.com/thejameskyle).
46 |
47 |
48 | [0]: http://www.meetup.com/NodejsHouston/
49 | [1]: presentation.pdf
50 |
--------------------------------------------------------------------------------
/graphql-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericclemmons/graphql-demo/899cf0c4c40b6d50d48d6a06b21beb93e5d8dcc7/graphql-demo.gif
--------------------------------------------------------------------------------
/knexfile.js:
--------------------------------------------------------------------------------
1 | require("babel-register")();
2 |
3 | module.exports = {
4 | development: {
5 | client: "mysql",
6 | connection: {
7 | database: "graphql_demo",
8 | user: "root",
9 | },
10 | pool: {
11 | min: 0,
12 | max: 1,
13 | },
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/migrations/20160411194216_blog.js:
--------------------------------------------------------------------------------
1 |
2 | export function up(knex, Promise) {
3 | return Promise
4 | .all([
5 | knex.schema.dropTableIfExists("comment"),
6 | knex.schema.dropTableIfExists("post"),
7 | knex.schema.dropTableIfExists("author"),
8 | ])
9 | .then(() => knex.schema.createTable("author", function createUser(table) {
10 | table.increments("id").notNullable().primary();
11 | table.string("name").notNullable();
12 | table.string("email").notNullable().unique();
13 | table.timestamps();
14 | }))
15 | .then(() => knex.schema.createTable("comment", function createComment(table) {
16 | table.increments("id").notNullable().primary();
17 | table.string("name").notNullable();
18 | table.string("email").notNullable();
19 | table.string("body").notNullable();
20 | table.timestamps();
21 | }))
22 | .then(() => knex.schema.createTable("post", function createPost(table) {
23 | table.increments("id").primary();
24 | table.integer("author_id").notNullable().unsigned().references("id").inTable("author");
25 | table.string("title").notNullable();
26 | table.string("slug").notNullable().unique();
27 | table.string("body").notNullable();
28 | table.timestamps();
29 | }))
30 | .catch((err) => {
31 | throw err;
32 | })
33 | ;
34 | }
35 |
36 | export function down(knex, Promise) {
37 | return knex.schema.dropTable("post")
38 | .then(() => knex.schema.dropTable("comment"))
39 | .then(() => knex.schema.dropTable("author"))
40 | ;
41 | }
42 |
--------------------------------------------------------------------------------
/mysql:
--------------------------------------------------------------------------------
1 | CREATE DATABASE graphql_demo -u root
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "graphql-demo",
4 | "version": "1.0.0",
5 | "description": "Presented by Eric Clemmons @ http://www.meetup.com/NodejsHouston/events/229815892/",
6 | "scripts": {
7 | "database": "npm run database:create && npm run database:migrate && npm run database:seed",
8 | "database:create": "echo 'CREATE DATABASE IF NOT EXISTS graphql_demo' | mysql -u root",
9 | "database:migrate": "knex migrate:latest",
10 | "database:rollback": "knex migrate:rollback",
11 | "database:seed": "knex seed:run",
12 | "postinstall": "npm run database",
13 | "start": "webpack --config webpack.config.server.babel.js",
14 | "test": "echo \"Error: no test specified\" && exit 1"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "dependencies": {
20 | "@terse/webpack": "0.0.5",
21 | "babel-eslint": "6.0.2",
22 | "babel-plugin-react-html-attrs": "2.0.0",
23 | "babel-register": "6.7.2",
24 | "bulma": "0.0.19",
25 | "classnames": "2.2.3",
26 | "css-loader": "0.23.1",
27 | "dataloader": "1.2.0",
28 | "eslint-config-airbnb": "6.2.0",
29 | "eslint-loader": "1.3.0",
30 | "eslint-plugin-react": "4.3.0",
31 | "express-graphql": "0.5.1",
32 | "file-loader": "0.8.5",
33 | "graphql": "0.5.0",
34 | "knex": "0.10.0",
35 | "lodash": "4.10.0",
36 | "marked": "0.3.5",
37 | "mysql": "2.10.2",
38 | "raw-loader": "0.5.1",
39 | "react": "15.0.1",
40 | "react-codemirror": "0.2.6",
41 | "react-dom": "15.0.1",
42 | "react-router": "2.1.1",
43 | "spectacle": "1.0.5",
44 | "spectacle-code-slide": "0.1.10",
45 | "style-loader": "0.13.1",
46 | "url-loader": "0.5.7"
47 | },
48 | "devDependencies": {
49 | "babel-preset-react-hmre": "1.1.1"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/presentation.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericclemmons/graphql-demo/899cf0c4c40b6d50d48d6a06b21beb93e5d8dcc7/presentation.pdf
--------------------------------------------------------------------------------
/seeds/blog.js:
--------------------------------------------------------------------------------
1 | export function seed(knex, Promise) {
2 | return knex("post").del()
3 | .then(() => knex("comment").del())
4 | .then(() => knex("author").del())
5 | .then(() => Promise.all([
6 | knex("author").insert({
7 | id: 1,
8 | name: "Eric Clemmons",
9 | email: "eric@smarterspam.com",
10 | created_at: new Date(),
11 | }),
12 |
13 | knex("post").insert({
14 | id: 1,
15 | author_id: 1,
16 | title: "Hello GraphQL",
17 | slug: "hello-graphql",
18 | body: `
19 | > Just a normal demo
20 | > - Eric
21 | `,
22 | created_at: new Date(),
23 | }),
24 |
25 | knex("comment").insert({
26 | name: "Matthew Mueller",
27 | email: "mattmuelle@gmail.com",
28 | body: `
29 | Yea, [graph.ql][1] is cleaner.
30 |
31 | [1]: https://github.com/matthewmueller/graph.ql
32 | `,
33 | created_at: new Date(),
34 | }),
35 | ]))
36 | ;
37 | }
38 |
--------------------------------------------------------------------------------
/src/browser.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "react-dom";
3 |
4 | import { browserHistory, IndexRoute, Route, Router } from "react-router";
5 |
6 | import Blog from "./components/Blog";
7 | import Post from "./components/Post";
8 | import PostCreator from "./components/PostCreator";
9 | import PostEditor from "./components/PostEditor";
10 | import Posts from "./components/Posts";
11 | import Users from "./components/Users";
12 |
13 | render((
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ), document.getElementById("app"));
25 |
--------------------------------------------------------------------------------
/src/components/Blog.js:
--------------------------------------------------------------------------------
1 | import "bulma/css/bulma.css";
2 |
3 | import React from "react";
4 |
5 | import Footer from "./Footer";
6 | import Header from "./Header";
7 |
8 | export default class Blog extends React.Component {
9 | render() {
10 | return (
11 |
12 |
16 |
17 |
18 |
19 | {this.props.children}
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Editor.css:
--------------------------------------------------------------------------------
1 | @import "~codemirror/lib/codemirror.css";
2 | @import "~codemirror/theme/material.css";
3 |
4 | .cm-header,
5 | .cm-strong {
6 | font-weight: normal;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Editor.js:
--------------------------------------------------------------------------------
1 | import "codemirror/mode/gfm/gfm";
2 |
3 | import Codemirror from "react-codemirror";
4 | import React from "react";
5 |
6 | import "./Editor.css";
7 |
8 | export default class Editor extends React.Component {
9 | getValue = () => {
10 | return this.refs.editor.getCodeMirror().getValue();
11 | }
12 |
13 | render() {
14 | return (
15 |
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class Header extends React.Component {
4 | render() {
5 | return (
6 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 |
4 | import { Link } from "react-router";
5 |
6 | export default class Header extends React.Component {
7 | render() {
8 | const { pathname } = this.props.location;
9 |
10 | return (
11 |
12 |
42 |
43 |
57 |
58 |
83 |
84 | );
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class Notification extends React.Component {
4 | render() {
5 | const { error, type = "danger" } = this.props;
6 |
7 | if (!error) {
8 | return null;
9 | }
10 |
11 | return (
12 |
13 |
14 |
15 | {error.message}
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Post.js:
--------------------------------------------------------------------------------
1 | import marked from "marked";
2 | import React from "react";
3 | import { browserHistory, Link } from "react-router";
4 |
5 | import Notification from "./Notification";
6 |
7 | export default class Post extends React.Component {
8 | constructor() {
9 | super();
10 |
11 | this.state = {
12 | error: null,
13 | post: null,
14 | };
15 | }
16 |
17 | componentWillMount() {
18 | const query = `
19 | query ($slug: String!) {
20 | post(slug: $slug) {
21 | title
22 | slug
23 | body
24 |
25 | author {
26 | name
27 | email
28 | }
29 |
30 | lastUpdated
31 | }
32 | }
33 | `;
34 |
35 | const { slug } = this.props.params;
36 | const variables = { slug };
37 |
38 | const options = {
39 | method: "POST",
40 | body: JSON.stringify({ query, variables }),
41 | headers: { "Content-Type": "application/json" },
42 | };
43 |
44 | fetch("/api", options)
45 | .then((response) => response.json())
46 | .then((response) => {
47 | if (response.errors) {
48 | throw new Error(response.errors[0].message);
49 | }
50 |
51 | this.setState({
52 | error: null,
53 | post: response.data.post,
54 | });
55 | })
56 | .catch((error) => this.setState({ error }))
57 | ;
58 | }
59 |
60 | handleDelete = (event) => {
61 | const query = `
62 | mutation ($slug: String!) {
63 | deletePost(slug: $slug)
64 | }
65 | `;
66 |
67 | const { slug } = this.state.post;
68 | const variables = { slug };
69 |
70 | const options = {
71 | method: "POST",
72 | body: JSON.stringify({ query, variables }),
73 | headers: { "Content-Type": "application/json" },
74 | };
75 |
76 | fetch("/api", options)
77 | .then((response) => response.json())
78 | .then((response) => {
79 | if (response.errors) {
80 | throw new Error(response.errors[0].message);
81 | }
82 |
83 | browserHistory.push("/");
84 | })
85 | .catch((error) => this.setState({ error }))
86 | ;
87 |
88 | event.preventDefault();
89 | }
90 |
91 | render() {
92 | const { error, post } = this.state;
93 |
94 | if (error) {
95 | return (
96 |
101 | );
102 | }
103 |
104 | if (!post) {
105 | return (
106 |
111 | );
112 | }
113 |
114 | return (
115 |
116 |
117 |
118 |
Edit
119 |
120 |
121 |
122 |
123 | {post.title}
124 |
125 |
126 | {post.lastUpdated}
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | Delete
136 |
137 |
138 |
139 | );
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/components/PostCreator.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { browserHistory } from "react-router";
3 |
4 | import Editor from "./Editor";
5 | import Notification from "./Notification";
6 |
7 | export default class PostCreator extends React.Component {
8 | constructor() {
9 | super();
10 |
11 | this.state = {
12 | error: null,
13 | };
14 | }
15 |
16 | componentDidMount() {
17 | this.refs.title.focus();
18 | }
19 |
20 | handleSave = (event) => {
21 | const title = this.refs.title.value;
22 | const body = this.refs.editor.getValue();
23 |
24 | const query = `
25 | mutation (
26 | $title: String!
27 | $body: String!
28 | ) {
29 | createPost(
30 | title: $title
31 | body: $body
32 | ) {
33 | slug
34 | }
35 | }
36 | `;
37 |
38 | const variables = { title, body };
39 |
40 | const options = {
41 | method: "POST",
42 | body: JSON.stringify({ query, variables }),
43 | headers: { "Content-Type": "application/json" },
44 | };
45 |
46 | fetch("/api", options)
47 | .then((response) => response.json())
48 | .then((response) => {
49 | if (response.errors) {
50 | throw new Error(response.errors[0].message);
51 | }
52 |
53 | const { slug } = response.data.createPost;
54 |
55 | browserHistory.push(`/posts/${slug}`);
56 | })
57 | .catch((error) => this.setState({ error }))
58 | ;
59 |
60 | event.preventDefault();
61 | }
62 |
63 | render() {
64 | const { error } = this.state;
65 |
66 | return (
67 |
68 |
69 |
70 |
New Post
71 |
72 |
73 |
74 |
75 |
99 |
100 |
101 | );
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/components/PostEditor.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { browserHistory } from "react-router";
3 |
4 | import Editor from "./Editor";
5 | import Notification from "./Notification";
6 |
7 | export default class PostEditor extends React.Component {
8 | constructor() {
9 | super();
10 |
11 | this.state = {
12 | error: null,
13 | post: null,
14 | };
15 | }
16 |
17 | componentWillMount() {
18 | const query = `
19 | query ($slug: String!) {
20 | post(slug: $slug) {
21 | title
22 | slug
23 | body
24 | }
25 | }
26 | `;
27 |
28 | const { slug } = this.props.params;
29 | const variables = { slug };
30 |
31 | const options = {
32 | method: "POST",
33 | body: JSON.stringify({ query, variables }),
34 | headers: { "Content-Type": "application/json" },
35 | };
36 |
37 | fetch("/api", options)
38 | .then((response) => response.json())
39 | .then((response) => {
40 | if (response.errors) {
41 | throw new Error(response.errors[0].message);
42 | }
43 |
44 | this.setState({
45 | error: null,
46 | post: response.data.post,
47 | });
48 | })
49 | .catch((error) => this.setState({ error }))
50 | ;
51 | }
52 |
53 | handleSave = (event) => {
54 | const title = this.refs.title.value;
55 | const body = this.refs.editor.getValue();
56 | const { slug } = this.props.params;
57 |
58 | const query = `
59 | mutation (
60 | $title: String!
61 | $slug: String!
62 | $body: String!
63 | ) {
64 | updatePost(
65 | title: $title
66 | slug: $slug
67 | body: $body
68 | ) {
69 | slug
70 | }
71 | }
72 | `;
73 |
74 | const variables = { title, slug, body };
75 |
76 | const options = {
77 | method: "POST",
78 | body: JSON.stringify({ query, variables }),
79 | headers: { "Content-Type": "application/json" },
80 | };
81 |
82 | fetch("/api", options)
83 | .then((response) => response.json())
84 | .then((response) => {
85 | if (response.errors) {
86 | throw new Error(response.errors[0].message);
87 | }
88 |
89 | const { slug } = response.data.updatePost;
90 |
91 | browserHistory.push(`/posts/${slug}`);
92 | })
93 | .catch((error) => this.setState({ error }))
94 | ;
95 |
96 | event.preventDefault();
97 | }
98 |
99 | render() {
100 | const { error, post } = this.state;
101 |
102 | if (!post) {
103 | return (
104 |
109 | );
110 | }
111 |
112 | return (
113 |
114 |
115 |
116 |
117 |
118 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | );
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/components/Posts.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router";
3 |
4 | import Notification from "./Notification";
5 |
6 | export default class Posts extends React.Component {
7 | constructor() {
8 | super();
9 |
10 | this.state = {
11 | error: null,
12 | posts: [],
13 | };
14 | }
15 |
16 | componentWillMount() {
17 | const query = `
18 | query {
19 | posts {
20 | title
21 | slug
22 | lastUpdated
23 | }
24 | }
25 | `;
26 |
27 | const options = {
28 | method: "POST",
29 | body: JSON.stringify({ query }),
30 | headers: { "Content-Type": "application/json" },
31 | };
32 |
33 | fetch("/api", options)
34 | .then((response) => response.json())
35 | .then((response) => this.setState({
36 | error: null,
37 | posts: response.data.posts,
38 | }))
39 | .catch((error) => this.setState({ error }))
40 | ;
41 | }
42 |
43 | render() {
44 | const { error, posts } = this.state;
45 |
46 | if (!posts.length) {
47 | return (
48 |
53 | );
54 | }
55 |
56 | return (
57 |
58 |
59 |
60 |
61 | {posts.map((post) => (
62 |
63 |
64 |
65 |
66 | {post.title}
67 |
68 |
69 |
70 |
71 | {post.lastUpdated}
72 |
73 |
74 |
75 | ))}
76 |
77 |
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/Presentation/author.error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericclemmons/graphql-demo/899cf0c4c40b6d50d48d6a06b21beb93e5d8dcc7/src/components/Presentation/author.error.png
--------------------------------------------------------------------------------
/src/components/Presentation/author.fragment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericclemmons/graphql-demo/899cf0c4c40b6d50d48d6a06b21beb93e5d8dcc7/src/components/Presentation/author.fragment.png
--------------------------------------------------------------------------------
/src/components/Presentation/author.graphql:
--------------------------------------------------------------------------------
1 | {
2 | author(id: 1) {
3 | id
4 | name
5 | email
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Presentation/author.inline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericclemmons/graphql-demo/899cf0c4c40b6d50d48d6a06b21beb93e5d8dcc7/src/components/Presentation/author.inline.png
--------------------------------------------------------------------------------
/src/components/Presentation/author.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "author": {
4 | "id": "1",
5 | "name": "Eric Clemmons",
6 | "email": "eric@smarterspam.com"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Presentation/author.variables.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericclemmons/graphql-demo/899cf0c4c40b6d50d48d6a06b21beb93e5d8dcc7/src/components/Presentation/author.variables.png
--------------------------------------------------------------------------------
/src/components/Presentation/example.graphql:
--------------------------------------------------------------------------------
1 | {
2 | user(id: 123) {
3 | id,
4 | name,
5 | emal,
6 | avatar(size: 50) {
7 | uri,
8 | width,
9 | height
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Presentation/example.js:
--------------------------------------------------------------------------------
1 | {
2 | user: {
3 | id: 123,
4 | name: "Eric Clemmons",
5 | email: "eric@smarterspam.com",
6 | avatar: {
7 | uri: "http://.../pic.jpg",
8 | width: 50,
9 | height: 50
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Presentation/graphiql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericclemmons/graphql-demo/899cf0c4c40b6d50d48d6a06b21beb93e5d8dcc7/src/components/Presentation/graphiql.png
--------------------------------------------------------------------------------
/src/components/Presentation/index.js:
--------------------------------------------------------------------------------
1 | import "./presentation.css";
2 |
3 | import React from "react";
4 |
5 | import {
6 | Appear,
7 | BlockQuote,
8 | Deck,
9 | Heading,
10 | List,
11 | ListItem,
12 | Spectacle,
13 | Slide,
14 | Text,
15 | } from "spectacle";
16 |
17 | import CodeSlide from "spectacle-code-slide";
18 | import createTheme from "spectacle/lib/themes/default";
19 |
20 | const theme = createTheme({
21 | primary: "linear-gradient(141deg, rgb(34, 34, 51) 0%, rgb(51, 51, 51) 71%, rgb(85, 68, 68) 100%)",
22 | secondary: "#ffd",
23 | tertiary: "#fff",
24 | quartenary: "#afa",
25 | }, {
26 | primary: "Futura",
27 | secondary: "Arial",
28 | tertiary: "Arial",
29 | quartenary: "Arial",
30 | });
31 |
32 | export default class Presentation extends React.Component {
33 | render() {
34 | return (
35 |
36 |
37 |
38 | GraphQL
39 | Eric Clemmons
40 |
41 |
42 |
43 |
44 | What is GraphQL?
45 |
46 |
47 |
48 |
56 |
57 |
65 |
66 |
67 |
68 | Declarative
69 |
70 |
71 | Query responses are decided by the client rather than the server. A GraphQL query returns exactly what a client asks for and no more.
72 |
73 |
74 |
75 |
76 |
77 | Compositional
78 |
79 |
80 | A GraphQL query itself is a hierarchical set of fields. The query is shaped just like the data it returns. It is a natural way for product engineers to describe data requirements.
81 |
82 |
83 |
84 |
85 |
86 | Strong-typed
87 |
88 |
89 | A GraphQL query can be ensured to be valid within a GraphQL type system at development time allowing the server to make guarantees about the response. This makes it easier to build high-quality client tools.
90 |
91 |
92 |
93 |
94 |
95 | Why GraphQL?
96 |
97 |
98 |
99 |
100 |
101 | REST is great for CRUD, not graphs.
102 |
103 | Client defines only the data it needs.
104 | Client makes a single request.
105 |
106 | Server is responsible for:
107 |
108 |
109 | Schema
110 | Querying
111 | Caching
112 |
113 |
114 | How you fetch the data is up to you .
115 |
116 |
117 |
118 |
119 | Graphi QL
120 | Interactive API & Documentation
121 |
122 |
123 |
124 |
125 | Where can you use GraphQL?
126 |
127 |
128 |
129 | JavaScript
130 | Ruby
131 | PHP
132 | Python
133 | Java
134 | Go
135 | …
136 |
137 |
138 |
139 |
140 |
141 | When can you use GraphQL?
142 |
143 |
144 |
145 |
146 |
147 | Today.
148 |
149 |
150 |
151 |
152 |
153 | How to use GraphQL
154 |
155 |
156 |
157 |
165 |
166 |
176 |
177 |
185 |
186 |
196 |
197 |
206 |
207 |
208 |
209 | Querying an Author
210 |
211 |
212 |
213 |
214 |
215 | Errors
216 |
217 |
218 |
219 |
220 |
221 | Inline
222 |
223 |
224 |
225 |
226 |
227 | Variables
228 |
229 |
230 |
231 |
232 |
233 |
234 | Fragments
235 |
236 |
237 |
238 |
239 |
240 | DataLoader
241 |
242 |
243 | Simple wrapper for fetching & caching queries.
244 |
245 |
246 |
247 |
255 |
256 |
265 |
266 |
274 |
275 |
276 |
277 | Demo
278 |
279 |
280 |
281 |
282 |
283 | Recommended Resources
284 |
285 |
286 |
287 | learngraphql.com
288 | github.com/mugli/learning-graphql
289 | graphql.org
290 | github.com/chentsulin/awesome-graphql
291 | github.com/facebook/dataloader
292 | github.com/matthewmueller/graph.ql
293 | github.com/graphql/graphql-js
294 |
295 |
296 |
297 |
298 |
299 | Thanks!
300 |
301 |
302 | https://github.com/ericclemmons/graphql-demo
303 |
304 |
305 |
306 |
307 | );
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/src/components/Presentation/presentation.css:
--------------------------------------------------------------------------------
1 | .spectacle-content pre {
2 | font-family: "Operator Mono", Monaco, monospace;
3 | font-size: 1.5rem;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Users.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class Users extends React.Component {
4 | render() {
5 | return (
6 |
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/middleware/api.js:
--------------------------------------------------------------------------------
1 | import DataLoader from "dataloader";
2 | import graphql from "express-graphql";
3 | import express from "express";
4 | import knex from "knex";
5 |
6 | import schema from "../schema";
7 |
8 | const db = knex({
9 | client: "mysql",
10 | connection: {
11 | database: "graphql_demo",
12 | user: "root",
13 | },
14 | });
15 |
16 | const loaders = {
17 | post: new DataLoader(function fetchBySlugs(slugs) {
18 | console.info("[post] Fetching by ", slugs);
19 |
20 | return db("post").whereIn("slug", slugs);
21 | }),
22 | };
23 |
24 | export default express.Router()
25 | .all("/api", graphql({
26 | context: { db, loaders },
27 | graphiql: true,
28 | pretty: true,
29 | schema,
30 | }))
31 | ;
32 |
--------------------------------------------------------------------------------
/src/middleware/files.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 |
3 | export default express.Router().use(express.static("dist/browser"));
4 |
--------------------------------------------------------------------------------
/src/middleware/view.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 |
3 | export default express.Router()
4 | .get("*", (req, res) => {
5 | const entry = (req.path === "/presentation") ? "presentation" : "browser";
6 |
7 | res.send(`
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | `);
26 | })
27 | ;
28 |
--------------------------------------------------------------------------------
/src/presentation.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "react-dom";
3 |
4 | import Presentation from "./components/Presentation";
5 |
6 | render( , document.getElementById("app"));
7 |
--------------------------------------------------------------------------------
/src/schema/AuthorType.js:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLID,
3 | GraphQLNonNull,
4 | GraphQLObjectType,
5 | GraphQLString,
6 | } from "graphql";
7 |
8 | export default new GraphQLObjectType({
9 | name: "Author",
10 | description: "Blog author",
11 |
12 | fields() {
13 | return {
14 | id: { type: new GraphQLNonNull(GraphQLID) },
15 | name: { type: new GraphQLNonNull(GraphQLString) },
16 | email: { type: new GraphQLNonNull(GraphQLString) },
17 | createdAt: { type: new GraphQLNonNull(GraphQLString) },
18 | updatedAt: { type: GraphQLString },
19 | };
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/src/schema/MutationType.js:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLBoolean,
3 | GraphQLNonNull,
4 | GraphQLObjectType,
5 | GraphQLString,
6 | } from "graphql";
7 |
8 | import { kebabCase } from "lodash";
9 |
10 | import PostType from "./PostType";
11 |
12 | export default new GraphQLObjectType({
13 | name: "Mutation",
14 | description: "Create a post, comment, or user",
15 |
16 | fields() {
17 | return {
18 | createPost: {
19 | type: PostType,
20 |
21 | args: {
22 | title: { type: new GraphQLNonNull(GraphQLString) },
23 | body: { type: new GraphQLNonNull(GraphQLString) },
24 | },
25 |
26 | resolve(parent, args, context) {
27 | const { db } = context;
28 | const { post } = context.loaders;
29 |
30 | const { title, body } = args;
31 | const slug = kebabCase(title);
32 |
33 | return db("post")
34 | .insert({
35 | author_id: 1,
36 | title,
37 | slug,
38 | body,
39 | created_at: new Date(),
40 | })
41 | .then(() => post.load(slug))
42 | ;
43 | },
44 | },
45 |
46 | deletePost: {
47 | type: GraphQLBoolean,
48 |
49 | args: {
50 | slug: { type: new GraphQLNonNull(GraphQLString) },
51 | },
52 |
53 | resolve(parent, args, context) {
54 | const { db } = context;
55 | const { post } = context.loaders;
56 | const { slug } = args;
57 |
58 | return db("post")
59 | .where("slug", slug)
60 | .del()
61 | .then(() => post.clear(slug))
62 | ;
63 | },
64 | },
65 |
66 | updatePost: {
67 | type: PostType,
68 |
69 | args: {
70 | title: { type: new GraphQLNonNull(GraphQLString) },
71 | slug: { type: new GraphQLNonNull(GraphQLString) },
72 | body: { type: new GraphQLNonNull(GraphQLString) },
73 | },
74 |
75 | resolve(parent, args, context) {
76 | const { db } = context;
77 | const { post } = context.loaders;
78 | const { title, slug, body } = args;
79 |
80 | const newSlug = kebabCase(title);
81 |
82 | return db("post")
83 | .where("slug", slug)
84 | .update({
85 | title,
86 | slug: newSlug,
87 | body,
88 | updated_at: new Date(),
89 | })
90 | .then(() => post.clear(slug))
91 | .then(() => post.load(newSlug))
92 | ;
93 | },
94 | },
95 | };
96 | },
97 | });
98 |
--------------------------------------------------------------------------------
/src/schema/PostType.js:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLID,
3 | GraphQLNonNull,
4 | GraphQLObjectType,
5 | GraphQLString,
6 | } from "graphql";
7 |
8 | import AuthorType from "./AuthorType";
9 |
10 | export default new GraphQLObjectType({
11 | name: "Post",
12 | description: "Blog post",
13 |
14 | fields() {
15 | return {
16 | id: { type: new GraphQLNonNull(GraphQLID) },
17 |
18 | author: {
19 | type: new GraphQLNonNull(AuthorType),
20 | resolve(parent, args, context) {
21 | const { db } = context;
22 |
23 | return db("author").first().where("id", parent.author_id);
24 | },
25 | },
26 |
27 | title: { type: new GraphQLNonNull(GraphQLString) },
28 | slug: { type: new GraphQLNonNull(GraphQLString) },
29 | body: { type: new GraphQLNonNull(GraphQLString) },
30 |
31 | createdAt: {
32 | type: new GraphQLNonNull(GraphQLString),
33 | resolve(parent) {
34 | return parent.created_at;
35 | },
36 | },
37 |
38 | updatedAt: {
39 | type: GraphQLString,
40 | resolve(parent) {
41 | return parent.updated_at;
42 | },
43 | },
44 |
45 | lastUpdated: {
46 | type: new GraphQLNonNull(GraphQLString),
47 | resolve(parent) {
48 | return parent.updated_at || parent.created_at;
49 | },
50 | },
51 | };
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/src/schema/QueryType.js:
--------------------------------------------------------------------------------
1 | import {
2 | // GraphQLError,
3 | GraphQLID,
4 | GraphQLList,
5 | GraphQLNonNull,
6 | GraphQLObjectType,
7 | GraphQLString,
8 | } from "graphql";
9 |
10 | import AuthorType from "./AuthorType";
11 | import PostType from "./PostType";
12 |
13 | export default new GraphQLObjectType({
14 | name: "Query",
15 | description: "Blog posts, comments, & users",
16 |
17 | fields() {
18 | return {
19 | author: {
20 | type: AuthorType,
21 |
22 | args: {
23 | id: { type: new GraphQLNonNull(GraphQLID) },
24 | },
25 |
26 | resolve(parent, args, context) {
27 | const { db } = context;
28 | const { id } = args;
29 |
30 | return db("author")
31 | .first()
32 | .where("id", id)
33 | ;
34 | },
35 | },
36 |
37 | authors: {
38 | type: new GraphQLList(AuthorType),
39 |
40 | resolve(parent, args, context) {
41 | const { db } = context;
42 |
43 | return db("author");
44 | },
45 | },
46 |
47 | post: {
48 | type: PostType,
49 |
50 | args: {
51 | slug: { type: new GraphQLNonNull(GraphQLString) },
52 | },
53 |
54 | resolve(parent, args, context) {
55 | const { post } = context.loaders;
56 | const { slug } = args;
57 |
58 | return post.load(slug);
59 | },
60 | },
61 |
62 | posts: {
63 | type: new GraphQLList(PostType),
64 |
65 | resolve(parent, args, context) {
66 | const { db } = context;
67 | const { post } = context.loaders;
68 |
69 | return db("post")
70 | .select("slug")
71 | .map(({ slug }) => slug)
72 | .then((slugs) => post.loadMany(slugs))
73 | ;
74 | },
75 | },
76 | };
77 | },
78 | });
79 |
--------------------------------------------------------------------------------
/src/schema/index.js:
--------------------------------------------------------------------------------
1 | import { GraphQLSchema } from "graphql";
2 |
3 | import MutationType from "./MutationType";
4 | import QueryType from "./QueryType";
5 |
6 | export default new GraphQLSchema({
7 | mutation: MutationType,
8 | query: QueryType,
9 | });
10 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | import { middleware as webpack } from "@terse/webpack";
2 | import express from "express";
3 |
4 | import config from "../webpack.config.browser.babel.js";
5 |
6 | import api from "./middleware/api";
7 | import files from "./middleware/files";
8 | import view from "./middleware/view";
9 |
10 | express()
11 | .use(webpack(module, config))
12 | .use(files)
13 | .use(api)
14 | .use(view)
15 | .listen(3000, "localhost", (err) => {
16 | if (err) {
17 | throw err;
18 | }
19 |
20 | console.info("🌍 Listening at http://localhost:3000/"); // eslint-disable-line
21 | })
22 | ;
23 |
--------------------------------------------------------------------------------
/webpack.config.browser.babel.js:
--------------------------------------------------------------------------------
1 | import { BrowserConfig } from "@terse/webpack";
2 |
3 | export default new BrowserConfig()
4 | .src({
5 | browser: [ "./src/browser.js" ],
6 | presentation: [ "./src/presentation.js" ],
7 | })
8 | .dest("dist/browser")
9 | .create()
10 | ;
11 |
--------------------------------------------------------------------------------
/webpack.config.server.babel.js:
--------------------------------------------------------------------------------
1 | import { ServerConfig } from "@terse/webpack";
2 |
3 | export default new ServerConfig()
4 | .src("src/server.js")
5 | .dest("dist/server")
6 | .create()
7 | ;
8 |
--------------------------------------------------------------------------------