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,87 @@
|
|
1
|
+
module Graphiti
|
2
|
+
# Apply pagination logic to the scope
|
3
|
+
#
|
4
|
+
# If the user requests a page size greater than +MAX_PAGE_SIZE+,
|
5
|
+
# a +Graphiti::Errors::UnsupportedPageSize+ error will be raised.
|
6
|
+
#
|
7
|
+
# Notably, this will not fire when the `default: false` option is passed.
|
8
|
+
# This is the case for sideloads - if the user requests "give me the post
|
9
|
+
# and its comments", we shouldn't implicitly limit those comments to 20.
|
10
|
+
# *BUT* if the user requests, "give me the post and 3 of its comments", we
|
11
|
+
# *should* honor that pagination.
|
12
|
+
#
|
13
|
+
# This can be confusing because there are also 'default' and 'customized'
|
14
|
+
# pagination procs. The default comes 'for free'. Customized pagination
|
15
|
+
# looks like
|
16
|
+
#
|
17
|
+
# class PostResource < ApplicationResource
|
18
|
+
# paginate do |scope, current_page, per_page|
|
19
|
+
# # ... the custom logic ...
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# We should use the default unless the user has customized.
|
24
|
+
# @see Resource.paginate
|
25
|
+
class Scoping::Paginate < Scoping::Base
|
26
|
+
MAX_PAGE_SIZE = 1_000
|
27
|
+
|
28
|
+
# Apply the pagination logic. Raise error if over the max page size.
|
29
|
+
# @return the scope object we are chaining/modifying
|
30
|
+
def apply
|
31
|
+
if size > MAX_PAGE_SIZE
|
32
|
+
raise Graphiti::Errors::UnsupportedPageSize
|
33
|
+
.new(size, MAX_PAGE_SIZE)
|
34
|
+
elsif requested? && @opts[:sideload_parent_length].to_i > 1
|
35
|
+
raise Graphiti::Errors::UnsupportedPagination
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# We want to apply this logic unless we've explicitly received the
|
42
|
+
# +default: false+ option. In that case, only apply if pagination
|
43
|
+
# was explicitly specified in the request.
|
44
|
+
#
|
45
|
+
# @return [Boolean] should we apply this logic?
|
46
|
+
def apply?
|
47
|
+
if @opts[:default_paginate] == false
|
48
|
+
requested?
|
49
|
+
else
|
50
|
+
true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Proc, Nil] the custom pagination proc
|
55
|
+
def custom_scope
|
56
|
+
resource.pagination
|
57
|
+
end
|
58
|
+
|
59
|
+
# Apply default pagination proc via the Resource adapter
|
60
|
+
def apply_standard_scope
|
61
|
+
resource.adapter.paginate(@scope, number, size)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Apply the custom pagination proc
|
65
|
+
def apply_custom_scope
|
66
|
+
custom_scope.call(@scope, number, size, resource.context)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def requested?
|
72
|
+
not [page_param[:size], page_param[:number]].all?(&:nil?)
|
73
|
+
end
|
74
|
+
|
75
|
+
def page_param
|
76
|
+
@page_param ||= (query_hash[:page] || {})
|
77
|
+
end
|
78
|
+
|
79
|
+
def number
|
80
|
+
(page_param[:number] || 1).to_i
|
81
|
+
end
|
82
|
+
|
83
|
+
def size
|
84
|
+
(page_param[:size] || resource.default_page_size).to_i
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Graphiti
|
2
|
+
# Apply sorting logic to the scope.
|
3
|
+
#
|
4
|
+
# By default, sorting comes 'for free'. To specify a custom sorting proc:
|
5
|
+
#
|
6
|
+
# class PostResource < ApplicationResource
|
7
|
+
# sort do |scope, att, dir|
|
8
|
+
# int = dir == :desc ? -1 : 1
|
9
|
+
# scope.sort_by { |x| x[att] * int }
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# The sorting proc will be called once for each sort att/dir requested.
|
14
|
+
# @see Resource.sort
|
15
|
+
class Scoping::Sort < Scoping::Base
|
16
|
+
# @return [Proc, Nil] The custom proc specified by Resource DSL
|
17
|
+
def custom_scope
|
18
|
+
resource.sort_all
|
19
|
+
end
|
20
|
+
|
21
|
+
# Apply default scope logic via Resource adapter
|
22
|
+
# @return the scope we are chaining/modifying
|
23
|
+
def apply_standard_scope
|
24
|
+
each_sort do |attribute, direction|
|
25
|
+
if sort = resource.sorts[attribute]
|
26
|
+
@scope = sort.call(@scope, direction)
|
27
|
+
else
|
28
|
+
@scope = resource.adapter.order(@scope, attribute, direction)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
@scope
|
32
|
+
end
|
33
|
+
|
34
|
+
# Apply custom scoping proc configured via Resource DSL
|
35
|
+
# @return the scope we are chaining/modifying
|
36
|
+
def apply_custom_scope
|
37
|
+
each_sort do |attribute, direction|
|
38
|
+
@scope = custom_scope
|
39
|
+
.call(@scope, attribute, direction, resource.context)
|
40
|
+
end
|
41
|
+
@scope
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def each_sort
|
47
|
+
sort_param.each do |sort_hash|
|
48
|
+
attribute = sort_hash.keys.first
|
49
|
+
direction = sort_hash.values.first
|
50
|
+
yield attribute, direction
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def sort_param
|
55
|
+
@sort_param ||= begin
|
56
|
+
if query_hash[:sort].blank?
|
57
|
+
resource.default_sort
|
58
|
+
else
|
59
|
+
query_hash[:sort]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
module Graphiti
|
2
|
+
class Sideload
|
3
|
+
HOOK_ACTIONS = [:save, :create, :update, :destroy, :disassociate]
|
4
|
+
TYPES = [:has_many, :belongs_to, :has_one, :many_to_many]
|
5
|
+
|
6
|
+
attr_reader :name,
|
7
|
+
:resource_class,
|
8
|
+
:parent_resource_class,
|
9
|
+
:foreign_key,
|
10
|
+
:primary_key,
|
11
|
+
:parent,
|
12
|
+
:group_name
|
13
|
+
|
14
|
+
class_attribute :scope_proc,
|
15
|
+
:assign_proc,
|
16
|
+
:assign_each_proc,
|
17
|
+
:params_proc,
|
18
|
+
:pre_load_proc
|
19
|
+
|
20
|
+
def initialize(name, opts)
|
21
|
+
@name = name
|
22
|
+
@parent_resource_class = opts[:parent_resource]
|
23
|
+
@resource_class = opts[:resource]
|
24
|
+
@primary_key = opts[:primary_key]
|
25
|
+
@foreign_key = opts[:foreign_key]
|
26
|
+
@type = opts[:type]
|
27
|
+
@base_scope = opts[:base_scope]
|
28
|
+
@readable = opts[:readable]
|
29
|
+
@writable = opts[:writable]
|
30
|
+
@as = opts[:as]
|
31
|
+
|
32
|
+
# polymorphic-specific
|
33
|
+
@group_name = opts[:group_name]
|
34
|
+
@polymorphic_child = opts[:polymorphic_child]
|
35
|
+
@parent = opts[:parent]
|
36
|
+
if polymorphic_child?
|
37
|
+
parent.resource.polymorphic << resource_class
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.scope(&blk)
|
42
|
+
self.scope_proc = blk
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.assign(&blk)
|
46
|
+
self.assign_proc = blk
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.assign_each(&blk)
|
50
|
+
self.assign_each_proc = blk
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.params(&blk)
|
54
|
+
self.params_proc = blk
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.pre_load(&blk)
|
58
|
+
self.pre_load_proc = blk
|
59
|
+
end
|
60
|
+
|
61
|
+
def readable?
|
62
|
+
!!@readable
|
63
|
+
end
|
64
|
+
|
65
|
+
def writable?
|
66
|
+
!!@writable
|
67
|
+
end
|
68
|
+
|
69
|
+
def polymorphic_parent?
|
70
|
+
resource.polymorphic?
|
71
|
+
end
|
72
|
+
|
73
|
+
def polymorphic_child?
|
74
|
+
!!@polymorphic_child
|
75
|
+
end
|
76
|
+
|
77
|
+
def primary_key
|
78
|
+
@primary_key ||= :id
|
79
|
+
end
|
80
|
+
|
81
|
+
def foreign_key
|
82
|
+
@foreign_key ||= infer_foreign_key
|
83
|
+
end
|
84
|
+
|
85
|
+
def association_name
|
86
|
+
@as || name
|
87
|
+
end
|
88
|
+
|
89
|
+
def resource_class
|
90
|
+
@resource_class ||= infer_resource_class
|
91
|
+
end
|
92
|
+
|
93
|
+
def scope(parents)
|
94
|
+
raise "No #scope defined for sideload with name '#{name}'. Make sure to define this in your adapter, or pass a block that defines the scope."
|
95
|
+
end
|
96
|
+
|
97
|
+
def assign_each(parent, children)
|
98
|
+
raise 'Override #assign_each in subclass'
|
99
|
+
end
|
100
|
+
|
101
|
+
def type
|
102
|
+
@type || raise("Override #type in subclass. Should be one of #{TYPES.inspect}")
|
103
|
+
end
|
104
|
+
|
105
|
+
def load_params(parents, query)
|
106
|
+
raise 'Override #load_params in subclass'
|
107
|
+
end
|
108
|
+
|
109
|
+
def base_scope
|
110
|
+
if @base_scope
|
111
|
+
@base_scope.respond_to?(:call) ? @base_scope.call : @base_scope
|
112
|
+
else
|
113
|
+
resource.base_scope
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def load(parents, query)
|
118
|
+
params = load_params(parents, query)
|
119
|
+
params_proc.call(params, parents, query) if params_proc
|
120
|
+
opts = load_options(parents, query)
|
121
|
+
proxy = resource.class._all(params, opts, base_scope)
|
122
|
+
pre_load_proc.call(proxy) if pre_load_proc
|
123
|
+
proxy.to_a
|
124
|
+
end
|
125
|
+
|
126
|
+
# Override in subclass
|
127
|
+
def infer_foreign_key
|
128
|
+
model = parent_resource_class.model
|
129
|
+
namespace = namespace_for(model)
|
130
|
+
model_name = model.name.gsub("#{namespace}::", '')
|
131
|
+
:"#{model_name.underscore}_id"
|
132
|
+
end
|
133
|
+
|
134
|
+
def resource
|
135
|
+
@resource ||= resource_class.new
|
136
|
+
end
|
137
|
+
|
138
|
+
def parent_resource
|
139
|
+
@parent_resource ||= parent_resource_class.new
|
140
|
+
end
|
141
|
+
|
142
|
+
def assign(parents, children)
|
143
|
+
associated = []
|
144
|
+
parents.each do |parent|
|
145
|
+
relevant_children = fire_assign_each(parent, children)
|
146
|
+
if relevant_children.is_a?(Array)
|
147
|
+
associated |= relevant_children
|
148
|
+
associate_all(parent, relevant_children)
|
149
|
+
else
|
150
|
+
associated << relevant_children
|
151
|
+
associate(parent, relevant_children)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
(children - associated).each do |unassigned|
|
155
|
+
children.delete(unassigned)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def resolve(parents, query)
|
160
|
+
# legacy / custom / many-to-many
|
161
|
+
if self.class.scope_proc || type == :many_to_many
|
162
|
+
sideload_scope = fire_scope(parents)
|
163
|
+
sideload_scope = Scope.new sideload_scope,
|
164
|
+
resource,
|
165
|
+
query,
|
166
|
+
sideload_parent_length: parents.length,
|
167
|
+
default_paginate: false
|
168
|
+
sideload_scope.resolve do |sideload_results|
|
169
|
+
fire_assign(parents, sideload_results)
|
170
|
+
end
|
171
|
+
else
|
172
|
+
load(parents, query)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.after_save(only: [], except: [], &blk)
|
177
|
+
actions = HOOK_ACTIONS - except
|
178
|
+
actions = only & actions
|
179
|
+
actions = [:save] if only.empty? && except.empty?
|
180
|
+
actions.each do |a|
|
181
|
+
hooks[:"after_#{a}"] << blk
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def self.hooks
|
186
|
+
@hooks ||= {}.tap do |h|
|
187
|
+
HOOK_ACTIONS.each do |a|
|
188
|
+
h[:"after_#{a}"] = []
|
189
|
+
h[:"before_#{a}"] = []
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def fire_hooks!(parent, objects, method)
|
195
|
+
return unless self.class.hooks
|
196
|
+
|
197
|
+
all = self.class.hooks[:"after_#{method}"] + self.class.hooks[:after_save]
|
198
|
+
all.compact.each do |hook|
|
199
|
+
resource.instance_exec(parent, objects, &hook)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def associate_all(parent, children)
|
204
|
+
parent_resource.associate_all(parent, children, association_name, type)
|
205
|
+
end
|
206
|
+
|
207
|
+
def associate(parent, child)
|
208
|
+
parent_resource.associate(parent, child, association_name, type)
|
209
|
+
end
|
210
|
+
|
211
|
+
def disassociate(parent, child)
|
212
|
+
parent_resource.disassociate(parent, child, association_name, type)
|
213
|
+
end
|
214
|
+
|
215
|
+
def ids_for_parents(parents)
|
216
|
+
parent_ids = parents.map(&primary_key)
|
217
|
+
parent_ids.compact!
|
218
|
+
parent_ids.uniq!
|
219
|
+
parent_ids
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
def load_options(parents, query)
|
225
|
+
{}.tap do |opts|
|
226
|
+
opts[:default_paginate] = false
|
227
|
+
opts[:sideload_parent_length] = parents.length
|
228
|
+
opts[:after_resolve] = ->(results) {
|
229
|
+
fire_assign(parents, results)
|
230
|
+
}
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def fire_assign_each(parent, children)
|
235
|
+
if self.class.assign_each_proc
|
236
|
+
instance_exec(parent, children, &self.class.assign_each_proc)
|
237
|
+
else
|
238
|
+
assign_each(parent, children)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def fire_assign(parents, children)
|
243
|
+
if self.class.assign_proc
|
244
|
+
instance_exec(parents, children, &self.class.assign_proc)
|
245
|
+
else
|
246
|
+
assign(parents, children)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def fire_scope(parents)
|
251
|
+
parent_ids = ids_for_parents(parents)
|
252
|
+
if self.class.scope_proc
|
253
|
+
instance_exec(parent_ids, parents, &self.class.scope_proc)
|
254
|
+
else
|
255
|
+
method = method(:scope)
|
256
|
+
if [2,-2].include?(method.arity)
|
257
|
+
scope(parent_ids, parents)
|
258
|
+
else
|
259
|
+
scope(parent_ids)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def namespace_for(klass)
|
265
|
+
namespace = klass.name
|
266
|
+
split = namespace.split('::')
|
267
|
+
split[0,split.length-1].join('::')
|
268
|
+
end
|
269
|
+
|
270
|
+
def infer_resource_class
|
271
|
+
namespace = namespace_for(parent_resource.class)
|
272
|
+
inferred_name = "#{name.to_s.singularize.classify}Resource"
|
273
|
+
klass = "#{namespace}::#{inferred_name}".safe_constantize
|
274
|
+
klass ||= inferred_name.safe_constantize
|
275
|
+
unless klass
|
276
|
+
raise Errors::ResourceNotFound.new(parent_resource, name)
|
277
|
+
end
|
278
|
+
klass
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
|
2
|
+
def type
|
3
|
+
:belongs_to
|
4
|
+
end
|
5
|
+
|
6
|
+
def load_params(parents, query)
|
7
|
+
query.to_hash.tap do |hash|
|
8
|
+
hash[:filter] ||= {}
|
9
|
+
hash[:filter][primary_key] = ids_for_parents(parents)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def assign_each(parent, children)
|
14
|
+
children.find { |c| c.send(primary_key) == parent.send(foreign_key) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def ids_for_parents(parents)
|
18
|
+
parent_ids = parents.map(&foreign_key)
|
19
|
+
parent_ids.compact!
|
20
|
+
parent_ids.uniq!
|
21
|
+
parent_ids
|
22
|
+
end
|
23
|
+
|
24
|
+
def infer_foreign_key
|
25
|
+
if polymorphic_child?
|
26
|
+
parent.foreign_key
|
27
|
+
else
|
28
|
+
model = resource.model
|
29
|
+
namespace = namespace_for(model)
|
30
|
+
model_name = model.name.gsub("#{namespace}::", '')
|
31
|
+
:"#{model_name.underscore}_id"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|