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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +45 -0
- data/.gitignore +2 -0
- data/.travis.yml +3 -2
- data/CODEOWNERS +1 -0
- data/Gemfile +5 -3
- data/README.md +237 -31
- data/ci/run_couchbase.sh +22 -0
- data/couchbase-orm.gemspec +26 -20
- data/lib/couchbase-orm/active_record_compat.rb +92 -0
- data/lib/couchbase-orm/associations.rb +119 -0
- data/lib/couchbase-orm/base.rb +143 -166
- data/lib/couchbase-orm/changeable.rb +512 -0
- data/lib/couchbase-orm/connection.rb +28 -8
- data/lib/couchbase-orm/encrypt.rb +48 -0
- data/lib/couchbase-orm/error.rb +17 -2
- data/lib/couchbase-orm/inspectable.rb +37 -0
- data/lib/couchbase-orm/json_schema/json_validation_error.rb +13 -0
- data/lib/couchbase-orm/json_schema/loader.rb +47 -0
- data/lib/couchbase-orm/json_schema/validation.rb +18 -0
- data/lib/couchbase-orm/json_schema/validator.rb +45 -0
- data/lib/couchbase-orm/json_schema.rb +9 -0
- data/lib/couchbase-orm/json_transcoder.rb +27 -0
- data/lib/couchbase-orm/locale/en.yml +5 -0
- data/lib/couchbase-orm/n1ql.rb +133 -0
- data/lib/couchbase-orm/persistence.rb +61 -52
- data/lib/couchbase-orm/proxies/bucket_proxy.rb +36 -0
- data/lib/couchbase-orm/proxies/collection_proxy.rb +52 -0
- data/lib/couchbase-orm/proxies/n1ql_proxy.rb +40 -0
- data/lib/couchbase-orm/proxies/results_proxy.rb +23 -0
- data/lib/couchbase-orm/railtie.rb +6 -17
- data/lib/couchbase-orm/relation.rb +249 -0
- data/lib/couchbase-orm/strict_loading.rb +21 -0
- data/lib/couchbase-orm/timestamps/created.rb +20 -0
- data/lib/couchbase-orm/timestamps/updated.rb +21 -0
- data/lib/couchbase-orm/timestamps.rb +15 -0
- data/lib/couchbase-orm/types/array.rb +32 -0
- data/lib/couchbase-orm/types/date.rb +9 -0
- data/lib/couchbase-orm/types/date_time.rb +14 -0
- data/lib/couchbase-orm/types/encrypted.rb +17 -0
- data/lib/couchbase-orm/types/nested.rb +43 -0
- data/lib/couchbase-orm/types/timestamp.rb +18 -0
- data/lib/couchbase-orm/types.rb +20 -0
- data/lib/couchbase-orm/utilities/enum.rb +13 -1
- data/lib/couchbase-orm/utilities/has_many.rb +72 -36
- data/lib/couchbase-orm/utilities/ignored_properties.rb +15 -0
- data/lib/couchbase-orm/utilities/index.rb +18 -20
- data/lib/couchbase-orm/utilities/properties_always_exists_in_document.rb +16 -0
- data/lib/couchbase-orm/utilities/query_helper.rb +148 -0
- data/lib/couchbase-orm/utils.rb +25 -0
- data/lib/couchbase-orm/version.rb +1 -1
- data/lib/couchbase-orm/views.rb +38 -41
- data/lib/couchbase-orm.rb +44 -9
- data/lib/ext/query_n1ql.rb +124 -0
- data/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +3 -2
- data/spec/associations_spec.rb +219 -50
- data/spec/base_spec.rb +296 -14
- data/spec/collection_proxy_spec.rb +29 -0
- data/spec/connection_spec.rb +27 -0
- data/spec/couchbase-orm/active_record_compat_spec.rb +24 -0
- data/spec/couchbase-orm/changeable_spec.rb +16 -0
- data/spec/couchbase-orm/json_schema/validation_spec.rb +23 -0
- data/spec/couchbase-orm/json_schema/validator_spec.rb +13 -0
- data/spec/couchbase-orm/timestamps_spec.rb +85 -0
- data/spec/couchbase-orm/timestamps_spec_models.rb +36 -0
- data/spec/empty-json-schema/.gitkeep +0 -0
- data/spec/enum_spec.rb +34 -0
- data/spec/has_many_spec.rb +101 -54
- data/spec/index_spec.rb +13 -9
- data/spec/json-schema/JsonSchemaBaseTest.json +19 -0
- data/spec/json-schema/entity_snakecase.json +20 -0
- data/spec/json-schema/loader_spec.rb +42 -0
- data/spec/json-schema/specific_path.json +20 -0
- data/spec/json_schema_spec.rb +178 -0
- data/spec/n1ql_spec.rb +193 -0
- data/spec/persistence_spec.rb +49 -9
- data/spec/relation_nested_spec.rb +88 -0
- data/spec/relation_spec.rb +430 -0
- data/spec/support.rb +16 -8
- data/spec/type_array_spec.rb +52 -0
- data/spec/type_encrypted_spec.rb +114 -0
- data/spec/type_nested_spec.rb +191 -0
- data/spec/type_spec.rb +317 -0
- data/spec/utilities/ignored_properties_spec.rb +20 -0
- data/spec/utilities/properties_always_exists_in_document_spec.rb +24 -0
- data/spec/views_spec.rb +32 -11
- metadata +192 -29
@@ -0,0 +1,512 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module CouchbaseOrm
|
6
|
+
# Defines behavior for dirty tracking.
|
7
|
+
module Changeable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
# Get the changed attributes for the document.
|
11
|
+
#
|
12
|
+
# @example Get the changed attributes.
|
13
|
+
# model.changed
|
14
|
+
#
|
15
|
+
# @return [ Array<String> ] The changed attributes.
|
16
|
+
def changed
|
17
|
+
changed_attributes.keys.select { |attr| attribute_change(attr) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Has the document changed?
|
21
|
+
#
|
22
|
+
# @example Has the document changed?
|
23
|
+
# model.changed?
|
24
|
+
#
|
25
|
+
# @return [ true | false ] If the document is changed.
|
26
|
+
def changed?
|
27
|
+
changes.values.any? { |val| val } || children_changed?
|
28
|
+
end
|
29
|
+
|
30
|
+
def _children
|
31
|
+
attributes.select { |name, _value| self.class.type_for_attribute(name) == CouchbaseOrm::NestedDocument }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Have any children (embedded documents) of this document changed?
|
35
|
+
#
|
36
|
+
# @note This intentionally only considers children and not descendants.
|
37
|
+
#
|
38
|
+
# @return [ true | false ] If any children have changed.
|
39
|
+
def children_changed?
|
40
|
+
_children.any?(&:changed?)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get the attribute changes.
|
44
|
+
#
|
45
|
+
# @example Get the attribute changes.
|
46
|
+
# model.changed_attributes
|
47
|
+
#
|
48
|
+
# @return [ Hash<String, Object> ] The attribute changes.
|
49
|
+
def changed_attributes
|
50
|
+
@changed_attributes ||= {}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get all the changes for the document.
|
54
|
+
#
|
55
|
+
# @example Get all the changes.
|
56
|
+
# model.changes
|
57
|
+
#
|
58
|
+
# @return [ Hash<String, Array<Object, Object> ] The changes.
|
59
|
+
def changes
|
60
|
+
changed.each_with_object({}) do |attr, changes|
|
61
|
+
change = attribute_change(attr)
|
62
|
+
changes[attr] = change if change
|
63
|
+
end.with_indifferent_access
|
64
|
+
end
|
65
|
+
|
66
|
+
# Call this method after save, so the changes can be properly switched.
|
67
|
+
#
|
68
|
+
# This will unset the memoized children array, set new record flag to
|
69
|
+
# false, set the document as validated, and move the dirty changes.
|
70
|
+
#
|
71
|
+
# @example Move the changes to previous.
|
72
|
+
# person.move_changes
|
73
|
+
def move_changes
|
74
|
+
@changes_before_last_save = @previous_changes
|
75
|
+
@previous_changes = changes
|
76
|
+
@attributes_before_last_save = @previous_attributes
|
77
|
+
@previous_attributes = attributes.dup
|
78
|
+
changed_attributes.clear
|
79
|
+
end
|
80
|
+
|
81
|
+
def changes_applied
|
82
|
+
move_changes
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
def reset_object!
|
87
|
+
# @attributes = attributes
|
88
|
+
# @attributes_before_type_cast = @attributes.dup
|
89
|
+
@changed_attributes = {}
|
90
|
+
@previous_changes = {}
|
91
|
+
@previous_attributes = {}
|
92
|
+
end
|
93
|
+
# for AR compatibility
|
94
|
+
# TODO add coverage and move it in AR compat module
|
95
|
+
alias clear_changes_information reset_object!
|
96
|
+
|
97
|
+
# Get the previous changes on the document.
|
98
|
+
#
|
99
|
+
# @example Get the previous changes.
|
100
|
+
# model.previous_changes
|
101
|
+
#
|
102
|
+
# @return [ Hash<String, Array<Object, Object> ] The previous changes.
|
103
|
+
def previous_changes
|
104
|
+
@previous_changes ||= {}
|
105
|
+
end
|
106
|
+
|
107
|
+
# Gets all the new values for each of the changed fields, to be passed to
|
108
|
+
# a CouchbaseOrm $set modifier.
|
109
|
+
#
|
110
|
+
# @example Get the setters for the atomic updates.
|
111
|
+
# person = Person.new(:title => "Sir")
|
112
|
+
# person.title = "Madam"
|
113
|
+
# person.setters # returns { "title" => "Madam" }
|
114
|
+
#
|
115
|
+
# @return [ Hash ] A +Hash+ of atomic setters.
|
116
|
+
def setters
|
117
|
+
mods = {}
|
118
|
+
changes.each_pair do |name, changes|
|
119
|
+
next unless changes
|
120
|
+
|
121
|
+
old, new = changes
|
122
|
+
field = fields[name]
|
123
|
+
key = atomic_attribute_name(name)
|
124
|
+
if field&.resizable?
|
125
|
+
field.add_atomic_changes(self, name, key, mods, new, old)
|
126
|
+
else
|
127
|
+
mods[key] = new unless atomic_unsets.include?(key)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
mods
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns the original value of an attribute before the last save.
|
134
|
+
#
|
135
|
+
# This method is useful in after callbacks to get the original value of
|
136
|
+
# an attribute before the save that triggered the callbacks to run.
|
137
|
+
#
|
138
|
+
# @param [ Symbol | String ] attr The name of the attribute.
|
139
|
+
#
|
140
|
+
# @return [ Object ] Value of the attribute before the last save.
|
141
|
+
def attribute_before_last_save(attr)
|
142
|
+
attributes_before_last_save[attr]
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns the change to an attribute during the last save.
|
146
|
+
#
|
147
|
+
# @param [ Symbol | String ] attr The name of the attribute.
|
148
|
+
#
|
149
|
+
# @return [ Array<Object> | nil ] If the attribute was changed, returns
|
150
|
+
# an array containing the original value and the saved value, otherwise nil.
|
151
|
+
def saved_change_to_attribute(attr)
|
152
|
+
previous_changes[attr]
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns whether this attribute changed during the last save.
|
156
|
+
#
|
157
|
+
# This method is useful in after callbacks, to see the change
|
158
|
+
# in an attribute during the save that triggered the callbacks to run.
|
159
|
+
#
|
160
|
+
# @param [ String ] attr The name of the attribute.
|
161
|
+
# @param [ Object ] from The object the attribute was changed from (optional).
|
162
|
+
# @param [ Object ] to The object the attribute was changed to (optional).
|
163
|
+
#
|
164
|
+
# @return [ true | false ] Whether the attribute has changed during the last save.
|
165
|
+
def saved_change_to_attribute?(attr, from: Utils::PLACEHOLDER, to: Utils::PLACEHOLDER)
|
166
|
+
changes = saved_change_to_attribute(attr)
|
167
|
+
return false unless changes.is_a?(Array)
|
168
|
+
|
169
|
+
return true if Utils.placeholder?(from) && Utils.placeholder?(to)
|
170
|
+
return changes.first == from if Utils.placeholder?(to)
|
171
|
+
return changes.last == to if Utils.placeholder?(from)
|
172
|
+
|
173
|
+
changes.first == from && changes.last == to
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns whether this attribute change the next time we save.
|
177
|
+
#
|
178
|
+
# This method is useful in validations and before callbacks to determine
|
179
|
+
# if the next call to save will change a particular attribute.
|
180
|
+
#
|
181
|
+
# @param [ String ] attr The name of the attribute.
|
182
|
+
# @param **kwargs The optional keyword arguments.
|
183
|
+
#
|
184
|
+
# @option **kwargs [ Object ] :from The object the attribute was changed from.
|
185
|
+
# @option **kwargs [ Object ] :to The object the attribute was changed to.
|
186
|
+
#
|
187
|
+
# @return [ true | false ] Whether the attribute change the next time we save.
|
188
|
+
def will_save_change_to_attribute?(attr, **kwargs)
|
189
|
+
attribute_changed?(attr, **kwargs)
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
# Get attributes of the document before the document was saved.
|
195
|
+
#
|
196
|
+
# @return [ Hash ] Previous attributes
|
197
|
+
def previous_attributes
|
198
|
+
@previous_attributes ||= {}
|
199
|
+
end
|
200
|
+
|
201
|
+
def changes_before_last_save
|
202
|
+
@changes_before_last_save ||= {}
|
203
|
+
end
|
204
|
+
|
205
|
+
def attributes_before_last_save
|
206
|
+
@attributes_before_last_save ||= {}
|
207
|
+
end
|
208
|
+
|
209
|
+
# Get the old and new value for the provided attribute.
|
210
|
+
#
|
211
|
+
# @example Get the attribute change.
|
212
|
+
# model.attribute_change("name")
|
213
|
+
#
|
214
|
+
# @param [ String ] attr The name of the attribute.
|
215
|
+
#
|
216
|
+
# @return [ Array<Object> ] The old and new values.
|
217
|
+
def attribute_change(attr)
|
218
|
+
changed_attributes[attr] if attribute_changed?(attr)
|
219
|
+
end
|
220
|
+
|
221
|
+
# A class for representing the default value that an attribute was changed
|
222
|
+
# from or to.
|
223
|
+
#
|
224
|
+
# @api private
|
225
|
+
class Anything
|
226
|
+
# `Anything` objects are always equal to everything. This simplifies
|
227
|
+
# the logic for asking whether an attribute has changed or not. If the
|
228
|
+
# `from` or `to` value is a `Anything` (because it was not
|
229
|
+
# explicitly given), any comparison with it will suggest the value has
|
230
|
+
# not changed.
|
231
|
+
#
|
232
|
+
# @param [ Object ] _other The object being compared with this object.
|
233
|
+
#
|
234
|
+
# @return [ true ] Always returns true.
|
235
|
+
def ==(_other)
|
236
|
+
true
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# a singleton object to represent an optional `to` or `from` value
|
241
|
+
# that was not explicitly provided to #attribute_changed?
|
242
|
+
ATTRIBUTE_UNCHANGED = Anything.new
|
243
|
+
|
244
|
+
# Determine if a specific attribute has changed.
|
245
|
+
#
|
246
|
+
# @example Has the attribute changed?
|
247
|
+
# model.attribute_changed?("name")
|
248
|
+
#
|
249
|
+
# @param [ String ] attr The name of the attribute.
|
250
|
+
# @param [ Object ] from The object the attribute was changed from (optional).
|
251
|
+
# @param [ Object ] to The object the attribute was changed to (optional).
|
252
|
+
#
|
253
|
+
# @return [ true | false ] Whether the attribute has changed.
|
254
|
+
def attribute_changed?(attr, from: ATTRIBUTE_UNCHANGED, to: ATTRIBUTE_UNCHANGED)
|
255
|
+
return false unless changed_attributes.key?(attr)
|
256
|
+
return false if changed_attributes[attr] == attributes[attr]
|
257
|
+
return false if from != changed_attributes[attr]
|
258
|
+
return false if to != attributes[attr]
|
259
|
+
|
260
|
+
true
|
261
|
+
end
|
262
|
+
|
263
|
+
# Get whether or not the field has a different value from the default.
|
264
|
+
#
|
265
|
+
# @example Is the field different from the default?
|
266
|
+
# model.attribute_changed_from_default?
|
267
|
+
#
|
268
|
+
# @param [ String ] attr The name of the attribute.
|
269
|
+
#
|
270
|
+
# @return [ true | false ] If the attribute differs.
|
271
|
+
def attribute_changed_from_default?(attr)
|
272
|
+
return false unless (field = fields[attr])
|
273
|
+
|
274
|
+
attributes[attr] != field.eval_default(self)
|
275
|
+
end
|
276
|
+
|
277
|
+
# Get the previous value for the attribute.
|
278
|
+
#
|
279
|
+
# @example Get the previous value.
|
280
|
+
# model.attribute_was("name")
|
281
|
+
#
|
282
|
+
# @param [ String ] attr The attribute name.
|
283
|
+
def attribute_was(attr)
|
284
|
+
attribute_changed?(attr) ? changed_attributes[attr].first : attributes[attr]
|
285
|
+
end
|
286
|
+
|
287
|
+
# Get the previous attribute value that was changed
|
288
|
+
# before the document was saved.
|
289
|
+
#
|
290
|
+
# It the document has not been saved yet, or was just loaded from database,
|
291
|
+
# this method returns nil for all attributes.
|
292
|
+
#
|
293
|
+
# @param [ String ] attr The attribute name.
|
294
|
+
#
|
295
|
+
# @return [ Object | nil ] Attribute value before the document was saved,
|
296
|
+
# or nil if the document has not been saved yet.
|
297
|
+
def attribute_previously_was(attr)
|
298
|
+
if previous_changes.key?(attr)
|
299
|
+
previous_changes[attr].first
|
300
|
+
else
|
301
|
+
previous_attributes[attr]
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Flag an attribute as going to change.
|
306
|
+
#
|
307
|
+
# @example Flag the attribute.
|
308
|
+
# model.attribute_will_change!("name")
|
309
|
+
#
|
310
|
+
# @param [ String ] attr The name of the attribute.
|
311
|
+
#
|
312
|
+
# @return [ Object ] The old value.
|
313
|
+
def attribute_will_change!(attr)
|
314
|
+
return if changed_attributes.key?(attr)
|
315
|
+
|
316
|
+
changed_attributes[attr] = attributes[attr]&.__deep_copy__
|
317
|
+
end
|
318
|
+
|
319
|
+
# Set the attribute back to its old value.
|
320
|
+
#
|
321
|
+
# @example Reset the attribute.
|
322
|
+
# model.reset_attribute!("name")
|
323
|
+
#
|
324
|
+
# @param [ String ] attr The name of the attribute.
|
325
|
+
#
|
326
|
+
# @return [ Object ] The old value.
|
327
|
+
def reset_attribute!(attr)
|
328
|
+
attributes[attr] = changed_attributes.delete(attr) if attribute_changed?(attr)
|
329
|
+
end
|
330
|
+
|
331
|
+
def reset_attribute_to_default!(attr)
|
332
|
+
if (field = fields[attr])
|
333
|
+
__send__("#{attr}=", field.eval_default(self))
|
334
|
+
else
|
335
|
+
__send__("#{attr}=", nil)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def reset_attributes_before_type_cast
|
340
|
+
@attributes_before_type_cast = @attributes.dup
|
341
|
+
end
|
342
|
+
|
343
|
+
# Class-level methods for changeable objects.
|
344
|
+
module ClassMethods
|
345
|
+
private
|
346
|
+
|
347
|
+
# Generate all the dirty methods needed for the attribute.
|
348
|
+
#
|
349
|
+
# @example Generate the dirty methods.
|
350
|
+
# Model.create_dirty_methods("name", "name")
|
351
|
+
#
|
352
|
+
# @param [ String ] name The name of the field.
|
353
|
+
# @param [ String ] meth The name of the accessor.
|
354
|
+
#
|
355
|
+
# @return [ Module ] The fields module.
|
356
|
+
def create_dirty_methods(name, meth)
|
357
|
+
create_dirty_change_accessor(name, meth)
|
358
|
+
create_dirty_change_check(name, meth)
|
359
|
+
create_dirty_change_flag(name, meth)
|
360
|
+
create_dirty_default_change_check(name, meth)
|
361
|
+
create_dirty_previous_value_accessor(name, meth)
|
362
|
+
create_dirty_reset(name, meth)
|
363
|
+
create_dirty_reset_to_default(name, meth)
|
364
|
+
create_dirty_previously_changed?(name, meth)
|
365
|
+
create_dirty_previous_change(name, meth)
|
366
|
+
end
|
367
|
+
|
368
|
+
def create_setters(name)
|
369
|
+
define_method("#{name}=") do |new_attribute_value|
|
370
|
+
previous_value = attributes[name.to_s]
|
371
|
+
ret = super(new_attribute_value)
|
372
|
+
if previous_value != attributes[name.to_s]
|
373
|
+
changed_attributes.merge!(Hash[name, [previous_value, attributes[name.to_s]]])
|
374
|
+
end
|
375
|
+
ret
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# Creates the dirty change accessor.
|
380
|
+
#
|
381
|
+
# @example Create the accessor.
|
382
|
+
# Model.create_dirty_change_accessor("name", "alias")
|
383
|
+
#
|
384
|
+
# @param [ String ] name The attribute name.
|
385
|
+
# @param [ String ] meth The name of the accessor.
|
386
|
+
def create_dirty_change_accessor(name, meth)
|
387
|
+
define_method("#{meth}_change") do
|
388
|
+
attribute_change(name)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# Creates the dirty change check.
|
393
|
+
#
|
394
|
+
# @example Create the check.
|
395
|
+
# Model.create_dirty_change_check("name", "alias")
|
396
|
+
#
|
397
|
+
# @param [ String ] name The attribute name.
|
398
|
+
# @param [ String ] meth The name of the accessor.
|
399
|
+
def create_dirty_change_check(name, meth)
|
400
|
+
define_method("#{meth}_changed?") do |**kwargs|
|
401
|
+
attribute_changed?(name, **kwargs)
|
402
|
+
end
|
403
|
+
define_method("will_save_change_to_#{meth}?") do |**kwargs|
|
404
|
+
will_save_change_to_attribute?(name, **kwargs)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# Creates the dirty default change check.
|
409
|
+
#
|
410
|
+
# @example Create the check.
|
411
|
+
# Model.create_dirty_default_change_check("name", "alias")
|
412
|
+
#
|
413
|
+
# @param [ String ] name The attribute name.
|
414
|
+
# @param [ String ] meth The name of the accessor.
|
415
|
+
def create_dirty_default_change_check(name, meth)
|
416
|
+
define_method("#{meth}_changed_from_default?") do
|
417
|
+
attribute_changed_from_default?(name)
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# Creates the dirty change previous value accessors.
|
422
|
+
#
|
423
|
+
# @example Create the accessor.
|
424
|
+
# Model.create_dirty_previous_value_accessor("name", "alias")
|
425
|
+
#
|
426
|
+
# @param [ String ] name The attribute name.
|
427
|
+
# @param [ String ] meth The name of the accessor.
|
428
|
+
def create_dirty_previous_value_accessor(name, meth)
|
429
|
+
define_method("#{meth}_was") do
|
430
|
+
attribute_was(name)
|
431
|
+
end
|
432
|
+
define_method("#{meth}_previously_was") do
|
433
|
+
attribute_previously_was(name)
|
434
|
+
end
|
435
|
+
define_method("#{meth}_before_last_save") do
|
436
|
+
attribute_before_last_save(name)
|
437
|
+
end
|
438
|
+
define_method("saved_change_to_#{meth}") do
|
439
|
+
saved_change_to_attribute(name)
|
440
|
+
end
|
441
|
+
define_method("saved_change_to_#{meth}?") do |**kwargs|
|
442
|
+
saved_change_to_attribute?(name, **kwargs)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
# Creates the dirty change flag.
|
447
|
+
#
|
448
|
+
# @example Create the flag.
|
449
|
+
# Model.create_dirty_change_flag("name", "alias")
|
450
|
+
#
|
451
|
+
# @param [ String ] name The attribute name.
|
452
|
+
# @param [ String ] meth The name of the accessor.
|
453
|
+
def create_dirty_change_flag(name, meth)
|
454
|
+
define_method("#{meth}_will_change!") do
|
455
|
+
attribute_will_change!(name)
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
# Creates the dirty change reset.
|
460
|
+
#
|
461
|
+
# @example Create the reset.
|
462
|
+
# Model.create_dirty_reset("name", "alias")
|
463
|
+
#
|
464
|
+
# @param [ String ] name The attribute name.
|
465
|
+
# @param [ String ] meth The name of the accessor.
|
466
|
+
def create_dirty_reset(name, meth)
|
467
|
+
define_method("reset_#{meth}!") do
|
468
|
+
reset_attribute!(name)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# Creates the dirty change reset to default.
|
473
|
+
#
|
474
|
+
# @example Create the reset.
|
475
|
+
# Model.create_dirty_reset_to_default("name", "alias")
|
476
|
+
#
|
477
|
+
# @param [ String ] name The attribute name.
|
478
|
+
# @param [ String ] meth The name of the accessor.
|
479
|
+
def create_dirty_reset_to_default(name, meth)
|
480
|
+
define_method("reset_#{meth}_to_default!") do
|
481
|
+
reset_attribute_to_default!(name)
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
# Creates the dirty change check.
|
486
|
+
#
|
487
|
+
# @example Create the dirty change check.
|
488
|
+
# Model.create_dirty_previously_changed?("name", "alias")
|
489
|
+
#
|
490
|
+
# @param [ String ] name The attribute name.
|
491
|
+
# @param [ String ] meth The name of the accessor.
|
492
|
+
def create_dirty_previously_changed?(name, meth)
|
493
|
+
define_method("#{meth}_previously_changed?") do
|
494
|
+
previous_changes.key?(name)
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# Creates the dirty change accessor.
|
499
|
+
#
|
500
|
+
# @example Create the dirty change accessor.
|
501
|
+
# Model.create_dirty_previous_change("name", "alias")
|
502
|
+
#
|
503
|
+
# @param [ String ] name The attribute name.
|
504
|
+
# @param [ String ] meth The name of the accessor.
|
505
|
+
def create_dirty_previous_change(name, meth)
|
506
|
+
define_method("#{meth}_previous_change") do
|
507
|
+
previous_changes[name]
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
@@ -1,16 +1,36 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'libcouchbase'
|
1
|
+
require 'couchbase'
|
4
2
|
|
5
3
|
module CouchbaseOrm
|
6
4
|
class Connection
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
@@config = nil
|
6
|
+
def self.config
|
7
|
+
@@config || {
|
8
|
+
:connection_string => "couchbase://#{ENV['COUCHBASE_HOST'] || '127.0.0.1'}",
|
9
|
+
:username => ENV['COUCHBASE_USER'],
|
10
|
+
:password => ENV['COUCHBASE_PASSWORD'],
|
11
|
+
:bucket => ENV['COUCHBASE_BUCKET']
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.config=(config)
|
16
|
+
@@config = config
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.cluster
|
20
|
+
@cluster ||= begin
|
21
|
+
cb_config = Couchbase::Configuration.new
|
22
|
+
cb_config.connection_string = config[:connection_string] || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm connection string')
|
23
|
+
cb_config.username = config[:username] || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm username')
|
24
|
+
cb_config.password = config[:password] || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm password')
|
25
|
+
Couchbase::Cluster.connect(cb_config)
|
26
|
+
end
|
10
27
|
end
|
11
28
|
|
12
29
|
def self.bucket
|
13
|
-
@bucket ||=
|
30
|
+
@bucket ||= begin
|
31
|
+
bucket_name = config[:bucket] || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm bucket name')
|
32
|
+
cluster.bucket(bucket_name)
|
33
|
+
end
|
14
34
|
end
|
15
35
|
end
|
16
|
-
end
|
36
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true, encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
module CouchbaseOrm
|
4
|
+
module Encrypt
|
5
|
+
def encode_encrypted_attributes
|
6
|
+
attributes.map do |key, value|
|
7
|
+
type = self.class.attribute_types[key.to_s]
|
8
|
+
if type.is_a?(CouchbaseOrm::Types::Encrypted)
|
9
|
+
next unless value
|
10
|
+
raise "Can not serialize value #{value} of type '#{value.class}' for Tanker encrypted attribute" unless value.is_a?(String)
|
11
|
+
["encrypted$#{key}", {
|
12
|
+
alg: type.alg,
|
13
|
+
ciphertext: value
|
14
|
+
}]
|
15
|
+
else
|
16
|
+
[key,value]
|
17
|
+
end
|
18
|
+
end.compact.to_h
|
19
|
+
end
|
20
|
+
|
21
|
+
def decode_encrypted_attributes(attributes)
|
22
|
+
attributes.map do |key, value|
|
23
|
+
key = key.to_s
|
24
|
+
if key.start_with?('encrypted$')
|
25
|
+
key = key.gsub('encrypted$', '')
|
26
|
+
value = value.with_indifferent_access[:ciphertext]
|
27
|
+
end
|
28
|
+
[key, value]
|
29
|
+
end.to_h
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def to_json(*args, **kwargs)
|
34
|
+
as_json.to_json(*args, **kwargs)
|
35
|
+
end
|
36
|
+
|
37
|
+
def as_json(*args, **kwargs)
|
38
|
+
super(*args, **kwargs).map do |key, value|
|
39
|
+
type = self.class.attribute_types[key.to_s]
|
40
|
+
if type.is_a?(CouchbaseOrm::Types::Encrypted) && value
|
41
|
+
raise "Can not serialize value #{value} of type '#{value.class}' for encrypted attribute" unless value.is_a?(String)
|
42
|
+
end
|
43
|
+
[key, value]
|
44
|
+
end.to_h.with_indifferent_access
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
data/lib/couchbase-orm/error.rb
CHANGED
@@ -3,13 +3,28 @@
|
|
3
3
|
module CouchbaseOrm
|
4
4
|
class Error < ::StandardError
|
5
5
|
attr_reader :record
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(message = nil, record = nil)
|
8
8
|
@record = record
|
9
9
|
super(message)
|
10
10
|
end
|
11
11
|
|
12
|
-
class RecordInvalid < Error
|
12
|
+
class RecordInvalid < Error
|
13
|
+
def initialize(message = nil, record = nil)
|
14
|
+
if record
|
15
|
+
errors = record.errors.full_messages.join(", ")
|
16
|
+
message = I18n.t(
|
17
|
+
:"couchbase.#{record.class.design_document}.errors.messages.record_invalid",
|
18
|
+
errors: errors,
|
19
|
+
default: :"couchbase.errors.messages.record_invalid"
|
20
|
+
)
|
21
|
+
end
|
22
|
+
super(message, record)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
class TypeMismatchError < Error; end
|
13
26
|
class RecordExists < Error; end
|
27
|
+
class CouchbaseOrm::Error::EmptyNotAllowed < Error; end
|
14
28
|
end
|
29
|
+
class StrictLoadingViolationError < Error; end
|
15
30
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CouchbaseOrm
|
4
|
+
|
5
|
+
# Contains the behavior around inspecting documents via inspect.
|
6
|
+
module Inspectable
|
7
|
+
|
8
|
+
# Returns the class name plus its attributes.
|
9
|
+
#
|
10
|
+
# @example Inspect the document.
|
11
|
+
# person.inspect
|
12
|
+
#
|
13
|
+
# @return [ String ] A nice pretty string to look at.
|
14
|
+
def inspect
|
15
|
+
inspection = []
|
16
|
+
inspection.concat(inspect_attributes)
|
17
|
+
"#<#{self.class.name} id: #{respond_to?(:id)? id.inspect : 'no id'}, #{inspection * ', '}>"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Get an array of inspected fields for the document.
|
23
|
+
#
|
24
|
+
# @api private
|
25
|
+
#
|
26
|
+
# @example Inspect the defined fields.
|
27
|
+
# document.inspect_attributes
|
28
|
+
#
|
29
|
+
# @return [ String ] An array of pretty printed field values.
|
30
|
+
def inspect_attributes
|
31
|
+
attributes.map do |name, value|
|
32
|
+
next if name.to_s == "id"
|
33
|
+
"#{name}: #{value.inspect}"
|
34
|
+
end.compact
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CouchbaseOrm
|
4
|
+
module JsonSchema
|
5
|
+
class JsonValidationError < StandardError
|
6
|
+
|
7
|
+
def initialize(class_name, errors)
|
8
|
+
super("[COUCHBASEORM]: Invalid document #{class_name} with errors : #{errors}")
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|