couchbase-orm 3.1.0 → 3.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +28 -0
- data/.github/workflows/test.yml +4 -12
- data/README.md +27 -1
- data/RELEASE.md +6 -0
- data/couchbase-orm.gemspec +1 -1
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/03-defining-models.md +39 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/07-sqlpp-queries.md +41 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/09-nested-documents.md +19 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/11-encryption.md +11 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/12-logging.md +14 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/13-troubleshooting.md +2 -0
- data/lib/couchbase-orm/active_record_compat.rb +0 -26
- data/lib/couchbase-orm/base.rb +3 -0
- data/lib/couchbase-orm/n1ql.rb +9 -4
- data/lib/couchbase-orm/persistence.rb +4 -0
- data/lib/couchbase-orm/proxies/bucket_proxy.rb +2 -8
- data/lib/couchbase-orm/relation.rb +9 -4
- data/lib/couchbase-orm/types.rb +0 -7
- data/lib/couchbase-orm/unknown_attributes.rb +82 -0
- data/lib/couchbase-orm/version.rb +1 -1
- data/spec/base_spec.rb +137 -0
- data/spec/couchbase-orm/active_record_compat_spec.rb +73 -0
- data/spec/couchbase-orm/changeable_spec.rb +37 -0
- data/spec/n1ql_spec.rb +19 -0
- data/spec/proxies/bucket_proxy_spec.rb +53 -0
- data/spec/relation_spec.rb +33 -0
- data/spec/type_encrypted_spec.rb +20 -0
- data/spec/type_nested_spec.rb +92 -0
- data/spec/unknown_attributes_spec.rb +227 -0
- data/spec/utilities/ignored_properties_spec.rb +17 -0
- metadata +9 -3
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true, encoding: ASCII-8BIT
|
|
2
|
+
|
|
3
|
+
require File.expand_path("../support", __FILE__)
|
|
4
|
+
|
|
5
|
+
# A minimal stand-in for ActionController::Parameters (each_pair/each/except,
|
|
6
|
+
# no #to_hash). Real Parameters would work too, but pulling in actionpack
|
|
7
|
+
# here would make this spec's outcome depend on whatever version bundler
|
|
8
|
+
# happens to resolve it to; what actually matters is that the filter is
|
|
9
|
+
# guarded by `respond_to?(:each_pair)`, not `is_a?(Hash)`.
|
|
10
|
+
#
|
|
11
|
+
# `each` is required too, not just `each_pair`: ActiveModel::AttributeAssignment's
|
|
12
|
+
# own `_assign_attributes` (which our `super` eventually reaches) iterates via
|
|
13
|
+
# `.each` on ActiveModel < 8.0, and via `.each_pair` on 8.0+.
|
|
14
|
+
class EachPairOnly
|
|
15
|
+
def initialize(hash)
|
|
16
|
+
@hash = hash
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each_pair(&block)
|
|
20
|
+
@hash.each_pair(&block)
|
|
21
|
+
end
|
|
22
|
+
alias_method :each, :each_pair
|
|
23
|
+
|
|
24
|
+
def except(*keys)
|
|
25
|
+
EachPairOnly.new(@hash.except(*keys))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def empty?
|
|
29
|
+
@hash.empty?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class UnknownAttrsStrict < CouchbaseOrm::Base
|
|
34
|
+
attribute :name, :string
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class UnknownAttrsTolerant < CouchbaseOrm::Base
|
|
38
|
+
self.raise_on_unknown_attributes = false
|
|
39
|
+
attribute :name, :string
|
|
40
|
+
attribute :secret, :encrypted
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class UnknownAttrsTolerantChild < UnknownAttrsTolerant
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class UnknownAttrsBoth < CouchbaseOrm::Base
|
|
47
|
+
self.raise_on_unknown_attributes = false
|
|
48
|
+
self.ignored_properties = [:legacy_ignored]
|
|
49
|
+
attribute :name, :string
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class UnknownAttrsAssocParent < CouchbaseOrm::Base
|
|
53
|
+
attribute :name, :string
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class UnknownAttrsAssocChild < CouchbaseOrm::Base
|
|
57
|
+
self.raise_on_unknown_attributes = false
|
|
58
|
+
attribute :name, :string
|
|
59
|
+
belongs_to :unknown_attrs_assoc_parent, dependent: :destroy
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
RSpec.describe "raise_on_unknown_attributes" do
|
|
63
|
+
# No public reset API on purpose (it would be test-only surface on
|
|
64
|
+
# production code) - reach into the module's own warn-once tracking
|
|
65
|
+
# directly so these examples don't depend on run order.
|
|
66
|
+
before { CouchbaseOrm::UnknownAttributes.instance_variable_get(:@warned).clear }
|
|
67
|
+
|
|
68
|
+
describe "defaults" do
|
|
69
|
+
it "defaults to true" do
|
|
70
|
+
expect(CouchbaseOrm::Base.raise_on_unknown_attributes).to be true
|
|
71
|
+
expect(UnknownAttrsStrict.raise_on_unknown_attributes).to be true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "still raises ActiveModel::UnknownAttributeError when true" do
|
|
75
|
+
expect { UnknownAttrsStrict.new(name: 'bob', nope: 'x') }.to raise_error(ActiveModel::UnknownAttributeError)
|
|
76
|
+
model = UnknownAttrsStrict.new(name: 'bob')
|
|
77
|
+
expect { model.assign_attributes(nope: 'x') }.to raise_error(ActiveModel::UnknownAttributeError)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "inheritance" do
|
|
82
|
+
it "is inherited by subclasses, unlike ignored_properties" do
|
|
83
|
+
expect(UnknownAttrsTolerantChild.raise_on_unknown_attributes).to be false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "does not leak from a subclass back to its parent or to Base" do
|
|
87
|
+
expect(UnknownAttrsTolerant.raise_on_unknown_attributes).to be false
|
|
88
|
+
expect(CouchbaseOrm::Base.raise_on_unknown_attributes).to be true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe "association writers are not treated as unknown attributes" do
|
|
93
|
+
it "still resolves belongs_to via its writer, not via attribute_names" do
|
|
94
|
+
parent = UnknownAttrsAssocParent.create!(name: 'parent')
|
|
95
|
+
child = UnknownAttrsAssocChild.new(name: 'child', unknown_attrs_assoc_parent: parent, really_unknown: 'x')
|
|
96
|
+
|
|
97
|
+
expect(child.unknown_attrs_assoc_parent_id).to eq(parent.id)
|
|
98
|
+
expect(child.respond_to?(:really_unknown)).to be false
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe "given a document with an unknown property" do
|
|
103
|
+
let(:doc_id) { "unknown_attrs_tolerant_#{SecureRandom.hex}" }
|
|
104
|
+
let(:document_properties) do
|
|
105
|
+
{
|
|
106
|
+
'type' => UnknownAttrsTolerant.design_document,
|
|
107
|
+
'name' => 'Alice',
|
|
108
|
+
'server_encrypted_content' => 'not yet known to this code'
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
before { UnknownAttrsTolerant.bucket.default_collection.upsert doc_id, document_properties }
|
|
113
|
+
after { UnknownAttrsTolerant.bucket.default_collection.remove doc_id }
|
|
114
|
+
|
|
115
|
+
it "does not raise on find (the INC-5405 path) and drops the unknown key" do
|
|
116
|
+
model = UnknownAttrsTolerant.find(doc_id)
|
|
117
|
+
expect(model.name).to eq('Alice')
|
|
118
|
+
expect(model.attributes.keys).not_to include('server_encrypted_content')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "raises on find when raise_on_unknown_attributes is true" do
|
|
122
|
+
document_properties['type'] = UnknownAttrsStrict.design_document
|
|
123
|
+
UnknownAttrsStrict.bucket.default_collection.upsert doc_id, document_properties
|
|
124
|
+
expect { UnknownAttrsStrict.find(doc_id) }.to raise_error(ActiveModel::UnknownAttributeError)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "does not raise on reload" do
|
|
128
|
+
model = UnknownAttrsTolerant.find(doc_id)
|
|
129
|
+
expect { model.reload }.not_to raise_error
|
|
130
|
+
expect(model.attributes.keys).not_to include('server_encrypted_content')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "logs a warning exactly once per (class, key), and debug on every call" do
|
|
134
|
+
allow(CouchbaseOrm.logger).to receive(:debug).and_call_original
|
|
135
|
+
expect(CouchbaseOrm.logger).to receive(:warn).once.with(a_string_including('server_encrypted_content'))
|
|
136
|
+
|
|
137
|
+
UnknownAttrsTolerant.find(doc_id)
|
|
138
|
+
UnknownAttrsTolerant.find(doc_id)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "emits no warning at all when raise_on_unknown_attributes is true" do
|
|
142
|
+
document_properties['type'] = UnknownAttrsStrict.design_document
|
|
143
|
+
UnknownAttrsStrict.bucket.default_collection.upsert doc_id, document_properties
|
|
144
|
+
expect(CouchbaseOrm.logger).not_to receive(:warn)
|
|
145
|
+
expect { UnknownAttrsStrict.find(doc_id) }.to raise_error(ActiveModel::UnknownAttributeError)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "drops the unknown key from the stored document on the next save (same trade-off as ignored_properties)" do
|
|
149
|
+
model = UnknownAttrsTolerant.find(doc_id)
|
|
150
|
+
model.name = 'Alice Updated'
|
|
151
|
+
expect { model.save! }.to change {
|
|
152
|
+
UnknownAttrsTolerant.bucket.default_collection.get(doc_id).content.keys.sort
|
|
153
|
+
}.from(%w[name server_encrypted_content type]).to(%w[name type])
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe "interaction with encrypted$-prefixed keys" do
|
|
158
|
+
it "tolerates an unknown key that only happens to carry the encrypted$ prefix" do
|
|
159
|
+
doc_id = "unknown_attrs_tolerant_enc_#{SecureRandom.hex}"
|
|
160
|
+
UnknownAttrsTolerant.bucket.default_collection.upsert doc_id, {
|
|
161
|
+
'type' => UnknownAttrsTolerant.design_document,
|
|
162
|
+
'name' => 'Bob',
|
|
163
|
+
'encrypted$tanker_encrypted_content' => { 'alg' => 'tanker', 'ciphertext' => 'legacy' }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
model = UnknownAttrsTolerant.find(doc_id)
|
|
167
|
+
expect(model.name).to eq('Bob')
|
|
168
|
+
expect(model.attributes.keys).not_to include('tanker_encrypted_content', 'encrypted$tanker_encrypted_content')
|
|
169
|
+
ensure
|
|
170
|
+
UnknownAttrsTolerant.bucket.default_collection.remove doc_id
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe "combined with ignored_properties" do
|
|
175
|
+
let(:doc_id) { "unknown_attrs_both_#{SecureRandom.hex}" }
|
|
176
|
+
|
|
177
|
+
before do
|
|
178
|
+
UnknownAttrsBoth.bucket.default_collection.upsert doc_id, {
|
|
179
|
+
'type' => UnknownAttrsBoth.design_document,
|
|
180
|
+
'name' => 'Carol',
|
|
181
|
+
'legacy_ignored' => 'handled by ignored_properties',
|
|
182
|
+
'other_unknown' => 'handled by the flag'
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
after { UnknownAttrsBoth.bucket.default_collection.remove doc_id }
|
|
186
|
+
|
|
187
|
+
it "tolerates both without raising, and only warns about the one the flag handled" do
|
|
188
|
+
expect(CouchbaseOrm.logger).to receive(:warn).once.with(a_string_including('other_unknown'))
|
|
189
|
+
model = UnknownAttrsBoth.find(doc_id)
|
|
190
|
+
expect(model.name).to eq('Carol')
|
|
191
|
+
expect(model.attributes.keys).not_to include('legacy_ignored', 'other_unknown')
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
describe "duck-typed, non-Hash input (e.g. ActionController::Parameters)" do
|
|
196
|
+
it "filters through a respond_to?(:each_pair) object just like a Hash" do
|
|
197
|
+
# Calling assign_attributes directly (rather than .new) matters here:
|
|
198
|
+
# Document#initialize's generic branch runs every non-GetResult,
|
|
199
|
+
# non-CouchbaseOrm::Base argument through Kernel#Hash(), which would
|
|
200
|
+
# coerce a real permitted ActionController::Parameters (it defines
|
|
201
|
+
# #to_hash) before assign_attributes ever saw it - masking exactly
|
|
202
|
+
# the guard this test means to exercise.
|
|
203
|
+
model = UnknownAttrsTolerant.new(name: 'Dave')
|
|
204
|
+
model.assign_attributes(EachPairOnly.new(name: 'Dave Updated', nope: 'x'))
|
|
205
|
+
expect(model.name).to eq('Dave Updated')
|
|
206
|
+
expect(model.respond_to?(:nope)).to be false
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it "still raises for a non-Hash, non-each_pair argument, same as before this feature existed" do
|
|
210
|
+
# Persistence#assign_attributes (unrelated to raise_on_unknown_attributes,
|
|
211
|
+
# pre-existing on CouchbaseOrm::Base) calls `hash.except("type")`
|
|
212
|
+
# unconditionally, so on Base this is a NoMethodError rather than the
|
|
213
|
+
# ArgumentError plain ActiveModel would raise. Either way, it must not
|
|
214
|
+
# be silently swallowed by the unknown-attributes filter.
|
|
215
|
+
expect { UnknownAttrsTolerant.new.assign_attributes(Object.new) }.to raise_error(StandardError)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
describe "attributes=" do
|
|
220
|
+
it "goes through the same filtering as assign_attributes" do
|
|
221
|
+
model = UnknownAttrsTolerant.new(name: 'Eve')
|
|
222
|
+
model.attributes = { name: 'Eve Updated', nope: 'x' }
|
|
223
|
+
expect(model.name).to eq('Eve Updated')
|
|
224
|
+
expect(model.respond_to?(:nope)).to be false
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'couchbase-orm'
|
|
1
2
|
require 'couchbase-orm/utilities/ignored_properties'
|
|
2
3
|
|
|
3
4
|
class DummyClass
|
|
@@ -17,4 +18,20 @@ RSpec.describe CouchbaseOrm::IgnoredProperties do
|
|
|
17
18
|
expect(DummyClass2.ignored_properties).to be_empty
|
|
18
19
|
end
|
|
19
20
|
end
|
|
21
|
+
|
|
22
|
+
describe '#ignored_properties' do
|
|
23
|
+
# A fresh class per example, so the deprecated-setter test below can't
|
|
24
|
+
# leak state into (or depend on run order with) any other example.
|
|
25
|
+
let(:klass) { Class.new { extend CouchbaseOrm::IgnoredProperties } }
|
|
26
|
+
|
|
27
|
+
it 'defaults to an empty array' do
|
|
28
|
+
expect(klass.ignored_properties).to eq([])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'is deprecated as a setter and warns when called with arguments' do
|
|
32
|
+
expect(CouchbaseOrm.logger).to receive(:warn).with(a_string_including('deprecated'))
|
|
33
|
+
klass.ignored_properties(:legacy_property)
|
|
34
|
+
expect(klass.ignored_properties).to eq(['legacy_property'])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
20
37
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: couchbase-orm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen von Takach
|
|
@@ -11,7 +11,7 @@ authors:
|
|
|
11
11
|
autorequire:
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date: 2026-
|
|
14
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: activemodel
|
|
@@ -216,6 +216,7 @@ extensions: []
|
|
|
216
216
|
extra_rdoc_files: []
|
|
217
217
|
files:
|
|
218
218
|
- ".github/workflows/deploy.yml"
|
|
219
|
+
- ".github/workflows/release.yml"
|
|
219
220
|
- ".github/workflows/test.yml"
|
|
220
221
|
- ".gitignore"
|
|
221
222
|
- ".rspec"
|
|
@@ -299,6 +300,7 @@ files:
|
|
|
299
300
|
- lib/couchbase-orm/types/encrypted.rb
|
|
300
301
|
- lib/couchbase-orm/types/nested.rb
|
|
301
302
|
- lib/couchbase-orm/types/timestamp.rb
|
|
303
|
+
- lib/couchbase-orm/unknown_attributes.rb
|
|
302
304
|
- lib/couchbase-orm/utilities/ensure_unique.rb
|
|
303
305
|
- lib/couchbase-orm/utilities/enum.rb
|
|
304
306
|
- lib/couchbase-orm/utilities/has_many.rb
|
|
@@ -336,6 +338,7 @@ files:
|
|
|
336
338
|
- spec/json_schema_spec.rb
|
|
337
339
|
- spec/n1ql_spec.rb
|
|
338
340
|
- spec/persistence_spec.rb
|
|
341
|
+
- spec/proxies/bucket_proxy_spec.rb
|
|
339
342
|
- spec/relation_nested_spec.rb
|
|
340
343
|
- spec/relation_spec.rb
|
|
341
344
|
- spec/support.rb
|
|
@@ -343,6 +346,7 @@ files:
|
|
|
343
346
|
- spec/type_encrypted_spec.rb
|
|
344
347
|
- spec/type_nested_spec.rb
|
|
345
348
|
- spec/type_spec.rb
|
|
349
|
+
- spec/unknown_attributes_spec.rb
|
|
346
350
|
- spec/utilities/ignored_properties_spec.rb
|
|
347
351
|
- spec/utilities/properties_always_exists_in_document_spec.rb
|
|
348
352
|
- spec/views_spec.rb
|
|
@@ -362,7 +366,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
362
366
|
requirements:
|
|
363
367
|
- - ">="
|
|
364
368
|
- !ruby/object:Gem::Version
|
|
365
|
-
version: 3.
|
|
369
|
+
version: 3.3.0
|
|
366
370
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
367
371
|
requirements:
|
|
368
372
|
- - ">="
|
|
@@ -396,6 +400,7 @@ test_files:
|
|
|
396
400
|
- spec/json_schema_spec.rb
|
|
397
401
|
- spec/n1ql_spec.rb
|
|
398
402
|
- spec/persistence_spec.rb
|
|
403
|
+
- spec/proxies/bucket_proxy_spec.rb
|
|
399
404
|
- spec/relation_nested_spec.rb
|
|
400
405
|
- spec/relation_spec.rb
|
|
401
406
|
- spec/support.rb
|
|
@@ -403,6 +408,7 @@ test_files:
|
|
|
403
408
|
- spec/type_encrypted_spec.rb
|
|
404
409
|
- spec/type_nested_spec.rb
|
|
405
410
|
- spec/type_spec.rb
|
|
411
|
+
- spec/unknown_attributes_spec.rb
|
|
406
412
|
- spec/utilities/ignored_properties_spec.rb
|
|
407
413
|
- spec/utilities/properties_always_exists_in_document_spec.rb
|
|
408
414
|
- spec/views_spec.rb
|