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,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
|