├── .gitignore ├── Gemfile ├── lib └── graphql │ ├── rails │ ├── version.rb │ ├── fields.rb │ ├── node_identification.rb │ ├── controller_extensions.rb │ ├── dsl.rb │ ├── config.rb │ ├── extensions │ │ ├── cancan.rb │ │ └── mongoid.rb │ ├── schema.rb │ ├── callbacks.rb │ ├── engine.rb │ ├── types.rb │ └── operations.rb │ └── rails.rb ├── config ├── initializers │ └── graphiql.rb └── routes.rb ├── LICENSE ├── graphql-rails.gemspec └── app └── controllers └── graphql └── rails └── schema_controller.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | /*.gem 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /lib/graphql/rails/version.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | VERSION = '0.0.9' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/graphiql.rb: -------------------------------------------------------------------------------- 1 | # There is no apparent harm to enabling CSRF token-passing for GraphiQL, even 2 | # if the Rails app doesn't use CSRF protection. 3 | GraphiQL::Rails.config.csrf = true 4 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | Engine.routes.draw do 4 | if Rails.config.graphiql 5 | # Empty :graphql_path will cause GraphiQL to use its own URL. 6 | mount GraphiQL::Rails::Engine => '/', :graphql_path => '' 7 | end 8 | 9 | post '/' => 'schema#execute' 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/graphql/rails/fields.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | # Object that transforms key lookups on an object to the active field 4 | # naming convention, delegating all remaining methods as-is. 5 | class Fields < SimpleDelegator 6 | def [](key) 7 | __getobj__[Types.to_field_name(key)] 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/graphql/rails/node_identification.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | # Implements globally-unique object IDs for Relay compatibility. 4 | NodeIdentification = GraphQL::Relay::GlobalNodeIdentification.define do 5 | # TODO: Add security checks. 6 | object_from_id -> (id, ctx) do 7 | Types.lookup(*NodeIdentification.from_global_id(id)) 8 | end 9 | 10 | type_from_object -> (obj) do 11 | Types.resolve(obj.class) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/graphql/rails.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | require 'graphql' 3 | require 'graphql/relay' 4 | require 'graphiql/rails' 5 | 6 | # Order dependent. 7 | 8 | require 'graphql/rails/version' 9 | require 'graphql/rails/config' 10 | require 'graphql/rails/engine' 11 | 12 | require 'graphql/rails/dsl' 13 | require 'graphql/rails/types' 14 | require 'graphql/rails/fields' 15 | require 'graphql/rails/schema' 16 | require 'graphql/rails/callbacks' 17 | require 'graphql/rails/operations' 18 | require 'graphql/rails/node_identification' 19 | require 'graphql/rails/controller_extensions' 20 | -------------------------------------------------------------------------------- /lib/graphql/rails/controller_extensions.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | # Extensions are dynamically loaded once during engine initialization; 4 | # however, SchemaController can be reloaded at any time by Rails. To 5 | # preserve extensions to SchemaController, they're registered here. 6 | module ControllerExtensions 7 | extend self 8 | 9 | def add(&block) 10 | extensions.push block 11 | end 12 | 13 | def included(base) 14 | extensions.each do |extensions| 15 | base.class_eval(&extensions) 16 | end 17 | end 18 | 19 | private 20 | 21 | def extensions 22 | @extensions ||= [] 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/graphql/rails/dsl.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | # Object that runs a block in the context of itself, but delegates unknown 4 | # methods back to the block's original context. This is useful for creating 5 | # DSLs to aid with object initialization. 6 | # 7 | # Note that this class extends from BasicObject, which means that _all_ 8 | # global classes and modules must be prefixed by a double-colon (::) in 9 | # order to resolve. 10 | class DSL < BasicObject 11 | def run(&block) 12 | @self = eval('self', block.binding) 13 | instance_eval(&block) 14 | end 15 | 16 | private 17 | 18 | def method_missing(method, *args, &block) 19 | begin 20 | @self.send(method, *args, &block) 21 | rescue 22 | super 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 James Reggio 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 | -------------------------------------------------------------------------------- /graphql-rails.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | 3 | require 'graphql/rails/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'graphql-rails' 7 | s.version = GraphQL::Rails::VERSION 8 | s.license = 'MIT' 9 | s.authors = ['James Reggio'] 10 | s.email = ['james.reggio@gmail.com'] 11 | s.homepage = 'https://github.com/jamesreggio/graphql-rails' 12 | s.summary = 'Zero-configuration GraphQL + Relay support for Rails' 13 | s.description = <<-EOM 14 | Zero-configuration GraphQL + Relay support for Rails. Adds a route to process 15 | GraphQL operations and provides a visual editor (GraphiQL) during development. 16 | Allows you to specify GraphQL queries and mutations as though they were 17 | controller actions. Automatically maps Mongoid models to GraphQL types. 18 | Seamlessly integrates with CanCan. 19 | EOM 20 | s.required_ruby_version = '>= 2.1.0' 21 | 22 | s.files = Dir['{app,config,lib}/**/*', 'LICENSE'] 23 | s.required_ruby_version = '>= 2.1.0' 24 | 25 | s.add_dependency 'rails', '~> 4' 26 | s.add_dependency 'graphql', '~> 0.13' 27 | s.add_dependency 'graphql-relay', '~> 0.9' 28 | s.add_dependency 'graphiql-rails', '~> 1.2' 29 | end 30 | -------------------------------------------------------------------------------- /lib/graphql/rails/config.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | extend self 4 | 5 | # Yields the configuration object to a block, per convention. 6 | def configure 7 | yield config 8 | end 9 | 10 | # Configuration for this gem. 11 | def config 12 | @config ||= OpenStruct.new({ 13 | # Should graphql-ruby be placed into debug mode? 14 | :debug => ::Rails.env.development?, 15 | 16 | # Should the GraphiQL web interface be served? 17 | :graphiql => ::Rails.env.development?, 18 | 19 | # Should names be converted to lowerCamelCase per GraphQL convention? 20 | # For example, should :get_user_tasks become 'getUserTasks'? 21 | :camel_case => true, 22 | 23 | # Should object IDs be globally unique? 24 | # This is necessary to conform to the Relay Global Object ID spec. 25 | :global_ids => true, 26 | 27 | # Maximum nesting for GraphQL queries. 28 | # Specify nil for unlimited nesting depth. 29 | :max_depth => 8, 30 | 31 | # Should the following extensions be loaded? 32 | :mongoid => defined?(::Mongoid), 33 | :cancan => defined?(::CanCan), 34 | }) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/graphql/rails/extensions/cancan.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | if Rails.config.cancan 4 | Rails.logger.debug 'Loading CanCan extensions' 5 | 6 | # Implement methods from CanCan::ControllerAdditions in Operations. 7 | # http://www.rubydoc.info/github/ryanb/cancan/CanCan/ControllerAdditions 8 | Operations.class_eval do 9 | extend Forwardable 10 | def_delegators :current_ability, :can?, :cannot? 11 | 12 | def self.check_authorization(options = {}) 13 | self.after_filter(options.slice(:only, :except)) do |instance| 14 | next if instance.instance_variable_defined?(:@authorized) 15 | next if options[:if] && !instance.send(options[:if]) 16 | next if options[:unless] && instance.send(options[:unless]) 17 | raise 'This operation failed to perform an authorization check' 18 | end 19 | end 20 | 21 | def self.skip_authorization_check(*args) 22 | self.before_filter(*args) do |instance| 23 | instance.instance_variable_set(:@authorized, true) 24 | end 25 | end 26 | 27 | def authorize!(*args) 28 | begin 29 | @authorized = true 30 | current_ability.authorize!(*args) 31 | rescue ::CanCan::AccessDenied 32 | raise 'You are not authorized to perform this operation' 33 | end 34 | end 35 | 36 | def current_ability 37 | @current_ability ||= ::Ability.new(current_user) 38 | end 39 | 40 | def current_user 41 | context[:current_user] 42 | end 43 | end 44 | 45 | # Make the current_user available during GraphQL execution via the 46 | # operation context object. 47 | ControllerExtensions.add do 48 | before_filter do 49 | context[:current_user] = current_user 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/controllers/graphql/rails/schema_controller.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | class SchemaController < ActionController::Base 4 | # Extensions are dynamically loaded once during engine initialization; 5 | # however, this controller can be reloaded at any time by Rails. To 6 | # preserve extensions, we use the ControllerExtensions module as a cache. 7 | include ControllerExtensions 8 | 9 | # Defined in order of increasing specificity. 10 | rescue_from Exception, :with => :internal_error 11 | rescue_from GraphQL::ParseError, :with => :invalid_query 12 | rescue_from JSON::ParserError, :with => :invalid_variables 13 | 14 | # Execute a GraphQL query against the current schema. 15 | def execute 16 | render json: Schema.instance.execute( 17 | params[:query], 18 | variables: to_hash(params[:variables]), 19 | context: context 20 | ) 21 | end 22 | 23 | private 24 | 25 | def context 26 | @context ||= {} 27 | end 28 | 29 | def to_hash(param) 30 | if param.blank? 31 | {} 32 | elsif param.is_a?(String) 33 | JSON.parse(param) 34 | else 35 | param 36 | end 37 | end 38 | 39 | def render_error(status, message) 40 | render json: { 41 | :errors => [{:message => message}], 42 | }, :status => status 43 | end 44 | 45 | def invalid_request(message) 46 | render_error 400, message 47 | end 48 | 49 | def invalid_query 50 | invalid_request 'Unable to parse query' 51 | end 52 | 53 | def invalid_variables 54 | invalid_request 'Unable to parse variables' 55 | end 56 | 57 | def internal_error(e) 58 | Rails.logger.error 'Unexpected exception during execution' 59 | Rails.logger.exception e 60 | render_error 500, 'Internal error' 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/graphql/rails/schema.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | # Defines the GraphQL schema, consisting of 4 | # queries, mutations, and subscriptions. 5 | module Schema 6 | extend self 7 | 8 | # Clear internal state, probably due to a Rails reload. 9 | def clear 10 | @schema = nil 11 | @fields = Hash.new { |hash, key| hash[key] = [] } 12 | end 13 | 14 | TYPES = [:query, :mutation, :subscription] 15 | 16 | # Register a field in the GraphQL schema. 17 | TYPES.each do |type| 18 | define_method "add_#{type.to_s}" do |field| 19 | @schema = nil # Invalidate cached schema. 20 | @fields[type].push field 21 | end 22 | end 23 | 24 | # Lazily build the GraphQL schema instance. 25 | def instance 26 | @schema ||= GraphQL::Schema.new begin 27 | TYPES.reduce({ 28 | max_depth: Rails.config.max_depth, 29 | types: Types.explicit, 30 | }) do |schema, type| 31 | fields = @fields[type] 32 | unless fields.empty? 33 | # Build an object for each operation type. 34 | schema[type] = GraphQL::ObjectType.define do 35 | name type.to_s.capitalize 36 | description "Root #{type.to_s} for this schema" 37 | # Add a field for each operation. 38 | fields.each do |value| 39 | field value.name, field: value 40 | end 41 | # Add the global node ID lookup query. 42 | if Rails.config.global_ids && type == :query 43 | field :node, field: NodeIdentification.field 44 | end 45 | end 46 | end 47 | schema 48 | end 49 | end 50 | if Rails.config.global_ids 51 | @schema.node_identification = NodeIdentification 52 | end 53 | @schema 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/graphql/rails/callbacks.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | class Operations 4 | # Implement callback methods on Operations. 5 | # These are akin to the 'filters' available on ActionController::Base. 6 | # http://api.rubyonrails.org/classes/AbstractController/Callbacks.html 7 | module Callbacks 8 | extend ActiveSupport::Concern 9 | include ActiveSupport::Callbacks 10 | 11 | # All callbacks are registered under the :perform_operation event. 12 | included do 13 | define_callbacks :perform_operation 14 | end 15 | 16 | module ClassMethods 17 | # Callbacks can be registered with the following methods: 18 | # before_operation, before_filter 19 | # around_operation, around_filter 20 | # after_operation, after_filter 21 | [:before, :after, :around].each do |callback| 22 | define_method "#{callback}_operation" do |*names, &block| 23 | insert_callbacks(names, block) do |target, options| 24 | set_callback :perform_operation, callback, target, options 25 | end 26 | end 27 | alias_method :"#{callback}_filter", :"#{callback}_operation" 28 | end 29 | 30 | private 31 | 32 | # Convert :only and :except options into :if and :unless blocks. 33 | def normalize_callback_options(options) 34 | normalize_callback_option(options, :only, :if) 35 | normalize_callback_option(options, :except, :unless) 36 | end 37 | 38 | # Convert an operation name-based condition into an executable block. 39 | def normalize_callback_option(options, from, to) 40 | return unless options[from] 41 | check = -> do 42 | Array(options[from]).find { |operation| name == operation } 43 | end 44 | options[to] = Array(options[to]) + [check] 45 | end 46 | 47 | # Normalize the arguments passed during callback registration. 48 | def insert_callbacks(callbacks, block = nil) 49 | options = callbacks.extract_options! 50 | normalize_callback_options(options) 51 | callbacks.push(block) if block 52 | callbacks.each do |callback| 53 | yield callback, options 54 | end 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/graphql/rails/engine.rb: -------------------------------------------------------------------------------- 1 | # Inflections must be added before the namespace is isolated, because the 2 | # namespace's route prefix is calculated and cached at that time. 3 | ActiveSupport::Inflector.inflections do |inflect| 4 | inflect.acronym 'GraphQL' 5 | end 6 | 7 | module GraphQL 8 | module Rails 9 | mattr_accessor :logger 10 | 11 | class Engine < ::Rails::Engine 12 | isolate_namespace GraphQL::Rails 13 | 14 | # Even though we aren't using symbolic autoloading of operations, they 15 | # must be included in autoload_paths in order to be unloaded during 16 | # reload operations. 17 | initializer 'graphql-rails.autoload', :before => :set_autoload_paths do |app| 18 | @graph_path = app.root.join('app', 'graph') 19 | app.config.autoload_paths += [ 20 | @graph_path.join('types'), 21 | @graph_path.join('operations'), 22 | ] 23 | end 24 | 25 | # Extend the Rails logger with a facility for logging exceptions. 26 | initializer 'graphql-rails.logger', :after => :initialize_logger do |app| 27 | logger = ::Rails.logger.clone 28 | logger.class_eval do 29 | def exception(e) 30 | begin 31 | error "#{e.class.name} (#{e.message}):" 32 | error " #{e.backtrace.join("\n ")}" 33 | rescue 34 | end 35 | end 36 | end 37 | Rails.logger = logger 38 | Rails.logger.debug 'Initialized logger' 39 | end 40 | 41 | # Extensions depend upon a loaded Rails app, so we load them dynamically. 42 | initializer 'graphql-rails.extensions', :after => :load_config_initializers do |app| 43 | extensions = File.join(File.dirname(__FILE__), 'extensions', '*.rb') 44 | Dir[extensions].each do |file| 45 | require file 46 | end 47 | end 48 | 49 | # Hook into Rails reloading in order to clear state from internal 50 | # stateful modules and reload operations from the Rails app. 51 | initializer 'graphql-rails.prepare', :before => :add_to_prepare_blocks do 52 | # The block executes in the context of the reloader, so we have to 53 | # preserve a reference to the engine instance. 54 | engine = self 55 | config.to_prepare_blocks.push -> do 56 | engine.reload! 57 | end 58 | end 59 | 60 | # Clear state and load operations from the Rails app. 61 | def reload! 62 | Types.clear 63 | Schema.clear 64 | Rails.logger.debug 'Loading operations' 65 | Dir[@graph_path.join('operations', '**', '*.rb')].each do |file| 66 | Rails.logger.debug "Loading file: #{file}" 67 | require_dependency file 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/graphql/rails/types.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | # Type system responsible for resolving GraphQL types. 4 | # Delegates creation of GraphQL types to ORM-specific extensions. 5 | module Types 6 | extend self 7 | 8 | # Clear internal state, probably due to a Rails reload. 9 | def clear 10 | @types = nil 11 | @explicit = nil 12 | extensions.each do |extension| 13 | extension.clear 14 | end 15 | end 16 | 17 | # Resolve an arbitrary type to a GraphQL type. 18 | # Lists can be specified with single-element arrays; for example: 19 | # [String] resolves to a list of GraphQL::STRING_TYPE objects. 20 | def resolve(type, required = false) 21 | if type.nil? 22 | raise 'Cannot resolve nil type' 23 | elsif required 24 | resolve(type).to_non_null_type 25 | elsif type.is_a?(GraphQL::BaseType) 26 | type 27 | elsif type.is_a?(Array) 28 | unless type.length == 1 29 | raise 'Lists must be specified with single-element arrays' 30 | end 31 | resolve(type.first).to_list_type 32 | elsif types.include?(type) 33 | resolve(types[type]) 34 | else 35 | resolve(try_extensions(:resolve, type) || begin 36 | # TODO: Decide whether to use String as a fallback, or raise. 37 | Rails.logger.warn "Unable to resolve type: #{type.name}" 38 | String 39 | end) 40 | end 41 | end 42 | 43 | # Array of types that should be explicitly included in the schema. 44 | # Useful for ensuring that interface implmentations are included. 45 | def explicit 46 | @explicit ||= [] 47 | end 48 | 49 | # Lookup an arbitrary object from its GraphQL type name and ID. 50 | def lookup(type_name, id) 51 | try_extensions(:lookup, type_name, id) 52 | end 53 | 54 | # Should extensions namespace their type names? 55 | # This is necessary if multiple extensions are loaded, so as to avoid 56 | # collisions in the shared type namespace. 57 | def use_namespaces? 58 | extensions.count > 1 59 | end 60 | 61 | # Add an extension to the type system. 62 | # Generally, each ORM will have its own extension. 63 | def add_extension(extension) 64 | extensions.push extension 65 | end 66 | 67 | # Convert a type name to a string with the correct convention, 68 | # applying an optional namespace. 69 | def to_type_name(name, namespace = '') 70 | return namespace + to_type_name(name) unless namespace.blank? 71 | name = name.to_s 72 | name = name.camelize(:upper) if Rails.config.camel_case 73 | name = name.gsub(/\W/, '_') 74 | name 75 | end 76 | 77 | # Convert a field name to a string with the correct convention. 78 | def to_field_name(name) 79 | # camelize strips leading underscores, which is undesirable. 80 | if name.to_s.starts_with?('_') 81 | "_#{to_field_name(name.to_s[1..-1])}" 82 | elsif Rails.config.camel_case 83 | name.to_s.camelize(:lower) 84 | else 85 | name.to_s 86 | end 87 | end 88 | 89 | private 90 | 91 | # Default mapping of built-in scalar types to GraphQL types. 92 | def types 93 | @types ||= { 94 | String => GraphQL::STRING_TYPE, 95 | 96 | Fixnum => GraphQL::INT_TYPE, 97 | Integer => GraphQL::INT_TYPE, 98 | Float => GraphQL::FLOAT_TYPE, 99 | 100 | Date => GraphQL::STRING_TYPE, 101 | Time => GraphQL::STRING_TYPE, 102 | DateTime => GraphQL::STRING_TYPE, 103 | 104 | Array => GraphQL::STRING_TYPE, 105 | Object => GraphQL::STRING_TYPE, 106 | Hash => GraphQL::STRING_TYPE, 107 | } 108 | end 109 | 110 | # List of registered extensions. 111 | def extensions 112 | @extensions ||= [] 113 | end 114 | 115 | # Try a function on each extension, and return the result from 116 | # the first extension that returns a non-nil value. 117 | def try_extensions(method, *args, &block) 118 | extensions.each do |extension| 119 | result = extension.send(method, *args, &block) 120 | return result unless result.nil? 121 | end 122 | nil 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/graphql/rails/extensions/mongoid.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | if Rails.config.mongoid 4 | Rails.logger.debug 'Loading Mongoid extensions' 5 | 6 | # Use the built-in RelationConnection to handle Mongoid relations. 7 | GraphQL::Relay::BaseConnection.register_connection_implementation( 8 | ::Mongoid::Relations::Targets::Enumerable, 9 | GraphQL::Relay::RelationConnection 10 | ) 11 | 12 | # Mongoid type extension for the GraphQL type system. 13 | module Mongoid 14 | extend self 15 | 16 | # Clear internal state, probably due to a Rails reload. 17 | def clear 18 | @types = nil 19 | end 20 | 21 | # Resolve an arbitrary type to a GraphQL type. 22 | # Returns nil if the type isn't a Mongoid document. 23 | def resolve(type) 24 | types[type] || build_type(type) 25 | end 26 | 27 | # Lookup an arbitrary object from its GraphQL type name and ID. 28 | def lookup(type_name, id) 29 | return unless type_name.starts_with?(namespace) 30 | types.each_pair do |type, graph_type| 31 | return type.find(id) if graph_type.name == type_name 32 | end 33 | nil 34 | end 35 | 36 | private 37 | 38 | # Namespace for Mongoid types, if namespaces are required. 39 | def namespace 40 | if Types.use_namespaces? 41 | 'MG' 42 | else 43 | '' 44 | end 45 | end 46 | 47 | # Cached mapping of Mongoid types to GraphQL types, initialized with 48 | # mappings for common built-in scalar types. 49 | def types 50 | @types ||= { 51 | Boolean => GraphQL::BOOLEAN_TYPE, 52 | ::Mongoid::Boolean => GraphQL::BOOLEAN_TYPE, 53 | BSON::ObjectId => GraphQL::STRING_TYPE, 54 | } 55 | end 56 | 57 | # Build a GraphQL type for a Mongoid document. 58 | # Returns nil if the type isn't a Mongoid document. 59 | def build_type(type) 60 | return nil unless type.included_modules.include?(::Mongoid::Document) 61 | Rails.logger.debug "Building Mongoid::Document type: #{type.name}" 62 | 63 | # Build and cache the GraphQL type. 64 | # TODO: Map type inheritance to GraphQL interfaces. 65 | type_name = Types.to_type_name(type.name, namespace) 66 | types[type] = GraphQL::ObjectType.define do 67 | name type_name 68 | 69 | # Add the global node ID, if enabled; otherwise, document ID. 70 | if Rails.config.global_ids 71 | interfaces [NodeIdentification.interface] 72 | global_id_field :id 73 | else 74 | field :id do 75 | type -> { Types.resolve(BSON::ObjectId) } 76 | end 77 | end 78 | 79 | # Add each field from the document. 80 | # TODO: Support field exclusion and renaming. 81 | type.fields.each_value do |field_value| 82 | field Types.to_field_name(field_value.name) do 83 | property field_value.name.to_sym 84 | type -> { Types.resolve(field_value.type) } 85 | description field_value.label unless field_value.label.blank? 86 | end 87 | end 88 | 89 | # Add each relationship from the document as a Relay connection. 90 | type.relations.each_value do |relationship| 91 | # TODO: Add polymorphic support. 92 | if relationship.polymorphic? 93 | Rails.logger.warn( 94 | "Skipping polymorphic relationship: #{relationship.name}" 95 | ) 96 | next 97 | end 98 | 99 | # Check that relationship has a valid type. 100 | begin 101 | klass = relationship.klass 102 | rescue 103 | Rails.logger.warn( 104 | "Skipping relationship with invalid class: #{relationship.name}" 105 | ) 106 | next 107 | end 108 | 109 | if relationship.many? 110 | connection Types.to_field_name(relationship.name) do 111 | property relationship.name.to_sym 112 | type -> { Types.resolve(klass).connection_type } 113 | end 114 | else 115 | field Types.to_field_name(relationship.name) do 116 | property relationship.name.to_sym 117 | type -> { Types.resolve(klass) } 118 | end 119 | end 120 | end 121 | end 122 | end 123 | end 124 | 125 | Types.add_extension Mongoid 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/graphql/rails/operations.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Rails 3 | # Base type for operations classes in the Rails app. 4 | # Operations are specified in a manner similar to controller actions, and 5 | # can access variables and state localized to the current operation. 6 | # Classes can define callbacks similar to controller 'filters'. 7 | class Operations 8 | extend Forwardable 9 | include Callbacks 10 | 11 | # Initialize an instance with state pertaining to the current operation. 12 | # Accessors for this state are created and proxied through to the 13 | # specified options hash. 14 | def initialize(options = {}) 15 | @options = OpenStruct.new(options) 16 | self.class.instance_eval do 17 | def_delegators :@options, *options.keys 18 | end 19 | end 20 | 21 | # Define a query operation. 22 | # Definitions should have the following form: 23 | # 24 | # query :find_cats => [AnimalInterface] do 25 | # description 'This query returns a list of Cat models' 26 | # argument :age, Integer, :required 27 | # argument :breed, String 28 | # uses CatType 29 | # resolve do 30 | # raise 'Too old' if args[:age] > 20 31 | # Cat.find(age: args[:age], breed: args[:breed]) 32 | # end 33 | # end 34 | def self.query(hash, &block) 35 | hash = extract_pair(hash) 36 | Rails.logger.debug "Adding query: #{Types.to_field_name(hash[:name])}" 37 | 38 | definition = QueryDefinition.new(self) 39 | definition.run(&block) 40 | definition.run do 41 | name hash[:name] 42 | type hash[:type] 43 | end 44 | Schema.add_query definition.field 45 | end 46 | 47 | # Define a mutation operation. 48 | # Definitions should have the following form: 49 | # 50 | # mutation :feed_cat => {appetite: Integer, last_meal: DateTime} do 51 | # description 'This mutation feeds the cat and returns its appetite' 52 | # argument :cat_id, Integer, :required 53 | # argument :mouse_id, Integer, :required 54 | # resolve do 55 | # cat = Cat.find(args[:cat_id]) 56 | # mouse = Mouse.find(args[:mouse_id]) 57 | # raise 'Cannot find cat or mouse' if cat.nil? || mouse.nil? 58 | # 59 | # cat.feed(mouse) 60 | # {appetite: cat.appetite, last_meal: DateTime.now} 61 | # end 62 | # end 63 | def self.mutation(hash, &block) 64 | hash = extract_pair(hash) 65 | unless hash[:type].is_a?(Hash) 66 | raise 'Mutation must be specified with a Hash result type' 67 | end 68 | Rails.logger.debug "Adding mutation: #{Types.to_field_name(hash[:name])}" 69 | 70 | definition = MutationDefinition.new(self) 71 | definition.run(&block) 72 | definition.run do 73 | name hash[:name] 74 | type hash[:type] 75 | end 76 | Schema.add_mutation definition.field 77 | end 78 | 79 | # TODO: Implement subscriptions. 80 | 81 | private 82 | 83 | # DSL for query definition. 84 | # TODO: Support resolve-only blocks. 85 | class QueryDefinition < DSL 86 | attr_reader :field 87 | 88 | def initialize(klass) 89 | @klass = klass 90 | @field = ::GraphQL::Field.new 91 | end 92 | 93 | def name(name) 94 | @name = name 95 | @field.name = Types.to_field_name(name) 96 | end 97 | 98 | def uses(type) 99 | Types.explicit.push Types.resolve(type) 100 | end 101 | 102 | def type(type) 103 | @type = type 104 | @field.type = Types.resolve(type) 105 | end 106 | 107 | def description(description) 108 | @field.description = description 109 | end 110 | 111 | def argument(name, type, required = false) 112 | argument = ::GraphQL::Argument.define do 113 | name Types.to_field_name(name) 114 | type Types.resolve(type, required == :required) 115 | end 116 | @field.arguments[argument.name] = argument 117 | end 118 | 119 | def resolve(&block) 120 | @field.resolve = -> (obj, args, ctx) do 121 | # Instantiate the Operations class with state on this query. 122 | instance = @klass.new({ 123 | op: :query, name: @name, type: @type, 124 | obj: obj, args: Fields.new(args), ctx: ctx, context: ctx 125 | }) 126 | 127 | begin 128 | # Run callbacks for this Operations class. 129 | instance.run_callbacks(:perform_operation) do 130 | # Call out to the app-defined resolver. 131 | instance.instance_eval(&block) 132 | end 133 | rescue => e 134 | # Surface messages from standard errors in GraphQL response. 135 | ::GraphQL::ExecutionError.new(e.message) 136 | rescue ::Exception => e 137 | # Log and genericize other runtime errors. 138 | Rails.logger.error "Unexpected exception during query: #{@name}" 139 | Rails.logger.exception e 140 | ::GraphQL::ExecutionError.new('Internal error') 141 | end 142 | end 143 | end 144 | end 145 | 146 | # DSL for mutation definition. 147 | class MutationDefinition < QueryDefinition 148 | def initialize(klass) 149 | super 150 | @input = ::GraphQL::InputObjectType.new 151 | @output = ::GraphQL::ObjectType.new 152 | end 153 | 154 | def type(hash) 155 | hash.each do |name, type| 156 | field = ::GraphQL::Field.define do 157 | name Types.to_field_name(name) 158 | type Types.resolve(type) 159 | end 160 | @output.fields[field.name] = field 161 | end 162 | end 163 | 164 | def argument(name, type, required = false) 165 | argument = ::GraphQL::Argument.define do 166 | name Types.to_field_name(name) 167 | type Types.resolve(type, required == :required) 168 | end 169 | @input.arguments[argument.name] = argument 170 | end 171 | 172 | def field 173 | # Build input object according to mutation specification. 174 | input = @input 175 | input.name = "#{@name.to_s.camelize(:upper)}Input" 176 | input.description = "Generated input type for #{@field.name}" 177 | input.arguments['clientMutationId'] = ::GraphQL::Argument.define do 178 | name 'clientMutationId' 179 | type Types.resolve(::String) 180 | description 'Unique identifier for client performing mutation' 181 | end 182 | 183 | # Build compound output object according to mutation specification. 184 | output = @output 185 | output.name = "#{@name.to_s.camelize(:upper)}Output" 186 | output.description = "Generated output type for #{@field.name}" 187 | output.fields['clientMutationId'] = ::GraphQL::Field.define do 188 | name 'clientMutationId' 189 | type Types.resolve(::String) 190 | description 'Unique identifier for client performing mutation' 191 | end 192 | 193 | @field.type = output 194 | @field.arguments['input'] = ::GraphQL::Argument.define do 195 | name 'input' 196 | type Types.resolve(input, true) 197 | end 198 | @field 199 | end 200 | 201 | def resolve(&block) 202 | @field.resolve = -> (obj, args, ctx) do 203 | # Instantiate the Operations class with state on this query. 204 | instance = @klass.new({ 205 | op: :mutation, name: @name, type: @type, 206 | obj: obj, args: Fields.new(args[:input]), ctx: ctx, context: ctx 207 | }) 208 | 209 | begin 210 | # Run callbacks for this Operations class. 211 | instance.run_callbacks(:perform_operation) do 212 | # Call out to the app-defined resolver. 213 | result = instance.instance_eval(&block) 214 | 215 | # Transform the result keys to the expected convention. 216 | unless result.is_a?(::Hash) 217 | raise 'Mutation must resolve to a Hash result' 218 | end 219 | 220 | result = result.inject({ 221 | 'clientMutationId' => args['clientMutationId'] 222 | }) do |hash, (key, value)| 223 | hash[Types.to_field_name(key)] = value 224 | hash 225 | end 226 | ::OpenStruct.new(result) 227 | end 228 | rescue => e 229 | # Surface messages from standard errors in GraphQL response. 230 | ::GraphQL::ExecutionError.new(e.message) 231 | rescue ::Exception => e 232 | # Log and genericize other runtime errors. 233 | Rails.logger.error "Unexpected exception during mutation: #{@name}" 234 | Rails.logger.exception e 235 | ::GraphQL::ExecutionError.new('Internal error') 236 | end 237 | end 238 | end 239 | end 240 | 241 | # Extract parts from a hash passed to the operation definition DSL. 242 | def self.extract_pair(hash) 243 | unless hash.length == 1 244 | raise 'Hash must contain a single :name => Type pair' 245 | end 246 | {name: hash.keys.first, type: hash.values.first} 247 | end 248 | end 249 | end 250 | end 251 | --------------------------------------------------------------------------------