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,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string/inflections'
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 ActionPathParser
10
+ # accepts path of given format "controller_name#action"
11
+ def initialize(action_path, **options)
12
+ @action_path = action_path
13
+ @module_name = options[:module] || ''
14
+ end
15
+
16
+ def return_type
17
+ return_type = action.return_type || default_type
18
+
19
+ if action.can_return_nil?
20
+ return_type
21
+ else
22
+ return_type.to_non_null_type
23
+ end
24
+ end
25
+
26
+ def arguments
27
+ action.attributes.values
28
+ end
29
+
30
+ def controller
31
+ @controller ||= "#{namespaced_controller_name}_controller".classify.constantize
32
+ end
33
+
34
+ def action_name
35
+ @action_name ||= action_path.split('#').last
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :action_path, :module_name
41
+
42
+ def action
43
+ controller.controller_configuration.action(action_name)
44
+ end
45
+
46
+ def namespaced_controller_name
47
+ [module_name, controller_name].reject(&:empty?).join('/')
48
+ end
49
+
50
+ def controller_name
51
+ @controller_name ||= action_path.split('#').first
52
+ end
53
+
54
+ def action_model
55
+ model_path = namespaced_controller_name.singularize.classify.split('::')
56
+ model_name = model_path.pop
57
+
58
+ while model_path.any?
59
+ begin
60
+ return [model_path, model_name].join('::').constantize
61
+ rescue NameError => err
62
+ raise unless err.message.match?(/uninitialized constant/)
63
+ model_path.pop
64
+ end
65
+ end
66
+
67
+ model_name.constantize
68
+ end
69
+
70
+ def default_type
71
+ action_model.graphql.graphql_type
72
+ end
73
+ end
74
+ end
75
+ 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
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ require_relative 'router/schema_builder'
6
+ require_relative 'router/mutation_action'
7
+ require_relative 'router/query_action'
8
+ require_relative 'router/resource_actions_builder'
9
+
10
+ module GraphqlRails
11
+ # graphql router that mimics Rails.application.routes
12
+ class Router
13
+ def self.draw(&block)
14
+ router = new
15
+ router.instance_eval(&block)
16
+ router.graphql_schema
17
+ end
18
+
19
+ attr_reader :actions, :namespace_name
20
+
21
+ def initialize(module_name: '')
22
+ @module_name = module_name
23
+ @actions ||= Set.new
24
+ end
25
+
26
+ def scope(**options, &block)
27
+ full_module_name = [module_name, options[:module]].reject(&:empty?).join('/')
28
+ scoped_router = self.class.new(module_name: full_module_name)
29
+ scoped_router.instance_eval(&block)
30
+ actions.merge(scoped_router.actions)
31
+ end
32
+
33
+ def resources(name, **options, &block)
34
+ builder_options = default_action_options.merge(options)
35
+ actions_builder = ResourceActionsBuilder.new(name, **builder_options)
36
+ actions_builder.instance_eval(&block) if block
37
+ actions.merge(actions_builder.actions)
38
+ end
39
+
40
+ def query(name, **options)
41
+ actions << build_action(QueryAction, name, **options)
42
+ end
43
+
44
+ def mutation(name, **options)
45
+ actions << build_action(MutationAction, name, **options)
46
+ end
47
+
48
+ def graphql_schema
49
+ SchemaBuilder.new(queries: actions.select(&:query?), mutations: actions.select(&:mutation?)).call
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :module_name
55
+
56
+ def build_action(action_builder, name, **options)
57
+ action_options = default_action_options.merge(options)
58
+ action_builder.new(name, action_options)
59
+ end
60
+
61
+ def default_action_options
62
+ { module: module_name }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../controller/controller_function'
4
+
5
+ module GraphqlRails
6
+ class Router
7
+ # Generic class for any type graphql action. Should not be used directly
8
+ class Action
9
+ attr_reader :name, :controller_action_path
10
+
11
+ def initialize(name, to:, **options)
12
+ @name = name.to_s.camelize(:lower)
13
+ @controller_action_path = [options[:module].to_s, to].reject(&:empty?).join('/')
14
+ end
15
+
16
+ def options
17
+ { function: Controller::ControllerFunction.build(controller_action_path) }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'action'
4
+
5
+ module GraphqlRails
6
+ class Router
7
+ # stores mutation type graphql action info
8
+ class MutationAction < Action
9
+ def query?
10
+ false
11
+ end
12
+
13
+ def mutation?
14
+ true
15
+ end
16
+ end
17
+ end
18
+ end