nobrainer 0.42.0 → 0.43.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69052877b8fd9f2684f63a0bc7a917812fd771f793d55e7f5182149fc1346643
4
- data.tar.gz: 33dbe632e862a31bb4ac95ab8015834947bd1207df3bbdfb921b742274d06517
3
+ metadata.gz: ea5237da296873c106bd564c6ca6e62674dd63bf3e1214e8542c3097bb978c82
4
+ data.tar.gz: bc02529b83bc6cb8fac1d1895fc3ae927220f6f497a4c4c921e41786b41d3a1a
5
5
  SHA512:
6
- metadata.gz: 102682b8e844b1fd574b3d29f386923bb4dfaee33e88e7da6c5615b4901d20023267cbd4e698bec34a94b77718ca04f251f8aebdfd8f74ef5837c637931dd206
7
- data.tar.gz: 3cb7abcc47fd15d71abac788e7c5844229d30f532c01000d177d7c5b718848b48d2150cda32a1725ed7d0b68adbb775f4461805d9c2f4768104ba1320eb95d52
6
+ metadata.gz: 8aa09bfe2bfc6948258d43d6e9ffc16261ea718da79aabfe2b012ead4b69a42fb0fc5d8d30e6a26a0a1880210ae7f0d342ef0911f3193fa91235e07edb87f509
7
+ data.tar.gz: 999c029ba2e70dd0fd4f458f091c25bbc493d4be0a181e09ae66871a0f6d43bfab005088d509c509cd6cf58bc3528daf72b296a7e02e589d1d23a59b1502c09d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  ## [Unreleased]
8
8
 
9
9
 
10
+ ## [0.43.0] - 2022-06-16
11
+ ### Added
12
+ - Implements polymorphic associations
13
+
10
14
  ## [0.42.0] - 2022-06-15
11
15
  ### Added
12
16
  - Add support for partial compound index queries
@@ -124,8 +128,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
124
128
  - Locks: bug fix: allow small timeouts in lock()
125
129
  - Fix reentrant lock counter on steals
126
130
 
127
- [Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.42.0...HEAD
128
- [0.41.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.1...v0.42.0
131
+ [Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.43.0...HEAD
132
+ [0.43.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.42.0...v0.43.0
133
+ [0.42.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.1...v0.42.0
129
134
  [0.41.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.0...v0.41.1
130
135
  [0.41.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.40.0...v0.41.0
131
136
  [0.40.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.36.0...v0.40.0
@@ -16,6 +16,7 @@ module NoBrainer::Criteria::Join
16
16
  association = model.association_metadata[k.to_sym]
17
17
  raise "`#{k}' must be an association on `#{model}'" unless association
18
18
  raise "join() does not support through associations" if association.options[:through]
19
+ raise "join() does not support polymorphic associations" if association.options[:polymorphic]
19
20
 
20
21
  criteria = association.base_criteria
21
22
  criteria = case v
@@ -2,8 +2,11 @@ class NoBrainer::Document::Association::BelongsTo
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :foreign_key_store_as,
6
- :index, :validates, :required, :uniq, :unique]
5
+ VALID_OPTIONS = %i[
6
+ primary_key foreign_key foreign_type class_name foreign_key_store_as
7
+ index validates required uniq unique polymorphic
8
+ ]
9
+
7
10
  include NoBrainer::Document::Association::Core::Metadata
8
11
  include NoBrainer::Document::Association::EagerLoader::Generic
9
12
 
@@ -11,6 +14,12 @@ class NoBrainer::Document::Association::BelongsTo
11
14
  options[:foreign_key].try(:to_sym) || :"#{target_name}_#{primary_key}"
12
15
  end
13
16
 
17
+ def foreign_type
18
+ return nil unless options[:polymorphic]
19
+
20
+ options[:foreign_type].try(:to_sym) || (:"#{target_name}_type")
21
+ end
22
+
14
23
  def primary_key
15
24
  # We default the primary_key to `:id' and not `target_model.pk_name',
16
25
  # because we don't want to require the target_model to be already loaded.
@@ -30,12 +39,22 @@ class NoBrainer::Document::Association::BelongsTo
30
39
  end
31
40
  end
32
41
 
33
- def target_model
34
- get_model_by_name(options[:class_name] || target_name.to_s.camelize)
42
+ def target_model(target_class = nil)
43
+ return if options[:polymorphic] && target_class.nil?
44
+
45
+ model_name = if options[:polymorphic]
46
+ target_class
47
+ else
48
+ options[:class_name] || target_name.to_s.camelize
49
+ end
50
+
51
+ get_model_by_name(model_name)
35
52
  end
36
53
 
37
- def base_criteria
38
- target_model.without_ordering
54
+ def base_criteria(target_class = nil)
55
+ model = target_model(target_class)
56
+
57
+ model ? model.without_ordering : nil
39
58
  end
40
59
 
41
60
  def hook
@@ -47,6 +66,11 @@ class NoBrainer::Document::Association::BelongsTo
47
66
  raise "Cannot declare `#{target_name}' in #{owner_model}: the foreign_key `#{foreign_key}' is already used"
48
67
  end
49
68
 
69
+ if options[:polymorphic] && options[:class_name]
70
+ raise 'You cannot set class_name on a polymorphic belongs_to'
71
+ end
72
+
73
+ owner_model.field(foreign_type) if options[:polymorphic]
50
74
  owner_model.field(foreign_key, :store_as => options[:foreign_key_store_as], :index => options[:index])
51
75
 
52
76
  unless options[:validates] == false
@@ -85,6 +109,7 @@ class NoBrainer::Document::Association::BelongsTo
85
109
  end
86
110
 
87
111
  def eager_load_owner_key; foreign_key; end
112
+ def eager_load_owner_type; foreign_type; end
88
113
  def eager_load_target_key; primary_key; end
89
114
  end
90
115
 
@@ -97,6 +122,17 @@ class NoBrainer::Document::Association::BelongsTo
97
122
  @target_container = nil
98
123
  end
99
124
 
125
+ def polymorphic_read
126
+ return target if loaded?
127
+
128
+ target_class = owner.read_attribute(foreign_type)
129
+ fk = owner.read_attribute(foreign_key)
130
+
131
+ if target_class && fk
132
+ preload(base_criteria(target_class).where(primary_key => fk).first)
133
+ end
134
+ end
135
+
100
136
  def read
101
137
  return target if loaded?
102
138
 
@@ -105,6 +141,12 @@ class NoBrainer::Document::Association::BelongsTo
105
141
  end
106
142
  end
107
143
 
144
+ def polymorphic_write(target)
145
+ owner.write_attribute(foreign_key, target.try(primary_key))
146
+ owner.write_attribute(foreign_type, target.root_class.name)
147
+ preload(target)
148
+ end
149
+
108
150
  def write(target)
109
151
  assert_target_type(target)
110
152
  owner.write_attribute(foreign_key, target.try(primary_key))
@@ -33,8 +33,8 @@ module NoBrainer::Document::Association::Core
33
33
 
34
34
  def hook
35
35
  options.assert_valid_keys(*self.class.const_get(:VALID_OPTIONS))
36
- delegate("#{target_name}=", :write)
37
- delegate("#{target_name}", :read)
36
+ delegate("#{target_name}=", "#{'polymorphic_' if options[:polymorphic]}write".to_sym)
37
+ delegate("#{target_name}", "#{'polymorphic_' if options[:polymorphic]}read".to_sym)
38
38
  end
39
39
 
40
40
  def add_callback_for(what)
@@ -62,7 +62,8 @@ module NoBrainer::Document::Association::Core
62
62
 
63
63
  included { attr_accessor :metadata, :owner }
64
64
 
65
- delegate :primary_key, :foreign_key, :target_name, :target_model, :base_criteria, :to => :metadata
65
+ delegate :primary_key, :foreign_key, :foreign_type, :target_name,
66
+ :target_model, :base_criteria, :to => :metadata
66
67
 
67
68
  def initialize(metadata, owner)
68
69
  @metadata, @owner = metadata, owner
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NoBrainer::Document::Association::EagerLoader
2
4
  extend self
3
5
 
@@ -5,11 +7,22 @@ module NoBrainer::Document::Association::EagerLoader
5
7
  # Used in associations to declare generic eager loading capabilities
6
8
  # The association should implement loaded?, preload,
7
9
  # eager_load_owner_key and eager_load_target_key.
8
- def eager_load(docs, additional_criteria=nil)
10
+ def eager_load(docs, additional_criteria = nil)
9
11
  owner_key = eager_load_owner_key
12
+ owner_type = eager_load_owner_type
10
13
  target_key = eager_load_target_key
11
14
 
12
- criteria = base_criteria
15
+ if is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) && owner_type
16
+ target_class = docs.first.__send__(owner_type)
17
+
18
+ if docs.detect { |doc| doc.__send__(owner_type) != target_class }
19
+ raise NoBrainer::Error::PolymorphicAssociationWithDifferentTypes,
20
+ "The documents to be eager loaded doesn't have the same " \
21
+ 'type, which is not supported'
22
+ end
23
+ end
24
+
25
+ criteria = target_class ? base_criteria(target_class) : base_criteria
13
26
  criteria = criteria.merge(additional_criteria) if additional_criteria
14
27
 
15
28
  unloaded_docs = docs.reject { |doc| doc.associations[self].loaded? }
@@ -2,12 +2,20 @@ class NoBrainer::Document::Association::HasMany
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope]
5
+ VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope,
6
+ :as]
6
7
  include NoBrainer::Document::Association::Core::Metadata
7
8
  include NoBrainer::Document::Association::EagerLoader::Generic
8
9
 
9
10
  def foreign_key
10
- options[:foreign_key].try(:to_sym) || :"#{owner_model.name.split('::').last.underscore}_#{primary_key}"
11
+ return options[:foreign_key].try(:to_sym) if options.key?(:foreign_key)
12
+ return :"#{options[:as]}_#{primary_key}" if options[:as]
13
+
14
+ :"#{owner_model.name.split('::').last.underscore}_#{primary_key}"
15
+ end
16
+
17
+ def foreign_type
18
+ options[:foreign_type].try(:to_sym) || (options[:as] && :"#{options[:as]}_type")
11
19
  end
12
20
 
13
21
  def primary_key
@@ -30,9 +38,9 @@ class NoBrainer::Document::Association::HasMany
30
38
  # caching is hard (rails console reload, etc.).
31
39
  target_model.association_metadata.values.select do |assoc|
32
40
  assoc.is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) and
33
- assoc.foreign_key == self.foreign_key and
34
- assoc.primary_key == self.primary_key and
35
- assoc.target_model.root_class == owner_model.root_class
41
+ assoc.foreign_key == foreign_key and
42
+ assoc.primary_key == primary_key and
43
+ assoc.target_model(target_model).root_class == owner_model.root_class
36
44
  end
37
45
  end
38
46
 
@@ -46,7 +54,7 @@ class NoBrainer::Document::Association::HasMany
46
54
 
47
55
  if options[:dependent]
48
56
  unless [:destroy, :delete, :nullify, :restrict, nil].include?(options[:dependent])
49
- raise "Invalid dependent option: `#{options[:dependent].inspect}'. " +
57
+ raise "Invalid dependent option: `#{options[:dependent].inspect}'. " \
50
58
  "Valid options are: :destroy, :delete, :nullify, or :restrict"
51
59
  end
52
60
  add_callback_for(:before_destroy)
@@ -54,12 +62,22 @@ class NoBrainer::Document::Association::HasMany
54
62
  end
55
63
 
56
64
  def eager_load_owner_key; primary_key; end
65
+ def eager_load_owner_type; foreign_type; end
57
66
  def eager_load_target_key; foreign_key; end
58
67
  end
59
68
 
60
69
  def target_criteria
61
- @target_criteria ||= base_criteria.where(foreign_key => owner.__send__(primary_key))
62
- .after_find(set_inverse_proc)
70
+ @target_criteria ||= begin
71
+ query_criteria = { foreign_key => owner.__send__(primary_key) }
72
+
73
+ if metadata.options[:as]
74
+ query_criteria = query_criteria.merge(
75
+ foreign_type => owner.root_class.name
76
+ )
77
+ end
78
+
79
+ base_criteria.where(query_criteria).after_find(set_inverse_proc)
80
+ end
63
81
  end
64
82
 
65
83
  def read
@@ -1,19 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NoBrainer::Error
2
- class Connection < RuntimeError; end
3
- class DocumentNotFound < RuntimeError; end
4
- class DocumentNotPersisted < RuntimeError; end
5
- class ChildrenExist < RuntimeError; end
6
- class CannotUseIndex < RuntimeError; end
7
- class MissingIndex < RuntimeError; end
8
- class AssociationNotPersisted < RuntimeError; end
9
- class ReadonlyField < RuntimeError; end
10
- class MissingAttribute < RuntimeError; end
11
- class UnknownAttribute < RuntimeError; end
12
- class AtomicBlock < RuntimeError; end
13
- class LostLock < RuntimeError; end
14
- class LockInvalidOp < RuntimeError; end
15
- class LockUnavailable < RuntimeError; end
16
- class InvalidPolymorphicType < RuntimeError; end
4
+ class AssociationNotPersisted < RuntimeError; end
5
+ class AtomicBlock < RuntimeError; end
6
+ class ChildrenExist < RuntimeError; end
7
+ class Connection < RuntimeError; end
8
+ class DocumentNotFound < RuntimeError; end
9
+ class DocumentNotPersisted < RuntimeError; end
10
+ class InvalidPolymorphicType < RuntimeError; end
11
+ class LockInvalidOp < RuntimeError; end
12
+ class LostLock < RuntimeError; end
13
+ class LockUnavailable < RuntimeError; end
14
+ class MissingAttribute < RuntimeError; end
15
+ class MissingIndex < RuntimeError; end
16
+ class PolymorphicAssociationWithDifferentTypes < RuntimeError; end
17
+ class ReadonlyField < RuntimeError; end
18
+ class UnknownAttribute < RuntimeError; end
17
19
 
18
20
  class DocumentInvalid < RuntimeError
19
21
  attr_accessor :instance
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nobrainer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.42.0
4
+ version: 0.43.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Viennot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-15 00:00:00.000000000 Z
11
+ date: 2022-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel