couchbase-orm 1.1.1 → 2.0.2

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 (88) 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/LICENSE +201 -24
  8. data/README.md +248 -35
  9. data/ci/run_couchbase.sh +22 -0
  10. data/couchbase-orm.gemspec +26 -20
  11. data/lib/couchbase-orm/active_record_compat.rb +92 -0
  12. data/lib/couchbase-orm/associations.rb +119 -0
  13. data/lib/couchbase-orm/base.rb +143 -166
  14. data/lib/couchbase-orm/changeable.rb +512 -0
  15. data/lib/couchbase-orm/connection.rb +28 -8
  16. data/lib/couchbase-orm/encrypt.rb +48 -0
  17. data/lib/couchbase-orm/error.rb +17 -2
  18. data/lib/couchbase-orm/inspectable.rb +37 -0
  19. data/lib/couchbase-orm/json_schema/json_validation_error.rb +13 -0
  20. data/lib/couchbase-orm/json_schema/loader.rb +47 -0
  21. data/lib/couchbase-orm/json_schema/validation.rb +18 -0
  22. data/lib/couchbase-orm/json_schema/validator.rb +45 -0
  23. data/lib/couchbase-orm/json_schema.rb +9 -0
  24. data/lib/couchbase-orm/json_transcoder.rb +27 -0
  25. data/lib/couchbase-orm/locale/en.yml +5 -0
  26. data/lib/couchbase-orm/n1ql.rb +133 -0
  27. data/lib/couchbase-orm/persistence.rb +61 -52
  28. data/lib/couchbase-orm/proxies/bucket_proxy.rb +36 -0
  29. data/lib/couchbase-orm/proxies/collection_proxy.rb +52 -0
  30. data/lib/couchbase-orm/proxies/n1ql_proxy.rb +40 -0
  31. data/lib/couchbase-orm/proxies/results_proxy.rb +23 -0
  32. data/lib/couchbase-orm/railtie.rb +6 -17
  33. data/lib/couchbase-orm/relation.rb +249 -0
  34. data/lib/couchbase-orm/strict_loading.rb +21 -0
  35. data/lib/couchbase-orm/timestamps/created.rb +20 -0
  36. data/lib/couchbase-orm/timestamps/updated.rb +21 -0
  37. data/lib/couchbase-orm/timestamps.rb +15 -0
  38. data/lib/couchbase-orm/types/array.rb +32 -0
  39. data/lib/couchbase-orm/types/date.rb +9 -0
  40. data/lib/couchbase-orm/types/date_time.rb +14 -0
  41. data/lib/couchbase-orm/types/encrypted.rb +17 -0
  42. data/lib/couchbase-orm/types/nested.rb +43 -0
  43. data/lib/couchbase-orm/types/timestamp.rb +18 -0
  44. data/lib/couchbase-orm/types.rb +20 -0
  45. data/lib/couchbase-orm/utilities/enum.rb +13 -1
  46. data/lib/couchbase-orm/utilities/has_many.rb +72 -36
  47. data/lib/couchbase-orm/utilities/ignored_properties.rb +15 -0
  48. data/lib/couchbase-orm/utilities/index.rb +18 -20
  49. data/lib/couchbase-orm/utilities/properties_always_exists_in_document.rb +16 -0
  50. data/lib/couchbase-orm/utilities/query_helper.rb +148 -0
  51. data/lib/couchbase-orm/utils.rb +25 -0
  52. data/lib/couchbase-orm/version.rb +1 -1
  53. data/lib/couchbase-orm/views.rb +38 -41
  54. data/lib/couchbase-orm.rb +44 -9
  55. data/lib/ext/query_n1ql.rb +124 -0
  56. data/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +3 -2
  57. data/spec/associations_spec.rb +219 -50
  58. data/spec/base_spec.rb +296 -14
  59. data/spec/collection_proxy_spec.rb +29 -0
  60. data/spec/connection_spec.rb +27 -0
  61. data/spec/couchbase-orm/active_record_compat_spec.rb +24 -0
  62. data/spec/couchbase-orm/changeable_spec.rb +16 -0
  63. data/spec/couchbase-orm/json_schema/validation_spec.rb +23 -0
  64. data/spec/couchbase-orm/json_schema/validator_spec.rb +13 -0
  65. data/spec/couchbase-orm/timestamps_spec.rb +85 -0
  66. data/spec/couchbase-orm/timestamps_spec_models.rb +36 -0
  67. data/spec/empty-json-schema/.gitkeep +0 -0
  68. data/spec/enum_spec.rb +34 -0
  69. data/spec/has_many_spec.rb +101 -54
  70. data/spec/index_spec.rb +13 -9
  71. data/spec/json-schema/JsonSchemaBaseTest.json +19 -0
  72. data/spec/json-schema/entity_snakecase.json +20 -0
  73. data/spec/json-schema/loader_spec.rb +42 -0
  74. data/spec/json-schema/specific_path.json +20 -0
  75. data/spec/json_schema_spec.rb +178 -0
  76. data/spec/n1ql_spec.rb +193 -0
  77. data/spec/persistence_spec.rb +49 -9
  78. data/spec/relation_nested_spec.rb +88 -0
  79. data/spec/relation_spec.rb +430 -0
  80. data/spec/support.rb +16 -8
  81. data/spec/type_array_spec.rb +52 -0
  82. data/spec/type_encrypted_spec.rb +114 -0
  83. data/spec/type_nested_spec.rb +191 -0
  84. data/spec/type_spec.rb +317 -0
  85. data/spec/utilities/ignored_properties_spec.rb +20 -0
  86. data/spec/utilities/properties_always_exists_in_document_spec.rb +24 -0
  87. data/spec/views_spec.rb +32 -11
  88. metadata +193 -30
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+
4
+ module CouchbaseOrm
5
+ module JsonSchema
6
+ class Loader
7
+ include Singleton
8
+ class Error < StandardError; end
9
+
10
+ JSON_SCHEMAS_PATH = 'db/cborm_schemas'
11
+
12
+ attr_reader :schemas
13
+
14
+ def initialize(json_schemas_path = JSON_SCHEMAS_PATH)
15
+ @schemas_directory = json_schemas_path
16
+ @schemas = {}
17
+ unless File.directory?(schemas_directory)
18
+ CouchbaseOrm.logger.info { "Directory not found #{schemas_directory}" }
19
+ end
20
+ end
21
+
22
+ def extract_type(entity = {})
23
+ entity[:type]
24
+ end
25
+
26
+ def get_json_schema!(entity, schema_path: nil)
27
+ document_type = extract_type!(entity)
28
+
29
+ return schemas[document_type] if schemas.key?(document_type)
30
+
31
+ schema_path ||= File.join(schemas_directory, "#{document_type}.json")
32
+
33
+ raise(Error, "Schema not found for #{document_type} in #{schema_path}") unless File.exist?(schema_path)
34
+
35
+ schemas[document_type] = File.read schema_path
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :schemas_directory
41
+
42
+ def extract_type!(entity = {})
43
+ extract_type(entity) || raise(Error, "No type found in #{entity}")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,18 @@
1
+ module CouchbaseOrm
2
+ module JsonSchema
3
+ module Validation
4
+
5
+ def validate_json_schema(mode: :strict, schema_path: nil)
6
+ @json_validation_config = {
7
+ enabled: true,
8
+ mode: mode,
9
+ schema_path: schema_path,
10
+ }.freeze
11
+ end
12
+
13
+ def json_validation_config
14
+ @json_validation_config ||= {}
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require 'json-schema'
5
+
6
+ module CouchbaseOrm
7
+ module JsonSchema
8
+ class Validator
9
+
10
+ def initialize(json_validation_config)
11
+ @json_validation_config = json_validation_config
12
+ end
13
+
14
+ def validate_entity(entity, json)
15
+ case json_validation_config[:mode]
16
+ when :strict
17
+ strict_validation(entity, json)
18
+ when :logger
19
+ logger_validation(entity, json)
20
+ else
21
+ raise "Unknown validation mode #{json_validation_config[:mode]}"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :json_validation_config
28
+
29
+ def strict_validation(entity, json)
30
+ error_results = common_validate(entity, json)
31
+ raise JsonValidationError.new(Loader.instance.extract_type(entity), error_results) unless error_results.empty?
32
+ end
33
+
34
+ def logger_validation(entity, json)
35
+ error_results = common_validate(entity, json)
36
+ CouchbaseOrm.logger.error { "[COUCHBASEORM]: Invalid document #{Loader.instance.extract_type(entity)} with errors : #{error_results}" } unless error_results.empty?
37
+ end
38
+
39
+ def common_validate(entity, json)
40
+ schema = Loader.instance.get_json_schema!(entity, schema_path: json_validation_config[:schema_path])
41
+ JSON::Validator.fully_validate(schema, json)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'couchbase-orm/json_schema/loader'
4
+ require 'couchbase-orm/json_schema/validator'
5
+ require 'couchbase-orm/json_schema/json_validation_error'
6
+ module CouchbaseOrm
7
+ module JsonSchema
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ require "json"
2
+ require 'couchbase/json_transcoder'
3
+ require 'couchbase-orm/json_schema'
4
+
5
+ module CouchbaseOrm
6
+ class JsonTranscoder < Couchbase::JsonTranscoder
7
+
8
+ attr_reader :ignored_properties, :json_validation_config
9
+
10
+ def initialize(ignored_properties: [], json_validation_config: {}, **options, &block)
11
+ @ignored_properties = ignored_properties
12
+ @json_validation_config = json_validation_config
13
+ super(**options, &block)
14
+ end
15
+
16
+ def decode(blob, _flags)
17
+ original = super
18
+ original&.except(*ignored_properties)
19
+ end
20
+
21
+ def encode(document)
22
+ original = super
23
+ CouchbaseOrm::JsonSchema::Validator.new(json_validation_config).validate_entity(document, original[0]) if document.present? && !original.empty? && json_validation_config[:enabled]
24
+ original
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ en:
2
+ couchbase:
3
+ errors:
4
+ messages:
5
+ record_invalid: "Validation failed: %{errors}"
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'active_support/core_ext/array/wrap'
5
+ require 'active_support/core_ext/object/try'
6
+
7
+ module CouchbaseOrm
8
+ module N1ql
9
+ extend ActiveSupport::Concern
10
+ NO_VALUE = :no_value_specified
11
+ DEFAULT_SCAN_CONSISTENCY = :request_plus
12
+ # sanitize for injection query
13
+ def self.sanitize(value)
14
+ if value.is_a?(String)
15
+ value.gsub("'", "''").gsub("\\"){"\\\\"}.gsub('"', '\"')
16
+ elsif value.is_a?(Array)
17
+ value.map{ |v| sanitize(v) }
18
+ else
19
+ value
20
+ end
21
+ end
22
+
23
+ def self.config(new_config = nil)
24
+ Thread.current['__couchbaseorm_n1ql_config__'] = new_config if new_config
25
+ Thread.current['__couchbaseorm_n1ql_config__'] || {
26
+ scan_consistency: DEFAULT_SCAN_CONSISTENCY
27
+ }
28
+ end
29
+
30
+ module ClassMethods
31
+ # Defines a query N1QL for the model
32
+ #
33
+ # @param [Symbol, String, Array] names names of the views
34
+ # @param [Hash] options options passed to the {Couchbase::N1QL}
35
+ #
36
+ # @example Define some N1QL queries for a model
37
+ # class Post < CouchbaseOrm::Base
38
+ # n1ql :by_rating, emit_key: :rating
39
+ # end
40
+ #
41
+ # Post.by_rating do |response|
42
+ # # ...
43
+ # end
44
+ # TODO: add range keys [:startkey, :endkey]
45
+ def n1ql(name, query_fn: nil, emit_key: [], custom_order: nil, **options)
46
+ raise ArgumentError, "#{self} already respond_to? #{name}" if self.respond_to?(name)
47
+
48
+ emit_key = Array.wrap(emit_key)
49
+ emit_key.each do |key|
50
+ raise "unknown emit_key attribute for n1ql :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s)
51
+ end
52
+ options = N1QL_DEFAULTS.merge(options)
53
+ method_opts = {}
54
+ method_opts[:emit_key] = emit_key
55
+
56
+ @indexes ||= {}
57
+ @indexes[name] = method_opts
58
+
59
+ singleton_class.__send__(:define_method, name) do |key: NO_VALUE, **opts, &result_modifier|
60
+ opts = options.merge(opts).reverse_merge(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency])
61
+ values = key == NO_VALUE ? NO_VALUE : convert_values(method_opts[:emit_key], key)
62
+ current_query = run_query(method_opts[:emit_key], values, query_fn, custom_order: custom_order, **opts.except(:include_docs, :key))
63
+ if result_modifier
64
+ opts[:include_docs] = true
65
+ current_query.results &result_modifier
66
+ elsif opts[:include_docs]
67
+ current_query.results { |res| find(res) }
68
+ else
69
+ current_query.results
70
+ end
71
+ end
72
+ end
73
+ N1QL_DEFAULTS = { include_docs: true }
74
+
75
+ # add a n1ql query and lookup method to the model for finding all records
76
+ # using a value in the supplied attr.
77
+ def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil)
78
+ n1ql_method ||= "by_#{attr}"
79
+ find_method ||= "find_#{n1ql_method}"
80
+
81
+ validates(attr, presence: true) if validate
82
+ n1ql n1ql_method, emit_key: attr
83
+
84
+ define_singleton_method find_method do |value|
85
+ send n1ql_method, key: [value]
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def convert_values(keys, values)
92
+ return values if keys.empty? && Array.wrap(values).any?
93
+ keys.zip(Array.wrap(values)).map do |key, value_before_type_cast|
94
+ serialize_value(key, value_before_type_cast)
95
+ end
96
+ end
97
+
98
+ def build_where(keys, values)
99
+ where = values == NO_VALUE ? '' : keys.zip(Array.wrap(values))
100
+ .reject { |key, value| key.nil? && value.nil? }
101
+ .map { |key, value| build_match(key, value) }
102
+ .join(" AND ")
103
+ "type=\"#{design_document}\" #{"AND " + where unless where.blank?}"
104
+ end
105
+
106
+ # order-by-clause ::= ORDER BY ordering-term [ ',' ordering-term ]*
107
+ # ordering-term ::= expr [ ASC | DESC ] [ NULLS ( FIRST | LAST ) ]
108
+ # see https://docs.couchbase.com/server/5.0/n1ql/n1ql-language-reference/orderby.html
109
+ def build_order(keys, descending)
110
+ "#{keys.dup.push("meta().id").map { |k| "#{k} #{descending ? "desc" : "asc" }" }.join(",")}"
111
+ end
112
+
113
+ def build_limit(limit)
114
+ limit ? "limit #{limit}" : ""
115
+ end
116
+
117
+ def run_query(keys, values, query_fn, custom_order: nil, descending: false, limit: nil, **options)
118
+ if query_fn
119
+ N1qlProxy.new(query_fn.call(bucket, values, Couchbase::Options::Query.new(**options)))
120
+ else
121
+ bucket_name = bucket.name
122
+ where = build_where(keys, values)
123
+ order = custom_order || build_order(keys, descending)
124
+ limit = build_limit(limit)
125
+ n1ql_query = "select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}"
126
+ result = cluster.query(n1ql_query, Couchbase::Options::Query.new(**options))
127
+ CouchbaseOrm.logger.debug "N1QL query: #{n1ql_query} return #{result.rows.to_a.length} rows with scan_consistency : #{options[:scan_consistency]}"
128
+ N1qlProxy.new(result)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -2,11 +2,18 @@
2
2
 
3
3
  require 'active_model'
4
4
  require 'active_support/hash_with_indifferent_access'
5
+ require 'couchbase-orm/json_transcoder'
6
+ require 'couchbase-orm/encrypt'
5
7
 
6
8
  module CouchbaseOrm
7
9
  module Persistence
8
10
  extend ActiveSupport::Concern
9
11
 
12
+ include CouchbaseOrm::Encrypt
13
+
14
+ included do
15
+ attribute :id, :string
16
+ end
10
17
 
11
18
  module ClassMethods
12
19
  def create(attributes = nil, &block)
@@ -15,6 +22,7 @@ module CouchbaseOrm
15
22
  else
16
23
  instance = new(attributes, &block)
17
24
  instance.save
25
+ instance.reset_object!
18
26
  instance
19
27
  end
20
28
  end
@@ -25,6 +33,7 @@ module CouchbaseOrm
25
33
  else
26
34
  instance = new(attributes, &block)
27
35
  instance.save!
36
+ instance.reset_object!
28
37
  instance
29
38
  end
30
39
  end
@@ -54,20 +63,19 @@ module CouchbaseOrm
54
63
  # Returns true if this object hasn't been saved yet -- that is, a record
55
64
  # for the object doesn't exist in the database yet; otherwise, returns false.
56
65
  def new_record?
57
- @__metadata__.cas.nil? && @__metadata__.key.nil?
66
+ @__metadata__.cas.nil?
58
67
  end
59
68
  alias_method :new?, :new_record?
60
69
 
61
70
  # Returns true if this object has been destroyed, otherwise returns false.
62
71
  def destroyed?
63
- !!(@__metadata__.cas && @__metadata__.key.nil?)
72
+ @destroyed ||= false
64
73
  end
65
74
 
66
75
  # Returns true if the record is persisted, i.e. it's not a new record and it was
67
76
  # not destroyed, otherwise returns false.
68
77
  def persisted?
69
- # Changed? is provided by ActiveModel::Dirty
70
- !!@__metadata__.key
78
+ !new_record? && !destroyed?
71
79
  end
72
80
  alias_method :exists?, :persisted?
73
81
 
@@ -99,16 +107,18 @@ module CouchbaseOrm
99
107
  # The record is simply removed, no callbacks are executed.
100
108
  def delete(with_cas: false, **options)
101
109
  options[:cas] = @__metadata__.cas if with_cas
102
- self.class.bucket.delete(@__metadata__.key, options)
103
-
104
- @__metadata__.key = nil
105
- @id = nil
110
+ CouchbaseOrm.logger.debug "Data - Delete #{self.id}"
111
+ self.class.collection.remove(self.id, **options)
106
112
 
113
+ self.id = nil
107
114
  clear_changes_information
115
+ @destroyed = true
108
116
  self.freeze
109
117
  self
110
118
  end
111
119
 
120
+ alias :remove :delete
121
+
112
122
  # Deletes the record in the database and freezes this instance to reflect
113
123
  # that no changes should be made (since they can't be persisted).
114
124
  #
@@ -121,12 +131,13 @@ module CouchbaseOrm
121
131
  destroy_associations!
122
132
 
123
133
  options[:cas] = @__metadata__.cas if with_cas
124
- self.class.bucket.delete(@__metadata__.key, options)
134
+ CouchbaseOrm.logger.debug "Data - Destroy #{id}"
135
+ self.class.collection.remove(id, **options)
125
136
 
126
- @__metadata__.key = nil
127
- @id = nil
137
+ self.id = nil
128
138
 
129
139
  clear_changes_information
140
+ @destroyed = true
130
141
  freeze
131
142
  end
132
143
  end
@@ -141,7 +152,12 @@ module CouchbaseOrm
141
152
  public_send(:"#{name}=", value)
142
153
  changed? ? save(validate: false) : true
143
154
  end
144
-
155
+
156
+ def assign_attributes(hash)
157
+ hash = hash.with_indifferent_access if hash.is_a?(Hash)
158
+ super(hash.except("type"))
159
+ end
160
+
145
161
  # Updates the attributes of the model from the passed-in hash and saves the
146
162
  # record. If the object is invalid, the saving will fail and false will be returned.
147
163
  def update(hash)
@@ -158,10 +174,12 @@ module CouchbaseOrm
158
174
  end
159
175
  alias_method :update_attributes!, :update!
160
176
 
161
- # Updates the record without validating or running callbacks
177
+ # Updates the record without validating or running callbacks.
178
+ # Updates only the attributes that are passed in as parameters
179
+ # except if there is more than 16 attributes, in which case
180
+ # the whole record is saved.
162
181
  def update_columns(with_cas: false, **hash)
163
- _id = @__metadata__.key
164
- raise "unable to update columns, model not persisted" unless _id
182
+ raise "unable to update columns, model not persisted" unless id
165
183
 
166
184
  assign_attributes(hash)
167
185
 
@@ -170,20 +188,17 @@ module CouchbaseOrm
170
188
 
171
189
  # There is a limit of 16 subdoc operations per request
172
190
  resp = if hash.length <= 16
173
- subdoc = self.class.bucket.subdoc(_id)
174
- hash.each do |key, value|
175
- subdoc.dict_upsert(key, value)
176
- end
177
- subdoc.execute!(options)
191
+ self.class.collection.mutate_in(
192
+ id,
193
+ hash.map { |k, v| Couchbase::MutateInSpec.replace(k.to_s, v) }
194
+ )
178
195
  else
179
196
  # Fallback to writing the whole document
180
- @__attributes__[:type] = self.class.design_document
181
- @__attributes__.delete(:id)
182
- self.class.bucket.replace(_id, @__attributes__, **options)
197
+ CouchbaseOrm.logger.debug { "Data - Replace #{id} #{attributes.to_s.truncate(200)}" }
198
+ self.class.collection.replace(id, attributes.except("id").merge(type: self.class.design_document), **options)
183
199
  end
184
200
 
185
201
  # Ensure the model is up to date
186
- @__metadata__.key = resp.key
187
202
  @__metadata__.cas = resp.cas
188
203
 
189
204
  changes_applied
@@ -194,47 +209,43 @@ module CouchbaseOrm
194
209
  #
195
210
  # This method finds record by its key and modifies the receiver in-place:
196
211
  def reload
197
- key = @__metadata__.key
198
- raise "unable to reload, model not persisted" unless key
212
+ raise "unable to reload, model not persisted" unless id
199
213
 
200
- resp = self.class.bucket.get(key, quiet: false, extended: true)
201
- @__attributes__ = ::ActiveSupport::HashWithIndifferentAccess.new(resp.value)
202
- @__metadata__.key = resp.key
214
+ CouchbaseOrm.logger.debug "Data - Get #{id}"
215
+ resp = self.class.collection.get!(id)
216
+ assign_attributes(decode_encrypted_attributes(resp.content.except("id", *self.class.ignored_properties ))) # API return a nil id
203
217
  @__metadata__.cas = resp.cas
204
218
 
205
219
  reset_associations
206
220
  clear_changes_information
221
+ reset_object!
207
222
  self
208
223
  end
209
224
 
210
225
  # Updates the TTL of the document
211
226
  def touch(**options)
212
- res = self.class.bucket.touch(@__metadata__.key, async: false, **options)
227
+ CouchbaseOrm.logger.debug "Data - Touch #{id}"
228
+ _res = self.class.collection.touch(id, async: false, **options)
213
229
  @__metadata__.cas = resp.cas
214
230
  self
215
231
  end
216
232
 
217
233
 
218
- protected
219
-
220
234
 
221
- def _update_record(with_cas: false, **options)
235
+ def _update_record(*_args, with_cas: false, **options)
222
236
  return false unless perform_validations(:update, options)
223
- return true unless changed?
237
+ return true unless changed? || self.class.attribute_types.any? { |_, type| type.is_a?(CouchbaseOrm::Types::Nested) || type.is_a?(CouchbaseOrm::Types::Array) }
224
238
 
225
239
  run_callbacks :update do
226
240
  run_callbacks :save do
227
- # Ensure the type is set
228
- @__attributes__[:type] = self.class.design_document
229
- @__attributes__.delete(:id)
230
-
231
- _id = @__metadata__.key
232
241
  options[:cas] = @__metadata__.cas if with_cas
233
-
234
- resp = self.class.bucket.replace(_id, @__attributes__, **options)
242
+ CouchbaseOrm.logger.debug { "_update_record - replace #{id} #{serialized_attributes.to_s.truncate(200)}" }
243
+ if options[:transcoder].nil?
244
+ options[:transcoder] = CouchbaseOrm::JsonTranscoder.new(json_validation_config: self.class.json_validation_config)
245
+ end
246
+ resp = self.class.collection.replace(id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options))
235
247
 
236
248
  # Ensure the model is up to date
237
- @__metadata__.key = resp.key
238
249
  @__metadata__.cas = resp.cas
239
250
 
240
251
  changes_applied
@@ -242,21 +253,19 @@ module CouchbaseOrm
242
253
  end
243
254
  end
244
255
  end
245
-
246
- def _create_record(**options)
256
+ def _create_record(*_args, **options)
247
257
  return false unless perform_validations(:create, options)
248
258
 
249
259
  run_callbacks :create do
250
260
  run_callbacks :save do
251
- # Ensure the type is set
252
- @__attributes__[:type] = self.class.design_document
253
- @__attributes__.delete(:id)
254
-
255
- _id = @id || self.class.uuid_generator.next(self)
256
- resp = self.class.bucket.add(_id, @__attributes__, **options)
261
+ assign_attributes(id: self.class.uuid_generator.next(self)) unless self.id
262
+ CouchbaseOrm.logger.debug { "_create_record - Upsert #{id} #{serialized_attributes.to_s.truncate(200)}" }
263
+ if options[:transcoder].nil?
264
+ options[:transcoder] = CouchbaseOrm::JsonTranscoder.new(json_validation_config: self.class.json_validation_config)
265
+ end
266
+ resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options))
257
267
 
258
268
  # Ensure the model is up to date
259
- @__metadata__.key = resp.key
260
269
  @__metadata__.cas = resp.cas
261
270
 
262
271
  changes_applied
@@ -270,4 +279,4 @@ module CouchbaseOrm
270
279
  true
271
280
  end
272
281
  end
273
- end
282
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true, encoding: ASCII-8BIT
2
+
3
+ require 'couchbase-orm/proxies/n1ql_proxy'
4
+
5
+ module CouchbaseOrm
6
+ class BucketProxy
7
+ def initialize(proxyfied)
8
+ raise ArgumentError, "Must proxy a non nil object" if proxyfied.nil?
9
+
10
+ @proxyfied = proxyfied
11
+
12
+ self.class.define_method(:n1ql) do
13
+ N1qlProxy.new(@proxyfied.n1ql)
14
+ end
15
+
16
+ self.class.define_method(:view) do |design, view, **opts, &block|
17
+ @results = nil if @current_query != "#{design}_#{view}"
18
+ @current_query = "#{design}_#{view}"
19
+ return @results if @results
20
+
21
+ CouchbaseOrm.logger.debug "View - #{design} #{view}"
22
+ @results = ResultsProxy.new(@proxyfied.send(:view, design, view, **opts, &block))
23
+ end
24
+ end
25
+
26
+ if RUBY_VERSION.to_i >= 3
27
+ def method_missing(name, *args, **options, &block)
28
+ @proxyfied.public_send(name, *args, **options, &block)
29
+ end
30
+ else
31
+ def method_missing(name, *args, &block)
32
+ @proxyfied.public_send(name, *args, &block)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,52 @@
1
+ require "couchbase"
2
+
3
+ module CouchbaseOrm
4
+ class CollectionProxy
5
+
6
+ def get!(id, **options)
7
+ @proxyfied.get(id, Couchbase::Options::Get.new(**options))
8
+ end
9
+
10
+ def get(id, **options)
11
+ @proxyfied.get(id, Couchbase::Options::Get.new(**options))
12
+ rescue Couchbase::Error::DocumentNotFound
13
+ nil
14
+ end
15
+
16
+ def get_multi!(*ids, **options)
17
+ result = @proxyfied.get_multi(*ids, Couchbase::Options::GetMulti.new(**options))
18
+ first_result_with_error = result.find(&:error)
19
+ raise first_result_with_error.error if first_result_with_error
20
+ result
21
+ end
22
+
23
+ def get_multi(*ids, **options)
24
+ @proxyfied.get_multi(*ids, Couchbase::Options::GetMulti.new(**options))
25
+ end
26
+
27
+ def remove!(id, **options)
28
+ @proxyfied.remove(id, Couchbase::Options::Remove.new(**options))
29
+ end
30
+
31
+ def remove(id, **options)
32
+ @proxyfied.remove(id, Couchbase::Options::Remove.new(**options))
33
+ rescue Couchbase::Error::DocumentNotFound
34
+ nil
35
+ end
36
+
37
+ def initialize(proxyfied)
38
+ raise "Must proxy a non nil object" if proxyfied.nil?
39
+ @proxyfied = proxyfied
40
+ end
41
+
42
+ if RUBY_VERSION.to_i >= 3
43
+ def method_missing(name, *args, **options, &block)
44
+ @proxyfied.public_send(name, *args, **options, &block)
45
+ end
46
+ else # :nocov:
47
+ def method_missing(name, *args, &block)
48
+ @proxyfied.public_send(name, *args, &block)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true, encoding: ASCII-8BIT
2
+
3
+ require 'couchbase-orm/proxies/results_proxy'
4
+
5
+ module CouchbaseOrm
6
+ class N1qlProxy
7
+ def initialize(proxyfied)
8
+ @proxyfied = proxyfied
9
+
10
+ self.class.define_method(:results) do |*params, &block|
11
+ @results = nil if @current_query != self.to_s
12
+ @current_query = self.to_s
13
+ return @results if @results
14
+
15
+ CouchbaseOrm.logger.debug { 'Query - ' + self.to_s }
16
+
17
+ results = @proxyfied.rows
18
+ results = results.map { |r| block.call(r) } if block
19
+ @results = ResultsProxy.new(results.to_a)
20
+ end
21
+
22
+ self.class.define_method(:to_s) do
23
+ @proxyfied.to_s.tr("\n", ' ')
24
+ end
25
+
26
+ proxyfied.public_methods.each do |method|
27
+ next if self.public_methods.include?(method)
28
+
29
+ self.class.define_method(method) do |*params, &block|
30
+ ret = @proxyfied.send(method, *params, &block)
31
+ ret.is_a?(@proxyfied.class) ? self : ret
32
+ end
33
+ end
34
+ end
35
+
36
+ def method_missing(m, *args, &block)
37
+ self.results.send(m, *args, &block)
38
+ end
39
+ end
40
+ end