couchbase-orm 1.1.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +45 -0
  3. data/.gitignore +2 -0
  4. data/.travis.yml +3 -2
  5. data/CODEOWNERS +1 -0
  6. data/Gemfile +5 -3
  7. data/README.md +237 -31
  8. data/ci/run_couchbase.sh +22 -0
  9. data/couchbase-orm.gemspec +26 -20
  10. data/lib/couchbase-orm/active_record_compat.rb +92 -0
  11. data/lib/couchbase-orm/associations.rb +119 -0
  12. data/lib/couchbase-orm/base.rb +143 -166
  13. data/lib/couchbase-orm/changeable.rb +512 -0
  14. data/lib/couchbase-orm/connection.rb +28 -8
  15. data/lib/couchbase-orm/encrypt.rb +48 -0
  16. data/lib/couchbase-orm/error.rb +17 -2
  17. data/lib/couchbase-orm/inspectable.rb +37 -0
  18. data/lib/couchbase-orm/json_schema/json_validation_error.rb +13 -0
  19. data/lib/couchbase-orm/json_schema/loader.rb +47 -0
  20. data/lib/couchbase-orm/json_schema/validation.rb +18 -0
  21. data/lib/couchbase-orm/json_schema/validator.rb +45 -0
  22. data/lib/couchbase-orm/json_schema.rb +9 -0
  23. data/lib/couchbase-orm/json_transcoder.rb +27 -0
  24. data/lib/couchbase-orm/locale/en.yml +5 -0
  25. data/lib/couchbase-orm/n1ql.rb +133 -0
  26. data/lib/couchbase-orm/persistence.rb +61 -52
  27. data/lib/couchbase-orm/proxies/bucket_proxy.rb +36 -0
  28. data/lib/couchbase-orm/proxies/collection_proxy.rb +52 -0
  29. data/lib/couchbase-orm/proxies/n1ql_proxy.rb +40 -0
  30. data/lib/couchbase-orm/proxies/results_proxy.rb +23 -0
  31. data/lib/couchbase-orm/railtie.rb +6 -17
  32. data/lib/couchbase-orm/relation.rb +249 -0
  33. data/lib/couchbase-orm/strict_loading.rb +21 -0
  34. data/lib/couchbase-orm/timestamps/created.rb +20 -0
  35. data/lib/couchbase-orm/timestamps/updated.rb +21 -0
  36. data/lib/couchbase-orm/timestamps.rb +15 -0
  37. data/lib/couchbase-orm/types/array.rb +32 -0
  38. data/lib/couchbase-orm/types/date.rb +9 -0
  39. data/lib/couchbase-orm/types/date_time.rb +14 -0
  40. data/lib/couchbase-orm/types/encrypted.rb +17 -0
  41. data/lib/couchbase-orm/types/nested.rb +43 -0
  42. data/lib/couchbase-orm/types/timestamp.rb +18 -0
  43. data/lib/couchbase-orm/types.rb +20 -0
  44. data/lib/couchbase-orm/utilities/enum.rb +13 -1
  45. data/lib/couchbase-orm/utilities/has_many.rb +72 -36
  46. data/lib/couchbase-orm/utilities/ignored_properties.rb +15 -0
  47. data/lib/couchbase-orm/utilities/index.rb +18 -20
  48. data/lib/couchbase-orm/utilities/properties_always_exists_in_document.rb +16 -0
  49. data/lib/couchbase-orm/utilities/query_helper.rb +148 -0
  50. data/lib/couchbase-orm/utils.rb +25 -0
  51. data/lib/couchbase-orm/version.rb +1 -1
  52. data/lib/couchbase-orm/views.rb +38 -41
  53. data/lib/couchbase-orm.rb +44 -9
  54. data/lib/ext/query_n1ql.rb +124 -0
  55. data/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +3 -2
  56. data/spec/associations_spec.rb +219 -50
  57. data/spec/base_spec.rb +296 -14
  58. data/spec/collection_proxy_spec.rb +29 -0
  59. data/spec/connection_spec.rb +27 -0
  60. data/spec/couchbase-orm/active_record_compat_spec.rb +24 -0
  61. data/spec/couchbase-orm/changeable_spec.rb +16 -0
  62. data/spec/couchbase-orm/json_schema/validation_spec.rb +23 -0
  63. data/spec/couchbase-orm/json_schema/validator_spec.rb +13 -0
  64. data/spec/couchbase-orm/timestamps_spec.rb +85 -0
  65. data/spec/couchbase-orm/timestamps_spec_models.rb +36 -0
  66. data/spec/empty-json-schema/.gitkeep +0 -0
  67. data/spec/enum_spec.rb +34 -0
  68. data/spec/has_many_spec.rb +101 -54
  69. data/spec/index_spec.rb +13 -9
  70. data/spec/json-schema/JsonSchemaBaseTest.json +19 -0
  71. data/spec/json-schema/entity_snakecase.json +20 -0
  72. data/spec/json-schema/loader_spec.rb +42 -0
  73. data/spec/json-schema/specific_path.json +20 -0
  74. data/spec/json_schema_spec.rb +178 -0
  75. data/spec/n1ql_spec.rb +193 -0
  76. data/spec/persistence_spec.rb +49 -9
  77. data/spec/relation_nested_spec.rb +88 -0
  78. data/spec/relation_spec.rb +430 -0
  79. data/spec/support.rb +16 -8
  80. data/spec/type_array_spec.rb +52 -0
  81. data/spec/type_encrypted_spec.rb +114 -0
  82. data/spec/type_nested_spec.rb +191 -0
  83. data/spec/type_spec.rb +317 -0
  84. data/spec/utilities/ignored_properties_spec.rb +20 -0
  85. data/spec/utilities/properties_always_exists_in_document_spec.rb +24 -0
  86. data/spec/views_spec.rb +32 -11
  87. 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 = (class_name || model.to_s.singularize.camelcase).to_s
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
- relset_varname = "@#{model}_rel_set"
16
+ instance_var = "@__assoc_#{model}"
20
17
 
21
18
  klass = begin
22
- class_name.constantize
23
- rescue NameError => e
24
- puts "WARNING: #{class_name} referenced in #{self.name} before it was loaded"
19
+ class_name.constantize
20
+ rescue NameError
21
+ warn "WARNING: #{class_name} referenced in #{self.name} before it was aded"
25
22
 
26
- # Open the class early - load order will have to be changed to prevent this.
27
- # Warning notice required as a misspelling will not raise an error
28
- Object.class_eval <<-EKLASS
29
- class #{class_name} < CouchbaseOrm::Base
30
- attribute :#{foreign_key}
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(relset_varname) if instance_variable_defined?(relset_varname)
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
- remote_klass.find(row.value[through_key])
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(relset_varname, enum)
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(relset_varname) if instance_variable_defined?(relset_varname)
65
- self.instance_variable_set(relset_varname, klass.__send__(remote_method, self.id))
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
- id = self.bucket.get(key, quiet: true)
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.bucket.delete(key, quiet: true)
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
- define_attribute_methods attr
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
- check_ref_id = record.class.bucket.get(original_key, extended: true, quiet: true)
104
- if check_ref_id && check_ref_id.value == record.id
105
- begin
106
- record.class.bucket.delete(original_key, cas: check_ref_id.cas)
107
- rescue ::Libcouchbase::Error::KeyExists
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
- unless presence == false && attrs.length == 1 && record[attrs[0]].nil?
114
- record.class.bucket.set(record.send(bucket_key_method), record.id, plain: true)
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.bucket.get(record.send(bucket_key_method), extended: true, quiet: true)
123
- if check_ref_id && check_ref_id.value == record.id
124
- begin
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true, encoding: ASCII-8BIT
2
2
 
3
3
  module CouchbaseOrm
4
- VERSION = '1.1.1'
4
+ VERSION = '2.0.0'
5
5
  end
@@ -19,16 +19,18 @@ module CouchbaseOrm
19
19
  # view :by_rating, emit_key: :rating
20
20
  # end
21
21
  #
22
- # Post.by_rating.stream do |response|
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 && @attributes[key].nil?
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 && @attributes[emit_key].nil?
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[:include_docs] = true
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.view(@design_document, name, **opts) { |row|
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.view(@design_document, name, **opts)
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
- ddoc = bucket.design_docs[@design_document]
120
- existing = ddoc.view_config if ddoc
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
- doc = document.dup
126
- views_actual[name] = doc
127
- doc[:map] = doc[:map].gsub('{{design_document}}', @design_document) if doc[:map]
128
- doc[:reduce] = doc[:reduce].gsub('{{design_document}}', @design_document) if doc[:reduce]
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[:map] || '').gsub(/\s+/, '')
136
- creduce = (check[:reduce] || '').gsub(/\s+/, '')
137
- dmap = (desired[:map] || '').gsub(/\s+/, '')
138
- dreduce = (desired[:reduce] || '').gsub(/\s+/, '')
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
- bucket.save_design_doc({
153
- views: views_actual
154
- }, @design_document)
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
- require 'libcouchbase'
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
- result = id.respond_to?(:cas) ? id : CouchbaseOrm::Base.bucket.get(id, quiet: true, extended: true)
14
-
15
- if result && result.value.is_a?(Hash) && result.value[:type]
16
- ddoc = result.value[:type]
17
- ::CouchbaseOrm::Base.descendants.each do |model|
18
- if model.design_document == ddoc
19
- return model.new(result)
20
- end
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