module ActiveGraph::Node module Persistence class RecordInvalidError < RuntimeError attr_reader :record def initialize(record) @record = record super(@record.errors.full_messages.join(', ')) end end extend ActiveSupport::Concern extend Forwardable include ActiveGraph::Shared::Persistence # Saves the model. # # If the model is new a record gets created in the database, otherwise the existing record gets updated. # If perform_validation is true validations run. # If any of them fail the action is cancelled and save returns false. # If the flag is false validations are bypassed altogether. # See ActiveRecord::Validations for more information. # There's a series of callbacks associated with save. # If any of the before_* callbacks return false the action is cancelled and save returns false. def save(*) cascade_save do association_proxy_cache.clear create_or_update end end # Increments concurrently a numeric attribute by a centain amount # @param [Symbol, String] name of the attribute to increment # @param [Integer, Float] amount to increment def concurrent_increment!(attribute, by = 1) increment_by_query! query_as(:n), attribute, by end # Persist the object to the database. Validations and Callbacks are included # by default but validation can be disabled by passing :validate => false # to #save! Creates a new transaction. # # @raise a RecordInvalidError if there is a problem during save. # @param (see ActiveGraph::Rails::Validations#save) # @return nil # @see #save # @see ActiveGraph::Rails::Validations ActiveGraph::Rails::Validations - for the :validate parameter # @see ActiveGraph::Rails::Callbacks ActiveGraph::Rails::Callbacks - for callbacks def save!(*args) save(*args) or fail(RecordInvalidError, self) # rubocop:disable Style/AndOr end # Creates a model with values matching those of the instance attributes and returns its id. # @private # @return true def create_model node = _create_node(props_for_create) init_on_load(node, node.props) @deferred_nodes = nil true end # TODO: This does not seem like it should be the responsibility of the node. # Creates an unwrapped node in the database. # @param [Hash] node_props The type-converted properties to be added to the new node. # @param [Array] labels The labels to use for creating the new node. # @return [ActiveGraph::Node] A CypherNode or EmbeddedNode def _create_node(node_props, labels = labels_for_create) query = "CREATE (n:`#{Array(labels).join('`:`')}`) SET n = $props RETURN n" neo4j_query(query, {props: node_props}, wrap_level: :core_entity).to_a[0].n end # As the name suggests, this inserts the primary key (id property) into the properties hash. # The method called here, `default_property_values`, is a holdover from an earlier version of the gem. It does NOT # contain the default values of properties, it contains the Default Property, which we now refer to as the ID Property. # It will be deprecated and renamed in a coming refactor. # @param [Hash] converted_props A hash of properties post-typeconversion, ready for insertion into the DB. def inject_primary_key!(converted_props) self.class.default_property_values(self).tap do |destination_props| destination_props.merge!(converted_props) if converted_props.is_a?(Hash) end end # @return [Array] Labels to be set on the node during a create event def labels_for_create self.class.mapped_label_names end private def destroy_query query_as(:n).break.optional_match('(n)-[r]-()').delete(:n, :r) end # The pending associations are cleared during the save process, so it's necessary to # build the processable hash before it begins. def cascade_save ActiveGraph::Base.transaction do yield.tap { process_unpersisted_nodes! } end end module ClassMethods # Creates and saves a new node # @param [Hash] props the properties the new node should have def create(props = {}) new(props).tap do |obj| yield obj if block_given? obj.save end end # Same as #create, but raises an error if there is a problem during save. def create!(props = {}) new(props).tap do |o| yield o if block_given? o.save! end end def merge(match_attributes, optional_attrs = {}) options = [:on_create, :on_match, :set] optional_attrs.assert_valid_keys(*options) optional_attrs.default = {} on_create_attrs, on_match_attrs, set_attrs = optional_attrs.values_at(*options) new_query.merge(n: {self.mapped_label_names => match_attributes}) .on_create_set(on_create_clause(on_create_attrs)) .on_match_set(on_match_clause(on_match_attrs)) .break.set(n: set_attrs) .pluck(:n).first end def find_or_create(find_attributes, set_attributes = {}) on_create_attributes = set_attributes.reverse_merge(find_attributes.merge(self.new(find_attributes).props_for_create)) new_query.merge(n: {self.mapped_label_names => find_attributes}) .on_create_set(n: on_create_attributes) .pluck(:n).first end # Finds the first node with the given attributes, or calls create if none found def find_or_create_by(attributes, &block) find_by(attributes) || create(attributes, &block) end # Same as #find_or_create_by, but calls #create! so it raises an error if there is a problem during save. def find_or_create_by!(attributes, &block) find_by(attributes) || create!(attributes, &block) end def find_or_initialize_by(attributes) find_by(attributes) || new(attributes).tap { |o| yield(o) if block_given? } end def load_entity(id) query = query_base_for(id, :n).return(:n) result = neo4j_query(query).first result && result.n end def query_base_for(neo_id, var = :n) ActiveGraph::Base.new_query.match(var).where(var => {neo_id: neo_id}) end private def on_create_clause(clause) if clause.is_a?(Hash) {n: clause.merge(self.new(clause).props_for_create)} else clause end end def on_match_clause(clause) if clause.is_a?(Hash) {n: clause.merge(attributes_nil_hash.key?('updated_at') ? {updated_at: Time.new.to_i} : {})} else clause end end end end end