graphql_rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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