graphql_rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.hound.yml +3 -0
- data/.rspec +3 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +98 -0
- data/LICENSE.txt +21 -0
- data/README.md +122 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/graphql_rails.gemspec +29 -0
- data/lib/graphiti.rb +10 -0
- data/lib/graphiti/attribute.rb +86 -0
- data/lib/graphiti/controller.rb +54 -0
- data/lib/graphiti/controller/action_configuration.rb +46 -0
- data/lib/graphiti/controller/configuration.rb +32 -0
- data/lib/graphiti/controller/controller_function.rb +41 -0
- data/lib/graphiti/controller/request.rb +40 -0
- data/lib/graphiti/errors/execution_error.rb +18 -0
- data/lib/graphiti/errors/validation_error.rb +26 -0
- data/lib/graphiti/model.rb +33 -0
- data/lib/graphiti/model/configuration.rb +81 -0
- data/lib/graphiti/router.rb +65 -0
- data/lib/graphiti/router/action.rb +21 -0
- data/lib/graphiti/router/mutation_action.rb +18 -0
- data/lib/graphiti/router/query_action.rb +18 -0
- data/lib/graphiti/router/resource_actions_builder.rb +82 -0
- data/lib/graphiti/router/schema_builder.rb +37 -0
- data/lib/graphiti/version.rb +5 -0
- data/lib/graphql_rails.rb +10 -0
- data/lib/graphql_rails/attribute.rb +86 -0
- data/lib/graphql_rails/controller.rb +54 -0
- data/lib/graphql_rails/controller/action_configuration.rb +46 -0
- data/lib/graphql_rails/controller/action_path_parser.rb +75 -0
- data/lib/graphql_rails/controller/configuration.rb +32 -0
- data/lib/graphql_rails/controller/controller_function.rb +41 -0
- data/lib/graphql_rails/controller/request.rb +40 -0
- data/lib/graphql_rails/errors/execution_error.rb +18 -0
- data/lib/graphql_rails/errors/validation_error.rb +26 -0
- data/lib/graphql_rails/model.rb +33 -0
- data/lib/graphql_rails/model/configuration.rb +81 -0
- data/lib/graphql_rails/router.rb +65 -0
- data/lib/graphql_rails/router/action.rb +21 -0
- data/lib/graphql_rails/router/mutation_action.rb +18 -0
- data/lib/graphql_rails/router/query_action.rb +18 -0
- data/lib/graphql_rails/router/resource_actions_builder.rb +82 -0
- data/lib/graphql_rails/router/schema_builder.rb +37 -0
- data/lib/graphql_rails/version.rb +5 -0
- metadata +166 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "graphql_rails/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'graphql_rails'
|
8
|
+
spec.version = GraphqlRails::VERSION
|
9
|
+
spec.authors = ['Povilas Jurčys']
|
10
|
+
spec.email = ['bloomrain@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = %q{GrapQL server and client for rails}
|
13
|
+
spec.homepage = 'https://github.com/povilasjurcys/graphql_rails'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = 'exe'
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_dependency 'graphql', '~> 1'
|
24
|
+
spec.add_dependency 'activesupport', '~> 5'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
27
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
29
|
+
end
|
data/lib/graphiti.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
|
5
|
+
module GraphqlRails
|
6
|
+
# contains info about single graphql attribute
|
7
|
+
class Attribute
|
8
|
+
attr_reader :name, :type
|
9
|
+
|
10
|
+
def initialize(name, type = nil, required: false, hidden: false)
|
11
|
+
@name = name.to_s
|
12
|
+
@type = parse_type(type || type_by_attribute_name)
|
13
|
+
@required = required
|
14
|
+
@hidden = hidden
|
15
|
+
end
|
16
|
+
|
17
|
+
def graphql_field_type
|
18
|
+
@graphql_field_type ||= required? ? type.to_non_null_type : type
|
19
|
+
end
|
20
|
+
|
21
|
+
def required?
|
22
|
+
@required
|
23
|
+
end
|
24
|
+
|
25
|
+
def hidden?
|
26
|
+
@hidden
|
27
|
+
end
|
28
|
+
|
29
|
+
def field_name
|
30
|
+
field =
|
31
|
+
if name.end_with?('?')
|
32
|
+
"is_#{name.remove(/\?\Z/)}"
|
33
|
+
else
|
34
|
+
name
|
35
|
+
end
|
36
|
+
|
37
|
+
field.camelize(:lower)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def type_by_attribute_name
|
43
|
+
case name
|
44
|
+
when 'id', /_id\Z/
|
45
|
+
GraphQL::ID_TYPE
|
46
|
+
when /\?\Z/
|
47
|
+
GraphQL::BOOLEAN_TYPE
|
48
|
+
else
|
49
|
+
GraphQL::STRING_TYPE
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_type(type)
|
54
|
+
if graphql_type?(type)
|
55
|
+
type
|
56
|
+
elsif type.is_a?(String) || type.is_a?(Symbol)
|
57
|
+
map_type_name_to_type(type.to_s.downcase)
|
58
|
+
else
|
59
|
+
raise "Unsupported type #{type.inspect} (class: #{type.class})"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def graphql_type?(type)
|
64
|
+
type.is_a?(GraphQL::BaseType) ||
|
65
|
+
type.is_a?(GraphQL::ObjectType) ||
|
66
|
+
(defined?(GraphQL::Schema::Member) && type.is_a?(Class) && type < GraphQL::Schema::Member)
|
67
|
+
end
|
68
|
+
|
69
|
+
def map_type_name_to_type(type_name)
|
70
|
+
case type_name
|
71
|
+
when 'id'
|
72
|
+
GraphQL::ID_TYPE
|
73
|
+
when 'int', 'integer'
|
74
|
+
GraphQL::INT_TYPE
|
75
|
+
when 'string', 'str', 'text', 'time', 'date'
|
76
|
+
GraphQL::STRING_TYPE
|
77
|
+
when 'bool', 'boolean', 'mongoid::boolean'
|
78
|
+
GraphQL::BOOLEAN_TYPE
|
79
|
+
when 'float', 'double', 'decimal'
|
80
|
+
GraphQL::FLOAT_TYPE
|
81
|
+
else
|
82
|
+
raise "Don't know how to parse type with name #{type_name.inspect}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/hash_with_indifferent_access'
|
4
|
+
require_relative 'controller/configuration'
|
5
|
+
require_relative 'controller/request'
|
6
|
+
|
7
|
+
module GraphqlRails
|
8
|
+
# base class for all graphql_rails controllers
|
9
|
+
class Controller
|
10
|
+
class << self
|
11
|
+
def before_action(action_name)
|
12
|
+
controller_configuration.add_before_action(action_name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def action(method_name)
|
16
|
+
controller_configuration.action(method_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def controller_configuration
|
20
|
+
@controller_configuration ||= Controller::Configuration.new(self)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(graphql_request)
|
25
|
+
@graphql_request = graphql_request
|
26
|
+
end
|
27
|
+
|
28
|
+
def call(method_name)
|
29
|
+
self.class.controller_configuration.before_actions.each { |action_name| send(action_name) }
|
30
|
+
|
31
|
+
begin
|
32
|
+
response = public_send(method_name)
|
33
|
+
render response if graphql_request.no_object_to_return?
|
34
|
+
rescue StandardError => error
|
35
|
+
render error: error
|
36
|
+
end
|
37
|
+
|
38
|
+
graphql_request.object_to_return
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
attr_reader :graphql_request
|
44
|
+
|
45
|
+
def render(object = nil, error: nil, errors: Array(error))
|
46
|
+
graphql_request.errors = errors
|
47
|
+
graphql_request.object_to_return = object
|
48
|
+
end
|
49
|
+
|
50
|
+
def params
|
51
|
+
@params = HashWithIndifferentAccess.new(graphql_request.params)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string/filters'
|
4
|
+
require 'graphql_rails/attribute'
|
5
|
+
|
6
|
+
module GraphqlRails
|
7
|
+
class Controller
|
8
|
+
# stores all graphql_rails contoller specific config
|
9
|
+
class ActionConfiguration
|
10
|
+
attr_reader :attributes, :return_type
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@attributes = {}
|
14
|
+
@can_return_nil = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def permit(*no_type_attributes, **typed_attributes)
|
18
|
+
no_type_attributes.each { |attribute| permit_attribute(attribute) }
|
19
|
+
typed_attributes.each { |attribute, type| permit_attribute(attribute, type) }
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def can_return_nil
|
24
|
+
@can_return_nil = true
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def returns(new_return_type)
|
29
|
+
@return_type = new_return_type
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def can_return_nil?
|
34
|
+
@can_return_nil
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def permit_attribute(name, type = nil)
|
40
|
+
field_name = name.to_s.remove(/!\Z/)
|
41
|
+
required = name.to_s.end_with?('!')
|
42
|
+
attributes[field_name] = Attribute.new(field_name, type, required: required)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'graphql_rails/attribute'
|
5
|
+
require 'graphql_rails/controller/action_configuration'
|
6
|
+
|
7
|
+
module GraphqlRails
|
8
|
+
class Controller
|
9
|
+
# stores all graphql_rails contoller specific config
|
10
|
+
class Configuration
|
11
|
+
attr_reader :before_actions
|
12
|
+
|
13
|
+
def initialize(controller)
|
14
|
+
@controller = controller
|
15
|
+
@before_actions = Set.new
|
16
|
+
@action_by_name = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_before_action(name)
|
20
|
+
before_actions << name
|
21
|
+
end
|
22
|
+
|
23
|
+
def action(method_name)
|
24
|
+
@action_by_name[method_name.to_s] ||= ActionConfiguration.new
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :controller
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql_rails/controller/action_path_parser'
|
4
|
+
require_relative 'request'
|
5
|
+
|
6
|
+
module GraphqlRails
|
7
|
+
class Controller
|
8
|
+
# graphql resolver which redirects actions to appropriate controller and controller action
|
9
|
+
class ControllerFunction < GraphQL::Function
|
10
|
+
# accepts path of given format "controller_name#action"
|
11
|
+
attr_reader :type
|
12
|
+
|
13
|
+
def initialize(controller, action_name, return_type)
|
14
|
+
@controller = controller
|
15
|
+
@action_name = action_name
|
16
|
+
@type = return_type
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.build(action_path, **options)
|
20
|
+
action_parser = ActionPathParser.new(action_path, **options)
|
21
|
+
|
22
|
+
action_function = Class.new(self) do
|
23
|
+
action_parser.arguments.each do |action_attribute|
|
24
|
+
argument(action_attribute.field_name, action_attribute.graphql_field_type)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
action_function.new(action_parser.controller, action_parser.action_name, action_parser.return_type)
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(object, inputs, ctx)
|
32
|
+
request = Request.new(object, inputs, ctx)
|
33
|
+
controller.new(request).call(action_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :controller, :action_name
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors/execution_error'
|
4
|
+
|
5
|
+
module GraphqlRails
|
6
|
+
class Controller
|
7
|
+
# Contains all info related with single request to controller
|
8
|
+
class Request
|
9
|
+
attr_accessor :object_to_return
|
10
|
+
attr_reader :errors
|
11
|
+
|
12
|
+
def initialize(graphql_object, inputs, context)
|
13
|
+
@graphql_object = graphql_object
|
14
|
+
@inputs = inputs
|
15
|
+
@context = context
|
16
|
+
end
|
17
|
+
|
18
|
+
def errors=(new_errors)
|
19
|
+
@errors = new_errors
|
20
|
+
|
21
|
+
new_errors.each do |error|
|
22
|
+
error_message = error.is_a?(String) ? error : error.message
|
23
|
+
context.add_error(ExecutionError.new(error_message))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def no_object_to_return?
|
28
|
+
!defined?(@object_to_return)
|
29
|
+
end
|
30
|
+
|
31
|
+
def params
|
32
|
+
inputs.to_h
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :graphql_object, :inputs, :context
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlRails
|
4
|
+
# base class which is returned in case something bad happens. Contains all error rendering tructure
|
5
|
+
class ExecutionError < GraphQL::ExecutionError
|
6
|
+
def to_h
|
7
|
+
super.except('locations').merge('type' => type, 'http_status_code' => http_status_code)
|
8
|
+
end
|
9
|
+
|
10
|
+
def type
|
11
|
+
'system_error'
|
12
|
+
end
|
13
|
+
|
14
|
+
def http_status_code
|
15
|
+
500
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlRails
|
4
|
+
# GrapqhQL error that is raised when invalid data is given
|
5
|
+
class ValidationError < ExecutionError
|
6
|
+
attr_reader :short_message, :field
|
7
|
+
|
8
|
+
def initialize(short_message, field)
|
9
|
+
super([field.presence, short_message].compact.join(' '))
|
10
|
+
@short_message = short_message
|
11
|
+
@field = field
|
12
|
+
end
|
13
|
+
|
14
|
+
def type
|
15
|
+
'validation_error'
|
16
|
+
end
|
17
|
+
|
18
|
+
def http_status_code
|
19
|
+
422
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
super.merge('field' => field, 'short_message' => short_message)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'model/configuration'
|
4
|
+
|
5
|
+
module GraphqlRails
|
6
|
+
# this module allows to convert any ruby class in to grapql type object
|
7
|
+
#
|
8
|
+
# usage:
|
9
|
+
# class YourModel
|
10
|
+
# include GraphqlRails::Model
|
11
|
+
#
|
12
|
+
# graphql do
|
13
|
+
# attribute :id
|
14
|
+
# attribute :title
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# YourModel.new.grapql_type # => type with [:id, :title] attributes
|
19
|
+
module Model
|
20
|
+
def self.included(base)
|
21
|
+
base.extend(ClassMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
# static methods for GraphqlRails::Model
|
25
|
+
module ClassMethods
|
26
|
+
def graphql
|
27
|
+
@graphql ||= Model::Configuration.new(self)
|
28
|
+
yield(@graphql) if block_given?
|
29
|
+
@graphql
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql_rails/attribute'
|
4
|
+
|
5
|
+
module GraphqlRails
|
6
|
+
module Model
|
7
|
+
# stores information about model specific config, like attributes and types
|
8
|
+
class Configuration
|
9
|
+
attr_reader :attributes
|
10
|
+
|
11
|
+
def initialize(model_class)
|
12
|
+
@model_class = model_class
|
13
|
+
@attributes = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def attribute(attribute_name, type: nil, hidden: false)
|
17
|
+
attributes[attribute_name.to_s] = Attribute.new(attribute_name, type, hidden: hidden)
|
18
|
+
end
|
19
|
+
|
20
|
+
def include_model_attributes(except: [])
|
21
|
+
except = Array(except).map(&:to_s)
|
22
|
+
|
23
|
+
if defined?(Mongoid) && model_class < Mongoid::Document
|
24
|
+
assign_default_mongoid_attributes(except: except)
|
25
|
+
elsif defined?(ActiveRecord) && model_class < ActiveRecord::Base
|
26
|
+
assign_default_active_record_attributes(except: except)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def graphql_type
|
31
|
+
@graphql_type ||= generate_graphql_type(graphql_type_name, visible_attributes)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :model_class
|
37
|
+
|
38
|
+
def visible_attributes
|
39
|
+
attributes.reject { |_name, attribute| attribute.hidden? }
|
40
|
+
end
|
41
|
+
|
42
|
+
def graphql_type_name
|
43
|
+
model_class.name.split('::').last
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_graphql_type(type_name, attributes)
|
47
|
+
GraphQL::ObjectType.define do
|
48
|
+
name(type_name)
|
49
|
+
description("Generated programmatically from model: #{type_name}")
|
50
|
+
|
51
|
+
attributes.each_value do |attribute|
|
52
|
+
field(attribute.field_name, attribute.graphql_field_type, property: attribute.name.to_sym)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def assign_default_mongoid_attributes(except: [])
|
58
|
+
allowed_fields = model_class.fields.except('_type', '_id', *except)
|
59
|
+
|
60
|
+
attribute('id', type: 'id')
|
61
|
+
|
62
|
+
allowed_fields.each_value do |field|
|
63
|
+
attribute(field.name, type: field.type.to_s.split('::').last)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def assign_default_active_record_attributes(except: [])
|
68
|
+
allowed_fields = model_class.columns.index_by(&:name).except('type', *except)
|
69
|
+
|
70
|
+
allowed_fields.each_value do |field|
|
71
|
+
field_type = field.cast_type.class.to_s.downcase.split('::').last
|
72
|
+
field_type = 'string' if field_type.ends_with?('string')
|
73
|
+
field_type = 'date' if field_type.include?('date')
|
74
|
+
field_type = 'time' if field_type.include?('time')
|
75
|
+
|
76
|
+
attribute(field.name, type: field_type.to_s.split('::').last)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|