activeentity 0.0.1.beta9 → 0.0.1.beta10
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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c77d6daeebeba0865b5de31611679a61db4f94f3c5a89d70e33e52f1bcfcf92
|
4
|
+
data.tar.gz: 01c96c7a6e7580563fd4ddfa81e8864b2fb1fc0550d853fd92de3eb9baf4eb13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6fe7aac59d6298f3b952064b78d83f4f566c9dd646e86617af567f3f6c78d0702544e5106f9c54ff8013a07d59f33b6992a703b142ee40e319eaf457ccc0ca30
|
7
|
+
data.tar.gz: 5586c053be18d9e9aeef3c91f8362974c98d1819173169f999c2988dca50dbc12c43b44420015594f1e7a4d1639da8e429a037ac47235b9a7c4eefc35f218c47
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveEntity
|
4
|
+
module Validations
|
5
|
+
class UniquenessOnActiveRecordValidator < ActiveModel::EachValidator # :nodoc:
|
6
|
+
def initialize(options)
|
7
|
+
if options[:conditions] && !options[:conditions].respond_to?(:call)
|
8
|
+
raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
|
9
|
+
"Pass a callable instead: `conditions: -> { where(approved: true) }`"
|
10
|
+
end
|
11
|
+
unless Array(options[:scope]).all? { |scope| scope.respond_to?(:to_sym) }
|
12
|
+
raise ArgumentError, "#{options[:scope]} is not supported format for :scope option. " \
|
13
|
+
"Pass a symbol or an array of symbols instead: `scope: :user_id`"
|
14
|
+
end
|
15
|
+
|
16
|
+
super
|
17
|
+
|
18
|
+
@finder_class =
|
19
|
+
if options[:ar_class_name].present?
|
20
|
+
options[:ar_class_name].safe_constantize
|
21
|
+
elsif options[:ar_class].present? && options[:ar_class].is_a?(Class)
|
22
|
+
options[:ar_class]
|
23
|
+
else
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
unless @finder_class
|
28
|
+
raise ArgumentError, "Must provide one of option :class_name or :class."
|
29
|
+
end
|
30
|
+
unless @finder_class < ActiveRecord::Base
|
31
|
+
raise ArgumentError, "Class must be an Active Record model, but got #{@finder_class}."
|
32
|
+
end
|
33
|
+
if @finder_class.abstract_class?
|
34
|
+
raise ArgumentError, "Class can't be an abstract class."
|
35
|
+
end
|
36
|
+
|
37
|
+
@primary_key_attribute_name = options[:primary_key_attribute_name]
|
38
|
+
@present_only = options[:present_only]
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_each(record, attribute, value)
|
42
|
+
value = map_enum_attribute(@finder_class, attribute, value)
|
43
|
+
if @present_only && value.blank?
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
relation = build_relation(@finder_class, attribute, value)
|
48
|
+
if @primary_key_attribute_name.present?
|
49
|
+
primary_key_attribute = record.read_attribute(@primary_key_attribute_name)
|
50
|
+
if primary_key_attribute.present?
|
51
|
+
if @finder_class.primary_key
|
52
|
+
relation = relation.where.not(@finder_class.primary_key => primary_key_attribute)
|
53
|
+
else
|
54
|
+
raise ActiveRecord::UnknownPrimaryKey.new(@finder_class, "Can not validate uniqueness for persisted record without primary key.")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
relation = scope_relation(record, relation)
|
59
|
+
relation = relation.merge(options[:conditions]) if options[:conditions]
|
60
|
+
|
61
|
+
if relation.exists?
|
62
|
+
error_options = options.except(:case_sensitive, :scope, :conditions, :ar_class, :ar_class_name, :primary_key_attribute_name)
|
63
|
+
error_options[:value] = value
|
64
|
+
|
65
|
+
record.errors.add(attribute, :taken, error_options)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def build_relation(klass, attribute, value)
|
72
|
+
relation = klass.unscoped
|
73
|
+
comparison = relation.bind_attribute(attribute, value) do |attr, bind|
|
74
|
+
return relation.none! if bind.unboundable?
|
75
|
+
|
76
|
+
if !options.key?(:case_sensitive) || bind.nil?
|
77
|
+
klass.connection.default_uniqueness_comparison(attr, bind, klass)
|
78
|
+
elsif options[:case_sensitive]
|
79
|
+
klass.connection.case_sensitive_comparison(attr, bind)
|
80
|
+
else
|
81
|
+
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
|
82
|
+
klass.connection.case_insensitive_comparison(attr, bind)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
relation.where!(comparison)
|
87
|
+
end
|
88
|
+
|
89
|
+
def scope_relation(record, relation)
|
90
|
+
Array(options[:scope]).each do |scope_item|
|
91
|
+
scope_value = if record.class._reflect_on_association(scope_item)
|
92
|
+
record.association(scope_item).reader
|
93
|
+
else
|
94
|
+
record._read_attribute(scope_item)
|
95
|
+
end
|
96
|
+
relation = relation.where(scope_item => scope_value)
|
97
|
+
end
|
98
|
+
|
99
|
+
relation
|
100
|
+
end
|
101
|
+
|
102
|
+
def map_enum_attribute(klass, attribute, value)
|
103
|
+
mapping = klass.defined_enums[attribute.to_s]
|
104
|
+
value = mapping[value] if value && mapping
|
105
|
+
value
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
module ClassMethods
|
110
|
+
# Validates whether the value of the specified attributes are unique
|
111
|
+
# across the system. Useful for making sure that only one user
|
112
|
+
# can be named "davidhh".
|
113
|
+
#
|
114
|
+
# class Person < ActiveRecord::Base
|
115
|
+
# validates_uniqueness_of :user_name
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# It can also validate whether the value of the specified attributes are
|
119
|
+
# unique based on a <tt>:scope</tt> parameter:
|
120
|
+
#
|
121
|
+
# class Person < ActiveRecord::Base
|
122
|
+
# validates_uniqueness_of :user_name, scope: :account_id
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# Or even multiple scope parameters. For example, making sure that a
|
126
|
+
# teacher can only be on the schedule once per semester for a particular
|
127
|
+
# class.
|
128
|
+
#
|
129
|
+
# class TeacherSchedule < ActiveRecord::Base
|
130
|
+
# validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# It is also possible to limit the uniqueness constraint to a set of
|
134
|
+
# records matching certain conditions. In this example archived articles
|
135
|
+
# are not being taken into consideration when validating uniqueness
|
136
|
+
# of the title attribute:
|
137
|
+
#
|
138
|
+
# class Article < ActiveRecord::Base
|
139
|
+
# validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
# When the record is created, a check is performed to make sure that no
|
143
|
+
# record exists in the database with the given value for the specified
|
144
|
+
# attribute (that maps to a column). When the record is updated,
|
145
|
+
# the same check is made but disregarding the record itself.
|
146
|
+
#
|
147
|
+
# Configuration options:
|
148
|
+
#
|
149
|
+
# * <tt>:message</tt> - Specifies a custom error message (default is:
|
150
|
+
# "has already been taken").
|
151
|
+
# * <tt>:scope</tt> - One or more columns by which to limit the scope of
|
152
|
+
# the uniqueness constraint.
|
153
|
+
# * <tt>:conditions</tt> - Specify the conditions to be included as a
|
154
|
+
# <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
|
155
|
+
# (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
|
156
|
+
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
|
157
|
+
# non-text columns (+true+ by default).
|
158
|
+
# * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
|
159
|
+
# attribute is +nil+ (default is +false+).
|
160
|
+
# * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
|
161
|
+
# attribute is blank (default is +false+).
|
162
|
+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
|
163
|
+
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
|
164
|
+
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
|
165
|
+
# proc or string should return or evaluate to a +true+ or +false+ value.
|
166
|
+
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
|
167
|
+
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
|
168
|
+
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
169
|
+
# method, proc or string should return or evaluate to a +true+ or +false+
|
170
|
+
# value.
|
171
|
+
#
|
172
|
+
# === Concurrency and integrity
|
173
|
+
#
|
174
|
+
# Using this validation method in conjunction with
|
175
|
+
# {ActiveRecord::Base#save}[rdoc-ref:Persistence#save]
|
176
|
+
# does not guarantee the absence of duplicate record insertions, because
|
177
|
+
# uniqueness checks on the application level are inherently prone to race
|
178
|
+
# conditions. For example, suppose that two users try to post a Comment at
|
179
|
+
# the same time, and a Comment's title must be unique. At the database-level,
|
180
|
+
# the actions performed by these users could be interleaved in the following manner:
|
181
|
+
#
|
182
|
+
# User 1 | User 2
|
183
|
+
# ------------------------------------+--------------------------------------
|
184
|
+
# # User 1 checks whether there's |
|
185
|
+
# # already a comment with the title |
|
186
|
+
# # 'My Post'. This is not the case. |
|
187
|
+
# SELECT * FROM comments |
|
188
|
+
# WHERE title = 'My Post' |
|
189
|
+
# |
|
190
|
+
# | # User 2 does the same thing and also
|
191
|
+
# | # infers that their title is unique.
|
192
|
+
# | SELECT * FROM comments
|
193
|
+
# | WHERE title = 'My Post'
|
194
|
+
# |
|
195
|
+
# # User 1 inserts their comment. |
|
196
|
+
# INSERT INTO comments |
|
197
|
+
# (title, content) VALUES |
|
198
|
+
# ('My Post', 'hi!') |
|
199
|
+
# |
|
200
|
+
# | # User 2 does the same thing.
|
201
|
+
# | INSERT INTO comments
|
202
|
+
# | (title, content) VALUES
|
203
|
+
# | ('My Post', 'hello!')
|
204
|
+
# |
|
205
|
+
# | # ^^^^^^
|
206
|
+
# | # Boom! We now have a duplicate
|
207
|
+
# | # title!
|
208
|
+
#
|
209
|
+
# The best way to work around this problem is to add a unique index to the database table using
|
210
|
+
# {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
|
211
|
+
# In the rare case that a race condition occurs, the database will guarantee
|
212
|
+
# the field's uniqueness.
|
213
|
+
#
|
214
|
+
# When the database catches such a duplicate insertion,
|
215
|
+
# {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveRecord::StatementInvalid
|
216
|
+
# exception. You can either choose to let this error propagate (which
|
217
|
+
# will result in the default Rails exception page being shown), or you
|
218
|
+
# can catch it and restart the transaction (e.g. by telling the user
|
219
|
+
# that the title already exists, and asking them to re-enter the title).
|
220
|
+
# This technique is also known as
|
221
|
+
# {optimistic concurrency control}[https://en.wikipedia.org/wiki/Optimistic_concurrency_control].
|
222
|
+
#
|
223
|
+
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
|
224
|
+
# constraint errors from other types of database errors by throwing an
|
225
|
+
# ActiveRecord::RecordNotUnique exception. For other adapters you will
|
226
|
+
# have to parse the (database-specific) exception message to detect such
|
227
|
+
# a case.
|
228
|
+
#
|
229
|
+
# The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
|
230
|
+
#
|
231
|
+
# * ActiveRecord::ConnectionAdapters::Mysql2Adapter.
|
232
|
+
# * ActiveRecord::ConnectionAdapters::SQLite3Adapter.
|
233
|
+
# * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.
|
234
|
+
def validates_uniqueness_on_active_record_of(*attr_names)
|
235
|
+
validates_with UniquenessOnActiveRecordValidator, _merge_attributes(attr_names)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activeentity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.1.
|
4
|
+
version: 0.0.1.beta10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- jasl
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-05-
|
11
|
+
date: 2019-05-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -130,6 +130,7 @@ files:
|
|
130
130
|
- lib/active_entity/validations/presence.rb
|
131
131
|
- lib/active_entity/validations/subset.rb
|
132
132
|
- lib/active_entity/validations/uniqueness_in_embedding.rb
|
133
|
+
- lib/active_entity/validations/uniqueness_on_active_record.rb
|
133
134
|
- lib/active_entity/version.rb
|
134
135
|
- lib/core_ext/array_without_blank.rb
|
135
136
|
- lib/tasks/active_entity_tasks.rake
|