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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.hound.yml +3 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +34 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +5 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +20 -0
  10. data/Gemfile.lock +98 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +122 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +8 -0
  16. data/graphql_rails.gemspec +29 -0
  17. data/lib/graphiti.rb +10 -0
  18. data/lib/graphiti/attribute.rb +86 -0
  19. data/lib/graphiti/controller.rb +54 -0
  20. data/lib/graphiti/controller/action_configuration.rb +46 -0
  21. data/lib/graphiti/controller/configuration.rb +32 -0
  22. data/lib/graphiti/controller/controller_function.rb +41 -0
  23. data/lib/graphiti/controller/request.rb +40 -0
  24. data/lib/graphiti/errors/execution_error.rb +18 -0
  25. data/lib/graphiti/errors/validation_error.rb +26 -0
  26. data/lib/graphiti/model.rb +33 -0
  27. data/lib/graphiti/model/configuration.rb +81 -0
  28. data/lib/graphiti/router.rb +65 -0
  29. data/lib/graphiti/router/action.rb +21 -0
  30. data/lib/graphiti/router/mutation_action.rb +18 -0
  31. data/lib/graphiti/router/query_action.rb +18 -0
  32. data/lib/graphiti/router/resource_actions_builder.rb +82 -0
  33. data/lib/graphiti/router/schema_builder.rb +37 -0
  34. data/lib/graphiti/version.rb +5 -0
  35. data/lib/graphql_rails.rb +10 -0
  36. data/lib/graphql_rails/attribute.rb +86 -0
  37. data/lib/graphql_rails/controller.rb +54 -0
  38. data/lib/graphql_rails/controller/action_configuration.rb +46 -0
  39. data/lib/graphql_rails/controller/action_path_parser.rb +75 -0
  40. data/lib/graphql_rails/controller/configuration.rb +32 -0
  41. data/lib/graphql_rails/controller/controller_function.rb +41 -0
  42. data/lib/graphql_rails/controller/request.rb +40 -0
  43. data/lib/graphql_rails/errors/execution_error.rb +18 -0
  44. data/lib/graphql_rails/errors/validation_error.rb +26 -0
  45. data/lib/graphql_rails/model.rb +33 -0
  46. data/lib/graphql_rails/model/configuration.rb +81 -0
  47. data/lib/graphql_rails/router.rb +65 -0
  48. data/lib/graphql_rails/router/action.rb +21 -0
  49. data/lib/graphql_rails/router/mutation_action.rb +18 -0
  50. data/lib/graphql_rails/router/query_action.rb +18 -0
  51. data/lib/graphql_rails/router/resource_actions_builder.rb +82 -0
  52. data/lib/graphql_rails/router/schema_builder.rb +37 -0
  53. data/lib/graphql_rails/version.rb +5 -0
  54. 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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_rails/version'
4
+ require 'graphql_rails/model'
5
+ require 'graphql_rails/router'
6
+ require 'graphql_rails/controller'
7
+
8
+ # wonders starts here
9
+ module GraphqlRails
10
+ end
@@ -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