activegraph 10.0.0.pre.alpha.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1989 -0
  3. data/CONTRIBUTORS +12 -0
  4. data/Gemfile +24 -0
  5. data/README.md +107 -0
  6. data/bin/rake +17 -0
  7. data/config/locales/en.yml +5 -0
  8. data/config/neo4j/add_classnames.yml +1 -0
  9. data/config/neo4j/config.yml +38 -0
  10. data/lib/neo4j.rb +116 -0
  11. data/lib/neo4j/active_base.rb +89 -0
  12. data/lib/neo4j/active_node.rb +108 -0
  13. data/lib/neo4j/active_node/callbacks.rb +8 -0
  14. data/lib/neo4j/active_node/dependent.rb +11 -0
  15. data/lib/neo4j/active_node/dependent/association_methods.rb +49 -0
  16. data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +51 -0
  17. data/lib/neo4j/active_node/enum.rb +26 -0
  18. data/lib/neo4j/active_node/has_n.rb +612 -0
  19. data/lib/neo4j/active_node/has_n/association.rb +278 -0
  20. data/lib/neo4j/active_node/has_n/association/rel_factory.rb +61 -0
  21. data/lib/neo4j/active_node/has_n/association/rel_wrapper.rb +23 -0
  22. data/lib/neo4j/active_node/has_n/association_cypher_methods.rb +108 -0
  23. data/lib/neo4j/active_node/id_property.rb +224 -0
  24. data/lib/neo4j/active_node/id_property/accessor.rb +62 -0
  25. data/lib/neo4j/active_node/initialize.rb +21 -0
  26. data/lib/neo4j/active_node/labels.rb +207 -0
  27. data/lib/neo4j/active_node/labels/index.rb +37 -0
  28. data/lib/neo4j/active_node/labels/reloading.rb +21 -0
  29. data/lib/neo4j/active_node/node_list_formatter.rb +13 -0
  30. data/lib/neo4j/active_node/node_wrapper.rb +54 -0
  31. data/lib/neo4j/active_node/orm_adapter.rb +82 -0
  32. data/lib/neo4j/active_node/persistence.rb +187 -0
  33. data/lib/neo4j/active_node/property.rb +60 -0
  34. data/lib/neo4j/active_node/query.rb +76 -0
  35. data/lib/neo4j/active_node/query/query_proxy.rb +374 -0
  36. data/lib/neo4j/active_node/query/query_proxy_eager_loading.rb +177 -0
  37. data/lib/neo4j/active_node/query/query_proxy_eager_loading/association_tree.rb +75 -0
  38. data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +110 -0
  39. data/lib/neo4j/active_node/query/query_proxy_find_in_batches.rb +19 -0
  40. data/lib/neo4j/active_node/query/query_proxy_link.rb +139 -0
  41. data/lib/neo4j/active_node/query/query_proxy_methods.rb +302 -0
  42. data/lib/neo4j/active_node/query/query_proxy_methods_of_mass_updating.rb +86 -0
  43. data/lib/neo4j/active_node/query_methods.rb +68 -0
  44. data/lib/neo4j/active_node/reflection.rb +86 -0
  45. data/lib/neo4j/active_node/rels.rb +11 -0
  46. data/lib/neo4j/active_node/scope.rb +166 -0
  47. data/lib/neo4j/active_node/unpersisted.rb +48 -0
  48. data/lib/neo4j/active_node/validations.rb +59 -0
  49. data/lib/neo4j/active_rel.rb +67 -0
  50. data/lib/neo4j/active_rel/callbacks.rb +15 -0
  51. data/lib/neo4j/active_rel/initialize.rb +28 -0
  52. data/lib/neo4j/active_rel/persistence.rb +134 -0
  53. data/lib/neo4j/active_rel/persistence/query_factory.rb +95 -0
  54. data/lib/neo4j/active_rel/property.rb +95 -0
  55. data/lib/neo4j/active_rel/query.rb +101 -0
  56. data/lib/neo4j/active_rel/rel_wrapper.rb +31 -0
  57. data/lib/neo4j/active_rel/related_node.rb +87 -0
  58. data/lib/neo4j/active_rel/types.rb +82 -0
  59. data/lib/neo4j/active_rel/validations.rb +8 -0
  60. data/lib/neo4j/ansi.rb +14 -0
  61. data/lib/neo4j/class_arguments.rb +39 -0
  62. data/lib/neo4j/config.rb +135 -0
  63. data/lib/neo4j/core.rb +14 -0
  64. data/lib/neo4j/core/connection_failed_error.rb +6 -0
  65. data/lib/neo4j/core/cypher_error.rb +37 -0
  66. data/lib/neo4j/core/driver.rb +66 -0
  67. data/lib/neo4j/core/has_uri.rb +63 -0
  68. data/lib/neo4j/core/instrumentable.rb +36 -0
  69. data/lib/neo4j/core/label.rb +158 -0
  70. data/lib/neo4j/core/logging.rb +44 -0
  71. data/lib/neo4j/core/node.rb +23 -0
  72. data/lib/neo4j/core/querable.rb +88 -0
  73. data/lib/neo4j/core/query.rb +487 -0
  74. data/lib/neo4j/core/query_builder.rb +32 -0
  75. data/lib/neo4j/core/query_clauses.rb +727 -0
  76. data/lib/neo4j/core/query_ext.rb +24 -0
  77. data/lib/neo4j/core/query_find_in_batches.rb +49 -0
  78. data/lib/neo4j/core/relationship.rb +13 -0
  79. data/lib/neo4j/core/responses.rb +50 -0
  80. data/lib/neo4j/core/result.rb +33 -0
  81. data/lib/neo4j/core/schema.rb +30 -0
  82. data/lib/neo4j/core/schema_errors.rb +12 -0
  83. data/lib/neo4j/core/wrappable.rb +30 -0
  84. data/lib/neo4j/errors.rb +57 -0
  85. data/lib/neo4j/migration.rb +148 -0
  86. data/lib/neo4j/migrations.rb +27 -0
  87. data/lib/neo4j/migrations/base.rb +77 -0
  88. data/lib/neo4j/migrations/check_pending.rb +20 -0
  89. data/lib/neo4j/migrations/helpers.rb +105 -0
  90. data/lib/neo4j/migrations/helpers/id_property.rb +75 -0
  91. data/lib/neo4j/migrations/helpers/relationships.rb +66 -0
  92. data/lib/neo4j/migrations/helpers/schema.rb +51 -0
  93. data/lib/neo4j/migrations/migration_file.rb +24 -0
  94. data/lib/neo4j/migrations/runner.rb +195 -0
  95. data/lib/neo4j/migrations/schema.rb +44 -0
  96. data/lib/neo4j/migrations/schema_migration.rb +14 -0
  97. data/lib/neo4j/model_schema.rb +139 -0
  98. data/lib/neo4j/paginated.rb +27 -0
  99. data/lib/neo4j/railtie.rb +105 -0
  100. data/lib/neo4j/schema/operation.rb +102 -0
  101. data/lib/neo4j/shared.rb +60 -0
  102. data/lib/neo4j/shared/attributes.rb +216 -0
  103. data/lib/neo4j/shared/callbacks.rb +68 -0
  104. data/lib/neo4j/shared/cypher.rb +37 -0
  105. data/lib/neo4j/shared/declared_properties.rb +204 -0
  106. data/lib/neo4j/shared/declared_property.rb +109 -0
  107. data/lib/neo4j/shared/declared_property/index.rb +37 -0
  108. data/lib/neo4j/shared/enum.rb +167 -0
  109. data/lib/neo4j/shared/filtered_hash.rb +79 -0
  110. data/lib/neo4j/shared/identity.rb +34 -0
  111. data/lib/neo4j/shared/initialize.rb +64 -0
  112. data/lib/neo4j/shared/marshal.rb +23 -0
  113. data/lib/neo4j/shared/mass_assignment.rb +64 -0
  114. data/lib/neo4j/shared/permitted_attributes.rb +28 -0
  115. data/lib/neo4j/shared/persistence.rb +282 -0
  116. data/lib/neo4j/shared/property.rb +240 -0
  117. data/lib/neo4j/shared/query_factory.rb +102 -0
  118. data/lib/neo4j/shared/rel_type_converters.rb +43 -0
  119. data/lib/neo4j/shared/serialized_properties.rb +30 -0
  120. data/lib/neo4j/shared/type_converters.rb +433 -0
  121. data/lib/neo4j/shared/typecasted_attributes.rb +98 -0
  122. data/lib/neo4j/shared/typecaster.rb +53 -0
  123. data/lib/neo4j/shared/validations.rb +44 -0
  124. data/lib/neo4j/tasks/migration.rake +202 -0
  125. data/lib/neo4j/timestamps.rb +11 -0
  126. data/lib/neo4j/timestamps/created.rb +9 -0
  127. data/lib/neo4j/timestamps/updated.rb +9 -0
  128. data/lib/neo4j/transaction.rb +139 -0
  129. data/lib/neo4j/type_converters.rb +7 -0
  130. data/lib/neo4j/undeclared_properties.rb +53 -0
  131. data/lib/neo4j/version.rb +3 -0
  132. data/lib/neo4j/wrapper.rb +4 -0
  133. data/lib/rails/generators/neo4j/migration/migration_generator.rb +14 -0
  134. data/lib/rails/generators/neo4j/migration/templates/migration.erb +9 -0
  135. data/lib/rails/generators/neo4j/model/model_generator.rb +88 -0
  136. data/lib/rails/generators/neo4j/model/templates/migration.erb +9 -0
  137. data/lib/rails/generators/neo4j/model/templates/model.erb +15 -0
  138. data/lib/rails/generators/neo4j/upgrade_v8/templates/migration.erb +17 -0
  139. data/lib/rails/generators/neo4j/upgrade_v8/upgrade_v8_generator.rb +32 -0
  140. data/lib/rails/generators/neo4j_generator.rb +119 -0
  141. data/neo4j.gemspec +51 -0
  142. metadata +421 -0
@@ -0,0 +1,37 @@
1
+ module Neo4j::ActiveNode::Labels
2
+ module Index
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ extend Forwardable
7
+
8
+ def_delegators :declared_properties, :indexed_properties
9
+
10
+ # Creates a Neo4j index on given property
11
+ #
12
+ # This can also be done on the property directly, see Neo4j::ActiveNode::Property::ClassMethods#property.
13
+ #
14
+ # @param [Symbol] property the property we want a Neo4j index on
15
+ #
16
+ # @example
17
+ # class Person
18
+ # include Neo4j::ActiveNode
19
+ # property :name
20
+ # index :name
21
+ # end
22
+ def index(property)
23
+ return if Neo4j::ModelSchema.defined_constraint?(self, property)
24
+
25
+ Neo4j::ModelSchema.add_defined_index(self, property)
26
+ end
27
+
28
+ # Creates a neo4j constraint on this class for given property
29
+ #
30
+ # @example
31
+ # Person.constraint :name, type: :unique
32
+ def constraint(property, _constraints = {type: :unique})
33
+ Neo4j::ModelSchema.add_defined_constraint(self, property)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ module Neo4j::ActiveNode::Labels
2
+ module Reloading
3
+ extend ActiveSupport::Concern
4
+
5
+ MODELS_TO_RELOAD = []
6
+
7
+ def self.reload_models!
8
+ MODELS_TO_RELOAD.each(&:constantize)
9
+ MODELS_TO_RELOAD.clear
10
+ end
11
+
12
+ module ClassMethods
13
+ def before_remove_const
14
+ associations.each_value(&:queue_model_refresh!)
15
+ MODELS_FOR_LABELS_CACHE.clear
16
+ WRAPPED_CLASSES.each { |c| MODELS_TO_RELOAD << c.name }
17
+ WRAPPED_CLASSES.clear
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module Neo4j::ActiveNode
2
+ class NodeListFormatter
3
+ def initialize(list, max_elements = 5)
4
+ @list = list
5
+ @max_elements = max_elements
6
+ end
7
+
8
+ def inspect
9
+ return @list.inspect if !@max_elements || @list.length <= @max_elements
10
+ "[#{@list.take(5).map!(&:inspect).join(', ')}, ...]"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support/inflector'
2
+ require 'neo4j/core/node'
3
+
4
+ wrapping_proc = proc do |node|
5
+ found_class = Neo4j::NodeWrapping.class_to_wrap(node.labels)
6
+ next node if not found_class
7
+
8
+ found_class.new.tap do |wrapped_node|
9
+ wrapped_node.init_on_load(node, node.props)
10
+ end
11
+ end
12
+ Neo4j::Driver::Types::Node.wrapper_callback(wrapping_proc)
13
+
14
+ module Neo4j
15
+ module NodeWrapping
16
+ # Only load classes once for performance
17
+ CONSTANTS_FOR_LABELS_CACHE = {}
18
+
19
+ class << self
20
+ def class_to_wrap(labels)
21
+ load_classes_from_labels(labels)
22
+ Neo4j::ActiveNode::Labels.model_for_labels(labels).tap do |model_class|
23
+ populate_constants_for_labels_cache(model_class, labels)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def load_classes_from_labels(labels)
30
+ labels.each { |label| constant_for_label(label) }
31
+ end
32
+
33
+ def constant_for_label(label)
34
+ CONSTANTS_FOR_LABELS_CACHE[label] || CONSTANTS_FOR_LABELS_CACHE[label] = constantized_label(label)
35
+ end
36
+
37
+ def constantized_label(label)
38
+ "#{association_model_namespace}::#{label}".constantize
39
+ rescue NameError, LoadError
40
+ nil
41
+ end
42
+
43
+ def populate_constants_for_labels_cache(model_class, labels)
44
+ labels.each do |label|
45
+ CONSTANTS_FOR_LABELS_CACHE[label] = model_class if CONSTANTS_FOR_LABELS_CACHE[label].nil?
46
+ end
47
+ end
48
+
49
+ def association_model_namespace
50
+ Neo4j::Config.association_model_namespace_string
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,82 @@
1
+ require 'orm_adapter'
2
+
3
+ module Neo4j
4
+ module ActiveNode
5
+ module ClassMethods
6
+ include OrmAdapter::ToAdapter
7
+ end
8
+
9
+ class OrmAdapter < ::OrmAdapter::Base
10
+ module ClassMethods
11
+ include ActiveModel::Callbacks
12
+ end
13
+
14
+ def column_names
15
+ klass._decl_props.keys
16
+ end
17
+
18
+ def i18n_scope
19
+ :neo4j
20
+ end
21
+
22
+ # Get an instance by id of the model
23
+ def get!(id)
24
+ klass.find(wrap_key(id)).tap do |node|
25
+ fail 'No record found' if node.nil?
26
+ end
27
+ end
28
+
29
+ # Get an instance by id of the model
30
+ def get(id)
31
+ klass.find_by(klass.id_property_name => wrap_key(id))
32
+ end
33
+
34
+ # Find the first instance matching conditions
35
+ def find_first(options = {})
36
+ conditions, order = extract_conditions!(options)
37
+ extract_id!(conditions)
38
+ order = hasherize_order(order)
39
+
40
+ result = klass.where(conditions)
41
+ result = result.order(order) unless order.empty?
42
+ result.first
43
+ end
44
+
45
+ # Find all models matching conditions
46
+ def find_all(options = {})
47
+ conditions, order, limit, offset = extract_conditions!(options)
48
+ extract_id!(conditions)
49
+ order = hasherize_order(order)
50
+
51
+ result = klass.where(conditions)
52
+ result = result.order(order) unless order.empty?
53
+ result = result.skip(offset) if offset
54
+ result = result.limit(limit) if limit
55
+ result.to_a
56
+ end
57
+
58
+ # Create a model using attributes
59
+ def create!(attributes = {})
60
+ klass.create!(attributes)
61
+ end
62
+
63
+ # @see OrmAdapter::Base#destroy
64
+ def destroy(object)
65
+ object.destroy && true if valid_object?(object)
66
+ end
67
+
68
+ private
69
+
70
+ def hasherize_order(order)
71
+ (order || []).map { |clause| Hash[*clause] }
72
+ end
73
+
74
+ def extract_id!(conditions)
75
+ id = conditions.delete(:id)
76
+ return if not id
77
+
78
+ conditions[klass.id_property_name.to_sym] = id
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,187 @@
1
+ module Neo4j::ActiveNode
2
+ module Persistence
3
+ class RecordInvalidError < RuntimeError
4
+ attr_reader :record
5
+
6
+ def initialize(record)
7
+ @record = record
8
+ super(@record.errors.full_messages.join(', '))
9
+ end
10
+ end
11
+
12
+ extend ActiveSupport::Concern
13
+ extend Forwardable
14
+ include Neo4j::Shared::Persistence
15
+
16
+ # Saves the model.
17
+ #
18
+ # If the model is new a record gets created in the database, otherwise the existing record gets updated.
19
+ # If perform_validation is true validations run.
20
+ # If any of them fail the action is cancelled and save returns false.
21
+ # If the flag is false validations are bypassed altogether.
22
+ # See ActiveRecord::Validations for more information.
23
+ # There's a series of callbacks associated with save.
24
+ # If any of the before_* callbacks return false the action is cancelled and save returns false.
25
+ def save(*)
26
+ cascade_save do
27
+ association_proxy_cache.clear
28
+ create_or_update
29
+ end
30
+ end
31
+
32
+ # Increments concurrently a numeric attribute by a centain amount
33
+ # @param [Symbol, String] name of the attribute to increment
34
+ # @param [Integer, Float] amount to increment
35
+ def concurrent_increment!(attribute, by = 1)
36
+ increment_by_query! query_as(:n), attribute, by
37
+ end
38
+
39
+ # Persist the object to the database. Validations and Callbacks are included
40
+ # by default but validation can be disabled by passing :validate => false
41
+ # to #save! Creates a new transaction.
42
+ #
43
+ # @raise a RecordInvalidError if there is a problem during save.
44
+ # @param (see Neo4j::Rails::Validations#save)
45
+ # @return nil
46
+ # @see #save
47
+ # @see Neo4j::Rails::Validations Neo4j::Rails::Validations - for the :validate parameter
48
+ # @see Neo4j::Rails::Callbacks Neo4j::Rails::Callbacks - for callbacks
49
+ def save!(*args)
50
+ save(*args) or fail(RecordInvalidError, self) # rubocop:disable Style/AndOr
51
+ end
52
+
53
+ # Creates a model with values matching those of the instance attributes and returns its id.
54
+ # @private
55
+ # @return true
56
+ def create_model
57
+ node = _create_node(props_for_create)
58
+ init_on_load(node, node.props)
59
+ @deferred_nodes = nil
60
+ true
61
+ end
62
+
63
+ # TODO: This does not seem like it should be the responsibility of the node.
64
+ # Creates an unwrapped node in the database.
65
+ # @param [Hash] node_props The type-converted properties to be added to the new node.
66
+ # @param [Array] labels The labels to use for creating the new node.
67
+ # @return [Neo4j::Node] A CypherNode or EmbeddedNode
68
+ def _create_node(node_props, labels = labels_for_create)
69
+ query = "CREATE (n:`#{Array(labels).join('`:`')}`) SET n = {props} RETURN n"
70
+ neo4j_query(query, {props: node_props}, wrap_level: :core_entity).to_a[0].n
71
+ end
72
+
73
+ # As the name suggests, this inserts the primary key (id property) into the properties hash.
74
+ # The method called here, `default_property_values`, is a holdover from an earlier version of the gem. It does NOT
75
+ # contain the default values of properties, it contains the Default Property, which we now refer to as the ID Property.
76
+ # It will be deprecated and renamed in a coming refactor.
77
+ # @param [Hash] converted_props A hash of properties post-typeconversion, ready for insertion into the DB.
78
+ def inject_primary_key!(converted_props)
79
+ self.class.default_property_values(self).tap do |destination_props|
80
+ destination_props.merge!(converted_props) if converted_props.is_a?(Hash)
81
+ end
82
+ end
83
+
84
+ # @return [Array] Labels to be set on the node during a create event
85
+ def labels_for_create
86
+ self.class.mapped_label_names
87
+ end
88
+
89
+ private
90
+
91
+ def destroy_query
92
+ query_as(:n).break.optional_match('(n)-[r]-()').delete(:n, :r)
93
+ end
94
+
95
+ # The pending associations are cleared during the save process, so it's necessary to
96
+ # build the processable hash before it begins. If there are nodes and associations that
97
+ # need to be created after the node is saved, a new transaction is started.
98
+ def cascade_save
99
+ self.class.run_transaction(pending_deferred_creations?) do
100
+ yield.tap { process_unpersisted_nodes! }
101
+ end
102
+ end
103
+
104
+ module ClassMethods
105
+ # Creates and saves a new node
106
+ # @param [Hash] props the properties the new node should have
107
+ def create(props = {})
108
+ new(props).tap do |obj|
109
+ yield obj if block_given?
110
+ obj.save
111
+ end
112
+ end
113
+
114
+ # Same as #create, but raises an error if there is a problem during save.
115
+ def create!(props = {})
116
+ new(props).tap do |o|
117
+ yield o if block_given?
118
+ o.save!
119
+ end
120
+ end
121
+
122
+ def merge(match_attributes, optional_attrs = {})
123
+ options = [:on_create, :on_match, :set]
124
+ optional_attrs.assert_valid_keys(*options)
125
+
126
+ optional_attrs.default = {}
127
+ on_create_attrs, on_match_attrs, set_attrs = optional_attrs.values_at(*options)
128
+
129
+ new_query.merge(n: {self.mapped_label_names => match_attributes})
130
+ .on_create_set(on_create_clause(on_create_attrs))
131
+ .on_match_set(on_match_clause(on_match_attrs))
132
+ .break.set(n: set_attrs)
133
+ .pluck(:n).first
134
+ end
135
+
136
+ def find_or_create(find_attributes, set_attributes = {})
137
+ on_create_attributes = set_attributes.reverse_merge(find_attributes.merge(self.new(find_attributes).props_for_create))
138
+
139
+ new_query.merge(n: {self.mapped_label_names => find_attributes})
140
+ .on_create_set(n: on_create_attributes)
141
+ .pluck(:n).first
142
+ end
143
+
144
+ # Finds the first node with the given attributes, or calls create if none found
145
+ def find_or_create_by(attributes, &block)
146
+ find_by(attributes) || create(attributes, &block)
147
+ end
148
+
149
+ # Same as #find_or_create_by, but calls #create! so it raises an error if there is a problem during save.
150
+ def find_or_create_by!(attributes, &block)
151
+ find_by(attributes) || create!(attributes, &block)
152
+ end
153
+
154
+ def find_or_initialize_by(attributes)
155
+ find_by(attributes) || new(attributes).tap { |o| yield(o) if block_given? }
156
+ end
157
+
158
+ def load_entity(id)
159
+ query = query_base_for(id, :n).return(:n)
160
+ result = neo4j_query(query).first
161
+ result && result.n
162
+ end
163
+
164
+ def query_base_for(neo_id, var = :n)
165
+ Neo4j::ActiveBase.new_query.match(var).where(var => {neo_id: neo_id})
166
+ end
167
+
168
+ private
169
+
170
+ def on_create_clause(clause)
171
+ if clause.is_a?(Hash)
172
+ {n: clause.merge(self.new(clause).props_for_create)}
173
+ else
174
+ clause
175
+ end
176
+ end
177
+
178
+ def on_match_clause(clause)
179
+ if clause.is_a?(Hash)
180
+ {n: clause.merge(attributes_nil_hash.key?('updated_at') ? {updated_at: Time.new.to_i} : {})}
181
+ else
182
+ clause
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,60 @@
1
+ module Neo4j::ActiveNode
2
+ module Property
3
+ extend ActiveSupport::Concern
4
+ include Neo4j::Shared::Property
5
+
6
+ def initialize(attributes = nil)
7
+ super(attributes)
8
+ @attributes ||= Hash[self.class.attributes_nil_hash]
9
+ end
10
+
11
+ module ClassMethods
12
+ # Extracts keys from attributes hash which are associations of the model
13
+ # TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save?
14
+ def extract_association_attributes!(attributes)
15
+ return unless contains_association?(attributes)
16
+ attributes.each_with_object({}) do |(key, _), result|
17
+ result[key] = attributes.delete(key) if self.association_key?(key)
18
+ end
19
+ end
20
+
21
+ def association_key?(key)
22
+ association_method_keys.include?(key.to_sym)
23
+ end
24
+
25
+ private
26
+
27
+ def contains_association?(attributes)
28
+ return false unless attributes
29
+ attributes.each_key { |k| return true if association_key?(k) }
30
+ false
31
+ end
32
+
33
+ # All keys which could be association setter methods (including _id/_ids)
34
+ def association_method_keys
35
+ @association_method_keys ||=
36
+ associations_keys.map(&:to_sym) +
37
+ associations.values.map do |association|
38
+ if association.type == :has_one
39
+ "#{association.name}_id"
40
+ elsif association.type == :has_many
41
+ "#{association.name.to_s.singularize}_ids"
42
+ end.to_sym
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def inspect_attributes
50
+ id_property_name = self.class.id_property_name.to_s
51
+
52
+ attribute_pairs = attributes.except(id_property_name).sort.map do |key, value|
53
+ [key, (value.is_a?(String) && value.size > 100) ? value.dup[0..100] : value]
54
+ end
55
+
56
+ attribute_pairs.unshift([id_property_name, self.send(id_property_name)])
57
+ attribute_pairs
58
+ end
59
+ end
60
+ end