graphiti-rb 1.0.alpha.1

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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +20 -0
  6. data/.yardopts +2 -0
  7. data/Appraisals +11 -0
  8. data/CODE_OF_CONDUCT.md +49 -0
  9. data/Gemfile +12 -0
  10. data/Guardfile +32 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +75 -0
  13. data/Rakefile +15 -0
  14. data/bin/appraisal +17 -0
  15. data/bin/console +14 -0
  16. data/bin/rspec +17 -0
  17. data/bin/setup +8 -0
  18. data/gemfiles/rails_4.gemfile +17 -0
  19. data/gemfiles/rails_5.gemfile +17 -0
  20. data/graphiti.gemspec +34 -0
  21. data/lib/generators/jsonapi/resource_generator.rb +169 -0
  22. data/lib/generators/jsonapi/templates/application_resource.rb.erb +15 -0
  23. data/lib/generators/jsonapi/templates/controller.rb.erb +61 -0
  24. data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +30 -0
  25. data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +20 -0
  26. data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +22 -0
  27. data/lib/generators/jsonapi/templates/resource.rb.erb +11 -0
  28. data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
  29. data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
  30. data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +21 -0
  31. data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +34 -0
  32. data/lib/graphiti-rb.rb +1 -0
  33. data/lib/graphiti.rb +121 -0
  34. data/lib/graphiti/adapters/abstract.rb +516 -0
  35. data/lib/graphiti/adapters/active_record.rb +6 -0
  36. data/lib/graphiti/adapters/active_record/base.rb +249 -0
  37. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +17 -0
  38. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +17 -0
  39. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +17 -0
  40. data/lib/graphiti/adapters/active_record/inferrence.rb +12 -0
  41. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +30 -0
  42. data/lib/graphiti/adapters/null.rb +236 -0
  43. data/lib/graphiti/base.rb +70 -0
  44. data/lib/graphiti/configuration.rb +21 -0
  45. data/lib/graphiti/context.rb +16 -0
  46. data/lib/graphiti/deserializer.rb +208 -0
  47. data/lib/graphiti/errors.rb +309 -0
  48. data/lib/graphiti/extensions/boolean_attribute.rb +33 -0
  49. data/lib/graphiti/extensions/extra_attribute.rb +70 -0
  50. data/lib/graphiti/extensions/temp_id.rb +26 -0
  51. data/lib/graphiti/filter_operators.rb +25 -0
  52. data/lib/graphiti/hash_renderer.rb +57 -0
  53. data/lib/graphiti/jsonapi_serializable_ext.rb +50 -0
  54. data/lib/graphiti/query.rb +251 -0
  55. data/lib/graphiti/rails.rb +28 -0
  56. data/lib/graphiti/railtie.rb +74 -0
  57. data/lib/graphiti/renderer.rb +60 -0
  58. data/lib/graphiti/resource.rb +110 -0
  59. data/lib/graphiti/resource/configuration.rb +239 -0
  60. data/lib/graphiti/resource/dsl.rb +138 -0
  61. data/lib/graphiti/resource/interface.rb +32 -0
  62. data/lib/graphiti/resource/polymorphism.rb +68 -0
  63. data/lib/graphiti/resource/sideloading.rb +102 -0
  64. data/lib/graphiti/resource_proxy.rb +127 -0
  65. data/lib/graphiti/responders.rb +19 -0
  66. data/lib/graphiti/runner.rb +25 -0
  67. data/lib/graphiti/scope.rb +98 -0
  68. data/lib/graphiti/scoping/base.rb +99 -0
  69. data/lib/graphiti/scoping/default_filter.rb +58 -0
  70. data/lib/graphiti/scoping/extra_attributes.rb +29 -0
  71. data/lib/graphiti/scoping/filter.rb +93 -0
  72. data/lib/graphiti/scoping/filterable.rb +36 -0
  73. data/lib/graphiti/scoping/paginate.rb +87 -0
  74. data/lib/graphiti/scoping/sort.rb +64 -0
  75. data/lib/graphiti/sideload.rb +281 -0
  76. data/lib/graphiti/sideload/belongs_to.rb +34 -0
  77. data/lib/graphiti/sideload/has_many.rb +16 -0
  78. data/lib/graphiti/sideload/has_one.rb +9 -0
  79. data/lib/graphiti/sideload/many_to_many.rb +24 -0
  80. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +108 -0
  81. data/lib/graphiti/stats/dsl.rb +89 -0
  82. data/lib/graphiti/stats/payload.rb +49 -0
  83. data/lib/graphiti/types.rb +172 -0
  84. data/lib/graphiti/util/attribute_check.rb +88 -0
  85. data/lib/graphiti/util/field_params.rb +16 -0
  86. data/lib/graphiti/util/hash.rb +51 -0
  87. data/lib/graphiti/util/hooks.rb +33 -0
  88. data/lib/graphiti/util/include_params.rb +39 -0
  89. data/lib/graphiti/util/persistence.rb +219 -0
  90. data/lib/graphiti/util/relationship_payload.rb +64 -0
  91. data/lib/graphiti/util/serializer_attributes.rb +97 -0
  92. data/lib/graphiti/util/sideload.rb +33 -0
  93. data/lib/graphiti/util/validation_response.rb +78 -0
  94. data/lib/graphiti/version.rb +3 -0
  95. metadata +317 -0
@@ -0,0 +1,138 @@
1
+ module Graphiti
2
+ class Resource
3
+ module DSL
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def filter(name, *args, &blk)
8
+ opts = args.extract_options!
9
+
10
+ if att = get_attr(name, :filterable, raise_error: :only_unsupported)
11
+ aliases = [name, opts[:aliases]].flatten.compact
12
+ operators = FilterOperators.build(&blk)
13
+ config[:filters][name.to_sym] = {
14
+ aliases: aliases,
15
+ type: att[:type]
16
+ }.merge(operators.to_hash)
17
+ else
18
+ if type = args[0]
19
+ attribute name, type, only: [:filterable]
20
+ filter(name, opts, &blk)
21
+ else
22
+ raise Errors::ImplicitFilterTypeMissing.new(self, name)
23
+ end
24
+ end
25
+ end
26
+
27
+ def sort_all(&blk)
28
+ if block_given?
29
+ config[:_sort_all] = blk
30
+ else
31
+ config[:_sort_all]
32
+ end
33
+ end
34
+
35
+ def sort(name, *args, &blk)
36
+ opts = args.extract_options!
37
+
38
+ if get_attr(name, :sortable, raise_error: :only_unsupported)
39
+ config[:sorts][name] = blk
40
+ else
41
+ if type = args[0]
42
+ attribute name, type, only: [:sortable]
43
+ sort(name, opts, &blk)
44
+ else
45
+ raise Errors::ImplicitSortTypeMissing.new(self, name)
46
+ end
47
+ end
48
+ end
49
+
50
+ def paginate(&blk)
51
+ config[:pagination] = blk
52
+ end
53
+
54
+ def stat(symbol_or_hash, &blk)
55
+ dsl = Stats::DSL.new(adapter, symbol_or_hash)
56
+ dsl.instance_eval(&blk) if blk
57
+ config[:stats][dsl.name] = dsl
58
+ end
59
+
60
+ def default_filter(name = nil, &blk)
61
+ name ||= :__default
62
+ config[:default_filters][name.to_sym] = {
63
+ filter: blk
64
+ }
65
+ end
66
+
67
+ def before_commit(only: [:create, :update, :destroy], &blk)
68
+ Array(only).each do |verb|
69
+ config[:before_commit][verb] = blk
70
+ end
71
+ end
72
+
73
+ def attribute(name, type, options = {}, &blk)
74
+ raise Errors::TypeNotFound.new(self, name, type) unless Types[type]
75
+ attribute_option(options, :readable)
76
+ attribute_option(options, :writable)
77
+ attribute_option(options, :sortable)
78
+ attribute_option(options, :filterable)
79
+ options[:type] = type
80
+ options[:proc] = blk
81
+ config[:attributes][name] = options
82
+ apply_attributes_to_serializer
83
+ filter(name) if options[:filterable]
84
+ end
85
+
86
+ def extra_attribute(name, type, options = {}, &blk)
87
+ raise Errors::TypeNotFound.new(self, name, type) unless Types[type]
88
+ defaults = {
89
+ type: type,
90
+ proc: blk,
91
+ readable: true,
92
+ writable: false,
93
+ sortable: false,
94
+ filterable: false
95
+ }
96
+ options = defaults.merge(options)
97
+ config[:extra_attributes][name] = options
98
+ apply_extra_attributes_to_serializer
99
+ end
100
+
101
+ def all_attributes
102
+ attributes.merge(extra_attributes)
103
+ end
104
+
105
+ def apply_attributes_to_serializer
106
+ serializer.type(type)
107
+ Util::SerializerAttributes.new(self, attributes).apply
108
+ end
109
+ private :apply_attributes_to_serializer
110
+
111
+ def apply_extra_attributes_to_serializer
112
+ Util::SerializerAttributes.new(self, extra_attributes, true).apply
113
+ end
114
+
115
+ def attribute_option(options, name)
116
+ if options[name] != false
117
+ default = if only = options[:only]
118
+ Array(only).include?(name) ? true : false
119
+ elsif except = options[:except]
120
+ Array(except).include?(name) ? false : true
121
+ else
122
+ send(:"attributes_#{name}_by_default")
123
+ end
124
+ options[name] ||= default
125
+ end
126
+ end
127
+ private :attribute_option
128
+
129
+ def relationship_option(options, name)
130
+ if options[name] != false
131
+ options[name] ||= send(:"relationships_#{name}_by_default")
132
+ end
133
+ end
134
+ private :attribute_option
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,32 @@
1
+ module Graphiti
2
+ class Resource
3
+ module Interface
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def all(params = {}, base_scope = nil)
8
+ _all(params, {}, base_scope)
9
+ end
10
+
11
+ def _all(params, opts, base_scope)
12
+ runner = Runner.new(self, params)
13
+ runner.proxy(base_scope, opts)
14
+ end
15
+
16
+ def find(params, base_scope = nil)
17
+ id = params[:data].try(:[], :id) || params.delete(:id)
18
+ params[:filter] ||= {}
19
+ params[:filter].merge!(id: id)
20
+
21
+ runner = Runner.new(self, params)
22
+ runner.proxy(base_scope, single: true, raise_on_missing: true)
23
+ end
24
+
25
+ def build(params, base_scope = nil)
26
+ runner = Runner.new(self, params)
27
+ runner.proxy(base_scope, single: true, raise_on_missing: true)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,68 @@
1
+ # For "Rails STI" behavior
2
+ # CreditCard.all # => [<Visa>, <Mastercard>, etc]
3
+ module Graphiti
4
+ class Resource
5
+ module Polymorphism
6
+ def self.prepended(klass)
7
+ klass.extend ClassMethods
8
+ end
9
+
10
+ def serializer_for(model)
11
+ if polymorphic_child?
12
+ serializer
13
+ else
14
+ child = self.class.resource_for_model(model)
15
+ child.serializer
16
+ end
17
+ end
18
+
19
+ def associate_all(parent, children, association_name, type)
20
+ children.each do |c|
21
+ associate(parent, c, association_name, type)
22
+ end
23
+ end
24
+
25
+ def associate(parent, child, association_name, type)
26
+ child_resource = self.class.resource_for_model(parent)
27
+ if child_resource.sideloads[association_name]
28
+ child_resource.adapter
29
+ .associate(parent, child, association_name, type)
30
+ end
31
+ end
32
+
33
+ module ClassMethods
34
+ def inherited(klass)
35
+ klass.type = nil
36
+ klass.model = klass.infer_model
37
+ klass.polymorphic_child = true
38
+ super
39
+ end
40
+
41
+ def sideload(name)
42
+ sl = super
43
+ if !polymorphic_child? && sl.nil?
44
+ children.each do |c|
45
+ break if sl = c.sideloads[name]
46
+ end
47
+ end
48
+ sl
49
+ end
50
+
51
+ def children
52
+ @children ||= polymorphic.map do |klass|
53
+ klass.is_a?(String) ? klass.safe_constantize : klass
54
+ end
55
+ end
56
+
57
+ def resource_for_model(model)
58
+ resource = children.find { |c| model.is_a?(c.model) }
59
+ if resource.nil?
60
+ raise Errors::PolymorphicChildNotFound.new(self, model)
61
+ else
62
+ resource
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,102 @@
1
+ module Graphiti
2
+ class Resource
3
+ module Sideloading
4
+ def self.included(klass)
5
+ klass.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def allow_sideload(name, opts = {}, &blk)
10
+ klass = Class.new(opts.delete(:class) || Sideload)
11
+ klass.class_eval(&blk) if blk
12
+ opts[:parent_resource] = self
13
+ relationship_option(opts, :readable)
14
+ relationship_option(opts, :writable)
15
+ sideload = klass.new(name, opts)
16
+ if parent = opts[:parent]
17
+ parent.children[name] = sideload
18
+ else
19
+ config[:sideloads][name] = sideload
20
+ apply_sideloads_to_serializer
21
+ end
22
+ sideload
23
+ end
24
+
25
+ def apply_sideloads_to_serializer
26
+ config[:sideloads].each_pair do |name, sideload|
27
+ if serializer.relationship_blocks[name].nil? && sideload.readable?
28
+ serializer.relationship(name)
29
+ end
30
+ end
31
+ end
32
+
33
+ def has_many(name, opts = {}, &blk)
34
+ opts[:class] = adapter.sideloading_classes[:has_many]
35
+ allow_sideload(name, opts, &blk)
36
+ end
37
+
38
+ def belongs_to(name, opts = {}, &blk)
39
+ opts[:class] = adapter.sideloading_classes[:belongs_to]
40
+ allow_sideload(name, opts, &blk)
41
+ end
42
+
43
+ def has_one(name, opts = {}, &blk)
44
+ opts[:class] = adapter.sideloading_classes[:has_one]
45
+ allow_sideload(name, opts, &blk)
46
+ end
47
+
48
+ def many_to_many(name, opts = {}, &blk)
49
+ opts[:class] = adapter.sideloading_classes[:many_to_many]
50
+ allow_sideload(name, opts, &blk)
51
+ end
52
+
53
+ def polymorphic_belongs_to(name, opts = {}, &blk)
54
+ opts[:resource] ||= Class.new(::Graphiti::Resource) do
55
+ self.polymorphic = []
56
+ self.abstract_class = true
57
+ end
58
+ # adapters *probably* don't need to override this, but it's allowed
59
+ opts[:class] ||= adapter.sideloading_classes[:polymorphic_belongs_to]
60
+ opts[:class] ||= ::Graphiti::Sideload::PolymorphicBelongsTo
61
+ allow_sideload(name, opts, &blk)
62
+ end
63
+
64
+ def sideload(name)
65
+ sideloads[name]
66
+ end
67
+
68
+ def all_sideloads(memo = {})
69
+ sideloads.each_pair do |name, sideload|
70
+ unless memo[name]
71
+ memo[name] = sideload
72
+ memo.merge!(sideload.resource.class.all_sideloads(memo))
73
+ end
74
+ end
75
+ memo
76
+ end
77
+
78
+ def association_names(memo = [])
79
+ all_sideloads.each_pair do |name, sl|
80
+ unless memo.include?(sl.name)
81
+ memo << sl.name
82
+ memo |= sl.resource.class.association_names(memo)
83
+ end
84
+ end
85
+
86
+ memo
87
+ end
88
+
89
+ def association_types(memo = [])
90
+ all_sideloads.each_pair do |name, sl|
91
+ unless memo.include?(sl.resource.type)
92
+ memo << sl.resource.type
93
+ memo |= sl.resource.class.association_types(memo)
94
+ end
95
+ end
96
+
97
+ memo
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,127 @@
1
+ module Graphiti
2
+ class ResourceProxy
3
+ include Enumerable
4
+
5
+ attr_reader :resource, :query, :scope
6
+
7
+ def initialize(resource, scope, query, payload: nil, single: false, raise_on_missing: false)
8
+ @resource = resource
9
+ @scope = scope
10
+ @query = query
11
+ @payload = payload
12
+ @single = single
13
+ @raise_on_missing = raise_on_missing
14
+ end
15
+
16
+ def single?
17
+ !!@single
18
+ end
19
+
20
+ def raise_on_missing?
21
+ !!@raise_on_missing
22
+ end
23
+
24
+ def errors
25
+ data.errors
26
+ end
27
+
28
+ def [](val)
29
+ data[val]
30
+ end
31
+
32
+ def jsonapi_render_options(opts = {})
33
+ opts[:meta] ||= {}
34
+ opts[:expose] ||= {}
35
+ opts[:expose][:context] = Graphiti.context[:object]
36
+ opts
37
+ end
38
+
39
+ def to_jsonapi(options = {})
40
+ options = jsonapi_render_options(options)
41
+ Renderer.new(self, options).to_jsonapi
42
+ end
43
+
44
+ def to_json(options = {})
45
+ Renderer.new(self, options).to_json
46
+ end
47
+
48
+ def to_xml(options = {})
49
+ Renderer.new(self, options).to_xml
50
+ end
51
+
52
+ def data
53
+ @data ||= begin
54
+ records = @scope.resolve
55
+ if records.empty? && raise_on_missing?
56
+ raise Graphiti::Errors::RecordNotFound
57
+ end
58
+ records = records[0] if single?
59
+ records
60
+ end
61
+ end
62
+ alias :to_a :data
63
+
64
+ def each(&blk)
65
+ to_a.each(&blk)
66
+ end
67
+
68
+ def stats
69
+ @stats ||= @scope.resolve_stats
70
+ end
71
+
72
+ def save(action: :create)
73
+ validator = persist do
74
+ @resource.persist_with_relationships \
75
+ @payload.meta(action: action),
76
+ @payload.attributes,
77
+ @payload.relationships
78
+ end
79
+ @data, success = validator.to_a
80
+ success
81
+ end
82
+
83
+ def destroy
84
+ validator = @resource.transaction do
85
+ model = @resource.destroy(@query.filters[:id])
86
+ model.instance_variable_set(:@__serializer_klass, @resource.serializer)
87
+ validator = ::Graphiti::Util::ValidationResponse.new \
88
+ model, @payload
89
+ validator.validate!
90
+ @resource.before_commit(model, :destroy)
91
+ validator
92
+ end
93
+ @data, success = validator.to_a
94
+ success
95
+ end
96
+
97
+ def update_attributes
98
+ save(action: :update)
99
+ end
100
+
101
+ def include_hash
102
+ @payload ? @payload.include_hash : @query.include_hash
103
+ end
104
+
105
+ def fields
106
+ query.fields
107
+ end
108
+
109
+ def extra_fields
110
+ query.extra_fields
111
+ end
112
+
113
+ private
114
+
115
+ def persist
116
+ @resource.transaction do
117
+ ::Graphiti::Util::Hooks.record do
118
+ model = yield
119
+ validator = ::Graphiti::Util::ValidationResponse.new \
120
+ model, @payload
121
+ validator.validate!
122
+ validator
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end