# frozen_string_literal: true module Dynamoid module Associations module ManyAssociation include Association attr_accessor :query def initialize(*args) @query = {} super end include Enumerable # @private # Delegate methods to the records the association represents. delegate :first, :last, :empty?, :size, :class, to: :records # The records associated to the source. # # @return the association records; depending on which association this is, either a single instance or an array # # @private # @since 0.2.0 def find_target return [] if source_ids.empty? Array(target_class.find(source_ids.to_a, raise_error: false)) end # @private def records if query.empty? target else results_with_query(target) end end # Alias convenience methods for the associations. alias all records alias count size alias nil? empty? # Delegate include? to the records. def include?(object) records.include?(object) end # Delete an object or array of objects from the association. # # tag.posts.delete(post) # tag.posts.delete([post1, post2, post3]) # # This removes their records from the association field on the source, # and attempts to remove the source from the target association if it is # detected to exist. # # It saves both models immediately - the source model and the target one # so any not saved changes will be saved as well. # # @param object [Dynamoid::Document|Array] model (or array of models) to remove from the association # @return [Dynamoid::Document|Array] the deleted model # @since 0.2.0 def delete(object) disassociate(Array(object).collect(&:hash_key)) if target_association Array(object).each { |obj| obj.send(target_association).disassociate(source.hash_key) } end object end # Add an object or array of objects to an association. # # tag.posts << post # tag.posts << [post1, post2, post3] # # This preserves the current records in the association (if any) and adds # the object to the target association if it is detected to exist. # # It saves both models immediately - the source model and the target one # so any not saved changes will be saved as well. # # @param object [Dynamoid::Document|Array] model (or array of models) to add to the association # @return [Dynamoid::Document] the added model # @since 0.2.0 def <<(object) associate(Array(object).collect(&:hash_key)) if target_association Array(object).each { |obj| obj.send(target_association).associate(source.hash_key) } end object end # Replace an association with object or array of objects. This removes all of the existing associated records and replaces them with # the passed object(s), and associates the target association if it is detected to exist. # # @param [Dynamoid::Document] object the object (or array of objects) to add to the association # # @return [Dynamoid::Document|Array] the added object # # @private # @since 0.2.0 def setter(object) target.each { |o| delete(o) } self << object object end # Create a new instance of the target class, persist it and add directly # to the association. # # tag.posts.create!(title: 'foo') # # Several models can be created at once when an array of attributes # specified: # # tag.posts.create!([{ title: 'foo' }, {title: 'bar'} ]) # # If the creation fails an exception will be raised. # # @param attributes [Hash] attribute values for the new object # @return [Dynamoid::Document|Array] the newly-created object # @since 0.2.0 def create!(attributes = {}) self << target_class.create!(attributes) end # Create a new instance of the target class, persist it and add directly # to the association. # # tag.posts.create(title: 'foo') # # Several models can be created at once when an array of attributes # specified: # # tag.posts.create([{ title: 'foo' }, {title: 'bar'} ]) # # @param attributes [Hash] attribute values for the new object # @return [Dynamoid::Document|Array] the newly-created object # @since 0.2.0 def create(attributes = {}) self << target_class.create(attributes) end # Create a new instance of the target class and add it directly to the association. If the create fails an exception will be raised. # # @return [Dynamoid::Document] the newly-created object # # @private # @since 0.2.0 def each(&block) records.each(&block) end # Destroys all members of the association and removes them from the # association. # # tag.posts.destroy_all # # @since 0.2.0 def destroy_all objs = target source.update_attribute(source_attribute, nil) objs.each(&:destroy) end # Deletes all members of the association and removes them from the # association. # # tag.posts.delete_all # # @since 0.2.0 def delete_all objs = target source.update_attribute(source_attribute, nil) objs.each(&:delete) end # Naive association filtering. # # tag.posts.where(title: 'foo') # # It loads lazily all the associated models and checks provided # conditions. That's why only equality conditions can be specified. # # @param args [Hash] A hash of attributes; each must match every returned object's attribute exactly. # @return [Dynamoid::Association] the association this method was called on (for chaining purposes) # @since 0.2.0 def where(args) filtered = clone filtered.query = query.clone args.each { |k, v| filtered.query[k] = v } filtered end # Is this array equal to the association's records? # # @return [Boolean] true/false # # @since 0.2.0 def ==(other) records == Array(other) end # Delegate methods we don't find directly to the records array. # # @private # @since 0.2.0 def method_missing(method, *args) if records.respond_to?(method) records.send(method, *args) else super end end # @private def associate(hash_key) source.update_attribute(source_attribute, source_ids.merge(Array(hash_key))) end # @private def disassociate(hash_key) source.update_attribute(source_attribute, source_ids - Array(hash_key)) end private # If a query exists, filter all existing results based on that query. # # @param [Array] results the raw results for the association # # @return [Array] the filtered results for the query # # @since 0.2.0 def results_with_query(results) results.find_all do |result| query.all? do |attribute, value| result.send(attribute) == value end end end end end end