├── .gitignore
├── .ruby-version
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── Rakefile
├── app
├── controllers
│ ├── application_controller.rb
│ └── repositories_controller.rb
└── views
│ ├── layouts
│ └── application.html.erb
│ └── repositories
│ ├── _icon.html.erb
│ ├── _list_item.html.erb
│ ├── _navigation.html.erb
│ ├── _repositories.html.erb
│ ├── _star.html.erb
│ ├── index.html.erb
│ └── show.html.erb
├── bin
├── bundle
├── rails
└── rake
├── config.ru
├── config
├── application.rb
├── boot.rb
├── environment.rb
├── environments
│ └── development.rb
├── routes.rb
└── secrets.yml
├── db
└── schema.json
├── lib
└── tasks
│ └── schema.rake
└── public
├── application.css
├── application.js
├── favicon.ico
├── octicons.eot
├── octicons.min.css
├── octicons.svg
├── octicons.ttf
├── octicons.woff
└── octicons.woff2
/.gitignore:
--------------------------------------------------------------------------------
1 | *.rbc
2 | capybara-*.html
3 | .rspec
4 | /log
5 | /tmp
6 | /db/*.sqlite3
7 | /db/*.sqlite3-journal
8 | /public/system
9 | /coverage/
10 | /spec/tmp
11 | *.orig
12 | rerun.txt
13 | pickle-email-*.html
14 |
15 | # Ignore Byebug command history file.
16 | .byebug_history
17 |
18 | # Ignore precompiled javascript packs
19 | /public/packs
20 | /public/packs-test
21 | /public/assets
22 |
23 | # Ignore uploaded files in development
24 | /storage/*
25 | !/storage/.keep
26 |
27 | # Editors
28 | *.sublime-workspace
29 | *.sublime-project
30 | .idea
31 | .vscode
32 |
33 | .DS_Store
34 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.4.2
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "rails", "~> 5.0.7"
4 | gem "graphql", "1.2.2"
5 | gem "graphql-client", "0.2.3"
6 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (5.0.7)
5 | actionpack (= 5.0.7)
6 | nio4r (>= 1.2, < 3.0)
7 | websocket-driver (~> 0.6.1)
8 | actionmailer (5.0.7)
9 | actionpack (= 5.0.7)
10 | actionview (= 5.0.7)
11 | activejob (= 5.0.7)
12 | mail (~> 2.5, >= 2.5.4)
13 | rails-dom-testing (~> 2.0)
14 | actionpack (5.0.7)
15 | actionview (= 5.0.7)
16 | activesupport (= 5.0.7)
17 | rack (~> 2.0)
18 | rack-test (~> 0.6.3)
19 | rails-dom-testing (~> 2.0)
20 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
21 | actionview (5.0.7)
22 | activesupport (= 5.0.7)
23 | builder (~> 3.1)
24 | erubis (~> 2.7.0)
25 | rails-dom-testing (~> 2.0)
26 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
27 | activejob (5.0.7)
28 | activesupport (= 5.0.7)
29 | globalid (>= 0.3.6)
30 | activemodel (5.0.7)
31 | activesupport (= 5.0.7)
32 | activerecord (5.0.7)
33 | activemodel (= 5.0.7)
34 | activesupport (= 5.0.7)
35 | arel (~> 7.0)
36 | activesupport (5.0.7)
37 | concurrent-ruby (~> 1.0, >= 1.0.2)
38 | i18n (>= 0.7, < 2)
39 | minitest (~> 5.1)
40 | tzinfo (~> 1.1)
41 | arel (7.1.4)
42 | builder (3.2.3)
43 | concurrent-ruby (1.0.5)
44 | crass (1.0.4)
45 | erubis (2.7.0)
46 | globalid (0.4.1)
47 | activesupport (>= 4.2.0)
48 | graphql (1.2.2)
49 | graphql-client (0.2.3)
50 | activesupport (>= 3.0, < 6.0)
51 | graphql (>= 0.19.2)
52 | i18n (1.1.1)
53 | concurrent-ruby (~> 1.0)
54 | loofah (2.2.2)
55 | crass (~> 1.0.2)
56 | nokogiri (>= 1.5.9)
57 | mail (2.7.1)
58 | mini_mime (>= 0.1.1)
59 | method_source (0.9.0)
60 | mini_mime (1.0.1)
61 | mini_portile2 (2.3.0)
62 | minitest (5.11.3)
63 | nio4r (2.3.1)
64 | nokogiri (1.8.5)
65 | mini_portile2 (~> 2.3.0)
66 | rack (2.0.5)
67 | rack-test (0.6.3)
68 | rack (>= 1.0)
69 | rails (5.0.7)
70 | actioncable (= 5.0.7)
71 | actionmailer (= 5.0.7)
72 | actionpack (= 5.0.7)
73 | actionview (= 5.0.7)
74 | activejob (= 5.0.7)
75 | activemodel (= 5.0.7)
76 | activerecord (= 5.0.7)
77 | activesupport (= 5.0.7)
78 | bundler (>= 1.3.0)
79 | railties (= 5.0.7)
80 | sprockets-rails (>= 2.0.0)
81 | rails-dom-testing (2.0.3)
82 | activesupport (>= 4.2.0)
83 | nokogiri (>= 1.6)
84 | rails-html-sanitizer (1.0.4)
85 | loofah (~> 2.2, >= 2.2.2)
86 | railties (5.0.7)
87 | actionpack (= 5.0.7)
88 | activesupport (= 5.0.7)
89 | method_source
90 | rake (>= 0.8.7)
91 | thor (>= 0.18.1, < 2.0)
92 | rake (12.3.1)
93 | sprockets (3.7.2)
94 | concurrent-ruby (~> 1.0)
95 | rack (> 1, < 3)
96 | sprockets-rails (3.2.1)
97 | actionpack (>= 4.0)
98 | activesupport (>= 4.0)
99 | sprockets (>= 3.0.0)
100 | thor (0.20.0)
101 | thread_safe (0.3.6)
102 | tzinfo (1.2.5)
103 | thread_safe (~> 0.1)
104 | websocket-driver (0.6.5)
105 | websocket-extensions (>= 0.1.0)
106 | websocket-extensions (0.1.3)
107 |
108 | PLATFORMS
109 | ruby
110 |
111 | DEPENDENCIES
112 | graphql (= 1.2.2)
113 | graphql-client (= 0.2.3)
114 | rails (~> 5.0.7)
115 |
116 | BUNDLED WITH
117 | 1.16.6
118 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 GitHub, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require_relative 'config/application'
2 | Rails.application.load_tasks
3 |
--------------------------------------------------------------------------------
/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/controllers/repositories_controller.rb:
--------------------------------------------------------------------------------
1 | class RepositoriesController < ApplicationController
2 | # Define query for repository listing.
3 | #
4 | # All queries MUST be assigned to constants and therefore be statically
5 | # defined. Queries MUST NOT be generated at request time.
6 | IndexQuery = GitHub::Client.parse <<-'GRAPHQL'
7 | # All read requests are defined in a "query" operation
8 | query {
9 | # viewer is the currently authenticated User
10 | viewer {
11 | # "...FooConstant" is the fragment spread syntax to include the index
12 | # view's fragment.
13 | #
14 | # "Views::Repositories::Index::Viewer" means the fragment is defined
15 | # in app/views/repositories/index.html.erb and named Viewer.
16 | ...Views::Repositories::Index::Viewer
17 | }
18 | }
19 | GRAPHQL
20 |
21 | # GET /repositories
22 | def index
23 | # Use query helper defined in ApplicationController to execute the query.
24 | # `query` returns a GraphQL::Client::QueryResult instance with accessors
25 | # that map to the query structure.
26 | data = query IndexQuery
27 |
28 | # Render the app/views/repositories/index.html.erb template with our
29 | # current User.
30 | #
31 | # Using explicit render calls with locals is preferred to implicit render
32 | # with instance variables.
33 | render "repositories/index", locals: {
34 | viewer: data.viewer
35 | }
36 | end
37 |
38 |
39 | # Define query for "Show more repositories..." AJAX action.
40 | MoreQuery = GitHub::Client.parse <<-'GRAPHQL'
41 | # This query uses variables to accept an "after" param to load the next
42 | # 10 repositories.
43 | query($after: String!) {
44 | viewer {
45 | repositories(first: 10, after: $after) {
46 | # Instead of refetching all of the index page's data, we only need
47 | # the data for the repositories container partial.
48 | ...Views::Repositories::Repositories::RepositoryConnection
49 | }
50 | }
51 | }
52 | GRAPHQL
53 |
54 | # GET /repositories/more?after=CURSOR
55 | def more
56 | # Execute the MoreQuery passing along data from params to the query.
57 | data = query MoreQuery, after: params[:after]
58 |
59 | # Using an explicit render again, just render the repositories list partial
60 | # and return it to the client.
61 | render partial: "repositories/repositories", locals: {
62 | repositories: data.viewer.repositories
63 | }
64 | end
65 |
66 |
67 | # Define query for repository show page.
68 | ShowQuery = GitHub::Client.parse <<-'GRAPHQL'
69 | # Query is parameterized by a $id variable.
70 | query($id: ID!) {
71 | # Use global id Node lookup
72 | node(id: $id) {
73 | # Include fragment for app/views/repositories/show.html.erb
74 | ...Views::Repositories::Show::Repository
75 | }
76 | }
77 | GRAPHQL
78 |
79 | # GET /repositories/ID
80 | def show
81 | # Though we've only defined part of the ShowQuery in the controller, when
82 | # query(ShowQuery) is executed, we're sending along the query as well as
83 | # all of its fragment dependencies to the API server.
84 | #
85 | # Here's the raw query that's actually being sent.
86 | #
87 | # query RepositoriesController__ShowQuery($id: ID!) {
88 | # node(id: $id) {
89 | # ...Views__Repositories__Show__Repository
90 | # }
91 | # }
92 | #
93 | # fragment Views__Repositories__Show__Repository on Repository {
94 | # id
95 | # owner {
96 | # login
97 | # }
98 | # name
99 | # description
100 | # homepageUrl
101 | # ...Views__Repositories__Navigation__Repository
102 | # }
103 | #
104 | # fragment Views__Repositories__Navigation__Repository on Repository {
105 | # hasIssuesEnabled
106 | # }
107 | data = query ShowQuery, id: params[:id]
108 |
109 | if repository = data.node
110 | render "repositories/show", locals: {
111 | repository: repository
112 | }
113 | else
114 | # If node can't be found, 404. This may happen if the repository doesn't
115 | # exist, we don't have permission or we used a global ID that was the
116 | # wrong type.
117 | head :not_found
118 | end
119 | end
120 |
121 | StarMutation = GitHub::Client.parse <<-'GRAPHQL'
122 | mutation($id: ID!) {
123 | star(input: { starrableId: $id }) {
124 | starrable {
125 | ...Views::Repositories::Star::Repository
126 | }
127 | }
128 | }
129 | GRAPHQL
130 |
131 | def star
132 | data = query StarMutation, id: params[:id]
133 |
134 | if repository = data.star
135 | respond_to do |format|
136 | format.js {
137 | render partial: "repositories/star", locals: { repository: data.star.starrable }
138 | }
139 |
140 | format.html {
141 | redirect_to "/repositories"
142 | }
143 | end
144 | else
145 | head :not_found
146 | end
147 | end
148 |
149 | UnstarMutation = GitHub::Client.parse <<-'GRAPHQL'
150 | mutation($id: ID!) {
151 | unstar(input: { starrableId: $id }) {
152 | starrable {
153 | ...Views::Repositories::Star::Repository
154 | }
155 | }
156 | }
157 | GRAPHQL
158 |
159 | def unstar
160 | data = query UnstarMutation, id: params[:id]
161 |
162 | if repository = data.unstar
163 | respond_to do |format|
164 | format.js {
165 | render partial: "repositories/star", locals: { repository: data.unstar.starrable }
166 | }
167 |
168 | format.html {
169 | redirect_to "/repositories"
170 | }
171 | end
172 | else
173 | head :not_found
174 | end
175 | end
176 | end
177 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | GitHub
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/views/repositories/_icon.html.erb:
--------------------------------------------------------------------------------
1 | <%graphql
2 | fragment Repository on Repository {
3 | isFork
4 | isMirror
5 | isPrivate
6 | }
7 | %>
8 | <% repository = Views::Repositories::Icon::Repository.new(repository) %>
9 |
10 | <% if repository.is_fork? %>
11 |
12 | <% elsif repository.is_private? %>
13 |
14 | <% elsif repository.is_mirror? %>
15 |
16 | <% else %>
17 |
18 | <% end %>
19 |
--------------------------------------------------------------------------------
/app/views/repositories/_list_item.html.erb:
--------------------------------------------------------------------------------
1 | <%graphql
2 | fragment Repository on Repository {
3 | id
4 | owner {
5 | login
6 | }
7 | name
8 | stargazers {
9 | totalCount
10 | }
11 | ...Views::Repositories::Icon::Repository
12 | ...Views::Repositories::Star::Repository
13 | }
14 | %>
15 | <% repository = Views::Repositories::ListItem::Repository.new(repository) %>
16 |
17 |
18 | <%= render "repositories/star", repository: repository %>
19 | <%= render "repositories/icon", repository: repository %>
20 |
21 |
22 | <%= repository.owner.login %>/<%= repository.name %>
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/views/repositories/_navigation.html.erb:
--------------------------------------------------------------------------------
1 | <%graphql
2 | fragment Repository on Repository {
3 | hasIssuesEnabled
4 | issues {
5 | totalCount
6 | }
7 | pullRequests {
8 | totalCount
9 | }
10 | }
11 | %>
12 | <% repository = Views::Repositories::Navigation::Repository.new(repository) %>
13 |
14 |
51 |
--------------------------------------------------------------------------------
/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 |
40 |
41 | Show more repositories...
42 |
43 |
44 |
45 | <% end %>
46 |
--------------------------------------------------------------------------------
/app/views/repositories/_star.html.erb:
--------------------------------------------------------------------------------
1 | <%graphql
2 | fragment Repository on Repository {
3 | id
4 | viewerHasStarred
5 | stargazers {
6 | totalCount
7 | }
8 | }
9 | %>
10 | <% repository = Views::Repositories::Star::Repository.new(repository) %>
11 |
12 |
13 | <%= repository.stargazers.total_count %>
14 | <%= form_tag repository.viewer_has_starred? ? unstar_repository_path(repository.id) : star_repository_path(repository.id), method: :put, class: "star-form" do %>
15 |
16 |
17 |
18 | <% end %>
19 |
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
70 |
71 |
72 |
73 |
74 |
<%= repository.name %>
75 |
76 | <% if repository.description.present? %>
77 |
<%= repository.description %>
78 | <% end %>
79 |
80 |
81 | <%# NOTE: homepage_url is snake case here %>
82 | <% if repository.homepage_url.present? %>
83 | Homepage
84 | <% end %>
85 |
86 | View on GitHub
87 |
88 |
89 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../config/application', __dir__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | require_relative 'config/environment'
2 | run Rails.application
3 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails"
4 | require "action_controller/railtie"
5 | require "action_view/railtie"
6 | require "graphql/client/railtie"
7 | require "graphql/client/http"
8 |
9 | Bundler.require(*Rails.groups)
10 |
11 | module GitHub
12 | class Application < Rails::Application
13 | end
14 |
15 | HTTPAdapter = GraphQL::Client::HTTP.new("https://api.github.com/graphql") do
16 | def headers(context)
17 | unless token = context[:access_token] || Application.secrets.github_access_token
18 | # $ GITHUB_ACCESS_TOKEN=abc123 bin/rails server
19 | # https://help.github.com/articles/creating-an-access-token-for-command-line-use
20 | fail "Missing GitHub access token"
21 | end
22 |
23 | {
24 | "Authorization" => "Bearer #{token}"
25 | }
26 | end
27 | end
28 |
29 | Client = GraphQL::Client.new(
30 | schema: Application.root.join("db/schema.json").to_s,
31 | execute: HTTPAdapter
32 | )
33 | Application.config.graphql.client = Client
34 | end
35 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 | require 'bundler/setup'
3 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | require_relative 'application'
2 | Rails.application.initialize!
3 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.cache_classes = false
3 | config.eager_load = false
4 | config.consider_all_requests_local = true
5 |
6 | config.action_controller.perform_caching = false
7 | config.cache_store = :null_store
8 |
9 | config.active_support.deprecation = :log
10 | end
11 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :repositories do
3 | get "more", on: :collection
4 | put "star", on: :member
5 | put "unstar", on: :member
6 | end
7 | get "/", to: redirect("/repositories")
8 | end
9 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | development:
2 | secret_key_base: f81cf4b9e7cdc87601e48bf02d34b14b5b2ef64b2c4a3d3d8fa7d15595102fee7b5d40812da93da3ef1412bb01a61f6a2aec94bf2cd95f2bf990f0fb6c115a9a
3 | github_access_token: <%= ENV["GITHUB_ACCESS_TOKEN"] %>
4 |
--------------------------------------------------------------------------------
/lib/tasks/schema.rake:
--------------------------------------------------------------------------------
1 | namespace :schema do
2 | # The public schema will evolve over time, so you'll want to periodically
3 | # refetch the latest and check in the changes.
4 | #
5 | # An offline copy of the schema allows queries to be typed checked statically
6 | # before even sending a request.
7 | desc "Update GitHub GraphQL schema"
8 | task :update do
9 | GraphQL::Client.dump_schema(GitHub::HTTPAdapter, "db/schema.json")
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/public/application.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding-top: 20px;
3 | padding-bottom: 20px;
4 | }
5 |
6 | .inline-block {
7 | display: inline-block;
8 | }
9 |
10 | .header {
11 | margin-top: 0;
12 | margin-bottom: 0;
13 | }
14 |
15 | .header h3 {
16 | margin-top: 0;
17 | margin-bottom: 0;
18 | line-height: 40px;
19 | }
20 |
21 | .octicon {
22 | vertical-align: text-top;
23 | }
24 |
25 | .repositories {
26 | width: 350px;
27 | }
28 |
29 | .star-badge {
30 | float: right;
31 | color: #777;
32 | }
33 |
34 | .star-badge-header {
35 | margin-left: 10px;
36 | vertical-align: text-bottom;
37 | }
38 |
39 | .star-badge .octicon {
40 | color: #777;
41 | }
42 |
43 | .star-badge .highlight {
44 | color: #e36209;
45 | }
46 |
47 | .star-form {
48 | display: inline;
49 | }
50 |
51 | .star-form button {
52 | background: transparent;
53 | border: none;
54 | margin: 0;
55 | padding: 0;
56 | }
57 |
58 | .nav-pills .badge {
59 | background-color: #ccc;
60 | }
61 |
62 | .show-more .spinner {
63 | display: none;
64 | }
65 |
66 | .show-more.loading .spinner {
67 | display: inline-block;
68 | float: right;
69 | animation-name: spin;
70 | animation-duration: 2000ms;
71 | animation-iteration-count: infinite;
72 | animation-timing-function: linear;
73 | padding-left: 4px;
74 | }
75 |
76 | @keyframes spin {
77 | from {
78 | transform: rotate(0deg);
79 | } to {
80 | transform: rotate(360deg);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/public/application.js:
--------------------------------------------------------------------------------
1 | function loadMoreRepositories(link) {
2 | var container = link.parentElement;
3 | container.classList.add('loading');
4 |
5 | fetch(link.href).then(function(response) {
6 | response.text().then(function(text) {
7 | container.insertAdjacentHTML('afterend', text);
8 | container.remove();
9 | })
10 | })
11 | }
12 |
13 | function toggleStar(el) {
14 | fetch(el.action, { method: 'PUT', headers: { 'X-Requested-With': 'XMLHttpRequest' } }).then(function(response) {
15 | response.text().then(function(text) {
16 | // Parse text to get an actual element
17 | var div = document.createElement('div')
18 | div.innerHTML = text
19 |
20 | // Find the star container
21 | var container = el.closest('.star-badge')
22 | container.replaceWith(div.firstElementChild)
23 | })
24 | })
25 | }
26 |
27 | document.addEventListener('submit', function(e) {
28 | var form = e.target
29 | toggleStar(form)
30 | e.preventDefault()
31 | })
32 |
33 | // Basic event delegation
34 | document.addEventListener('click', function(e) {
35 | var loadMoreLink = e.target.closest('.js-load-more')
36 | if (loadMoreLink) {
37 | loadMoreRepositories(loadMoreLink)
38 | e.preventDefault()
39 | }
40 | })
41 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/github-graphql-rails-example/8d274407aba1952605649b1152cd43aa3a78e67a/public/favicon.ico
--------------------------------------------------------------------------------
/public/octicons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/github-graphql-rails-example/8d274407aba1952605649b1152cd43aa3a78e67a/public/octicons.eot
--------------------------------------------------------------------------------
/public/octicons.min.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:Octicons;src:url(octicons.eot?ef21c39f0ca9b1b5116e5eb7ac5eabe6);src:url(octicons.eot?#iefix) format("embedded-opentype"),url(octicons.woff2?ef21c39f0ca9b1b5116e5eb7ac5eabe6) format("woff2"),url(octicons.woff?ef21c39f0ca9b1b5116e5eb7ac5eabe6) format("woff"),url(octicons.ttf?ef21c39f0ca9b1b5116e5eb7ac5eabe6) format("truetype"),url(octicons.svg?ef21c39f0ca9b1b5116e5eb7ac5eabe6#octicons) format("svg");font-weight:400;font-style:normal}.mega-octicon,.octicon{font:normal normal normal 16px/1 Octicons;display:inline-block;text-decoration:none;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-user-select:none;-ms-user-select:none;user-select:none;speak:none}.mega-octicon{font-size:32px}.octicon-alert:before{content:"\f02d"}.octicon-arrow-down:before{content:"\f03f"}.octicon-arrow-left:before{content:"\f040"}.octicon-arrow-right:before{content:"\f03e"}.octicon-arrow-small-down:before{content:"\f0a0"}.octicon-arrow-small-left:before{content:"\f0a1"}.octicon-arrow-small-right:before{content:"\f071"}.octicon-arrow-small-up:before{content:"\f09f"}.octicon-arrow-up:before{content:"\f03d"}.octicon-beaker:before{content:"\f0dd"}.octicon-bell:before{content:"\f0de"}.octicon-bold:before{content:"\f0e2"}.octicon-book:before{content:"\f007"}.octicon-bookmark:before{content:"\f07b"}.octicon-briefcase:before{content:"\f0d3"}.octicon-broadcast:before{content:"\f048"}.octicon-browser:before{content:"\f0c5"}.octicon-bug:before{content:"\f091"}.octicon-calendar:before{content:"\f068"}.octicon-check:before{content:"\f03a"}.octicon-checklist:before{content:"\f076"}.octicon-chevron-down:before{content:"\f0a3"}.octicon-chevron-left:before{content:"\f0a4"}.octicon-chevron-right:before{content:"\f078"}.octicon-chevron-up:before{content:"\f0a2"}.octicon-circle-slash:before{content:"\f084"}.octicon-circuit-board:before{content:"\f0d6"}.octicon-clippy:before{content:"\f035"}.octicon-clock:before{content:"\f046"}.octicon-cloud-download:before{content:"\f00b"}.octicon-cloud-upload:before{content:"\f00c"}.octicon-code:before{content:"\f05f"}.octicon-comment-discussion:before{content:"\f04f"}.octicon-comment:before{content:"\f02b"}.octicon-credit-card:before{content:"\f045"}.octicon-dash:before{content:"\f0ca"}.octicon-dashboard:before{content:"\f07d"}.octicon-database:before{content:"\f096"}.octicon-desktop-download:before{content:"\f0dc"}.octicon-device-camera-video:before{content:"\f057"}.octicon-device-camera:before{content:"\f056"}.octicon-device-desktop:before{content:"\f27c"}.octicon-device-mobile:before{content:"\f038"}.octicon-diff-added:before{content:"\f06b"}.octicon-diff-ignored:before{content:"\f099"}.octicon-diff-modified:before{content:"\f06d"}.octicon-diff-removed:before{content:"\f06c"}.octicon-diff-renamed:before{content:"\f06e"}.octicon-diff:before{content:"\f04d"}.octicon-ellipses:before{content:"\f101"}.octicon-ellipsis:before{content:"\f09a"}.octicon-eye:before{content:"\f04e"}.octicon-file-binary:before{content:"\f094"}.octicon-file-code:before{content:"\f010"}.octicon-file-directory:before{content:"\f016"}.octicon-file-media:before{content:"\f012"}.octicon-file-pdf:before{content:"\f014"}.octicon-file-submodule:before{content:"\f017"}.octicon-file-symlink-directory:before{content:"\f0b1"}.octicon-file-symlink-file:before{content:"\f0b0"}.octicon-file-text:before{content:"\f011"}.octicon-file-zip:before{content:"\f013"}.octicon-file:before{content:"\f102"}.octicon-flame:before{content:"\f0d2"}.octicon-fold:before{content:"\f0cc"}.octicon-gear:before{content:"\f02f"}.octicon-gift:before{content:"\f042"}.octicon-gist-secret:before{content:"\f08c"}.octicon-gist:before{content:"\f00e"}.octicon-git-branch:before{content:"\f020"}.octicon-git-commit:before{content:"\f01f"}.octicon-git-compare:before{content:"\f0ac"}.octicon-git-merge:before{content:"\f023"}.octicon-git-pull-request:before{content:"\f009"}.octicon-globe:before{content:"\f0b6"}.octicon-grabber:before{content:"\f103"}.octicon-graph:before{content:"\f043"}.octicon-heart:before{content:"\2665"}.octicon-history:before{content:"\f07e"}.octicon-home:before{content:"\f08d"}.octicon-horizontal-rule:before{content:"\f070"}.octicon-hubot:before{content:"\f09d"}.octicon-inbox:before{content:"\f0cf"}.octicon-info:before{content:"\f059"}.octicon-issue-closed:before{content:"\f028"}.octicon-issue-opened:before{content:"\f026"}.octicon-issue-reopened:before{content:"\f027"}.octicon-italic:before{content:"\f0e4"}.octicon-jersey:before{content:"\f019"}.octicon-key:before{content:"\f049"}.octicon-keyboard:before{content:"\f00d"}.octicon-law:before{content:"\f0d8"}.octicon-light-bulb:before{content:"\f000"}.octicon-link-external:before{content:"\f07f"}.octicon-link:before{content:"\f05c"}.octicon-list-ordered:before{content:"\f062"}.octicon-list-unordered:before{content:"\f061"}.octicon-location:before{content:"\f060"}.octicon-lock:before{content:"\f06a"}.octicon-logo-gist:before{content:"\f0ad"}.octicon-logo-github:before{content:"\f092"}.octicon-mail-read:before{content:"\f03c"}.octicon-mail-reply:before{content:"\f051"}.octicon-mail:before{content:"\f03b"}.octicon-mark-github:before{content:"\f00a"}.octicon-markdown:before{content:"\f0c9"}.octicon-megaphone:before{content:"\f077"}.octicon-mention:before{content:"\f0be"}.octicon-milestone:before{content:"\f075"}.octicon-mirror:before{content:"\f024"}.octicon-mortar-board:before{content:"\f0d7"}.octicon-mute:before{content:"\f080"}.octicon-no-newline:before{content:"\f09c"}.octicon-octoface:before{content:"\f008"}.octicon-organization:before{content:"\f037"}.octicon-package:before{content:"\f0c4"}.octicon-paintcan:before{content:"\f0d1"}.octicon-pencil:before{content:"\f058"}.octicon-person:before{content:"\f018"}.octicon-pin:before{content:"\f041"}.octicon-plug:before{content:"\f0d4"}.octicon-plus-small:before{content:"\f104"}.octicon-plus:before{content:"\f05d"}.octicon-primitive-dot:before{content:"\f052"}.octicon-primitive-square:before{content:"\f053"}.octicon-pulse:before{content:"\f085"}.octicon-question:before{content:"\f02c"}.octicon-quote:before{content:"\f063"}.octicon-radio-tower:before{content:"\f030"}.octicon-reply:before{content:"\f105"}.octicon-repo-clone:before{content:"\f04c"}.octicon-repo-force-push:before{content:"\f04a"}.octicon-repo-forked:before{content:"\f002"}.octicon-repo-pull:before{content:"\f006"}.octicon-repo-push:before{content:"\f005"}.octicon-repo:before{content:"\f001"}.octicon-rocket:before{content:"\f033"}.octicon-rss:before{content:"\f034"}.octicon-ruby:before{content:"\f047"}.octicon-search:before{content:"\f02e"}.octicon-server:before{content:"\f097"}.octicon-settings:before{content:"\f07c"}.octicon-shield:before{content:"\f0e1"}.octicon-sign-in:before{content:"\f036"}.octicon-sign-out:before{content:"\f032"}.octicon-smiley:before{content:"\f0e7"}.octicon-squirrel:before{content:"\f0b2"}.octicon-star:before{content:"\f02a"}.octicon-stop:before{content:"\f08f"}.octicon-sync:before{content:"\f087"}.octicon-tag:before{content:"\f015"}.octicon-tasklist:before{content:"\f0e5"}.octicon-telescope:before{content:"\f088"}.octicon-terminal:before{content:"\f0c8"}.octicon-text-size:before{content:"\f0e3"}.octicon-three-bars:before{content:"\f05e"}.octicon-thumbsdown:before{content:"\f0db"}.octicon-thumbsup:before{content:"\f0da"}.octicon-tools:before{content:"\f031"}.octicon-trashcan:before{content:"\f0d0"}.octicon-triangle-down:before{content:"\f05b"}.octicon-triangle-left:before{content:"\f044"}.octicon-triangle-right:before{content:"\f05a"}.octicon-triangle-up:before{content:"\f0aa"}.octicon-unfold:before{content:"\f039"}.octicon-unmute:before{content:"\f0ba"}.octicon-unverified:before{content:"\f0e8"}.octicon-verified:before{content:"\f0e6"}.octicon-versions:before{content:"\f064"}.octicon-watch:before{content:"\f0e0"}.octicon-x:before{content:"\f081"}.octicon-zap:before{content:"\26a1"}
--------------------------------------------------------------------------------
/public/octicons.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Created by FontForge 20150913 at Mon Jul 11 12:02:11 2016
6 | By Aaron Shekey
7 |
8 |
9 |
10 |
23 |
25 |
27 |
29 |
31 |
33 |
35 |
38 |
40 |
43 |
45 |
47 |
50 |
53 |
56 |
59 |
61 |
63 |
66 |
68 |
70 |
72 |
74 |
76 |
79 |
81 |
83 |
85 |
87 |
90 |
92 |
95 |
98 |
100 |
102 |
105 |
107 |
109 |
111 |
114 |
116 |
118 |
121 |
124 |
126 |
128 |
131 |
133 |
136 |
138 |
140 |
142 |
144 |
146 |
148 |
150 |
152 |
154 |
156 |
158 |
160 |
163 |
165 |
167 |
169 |
171 |
173 |
176 |
178 |
180 |
183 |
185 |
187 |
189 |
191 |
193 |
195 |
197 |
199 |
201 |
204 |
206 |
208 |
211 |
213 |
215 |
217 |
219 |
222 |
225 |
227 |
229 |
232 |
234 |
236 |
238 |
240 |
242 |
244 |
246 |
248 |
251 |
254 |
256 |
258 |
260 |
263 |
265 |
267 |
269 |
271 |
273 |
275 |
277 |
279 |
281 |
283 |
285 |
288 |
292 |
294 |
297 |
300 |
302 |
304 |
306 |
309 |
311 |
313 |
315 |
317 |
319 |
321 |
323 |
326 |
329 |
331 |
333 |
336 |
342 |
344 |
347 |
349 |
351 |
353 |
355 |
357 |
359 |
361 |
363 |
366 |
369 |
371 |
373 |
376 |
379 |
382 |
385 |
388 |
390 |
392 |
394 |
396 |
398 |
400 |
402 |
404 |
406 |
409 |
412 |
416 |
418 |
420 |
422 |
424 |
426 |
428 |
429 |
430 |
--------------------------------------------------------------------------------
/public/octicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/github-graphql-rails-example/8d274407aba1952605649b1152cd43aa3a78e67a/public/octicons.ttf
--------------------------------------------------------------------------------
/public/octicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/github-graphql-rails-example/8d274407aba1952605649b1152cd43aa3a78e67a/public/octicons.woff
--------------------------------------------------------------------------------
/public/octicons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/github-graphql-rails-example/8d274407aba1952605649b1152cd43aa3a78e67a/public/octicons.woff2
--------------------------------------------------------------------------------