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.
Files changed (87) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +45 -0
  3. data/.gitignore +2 -0
  4. data/.travis.yml +5 -4
  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 +67 -50
  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 +23 -8
  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 +55 -8
  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 +34 -13
  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
 
@@ -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
- unless presence == false && attrs.length == 1 && record[attrs[0]].nil?
104
- record.class.bucket.set(record.send(bucket_key_method), record.id, plain: true)
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.bucket.delete(record.send(bucket_key_method), quiet: true)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true, encoding: ASCII-8BIT
2
2
 
3
3
  module CouchbaseOrm
4
- VERSION = '1.1.0'
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