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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/.yardopts +2 -0
- data/Appraisals +11 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +12 -0
- data/Guardfile +32 -0
- data/LICENSE.txt +21 -0
- data/README.md +75 -0
- data/Rakefile +15 -0
- data/bin/appraisal +17 -0
- data/bin/console +14 -0
- data/bin/rspec +17 -0
- data/bin/setup +8 -0
- data/gemfiles/rails_4.gemfile +17 -0
- data/gemfiles/rails_5.gemfile +17 -0
- data/graphiti.gemspec +34 -0
- data/lib/generators/jsonapi/resource_generator.rb +169 -0
- data/lib/generators/jsonapi/templates/application_resource.rb.erb +15 -0
- data/lib/generators/jsonapi/templates/controller.rb.erb +61 -0
- data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +30 -0
- data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +20 -0
- data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +22 -0
- data/lib/generators/jsonapi/templates/resource.rb.erb +11 -0
- data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
- data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
- data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +21 -0
- data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +34 -0
- data/lib/graphiti-rb.rb +1 -0
- data/lib/graphiti.rb +121 -0
- data/lib/graphiti/adapters/abstract.rb +516 -0
- data/lib/graphiti/adapters/active_record.rb +6 -0
- data/lib/graphiti/adapters/active_record/base.rb +249 -0
- data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +17 -0
- data/lib/graphiti/adapters/active_record/has_many_sideload.rb +17 -0
- data/lib/graphiti/adapters/active_record/has_one_sideload.rb +17 -0
- data/lib/graphiti/adapters/active_record/inferrence.rb +12 -0
- data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +30 -0
- data/lib/graphiti/adapters/null.rb +236 -0
- data/lib/graphiti/base.rb +70 -0
- data/lib/graphiti/configuration.rb +21 -0
- data/lib/graphiti/context.rb +16 -0
- data/lib/graphiti/deserializer.rb +208 -0
- data/lib/graphiti/errors.rb +309 -0
- data/lib/graphiti/extensions/boolean_attribute.rb +33 -0
- data/lib/graphiti/extensions/extra_attribute.rb +70 -0
- data/lib/graphiti/extensions/temp_id.rb +26 -0
- data/lib/graphiti/filter_operators.rb +25 -0
- data/lib/graphiti/hash_renderer.rb +57 -0
- data/lib/graphiti/jsonapi_serializable_ext.rb +50 -0
- data/lib/graphiti/query.rb +251 -0
- data/lib/graphiti/rails.rb +28 -0
- data/lib/graphiti/railtie.rb +74 -0
- data/lib/graphiti/renderer.rb +60 -0
- data/lib/graphiti/resource.rb +110 -0
- data/lib/graphiti/resource/configuration.rb +239 -0
- data/lib/graphiti/resource/dsl.rb +138 -0
- data/lib/graphiti/resource/interface.rb +32 -0
- data/lib/graphiti/resource/polymorphism.rb +68 -0
- data/lib/graphiti/resource/sideloading.rb +102 -0
- data/lib/graphiti/resource_proxy.rb +127 -0
- data/lib/graphiti/responders.rb +19 -0
- data/lib/graphiti/runner.rb +25 -0
- data/lib/graphiti/scope.rb +98 -0
- data/lib/graphiti/scoping/base.rb +99 -0
- data/lib/graphiti/scoping/default_filter.rb +58 -0
- data/lib/graphiti/scoping/extra_attributes.rb +29 -0
- data/lib/graphiti/scoping/filter.rb +93 -0
- data/lib/graphiti/scoping/filterable.rb +36 -0
- data/lib/graphiti/scoping/paginate.rb +87 -0
- data/lib/graphiti/scoping/sort.rb +64 -0
- data/lib/graphiti/sideload.rb +281 -0
- data/lib/graphiti/sideload/belongs_to.rb +34 -0
- data/lib/graphiti/sideload/has_many.rb +16 -0
- data/lib/graphiti/sideload/has_one.rb +9 -0
- data/lib/graphiti/sideload/many_to_many.rb +24 -0
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +108 -0
- data/lib/graphiti/stats/dsl.rb +89 -0
- data/lib/graphiti/stats/payload.rb +49 -0
- data/lib/graphiti/types.rb +172 -0
- data/lib/graphiti/util/attribute_check.rb +88 -0
- data/lib/graphiti/util/field_params.rb +16 -0
- data/lib/graphiti/util/hash.rb +51 -0
- data/lib/graphiti/util/hooks.rb +33 -0
- data/lib/graphiti/util/include_params.rb +39 -0
- data/lib/graphiti/util/persistence.rb +219 -0
- data/lib/graphiti/util/relationship_payload.rb +64 -0
- data/lib/graphiti/util/serializer_attributes.rb +97 -0
- data/lib/graphiti/util/sideload.rb +33 -0
- data/lib/graphiti/util/validation_response.rb +78 -0
- data/lib/graphiti/version.rb +3 -0
- metadata +317 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Base
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def jsonapi_resource
|
6
|
+
@jsonapi_resource
|
7
|
+
end
|
8
|
+
|
9
|
+
def query
|
10
|
+
@query ||= Query.new(jsonapi_resource, params)
|
11
|
+
end
|
12
|
+
|
13
|
+
def query_hash
|
14
|
+
@query_hash ||= query.to_hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def wrap_context
|
18
|
+
Graphiti.with_context(jsonapi_context, action_name.to_sym) do
|
19
|
+
yield
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def jsonapi_context
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def jsonapi_scope(scope, opts = {})
|
28
|
+
jsonapi_resource.build_scope(scope, query, opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
def normalized_params
|
32
|
+
normalized = params
|
33
|
+
if normalized.respond_to?(:to_unsafe_h)
|
34
|
+
normalized = normalized.to_unsafe_h.deep_symbolize_keys
|
35
|
+
end
|
36
|
+
normalized
|
37
|
+
end
|
38
|
+
|
39
|
+
def deserialized_params
|
40
|
+
@deserialized_params ||= begin
|
41
|
+
payload = normalized_params
|
42
|
+
if payload[:data] && payload[:data][:type]
|
43
|
+
Graphiti::Deserializer.new(payload)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def jsonapi_render_options
|
49
|
+
options = {}
|
50
|
+
options.merge!(default_jsonapi_render_options)
|
51
|
+
options[:meta] ||= {}
|
52
|
+
options[:expose] ||= {}
|
53
|
+
options[:expose][:context] = jsonapi_context
|
54
|
+
options
|
55
|
+
end
|
56
|
+
|
57
|
+
def proxy(base = nil, opts = {})
|
58
|
+
base ||= jsonapi_resource.base_scope
|
59
|
+
scope_opts = opts.slice :sideload_parent_length,
|
60
|
+
:default_paginate, :after_resolve
|
61
|
+
scope = jsonapi_scope(base, scope_opts)
|
62
|
+
ResourceProxy.new jsonapi_resource,
|
63
|
+
scope,
|
64
|
+
query,
|
65
|
+
payload: deserialized_params,
|
66
|
+
single: opts[:single],
|
67
|
+
raise_on_missing: opts[:raise_on_missing]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# https://robots.thoughtbot.com/mygem-configure-block
|
2
|
+
module Graphiti
|
3
|
+
class Configuration
|
4
|
+
# @return [Boolean] Should we raise when the client requests a relationship not defined on the server?
|
5
|
+
# Defaults to true.
|
6
|
+
attr_accessor :raise_on_missing_sideload
|
7
|
+
# @return [Boolean] Concurrently fetch sideloads?
|
8
|
+
# Defaults to false OR if classes are cached (Rails-only)
|
9
|
+
attr_accessor :concurrency
|
10
|
+
|
11
|
+
attr_accessor :respond_to
|
12
|
+
|
13
|
+
# Set defaults
|
14
|
+
# @api private
|
15
|
+
def initialize
|
16
|
+
@raise_on_missing_sideload = true
|
17
|
+
@concurrency = false
|
18
|
+
@respond_to = [:json, :jsonapi, :xml]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Context
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module Overrides
|
6
|
+
def sideload_whitelist=(val)
|
7
|
+
super(JSONAPI::IncludeDirective.new(val).to_hash)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
included do
|
12
|
+
class_attribute :sideload_whitelist
|
13
|
+
class << self;prepend Overrides;end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# Responsible for parsing incoming write payloads
|
2
|
+
#
|
3
|
+
# Given a PUT payload like:
|
4
|
+
#
|
5
|
+
# {
|
6
|
+
# data: {
|
7
|
+
# id: '1',
|
8
|
+
# type: 'posts',
|
9
|
+
# attributes: { title: 'My Title' },
|
10
|
+
# relationships: {
|
11
|
+
# author: {
|
12
|
+
# data: {
|
13
|
+
# id: '1',
|
14
|
+
# type: 'authors'
|
15
|
+
# }
|
16
|
+
# }
|
17
|
+
# }
|
18
|
+
# },
|
19
|
+
# included: [
|
20
|
+
# {
|
21
|
+
# id: '1'
|
22
|
+
# type: 'authors',
|
23
|
+
# attributes: { name: 'Joe Author' }
|
24
|
+
# }
|
25
|
+
# ]
|
26
|
+
# }
|
27
|
+
#
|
28
|
+
# You can now easily deal with this payload:
|
29
|
+
#
|
30
|
+
# deserializer.attributes
|
31
|
+
# # => { id: '1', title: 'My Title' }
|
32
|
+
# deserializer.meta
|
33
|
+
# # => { type: 'posts', method: :update }
|
34
|
+
# deserializer.relationships
|
35
|
+
# # {
|
36
|
+
# # author: {
|
37
|
+
# # meta: { ... },
|
38
|
+
# # attributes: { ... },
|
39
|
+
# # relationships: { ... }
|
40
|
+
# # }
|
41
|
+
# # }
|
42
|
+
#
|
43
|
+
# When creating objects, we accept a +temp-id+ so that the client can track
|
44
|
+
# the object it just created. Expect this in +meta+:
|
45
|
+
#
|
46
|
+
# { type: 'authors', method: :create, temp_id: 'abc123' }
|
47
|
+
class Graphiti::Deserializer
|
48
|
+
def initialize(payload)
|
49
|
+
@payload = payload
|
50
|
+
@payload = @payload[:_jsonapi] if @payload.has_key?(:_jsonapi)
|
51
|
+
end
|
52
|
+
|
53
|
+
def params
|
54
|
+
@payload
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Hash] the raw :data value of the payload
|
58
|
+
def data
|
59
|
+
@payload[:data]
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [String] the raw :id value of the payload
|
63
|
+
def id
|
64
|
+
data[:id]
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Hash] the raw :attributes hash + +id+
|
68
|
+
def attributes
|
69
|
+
@attributes ||= raw_attributes.tap do |hash|
|
70
|
+
hash[:id] = id if id
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Override the attributes
|
75
|
+
# # @see #attributes
|
76
|
+
def attributes=(attrs)
|
77
|
+
@attributes = attrs
|
78
|
+
end
|
79
|
+
|
80
|
+
# 'meta' information about this resource. Includes:
|
81
|
+
#
|
82
|
+
# +type+: the jsonapi type
|
83
|
+
# +method+: create/update/destroy/disassociate. Based on the request env or the +method+ within the +relationships+ hash
|
84
|
+
# +temp_id+: the +temp-id+, if specified
|
85
|
+
#
|
86
|
+
# @return [Hash]
|
87
|
+
def meta(action: :update)
|
88
|
+
{
|
89
|
+
type: data[:type],
|
90
|
+
temp_id: data[:'temp-id'],
|
91
|
+
method: action
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Hash] the relationships hash
|
96
|
+
def relationships
|
97
|
+
@relationships ||= process_relationships(raw_relationships)
|
98
|
+
end
|
99
|
+
|
100
|
+
def include_hash(memo = {}, relationship_node = nil)
|
101
|
+
relationship_node ||= relationships
|
102
|
+
|
103
|
+
relationship_node.each_pair do |name, relationship_payload|
|
104
|
+
merge_include_hash(memo, name, relationship_payload)
|
105
|
+
end
|
106
|
+
|
107
|
+
memo
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def merge_include_hash(memo, name, relationship_payload)
|
113
|
+
arrayified = [relationship_payload].flatten
|
114
|
+
return if arrayified.all? { |rp| removed?(rp) }
|
115
|
+
|
116
|
+
memo[name] ||= {}
|
117
|
+
deep_merge!(memo[name], nested_include_hashes(memo[name], arrayified))
|
118
|
+
memo
|
119
|
+
end
|
120
|
+
|
121
|
+
def included
|
122
|
+
@payload[:included] || []
|
123
|
+
end
|
124
|
+
|
125
|
+
def removed?(relationship_payload)
|
126
|
+
method = relationship_payload[:meta][:method]
|
127
|
+
[:disassociate, :destroy].include?(method)
|
128
|
+
end
|
129
|
+
|
130
|
+
def nested_include_hashes(memo, relationship_payloads)
|
131
|
+
{}.tap do |subs|
|
132
|
+
relationship_payloads.each do |rp|
|
133
|
+
nested = include_hash(memo, rp[:relationships])
|
134
|
+
deep_merge!(subs, nested)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def deep_merge!(a, b)
|
140
|
+
Graphiti::Util::Hash.deep_merge!(a, b)
|
141
|
+
end
|
142
|
+
|
143
|
+
def process_relationships(relationship_hash)
|
144
|
+
{}.tap do |hash|
|
145
|
+
relationship_hash.each_pair do |name, relationship_payload|
|
146
|
+
name = name.to_sym
|
147
|
+
|
148
|
+
if relationship_payload[:data]
|
149
|
+
hash[name] = process_relationship(relationship_payload[:data])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def process_relationship(relationship_data)
|
156
|
+
if relationship_data.is_a?(Array)
|
157
|
+
relationship_data.map do |rd|
|
158
|
+
process_relationship_datum(rd)
|
159
|
+
end
|
160
|
+
else
|
161
|
+
process_relationship_datum(relationship_data)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def process_relationship_datum(datum)
|
166
|
+
temp_id = datum[:'temp-id']
|
167
|
+
included_object = included.find do |i|
|
168
|
+
next unless i[:type] == datum[:type]
|
169
|
+
|
170
|
+
(i[:id] && i[:id] == datum[:id]) ||
|
171
|
+
(i[:'temp-id'] && i[:'temp-id'] == temp_id)
|
172
|
+
end
|
173
|
+
included_object ||= {}
|
174
|
+
included_object[:relationships] ||= {}
|
175
|
+
|
176
|
+
attributes = included_object[:attributes] || {}
|
177
|
+
attributes[:id] = datum[:id] if datum[:id]
|
178
|
+
relationships = process_relationships(included_object[:relationships] || {})
|
179
|
+
method = datum[:method]
|
180
|
+
method = method.to_sym if method
|
181
|
+
|
182
|
+
{
|
183
|
+
meta: {
|
184
|
+
jsonapi_type: datum[:type],
|
185
|
+
temp_id: temp_id,
|
186
|
+
method: method
|
187
|
+
},
|
188
|
+
attributes: attributes,
|
189
|
+
relationships: relationships
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
def raw_attributes
|
194
|
+
if data
|
195
|
+
data[:attributes] || {}
|
196
|
+
else
|
197
|
+
{}
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def raw_relationships
|
202
|
+
if data
|
203
|
+
data[:relationships] || {}
|
204
|
+
else
|
205
|
+
{}
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,309 @@
|
|
1
|
+
module Graphiti
|
2
|
+
module Errors
|
3
|
+
class Base < StandardError;end
|
4
|
+
|
5
|
+
class AdapterNotImplemented < Base
|
6
|
+
def initialize(adapter, attribute, method)
|
7
|
+
@adapter = adapter
|
8
|
+
@attribute = attribute
|
9
|
+
@method = method
|
10
|
+
end
|
11
|
+
|
12
|
+
def message
|
13
|
+
<<-MSG
|
14
|
+
The adapter #{@adapter.class} does not implement method '#{@method}', which was requested for attribute '#{@attribute}'. Add this method to your adapter to support this filter operator.
|
15
|
+
MSG
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class AttributeError < Base
|
20
|
+
attr_reader :resource,
|
21
|
+
:name,
|
22
|
+
:flag,
|
23
|
+
:exists,
|
24
|
+
:request,
|
25
|
+
:guard
|
26
|
+
|
27
|
+
def initialize(resource, name, flag, **opts)
|
28
|
+
@resource = resource
|
29
|
+
@name = name
|
30
|
+
@flag = flag
|
31
|
+
@exists = opts[:exists] || false
|
32
|
+
@request = opts[:request] || false
|
33
|
+
@guard = opts[:guard]
|
34
|
+
end
|
35
|
+
|
36
|
+
def action
|
37
|
+
if @request
|
38
|
+
{
|
39
|
+
sortable: 'sort on',
|
40
|
+
filterable: 'filter on',
|
41
|
+
readable: 'read',
|
42
|
+
writable: 'write'
|
43
|
+
}[@flag]
|
44
|
+
else
|
45
|
+
{
|
46
|
+
sortable: 'add sort',
|
47
|
+
filterable: 'add filter',
|
48
|
+
readable: 'read',
|
49
|
+
writable: 'write'
|
50
|
+
}[@flag]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def resource_name
|
55
|
+
name = if @resource.is_a?(Graphiti::Resource)
|
56
|
+
@resource.class.name
|
57
|
+
else
|
58
|
+
@resource.name
|
59
|
+
end
|
60
|
+
name || 'AnonymousResourceClass'
|
61
|
+
end
|
62
|
+
|
63
|
+
def message
|
64
|
+
msg = "#{resource_name}: Tried to #{action} attribute #{@name.inspect}"
|
65
|
+
if @exists
|
66
|
+
if @guard
|
67
|
+
msg << ", but the guard #{@guard.inspect} did not pass."
|
68
|
+
else
|
69
|
+
msg << ", but the attribute was marked #{@flag.inspect} => false."
|
70
|
+
end
|
71
|
+
else
|
72
|
+
msg << ", but could not find an attribute with that name."
|
73
|
+
end
|
74
|
+
msg
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class PolymorphicChildNotFound < Base
|
79
|
+
def initialize(resource_class, model)
|
80
|
+
@resource_class = resource_class
|
81
|
+
@model = model
|
82
|
+
end
|
83
|
+
|
84
|
+
def message
|
85
|
+
<<-MSG
|
86
|
+
#{@resource_class}: Tried to find subclass with model #{@model.class}, but nothing found!
|
87
|
+
|
88
|
+
Make sure all your child classes are assigned and associated to the right models:
|
89
|
+
|
90
|
+
self.polymorphic = ['Subclass1Resource', 'Subclass2Resource']
|
91
|
+
MSG
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class ValidationError < Base
|
96
|
+
attr_reader :validation_response
|
97
|
+
|
98
|
+
def initialize(validation_response)
|
99
|
+
@validation_response = validation_response
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class ImplicitFilterTypeMissing < Base
|
104
|
+
def initialize(resource_class, name)
|
105
|
+
@resource_class = resource_class
|
106
|
+
@name = name
|
107
|
+
end
|
108
|
+
|
109
|
+
def message
|
110
|
+
<<-MSG
|
111
|
+
Tried to add filter-only attribute #{@name.inspect}, but type was missing!
|
112
|
+
|
113
|
+
If you are adding a filter that does not have a corresponding attribute, you must pass a type:
|
114
|
+
|
115
|
+
filter :name, :string do <--- like this
|
116
|
+
# ... code ...
|
117
|
+
end
|
118
|
+
MSG
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class ImplicitSortTypeMissing < Base
|
123
|
+
def initialize(resource_class, name)
|
124
|
+
@resource_class = resource_class
|
125
|
+
@name = name
|
126
|
+
end
|
127
|
+
|
128
|
+
def message
|
129
|
+
<<-MSG
|
130
|
+
Tried to add sort-only attribute #{@name.inspect}, but type was missing!
|
131
|
+
|
132
|
+
If you are adding a sort that does not have a corresponding attribute, you must pass a type:
|
133
|
+
|
134
|
+
sort :name, :string do <--- like this
|
135
|
+
# ... code ...
|
136
|
+
end
|
137
|
+
MSG
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class TypecastFailed < Base
|
142
|
+
def initialize(resource, name, value, error)
|
143
|
+
@resource = resource
|
144
|
+
@name = name
|
145
|
+
@value = value
|
146
|
+
@error = error
|
147
|
+
end
|
148
|
+
|
149
|
+
def message
|
150
|
+
<<-MSG
|
151
|
+
#{@resource.class}: Failed typecasting #{@name.inspect}! Given #{@value.inspect} but the following error was raised:
|
152
|
+
|
153
|
+
#{@error.message}
|
154
|
+
|
155
|
+
#{@error.backtrace.join("\n")}
|
156
|
+
MSG
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class ModelNotFound < Base
|
161
|
+
def initialize(resource_class)
|
162
|
+
@resource_class = resource_class
|
163
|
+
end
|
164
|
+
|
165
|
+
def message
|
166
|
+
<<-MSG
|
167
|
+
Could not find model for Resource '#{@resource_class}'
|
168
|
+
|
169
|
+
Manually set model (self.model = MyModel) if it does not match name of the Resource.
|
170
|
+
MSG
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class TypeNotFound < Base
|
175
|
+
def initialize(resource, attribute, type)
|
176
|
+
@resource = resource
|
177
|
+
@attribute = attribute
|
178
|
+
@type = type
|
179
|
+
end
|
180
|
+
|
181
|
+
def message
|
182
|
+
<<-MSG
|
183
|
+
Could not find type #{@type.inspect}! This was specified on attribute #{@attribute.inspect} within resource #{@resource.name}
|
184
|
+
|
185
|
+
Valid types are: #{Graphiti::Types.map.keys.inspect}
|
186
|
+
MSG
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class PolymorphicChildNotFound < Base
|
191
|
+
def initialize(sideload, name)
|
192
|
+
@sideload = sideload
|
193
|
+
@name = name
|
194
|
+
end
|
195
|
+
|
196
|
+
def message
|
197
|
+
<<-MSG
|
198
|
+
#{@sideload.parent_resource}: Found record with #{@sideload.grouper.field_name.inspect} == #{@name.inspect}, which is not registered!
|
199
|
+
|
200
|
+
Register the behavior of different types like so:
|
201
|
+
|
202
|
+
polymorphic_belongs_to #{@sideload.name.inspect} do
|
203
|
+
group_by(#{@sideload.grouper.field_name.inspect}) do
|
204
|
+
on(#{@name.to_sym.inspect}) <---- this is what's missing
|
205
|
+
on(:foo).belongs_to :foo, resource: FooResource (long-hand example)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
MSG
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
class ResourceNotFound < Base
|
213
|
+
def initialize(resource, sideload_name)
|
214
|
+
@resource = resource
|
215
|
+
@sideload_name = sideload_name
|
216
|
+
end
|
217
|
+
|
218
|
+
def message
|
219
|
+
<<-MSG
|
220
|
+
Could not find resource class for sideload '#{@sideload_name}' on Resource '#{@resource.class.name}'!
|
221
|
+
|
222
|
+
If this follows a non-standard naming convention, use the :resource option to pass it directly:
|
223
|
+
|
224
|
+
has_many :comments, resource: SpecialCommentResource
|
225
|
+
MSG
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class UnsupportedPagination < Base
|
230
|
+
def message
|
231
|
+
<<-MSG
|
232
|
+
It looks like you are requesting pagination of a sideload, but there are > 1 parents.
|
233
|
+
|
234
|
+
This is not supported. In other words, you can do
|
235
|
+
|
236
|
+
/employees/1?include=positions&page[positions][size]=2
|
237
|
+
|
238
|
+
But not
|
239
|
+
|
240
|
+
/employees?include=positions&page[positions][size]=2
|
241
|
+
|
242
|
+
This is a limitation of most datastores; the same issue exists in ActiveRecord.
|
243
|
+
|
244
|
+
Consider using a named relationship instead, e.g. 'has_one :top_comment'
|
245
|
+
MSG
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
class UnsupportedPageSize < Base
|
250
|
+
def initialize(size, max)
|
251
|
+
@size, @max = size, max
|
252
|
+
end
|
253
|
+
|
254
|
+
def message
|
255
|
+
"Requested page size #{@size} is greater than max supported size #{@max}"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
class InvalidInclude < Base
|
260
|
+
def initialize(relationship, parent_resource)
|
261
|
+
@relationship = relationship
|
262
|
+
@parent_resource = parent_resource
|
263
|
+
end
|
264
|
+
|
265
|
+
def message
|
266
|
+
"The requested included relationship \"#{@relationship}\" is not supported on resource \"#{@parent_resource}\""
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
class StatNotFound < Base
|
271
|
+
def initialize(attribute, calculation)
|
272
|
+
@attribute = attribute
|
273
|
+
@calculation = calculation
|
274
|
+
end
|
275
|
+
|
276
|
+
def message
|
277
|
+
"No stat configured for calculation #{pretty(@calculation)} on attribute #{pretty(@attribute)}"
|
278
|
+
end
|
279
|
+
|
280
|
+
private
|
281
|
+
|
282
|
+
def pretty(input)
|
283
|
+
if input.is_a?(Symbol)
|
284
|
+
":#{input}"
|
285
|
+
else
|
286
|
+
"'#{input}'"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
class RecordNotFound < Base
|
292
|
+
end
|
293
|
+
|
294
|
+
class RequiredFilter < Base
|
295
|
+
def initialize(resource, attributes)
|
296
|
+
@resource = resource
|
297
|
+
@attributes = Array(attributes)
|
298
|
+
end
|
299
|
+
|
300
|
+
def message
|
301
|
+
if @attributes.length > 1
|
302
|
+
"The required filters \"#{@attributes.join(', ')}\" on resource #{@resource.class} were not provided"
|
303
|
+
else
|
304
|
+
"The required filter \"#{@attributes[0]}\" on resource #{@resource.class} was not provided"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|