nobrainer 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/no_brainer/config.rb +20 -4
- data/lib/no_brainer/criteria/count.rb +1 -1
- data/lib/no_brainer/criteria/delete.rb +2 -2
- data/lib/no_brainer/criteria/first.rb +6 -0
- data/lib/no_brainer/criteria/order_by.rb +33 -10
- data/lib/no_brainer/criteria/update.rb +2 -2
- data/lib/no_brainer/criteria/where.rb +12 -6
- data/lib/no_brainer/decorated_symbol.rb +2 -1
- data/lib/no_brainer/document/association/belongs_to.rb +10 -5
- data/lib/no_brainer/document/association/core.rb +1 -1
- data/lib/no_brainer/document/association/has_many.rb +10 -4
- data/lib/no_brainer/document/attributes.rb +29 -3
- data/lib/no_brainer/document/callbacks.rb +2 -10
- data/lib/no_brainer/document/core.rb +5 -3
- data/lib/no_brainer/document/criteria.rb +11 -11
- data/lib/no_brainer/document/dirty.rb +11 -0
- data/lib/no_brainer/document/dynamic_attributes.rb +4 -0
- data/lib/no_brainer/document/id.rb +49 -4
- data/lib/no_brainer/document/index.rb +6 -2
- data/lib/no_brainer/document/persistance.rb +7 -6
- data/lib/no_brainer/document/readonly.rb +7 -0
- data/lib/no_brainer/document/serialization.rb +4 -0
- data/lib/no_brainer/document/types/boolean.rb +26 -0
- data/lib/no_brainer/document/types/date.rb +26 -0
- data/lib/no_brainer/document/types/float.rb +20 -0
- data/lib/no_brainer/document/types/integer.rb +18 -0
- data/lib/no_brainer/document/types/string.rb +14 -0
- data/lib/no_brainer/document/types/symbol.rb +21 -0
- data/lib/no_brainer/document/types/time.rb +41 -0
- data/lib/no_brainer/document/types.rb +64 -124
- data/lib/no_brainer/document/uniqueness.rb +4 -2
- data/lib/no_brainer/document/validation.rb +2 -1
- data/lib/no_brainer/document.rb +2 -0
- data/lib/no_brainer/error.rb +9 -9
- data/lib/no_brainer/query_runner/logger.rb +18 -12
- data/lib/no_brainer/query_runner/missing_index.rb +15 -3
- data/lib/no_brainer/query_runner/reconnect.rb +15 -4
- data/lib/no_brainer/query_runner/table_on_demand.rb +14 -25
- data/lib/no_brainer/query_runner/write_error.rb +5 -8
- data/lib/no_brainer/rql.rb +25 -0
- data/lib/nobrainer.rb +9 -6
- data/lib/rails/generators/nobrainer/model/model_generator.rb +2 -1
- data/lib/rails/generators/nobrainer/model/templates/model.rb.tt +7 -2
- metadata +18 -11
- data/lib/no_brainer/util.rb +0 -23
|
@@ -19,7 +19,8 @@ module NoBrainer::Document::Persistance
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def reload(options={})
|
|
22
|
-
attrs =
|
|
22
|
+
attrs = NoBrainer.run { self.class.selector_for(pk_value) }
|
|
23
|
+
raise NoBrainer::Error::DocumentNotFound, "#{self.class} #{self.class.pk_name}: #{pk_value} not found" unless attrs
|
|
23
24
|
instance_variables.each { |ivar| remove_instance_variable(ivar) } unless options[:keep_ivars]
|
|
24
25
|
initialize(attrs, :pristine => true, :from_db => true)
|
|
25
26
|
self
|
|
@@ -27,14 +28,14 @@ module NoBrainer::Document::Persistance
|
|
|
27
28
|
|
|
28
29
|
def _create(options={})
|
|
29
30
|
return false if options[:validate] && !valid?
|
|
30
|
-
keys = self.class.insert_all(@_attributes)
|
|
31
|
-
self.
|
|
31
|
+
keys = self.class.insert_all(self.class.persistable_attributes(@_attributes))
|
|
32
|
+
self.pk_value ||= keys.first
|
|
32
33
|
@new_record = false
|
|
33
34
|
true
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def _update(attrs)
|
|
37
|
-
selector.
|
|
38
|
+
NoBrainer.run { selector.update(attrs) }
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def _update_only_changed_attrs(options={})
|
|
@@ -49,7 +50,7 @@ module NoBrainer::Document::Persistance
|
|
|
49
50
|
attr = RethinkDB::RQL.new.literal(attr) if attr.is_a?(Hash)
|
|
50
51
|
[k, attr]
|
|
51
52
|
end]
|
|
52
|
-
_update(attrs) if attrs.present?
|
|
53
|
+
_update(self.class.persistable_attributes(attrs)) if attrs.present?
|
|
53
54
|
true
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -73,7 +74,7 @@ module NoBrainer::Document::Persistance
|
|
|
73
74
|
|
|
74
75
|
def delete
|
|
75
76
|
unless @destroyed
|
|
76
|
-
selector.
|
|
77
|
+
NoBrainer.run { selector.delete }
|
|
77
78
|
@destroyed = true
|
|
78
79
|
end
|
|
79
80
|
@_attributes.freeze
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# We namespace our fake Boolean class to avoid polluting the global namespace
|
|
2
|
+
class NoBrainer::Boolean
|
|
3
|
+
def initialize; raise; end
|
|
4
|
+
def self.inspect; 'Boolean'; end
|
|
5
|
+
def self.to_s; inspect; end
|
|
6
|
+
def self.name; inspect; end
|
|
7
|
+
|
|
8
|
+
module NoBrainerExtentions
|
|
9
|
+
InvalidType = NoBrainer::Error::InvalidType
|
|
10
|
+
|
|
11
|
+
def nobrainer_cast_user_to_model(value)
|
|
12
|
+
case value
|
|
13
|
+
when TrueClass then true
|
|
14
|
+
when FalseClass then false
|
|
15
|
+
when String, Integer
|
|
16
|
+
value = value.to_s.strip.downcase
|
|
17
|
+
return true if value.in? %w(true yes t 1)
|
|
18
|
+
return false if value.in? %w(false no f 0)
|
|
19
|
+
raise InvalidType
|
|
20
|
+
else raise InvalidType
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
extend NoBrainerExtentions
|
|
25
|
+
end
|
|
26
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Date
|
|
2
|
+
module NoBrainerExtentions
|
|
3
|
+
InvalidType = NoBrainer::Error::InvalidType
|
|
4
|
+
|
|
5
|
+
def nobrainer_cast_user_to_model(value)
|
|
6
|
+
case value
|
|
7
|
+
when Date then value
|
|
8
|
+
when String
|
|
9
|
+
value = value.strip
|
|
10
|
+
date = Date.parse(value) rescue (raise InvalidType)
|
|
11
|
+
raise InvalidType unless date.iso8601 == value
|
|
12
|
+
date
|
|
13
|
+
else raise InvalidType
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def nobrainer_cast_db_to_model(value)
|
|
18
|
+
value.is_a?(Time) ? value.to_date : value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def nobrainer_cast_model_to_db(value)
|
|
22
|
+
value.is_a?(Date) ? Time.utc(value.year, value.month, value.day) : value
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
extend NoBrainerExtentions
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class Float
|
|
2
|
+
module NoBrainerExtentions
|
|
3
|
+
InvalidType = NoBrainer::Error::InvalidType
|
|
4
|
+
|
|
5
|
+
def nobrainer_cast_user_to_model(value)
|
|
6
|
+
case value
|
|
7
|
+
when Float then value
|
|
8
|
+
when Integer then value.to_f
|
|
9
|
+
when String
|
|
10
|
+
value = value.strip.gsub(/^\+/, '')
|
|
11
|
+
value = value.gsub(/0+$/, '') if value['.']
|
|
12
|
+
value = value.gsub(/\.$/, '')
|
|
13
|
+
value = "#{value}.0" unless value['.']
|
|
14
|
+
value.to_f.tap { |new_value| new_value.to_s == value or raise InvalidType }
|
|
15
|
+
else raise InvalidType
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
extend NoBrainerExtentions
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class Integer
|
|
2
|
+
module NoBrainerExtentions
|
|
3
|
+
InvalidType = NoBrainer::Error::InvalidType
|
|
4
|
+
|
|
5
|
+
def nobrainer_cast_user_to_model(value)
|
|
6
|
+
case value
|
|
7
|
+
when Integer then value
|
|
8
|
+
when String
|
|
9
|
+
value = value.strip.gsub(/^\+/, '')
|
|
10
|
+
value.to_i.tap { |new_value| new_value.to_s == value or raise InvalidType }
|
|
11
|
+
when Float
|
|
12
|
+
value.to_i.tap { |new_value| new_value.to_f == value or raise InvalidType }
|
|
13
|
+
else raise InvalidType
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
extend NoBrainerExtentions
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class String
|
|
2
|
+
module NoBrainerExtentions
|
|
3
|
+
InvalidType = NoBrainer::Error::InvalidType
|
|
4
|
+
|
|
5
|
+
def nobrainer_cast_user_to_model(value)
|
|
6
|
+
case value
|
|
7
|
+
when String then value
|
|
8
|
+
when Symbol then value.to_s
|
|
9
|
+
else raise InvalidType
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
extend NoBrainerExtentions
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class Symbol
|
|
2
|
+
module NoBrainerExtentions
|
|
3
|
+
InvalidType = NoBrainer::Error::InvalidType
|
|
4
|
+
|
|
5
|
+
def nobrainer_cast_user_to_model(value)
|
|
6
|
+
case value
|
|
7
|
+
when Symbol then value
|
|
8
|
+
when String
|
|
9
|
+
value = value.strip
|
|
10
|
+
raise InvalidType if value.empty?
|
|
11
|
+
value.to_sym
|
|
12
|
+
else raise InvalidType
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def nobrainer_cast_db_to_model(value)
|
|
17
|
+
value.to_sym rescue (value.to_s.to_sym rescue value)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
extend NoBrainerExtentions
|
|
21
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
3
|
+
class Time
|
|
4
|
+
module NoBrainerExtentions
|
|
5
|
+
InvalidType = NoBrainer::Error::InvalidType
|
|
6
|
+
|
|
7
|
+
def nobrainer_cast_user_to_model(value)
|
|
8
|
+
case value
|
|
9
|
+
when Time then time = value
|
|
10
|
+
when String
|
|
11
|
+
value = value.strip.sub(/Z$/, '+00:00')
|
|
12
|
+
# Using DateTime to preserve the timezone offset
|
|
13
|
+
dt = DateTime.parse(value) rescue (raise InvalidType)
|
|
14
|
+
time = Time.new(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.zone)
|
|
15
|
+
raise InvalidType unless time.iso8601 == value
|
|
16
|
+
else raise InvalidType
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
nobrainer_timezoned(NoBrainer::Config.user_timezone, time)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def nobrainer_cast_db_to_model(value)
|
|
23
|
+
return value unless value.is_a?(Time)
|
|
24
|
+
nobrainer_timezoned(NoBrainer::Config.user_timezone, value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def nobrainer_cast_model_to_db(value)
|
|
28
|
+
return value unless value.is_a?(Time)
|
|
29
|
+
nobrainer_timezoned(NoBrainer::Config.db_timezone, value)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def nobrainer_timezoned(tz, value)
|
|
33
|
+
case tz
|
|
34
|
+
when :local then value.getlocal
|
|
35
|
+
when :utc then value.getutc
|
|
36
|
+
when :unchanged then value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
extend NoBrainerExtentions
|
|
41
|
+
end
|
|
@@ -1,102 +1,7 @@
|
|
|
1
1
|
module NoBrainer::Document::Types
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
extend self
|
|
6
|
-
InvalidType = NoBrainer::Error::InvalidType
|
|
7
|
-
|
|
8
|
-
def String(value)
|
|
9
|
-
case value
|
|
10
|
-
when Symbol then value.to_s
|
|
11
|
-
else raise InvalidType
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def Integer(value)
|
|
16
|
-
case value
|
|
17
|
-
when String
|
|
18
|
-
value = value.strip.gsub(/^\+/, '')
|
|
19
|
-
value.to_i.tap { |new_value| new_value.to_s == value or raise InvalidType }
|
|
20
|
-
when Float
|
|
21
|
-
value.to_i.tap { |new_value| new_value.to_f == value or raise InvalidType }
|
|
22
|
-
else raise InvalidType
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def Float(value)
|
|
27
|
-
case value
|
|
28
|
-
when Integer then value.to_f
|
|
29
|
-
when String
|
|
30
|
-
value = value.strip.gsub(/^\+/, '')
|
|
31
|
-
value = value.gsub(/0+$/, '') if value['.']
|
|
32
|
-
value = value.gsub(/\.$/, '')
|
|
33
|
-
value = "#{value}.0" unless value['.']
|
|
34
|
-
value.to_f.tap { |new_value| new_value.to_s == value or raise InvalidType }
|
|
35
|
-
else raise InvalidType
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def Boolean(value)
|
|
40
|
-
case value
|
|
41
|
-
when TrueClass then true
|
|
42
|
-
when FalseClass then false
|
|
43
|
-
when String, Integer
|
|
44
|
-
value = value.to_s.strip.downcase
|
|
45
|
-
return true if value.in? %w(true yes t 1)
|
|
46
|
-
return false if value.in? %w(false no f 0)
|
|
47
|
-
raise InvalidType
|
|
48
|
-
else raise InvalidType
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def Symbol(value)
|
|
53
|
-
case value
|
|
54
|
-
when String
|
|
55
|
-
value = value.strip
|
|
56
|
-
raise InvalidType if value.empty?
|
|
57
|
-
value.to_sym
|
|
58
|
-
else raise InvalidType
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def lookup(type)
|
|
63
|
-
public_method(type.to_s)
|
|
64
|
-
rescue NameError
|
|
65
|
-
proc { raise InvalidType }
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
module CastDBToUser
|
|
70
|
-
extend self
|
|
71
|
-
|
|
72
|
-
def Symbol(value)
|
|
73
|
-
value.to_sym rescue value
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def lookup(type)
|
|
77
|
-
public_method(type.to_s)
|
|
78
|
-
rescue NameError
|
|
79
|
-
nil
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
included do
|
|
84
|
-
# We namespace our fake Boolean class to avoid polluting the global namespace
|
|
85
|
-
class_exec do
|
|
86
|
-
class Boolean
|
|
87
|
-
def initialize; raise; end
|
|
88
|
-
def self.inspect; 'Boolean'; end
|
|
89
|
-
def self.to_s; inspect; end
|
|
90
|
-
def self.name; inspect; end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
before_validation :add_type_errors
|
|
94
|
-
|
|
95
|
-
# Fast access for db->user cast methods for performance when reading from
|
|
96
|
-
# the database.
|
|
97
|
-
singleton_class.send(:attr_accessor, :cast_db_to_user_fields)
|
|
98
|
-
self.cast_db_to_user_fields = Set.new
|
|
99
|
-
end
|
|
4
|
+
included { before_validation :add_type_errors }
|
|
100
5
|
|
|
101
6
|
def add_type_errors
|
|
102
7
|
return unless @pending_type_errors
|
|
@@ -108,49 +13,59 @@ module NoBrainer::Document::Types
|
|
|
108
13
|
def assign_attributes(attrs, options={})
|
|
109
14
|
super
|
|
110
15
|
if options[:from_db]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
value = @_attributes[attr.to_s]
|
|
115
|
-
unless value.nil? || value.is_a?(type)
|
|
116
|
-
@_attributes[attr.to_s] = field_def[:cast_db_to_user].call(value)
|
|
117
|
-
end
|
|
118
|
-
end
|
|
16
|
+
@_attributes = Hash[@_attributes.map do |k,v|
|
|
17
|
+
[k, self.class.cast_db_to_model_for(k, v)]
|
|
18
|
+
end].with_indifferent_access
|
|
119
19
|
end
|
|
120
20
|
end
|
|
121
21
|
|
|
122
22
|
module ClassMethods
|
|
123
|
-
def
|
|
124
|
-
|
|
125
|
-
return value if
|
|
126
|
-
type
|
|
127
|
-
|
|
128
|
-
|
|
23
|
+
def cast_user_to_model_for(attr, value)
|
|
24
|
+
type = fields[attr.to_sym].try(:[], :type)
|
|
25
|
+
return value if type.nil? || value.nil?
|
|
26
|
+
if type.respond_to?(:nobrainer_cast_user_to_model)
|
|
27
|
+
type.nobrainer_cast_user_to_model(value)
|
|
28
|
+
else
|
|
29
|
+
raise NoBrainer::Error::InvalidType unless value.is_a?(type)
|
|
30
|
+
value
|
|
31
|
+
end
|
|
129
32
|
rescue NoBrainer::Error::InvalidType => error
|
|
130
|
-
error.type =
|
|
33
|
+
error.type = type
|
|
131
34
|
error.value = value
|
|
132
35
|
error.attr_name = attr
|
|
133
36
|
raise error
|
|
134
37
|
end
|
|
135
38
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
|
|
39
|
+
def cast_model_to_db_for(attr, value)
|
|
40
|
+
type = fields[attr.to_sym].try(:[], :type)
|
|
41
|
+
return value if type.nil? || value.nil? || !type.respond_to?(:nobrainer_cast_model_to_db)
|
|
42
|
+
type.nobrainer_cast_model_to_db(value)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def cast_db_to_model_for(attr, value)
|
|
46
|
+
type = fields[attr.to_sym].try(:[], :type)
|
|
47
|
+
return value if type.nil? || value.nil? || !type.respond_to?(:nobrainer_cast_db_to_model)
|
|
48
|
+
type.nobrainer_cast_db_to_model(value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def cast_user_to_db_for(attr, value)
|
|
52
|
+
value = cast_user_to_model_for(attr, value)
|
|
53
|
+
cast_model_to_db_for(attr, value)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def persistable_attributes(attrs)
|
|
57
|
+
Hash[attrs.map { |k,v| [k, cast_model_to_db_for(k, v)] }]
|
|
139
58
|
end
|
|
140
59
|
|
|
141
60
|
def _field(attr, options={})
|
|
142
61
|
super
|
|
143
62
|
|
|
144
|
-
if options[:
|
|
145
|
-
([self] + descendants).each do |klass|
|
|
146
|
-
klass.cast_db_to_user_fields << attr
|
|
147
|
-
end
|
|
148
|
-
end
|
|
63
|
+
NoBrainer::Document::Types.load_type_extensions(options[:type]) if options[:type]
|
|
149
64
|
|
|
150
65
|
inject_in_layer :types do
|
|
151
66
|
define_method("#{attr}=") do |value|
|
|
152
67
|
begin
|
|
153
|
-
value = self.class.
|
|
68
|
+
value = self.class.cast_user_to_model_for(attr, value)
|
|
154
69
|
@pending_type_errors.try(:delete, attr)
|
|
155
70
|
rescue NoBrainer::Error::InvalidType => error
|
|
156
71
|
@pending_type_errors ||= {}
|
|
@@ -163,13 +78,38 @@ module NoBrainer::Document::Types
|
|
|
163
78
|
end
|
|
164
79
|
end
|
|
165
80
|
|
|
81
|
+
def _remove_field(attr, options={})
|
|
82
|
+
super
|
|
83
|
+
|
|
84
|
+
inject_in_layer :types do
|
|
85
|
+
remove_method("#{attr}=")
|
|
86
|
+
remove_method("#{attr}?") if method_defined?("#{attr}?")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
166
90
|
def field(attr, options={})
|
|
167
|
-
if options[:type]
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
:cast_db_to_user => NoBrainer::Document::Types::CastDBToUser.lookup(options[:type]))
|
|
91
|
+
if options[:type] == Array || options[:type] == Hash
|
|
92
|
+
# XXX For the moment, NoBrainer does not support these complex types
|
|
93
|
+
options.delete(:type)
|
|
171
94
|
end
|
|
172
95
|
super
|
|
173
96
|
end
|
|
174
97
|
end
|
|
98
|
+
|
|
99
|
+
require File.join(File.dirname(__FILE__), 'types', 'boolean')
|
|
100
|
+
Boolean = NoBrainer::Boolean
|
|
101
|
+
|
|
102
|
+
class << self
|
|
103
|
+
mattr_accessor :loaded_extensions
|
|
104
|
+
self.loaded_extensions = Set.new
|
|
105
|
+
def load_type_extensions(klass)
|
|
106
|
+
unless loaded_extensions.include?(klass)
|
|
107
|
+
begin
|
|
108
|
+
require File.join(File.dirname(__FILE__), 'types', klass.name.underscore)
|
|
109
|
+
rescue LoadError
|
|
110
|
+
end
|
|
111
|
+
loaded_extensions << klass
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
175
115
|
end
|
|
@@ -64,7 +64,9 @@ module NoBrainer::Document::Uniqueness
|
|
|
64
64
|
class UniquenessValidator < ActiveModel::EachValidator
|
|
65
65
|
attr_accessor :scope
|
|
66
66
|
|
|
67
|
-
def
|
|
67
|
+
def initialize(options={})
|
|
68
|
+
super
|
|
69
|
+
klass = options[:class]
|
|
68
70
|
self.scope = [*options[:scope]]
|
|
69
71
|
([klass] + klass.descendants).each do |_klass|
|
|
70
72
|
_klass.unique_validators << self
|
|
@@ -91,7 +93,7 @@ module NoBrainer::Document::Uniqueness
|
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
def exclude_doc(criteria, doc)
|
|
94
|
-
criteria.where(
|
|
96
|
+
criteria.where(doc.class.pk_name.ne => doc.pk_value)
|
|
95
97
|
end
|
|
96
98
|
end
|
|
97
99
|
end
|
|
@@ -6,7 +6,7 @@ module NoBrainer::Document::Validation
|
|
|
6
6
|
included do
|
|
7
7
|
# We don't want before_validation returning false to halt the chain.
|
|
8
8
|
define_callbacks :validation, :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name],
|
|
9
|
-
:terminator =>
|
|
9
|
+
:terminator => proc { false }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def valid?(context=nil)
|
|
@@ -18,6 +18,7 @@ module NoBrainer::Document::Validation
|
|
|
18
18
|
super
|
|
19
19
|
validates(attr, { :presence => options[:required] }) if options.has_key?(:required)
|
|
20
20
|
validates(attr, { :uniqueness => options[:unique] }) if options.has_key?(:unique)
|
|
21
|
+
validates(attr, { :inclusion => {:in => options[:in]} }) if options.has_key?(:in)
|
|
21
22
|
validates(attr, options[:validates]) if options[:validates]
|
|
22
23
|
end
|
|
23
24
|
end
|
data/lib/no_brainer/document.rb
CHANGED
data/lib/no_brainer/error.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
module NoBrainer::Error
|
|
2
|
-
class Connection
|
|
3
|
-
class DocumentNotFound
|
|
4
|
-
class
|
|
5
|
-
class ChildrenExist
|
|
6
|
-
class CannotUseIndex
|
|
7
|
-
class MissingIndex
|
|
8
|
-
class InvalidType
|
|
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 InvalidType < RuntimeError; end
|
|
9
9
|
class AssociationNotPersisted < RuntimeError; end
|
|
10
|
-
class ReadonlyField
|
|
10
|
+
class ReadonlyField < RuntimeError; end
|
|
11
11
|
|
|
12
12
|
class DocumentInvalid < RuntimeError
|
|
13
13
|
attr_accessor :instance
|
|
@@ -33,7 +33,7 @@ module NoBrainer::Error
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def message
|
|
36
|
-
"#{attr_name} should be used with a #{human_type_name}. Got `#{value}`"
|
|
36
|
+
"#{attr_name} should be used with a #{human_type_name}. Got `#{value}` (#{value.class})"
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -13,23 +13,29 @@ class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
|
|
|
13
13
|
return unless NoBrainer.logger.debug?
|
|
14
14
|
|
|
15
15
|
duration = Time.now - start_time
|
|
16
|
-
msg = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
msg_duration = (duration * 1000.0).round(1).to_s
|
|
18
|
+
msg_duration = " " * [0, 5 - msg_duration.size].max + msg_duration
|
|
19
|
+
msg_duration = "[#{msg_duration}ms] "
|
|
20
|
+
|
|
21
|
+
msg_db = "[#{env[:db_name]}] " if env[:db_name]
|
|
22
|
+
msg_query = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
|
|
23
|
+
msg_exception = " #{exception.class} #{exception.message.split("\n").first}" if exception
|
|
24
|
+
msg_last = nil
|
|
20
25
|
|
|
21
26
|
if NoBrainer::Config.colorize_logger
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
query_color = case NoBrainer::RQL.type_of(env[:query])
|
|
28
|
+
when :write then "\e[1;31m" # red
|
|
29
|
+
when :read then "\e[1;32m" # green
|
|
30
|
+
when :management then "\e[1;33m" # yellow
|
|
31
|
+
end
|
|
32
|
+
msg_duration = [query_color, msg_duration].join
|
|
33
|
+
msg_db = ["\e[0;34m", msg_db, query_color].join if msg_db
|
|
34
|
+
msg_exception = ["\e[0;31m", msg_exception].join if msg_exception
|
|
35
|
+
msg_last = "\e[0m"
|
|
31
36
|
end
|
|
32
37
|
|
|
38
|
+
msg = [msg_duration, msg_db, msg_query, msg_exception, msg_last].join
|
|
33
39
|
NoBrainer.logger.debug(msg)
|
|
34
40
|
end
|
|
35
41
|
end
|
|
@@ -2,9 +2,21 @@ class NoBrainer::QueryRunner::MissingIndex < NoBrainer::QueryRunner::Middleware
|
|
|
2
2
|
def call(env)
|
|
3
3
|
@runner.call(env)
|
|
4
4
|
rescue RethinkDB::RqlRuntimeError => e
|
|
5
|
-
if e.message =~ /^Index `(.+)` was not found
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
if e.message =~ /^Index `(.+)` was not found on table `(.+)\.(.+)`\.$/
|
|
6
|
+
index_name = $1
|
|
7
|
+
database_name = $2
|
|
8
|
+
table_name = $3
|
|
9
|
+
|
|
10
|
+
klass = NoBrainer::Document.all.select { |m| m.table_name == table_name }.first
|
|
11
|
+
if klass && klass.pk_name.to_s == index_name
|
|
12
|
+
err_msg = "Please run update the primary key `#{index_name}` in the table `#{database_name}.#{table_name}`."
|
|
13
|
+
else
|
|
14
|
+
err_msg = "Please run \"rake db:update_indexes\" to create the index `#{index_name}`"
|
|
15
|
+
err_msg += " in the table `#{database_name}.#{table_name}`."
|
|
16
|
+
err_msg += "\n--> Read http://nobrainer.io/docs/indexes for more information."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
raise NoBrainer::Error::MissingIndex.new(err_msg)
|
|
8
20
|
end
|
|
9
21
|
raise
|
|
10
22
|
end
|
|
@@ -4,20 +4,20 @@ class NoBrainer::QueryRunner::Reconnect < NoBrainer::QueryRunner::Middleware
|
|
|
4
4
|
rescue StandardError => e
|
|
5
5
|
# TODO test that thing
|
|
6
6
|
if is_connection_error_exception?(e)
|
|
7
|
-
retry if reconnect
|
|
7
|
+
retry if reconnect(e)
|
|
8
8
|
end
|
|
9
9
|
raise
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
private
|
|
13
13
|
|
|
14
|
-
def reconnect
|
|
14
|
+
def reconnect(e)
|
|
15
15
|
# FIXME thread safety? perhaps we need to use a connection pool
|
|
16
16
|
# XXX Possibly dangerous, as we could reexecute a non idempotent operation
|
|
17
17
|
# Check the semantics of the db
|
|
18
18
|
NoBrainer::Config.max_reconnection_tries.times do
|
|
19
19
|
begin
|
|
20
|
-
|
|
20
|
+
warn_reconnect(e)
|
|
21
21
|
sleep 1
|
|
22
22
|
NoBrainer.connection.reconnect(:noreply_wait => false)
|
|
23
23
|
return true
|
|
@@ -35,10 +35,21 @@ class NoBrainer::QueryRunner::Reconnect < NoBrainer::QueryRunner::Middleware
|
|
|
35
35
|
Errno::ECONNRESET, Errno::ETIMEDOUT, IOError
|
|
36
36
|
true
|
|
37
37
|
when RethinkDB::RqlRuntimeError
|
|
38
|
-
e.message =~ /
|
|
38
|
+
e.message =~ /No master available/ ||
|
|
39
|
+
e.message =~ /Master .* not available/ ||
|
|
39
40
|
e.message =~ /Error: Connection Closed/
|
|
40
41
|
else
|
|
41
42
|
false
|
|
42
43
|
end
|
|
43
44
|
end
|
|
45
|
+
|
|
46
|
+
def warn_reconnect(e)
|
|
47
|
+
if e.is_a?(RethinkDB::RqlRuntimeError)
|
|
48
|
+
e_msg = e.message.split("\n").first
|
|
49
|
+
msg = "Server #{NoBrainer::Config.rethinkdb_url} not ready - #{e_msg}, retrying..."
|
|
50
|
+
else
|
|
51
|
+
msg = "Connection issue with #{NoBrainer::Config.rethinkdb_url} - #{e}, retrying..."
|
|
52
|
+
end
|
|
53
|
+
NoBrainer.logger.try(:warn, msg)
|
|
54
|
+
end
|
|
44
55
|
end
|