Tamara Temple
tamara@tamouse.org
graphql-ruby
was chosen Ruby serveror...
My experiences learning 3 deep technologies when I've been doing something else for about 10 years.
ask for just what you need
query {
viewer {
posts {
title
author {
name
}
excerpt
published_date
comment_count
}
}
}
query {
viewer {
posts {
title
author {
name
}
excerpt
published_date
comment_count
}
}
}
The viewer is the root of the query tree into your graph.
Using the current viewer (i.e. user) as the root of the query let's you determine who's looking at the data.
This is used in many cases to perform some form of authorization.
query {
viewer {
posts {
title
author {
name
}
excerpt
published_date
comment_count
}
}
}
This tells graphql to return the posts the viewer can see.
query {
viewer {
posts {
title
author {
name
}
excerpt
published_date
comment_count
}
}
}
This gets the author for each post, which is a relation.
query {
viewer {
posts {
title
author {
name
}
excerpt
published_date
comment_count
}
}
}
this indicates we only want the author's name.
rather that trying to apply learning in the legacy app, I wrote some "toy" learning apps to explore things
Looking at my Github account , you can probably find several of them in various stages of disrepair.
Most recent one involves authentication using JWTs, and signup, login, and logout via GraphQL mutations. (PoC for a boss)
Before I got to the project, they had some help from outside, calling on folks from Github who've been implementing a lot with GraphQL.
When I discovered this I started to understand a lot more.
The server side requires several things to be in place before you can really get going serving up data
... i.e., situation normal for any Rails app
graphql:install
-- controller, schema,
root query and root mutation
graphql:object
--
create object types for how data looks
Graphql: graphql:enum graphql:function graphql:install graphql:interface graphql:loader graphql:mutation graphql:object graphql:union
def execute
authenticate! # sets current_user
result = GraphqlSchema.execute(
params[:query],
variables: params[:variables],
context: {current_user: current_user},
operation_name: params[:operation_name],
)
render json: result
end
graphql_controller
on github repo
the mouth of the graphql system
GraphqlSchema = GraphQL::Schema.define do
mutation(Types::MutationType)
query(Types::QueryType)
end
the stomach of the graphql system
Types::QueryType = GraphQL::ObjectType.define do
name "Query"
field :viewer, Types::ViewerType, "Viewer of data, may be an anonymous user or registered user" do
resolve ->(_object, _args, context) do
if context[:current_user].present?
context[:current_user]
else
NullUser.new
end
end
end
end
graphql-ruby
provides a DSLI hear you ...
"What's with that NullUser??"
more foreshadowing
authorization
Types::ViewerType = GraphQL::ObjectType.define do
name "Viewer"
field :id, !types.ID, hash_key: :uuid
field :name, !types.String
field :my_posts, types[Types::PostType] do
resolve ->(obj, _, _) { obj.posts }
end
field :public_posts, types[Types::PostType] do
resolve ->(_,_,_){ Post.published }
end
field :all_authors, types[Types::UserType] do
resolve ->(_,_,_){ User.all }
end
end
On a big system with lots of data
we're not going to want to return
literally everything like that.
Pagination is provided for in the library.
But not in my example
... yet
Other types are probably less interesting.
the liver of the graphql system
Types::MutationType = GraphQL::ObjectType.define do
name "Mutation"
field :createUser, Types::AuthenticateType do
description "Creates a new user"
argument :credentials, Types::AuthProviderCredentialsType
resolve Mutations::CreateUser.new
end
field :loginUser, Types::AuthenticateType do
description "Logs in a user"
argument :credentials, Types::AuthProviderCredentialsType
resolve Mutations::LoginUser.new
end
end
My project currently has two mutations
The C.U.D. parts can be handled via an "upsert" sort of mutation.
I am concerned about how this might explode for very complicated systems (lots of models, lots of interrelationships).
field :createUser, Types::AuthenticateType do
description "Creates a new user"
argument :credentials, Types::AuthProviderCredentialsType
resolve Mutations::CreateUser.new
end
field :createUser, Types::AuthenticateType do
Types::AuthenticateType = GraphQL::ObjectType.define do
name "Authenticate"
field :token, types.String
field :user, Types::UserType
en
:credentials
argument :credentials, Types::AuthProviderCredentialsType
Types::AuthProviderCredentialsType = GraphQL::InputObjectType
.define do
name "AuthProviderCredentials"
argument :name, types.String
argument :email, !types.String
argument :password, !types.String
end
resolve Mutations::CreateUser.new
class Mutations::CreateUser
def call(_obj, args, _ctx)
creds = args[:credentials]
user = User.create!(name: creds[:name],email: creds[:email],
password: creds[:password],
password_confirmation: creds[:password])
OpenStruct.new({
token: AuthToken.new.token(user),
user: user
})
rescue ActiveRecord::RecordInvalid => e
raise GraphQL::ExecutionError.new("Invalid input")
end
end
class Mutations::CreateUser
def call(_obj, args, _ctx)
creds = args[:credentials]
user = User.create!(name: creds[:name],email: creds[:email],
password: creds[:password],
password_confirmation: creds[:password])
OpenStruct.new({
token: AuthToken.new.token(user),
user: user
})
rescue ActiveRecord::RecordInvalid => e
raise GraphQL::ExecutionError.new("Invalid input")
end
end
class Mutations::CreateUser
def call(_obj, args, _ctx)
creds = args[:credentials]
user = User.create!(
name: creds[:name],email: creds[:email],
password: creds[:password],
password_confirmation: creds[:password])
OpenStruct.new({
token: AuthToken.new.token(user),
user: user
})
rescue ActiveRecord::RecordInvalid => e
raise GraphQL::ExecutionError.new("Invalid input")
end
end
class Mutations::CreateUser
def call(_obj, args, _ctx)
creds = args[:credentials]
user = User.create!(
name: creds[:name],email: creds[:email],
password: creds[:password],
password_confirmation: creds[:password])
OpenStruct.new({
token: AuthToken.new.token(user),
user: user
})
rescue ActiveRecord::RecordInvalid => e
raise GraphQL::ExecutionError.new("Invalid input")
end
end
In case you didn't get that, it's
graph-i-ql
"Graphical"
./client/src
./client/src/components/
./client/concerns/
./client/utils
.client/
src/
components/
concerns/
jobs/
detail/
index.jsx
index.test.jsx
SummaryListing/
index.jsx
index.test.jsx
SummaryActionBar/
...
utils/
...
create-react-app client
./client/
./client/src/
import {createNetworkInterface} from 'react-apollo'
const networkInterface = createNetworkInterface({
uri: "/graphql",
})
networkInterface.use([{
applyMiddleware(req, next) {
let token = window.sessionStorage
.getItem('token');
if (token) {
if (!req.options.headers) {
req.options.headers = {};
}
req.options.headers.authorization =
`Bearer ${token}`
}
next();
}
}]);
const client = new ApolloClient({
networkInterface,
})
Much like you wrap Redux Store providers, you wrap the Apollo Provider around the application
Apollo Provider can take the place of a lot of your redux store needs
it uses Redux under the hood
Even better in v. 2
<ApolloProvider client={client}>
<BrowserRouter>
<Layout>
<Route exact path="/" component={WhoAmI}/>
<Route exact path="/login" component={LogIn}/>
<Route exact path="/signup" component={SignUp}/>
<Route exact path="/logout" component={LogOut}/>
<Route exact path="/posts" component={PostsIndex}/>
</Layout>
</BrowserRouter>
</ApolloProvider>
SignUp.jsx
componentconst signUpMutation = gql`
mutation SignUpUser(
$credentials: AuthProviderCredentials
) {
createUser(credentials: $credentials)
{token user {name email}}
}`
constructor(props) {
super(props)
this.state = {
name: '',
email: '',
password: '',
mutate: props.mutate,
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange(e) {
const target = e.target
const value = target.type === 'checkbox' ?
target.checked :
target.value
const name = target.name
this.setState({
[name]: value,
})
}
handleSubmit(e) {
e.preventDefault()
const credentials = {
'name': this.state.name,
'email': this.state.password,
'password': this.state.password
}
this.state.mutate({ variables: {credentials} })
.then(response => {
let token = response.data.createUser.token
window.sessionStorage.setItem('token', token)
})
}
render() {
if (this.state.loggedIn) return <Redirect to="/"/>
return (
<div>
<form onSubmit={this.handleSubmit}>>
<div>
<label>Name: <input type="text" name="name"
value={this.state.name} onChange={this.handleChange}/></label>
</div>
<div>
<label>Email: <input type="email" name="email"
value={this.state.email} onChange={this.handleChange}/></label>
</div>
<div>
<label>Password: <input type="text" name="password"
value={this.state.password} onChange={this.handleChange}/></label>
</div>
<div> <input type="submit"/> </div>
</form>
</div>
)
}
const SignUpWithMutation =
graphql(signUpMutation)(SignUp)
export default SignUpWithMutation
Back in main application, we're importing this component and invoking on on a matching route
import SignUp from './SignUp'
<Route exact path="/signup" component={SignUp}/>
The mutation is calling the :createUser
mutation
When that completes, the new user's name and
email, and their JWT is returned
the mutate promise resolves, and the token is put into session
storage
because of the middleware in the network interface, the "Authorization" header is set with the token
The posts component gives a list of public posts
It's another React Component class
A posts query is wrapped around the display component
The query is rooted at the viewer, who is established via the authorization header on the server
const listPosts = gql`query Posts{
viewer {
public_posts {
id
title
excerpt
publishedAt
}}}`
GraphQL wrapped query components have a sort of canonical form
const MyComponent = props => {
const { data: {
loading, error, data_name } } = props;
if (loading) return <p>Loading...</p>;
if (error) return <p>Error! {error}</p>;
return (<div>
{ do something with the data returned }
</div>);
}
dereferencing the props passing in from graphql
const MyComponent = props => {
const { data: {
loading, error, data_name } } = props;
if (loading) return <p>Loading...</p>;
if (error) return <p>Error! {error}</p>;
return (<div>
{ do something with the data returned }
</div>);
}
data_name will correspond to the root of the query
In my example query, this would be viewer
const listPosts = gql`query Posts{
viewer {
public_posts {
id title excerpt
publishedAt
}
}}`
The data returned is a JSON object with the structure of
{
"data": {
"viewer": {
"public_posts" : [
{"id": 1, "title": "A fine day",
"excerpt": "It started off sunny ...",
"publishedAt": "2017-09-21..."},
]
}
}
}
return (
<div>
<h1>Posts</h1>
{public_posts.map((post, idx) => {
return <PostSummary post={post} key={idx} />
})}
</div>
)
just like we saw in the SignUp mutation, graphql wraps the component with the query, and returns the high order component.
const PostsIndexWithData =
graphql(listPosts)(PostsIndex)
export default PostsIndexWithData
???
Tamara Temple
tamara@tamouse.org