Skip to content

Commit

Permalink
Add basic query support with Mongoid extension
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesreggio committed Jun 2, 2016
1 parent 6d75b8c commit 0398e80
Show file tree
Hide file tree
Showing 13 changed files with 371 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
class GraphQL::Rails::APIController < ApplicationController
#TODO
class GraphQL::Rails::SchemaController < ActionController::Base
# TODO: This may be superfluous.
rescue_from GraphQL::ParseError, :with => :invalid_request

def execute
query_string = params[:query]
query_variables = to_hash(params[:variables])
# TODO: Detect and integrate CanCan.
ability = Ability.new(current_user)
render json: schema.execute(
query_string,
variables: query_variables,
context: {:ability => ability}
context: {:ability => ability},
debug: true
)
end

private

def schema
@schema ||= GraphQL::Schema.new(query: QueryType)
GraphQL::Rails::Schema.instance
end

def to_hash(param)
Expand Down
1 change: 1 addition & 0 deletions config/initializers/graphiql.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GraphiQL::Rails.config.csrf = true
4 changes: 0 additions & 4 deletions config/initializers/inflections.rb

This file was deleted.

14 changes: 10 additions & 4 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
Rails.application.routes.draw do
# TODO: Support all verbs
# TODO: Load path from configuration
post '/api/graph' => 'graphql/rails/api#execute'
module GraphQL
module Rails
Engine.routes.draw do
if Rails.config.graphiql
mount GraphiQL::Rails::Engine => '/', :graphql_path => :self
end

post '/' => 'schema#execute'
end
end
end
1 change: 1 addition & 0 deletions graphql-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ Gem::Specification.new do |s|
s.add_dependency 'rails', '~> 4'
s.add_dependency 'graphql', '~> 0.13'
s.add_dependency 'graphql-relay', '~> 0.9'
# s.add_dependency 'graphiql-rails', '~> 1.2'
s.add_development_dependency 'sqlite3'
end
8 changes: 7 additions & 1 deletion lib/graphql/rails.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
require 'rails'
require 'graphql'
require 'graphql/relay'
require 'rails'
require 'graphiql/rails'

require 'graphql/rails/version'
require 'graphql/rails/dsl'
require 'graphql/rails/engine'
require 'graphql/rails/config'
require 'graphql/rails/types'
require 'graphql/rails/schema'
require 'graphql/rails/operations'
26 changes: 26 additions & 0 deletions lib/graphql/rails/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module GraphQL
module Rails
extend self

def configure
yield config
end

def config
@config ||= OpenStruct.new({
# Should the GraphiQL web interface be served?
:graphiql => ::Rails.env.development?,

# Should the /app/graph directory automatically reload upon changes?
:autoload => ::Rails.env.development?,

# Should names be converted to lowerCamelCase per GraphQL convention?
# For example, should :get_user_tasks become 'getUserTasks'?
:camel_case => true,

# Should Mongoid type extensions be loaded?
:mongoid => defined?(::Mongoid)
})
end
end
end
35 changes: 35 additions & 0 deletions lib/graphql/rails/dsl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module GraphQL
module Rails
class DSL < BasicObject
def run(&block)
case block.arity
when 0
@self = eval('self', block.binding)
instance_eval(&block)
when 1
yield self
else
raise 'Block accepts too many arguments (expected 0 or 1).'
end
end

def method_missing(method, *args, &block)
@self.send(method, *args, &block)
end
end

class HashDSL < DSL
def initialize(hash)
@struct = ::OpenStruct.new(hash)
end

def method_missing(method, *args, &block)
begin
@struct.send(method, *args, &block)
rescue
super
end
end
end
end
end
58 changes: 54 additions & 4 deletions lib/graphql/rails/engine.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,56 @@
class GraphQL::Rails::Engine < ::Rails::Engine
initializer 'graphql-rails.autoload', :before => :set_autoload_paths do |app|
app.config.autoload_paths += %W(#{app.config.root}/app/graph/fields)
app.config.autoload_paths += %W(#{app.config.root}/app/graph/types)
# Inflections must be added before the namespace is isolated, because the
# namespace's route prefix is calculated and cached at that time.
ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym 'GraphQL'
end

module GraphQL
module Rails
mattr_accessor :logger

class Engine < ::Rails::Engine
isolate_namespace GraphQL::Rails

initializer 'graphql-rails' do |app|
@graph_path = app.root.join('app', 'graph')

# Initialize logger.
# TODO: Fix tagging issues.
logger = ActiveSupport::TaggedLogging.new(::Rails.logger.clone)
logger.push_tags 'graphql'
Rails.logger = logger
Rails.logger.debug 'Initialized logger'

# Load extensions.
extensions = File.join(File.dirname(__FILE__), 'extensions', '*.rb')
Dir[extensions].each do |file|
require file
end

# Watch for changes to the /app/graph directory.
if Rails.config.autoload
dirs = {@graph_path.to_s => [:rb]}
checker = ActiveSupport::FileUpdateChecker.new([], dirs) do
Rails.logger.debug 'Detected changes to /app/graph directory'
reload!
end
ActionDispatch::Reloader.to_prepare do
checker.execute_if_updated
end
end

# Perform initial load of files under the /app/graph directory.
reload!
end

# TODO: Assess whether changes to a model class requires Types to be invalidated.
def reload!
Schema.clear
Dir[@graph_path.join('**', '*.rb')].each do |file|
Rails.logger.debug "Loading file: #{file}"
load file
end
end
end
end
end
57 changes: 57 additions & 0 deletions lib/graphql/rails/extensions/mongoid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# TODO: Limit to one ORM, then support multiple ORMs.
module GraphQL
module Rails
if Rails.config.mongoid
module Mongoid
Rails.logger.debug 'Loading Mongoid extensions'

Types.add_type ::Mongoid::Boolean, Boolean
Types.add_type BSON::ObjectId, String
Types.add_type DateTime, String
Types.add_type Object, String
Types.add_type Hash, String

Types.add_builder do |type|
next unless type.included_modules.include?(::Mongoid::Document)
Rails.logger.debug "Building Mongoid::Document type: #{type.name}"

# TODO: Support parent types/interfaces.
GraphQL::ObjectType.define do
name type.name

type.fields.each_value do |field_value|
field field_value.name do
type -> { Types.resolve(field_value.type) }
description field_value.label unless field_value.label.blank?
end
end

type.relations.each_value do |relationship|
# TODO: Add polymorphic support.
if relationship.polymorphic?
msg = "Skipping polymorphic relationship: #{relationship.name}"
Rails.logger.warn msg
next
end

if relationship.many?
connection relationship.name do
type -> { Types.resolve(relationship.klass).connection_type }
end
else
field relationship.name do
type -> { Types.resolve(relationship.klass) }
end
end
end
end
end

GraphQL::Relay::BaseConnection.register_connection_implementation(
::Mongoid::Relations::Targets::Enumerable,
GraphQL::Relay::RelationConnection
)
end
end
end
end
66 changes: 66 additions & 0 deletions lib/graphql/rails/operations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
module GraphQL
module Rails
class Operations
def self.query(hash, &block)
hash = extract_pair(hash)
Rails.logger.debug "Adding query: #{to_name(hash[:name])}"

definition = QueryDefinition.new
definition.run(&block)
definition.run do
name hash[:name]
type hash[:type]
end
Schema.add_query definition.field
end

private

def self.extract_pair(hash)
unless hash.length == 1
raise 'Hash must contain a single :name => Type pair.'
end
{name: hash.keys.first, type: hash.values.first}
end

def self.to_name(symbol)
if Rails.config.camel_case
symbol.to_s.camelize(:lower)
else
symbol.to_s
end
end

class QueryDefinition < DSL
attr_reader :field

def initialize
# TODO: Determine why root scoping is necessary.
@field = ::GraphQL::Field.new
end

def name(name)
@field.name = to_name(name)
end

def type(type)
@field.type = Types.resolve(type)
end

def argument(name, type, required = false)
# TODO: Determine why root scoping is necessary.
argument = ::GraphQL::Argument.new
argument.name = to_name(name)
argument.type = Types.resolve(type, required == :required)
@field.arguments[argument.name] = argument
end

def execute(&block)
field.resolve = -> (obj, args, ctx) do
HashDSL.new({obj: obj, args: args, ctx: ctx}).run(&block)
end
end
end
end
end
end
40 changes: 40 additions & 0 deletions lib/graphql/rails/schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module GraphQL
module Rails
module Schema
extend self

def clear
@schema = nil
@fields = Hash.new { |hash, key| hash[key] = [] }
end

TYPES = [:query, :mutation, :subscription]

TYPES.each do |type|
define_method "add_#{type.to_s}" do |field|
@schema = nil # Invalidate cached schema.
@fields[type].push field
end
end

def instance
#TODO: Support max_depth and types.
@schema ||= GraphQL::Schema.new begin
TYPES.reduce({}) do |schema, type|
fields = @fields[type]
unless fields.empty?
schema[type] = GraphQL::ObjectType.define do
name type.to_s.capitalize
description "Root #{type.to_s} for this schema"
fields.each do |value|
field value.name, field: value
end
end
end
schema
end
end
end
end
end
end
Loading

0 comments on commit 0398e80

Please sign in to comment.