inventory_refresh 0.0.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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +47 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +4 -0
  5. data/.rspec_ci +4 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_cc.yml +5 -0
  8. data/.rubocop_local.yml +2 -0
  9. data/.travis.yml +12 -0
  10. data/.yamllint +12 -0
  11. data/CHANGELOG.md +0 -0
  12. data/Gemfile +6 -0
  13. data/LICENSE +202 -0
  14. data/README.md +35 -0
  15. data/Rakefile +47 -0
  16. data/bin/console +14 -0
  17. data/bin/setup +8 -0
  18. data/inventory_refresh.gemspec +34 -0
  19. data/lib/inventory_refresh.rb +11 -0
  20. data/lib/inventory_refresh/application_record_iterator.rb +56 -0
  21. data/lib/inventory_refresh/application_record_reference.rb +15 -0
  22. data/lib/inventory_refresh/graph.rb +157 -0
  23. data/lib/inventory_refresh/graph/topological_sort.rb +66 -0
  24. data/lib/inventory_refresh/inventory_collection.rb +1175 -0
  25. data/lib/inventory_refresh/inventory_collection/data_storage.rb +178 -0
  26. data/lib/inventory_refresh/inventory_collection/graph.rb +170 -0
  27. data/lib/inventory_refresh/inventory_collection/index/proxy.rb +230 -0
  28. data/lib/inventory_refresh/inventory_collection/index/type/base.rb +80 -0
  29. data/lib/inventory_refresh/inventory_collection/index/type/data.rb +26 -0
  30. data/lib/inventory_refresh/inventory_collection/index/type/local_db.rb +286 -0
  31. data/lib/inventory_refresh/inventory_collection/index/type/skeletal.rb +116 -0
  32. data/lib/inventory_refresh/inventory_collection/reference.rb +96 -0
  33. data/lib/inventory_refresh/inventory_collection/references_storage.rb +106 -0
  34. data/lib/inventory_refresh/inventory_collection/scanner.rb +117 -0
  35. data/lib/inventory_refresh/inventory_collection/serialization.rb +140 -0
  36. data/lib/inventory_refresh/inventory_object.rb +303 -0
  37. data/lib/inventory_refresh/inventory_object_lazy.rb +151 -0
  38. data/lib/inventory_refresh/save_collection/base.rb +38 -0
  39. data/lib/inventory_refresh/save_collection/recursive.rb +52 -0
  40. data/lib/inventory_refresh/save_collection/saver/base.rb +390 -0
  41. data/lib/inventory_refresh/save_collection/saver/batch.rb +17 -0
  42. data/lib/inventory_refresh/save_collection/saver/concurrent_safe.rb +71 -0
  43. data/lib/inventory_refresh/save_collection/saver/concurrent_safe_batch.rb +632 -0
  44. data/lib/inventory_refresh/save_collection/saver/default.rb +57 -0
  45. data/lib/inventory_refresh/save_collection/saver/sql_helper.rb +85 -0
  46. data/lib/inventory_refresh/save_collection/saver/sql_helper_update.rb +120 -0
  47. data/lib/inventory_refresh/save_collection/saver/sql_helper_upsert.rb +196 -0
  48. data/lib/inventory_refresh/save_collection/topological_sort.rb +38 -0
  49. data/lib/inventory_refresh/save_inventory.rb +38 -0
  50. data/lib/inventory_refresh/target.rb +73 -0
  51. data/lib/inventory_refresh/target_collection.rb +80 -0
  52. data/lib/inventory_refresh/version.rb +3 -0
  53. data/tools/ci/create_db_user.sh +3 -0
  54. metadata +207 -0
@@ -0,0 +1,303 @@
1
+ require "inventory_refresh/application_record_reference"
2
+ require "active_support/core_ext/module/delegation"
3
+
4
+ module InventoryRefresh
5
+ class InventoryObject
6
+ attr_accessor :object, :id
7
+ attr_reader :inventory_collection, :data, :reference
8
+
9
+ delegate :manager_ref, :base_class_name, :model_class, :to => :inventory_collection
10
+ delegate :[], :[]=, :to => :data
11
+
12
+ # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object owning the
13
+ # InventoryObject
14
+ # @param data [Hash] Data of the InventoryObject object
15
+ def initialize(inventory_collection, data)
16
+ @inventory_collection = inventory_collection
17
+ @data = data
18
+ @object = nil
19
+ @id = nil
20
+ @reference = inventory_collection.build_reference(data)
21
+ end
22
+
23
+ # @return [String] stringified reference
24
+ def manager_uuid
25
+ reference.stringified_reference
26
+ end
27
+
28
+ # @return [InventoryRefresh::InventoryObject] returns self
29
+ def load
30
+ self
31
+ end
32
+
33
+ def key
34
+ nil
35
+ end
36
+
37
+ # Transforms InventoryObject object data into hash format with keys that are column names and resolves correct
38
+ # values of the foreign keys (even the polymorphic ones)
39
+ #
40
+ # @param inventory_collection_scope [InventoryRefresh::InventoryCollection] parent InventoryCollection object
41
+ # @return [Hash] Data in DB format
42
+ def attributes(inventory_collection_scope = nil)
43
+ # We should explicitly pass a scope, since the inventory_object can be mapped to more InventoryCollections with
44
+ # different blacklist and whitelist. The generic code always passes a scope.
45
+ inventory_collection_scope ||= inventory_collection
46
+
47
+ attributes_for_saving = {}
48
+ # First transform the values
49
+ data.each do |key, value|
50
+ if !allowed?(inventory_collection_scope, key)
51
+ next
52
+ elsif value.kind_of?(Array) && value.any? { |x| loadable?(x) }
53
+ # Lets fill also the original data, so other InventoryObject referring to this attribute gets the right
54
+ # result
55
+ data[key] = value.compact.map(&:load).compact
56
+ # We can use built in _ids methods to assign array of ids into has_many relations. So e.g. the :key_pairs=
57
+ # relation setter will become :key_pair_ids=
58
+ attributes_for_saving[(key.to_s.singularize + "_ids").to_sym] = data[key].map(&:id).compact.uniq
59
+ elsif loadable?(value) || inventory_collection_scope.association_to_foreign_key_mapping[key]
60
+ # Lets fill also the original data, so other InventoryObject referring to this attribute gets the right
61
+ # result
62
+ data[key] = value.load if value.respond_to?(:load)
63
+ if (foreign_key = inventory_collection_scope.association_to_foreign_key_mapping[key])
64
+ # We have an association to fill, lets fill also the :key, cause some other InventoryObject can refer to it
65
+ record_id = data[key].try(:id)
66
+ attributes_for_saving[foreign_key.to_sym] = record_id
67
+
68
+ if (foreign_type = inventory_collection_scope.association_to_foreign_type_mapping[key])
69
+ # If we have a polymorphic association, we need to also fill a base class name, but we want to nullify it
70
+ # if record_id is missing
71
+ base_class = data[key].try(:base_class_name) || data[key].class.try(:base_class).try(:name)
72
+ attributes_for_saving[foreign_type.to_sym] = record_id ? base_class : nil
73
+ end
74
+ elsif data[key].kind_of?(::InventoryRefresh::InventoryObject)
75
+ # We have an association to fill but not an Activerecord association, so e.g. Ancestry, lets just load
76
+ # it here. This way of storing ancestry is ineffective in DB call count, but RAM friendly
77
+ attributes_for_saving[key.to_sym] = data[key].base_class_name.constantize.find_by(:id => data[key].id)
78
+ else
79
+ # We have a normal attribute to fill
80
+ attributes_for_saving[key.to_sym] = data[key]
81
+ end
82
+ else
83
+ attributes_for_saving[key.to_sym] = value
84
+ end
85
+ end
86
+
87
+ attributes_for_saving
88
+ end
89
+
90
+ # Transforms InventoryObject object data into hash format with keys that are column names and resolves correct
91
+ # values of the foreign keys (even the polymorphic ones)
92
+ #
93
+ # @param inventory_collection_scope [InventoryRefresh::InventoryCollection] parent InventoryCollection object
94
+ # @param all_attribute_keys [Array<Symbol>] Attribute keys we will modify based on object's data
95
+ # @return [Hash] Data in DB format
96
+ def attributes_with_keys(inventory_collection_scope = nil, all_attribute_keys = [])
97
+ # We should explicitly pass a scope, since the inventory_object can be mapped to more InventoryCollections with
98
+ # different blacklist and whitelist. The generic code always passes a scope.
99
+ inventory_collection_scope ||= inventory_collection
100
+
101
+ attributes_for_saving = {}
102
+ # First transform the values
103
+ data.each do |key, value|
104
+ if !allowed?(inventory_collection_scope, key)
105
+ next
106
+ elsif loadable?(value) || inventory_collection_scope.association_to_foreign_key_mapping[key]
107
+ # Lets fill also the original data, so other InventoryObject referring to this attribute gets the right
108
+ # result
109
+ data[key] = value.load if value.respond_to?(:load)
110
+ if (foreign_key = inventory_collection_scope.association_to_foreign_key_mapping[key])
111
+ # We have an association to fill, lets fill also the :key, cause some other InventoryObject can refer to it
112
+ record_id = data[key].try(:id)
113
+ foreign_key_to_sym = foreign_key.to_sym
114
+ attributes_for_saving[foreign_key_to_sym] = record_id
115
+ all_attribute_keys << foreign_key_to_sym
116
+ if (foreign_type = inventory_collection_scope.association_to_foreign_type_mapping[key])
117
+ # If we have a polymorphic association, we need to also fill a base class name, but we want to nullify it
118
+ # if record_id is missing
119
+ base_class = data[key].try(:base_class_name) || data[key].class.try(:base_class).try(:name)
120
+ foreign_type_to_sym = foreign_type.to_sym
121
+ attributes_for_saving[foreign_type_to_sym] = record_id ? base_class : nil
122
+ all_attribute_keys << foreign_type_to_sym
123
+ end
124
+ else
125
+ # We have a normal attribute to fill
126
+ attributes_for_saving[key] = data[key]
127
+ all_attribute_keys << key
128
+ end
129
+ else
130
+ attributes_for_saving[key] = value
131
+ all_attribute_keys << key
132
+ end
133
+ end
134
+
135
+ attributes_for_saving
136
+ end
137
+
138
+ # Given hash of attributes, we assign them to InventoryObject object using its public writers
139
+ #
140
+ # @param attributes [Hash] attributes we want to assign
141
+ # @return [InventoryRefresh::InventoryObject] self
142
+ def assign_attributes(attributes)
143
+ attributes.each do |k, v|
144
+ # We don't want timestamps or resource versions to be overwritten here, since those are driving the conditions
145
+ next if %i(resource_timestamps resource_timestamps_max resource_timestamp).include?(k)
146
+ next if %i(resource_versions resource_versions_max resource_version).include?(k)
147
+
148
+ if data[:resource_timestamp] && attributes[:resource_timestamp]
149
+ assign_only_newest(:resource_timestamp, :resource_timestamps, attributes, data, k, v)
150
+ elsif data[:resource_version] && attributes[:resource_version]
151
+ assign_only_newest(:resource_version, :resource_versions, attributes, data, k, v)
152
+ else
153
+ public_send("#{k}=", v)
154
+ end
155
+ end
156
+
157
+ if attributes[:resource_timestamp]
158
+ assign_full_row_version_attr(:resource_timestamp, attributes, data)
159
+ elsif attributes[:resource_version]
160
+ assign_full_row_version_attr(:resource_version, attributes, data)
161
+ end
162
+
163
+ self
164
+ end
165
+
166
+ # @return [String] stringified UUID
167
+ def to_s
168
+ manager_uuid
169
+ end
170
+
171
+ # @return [String] string format for nice logging
172
+ def inspect
173
+ "InventoryObject:('#{manager_uuid}', #{inventory_collection})"
174
+ end
175
+
176
+ # @return [TrueClass] InventoryObject object is always a dependency
177
+ def dependency?
178
+ true
179
+ end
180
+
181
+ # Adds setters and getters based on :inventory_object_attributes kwarg passed into InventoryCollection
182
+ # Methods already defined should not be redefined (causes unexpected behaviour)
183
+ #
184
+ # @param inventory_object_attributes [Array<Symbol>]
185
+ def self.add_attributes(inventory_object_attributes)
186
+ defined_methods = InventoryRefresh::InventoryObject.instance_methods(false)
187
+
188
+ inventory_object_attributes.each do |attr|
189
+ unless defined_methods.include?("#{attr}=".to_sym)
190
+ define_method("#{attr}=") do |value|
191
+ data[attr] = value
192
+ end
193
+ end
194
+
195
+ unless defined_methods.include?(attr.to_sym)
196
+ define_method(attr) do
197
+ data[attr]
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ private
204
+
205
+ # Assigns value based on the version attributes. If versions are specified, it asigns attribute only if it's
206
+ # newer than existing attribute.
207
+ #
208
+ # @param full_row_version_attr [Symbol] Attr name for full rows, allowed values are
209
+ # [:resource_timestamp, :resource_version]
210
+ # @param partial_row_version_attr [Symbol] Attr name for partial rows, allowed values are
211
+ # [:resource_timestamps, :resource_versions]
212
+ # @param attributes [Hash] New attributes we are assigning
213
+ # @param data [Hash] Existing attributes of the InventoryObject
214
+ # @param k [Symbol] Name of the attribute we are assigning
215
+ # @param v [Object] Value of the attribute we are assigning
216
+ def assign_only_newest(full_row_version_attr, partial_row_version_attr, attributes, data, k, v)
217
+ # If timestamps are in play, we will set only attributes that are newer
218
+ specific_attr_timestamp = attributes[partial_row_version_attr].try(:[], k)
219
+ specific_data_timestamp = data[partial_row_version_attr].try(:[], k)
220
+
221
+ assign = if !specific_attr_timestamp
222
+ # Data have no timestamp, we will ignore the check
223
+ true
224
+ elsif specific_attr_timestamp && !specific_data_timestamp
225
+ # Data specific timestamp is nil and we have new specific timestamp
226
+ if data.key?(k)
227
+ if attributes[full_row_version_attr] >= data[full_row_version_attr]
228
+ # We can save if the full timestamp is bigger, if the data already contains the attribute
229
+ true
230
+ end
231
+ else
232
+ # Data do not contain the attribute, so we are saving the newest
233
+ true
234
+ end
235
+ true
236
+ elsif specific_attr_timestamp > specific_data_timestamp
237
+ # both partial timestamps are there, newer must be bigger
238
+ true
239
+ end
240
+
241
+ if assign
242
+ public_send("#{k}=", v) # Attribute is newer than current one, lets use it
243
+ (data[partial_row_version_attr] ||= {})[k] = specific_attr_timestamp if specific_attr_timestamp # and set the latest timestamp
244
+ end
245
+ end
246
+
247
+ # Assigns attribute representing version of the whole row
248
+ #
249
+ # @param full_row_version_attr [Symbol] Attr name for full rows, allowed values are
250
+ # [:resource_timestamp, :resource_version]
251
+ # @param attributes [Hash] New attributes we are assigning
252
+ # @param data [Hash] Existing attributes of the InventoryObject
253
+ def assign_full_row_version_attr(full_row_version_attr, attributes, data)
254
+ if attributes[full_row_version_attr] && data[full_row_version_attr]
255
+ # If both timestamps are present, store the bigger one
256
+ data[full_row_version_attr] = attributes[full_row_version_attr] if attributes[full_row_version_attr] > data[full_row_version_attr]
257
+ elsif attributes[full_row_version_attr] && !data[full_row_version_attr]
258
+ # We are assigning timestamp that was missing
259
+ data[full_row_version_attr] = attributes[full_row_version_attr]
260
+ end
261
+ end
262
+
263
+ # Return true passed key representing a getter is an association
264
+ #
265
+ # @param inventory_collection_scope [InventoryRefresh::InventoryCollection]
266
+ # @param key [Symbol] key representing getter
267
+ # @return [Boolean] true if the passed key points to association
268
+ def association?(inventory_collection_scope, key)
269
+ # Is the key an association on inventory_collection_scope model class?
270
+ !inventory_collection_scope.association_to_foreign_key_mapping[key].nil?
271
+ end
272
+
273
+ # Return true if the attribute is allowed to be saved into the DB
274
+ #
275
+ # @param inventory_collection_scope [InventoryRefresh::InventoryCollection] InventoryCollection object owning the
276
+ # attribute
277
+ # @param key [Symbol] attribute name
278
+ # @return true if the attribute is allowed to be saved into the DB
279
+ def allowed?(inventory_collection_scope, key)
280
+ foreign_to_association = inventory_collection_scope.foreign_key_to_association_mapping[key] ||
281
+ inventory_collection_scope.foreign_type_to_association_mapping[key]
282
+
283
+ return false if inventory_collection_scope.attributes_blacklist.present? &&
284
+ (inventory_collection_scope.attributes_blacklist.include?(key) ||
285
+ (foreign_to_association && inventory_collection_scope.attributes_blacklist.include?(foreign_to_association)))
286
+
287
+ return false if inventory_collection_scope.attributes_whitelist.present? &&
288
+ (!inventory_collection_scope.attributes_whitelist.include?(key) &&
289
+ (!foreign_to_association || (foreign_to_association && inventory_collection_scope.attributes_whitelist.include?(foreign_to_association))))
290
+
291
+ true
292
+ end
293
+
294
+ # Return true if the object is loadable, which we determine by a list of loadable classes.
295
+ #
296
+ # @param value [Object] object we test
297
+ # @return true if the object is loadable
298
+ def loadable?(value)
299
+ value.kind_of?(::InventoryRefresh::InventoryObjectLazy) || value.kind_of?(::InventoryRefresh::InventoryObject) ||
300
+ value.kind_of?(::InventoryRefresh::ApplicationRecordReference)
301
+ end
302
+ end
303
+ end
@@ -0,0 +1,151 @@
1
+ require "active_support/core_ext/module/delegation"
2
+
3
+ module InventoryRefresh
4
+ class InventoryObjectLazy
5
+ attr_reader :reference, :inventory_collection, :key, :default, :transform_nested_lazy_finds
6
+
7
+ delegate :stringified_reference, :ref, :[], :to => :reference
8
+
9
+ # @param inventory_collection [InventoryRefresh::InventoryCollection] InventoryCollection object owning the
10
+ # InventoryObject
11
+ # @param index_data [Hash] data of the InventoryObject object
12
+ # @param ref [Symbol] reference name
13
+ # @param key [Symbol] key name, will be used to fetch attribute from resolved InventoryObject
14
+ # @param default [Object] a default value used if the :key will resolve to nil
15
+ # @param transform_nested_lazy_finds [Boolean] True if we want to convert all lazy objects in InventoryObject
16
+ # objects and reset the Reference. TODO(lsmola) we should be able to do this automatically, then we can
17
+ # remove this option
18
+ def initialize(inventory_collection, index_data, ref: :manager_ref, key: nil, default: nil, transform_nested_lazy_finds: false)
19
+ @inventory_collection = inventory_collection
20
+ @reference = inventory_collection.build_reference(index_data, ref)
21
+ @key = key
22
+ @default = default
23
+
24
+ @transform_nested_lazy_finds = transform_nested_lazy_finds
25
+
26
+ # We do not support skeletal pre-create for :key, since :key will not be available, we want to use local_db_find
27
+ # instead.
28
+ skeletal_precreate! unless @key
29
+ end
30
+
31
+ # @return [String] stringified reference
32
+ def to_s
33
+ # TODO(lsmola) do we need this method?
34
+ stringified_reference
35
+ end
36
+
37
+ # @return [String] string format for nice logging
38
+ def inspect
39
+ suffix = ""
40
+ suffix += ", ref: #{ref}" if ref.present?
41
+ suffix += ", key: #{key}" if key.present?
42
+ "InventoryObjectLazy:('#{self}', #{inventory_collection}#{suffix})"
43
+ end
44
+
45
+ # @return [InventoryRefresh::InventoryObject, Object] InventoryRefresh::InventoryObject instance or an attribute
46
+ # on key
47
+ def load
48
+ transform_nested_secondary_indexes! if transform_nested_lazy_finds && nested_secondary_index?
49
+
50
+ key ? load_object_with_key : load_object
51
+ end
52
+
53
+ # return [Boolean] true if the Lazy object is causing a dependency, Lazy link is always a dependency if no :key
54
+ # is provider or if it's transitive_dependency
55
+ def dependency?
56
+ # If key is not set, InventoryObjectLazy is a dependency, cause it points to the record itself. Otherwise
57
+ # InventoryObjectLazy is a dependency only if it points to an attribute which is a dependency or a relation.
58
+ !key || transitive_dependency?
59
+ end
60
+
61
+ # return [Boolean] true if the Lazy object is causing a transitive dependency, which happens if the :key points
62
+ # to an attribute that is causing a dependency.
63
+ def transitive_dependency?
64
+ # If the dependency is inventory_collection.lazy_find(:ems_ref, :key => :stack)
65
+ # and a :stack is a relation to another object, in the InventoryObject object,
66
+ # then this relation is considered transitive.
67
+ key && association?(key)
68
+ end
69
+
70
+ # @return [Boolean] true if the key is an association on inventory_collection_scope model class
71
+ def association?(key)
72
+ # TODO(lsmola) remove this if there will be better dependency scan, probably with transitive dependencies filled
73
+ # in a second pass, then we can get rid of this hardcoded symbols. Right now we are not able to introspect these.
74
+ return true if [:parent, :genealogy_parent].include?(key)
75
+
76
+ inventory_collection.dependency_attributes.key?(key) ||
77
+ !inventory_collection.association_to_foreign_key_mapping[key].nil?
78
+ end
79
+
80
+ def transform_nested_secondary_indexes!(depth = 0)
81
+ raise "Nested references are too deep!" if depth > 20
82
+
83
+ keys.each do |x|
84
+ attr = full_reference[x]
85
+ next unless attr.kind_of?(InventoryRefresh::InventoryObjectLazy)
86
+ next if attr.primary?
87
+
88
+ if attr.nested_secondary_index?
89
+ attr.transform_nested_secondary_indexes!(depth + 1)
90
+ end
91
+
92
+ full_reference[x] = full_reference[x].load
93
+ end
94
+
95
+ # Rebuild the reference to get the right value
96
+ self.reference = inventory_collection.build_reference(full_reference, ref)
97
+ end
98
+
99
+ private
100
+
101
+ delegate :parallel_safe?, :saved?, :saver_strategy, :skeletal_primary_index, :targeted?, :to => :inventory_collection
102
+ delegate :nested_secondary_index?, :primary?, :full_reference, :keys, :primary?, :to => :reference
103
+
104
+ attr_writer :reference
105
+
106
+ # Instead of loading the reference from the DB, we'll add the skeletal InventoryObject (having manager_ref and
107
+ # info from the default_values) to the correct InventoryCollection. Which will either be found in the DB or
108
+ # created as a skeletal object. The later refresh of the object will then fill the rest of the data, while not
109
+ # touching the reference.
110
+ #
111
+ # @return [InventoryRefresh::InventoryObject, NilClass] Returns pre-created InventoryObject or nil
112
+ def skeletal_precreate!
113
+ # We can do skeletal pre-create only for strategies using unique indexes. Since this can build records out of
114
+ # the given :arel scope, we will always attempt to create the recod, so we need unique index to avoid duplication
115
+ # of records.
116
+ return unless parallel_safe?
117
+ # Pre-create only for strategies that will be persisting data, i.e. are not saved already
118
+ return if saved?
119
+ # We can only do skeletal pre-create for primary index reference, since that is needed to create DB unique index
120
+ return unless primary?
121
+ # Full reference must be present
122
+ return if full_reference.blank?
123
+
124
+ # To avoid pre-creating invalid records all fields of a primary key must have value
125
+ # TODO(lsmola) for composite keys, it's still valid to have one of the keys nil, figure out how to allow this
126
+ return if keys.any? { |x| full_reference[x].blank? }
127
+
128
+ skeletal_primary_index.build(full_reference)
129
+ end
130
+
131
+ # @return [Object] value found or :key or default value if the value is nil
132
+ def load_object_with_key
133
+ # TODO(lsmola) Log error if we are accessing path that is present in blacklist or not present in whitelist
134
+ found = inventory_collection.find(reference)
135
+ if found.present?
136
+ if found.try(:data).present?
137
+ found.data[key] || default
138
+ else
139
+ found.public_send(key) || default
140
+ end
141
+ else
142
+ default
143
+ end
144
+ end
145
+
146
+ # @return [InventoryRefresh::InventoryObject, NilClass] InventoryRefresh::InventoryObject instance or nil if not found
147
+ def load_object
148
+ inventory_collection.find(reference)
149
+ end
150
+ end
151
+ end