couchbase-orm 1.1.1 → 2.0.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/.github/workflows/test.yml +45 -0
- data/.gitignore +2 -0
- data/.travis.yml +3 -2
- data/CODEOWNERS +1 -0
- data/Gemfile +5 -3
- data/README.md +237 -31
- data/ci/run_couchbase.sh +22 -0
- data/couchbase-orm.gemspec +26 -20
- data/lib/couchbase-orm/active_record_compat.rb +92 -0
- data/lib/couchbase-orm/associations.rb +119 -0
- data/lib/couchbase-orm/base.rb +143 -166
- data/lib/couchbase-orm/changeable.rb +512 -0
- data/lib/couchbase-orm/connection.rb +28 -8
- data/lib/couchbase-orm/encrypt.rb +48 -0
- data/lib/couchbase-orm/error.rb +17 -2
- data/lib/couchbase-orm/inspectable.rb +37 -0
- data/lib/couchbase-orm/json_schema/json_validation_error.rb +13 -0
- data/lib/couchbase-orm/json_schema/loader.rb +47 -0
- data/lib/couchbase-orm/json_schema/validation.rb +18 -0
- data/lib/couchbase-orm/json_schema/validator.rb +45 -0
- data/lib/couchbase-orm/json_schema.rb +9 -0
- data/lib/couchbase-orm/json_transcoder.rb +27 -0
- data/lib/couchbase-orm/locale/en.yml +5 -0
- data/lib/couchbase-orm/n1ql.rb +133 -0
- data/lib/couchbase-orm/persistence.rb +61 -52
- data/lib/couchbase-orm/proxies/bucket_proxy.rb +36 -0
- data/lib/couchbase-orm/proxies/collection_proxy.rb +52 -0
- data/lib/couchbase-orm/proxies/n1ql_proxy.rb +40 -0
- data/lib/couchbase-orm/proxies/results_proxy.rb +23 -0
- data/lib/couchbase-orm/railtie.rb +6 -17
- data/lib/couchbase-orm/relation.rb +249 -0
- data/lib/couchbase-orm/strict_loading.rb +21 -0
- data/lib/couchbase-orm/timestamps/created.rb +20 -0
- data/lib/couchbase-orm/timestamps/updated.rb +21 -0
- data/lib/couchbase-orm/timestamps.rb +15 -0
- data/lib/couchbase-orm/types/array.rb +32 -0
- data/lib/couchbase-orm/types/date.rb +9 -0
- data/lib/couchbase-orm/types/date_time.rb +14 -0
- data/lib/couchbase-orm/types/encrypted.rb +17 -0
- data/lib/couchbase-orm/types/nested.rb +43 -0
- data/lib/couchbase-orm/types/timestamp.rb +18 -0
- data/lib/couchbase-orm/types.rb +20 -0
- data/lib/couchbase-orm/utilities/enum.rb +13 -1
- data/lib/couchbase-orm/utilities/has_many.rb +72 -36
- data/lib/couchbase-orm/utilities/ignored_properties.rb +15 -0
- data/lib/couchbase-orm/utilities/index.rb +18 -20
- data/lib/couchbase-orm/utilities/properties_always_exists_in_document.rb +16 -0
- data/lib/couchbase-orm/utilities/query_helper.rb +148 -0
- data/lib/couchbase-orm/utils.rb +25 -0
- data/lib/couchbase-orm/version.rb +1 -1
- data/lib/couchbase-orm/views.rb +38 -41
- data/lib/couchbase-orm.rb +44 -9
- data/lib/ext/query_n1ql.rb +124 -0
- data/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +3 -2
- data/spec/associations_spec.rb +219 -50
- data/spec/base_spec.rb +296 -14
- data/spec/collection_proxy_spec.rb +29 -0
- data/spec/connection_spec.rb +27 -0
- data/spec/couchbase-orm/active_record_compat_spec.rb +24 -0
- data/spec/couchbase-orm/changeable_spec.rb +16 -0
- data/spec/couchbase-orm/json_schema/validation_spec.rb +23 -0
- data/spec/couchbase-orm/json_schema/validator_spec.rb +13 -0
- data/spec/couchbase-orm/timestamps_spec.rb +85 -0
- data/spec/couchbase-orm/timestamps_spec_models.rb +36 -0
- data/spec/empty-json-schema/.gitkeep +0 -0
- data/spec/enum_spec.rb +34 -0
- data/spec/has_many_spec.rb +101 -54
- data/spec/index_spec.rb +13 -9
- data/spec/json-schema/JsonSchemaBaseTest.json +19 -0
- data/spec/json-schema/entity_snakecase.json +20 -0
- data/spec/json-schema/loader_spec.rb +42 -0
- data/spec/json-schema/specific_path.json +20 -0
- data/spec/json_schema_spec.rb +178 -0
- data/spec/n1ql_spec.rb +193 -0
- data/spec/persistence_spec.rb +49 -9
- data/spec/relation_nested_spec.rb +88 -0
- data/spec/relation_spec.rb +430 -0
- data/spec/support.rb +16 -8
- data/spec/type_array_spec.rb +52 -0
- data/spec/type_encrypted_spec.rb +114 -0
- data/spec/type_nested_spec.rb +191 -0
- data/spec/type_spec.rb +317 -0
- data/spec/utilities/ignored_properties_spec.rb +20 -0
- data/spec/utilities/properties_always_exists_in_document_spec.rb +24 -0
- data/spec/views_spec.rb +32 -11
- metadata +192 -29
@@ -1,12 +1,9 @@
|
|
1
1
|
module CouchbaseOrm
|
2
2
|
module HasMany
|
3
|
-
private
|
4
|
-
|
5
3
|
# :foreign_key, :class_name, :through
|
6
|
-
def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_class: nil, through_key: nil, **options)
|
7
|
-
class_name
|
4
|
+
def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_class: nil, through_key: nil, type: :view, **options)
|
5
|
+
class_name = (class_name || model.to_s.singularize.camelcase).to_s
|
8
6
|
foreign_key = (foreign_key || ActiveSupport::Inflector.foreign_key(self.name)).to_sym
|
9
|
-
|
10
7
|
if through || through_class
|
11
8
|
remote_class = class_name
|
12
9
|
class_name = (through_class || through.to_s.camelcase).to_s
|
@@ -16,58 +13,97 @@ module CouchbaseOrm
|
|
16
13
|
remote_method = :"find_by_#{foreign_key}"
|
17
14
|
end
|
18
15
|
|
19
|
-
|
16
|
+
instance_var = "@__assoc_#{model}"
|
20
17
|
|
21
18
|
klass = begin
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
class_name.constantize
|
20
|
+
rescue NameError
|
21
|
+
warn "WARNING: #{class_name} referenced in #{self.name} before it was aded"
|
25
22
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
# Open the class early - load order will have to be changed to prevent this.
|
24
|
+
# Warning notice required as a misspelling will not raise an error
|
25
|
+
Object.class_eval <<-EKLASS
|
26
|
+
class #{class_name} < CouchbaseOrm::Base
|
27
|
+
attribute :#{foreign_key}
|
28
|
+
end
|
29
|
+
EKLASS
|
30
|
+
class_name.constantize
|
31
31
|
end
|
32
|
-
EKLASS
|
33
|
-
class_name.constantize
|
34
|
-
end
|
35
32
|
|
33
|
+
build_index(type, klass, remote_class, remote_method, through_key, foreign_key)
|
36
34
|
|
37
35
|
if remote_class
|
38
|
-
klass.class_eval do
|
39
|
-
view remote_method, map: <<-EMAP
|
40
|
-
function(doc) {
|
41
|
-
if (doc.type === "{{design_document}}" && doc.#{through_key}) {
|
42
|
-
emit(doc.#{foreign_key}, null);
|
43
|
-
}
|
44
|
-
}
|
45
|
-
EMAP
|
46
|
-
end
|
47
|
-
|
48
36
|
define_method(model) do
|
49
|
-
return self.instance_variable_get(
|
37
|
+
return self.instance_variable_get(instance_var) if instance_variable_defined?(instance_var)
|
50
38
|
|
51
39
|
remote_klass = remote_class.constantize
|
40
|
+
raise ArgumentError, "Can't find #{remote_method} without an id" unless self.id.present?
|
52
41
|
enum = klass.__send__(remote_method, key: self.id) { |row|
|
53
|
-
|
42
|
+
case type
|
43
|
+
when :n1ql
|
44
|
+
remote_klass.find(row)
|
45
|
+
when :view
|
46
|
+
remote_klass.find(row[through_key])
|
47
|
+
else
|
48
|
+
raise 'type is unknown'
|
49
|
+
end
|
54
50
|
}
|
55
51
|
|
56
|
-
self.instance_variable_set(
|
52
|
+
self.instance_variable_set(instance_var, enum)
|
57
53
|
end
|
58
54
|
else
|
59
|
-
klass.class_eval do
|
60
|
-
index_view foreign_key, validate: false
|
61
|
-
end
|
62
|
-
|
63
55
|
define_method(model) do
|
64
|
-
return self.instance_variable_get(
|
65
|
-
self.instance_variable_set(
|
56
|
+
return self.instance_variable_get(instance_var) if instance_variable_defined?(instance_var)
|
57
|
+
self.instance_variable_set(instance_var, self.id ? klass.__send__(remote_method, self.id) : [])
|
66
58
|
end
|
67
59
|
end
|
68
60
|
|
69
61
|
@associations ||= []
|
70
62
|
@associations << [model, options[:dependent]]
|
71
63
|
end
|
64
|
+
|
65
|
+
def build_index(type, klass, remote_class, remote_method, through_key, foreign_key)
|
66
|
+
case type
|
67
|
+
when :n1ql
|
68
|
+
build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key)
|
69
|
+
when :view
|
70
|
+
build_index_view(klass, remote_class, remote_method, through_key, foreign_key)
|
71
|
+
else
|
72
|
+
raise 'type is unknown'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_index_view(klass, remote_class, remote_method, through_key, foreign_key)
|
77
|
+
if remote_class
|
78
|
+
klass.class_eval do
|
79
|
+
view remote_method, map: <<-EMAP
|
80
|
+
function(doc) {
|
81
|
+
if (doc.type === "{{design_document}}" && doc.#{through_key}) {
|
82
|
+
emit(doc.#{foreign_key}, null);
|
83
|
+
}
|
84
|
+
}
|
85
|
+
EMAP
|
86
|
+
end
|
87
|
+
else
|
88
|
+
klass.class_eval do
|
89
|
+
index_view foreign_key, validate: false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key)
|
95
|
+
if remote_class
|
96
|
+
klass.class_eval do
|
97
|
+
n1ql remote_method, emit_key: 'id', query_fn: proc { |bucket, values, options|
|
98
|
+
raise ArgumentError, "values[0] must not be blank" if values[0].blank?
|
99
|
+
cluster.query("SELECT raw #{through_key} FROM `#{bucket.name}` where type = \"#{design_document}\" and #{foreign_key} = #{quote(values[0])}", options)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
else
|
103
|
+
klass.class_eval do
|
104
|
+
index_n1ql foreign_key, validate: false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
72
108
|
end
|
73
109
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module CouchbaseOrm
|
2
|
+
module IgnoredProperties
|
3
|
+
def ignored_properties=(properties)
|
4
|
+
@ignored_properties = properties.map(&:to_s)
|
5
|
+
end
|
6
|
+
|
7
|
+
def ignored_properties(*args)
|
8
|
+
if args.any?
|
9
|
+
CouchbaseOrm.logger.warn('Passing aruments to `.ignored_properties` is deprecated. PLease use `.ignored_properties=` instead.')
|
10
|
+
return send :ignored_properties=, args
|
11
|
+
end
|
12
|
+
@ignored_properties ||= []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -31,7 +31,7 @@ module CouchbaseOrm
|
|
31
31
|
|
32
32
|
# collect a list of values for each key component attribute
|
33
33
|
define_method(bucket_key_vals_method) do
|
34
|
-
attrs.collect {|attr| self[attr]}
|
34
|
+
attrs.collect {|attr| self.class.attribute_types[attr.to_s].cast(self[attr])}
|
35
35
|
end
|
36
36
|
|
37
37
|
|
@@ -40,6 +40,7 @@ module CouchbaseOrm
|
|
40
40
|
#----------------
|
41
41
|
# simple wrapper around the processor proc if supplied
|
42
42
|
define_singleton_method(processor_method) do |*values|
|
43
|
+
values = attrs.zip(values).map { |attr,value| attribute_types[attr.to_s].serialize(attribute_types[attr.to_s].cast(value)) }
|
43
44
|
if processor
|
44
45
|
processor.call(values.length == 1 ? values.first : values)
|
45
46
|
else
|
@@ -50,13 +51,16 @@ module CouchbaseOrm
|
|
50
51
|
# use the bucket key as an index - lookup records by attr values
|
51
52
|
define_singleton_method(find_by_method) do |*values|
|
52
53
|
key = self.send(class_bucket_key_method, *values)
|
53
|
-
|
54
|
+
CouchbaseOrm.logger.debug { "#{find_by_method}: #{class_bucket_key_method} with values #{values.inspect} give key: #{key}" }
|
55
|
+
id = self.collection.get(key)&.content
|
54
56
|
if id
|
55
57
|
mod = self.find_by_id(id)
|
56
58
|
return mod if mod
|
57
59
|
|
58
60
|
# Clean up record if the id doesn't exist
|
59
|
-
self.
|
61
|
+
self.collection.remove(key)
|
62
|
+
else
|
63
|
+
CouchbaseOrm.logger.debug("#{find_by_method}: #{key} not found")
|
60
64
|
end
|
61
65
|
|
62
66
|
nil
|
@@ -70,7 +74,7 @@ module CouchbaseOrm
|
|
70
74
|
if presence
|
71
75
|
attrs.each do |attr|
|
72
76
|
validates attr, presence: true
|
73
|
-
|
77
|
+
attribute attr
|
74
78
|
end
|
75
79
|
end
|
76
80
|
|
@@ -100,32 +104,26 @@ module CouchbaseOrm
|
|
100
104
|
original_key = instance_variable_get(original_bucket_key_var)
|
101
105
|
|
102
106
|
if original_key
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
# Errors here can be ignored. Just means the key was updated elswhere
|
107
|
+
begin
|
108
|
+
check_ref_id = record.class.collection.get(original_key)
|
109
|
+
if check_ref_id && check_ref_id.content == record.id
|
110
|
+
CouchbaseOrm.logger.debug { "Removing old key #{original_key}" }
|
111
|
+
record.class.collection.remove(original_key, cas: check_ref_id.cas)
|
109
112
|
end
|
110
113
|
end
|
111
114
|
end
|
112
115
|
|
113
|
-
|
114
|
-
|
115
|
-
end
|
116
|
+
record.class.collection.upsert(record.send(bucket_key_method), record.id)
|
117
|
+
|
116
118
|
instance_variable_set(original_bucket_key_var, nil)
|
117
119
|
end
|
118
120
|
|
119
121
|
# cleanup by removing the bucket key before the record is deleted
|
120
122
|
# TODO: handle unpersisted, modified component values
|
121
123
|
before_destroy do |record|
|
122
|
-
check_ref_id = record.class.
|
123
|
-
if check_ref_id && check_ref_id.
|
124
|
-
|
125
|
-
record.class.bucket.delete(record.send(bucket_key_method), cas: check_ref_id.cas)
|
126
|
-
rescue ::Libcouchbase::Error::KeyExists
|
127
|
-
# Errors here can be ignored. Just means the key was updated elswhere
|
128
|
-
end
|
124
|
+
check_ref_id = record.class.collection.get(record.send(bucket_key_method))
|
125
|
+
if check_ref_id && check_ref_id.content == record.id
|
126
|
+
record.class.collection.remove(record.send(bucket_key_method), cas: check_ref_id.cas)
|
129
127
|
end
|
130
128
|
true
|
131
129
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CouchbaseOrm
|
2
|
+
module PropertiesAlwaysExistsInDocument
|
3
|
+
|
4
|
+
DEFAULT_VALUE = false
|
5
|
+
def properties_always_exists_in_document=(value)
|
6
|
+
unless [true, false].include? value
|
7
|
+
raise ArgumentError.new("properties_always_exists_in_document must be a boolean")
|
8
|
+
end
|
9
|
+
@properties_always_exists_in_document = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def properties_always_exists_in_document
|
13
|
+
@properties_always_exists_in_document ||= DEFAULT_VALUE
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module CouchbaseOrm
|
2
|
+
module QueryHelper
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
def build_match(key, value)
|
8
|
+
use_is_null = self.properties_always_exists_in_document
|
9
|
+
key = "meta().id" if key.to_s == "id"
|
10
|
+
case
|
11
|
+
when value.nil? && use_is_null
|
12
|
+
"#{key} IS NULL"
|
13
|
+
when value.nil? && !use_is_null
|
14
|
+
"#{key} IS NOT VALUED"
|
15
|
+
when value.is_a?(Hash) && attribute_types[key.to_s].is_a?(CouchbaseOrm::Types::Array)
|
16
|
+
"any #{key.to_s.singularize} in #{key} satisfies (#{build_match_hash("#{key.to_s.singularize}", value)}) end"
|
17
|
+
when value.is_a?(Hash) && !attribute_types[key.to_s].is_a?(CouchbaseOrm::Types::Array)
|
18
|
+
build_match_hash(key, value)
|
19
|
+
when value.is_a?(Array) && value.include?(nil)
|
20
|
+
"(#{build_match(key, nil)} OR #{build_match(key, value.compact)})"
|
21
|
+
when value.is_a?(Array)
|
22
|
+
"#{key} IN #{quote(value)}"
|
23
|
+
when value.is_a?(Range)
|
24
|
+
build_match_range(key, value)
|
25
|
+
else
|
26
|
+
"#{key} = #{quote(value)}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_match_hash(key, value)
|
31
|
+
matches = []
|
32
|
+
value.each do |k, v|
|
33
|
+
case k
|
34
|
+
when :_gt
|
35
|
+
matches << "#{key} > #{quote(v)}"
|
36
|
+
when :_gte
|
37
|
+
matches << "#{key} >= #{quote(v)}"
|
38
|
+
when :_lt
|
39
|
+
matches << "#{key} < #{quote(v)}"
|
40
|
+
when :_lte
|
41
|
+
matches << "#{key} <= #{quote(v)}"
|
42
|
+
when :_ne
|
43
|
+
matches << "#{key} != #{quote(v)}"
|
44
|
+
|
45
|
+
# TODO v2
|
46
|
+
# when :_in
|
47
|
+
# matches << "#{key} IN #{quote(v)}"
|
48
|
+
# when :_nin
|
49
|
+
# matches << "#{key} NOT IN #{quote(v)}"
|
50
|
+
# when :_like
|
51
|
+
# matches << "#{key} LIKE #{quote(v)}"
|
52
|
+
# when :_nlike
|
53
|
+
# matches << "#{key} NOT LIKE #{quote(v)}"
|
54
|
+
# when :_between
|
55
|
+
# matches << "#{key} BETWEEN #{quote(v[0])} AND #{quote(v[1])}"
|
56
|
+
# when :_nbetween
|
57
|
+
# matches << "#{key} NOT BETWEEN #{quote(v[0])} AND #{quote(v[1])}"
|
58
|
+
# when :_exists
|
59
|
+
# matches << "#{key} IS #{v ? "" : "NOT "}VALUED"
|
60
|
+
# when :_regex
|
61
|
+
# matches << "#{key} REGEXP #{quote(v)}"
|
62
|
+
# when :_nregex
|
63
|
+
# matches << "#{key} NOT REGEXP #{quote(v)}"
|
64
|
+
# when :_match
|
65
|
+
# matches << "#{key} MATCH #{quote(v)}"
|
66
|
+
# when :_nmatch
|
67
|
+
# matches << "#{key} NOT MATCH #{quote(v)}"
|
68
|
+
|
69
|
+
# TODO v3
|
70
|
+
# when :_any
|
71
|
+
# matches << "#{key} ANY #{quote(v)}"
|
72
|
+
# when :_nany
|
73
|
+
# matches << "#{key} NOT ANY #{quote(v)}"
|
74
|
+
# when :_all
|
75
|
+
# matches << "#{key} ALL #{quote(v)}"
|
76
|
+
# when :_nall
|
77
|
+
# matches << "#{key} NOT ALL #{quote(v)}"
|
78
|
+
# when :_within
|
79
|
+
# matches << "#{key} WITHIN #{quote(v)}"
|
80
|
+
#when :_nwithin
|
81
|
+
# matches << "#{key} NOT WITHIN #{quote(v)}"
|
82
|
+
else
|
83
|
+
matches << build_match("#{key}.#{k}", v)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
matches.join(" AND ")
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_match_range(key, value)
|
91
|
+
matches = []
|
92
|
+
matches << "#{key} >= #{quote(value.begin)}"
|
93
|
+
if value.exclude_end?
|
94
|
+
matches << "#{key} < #{quote(value.end)}"
|
95
|
+
else
|
96
|
+
matches << "#{key} <= #{quote(value.end)}"
|
97
|
+
end
|
98
|
+
matches.join(" AND ")
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
def build_not_match(key, value)
|
103
|
+
use_is_null = self.properties_always_exists_in_document
|
104
|
+
key = "meta().id" if key.to_s == "id"
|
105
|
+
case
|
106
|
+
when value.nil? && use_is_null
|
107
|
+
"#{key} IS NOT NULL"
|
108
|
+
when value.nil? && !use_is_null
|
109
|
+
"#{key} IS VALUED"
|
110
|
+
when value.is_a?(Array) && value.include?(nil)
|
111
|
+
"(#{build_not_match(key, nil)} AND #{build_not_match(key, value.compact)})"
|
112
|
+
when value.is_a?(Array)
|
113
|
+
"#{key} NOT IN #{quote(value)}"
|
114
|
+
else
|
115
|
+
"#{key} != #{quote(value)}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def serialize_value(key, value_before_type_cast)
|
120
|
+
value =
|
121
|
+
if value_before_type_cast.is_a?(Array)
|
122
|
+
value_before_type_cast.map do |v|
|
123
|
+
attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(v))
|
124
|
+
end
|
125
|
+
else
|
126
|
+
attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(value_before_type_cast))
|
127
|
+
end
|
128
|
+
CouchbaseOrm.logger.debug { "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value.inspect} #{value.class} #{attribute_types[key.to_s]}" }
|
129
|
+
value
|
130
|
+
end
|
131
|
+
|
132
|
+
def quote(value)
|
133
|
+
if [String, Date].any? { |clazz| value.is_a?(clazz) }
|
134
|
+
"'#{N1ql.sanitize(value)}'"
|
135
|
+
elsif [DateTime, Time].any? { |clazz| value.is_a?(clazz) }
|
136
|
+
formatedDate = value&.iso8601(@precision)
|
137
|
+
"'#{N1ql.sanitize(formatedDate)}'"
|
138
|
+
elsif value.is_a? Array
|
139
|
+
"[#{value.map{|v|quote(v)}.join(', ')}]"
|
140
|
+
elsif value.nil?
|
141
|
+
nil
|
142
|
+
else
|
143
|
+
N1ql.sanitize(value).to_s
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CouchbaseOrm
|
4
|
+
# Utility functions for CouchbaseOrm.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
module Utils
|
8
|
+
extend self
|
9
|
+
|
10
|
+
# A unique placeholder value that will never accidentally collide with
|
11
|
+
# valid values. This is useful as a default keyword argument value when
|
12
|
+
# you want the argument to be optional, but you also want to be able to
|
13
|
+
# recognize that the caller did not provide a value for it.
|
14
|
+
PLACEHOLDER = Object.new.freeze
|
15
|
+
|
16
|
+
# Asks if the given value is a placeholder or not.
|
17
|
+
#
|
18
|
+
# @param [ Object ] value the value to compare
|
19
|
+
#
|
20
|
+
# @return [ true | false ] if the value is a placeholder or not.
|
21
|
+
def placeholder?(value)
|
22
|
+
value == PLACEHOLDER
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/couchbase-orm/views.rb
CHANGED
@@ -19,16 +19,18 @@ module CouchbaseOrm
|
|
19
19
|
# view :by_rating, emit_key: :rating
|
20
20
|
# end
|
21
21
|
#
|
22
|
-
# Post.by_rating
|
22
|
+
# Post.by_rating do |response|
|
23
23
|
# # ...
|
24
24
|
# end
|
25
25
|
def view(name, map: nil, emit_key: nil, reduce: nil, **options)
|
26
|
+
raise ArgumentError, "#{self} already respond_to? #{name}" if self.respond_to?(name)
|
27
|
+
|
26
28
|
if emit_key.class == Array
|
27
29
|
emit_key.each do |key|
|
28
|
-
raise "unknown emit_key attribute for view :#{name}, emit_key: :#{key}" if key &&
|
30
|
+
raise "unknown emit_key attribute for view :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s)
|
29
31
|
end
|
30
32
|
else
|
31
|
-
raise "unknown emit_key attribute for view :#{name}, emit_key: :#{emit_key}" if emit_key &&
|
33
|
+
raise "unknown emit_key attribute for view :#{name}, emit_key: :#{emit_key}" if emit_key && !attribute_names.include?(emit_key.to_s)
|
32
34
|
end
|
33
35
|
|
34
36
|
options = ViewDefaults.merge(options)
|
@@ -48,27 +50,13 @@ function(doc) {
|
|
48
50
|
EMAP
|
49
51
|
else
|
50
52
|
emit_key = emit_key || :created_at
|
51
|
-
|
52
|
-
if emit_key != :created_at && self.attributes[emit_key][:type].to_s == 'Array'
|
53
|
-
method_opts[:map] = <<-EMAP
|
54
|
-
function(doc) {
|
55
|
-
var i;
|
56
|
-
if (doc.type === "{{design_document}}") {
|
57
|
-
for (i = 0; i < doc.#{emit_key}.length; i += 1) {
|
58
|
-
emit(doc.#{emit_key}[i], null);
|
59
|
-
}
|
60
|
-
}
|
61
|
-
}
|
62
|
-
EMAP
|
63
|
-
else
|
64
|
-
method_opts[:map] = <<-EMAP
|
53
|
+
method_opts[:map] = <<-EMAP
|
65
54
|
function(doc) {
|
66
55
|
if (doc.type === "{{design_document}}") {
|
67
56
|
emit(doc.#{emit_key}, null);
|
68
57
|
}
|
69
58
|
}
|
70
59
|
EMAP
|
71
|
-
end
|
72
60
|
end
|
73
61
|
end
|
74
62
|
|
@@ -78,17 +66,14 @@ EMAP
|
|
78
66
|
@views[name] = method_opts
|
79
67
|
|
80
68
|
singleton_class.__send__(:define_method, name) do |**opts, &result_modifier|
|
81
|
-
opts = options.merge(opts)
|
82
|
-
|
69
|
+
opts = options.merge(opts).reverse_merge(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency])
|
70
|
+
CouchbaseOrm.logger.debug("View [#{@design_document}, #{name.inspect}] options: #{opts.inspect}")
|
83
71
|
if result_modifier
|
84
|
-
opts
|
85
|
-
bucket.view(@design_document, name, **opts, &result_modifier)
|
72
|
+
include_docs(bucket.view_query(@design_document, name.to_s, Couchbase::Options::View.new(**opts.except(:include_docs)))).map(&result_modifier)
|
86
73
|
elsif opts[:include_docs]
|
87
|
-
bucket.
|
88
|
-
self.new(row)
|
89
|
-
}
|
74
|
+
include_docs(bucket.view_query(@design_document, name.to_s, Couchbase::Options::View.new(**opts.except(:include_docs))))
|
90
75
|
else
|
91
|
-
bucket.
|
76
|
+
bucket.view_query(@design_document, name.to_s, Couchbase::Options::View.new(**opts.except(:include_docs)))
|
92
77
|
end
|
93
78
|
end
|
94
79
|
end
|
@@ -116,26 +101,28 @@ EMAP
|
|
116
101
|
update_required = false
|
117
102
|
|
118
103
|
# Grab the existing view details
|
119
|
-
|
120
|
-
|
121
|
-
|
104
|
+
begin
|
105
|
+
ddoc = bucket.view_indexes.get_design_document(@design_document, :production)
|
106
|
+
rescue Couchbase::Error::DesignDocumentNotFound
|
107
|
+
end
|
108
|
+
existing = ddoc.views if ddoc
|
122
109
|
views_actual = {}
|
123
110
|
# Fill in the design documents
|
124
111
|
@views.each do |name, document|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
112
|
+
views_actual[name.to_s] = Couchbase::Management::View.new(
|
113
|
+
document[:map]&.gsub('{{design_document}}', @design_document),
|
114
|
+
document[:reduce]&.gsub('{{design_document}}', @design_document)
|
115
|
+
)
|
129
116
|
end
|
130
117
|
|
131
118
|
# Check there are no changes we need to apply
|
132
119
|
views_actual.each do |name, desired|
|
133
120
|
check = existing[name]
|
134
121
|
if check
|
135
|
-
cmap = (check
|
136
|
-
creduce = (check
|
137
|
-
dmap = (desired
|
138
|
-
dreduce = (desired
|
122
|
+
cmap = (check.map || '').gsub(/\s+/, '')
|
123
|
+
creduce = (check.reduce || '').gsub(/\s+/, '')
|
124
|
+
dmap = (desired.map || '').gsub(/\s+/, '')
|
125
|
+
dreduce = (desired.reduce || '').gsub(/\s+/, '')
|
139
126
|
|
140
127
|
unless cmap == dmap && creduce == dreduce
|
141
128
|
update_required = true
|
@@ -149,16 +136,26 @@ EMAP
|
|
149
136
|
|
150
137
|
# Updated the design document
|
151
138
|
if update_required
|
152
|
-
|
153
|
-
|
154
|
-
|
139
|
+
document = Couchbase::Management::DesignDocument.new
|
140
|
+
document.views = views_actual
|
141
|
+
document.name = @design_document
|
142
|
+
bucket.view_indexes.upsert_design_document(document, :production)
|
155
143
|
|
156
|
-
puts "Couchbase views updated for #{self.name}, design doc: #{@design_document}"
|
157
144
|
true
|
158
145
|
else
|
159
146
|
false
|
160
147
|
end
|
161
148
|
end
|
149
|
+
|
150
|
+
def include_docs(view_result)
|
151
|
+
if view_result.rows.length > 1
|
152
|
+
self.find(view_result.rows.map(&:id))
|
153
|
+
elsif view_result.rows.length == 1
|
154
|
+
[self.find(view_result.rows.first.id)]
|
155
|
+
else
|
156
|
+
[]
|
157
|
+
end
|
158
|
+
end
|
162
159
|
end
|
163
160
|
end
|
164
161
|
end
|
data/lib/couchbase-orm.rb
CHANGED
@@ -1,23 +1,58 @@
|
|
1
1
|
# frozen_string_literal: true, encoding: ASCII-8BIT
|
2
|
+
require "logger"
|
3
|
+
require "active_support/lazy_load_hooks"
|
4
|
+
require "couchbase-orm/utils"
|
2
5
|
|
3
|
-
|
6
|
+
ActiveSupport.on_load(:i18n) do
|
7
|
+
I18n.load_path << File.expand_path("couchbase-orm/locale/en.yml", __dir__)
|
8
|
+
end
|
4
9
|
|
5
10
|
module CouchbaseOrm
|
11
|
+
autoload :Encrypt, 'couchbase-orm/encrypt'
|
6
12
|
autoload :Error, 'couchbase-orm/error'
|
7
13
|
autoload :Connection, 'couchbase-orm/connection'
|
8
14
|
autoload :IdGenerator, 'couchbase-orm/id_generator'
|
9
15
|
autoload :Base, 'couchbase-orm/base'
|
16
|
+
autoload :Document, 'couchbase-orm/base'
|
17
|
+
autoload :NestedDocument, 'couchbase-orm/base'
|
18
|
+
autoload :HasMany, 'couchbase-orm/utilities/has_many'
|
19
|
+
|
20
|
+
def self.logger
|
21
|
+
@@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO unless ENV["COUCHBASE_ORM_DEBUG"] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.logger=(logger)
|
25
|
+
@@logger = logger
|
26
|
+
end
|
10
27
|
|
11
28
|
def self.try_load(id)
|
12
29
|
result = nil
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
30
|
+
was_array = id.is_a?(Array)
|
31
|
+
if was_array && id.length == 1
|
32
|
+
query_id = id.first
|
33
|
+
else
|
34
|
+
query_id = id
|
35
|
+
end
|
36
|
+
|
37
|
+
result = query_id.is_a?(Array) ? CouchbaseOrm::Base.bucket.default_collection.get_multi(query_id) : CouchbaseOrm::Base.bucket.default_collection.get(query_id)
|
38
|
+
|
39
|
+
result = Array.wrap(result) if was_array
|
40
|
+
|
41
|
+
if result&.is_a?(Array)
|
42
|
+
return result.zip(id).map { |r, id| try_load_create_model(r, id) }.compact
|
43
|
+
end
|
44
|
+
|
45
|
+
return try_load_create_model(result, id)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def self.try_load_create_model(result, id)
|
51
|
+
ddoc = result&.content["type"]
|
52
|
+
return nil unless ddoc
|
53
|
+
::CouchbaseOrm::Base.descendants.each do |model|
|
54
|
+
if model.design_document == ddoc
|
55
|
+
return model.new(result, id: id)
|
21
56
|
end
|
22
57
|
end
|
23
58
|
nil
|