require "mongoid-denormalize/version" module Mongoid module Denormalize def self.included(base) base.extend ClassMethods end module ClassMethods def denormalize(*args) *fields, options = args unless options.is_a?(Hash) && options[:from] raise ArgumentError, 'Option :from is needed (e.g. denormalize :name, from: :user).' end fields = Mongoid::Denormalize.get_fields_with_names(fields, options) # Add fields to model fields.each { |field| field field[:as] } # Add hooks Mongoid::Denormalize.add_hook_to_child(self, fields, options) Mongoid::Denormalize.add_hook_to_parent(self, fields, options) end end # Check options and return name for each field def self.get_fields_with_names(fields, options) if options.include?(:as) options[:as] = [options[:as]] unless options[:as].is_a?(Array) unless fields.size == options[:as].size raise ArgumentError, 'When option :as is used you must pass a name for each field.' end return fields.map.with_index { |field, index| {name: field, as: options[:as][index]} } elsif options.include?(:prefix) return fields.map { |field| {name: field, as: "#{options[:prefix]}_#{field}"} } end fields.map { |field| {name: field, as: "#{options[:from]}_#{field}"} } end # Add hook to child class to denormalize fields when parent relation is changed def self.add_hook_to_child(child_class, fields, options) from = options[:from].to_s child_class.send(options[:child_callback] || 'before_save') do if send(from) && send("#{from}_id_changed?") fields.each do |field| send("#{field[:as]}=", send(from).send(field[:name])) end end end end # Add hook to parent class to denormalize fields when parent object is updated def self.add_hook_to_parent(child_class, fields, options) from = options[:from].to_s parent = (child_class.relations[from].class_name || child_class.relations[from].name.capitalize).constantize relation = parent.relations[child_class.relations[from].inverse_of.to_s] || parent.relations[child_class.model_name.plural] || parent.relations[child_class.model_name.singular] unless relation raise "Option :inverse_of is needed for 'belongs_to :#{from}' into #{child_class}." end parent.after_update do attributes = {} fields.each do |field| attributes[field[:as]] = send(field[:name]) if send("#{field[:name]}_changed?") end next if attributes.blank? case relation.relation.to_s when 'Mongoid::Relations::Referenced::One' if (document = send(relation.name)) document.collection.update_one({_id: document._id}, {'$set' => attributes}) end when 'Mongoid::Relations::Referenced::Many' send(relation.name).update_all('$set' => attributes) else raise "Relation type unsupported: #{relation.relation}" end end end end end