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
@@ -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,24 +174,31 @@ 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
 
168
- # Ensure the type is set
169
- @__attributes__[:type] = self.class.design_document
170
- @__attributes__.delete(:id)
171
-
172
- options = {}
186
+ options = {extended: true}
173
187
  options[:cas] = @__metadata__.cas if with_cas
174
188
 
175
- resp = self.class.bucket.replace(_id, @__attributes__, **options)
189
+ # There is a limit of 16 subdoc operations per request
190
+ resp = if hash.length <= 16
191
+ self.class.collection.mutate_in(
192
+ id,
193
+ hash.map { |k, v| Couchbase::MutateInSpec.replace(k.to_s, v) }
194
+ )
195
+ else
196
+ # Fallback to writing the whole document
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)
199
+ end
176
200
 
177
201
  # Ensure the model is up to date
178
- @__metadata__.key = resp.key
179
202
  @__metadata__.cas = resp.cas
180
203
 
181
204
  changes_applied
@@ -186,47 +209,43 @@ module CouchbaseOrm
186
209
  #
187
210
  # This method finds record by its key and modifies the receiver in-place:
188
211
  def reload
189
- key = @__metadata__.key
190
- raise "unable to reload, model not persisted" unless key
212
+ raise "unable to reload, model not persisted" unless id
191
213
 
192
- resp = self.class.bucket.get(key, quiet: false, extended: true)
193
- @__attributes__ = ::ActiveSupport::HashWithIndifferentAccess.new(resp.value)
194
- @__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
195
217
  @__metadata__.cas = resp.cas
196
218
 
197
219
  reset_associations
198
220
  clear_changes_information
221
+ reset_object!
199
222
  self
200
223
  end
201
224
 
202
225
  # Updates the TTL of the document
203
226
  def touch(**options)
204
- 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)
205
229
  @__metadata__.cas = resp.cas
206
230
  self
207
231
  end
208
232
 
209
233
 
210
- protected
211
234
 
212
-
213
- def _update_record(with_cas: false, **options)
235
+ def _update_record(*_args, with_cas: false, **options)
214
236
  return false unless perform_validations(:update, options)
215
- 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) }
216
238
 
217
239
  run_callbacks :update do
218
240
  run_callbacks :save do
219
- # Ensure the type is set
220
- @__attributes__[:type] = self.class.design_document
221
- @__attributes__.delete(:id)
222
-
223
- _id = @__metadata__.key
224
241
  options[:cas] = @__metadata__.cas if with_cas
225
-
226
- 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))
227
247
 
228
248
  # Ensure the model is up to date
229
- @__metadata__.key = resp.key
230
249
  @__metadata__.cas = resp.cas
231
250
 
232
251
  changes_applied
@@ -234,21 +253,19 @@ module CouchbaseOrm
234
253
  end
235
254
  end
236
255
  end
237
-
238
- def _create_record(**options)
256
+ def _create_record(*_args, **options)
239
257
  return false unless perform_validations(:create, options)
240
258
 
241
259
  run_callbacks :create do
242
260
  run_callbacks :save do
243
- # Ensure the type is set
244
- @__attributes__[:type] = self.class.design_document
245
- @__attributes__.delete(:id)
246
-
247
- _id = @id || self.class.uuid_generator.next(self)
248
- 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))
249
267
 
250
268
  # Ensure the model is up to date
251
- @__metadata__.key = resp.key
252
269
  @__metadata__.cas = resp.cas
253
270
 
254
271
  changes_applied
@@ -262,4 +279,4 @@ module CouchbaseOrm
262
279
  true
263
280
  end
264
281
  end
265
- 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