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
@@ -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