├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── rake └── setup ├── example ├── Capfile ├── Gemfile ├── Gemfile.lock ├── Procfile ├── README.md ├── Rakefile ├── bin │ ├── console │ ├── rackup │ └── rake ├── config.ru ├── config │ ├── deploy.rb │ └── deploy │ │ └── production.rb ├── db │ ├── app.sqlite3 │ ├── migrate │ │ └── 20150310163512_create_app.rb │ └── schema.rb ├── lib │ ├── app.rb │ └── app │ │ ├── client.rb │ │ ├── graph │ │ ├── album_field.rb │ │ ├── band_field.rb │ │ ├── create_band_membership_call.rb │ │ ├── date_field.rb │ │ ├── duration_field.rb │ │ ├── list_field.rb │ │ ├── membership_field.rb │ │ ├── model_field.rb │ │ ├── person_field.rb │ │ ├── role_field.rb │ │ ├── root_field.rb │ │ ├── song_field.rb │ │ └── you_field.rb │ │ ├── helper.rb │ │ ├── models │ │ ├── album.rb │ │ ├── attribution.rb │ │ ├── band.rb │ │ ├── concerns │ │ │ └── has_slug.rb │ │ ├── membership.rb │ │ ├── membership_role.rb │ │ ├── person.rb │ │ ├── role.rb │ │ └── song.rb │ │ ├── public │ │ ├── desktop.css │ │ ├── desktop.js │ │ ├── github.png │ │ ├── mobile.css │ │ └── mobile.js │ │ └── views │ │ ├── desktop.erb │ │ └── mobile.erb └── test │ └── fixtures │ ├── albums.yml │ ├── attributions.yml │ ├── bands.yml │ ├── membership_roles.yml │ ├── memberships.yml │ ├── people.yml │ ├── roles.yml │ └── songs.yml ├── gql.gemspec ├── lib ├── gql.rb └── gql │ ├── array.rb │ ├── boolean.rb │ ├── call.rb │ ├── config.rb │ ├── connection.rb │ ├── errors.rb │ ├── executor.rb │ ├── field.rb │ ├── lazy.rb │ ├── mixins │ ├── common.rb │ ├── has_calls.rb │ └── has_fields.rb │ ├── number.rb │ ├── object.rb │ ├── parser.rb │ ├── registry.rb │ ├── scalar.rb │ ├── schema │ ├── call.rb │ ├── caller_class.rb │ ├── field.rb │ ├── list.rb │ └── parameter.rb │ ├── string.rb │ ├── tokenizer.rb │ └── version.rb ├── support ├── parser.racc └── tokenizer.rex └── test ├── cases ├── array_test.rb ├── call_test.rb ├── config_test.rb ├── connection_test.rb ├── errors_test.rb ├── executor_test.rb ├── field_test.rb ├── helper.rb ├── lazy_test.rb ├── number_test.rb ├── object_test.rb ├── parser_test.rb ├── readme_test.rb ├── registry_test.rb ├── scalar_test.rb ├── schema_test.rb ├── string_test.rb ├── tokenizer_test.rb └── version_test.rb └── fixtures └── example.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.env 3 | /.yardoc 4 | /.minitest-perf.db 5 | /Gemfile.lock 6 | /coverage/ 7 | /doc/ 8 | /example/.env 9 | /pkg/ 10 | /spec/reports/ 11 | /tmp/ 12 | /lib/gql/parser.output 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | rvm: 4 | - "2.2" 5 | - "2.1" 6 | - "2.0" 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in gql.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Martin Andert 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gql 2 | 3 | [![Build Status](https://travis-ci.org/martinandert/gql.svg?branch=master)](https://travis-ci.org/martinandert/gql) 4 | [![Code Climate](https://codeclimate.com/github/martinandert/gql/badges/gpa.svg)](https://codeclimate.com/github/martinandert/gql) 5 | [![Test Coverage](https://codeclimate.com/github/martinandert/gql/badges/coverage.svg)](https://codeclimate.com/github/martinandert/gql) 6 | [![Dependency Status](https://gemnasium.com/martinandert/gql.svg)](https://gemnasium.com/martinandert/gql) 7 | 8 | A Ruby implementation of Facebook's yet-to-be-released GraphQL specification. 9 | 10 | Visit the [live demo](http://gql-demo.andert.io/). The source code for it can be found in the [example directory](example/). 11 | 12 | **Disclaimer:** I can only speculate about how the final spec will look like. The implementation provided here is merely my guessing based on [this talk](https://youtu.be/9sc8Pyc51uU) and [this gist](https://gist.github.com/wincent/598fa75e22bdfa44cf47). Nonetheless, this project represents how I wish the official specification will define things. 13 | 14 | 15 | ## Installation 16 | 17 | Add this line to your application's Gemfile: 18 | 19 | ```ruby 20 | gem 'gql' 21 | ``` 22 | 23 | And then execute: 24 | 25 | ```sh 26 | $ bundle 27 | ``` 28 | 29 | Or install it yourself as: 30 | 31 | ``` 32 | $ gem install gql 33 | ``` 34 | 35 | 36 | ## Usage 37 | 38 | Usage instructions and documentation will be added when Facebook releases the official GraphQL specification. 39 | 40 | Until then, if you have questions or comments, open a ticket in GitHub's issues tracker for this project. 41 | 42 | In order to see how things are done and to explore this gem's features, I encourage you to study the code and tests. 43 | 44 | 45 | ## Example 46 | 47 | Apart from the more full-fledged live demo linked above, there's a simpler example available in [test/fixtures/example.rb](test/fixtures/example.rb). 48 | 49 | To play around with it, run `bin/console` from the project root. This starts an interactive prompt loaded with the example's models/data. 50 | 51 | In the prompt, copy and paste the following Ruby code to execute your first query: 52 | 53 | ```ruby 54 | puts query(<<-QUERY_STRING).to_json 55 | user() { 56 | id, 57 | is_admin, 58 | full_name as name, 59 | created_at { year, month } as created_year_and_month, 60 | created_at.format("long") as created, 61 | account { 62 | bank_name, 63 | iban, 64 | saldo as saldo_string, 65 | saldo { 66 | currency, 67 | cents /* silly block comment */ 68 | } 69 | }, 70 | albums.first(2) { 71 | count, 72 | edges { 73 | cursor, 74 | node { 75 | artist, 76 | title, 77 | songs.first(2) { 78 | edges { 79 | id, 80 | title.upcase as upcased_title, 81 | title.upcase.length as upcased_title_length 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | = "ma" // a variable 90 | QUERY_STRING 91 | ``` 92 | 93 | It all goes well, this should result in the following JSON (after prettyfication): 94 | 95 | ```json 96 | { 97 | "id": "ma", 98 | "is_admin": true, 99 | "name": "Martin Andert", 100 | "created_year_and_month": { 101 | "year": 2010, 102 | "month": 3 103 | }, 104 | "created": "March 05, 2010 20:14", 105 | "account": { 106 | "bank_name": "Foo Bank", 107 | "iban": "987654321", 108 | "saldo_string": "100000.00 EUR", 109 | "saldo": { 110 | "currency": "EUR", 111 | "cents": 10000000 112 | } 113 | }, 114 | "albums": { 115 | "count": 2, 116 | "edges": [ 117 | { 118 | "cursor": 1, 119 | "node": { 120 | "artist": "Metallica", 121 | "title": "Black Album", 122 | "songs": { 123 | "edges": [ 124 | { 125 | "id": 1, 126 | "upcased_title": "ENTER SANDMAN", 127 | "upcased_title_length": 13 128 | }, { 129 | "id": 2, 130 | "upcased_title": "SAD BUT TRUE", 131 | "upcased_title_length": 12 132 | } 133 | ] 134 | } 135 | } 136 | }, { 137 | "cursor": 2, 138 | "node": { 139 | "artist": "Nirvana", 140 | "title": "Nevermind", 141 | "songs": { 142 | "edges": [ 143 | { 144 | "id": 5, 145 | "upcased_title": "SMELLS LIKE TEEN SPIRIT", 146 | "upcased_title_length": 23 147 | }, { 148 | "id": 6, 149 | "upcased_title": "COME AS YOU ARE", 150 | "upcased_title_length": 15 151 | } 152 | ] 153 | } 154 | } 155 | } 156 | ] 157 | } 158 | } 159 | ``` 160 | 161 | 162 | ## Development 163 | 164 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment. 165 | 166 | 167 | ## Contributing 168 | 169 | 1. Fork it ( https://github.com/martinandert/gql/fork ) 170 | 2. Run `bin/setup` to install dependencies. 171 | 3. Run the tests. We only take pull requests with passing tests, and it's great to know that you have a clean slate: `bin/rake test`. 172 | 4. Create your feature branch (`git checkout -b my-new-feature`) 173 | 5. Add a test for your change. Only refactoring and documentation changes require no new tests. If you are adding functionality or are fixing a bug, we need a test! 174 | 6. Make the test pass. 175 | 7. Commit your changes (`git commit -am 'add some feature'`) 176 | 8. Push to your fork (`git push origin my-new-feature`) 177 | 9. Create a new Pull Request 178 | 179 | 180 | ## Note 181 | 182 | For an alternative Ruby implementation, check out rmosolgo's [graphql-ruby](https://github.com/rmosolgo/graphql-ruby). 183 | 184 | The initial work on my gem was inspired by his code. Since then, both repos have diverged significantly. 185 | 186 | 187 | ## License 188 | 189 | Released under The MIT License. 190 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | dir = File.dirname(__FILE__) 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rake/testtask' 5 | 6 | file 'lib/gql/tokenizer.rb' => 'support/tokenizer.rex' do |t| 7 | sh "bundle exec rex #{t.prerequisites.first} --output-file #{t.name}" 8 | 9 | # use custom scan error class 10 | sh "sed --in-place 's/class ScanError/class Unused/' #{t.name}" 11 | sh "sed --in-place 's/ScanError/GQL::Errors::ScanError/' #{t.name}" 12 | end 13 | 14 | file 'lib/gql/parser.rb' => 'support/parser.racc' do |t| 15 | if ENV['DEBUG'] 16 | sh "bundle exec racc --debug --verbose --output-file=#{t.name} #{t.prerequisites.first}" 17 | else 18 | sh "bundle exec racc --output-file=#{t.name} #{t.prerequisites.first}" 19 | end 20 | 21 | # fix indentation of generated parser code to silence test warning 22 | sh "sed --in-place 's/ end\s*# module/end #/g' #{t.name}" 23 | end 24 | 25 | Rake::TestTask.new :test do |t| 26 | t.libs << 'test' 27 | t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb") 28 | # t.warning = true 29 | # t.verbose = true 30 | end 31 | 32 | desc 'Compile tokenizer and parser' 33 | task :compile => ['lib/gql/tokenizer.rb', 'lib/gql/parser.rb'] 34 | 35 | task :test => :compile 36 | task :build => :test 37 | task :default => :test 38 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'gql' 5 | 6 | require_relative '../test/fixtures/example' 7 | 8 | def ctxt 9 | @ctxt ||= { auth_token: 'ma' } 10 | end 11 | 12 | def query(string, context = ctxt) 13 | GQL.execute string, context 14 | end 15 | 16 | def parse(string) 17 | GQL.parse string 18 | end 19 | 20 | def tokenize(string, &block) 21 | GQL.tokenize string, &block 22 | end 23 | 24 | require 'irb' 25 | IRB.start 26 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rake' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rake', 'rake') 17 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | -------------------------------------------------------------------------------- /example/Capfile: -------------------------------------------------------------------------------- 1 | require "capistrano/setup" 2 | require "capistrano/deploy" 3 | require "capistrano/yutiriti" 4 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby ENV['RUBY_VERSION'] || '2.0.0' 4 | 5 | gem 'rake' 6 | gem 'thin' 7 | gem 'sinatra' 8 | gem 'sinatra-contrib' 9 | gem 'sinatra-activerecord' 10 | gem 'activesupport' 11 | gem 'gql' 12 | 13 | group :development do 14 | gem 'sqlite3' 15 | end 16 | 17 | group :production do 18 | gem 'pg' 19 | end 20 | 21 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activemodel (4.2.0) 5 | activesupport (= 4.2.0) 6 | builder (~> 3.1) 7 | activerecord (4.2.0) 8 | activemodel (= 4.2.0) 9 | activesupport (= 4.2.0) 10 | arel (~> 6.0) 11 | activesupport (4.2.0) 12 | i18n (~> 0.7) 13 | json (~> 1.7, >= 1.7.7) 14 | minitest (~> 5.1) 15 | thread_safe (~> 0.3, >= 0.3.4) 16 | tzinfo (~> 1.1) 17 | arel (6.0.0) 18 | backports (3.6.4) 19 | builder (3.2.2) 20 | daemons (1.2.1) 21 | eventmachine (1.0.7) 22 | gql (0.0.22) 23 | activesupport (~> 4.0) 24 | multi_json (~> 1.0) 25 | i18n (0.7.0) 26 | json (1.8.2) 27 | minitest (5.5.1) 28 | multi_json (1.11.0) 29 | pg (0.18.1) 30 | rack (1.6.0) 31 | rack-protection (1.5.3) 32 | rack 33 | rack-test (0.6.3) 34 | rack (>= 1.0) 35 | rake (10.4.2) 36 | sinatra (1.4.5) 37 | rack (~> 1.4) 38 | rack-protection (~> 1.4) 39 | tilt (~> 1.3, >= 1.3.4) 40 | sinatra-activerecord (2.0.5) 41 | activerecord (>= 3.2) 42 | sinatra (~> 1.0) 43 | sinatra-contrib (1.4.2) 44 | backports (>= 2.0) 45 | multi_json 46 | rack-protection 47 | rack-test 48 | sinatra (~> 1.4.0) 49 | tilt (~> 1.3) 50 | sqlite3 (1.3.10) 51 | thin (1.6.3) 52 | daemons (~> 1.0, >= 1.0.9) 53 | eventmachine (~> 1.0) 54 | rack (~> 1.0) 55 | thread_safe (0.3.5) 56 | tilt (1.4.1) 57 | tzinfo (1.2.2) 58 | thread_safe (~> 0.1) 59 | 60 | PLATFORMS 61 | ruby 62 | 63 | DEPENDENCIES 64 | activesupport 65 | gql 66 | pg 67 | rake 68 | sinatra 69 | sinatra-activerecord 70 | sinatra-contrib 71 | sqlite3 72 | thin 73 | -------------------------------------------------------------------------------- /example/Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec thin -R config.ru start -p $PORT -e $RACK_ENV 2 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # gql demo application 2 | 3 | This directory contains a small web application that shows how to use the gql gem. 4 | 5 | Synopsis: 6 | 7 | * Sinatra as web framework 8 | * Sqlite3 as database (in development) 9 | * ActiveRecord as ORM (see `lib/app/models`) 10 | * GQL field and call definitions can be found under `lib/app/graph` 11 | 12 | Run `bin/console` for an interactive prompt (loaded with example models/data). 13 | 14 | To start the web application, run `bin/rackup`. 15 | 16 | Don't forget to set a `DATABASE_URL` environment variable. To use the pre-populated sqlite3 database, set this variable to `sqlite3:db/app.sqlite3`. 17 | 18 | To get access to the schema through the `__type__` field and to see debug output, set the `DEBUG` environment variable: 19 | 20 | ```sh 21 | $ DEBUG=1 bin/console 22 | $ DEBUG=1 bin/rackup 23 | ``` 24 | 25 | To see the running web application in action, visit http://gql-demo.herokuapp.com/ 26 | -------------------------------------------------------------------------------- /example/Rakefile: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../lib', __FILE__) 2 | 3 | require 'sinatra/activerecord/rake' 4 | 5 | namespace :db do 6 | task :load_config do 7 | require 'app' 8 | 9 | # needed for loading fixtures 10 | include App::Models 11 | 12 | # hook sinatra-activerecord registration 13 | App::Client 14 | end 15 | 16 | namespace :data do 17 | desc 'Reset all data in the db to fixtures state' 18 | task :reset => :load_config do 19 | [Album, Attribution, Band, Membership, MembershipRole, Person, Role, Song].map(&:delete_all) 20 | Rake::Task['db:fixtures:load'].invoke 21 | end 22 | end 23 | end 24 | 25 | desc 'Push app in example directory to Heroku' 26 | task :publish do 27 | sh 'cap production deploy deploy:restart' 28 | end 29 | -------------------------------------------------------------------------------- /example/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 4 | 5 | require 'bundler/setup' 6 | require 'app' 7 | 8 | ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) 9 | ActiveRecord::Base.establish_connection ENV['DATABASE_URL'] 10 | ActiveRecord::Base.configurations = { 'development' => ActiveRecord::Base.connection.pool.spec.config } 11 | 12 | include App::Models 13 | 14 | def q(*args) 15 | GQL.execute(*args) 16 | end 17 | 18 | require 'irb' 19 | IRB.start 20 | -------------------------------------------------------------------------------- /example/bin/rackup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rackup' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rack', 'rackup') 17 | -------------------------------------------------------------------------------- /example/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rake' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rake', 'rake') 17 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | 4 | Bundler.require 5 | 6 | $LOAD_PATH.unshift File.expand_path('../lib', __FILE__) 7 | 8 | require 'app' 9 | 10 | run App::Client.new 11 | -------------------------------------------------------------------------------- /example/config/deploy.rb: -------------------------------------------------------------------------------- 1 | lock "3.4.0" # valid only for current version of Capistrano 2 | 3 | set :application, "gql-demo" 4 | set :log_level, :debug 5 | set :keep_releases, 3 6 | 7 | set :repo_url, "git@github.com:martinandert/gql.git" 8 | set :repo_tree, "example" 9 | 10 | set :buildpack_url, "https://github.com/heroku/heroku-buildpack-ruby.git#v137" 11 | set :foreman_options, port: 3012, user: "deploy" 12 | set :default_env, stack: "cedar-14" 13 | -------------------------------------------------------------------------------- /example/config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | server "do1", roles: %w(web app db) 2 | -------------------------------------------------------------------------------- /example/db/app.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinandert/gql/9683838868f719def7e108deaa43472774d9a7ed/example/db/app.sqlite3 -------------------------------------------------------------------------------- /example/db/migrate/20150310163512_create_app.rb: -------------------------------------------------------------------------------- 1 | class CreateApp < ActiveRecord::Migration 2 | def change 3 | create_table :people do |t| 4 | t.string :slug, :null => false 5 | t.string :first_name, :null => false 6 | t.string :last_name, :null => false 7 | t.timestamps 8 | end 9 | 10 | add_index :people, :slug, :unique => true 11 | 12 | create_table :bands do |t| 13 | t.string :slug, :null => false 14 | t.string :name, :null => false 15 | t.timestamps 16 | end 17 | 18 | add_index :bands, :slug, :unique => true 19 | 20 | create_table :albums do |t| 21 | t.string :slug, :null => false 22 | t.references :band, :null => false 23 | t.string :title, :null => false 24 | t.date :released_on, :null => false 25 | t.timestamps 26 | end 27 | 28 | add_index :albums, :slug, :unique => true 29 | add_index :albums, :band_id 30 | add_index :albums, :released_on 31 | add_index :albums, [:band_id, :released_on] 32 | 33 | create_table :songs do |t| 34 | t.string :slug, :null => false 35 | t.references :album, :null => false 36 | t.string :title, :null => false 37 | t.integer :duration, :null => false 38 | t.integer :track_number, :null => false 39 | t.text :note 40 | t.timestamps 41 | end 42 | 43 | add_index :songs, :slug, :unique => true 44 | add_index :songs, :album_id 45 | add_index :songs, :track_number 46 | add_index :songs, [:album_id, :track_number], :unique => true 47 | 48 | create_table :memberships do |t| 49 | t.references :band, :null => false 50 | t.references :member, :null => false 51 | t.integer :started_year, :null => false 52 | t.integer :ended_year 53 | t.timestamps 54 | end 55 | 56 | add_index :memberships, [:band_id, :member_id], :unique => true 57 | 58 | create_table :roles do |t| 59 | t.string :slug, :null => false 60 | t.string :name, :null => false 61 | t.timestamps 62 | end 63 | 64 | add_index :roles, :slug, :unique => true 65 | add_index :roles, :name, :unique => true 66 | 67 | create_table :membership_roles do |t| 68 | t.references :membership, :null => false 69 | t.references :role, :null => false 70 | t.timestamps 71 | end 72 | 73 | add_index :membership_roles, [:membership_id, :role_id], :unique => true 74 | 75 | create_table :attributions do |t| 76 | t.references :song, :null => false 77 | t.references :writer, :null => false 78 | t.timestamps 79 | end 80 | 81 | add_index :attributions, [:song_id, :writer_id], :unique => true 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /example/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20150310163512) do 15 | 16 | create_table "albums", force: :cascade do |t| 17 | t.string "slug", null: false 18 | t.integer "band_id", null: false 19 | t.string "title", null: false 20 | t.date "released_on", null: false 21 | t.datetime "created_at" 22 | t.datetime "updated_at" 23 | end 24 | 25 | add_index "albums", ["band_id", "released_on"], name: "index_albums_on_band_id_and_released_on" 26 | add_index "albums", ["band_id"], name: "index_albums_on_band_id" 27 | add_index "albums", ["released_on"], name: "index_albums_on_released_on" 28 | add_index "albums", ["slug"], name: "index_albums_on_slug", unique: true 29 | 30 | create_table "attributions", force: :cascade do |t| 31 | t.integer "song_id", null: false 32 | t.integer "writer_id", null: false 33 | t.datetime "created_at" 34 | t.datetime "updated_at" 35 | end 36 | 37 | add_index "attributions", ["song_id", "writer_id"], name: "index_attributions_on_song_id_and_writer_id", unique: true 38 | 39 | create_table "bands", force: :cascade do |t| 40 | t.string "slug", null: false 41 | t.string "name", null: false 42 | t.datetime "created_at" 43 | t.datetime "updated_at" 44 | end 45 | 46 | add_index "bands", ["slug"], name: "index_bands_on_slug", unique: true 47 | 48 | create_table "membership_roles", force: :cascade do |t| 49 | t.integer "membership_id", null: false 50 | t.integer "role_id", null: false 51 | t.datetime "created_at" 52 | t.datetime "updated_at" 53 | end 54 | 55 | add_index "membership_roles", ["membership_id", "role_id"], name: "index_membership_roles_on_membership_id_and_role_id", unique: true 56 | 57 | create_table "memberships", force: :cascade do |t| 58 | t.integer "band_id", null: false 59 | t.integer "member_id", null: false 60 | t.integer "started_year", null: false 61 | t.integer "ended_year" 62 | t.datetime "created_at" 63 | t.datetime "updated_at" 64 | end 65 | 66 | add_index "memberships", ["band_id", "member_id"], name: "index_memberships_on_band_id_and_member_id", unique: true 67 | 68 | create_table "people", force: :cascade do |t| 69 | t.string "slug", null: false 70 | t.string "first_name", null: false 71 | t.string "last_name", null: false 72 | t.datetime "created_at" 73 | t.datetime "updated_at" 74 | end 75 | 76 | add_index "people", ["slug"], name: "index_people_on_slug", unique: true 77 | 78 | create_table "roles", force: :cascade do |t| 79 | t.string "slug", null: false 80 | t.string "name", null: false 81 | t.datetime "created_at" 82 | t.datetime "updated_at" 83 | end 84 | 85 | add_index "roles", ["name"], name: "index_roles_on_name", unique: true 86 | add_index "roles", ["slug"], name: "index_roles_on_slug", unique: true 87 | 88 | create_table "songs", force: :cascade do |t| 89 | t.string "slug", null: false 90 | t.integer "album_id", null: false 91 | t.string "title", null: false 92 | t.integer "duration", null: false 93 | t.integer "track_number", null: false 94 | t.text "note" 95 | t.datetime "created_at" 96 | t.datetime "updated_at" 97 | end 98 | 99 | add_index "songs", ["album_id", "track_number"], name: "index_songs_on_album_id_and_track_number", unique: true 100 | add_index "songs", ["album_id"], name: "index_songs_on_album_id" 101 | add_index "songs", ["slug"], name: "index_songs_on_slug", unique: true 102 | add_index "songs", ["track_number"], name: "index_songs_on_track_number" 103 | 104 | end 105 | -------------------------------------------------------------------------------- /example/lib/app.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'active_record' 3 | require 'gql' 4 | 5 | module App 6 | extend ActiveSupport::Autoload 7 | 8 | autoload :Client 9 | autoload :Helper 10 | 11 | module Models 12 | extend ActiveSupport::Autoload 13 | 14 | autoload :Album 15 | autoload :Attribution 16 | autoload :Band 17 | autoload :Membership 18 | autoload :MembershipRole 19 | autoload :Person 20 | autoload :Role 21 | autoload :Song 22 | 23 | module Concerns 24 | extend ActiveSupport::Autoload 25 | 26 | autoload :HasSlug 27 | end 28 | end 29 | 30 | module Graph 31 | extend ActiveSupport::Autoload 32 | 33 | autoload :AlbumField 34 | autoload :BandField 35 | autoload :DateField 36 | autoload :DurationField 37 | autoload :ListField 38 | autoload :MembershipField 39 | autoload :ModelField 40 | autoload :PersonField 41 | autoload :RoleField 42 | autoload :RootField 43 | autoload :SongField 44 | autoload :YouField 45 | 46 | autoload :CreateBandMembershipCall 47 | 48 | GQL.field_types.update date: DateField, duration: DurationField 49 | GQL.default_list_class = ListField 50 | GQL.root_class = RootField 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /example/lib/app/client.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'sinatra/reloader' 3 | require 'sinatra/activerecord' 4 | require 'gql' 5 | require 'json' 6 | 7 | module App 8 | class Client < Sinatra::Base 9 | register Sinatra::ActiveRecordExtension 10 | enable :sessions 11 | 12 | configure :development do 13 | $stdout.sync = true 14 | register Sinatra::Reloader 15 | ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) 16 | end 17 | 18 | get '/' do 19 | view = request.user_agent =~ /mobile/i ? :mobile : :desktop 20 | 21 | erb view, locals: { 22 | queries: JSON.generate(Helper.queries), 23 | initial_query: JSON.generate([Helper.initial_query]) 24 | } 25 | end 26 | 27 | post '/query' do 28 | content_type :json 29 | 30 | context = { ip_address: request.ip, queries_count: queries_count } 31 | 32 | result = 33 | begin 34 | GQL.execute params[:q], context 35 | rescue => exc 36 | Helper.error_as_json exc 37 | end 38 | 39 | result = [result] unless result.respond_to?(:each) 40 | 41 | JSON.pretty_generate result 42 | end 43 | 44 | get '/reset-data' do 45 | `bin/rake db:data:reset` if params[:s] == ENV['DATA_RESET_SECRET'] 46 | content_type :text 47 | 'ok' 48 | end 49 | 50 | helpers do 51 | def queries_count 52 | session[:queries_count] ||= 0 53 | session[:queries_count] += 1 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /example/lib/app/graph/album_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class AlbumField < ModelField 4 | string :title 5 | object :band, object_class: BandField 6 | connection :songs, item_class: SongField 7 | date :released_on 8 | 9 | string :band_name, -> { target.band.name } 10 | number :songs_count, -> { target.songs.count } 11 | duration :duration, -> { target.songs.pluck(:duration).reduce(0, &:+) } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /example/lib/app/graph/band_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class BandField < ModelField 4 | connection :memberships, item_class: MembershipField 5 | connection :members, item_class: PersonField 6 | connection :albums, item_class: AlbumField 7 | connection :songs, item_class: SongField 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /example/lib/app/graph/create_band_membership_call.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class CreateBandMembershipCall < GQL::Call 4 | returns MembershipField 5 | 6 | def execute(person_id_or_slug, band_id_or_slug, started_year, ended_year = nil, role_ids_or_slugs = []) 7 | attributes = { 8 | band: Models::Band[band_id_or_slug], 9 | member: Models::Person[person_id_or_slug], 10 | started_year: started_year, 11 | ended_year: ended_year, 12 | roles: role_ids_or_slugs.map { |r| Models::Role[r] } 13 | } 14 | 15 | Models::Membership.create! attributes 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /example/lib/app/graph/date_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class DateField < GQL::Field 4 | call :format, returns: GQL::String do |format = 'default'| 5 | I18n.localize target, format: format.to_sym 6 | end 7 | 8 | number :year 9 | number :month 10 | number :day 11 | 12 | def scalar_value 13 | target.to_s :db 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /example/lib/app/graph/duration_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class DurationField < GQL::Number 4 | call :human, returns: GQL::String do 5 | if target <= 0 6 | "0:00" 7 | else 8 | min = target / 60 9 | sec = target - min * 60 10 | 11 | "#{min}:#{sec.to_s.rjust(2, '0')}" 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /example/lib/app/graph/list_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class ListField < GQL::Field 4 | MODEL_TO_FIELD_MAPPING = { 5 | Models::Person => 'App::Graph::PersonField', 6 | Models::Band => 'App::Graph::BandField', 7 | Models::Album => 'App::Graph::AlbumField', 8 | Models::Song => 'App::Graph::SongField', 9 | Models::Role => 'App::Graph::RoleField', 10 | Models::Membership => 'App::Graph::MembershipField' 11 | }.freeze 12 | 13 | number :count 14 | boolean :any, -> { target.any? } 15 | 16 | call :skip, -> size { target.offset(size) } 17 | call :take, -> size { target.limit(size) } 18 | 19 | # proc given here b/c we want no arguments (-> just a single record) and raise! if not found 20 | call :first, -> { target.first! }, returns: MODEL_TO_FIELD_MAPPING 21 | call :last, -> { target.last! }, returns: MODEL_TO_FIELD_MAPPING 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /example/lib/app/graph/membership_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class MembershipField < GQL::Field 4 | cursor :id 5 | 6 | number :id 7 | string :type, -> { target.class.name.split('::').last.downcase } 8 | object :band, object_class: BandField 9 | object :member, object_class: PersonField 10 | number :started_year 11 | number :ended_year 12 | 13 | connection :roles, -> { target.roles }, item_class: RoleField 14 | 15 | string :band_name, -> { target.band.name } 16 | string :member_name, -> { target.member.name } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /example/lib/app/graph/model_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class ModelField < GQL::Field 4 | cursor :slug 5 | 6 | number :id 7 | string :slug 8 | string :name 9 | 10 | string :type, -> { target.class.name.split('::').last.downcase } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /example/lib/app/graph/person_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class PersonField < ModelField 4 | string :first_name 5 | string :last_name 6 | 7 | connection :memberships, item_class: MembershipField 8 | connection :bands_as_member, item_class: BandField 9 | connection :songs_as_writer, item_class: SongField 10 | connection :roles_in_bands, item_class: RoleField 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /example/lib/app/graph/role_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class RoleField < ModelField 4 | connection :members, item_class: PersonField 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /example/lib/app/graph/root_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class RootField < GQL::Field 4 | connection :people, -> { Models::Person.all }, item_class: PersonField 5 | connection :bands, -> { Models::Band.all }, item_class: BandField 6 | connection :albums, -> { Models::Album.all }, item_class: AlbumField 7 | connection :songs, -> { Models::Song.all }, item_class: SongField 8 | connection :roles, -> { Models::Role.all }, item_class: RoleField 9 | 10 | call :person, -> id_or_slug { Models::Person[id_or_slug] }, returns: PersonField 11 | call :band, -> id_or_slug { Models::Band[id_or_slug] }, returns: BandField 12 | call :album, -> id_or_slug { Models::Album[id_or_slug] }, returns: AlbumField 13 | call :song, -> id_or_slug { Models::Song[id_or_slug] }, returns: SongField 14 | call :role, -> id_or_slug { Models::Role[id_or_slug] }, returns: RoleField 15 | 16 | def self.create_proc_for(model) 17 | -> attributes { model.create! attributes } 18 | end 19 | 20 | call :create_person, create_proc_for(Models::Person), returns: PersonField 21 | call :create_band, create_proc_for(Models::Band), returns: BandField 22 | call :create_album, create_proc_for(Models::Album), returns: AlbumField 23 | call :create_song, create_proc_for(Models::Song), returns: SongField 24 | call :create_role, create_proc_for(Models::Role), returns: RoleField 25 | 26 | call :assign_person_to_band, 'App::Graph::CreateBandMembershipCall' 27 | 28 | call :you, -> { context }, returns: YouField 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /example/lib/app/graph/song_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class SongField < ModelField 4 | string :title 5 | object :album, object_class: AlbumField 6 | object :band, object_class: BandField 7 | connection :writers, item_class: PersonField 8 | number :track_number 9 | duration :duration 10 | string :note 11 | 12 | string :album_title, -> { target.album.name } 13 | string :band_name, -> { target.band.name } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /example/lib/app/graph/you_field.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Graph 3 | class YouField < GQL::Field 4 | self.field_proc = -> id { -> { target[id] } } 5 | 6 | string :ip_address 7 | number :queries_count 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /example/lib/app/helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'gql' 3 | 4 | module App 5 | module Helper 6 | def error_as_json(exc) 7 | case exc 8 | when GQL::Error 9 | exc.as_json 10 | when ActiveRecord::RecordNotFound 11 | generic_error_as_json exc, 404 12 | when ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotDestroyed 13 | info = 14 | if exc.record && exc.record.errors.any? 15 | { failed_validations: exc.record.errors.full_messages } 16 | else 17 | {} 18 | end 19 | 20 | generic_error_as_json exc, 422, info 21 | else 22 | generic_error_as_json exc 23 | end 24 | end 25 | 26 | def generic_error_as_json(exc, code = 900, info = {}) 27 | result = { 28 | error: { 29 | code: code, 30 | type: exc.class.name.split('::').last.titleize.downcase 31 | }.merge(info) 32 | } 33 | 34 | result[:error][:message] = exc.message if ENV['DEBUG'] 35 | result 36 | end 37 | 38 | def initial_query 39 | <<-QUERY.strip_heredoc 40 | person("kurtcobain") { 41 | id, 42 | first_name, 43 | last_name, 44 | memberships as band_memberships { 45 | edges { 46 | band_name, 47 | started_year, 48 | ended_year 49 | } 50 | }, 51 | roles_in_bands { 52 | edges { name } 53 | } 54 | } 55 | QUERY 56 | end 57 | 58 | def queries 59 | [{ 60 | name: 'Details on the first 10 songs', 61 | value: <<-QUERY.strip_heredoc 62 | { 63 | songs.take(10) { 64 | count, 65 | edges { 66 | id, 67 | slug, 68 | type, 69 | band_name, 70 | title, 71 | duration.human as length, 72 | album_title, 73 | track_number, 74 | note, 75 | writers as written_by { edges { name } } 76 | } 77 | } 78 | } 79 | QUERY 80 | }, { 81 | name: 'Smells Like Teen Spirit', 82 | value: <<-QUERY.strip_heredoc 83 | song("nirvana-smells-like-teen-spirit") { 84 | id, 85 | title, 86 | duration.human as length, 87 | note, 88 | band { 89 | id, 90 | name, 91 | memberships { 92 | edges { 93 | member { 94 | id, 95 | name, 96 | roles_in_bands { 97 | edges { 98 | id, 99 | name 100 | } 101 | } 102 | }, 103 | started_year, 104 | ended_year 105 | } 106 | } 107 | }, 108 | writers as written_by { 109 | edges { 110 | id, 111 | name 112 | } 113 | }, 114 | album { 115 | id, 116 | title, 117 | released_on.format("long"), 118 | duration.human as length, 119 | songs { 120 | count, 121 | edges { 122 | id, 123 | title, 124 | track_number, 125 | writers { edges { name } } 126 | } 127 | } 128 | }, 129 | track_number 130 | } 131 | QUERY 132 | }, { 133 | name: 'Counts for each model', 134 | value: <<-QUERY.strip_heredoc 135 | { 136 | people { count }, 137 | bands { count }, 138 | albums { count }, 139 | songs { count }, 140 | roles { count } 141 | } 142 | QUERY 143 | }, { 144 | name: 'Details on all albums', 145 | value: <<-QUERY.strip_heredoc 146 | { 147 | albums { 148 | count, 149 | edges { 150 | id, 151 | band_name as artist, 152 | title, 153 | songs_count as tracks, 154 | duration.human as length, 155 | released_on.format("long") 156 | } 157 | } 158 | } 159 | QUERY 160 | }, { 161 | name: 'People by roles in bands', 162 | value: <<-QUERY.strip_heredoc 163 | { 164 | roles { 165 | edges { 166 | id, 167 | name, 168 | members { 169 | edges { 170 | id, 171 | name 172 | } 173 | } 174 | } 175 | } 176 | } 177 | QUERY 178 | }, { 179 | name: 'Add a new person', 180 | value: <<-QUERY.strip_heredoc 181 | create_person() { 182 | id, 183 | name 184 | } 185 | 186 | // a variable used as call argument 187 | = { 188 | "slug": "eddievedder", 189 | "first_name": "Eddie", 190 | "last_name": "Vedder" 191 | } 192 | QUERY 193 | }, { 194 | name: 'Assign a person to a band', 195 | value: <<-QUERY.strip_heredoc 196 | assign_person_to_band(, , , , ) { 197 | member { 198 | id, 199 | name 200 | }, 201 | band { 202 | id, 203 | name 204 | }, 205 | started_year, 206 | ended_year, 207 | roles { 208 | edges { 209 | name 210 | } 211 | } 212 | } 213 | 214 | // arguments for the call 215 | = "eddievedder" 216 | = "pearljam" 217 | = 1990 218 | = null 219 | = ["lead-vocals", "guitar"] 220 | QUERY 221 | }, { 222 | name: 'Schema: All root calls', 223 | value: <<-QUERY.strip_heredoc 224 | { 225 | __type__ { 226 | calls { 227 | edges { 228 | id, 229 | parameters, 230 | result_class 231 | } 232 | } 233 | } 234 | } 235 | QUERY 236 | }, { 237 | name: 'Schema: All root fields', 238 | value: <<-QUERY.strip_heredoc 239 | { 240 | __type__ { 241 | fields { 242 | edges { 243 | id 244 | } 245 | } 246 | } 247 | } 248 | QUERY 249 | }, { 250 | name: 'Schema info (first three levels)', 251 | value: <<-QUERY.strip_heredoc 252 | { 253 | __type__ { 254 | name, 255 | calls { 256 | count, 257 | edges { 258 | id, 259 | parameters, 260 | result_class { 261 | name, 262 | calls { count }, 263 | fields { count } 264 | } 265 | } 266 | }, 267 | fields { 268 | count, 269 | edges { 270 | id, 271 | name, 272 | calls { 273 | count, 274 | edges { 275 | id 276 | } 277 | }, 278 | fields { 279 | count, 280 | edges { 281 | id, 282 | name, 283 | fields { 284 | edges { 285 | id, 286 | name 287 | } 288 | } 289 | } 290 | } 291 | } 292 | } 293 | } 294 | } 295 | QUERY 296 | }, { 297 | name: "Syntax error", 298 | value: <<-QUERY.strip_heredoc 299 | 300 | 301 | foo() { 302 | bar, 303 | baz, // <-- bad comma 304 | } 305 | 306 | QUERY 307 | }, { 308 | name: "Unknown field", 309 | value: <<-QUERY.strip_heredoc 310 | person("kurtcobain") { 311 | first_name, 312 | zodiac_sign // <-- undefined field 313 | } 314 | QUERY 315 | }, { 316 | name: "Unknown call", 317 | value: <<-QUERY.strip_heredoc 318 | person("kurtcobain") { 319 | first_name.reverse // <-- undefined call 320 | } 321 | QUERY 322 | }, { 323 | name: 'You', 324 | value: <<-QUERY.strip_heredoc 325 | /* 326 | * shows context usage 327 | */ 328 | 329 | you { 330 | ip_address, 331 | queries_count 332 | } 333 | QUERY 334 | }] 335 | end 336 | 337 | extend self 338 | end 339 | end 340 | -------------------------------------------------------------------------------- /example/lib/app/models/album.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Models 3 | class Album < ActiveRecord::Base 4 | include Concerns::HasSlug 5 | 6 | belongs_to :band 7 | 8 | has_many :songs 9 | 10 | validates :band, :title, :released_on, :presence => true 11 | 12 | default_scope { order(:released_on) } 13 | 14 | alias_attribute :name, :title 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /example/lib/app/models/attribution.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Models 3 | class Attribution < ActiveRecord::Base 4 | belongs_to :song 5 | belongs_to :writer, :class_name => 'Person' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /example/lib/app/models/band.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Models 3 | class Band < ActiveRecord::Base 4 | include Concerns::HasSlug 5 | 6 | has_many :memberships 7 | has_many :members, :through => :memberships 8 | 9 | has_many :albums 10 | has_many :songs, :through => :albums 11 | 12 | validates :name, :presence => true 13 | 14 | default_scope { order(:name) } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /example/lib/app/models/concerns/has_slug.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Models 3 | module Concerns 4 | module HasSlug 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | validates :slug, 9 | presence: true, 10 | format: /\A[a-z][a-z0-9\-]*[a-z0-9]\z/, 11 | uniqueness: { case_sensitive: false } 12 | end 13 | 14 | module ClassMethods 15 | def [](value) 16 | value = value.to_s 17 | column = value =~ /\A\d+\z/ ? :id : :slug 18 | 19 | where(column => value).take! 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /example/lib/app/models/membership.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Models 3 | class Membership < ActiveRecord::Base 4 | belongs_to :band 5 | belongs_to :member, :class_name => 'Person' 6 | 7 | has_many :membership_roles 8 | has_many :roles, :through => :membership_roles 9 | 10 | validates :band, :member, :started_year, presence: true 11 | validates :started_year, :ended_year, numericality: { only_integer: true, greater_than: 1900, allow_blank: true } 12 | validates :member, uniqueness: { scope: :band, allow_blank: true } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example/lib/app/models/membership_role.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Models 3 | class MembershipRole < ActiveRecord::Base 4 | belongs_to :membership 5 | belongs_to :role 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /example/lib/app/models/person.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Models 3 | class Person < ActiveRecord::Base 4 | include Concerns::HasSlug 5 | 6 | has_many :memberships, :foreign_key => 'member_id' 7 | has_many :bands_as_member, :through => :memberships, :source => :band 8 | has_many :membership_roles, :through => :memberships 9 | has_many :roles_in_bands, :through => :membership_roles, :source => :role 10 | 11 | has_many :attributions, :foreign_key => 'writer_id' 12 | has_many :songs_as_writer, :through => :attributions, :source => :song 13 | 14 | validates :first_name, :last_name, :presence => true 15 | 16 | default_scope { order(:last_name, :first_name) } 17 | 18 | def name 19 | "#{first_name} #{last_name}" 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /example/lib/app/models/role.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Models 3 | class Role < ActiveRecord::Base 4 | include Concerns::HasSlug 5 | 6 | has_many :membership_roles 7 | has_many :memberships, :through => :membership_roles 8 | has_many :members, :through => :memberships 9 | 10 | validates :name, :presence => true, :uniqueness => true 11 | 12 | default_scope { order(:name) } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example/lib/app/models/song.rb: -------------------------------------------------------------------------------- 1 | module App 2 | module Models 3 | class Song < ActiveRecord::Base 4 | include Concerns::HasSlug 5 | 6 | belongs_to :album 7 | 8 | has_many :attributions 9 | has_many :writers, :through => :attributions, :class_name => 'Person' 10 | 11 | validates :album, :title, :duration, :track_number, :presence => true 12 | validates :duration, :track_number, :numericality => { :only_integer => true, :greater_than => 0 }, :allow_blank => true 13 | validates :track_number, :uniqueness => { :scope => :album }, :allow_blank => true 14 | 15 | default_scope { order(:track_number) } 16 | 17 | alias_attribute :name, :title 18 | 19 | def band 20 | album.band 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /example/lib/app/public/desktop.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | background-color: #f5f5f5; 9 | font-family: sans-serif; 10 | font-size: 16px; 11 | position: relative; 12 | } 13 | 14 | h1 { 15 | background-color: #666; 16 | color: #eee; 17 | margin: 0 0 25px; 18 | padding: 10px 25px; 19 | font-size: 20px; 20 | line-height: 1; 21 | } 22 | 23 | #github-link { 24 | position: absolute; 25 | right: 25px; 26 | top: 8px; 27 | } 28 | 29 | #github-link img { 30 | width: 26px; 31 | height: 26px; 32 | border: 0 none; 33 | } 34 | 35 | .panels { 36 | padding: 0 25px; 37 | } 38 | 39 | .panels > div { 40 | float: left; 41 | text-align: center; 42 | } 43 | 44 | .left-panel, .right-panel { 45 | width: 48%; 46 | } 47 | 48 | .left-panel { 49 | padding-right: 5px; 50 | } 51 | 52 | .right-panel { 53 | padding-left: 5px; 54 | } 55 | 56 | .center-panel { 57 | width: 4%; 58 | } 59 | 60 | h2 { 61 | margin: 0 0 2px; 62 | font-size: 16px; 63 | color: #666; 64 | } 65 | 66 | .ace_editor { 67 | width: 100%; 68 | height: 300px; 69 | border: solid 1px #999; 70 | margin-bottom: 10px; 71 | text-align: left; 72 | } 73 | 74 | button { 75 | width: 95%; 76 | font-weight: bold; 77 | height: 300px; 78 | font-size: 50px; 79 | border: solid 1px #2e6da4; 80 | background-color: #337ab7; 81 | color: #fff; 82 | cursor: pointer; 83 | } 84 | 85 | select { 86 | font-size: 16px; 87 | width: 400px; 88 | } 89 | 90 | button:hover { 91 | background-color: #286090; 92 | border-color: #204d74; 93 | } 94 | 95 | .reset-note { 96 | font-size: 80%; 97 | opacity: 0.8; 98 | } 99 | 100 | .ace_hidden-cursors .ace_cursor { 101 | opacity: 0 !important; 102 | } 103 | -------------------------------------------------------------------------------- /example/lib/app/public/desktop.js: -------------------------------------------------------------------------------- 1 | define('ace/mode/graphql', function(require, exports, module) { 2 | var oop = require("ace/lib/oop"); 3 | var TextMode = require("ace/mode/text").Mode; 4 | var Tokenizer = require("ace/tokenizer").Tokenizer; 5 | var GraphQLHighlightRules = require("ace/mode/graphql_highlight_rules").GraphQLHighlightRules; 6 | var MatchingBraceOutdent = require("ace/mode/matching_brace_outdent").MatchingBraceOutdent; 7 | var CstyleBehaviour = require("ace/mode/behaviour/cstyle").CstyleBehaviour; 8 | var CStyleFoldMode = require("ace/mode/folding/cstyle").FoldMode; 9 | 10 | var Mode = function() { 11 | this.$tokenizer = new Tokenizer(new GraphQLHighlightRules().getRules()); 12 | this.$outdent = new MatchingBraceOutdent(); 13 | this.$behaviour = new CstyleBehaviour(); 14 | this.foldingRules = new CStyleFoldMode(); 15 | }; 16 | 17 | oop.inherits(Mode, TextMode); 18 | 19 | (function() { 20 | this.getNextLineIndent = function(state, line, tab) { 21 | var indent = this.$getIndent(line); 22 | 23 | if (state == "start") { 24 | var match = line.match(/^.*[\{\(\[]\s*$/); 25 | 26 | if (match) { 27 | indent += tab; 28 | } 29 | } 30 | 31 | return indent; 32 | }; 33 | 34 | this.checkOutdent = function(state, line, input) { 35 | return this.$outdent.checkOutdent(line, input); 36 | }; 37 | 38 | this.autoOutdent = function(state, doc, row) { 39 | this.$outdent.autoOutdent(doc, row); 40 | }; 41 | }).call(Mode.prototype); 42 | 43 | exports.Mode = Mode; 44 | }); 45 | 46 | define('ace/mode/graphql_highlight_rules', function(require, exports, module) { 47 | "use strict"; 48 | 49 | var oop = require("../lib/oop"); 50 | var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; 51 | 52 | var GraphQLHighlightRules = function() { 53 | this.$rules = { 54 | "start": [ 55 | { 56 | token: "comment.block", 57 | regex: "\\/\\*", 58 | next: "rems" 59 | }, { 60 | token: "comment.line", 61 | regex: "\\/\\/", 62 | next: "rem" 63 | }, { 64 | token: "string", // single line 65 | regex: '"', 66 | next: "string" 67 | }, { 68 | token: "invalid.illegal", // single quoted strings are not allowed 69 | regex: "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']" 70 | }, { 71 | token: "constant.numeric", 72 | regex: "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b" 73 | }, { 74 | token: "constant.language.boolean", 75 | regex: "(?:true|false)\\b" 76 | }, { 77 | token: "constant.language", 78 | regex: "(?:null)\\b" 79 | }, { 80 | token: "keyword.operator", 81 | regex: "(?:[aA][sS]|=)\\b" 82 | }, { 83 | token: "identifier", 84 | regex: "[a-zA-Z_][a-zA-Z0-9_]*" 85 | }, { 86 | token: "paren.lparen", 87 | regex: "[[({]" 88 | }, { 89 | token: "paren.rparen", 90 | regex: "[\\])}]" 91 | }, { 92 | token: "punctuation.operator", 93 | regex: "[\\.,]" 94 | }, { 95 | token: "text", 96 | regex: "\\s+" 97 | }, { 98 | token: "invalid.illegal", 99 | regex: "\\." 100 | } 101 | ], 102 | 103 | "rems": [ 104 | { 105 | token: "comment.block", 106 | regex: "\\*\\/", 107 | next: "start" 108 | }, { 109 | token: "comment.block", 110 | regex: ".*(?=\\*\\/)" 111 | }, { 112 | token: "comment.block", 113 | regex: ".+(?=$|\\n)" 114 | }, { 115 | token: "comment.block", 116 | regex: "$|\\n" 117 | } 118 | ], 119 | 120 | "rem": [ 121 | { 122 | token: "comment.line", 123 | regex: "$|\\n", 124 | next: "start" 125 | }, { 126 | token: "comment.line", 127 | regex: ".*(?=$|\\n)" 128 | } 129 | ], 130 | 131 | "string": [ 132 | { 133 | token: "constant.language.escape", 134 | regex: /\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|["\\\/bfnrt])/ 135 | }, { 136 | token: "string", 137 | regex: '[^"\\\\]+' 138 | }, { 139 | token: "string", 140 | regex: '"', 141 | next: "start" 142 | }, { 143 | token: "string", 144 | regex: "", 145 | next: "start" 146 | } 147 | ] 148 | }; 149 | }; 150 | 151 | oop.inherits(GraphQLHighlightRules, TextHighlightRules); 152 | exports.GraphQLHighlightRules = GraphQLHighlightRules; 153 | }); 154 | 155 | function makeid(length) { 156 | length = length || 16; 157 | 158 | var id = ''; 159 | var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 160 | 161 | for (var i = 0; i < length; i++) { 162 | id += possible.charAt(Math.floor(Math.random() * possible.length)); 163 | } 164 | 165 | return id; 166 | } 167 | 168 | var Editor = React.createClass({ 169 | getDefaultProps: function() { 170 | return { 171 | mode: 'text', 172 | value: '', 173 | readOnly: false, 174 | showGutter: false, 175 | highlightActiveLine: false, 176 | fontSize: 16, 177 | onChange: null 178 | }; 179 | }, 180 | 181 | getEditorId: function() { 182 | this.editorId = this.editorId || makeid(); 183 | return this.editorId; 184 | }, 185 | 186 | componentDidMount: function() { 187 | this.editor = ace.edit(this.getEditorId()); 188 | this.editor.getSession().setMode('ace/mode/' + this.props.mode); 189 | this.editor.getSession().setTabSize(2); 190 | this.editor.setTheme('ace/theme/github'); 191 | this.editor.setShowPrintMargin(false); 192 | this.editor.setFontSize(this.props.fontSize); 193 | this.editor.setReadOnly(this.props.readOnly); 194 | this.editor.setHighlightActiveLine(this.props.highlightActiveLine); 195 | this.editor.renderer.setShowGutter(this.props.showGutter); 196 | this.editor.on('change', this.handleChanged); 197 | this.editor.setValue(this.props.value); 198 | this.editor.selection.selectFileStart(); 199 | 200 | $(React.findDOMNode(this)).data('ace', this.editor); 201 | }, 202 | 203 | componentWillReceiveProps: function(nextProps) { 204 | this.editor.getSession().setMode('ace/mode/' + nextProps.mode); 205 | this.editor.setFontSize(nextProps.fontSize); 206 | this.editor.setReadOnly(nextProps.readOnly); 207 | this.editor.setHighlightActiveLine(nextProps.highlightActiveLine); 208 | this.editor.renderer.setShowGutter(nextProps.showGutter); 209 | 210 | if (this.editor.getValue() !== nextProps.value) { 211 | this.editor.setValue(nextProps.value); 212 | this.editor.selection.selectFileStart(); 213 | } 214 | }, 215 | 216 | componentWillUnmount: function() { 217 | this.editor.destroy(); 218 | }, 219 | 220 | render: function() { 221 | return ( 222 |
223 | ); 224 | }, 225 | 226 | handleChanged: function() { 227 | var value = this.editor.getValue(); 228 | 229 | if (this.props.onChange) { 230 | this.props.onChange(value); 231 | } 232 | } 233 | }); 234 | 235 | var QuerySelector = React.createClass({ 236 | render: function() { 237 | var options = this.props.queries.map(function(query, i) { 238 | return ; 239 | }); 240 | 241 | return ( 242 |
243 | 244 | 245 | 249 |
250 | ); 251 | }, 252 | 253 | handleSelected: function(e) { 254 | if (e.target.value.length) { 255 | this.props.querySelected(e.target.value); 256 | } 257 | } 258 | }); 259 | 260 | var App = React.createClass({ 261 | getInitialState: function() { 262 | return { 263 | query: initialQuery, 264 | result: '' 265 | }; 266 | }, 267 | 268 | componentDidMount: function() { 269 | $(window).resize(this.adjustControlHeights).trigger('resize'); 270 | }, 271 | 272 | render: function() { 273 | return ( 274 |
275 |

GQL Demo Application

276 | 277 |
278 |
279 |

Query

280 | 281 | 282 |
283 | 284 |
285 |

 

286 | 287 |
288 | 289 |
290 |

Result

291 | 292 |

All data will be reset daily at 08:00 UTC.

293 |
294 |
295 |
296 | ); 297 | }, 298 | 299 | handleSubmit: function(e) { 300 | var self = this; 301 | 302 | $.post('/query', { q: this.state.query }, function(data) { 303 | self.setState({ result: data }); 304 | }, 'text'); 305 | 306 | e.preventDefault(); 307 | }, 308 | 309 | handleQueryChanged: function(queryString) { 310 | this.setState({ query: queryString }); 311 | }, 312 | 313 | adjustControlHeights: function() { 314 | var preferredHeight = $(window).height() - 200; 315 | 316 | $('.ace_editor, .execute').height(preferredHeight); 317 | 318 | $('.ace_editor').each(function() { 319 | $(this).data('ace').resize(); 320 | }); 321 | } 322 | }); 323 | 324 | React.render(, document.getElementById('root')); 325 | -------------------------------------------------------------------------------- /example/lib/app/public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinandert/gql/9683838868f719def7e108deaa43472774d9a7ed/example/lib/app/public/github.png -------------------------------------------------------------------------------- /example/lib/app/public/mobile.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | background-color: #f5f5f5; 9 | font-family: sans-serif; 10 | font-size: 16px; 11 | position: relative; 12 | } 13 | 14 | h1 { 15 | background-color: #666; 16 | color: #eee; 17 | margin: 0 0 25px; 18 | padding: 10px 15px; 19 | font-size: 20px; 20 | line-height: 1; 21 | } 22 | 23 | #github-link { 24 | position: absolute; 25 | right: 15px; 26 | top: 10px; 27 | } 28 | 29 | #github-link img { 30 | width: 20px; 31 | height: 20px; 32 | border: 0 none; 33 | } 34 | 35 | .controls { 36 | padding: 0 15px; 37 | } 38 | 39 | select, textarea, button { 40 | display: block; 41 | width: 100%; 42 | margin: 0 0 10px; 43 | font-size: 14px; 44 | } 45 | 46 | textarea { 47 | height: 250px; 48 | font-family: monospace; 49 | white-space: pre; 50 | word-wrap: normal; 51 | overflow-x: scroll; 52 | } 53 | 54 | button { 55 | font-weight: bold; 56 | font-size: 14px; 57 | } 58 | 59 | pre { 60 | overflow: auto; 61 | background-color: #eeeeee; 62 | padding: 5px; 63 | border: solid 1px #cccccc; 64 | min-height: 50px; 65 | margin: 0 0 10px; 66 | font-size: 12px; 67 | } 68 | 69 | .reset-note { 70 | margin-top: 25px; 71 | opacity: 0.6; 72 | font-size: 80%; 73 | text-align: center; 74 | } 75 | -------------------------------------------------------------------------------- /example/lib/app/public/mobile.js: -------------------------------------------------------------------------------- 1 | var App = React.createClass({ 2 | getInitialState: function() { 3 | return { 4 | query: initialQuery, 5 | result: '' 6 | }; 7 | }, 8 | 9 | componentDidMount: function() { 10 | $(window).resize(this.adjustControlHeights).trigger('resize'); 11 | }, 12 | 13 | render: function() { 14 | var options = queries.map(function(query, i) { 15 | return ; 16 | }); 17 | 18 | return ( 19 |
20 |

GQL Demo

21 | 22 |
23 | 27 | 28 |