{
channels {
id
url
title
moments {
id
by
kind
content
}
}
}
query Moi {
me {
id
email
handle
avatar
moments {
id
kind
content
channel
}
}
}
{
"data": {
"me": {
"id": "ohad79sdhnsibufi",
"email": "myemail@yoooo.com",
"handle": "surfer_bob",
"avatar": "http//:... || TFuI=",
"moments": [
{
"id": "qwertyyyy",
"kind": "text",
"content": "Wadup homies?!",
"channel": "reactnyc"
},
...
]
}
}
}
response:
{
"query": "query Moi {\n me { ... } \n}",
"variables": {},
"operationName": "Moi"
}
mutation PublishChannel ($url: String!, $title: String) {
publishChannel (url: $url, title: $title) {
id
url
title
}
}
{
"data": {
"publishChannel": {
"id": "aiu867aSGiunkaIBYSD88K",
"url": "katzzz",
"title": "All About KATZ"
}
}
}
response:
mutation getMe {
getMe {
id
handle
...
}
getMyChannels {
id
url
...
}
}
query Moi {
me {
id
handle
...
}
channels {
id
url
...
}
}
query WhatIfs ($what: Boolean, $who: Boolean) {
channels {
id
...
moments @skip(if: $what) {
id
...
authors @include(if: $who) {
id
....
}
}
}
}
query Moments {
moments {
id
kind
content
by {
id
... on Author {
email
}
}
channel {
...channelByMoment
}
}
}
fragment channelByMoment on Channel {
id
url
title
}
query {
channels {
id
...
moments {
id
...
authors {
id
....
moments {
id
...on and on and on
}
}
}
}
}
subscription ChannelActivity (
$channel: ID!,
$options: LiveChannelOptionInput
) {
channelActivity (channel: $channel, options: $options) {
event
payload
}
}
Lokka - Github
Adrenaline - Github
DIY:
function query(query, variables, operationName) {
return axios.post('/graphql', {
query, variables, operationName,
});
}
how I first learned GraphQL
open-source
rich & silky smooth documentation
Conceived by MDG - the dude(ette)s behind the Meteor framework
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
const App = ({ error, loading, me }) => ...;
export default createContainer((props) => {
let error = null;
const Moi = Meteor.subscribe('Moi', {
onReady: () => error && error = null,
onStop: (e) => e && error = e,
});
return {
error,
loading: Moi.ready(),
me: Meteor.user(),
};
}, App);
imports/ui/app.js
import React from 'react';
import { graphql } from 'react-apollo';
import query from '../queries/moi';
const App = ({ data: { errors, loading, me } }) => ...;
export default graphql(query)(App);
src/components/app.js
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { ApolloProvider } from 'react-apollo';
import configClient from './apollo';
import configStore from './store';
import Application from './components/app';
const client = configClient({
uri: `${protocol}//${host}/graphql`,
});
const store = configStore(window.__$__, {
apollo: client.reducer(),
}, [client.middleware()]);
const appElement = document.getElementById('app');
render(<ApolloProvider client={client} store={store}>
<BrowserRouter>
<Application />
</BrowserRouter>
</ApolloProvider>, appElement);
src/index.js
import React from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const mutation = gql`
mutation Login ($handle: String!, $password: String!) {
login (handle: $handle, password: $password) {
id
}
}
`;
class LoginForm extends React.Component {
constructor(props) { ... }
onSubmit(event) {
event.preventDefault();
this.props.mutate(this.state.handle, this.state.password)
.then((response) => console.log(response.data.login.id));
}
render() {
return <form onSubmit={this.onSubmit.bind(this)}>
<input id="handle" type="text" ... />
<input id="password" type="password" ... />
<button type="submit">Log In</button>
</form>;
}
}
export default graphql(mutation)(LoginForm);
src/components/LoginForm.jsx
import React from 'react';
import { graphql } from 'react-apollo';
import mutation from '../../schema/mutations/postMessage.gql';
class PostComposer extends React.Component {
constructor(props) { ... }
onSubmit(event) {
...
this.props.mutate({
variables: { channel, content, kind },
optimisticResponse: {
__type: 'Mutation',
postMessage: {
id: -1,
channel,
content,
kind,
__typename: 'Post',
},
},
})
}
render() {
return <form onSubmit={this.onSubmit.bind(this)}>...</form>;
}
}
export default graphql(mutation)(PostComposer);
src/components/PostComposer.jsx
import React from 'react';
import { graphql } from 'react-apollo';
import query from '../../schema/queries/channels.gql';
const ChannelList = ({ data: { errors, loading, channels } }) => ...;
export default graphql(query)(ChannelList);
src/components/ChannelList.js
query Channels ($limit: Int) {
channels (limit: $limit) {
id
url
title
}
}
schema/queries/channels.gql
import React from 'react';
import { graphql } from 'react-apollo';
import query from '../../schema/queries/moments.gql';
import subscription from '../../schema/subscriptions/momentsByChannel.gql';
class ChannelView extends React.PureComponent {
...
render() { ... }
}
export default graphql(query)(ChannelView);
src/components/ChannelView.js
schema/subscriptions/momentsByChannel.gql
subscription MomentsByChannel ($channel: ID!) {
momentsByChannel (channel: $channel) {
id
by
kind
content
}
}
class ChannelView extends React.PureComponent {
componentWillReceiveProps() {
const { loading, error, channel } = Props.data;
if (loading || error) return;
if (channel.id !== this.props.data.channel.id) {
if (this.subscription) this.unsubscribe();
this.subscription = this.subscribe();
}
}
componentWillUnmount() {
this.unsubscribe();
}
subscribe() { ... }
unsubscribe() {
this.subscription && this.subscription.unsubscribe();
this.subscription = null;
}
render() { ... }
}
src/components/ChannelView.js
class ChannelView extends React.PureComponent {
componentWillReceiveProps() { ... }
componentWillUnmount() { ... }
subscribe() {
const { data: { subscribeToMore, variables } } = this.props;
return subscribeToMore({
document: subscription,
onError: error => ...,
updateQuery: (prev, { subscriptionData }) => {
const { momentsByChannel } = subscriptionData.data;
return { ...prev, moments: [...prev.moments, momentsByChannel] }
},
});
}
unsubscribe() { ... }
render() { ... }
}
src/components/ChannelView.js
type Author {
id: ID!
handle: String
avatar: URL
channels: [Channel]
moments: [Moment]
email: String
}
type Channel { ... }
type Query {
tasks: [String]
me: Author
author(
id: ID
): Author
...
}
type Mutation {
....
publishChannel(
url: String!
title: String
): Channel
...
}
type Subscription { ... }
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
type Query {
me: Author
...
}
type Mutation {
login(
user: String!
pass: String!
): Author
...
}
type Subscription {
moments(
channels: [ID]
): Moment
...
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
Scalar
const mongoose = require('mongoose');
const { makeExecutableSchema } = require('graphql-tools');
const { withFilter, PubSub } = require('graphql-subscriptions');
const Users = mongoose.model('User');
const Channels = mongoose.model('Channel');
const Posts = mongoose.model('Post');
const pubsub = new PubSub();
const schemaIndex = require('../schema/index.graphql');
const typeDefs = [schemaIndex];
const resolvers = { ... };
module.exports = makeExecutableSchema({
typeDefs,
resolvers,
logger: { log: e => console.log(e) },
allowUndefinedInResolve: true,
});
server/schema.js
const resolvers = {
Query: {
me: root => root.user,
author: (root, args) => Users.findOne(args.id),
channels: async (root, { limit }) => Channels.search(limit),
...,
},
Mutation: {
login(root, args, ctx) {
if (root.user) return root.user;
return Users.loginUser(args.handle, args.password, ctx);
},
publishChannel: (root, { url, title }) =>
root.user && Channels.publish(url, title, root.user),
async postMessage(root, { channel, content, kind }) {
if (root.user) {
const memory = await Posts.create(root.user.id, channel, content, kind);
pubsub.publish('memory', { memory });
return true;
}
},
},
Subscription: {
moments: {
subscribe: withFilter(() => pubsub.asyncIterator('memory'),
({ messenger: { payload } }, variables) => payload.channel === variables.channel),
},
},
...,
};
server/schema.js
const { execute, subscribe } = require('graphql');
const { graphqlKoa, graphiqlKoa } = require('graphql-server-koa');
const { SubscriptionServer } = require('subscriptions-transport-ws');
const { createLocalInterface } = require('apollo-local-query');
const host = ...;
const path = ...;
const schema = require('./schema.js');
const getRootValue = async ctx => ...;
module.exports = { ... };
server/graphql.js
module.exports = {
localInterface: ...,
graphql: graphqlKoa(async ctx => ({
schema,
rootValue: await getRootValue(ctx),
context: ctx,
})),
graphiql: graphiqlKoa({
endpointURL: path,
subscriptionsEndpoint: `ws://${host}${path}`,
}),
createSubscriptionServer(options = {}) {
const { server, keepAlive = 1000 } = options;
return SubscriptionServer.create({
schema,
execute,
subscribe,
keepAlive,
}, { server, path, });
},
};
server/graphql.js
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws';
export default function createClient({ uri }) {
const subscriptionInterface = new SubscriptionClient(uri, {
lazy: true,
reconnect: true,
connectionCallback: error => console.log(error),
});
const networkInterface = createNetworkInterface({
uri,
opts: {
credentials: 'same-origin',
},
});
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) req.options.headers = {};
const token = localStorage.getItem('session.token');
req.options.headers.Authorization = token ? `JWT ${token}` : null;
next();
},
}]);
return new ApolloClient({
dataIdFromObject: o => o.id,
networkInterface: addGraphQLSubscriptions(networkInterface, subscriptionInterface),
connectToDevTools: process.env.NODE_ENV !== 'production',
});
}
src/apollo-client.js
import React from 'react';
import { graphql } from 'react-apollo';
import mutation from '../mutations/login.gql';
const ActionButton = ({ children, action }) =>
(<button
type="button"
onClick={action}>{
children
}</button>);
export default graphql(mutation, {
props({ ownProps: {
children,
onSuccess,
onFailure,
variables,
options = {},
}, mutate }) {
return {
children,
action(event) {
return mutate({ variables, ...options })
.then(onSuccess)
.catch(onFailure);
},
};
},
})(ActionButton);
src/components/LoginButton.jsx
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { ApolloClient, ApolloProvider, getDataFromTree } from 'react-apollo';
import configStore from '../store';
import Application from '../components/app';
export default async function render(ctx, {
...,
networkInterface,
}) {
const client = new ApolloClient({
..., // dataIdFromObject: o => o.id, etc.
networkInterface,
ssrMode: true,
});
const store = configStore(undefined, {
apollo: client.reducer()
}, [client.middleware()]);
const app = (<StaticRouter location={ctx.path} context={ctx.state}>
<ApolloProvider client={client} store={store}>
<Application isServer />
</ApolloProvider>
</StaticRouter>);
...continued
}
src/ssr/render.js
import Html from './Html';
export default async function render(ctx, {
css = [],
scripts = [],
...,
}) {
...from last slide
await getDataFromTree(app)
const html = ReactDOMServer.renderToString(app);
if ([301, 302, 404].includes(ctx.state.status)) {
if (ctx.state.status === 404) ctx.status = ctx.state.status;
else {
ctx.status = ctx.state.status;
return ctx.redirect(ctx.state.url);
}
}
ctx.type = 'text/html';
ctx.body = `<!DOCTYPE html>\n${ReactDOMServer.renderToStaticMarkup(
<Html
html={html}
css={css}
scripts={scripts}
window={{
__$__: store.getState(),
}} />)}`;
}
src/ssr/render.js
const { execute } = require('graphql');
const { createLocalInterface } = require('apollo-local-query');
const schema = require('...');
async function getRootValue(ctx) {...}
app.context.localInterface = async ctx => createLocalInterface(
{ execute }, schema, { rootValue: await getRootValue(ctx), context: ctx });
app.get('/*', async (ctx, next) => {
const networkInterface = await ctx.localInterface(ctx);
const { css, scripts, manifest } = ctx.assets;
await ctx.render(ctx, {
css,
scripts,
manifest,
networkInterface,
});
});
server/app.js
.eslintrc.js
module.exports = {
rules: {
...,
'graphql/template-strings': ['error', {
env: 'apollo',
schemaJson: require('./schema/schema.json'),
}],
},
};
npm install eslint-plugin-graphql --save-dev
webpack.config.js
module.exports = {
...,
module: {
rules: [..., {
test: /\.(graphql|gql)$/,
loader: 'graphql-tag/loader',
}],
},
};
npm install graphql-tag --save-dev
tests/graphql.test.js
const { graphql } = require('graphql');
describe('GraphQL', () => {
const schema = require('../schema/schema.js');
it('should just query', done => {
const query = `
query Moi {
me {
id
}
}
`;
const rootValue = {};
const context = {};
const variables = {};
const operationName = 'Moi';
graphql(schema, query, rootValue, context, variables, operationName)
.then(result => {
const me = result.data.me;
return done();
})
.catch(done);
});
});
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation
onFragment
onField
}
}
}
fragment FullType on __Type { ... }
fragment InputValue on __InputValue { ... }
fragment TypeRef on __Type { ... }
const http = require('http');
const renderApp = require('...');
const graphql = require('...');
const unix_socket = ...;
const port = ...;
http.createServer((request, response) => {
switch (request.method) {
default: {
response.statusCode = 400;
return response.end('Try GET for UI or POST for API.');
}
case 'GET': {
return renderApp(request, response);
}
case 'POST': {
return graphql(request, response);
}
}
}).listen(unix_socket || port);