graphql_rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|