51 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | class QueryError < StandardError; end
3 |
4 | private
5 | # Public: Define request scoped helper method for making GraphQL queries.
6 | #
7 | # Examples
8 | #
9 | # data = query(ViewerQuery)
10 | # data.viewer.login #=> "josh"
11 | #
12 | # definition - A query or mutation operation GraphQL::Client::Definition.
13 | # Client.parse("query { version }") returns a definition.
14 | # variables - Optional set of variables to use during the operation.
15 | # (default: {})
16 | #
17 | # Returns a structured query result or raises if the request failed.
18 | def query(definition, variables = {})
19 | response = GitHub::Client.query(definition, variables: variables, context: client_context)
20 |
21 | if response.errors.any?
22 | raise QueryError.new(response.errors[:data].join(", "))
23 | else
24 | response.data
25 | end
26 | end
27 |
28 | # Public: Useful helper method for tracking GraphQL context data to pass
29 | # along to the network adapter.
30 | def client_context
31 | # Use static access token from environment. However, here we have access
32 | # to the current request so we could configure the token to be retrieved
33 | # from a session cookie.
34 | { access_token: GitHub::Application.secrets.github_access_token }
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/views/repositories/_repositories.html.erb:
--------------------------------------------------------------------------------
1 | <%graphql
2 | # Connection types are similar to ActiveRecord associations. Connections
3 | # always expose "edges" and "pageInfo". "edges" are the items in the
4 | # collection and "pageInfo" is pagination related metadata.
5 | fragment RepositoryConnection on RepositoryConnection {
6 | # edges returns an Array, however it doesn't return an Array of
7 | # Repositories. Instead its a RepositoryEdge that has a node that is a
8 | # Repository. Sorry, for all the indirection.
9 | edges {
10 | # node is the Repository itself
11 | node {
12 | ...Views::Repositories::ListItem::Repository
13 | }
14 |
15 | # Cursor is an opaque identifier that lets you fetch items before or
16 | # after this repository in this collection. To fetch the next 10 items,
17 | # we'll want the last item's cursor.
18 | cursor
19 | }
20 |
21 | # Pagination related metadata
22 | pageInfo {
23 | hasNextPage
24 | # hasPreviousPage can also be checked
25 | }
26 | }
27 | %>
28 | <% repositories = Views::Repositories::Repositories::RepositoryConnection.new(repositories) %>
29 |
30 | <%#
31 | We could enumerate this list by repositories.edges.each { |edge| edge.node }
32 | but each_node provides a handy enumerator to get each repository.
33 | %>
34 | <% repositories.each_node do |repository| %>
35 | <%= render "repositories/list_item", repository: repository %>
36 | <% end %>
37 |
38 | <% if repositories.page_info.has_next_page? %>
39 |
45 | <% end %>
46 |
--------------------------------------------------------------------------------
/app/views/repositories/index.html.erb:
--------------------------------------------------------------------------------
1 | <%#
2 | GraphQL fragments are defined in the templates themselves.
3 |
4 | All data being used directly in this template should also be reflected
5 | statically in the fragment. And vice versa, all fields in the query fragment
6 | should only be used directly in this template. You MUST keep static queries
7 | and runtime usage in sync. Defining queries in the same file makes
8 | this aspect easier to manage.
9 | %>
10 | <%graphql
11 | # Fragments are parts of queries, they can't be executed themselves.
12 | # This fragment is defined on the User type and is named "Viewer". You can
13 | # name the fragment whatever you'd like. Its the name that is exported as
14 | # a Ruby constant, so it must start with a capital letter.
15 | #
16 | # Its exported as "Views::Repositories::Index::Viewer". The module path to
17 | # the tempate then the name of the fragment.
18 | fragment Viewer on User {
19 | # Initially, fetch the first 10 repositories. We'll show a "load more"
20 | # button to demostrate GraphQL connection cursors.
21 | repositories(first: 10) {
22 | # The only data we're using directly in this template is the total number
23 | # of repositories. Also note that its camelcase here.
24 | totalCount
25 |
26 | # Include data dependencies of app/views/repositories/_repositories.html.erb
27 | # All renders in this template will map to a fragment spread to
28 | # statically define the view composition relationship.
29 | ...Views::Repositories::Repositories::RepositoryConnection
30 | }
31 | }
32 | %>
33 | <%#
34 | The first step of any GraphQL defined view is to cast arguments to the locally
35 | defined fragment result wrapper. In a statically typed langauge, you can think
36 | of it as passing a concrete type into a function that accepts an interface.
37 |
38 | def repositories_index(viewer: Views::Repositories::Index::Viewer)
39 |
40 | The wrapper serves two primary roles:
41 |
42 | 1. Provides Ruby friendly snake case accessors for accessing the underlying data
43 | 2. Only expose fields explicitly defined by this fragment.
44 | `viewer.repositories` is a full set of data, but only `total_count` is
45 | exposed to this template since thats all we explicitly defined.
46 | %>
47 | <% viewer = Views::Repositories::Index::Viewer.new(viewer) %>
48 |
49 |
50 |
51 |
52 | Your repositories
53 | <%# NOTE: total_count is snake case here %>
54 | <%= viewer.repositories.total_count %>
55 |
56 |
57 | <%#
58 | render repositories subview passing along viewer.repositories
59 | See that Views::Repositories::Repositories::RepositoryConnection is
60 | declared in our static fragment.
61 | %>
62 | <%= render "repositories/repositories", repositories: viewer.repositories %>
63 |
64 |
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitHub GraphQL Rails example application
2 |
3 | Demonstrates how to use the [`graphql-client`](http://github.com/github/graphql-client) gem to build a simple repository listing web view against the [GitHub GraphQL API](https://developer.github.com/v4/guides/intro-to-graphql/).
4 |
5 |
6 |
7 | The application structure is setup like a typical Rails app using controllers, views and routes with one key difference, no models. This app doesn't connect directly to any database. All the data is being fetched remotely from the GitHub GraphQL API. Instead of declaring resource models, data queries are declared right along side their usage in controllers and views. This allows an efficient single request to be constructed rather than making numerous REST requests to render a single view.
8 |
9 | ## Table of Contents
10 |
11 | Jump right into the code and read the inline documentation. The following is a suggested reading order:
12 |
13 | 1. [app/controller/repositories_controller.rb](https://github.com/github/github-graphql-rails-example/blob/master/app/controllers/repositories_controller.rb) defines the top level GraphQL queries to fetch repository list and show pages.
14 | 2. [app/views/repositories/index.html.erb](https://github.com/github/github-graphql-rails-example/blob/master/app/views/repositories/index.html.erb) shows the root template's listing query and composition over subviews.
15 | 3. [app/views/repositories/_repositories.html.erb]( https://github.com/github/github-graphql-rails-example/blob/master/app/views/repositories/_repositories.html.erb) makes use of GraphQL connections to show the first couple items and a "load more" button.
16 | 4. [app/views/repositories/show.html.erb](https://github.com/github/github-graphql-rails-example/blob/master/app/views/repositories/show.html.erb) shows the root template for the repository show page.
17 | 5. [app/controller/application_controller.rb](https://github.com/github/github-graphql-rails-example/blob/master/app/controllers/application_controller.rb) defines controller helpers for executing GraphQL query requests.
18 | 6. [config/application.rb](https://github.com/github/github-graphql-rails-example/blob/master/config/application.rb) configures `GraphQL::Client` to point to the GitHub GraphQL endpoint.
19 |
20 | ## Running locally
21 |
22 | First, you'll need a [GitHub API access token](https://help.github.com/articles/creating-an-access-token-for-command-line-use) to make GraphQL API requests. This should be set as a `GITHUB_ACCESS_TOKEN` environment variable as configured in [config/secrets.yml](https://github.com/github/github-graphql-rails-example/blob/master/config/secrets.yml).
23 |
24 | ``` sh
25 | $ git clone https://github.com/github/github-graphql-rails-example
26 | $ cd github-graphql-rails-example/
27 | $ bundle install
28 | $ GITHUB_ACCESS_TOKEN=abc123 bin/rails server
29 | ```
30 |
31 | And visit [http://localhost:3000/](http://localhost:3000/).
32 |
33 | ## See Also
34 |
35 | * [Facebook's GraphQL homepage](http://graphql.org/)
36 | * [GitHub's GraphQL API Early Access program](https://developer.github.com/early-access/graphql)
37 | * [Ruby GraphQL Client library](https://github.com/github/graphql-client)
38 | * [Relay Modern example](https://github.com/github/github-graphql-relay-example)
39 |
--------------------------------------------------------------------------------
/app/views/repositories/show.html.erb:
--------------------------------------------------------------------------------
1 | <%#
2 | GraphQL fragments are defined in the templates themselves.
3 |
4 | All data being used directly in this template should also be reflected
5 | statically in the fragment. And vice versa, all fields in the query fragment
6 | should only be used directly in this template. You MUST keep static queries
7 | and runtime usage in sync. Defining queries in the same file makes
8 | this aspect easier to manage.
9 | %>
10 | <%graphql
11 | # Fragments are parts of queries, they can't be executed themselves.
12 | # This fragment is defined on the Repository type and is coincidentally named
13 | # "Repository". You can name the fragment whatever you'd like. Its the name
14 | # that is exported as a Ruby constant, so it must start with a capital letter.
15 | #
16 | # Its exported as "Views::Repositories::Show::Repository". The module path to
17 | # the tempate then the name of the fragment.
18 | fragment Repository on Repository {
19 | # id is a GraphQL global id, not a database id
20 | # It is an opaque base64 identifier we can use to refetch the entity.
21 | id
22 |
23 | # owner is a User or Organization
24 | owner {
25 | login
26 | }
27 |
28 | # The repository's name
29 | name
30 |
31 | # Optional description text and homepage URL
32 | description
33 | homepageUrl
34 |
35 | # Include repositories/_navigation.html.erb data dependencies
36 | ...Views::Repositories::Navigation::Repository
37 |
38 | ...Views::Repositories::Star::Repository
39 | }
40 | %>
41 | <%#
42 | The first step of any GraphQL defined view is to cast arguments to the locally
43 | defined fragment result wrapper. In a statically typed langauge, you can think
44 | of it as passing a concrete type into a function that accepts an interface.
45 |
46 | def repositories_show(repository: Views::Repositories::Show::Repository)
47 |
48 | The wrapper serves two primary roles:
49 |
50 | 1. Provides Ruby friendly snake case accessors for accessing the underlying data
51 | 2. Only expose fields explicitly defined by this fragment.
52 | `repository` is a full set of data, but fields included by the
53 | ...Views::Repositories::Navigation::Repository spread are hidden.
54 | %>
55 | <% repository = Views::Repositories::Show::Repository.new(repository) %>
56 |
57 |