couchbase-orm 1.1.0 → 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 +5 -5
- data/.github/workflows/test.yml +45 -0
- data/.gitignore +2 -0
- data/.travis.yml +5 -4
- 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 +67 -50
- 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 +23 -8
- 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 +55 -8
- 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 +34 -13
- 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
|
|
@@ -98,18 +102,29 @@ module CouchbaseOrm
|
|
98
102
|
# new one. the id of the current record is used as the key's value.
|
99
103
|
after_save do |record|
|
100
104
|
original_key = instance_variable_get(original_bucket_key_var)
|
101
|
-
record.class.bucket.delete(original_key, quiet: true) if original_key
|
102
105
|
|
103
|
-
|
104
|
-
|
106
|
+
if original_key
|
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)
|
112
|
+
end
|
113
|
+
end
|
105
114
|
end
|
115
|
+
|
116
|
+
record.class.collection.upsert(record.send(bucket_key_method), record.id)
|
117
|
+
|
106
118
|
instance_variable_set(original_bucket_key_var, nil)
|
107
119
|
end
|
108
120
|
|
109
121
|
# cleanup by removing the bucket key before the record is deleted
|
110
122
|
# TODO: handle unpersisted, modified component values
|
111
123
|
before_destroy do |record|
|
112
|
-
record.class.
|
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)
|
127
|
+
end
|
113
128
|
true
|
114
129
|
end
|
115
130
|
|
@@ -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
|