couchbase-orm 3.2.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/README.md +27 -1
- data/RELEASE.md +6 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/03-defining-models.md +39 -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/persistence.rb +4 -0
- data/lib/couchbase-orm/proxies/bucket_proxy.rb +2 -8
- 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/proxies/bucket_proxy_spec.rb +53 -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 +13 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90eb0b8fb472134579071ce58442b518bd42009d3647363116f041eb65b13a85
|
|
4
|
+
data.tar.gz: 066cad6c6ee614382262d035d8d86e55e8d829f42733d21daddd9a91deb03786
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2cfd5683e131c28d10499da7c42ab2000688bed5dc5382c9b7a2beab99cb8b142da16b9327966636777ca32c34bee101185c1fb8a8b1862203339b5a27adc24c
|
|
7
|
+
data.tar.gz: 393a5523c5a5ffb2154b8ac426e199179cebc48356fd32a7170ad6c1eed7a6984a56c4990e3e0601d88710071c22b50b8ad87c41a067b1dc5b4bdd0fec29a281
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- '[0-9]+.[0-9]+.[0-9]+'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
github-release:
|
|
13
|
+
runs-on: ubuntu-24.04
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v3
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
- name: Verify tag is on master
|
|
19
|
+
run: |
|
|
20
|
+
git fetch origin master
|
|
21
|
+
if ! git merge-base --is-ancestor "${{ github.sha }}" origin/master; then
|
|
22
|
+
echo "::error::Tag ${{ github.ref_name }} (${{ github.sha }}) is not an ancestor of origin/master. Refusing to create a GitHub Release for a commit that hasn't been merged to master."
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
- name: Create GitHub Release
|
|
26
|
+
env:
|
|
27
|
+
GH_TOKEN: ${{ github.token }}
|
|
28
|
+
run: gh release create "${{ github.ref_name }}" --title "${{ github.ref_name }}" --generate-notes
|
data/README.md
CHANGED
|
@@ -195,13 +195,39 @@ Check this couchbase help page to learn more on what's possible with compound ke
|
|
|
195
195
|
Ex : Compound keys allows to decide the order of the results, and you can reverse it by passing `descending: true`
|
|
196
196
|
|
|
197
197
|
```ruby
|
|
198
|
-
class Comment < CouchbaseOrm::
|
|
198
|
+
class Comment < CouchbaseOrm::Base
|
|
199
199
|
self.ignored_properties = [:old_name] # ignore old_name property in the model
|
|
200
200
|
self.properties_always_exists_in_document = true # use is null for nil value instead of not valued for performance purpose, only possible if all properties always exists in document
|
|
201
201
|
end
|
|
202
202
|
```
|
|
203
203
|
You can specify `properties_always_exists_in_document` to true if all properties always exists in document, this will allow to use `is null` instead of `not valued` for nil value, this will improve performance.
|
|
204
204
|
|
|
205
|
+
## Schema evolution
|
|
206
|
+
|
|
207
|
+
`ignored_properties` (above) drops a named, fixed list of legacy keys wherever a document is
|
|
208
|
+
decoded. For the more general case of a rolling/canary deploy - where a document written by a pod
|
|
209
|
+
running newer code can carry an attribute a pod still running older code hasn't declared yet -
|
|
210
|
+
there is `raise_on_unknown_attributes`:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
class Comment < CouchbaseOrm::Base
|
|
214
|
+
self.raise_on_unknown_attributes = false # tolerate any undeclared document key instead of raising
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
With the default (`true`), an undeclared key anywhere in `new`, `assign_attributes`, `find` or
|
|
219
|
+
`reload` raises `ActiveModel::UnknownAttributeError`, same as plain ActiveModel. Set it to `false`
|
|
220
|
+
and undeclared keys are dropped instead, with a warning logged the first time each one is seen on
|
|
221
|
+
a given class. Unlike `ignored_properties`, it is a `class_attribute`, so it is inherited by
|
|
222
|
+
subclasses and can be set once for a whole hierarchy (e.g. on your app's own base class) or
|
|
223
|
+
globally via `CouchbaseOrm::Document.raise_on_unknown_attributes = false`.
|
|
224
|
+
|
|
225
|
+
Because a dropped key was never assigned to an attribute, it is not written back out either: if
|
|
226
|
+
the model is saved afterwards, the key disappears from the stored document. If you need to
|
|
227
|
+
preserve an unknown key across a load-then-save round trip rather than only tolerate it on load,
|
|
228
|
+
`ignored_properties` won't help with that either - both mechanisms are read-side tolerance, not a
|
|
229
|
+
schemaless passthrough.
|
|
230
|
+
|
|
205
231
|
WARNING: If a document exists without a property, the query will failed! So you must be sure that all documents have all properties.
|
|
206
232
|
|
|
207
233
|
|
data/RELEASE.md
CHANGED
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
git tag X.Y.Z
|
|
35
35
|
git push origin X.Y.Z
|
|
36
36
|
|
|
37
|
+
Pushing the tag automatically triggers the `Release` GitHub Actions workflow, which creates the
|
|
38
|
+
corresponding GitHub Release (with auto-generated notes) once it confirms the tag's commit is on
|
|
39
|
+
`master`.
|
|
40
|
+
|
|
37
41
|
3. Verify Clean State
|
|
38
42
|
|
|
39
43
|
Ensure your local master branch is clean and up-to-date:
|
|
@@ -66,6 +70,8 @@
|
|
|
66
70
|
|
|
67
71
|
Post-Release
|
|
68
72
|
|
|
73
|
+
- Verify the GitHub Release was created automatically at
|
|
74
|
+
https://github.com/Couchbase-Ecosystem/couchbase-ruby-orm/releases/tag/X.Y.Z
|
|
69
75
|
- Announce the release if applicable (changelog, team communication, etc.)
|
|
70
76
|
- Update any documentation that references the version number
|
|
71
77
|
|
|
@@ -236,4 +236,43 @@ Validations are automatically run when saving a document. If any validations fai
|
|
|
236
236
|
|
|
237
237
|
By leveraging validations, you can ensure the quality and consistency of your data before it is persisted to Couchbase Server.
|
|
238
238
|
|
|
239
|
+
## 3.7. Handling Unknown Document Properties
|
|
240
|
+
|
|
241
|
+
By default, CouchbaseOrm requires every property found on a document to have a matching declared
|
|
242
|
+
attribute: loading a document with an undeclared key raises `ActiveModel::UnknownAttributeError`.
|
|
243
|
+
This is normally what you want, but it becomes a problem during a rolling or canary deploy: a pod
|
|
244
|
+
running newer code can write a document with an attribute that a pod still running older code
|
|
245
|
+
hasn't declared yet, and that older pod will raise the moment it tries to read the document back.
|
|
246
|
+
|
|
247
|
+
Set `raise_on_unknown_attributes` to `false` on a model to tolerate this instead:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
class User < CouchbaseOrm::Base
|
|
251
|
+
self.raise_on_unknown_attributes = false
|
|
252
|
+
|
|
253
|
+
attribute :name, :string
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
User.find(id) # a document with an extra, undeclared key no longer raises;
|
|
257
|
+
# the key is simply dropped and a warning is logged.
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
A few things worth knowing:
|
|
261
|
+
|
|
262
|
+
- It applies everywhere a document (or a hash) is turned into attributes: `new`, `assign_attributes`,
|
|
263
|
+
`find`, and `reload`.
|
|
264
|
+
- It is a `class_attribute`, so unlike `ignored_properties` it is **inherited** by subclasses, and it
|
|
265
|
+
can be set once for a whole hierarchy - for example on your application's own base class - rather
|
|
266
|
+
than repeated per model. You can also set a global default with
|
|
267
|
+
`CouchbaseOrm::Document.raise_on_unknown_attributes = false` from a Rails initializer.
|
|
268
|
+
- A key is considered "known" if the model responds to the corresponding writer, not just if it is a
|
|
269
|
+
declared `attribute` - so a `belongs_to`/`has_and_belongs_to_many` association key (e.g. `parent:`)
|
|
270
|
+
is never treated as unknown, even though its underlying attribute is actually named `parent_id`.
|
|
271
|
+
- A dropped key is not written back out: since it was never assigned to an attribute, saving the
|
|
272
|
+
model afterwards omits it from the stored document, the same trade-off `ignored_properties` makes.
|
|
273
|
+
- Unlike `ignored_properties`, which removes a fixed, named list of keys wherever a document is
|
|
274
|
+
decoded, `raise_on_unknown_attributes` tolerates *any* undeclared key. Use `ignored_properties`
|
|
275
|
+
when you know exactly which legacy keys you're phasing out; use `raise_on_unknown_attributes` when
|
|
276
|
+
you want a model to be structurally resilient to future attributes it doesn't know about yet.
|
|
277
|
+
|
|
239
278
|
With the model definition covered, including attributes, callbacks, and validations, you're ready to start querying and persisting data using CouchbaseOrm. In the next section, we'll explore the querying capabilities of CouchbaseOrm and how to retrieve data from Couchbase Server efficiently.
|
|
@@ -135,4 +135,23 @@ Nested documents provide a powerful way to model complex data structures and rel
|
|
|
135
135
|
|
|
136
136
|
However, it's important to consider the trade-offs when using nested documents. Embedding too much data within a single document can lead to large document sizes and potential performance issues. It's recommended to use nested documents judiciously and to consider the access patterns and data relationships of your application.
|
|
137
137
|
|
|
138
|
+
## 9.8. Unknown Properties on Nested Documents
|
|
139
|
+
|
|
140
|
+
`raise_on_unknown_attributes` (see [3.7](./03-defining-models.md#37-handling-unknown-document-properties))
|
|
141
|
+
applies to `CouchbaseOrm::NestedDocument` the same way it does to `CouchbaseOrm::Base`, but the
|
|
142
|
+
setting is per nested class - it is not inherited through composition from the parent document.
|
|
143
|
+
A tolerant parent embedding a strict nested class still raises if that nested document carries an
|
|
144
|
+
undeclared property, and vice versa:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
class Part < CouchbaseOrm::NestedDocument
|
|
148
|
+
self.raise_on_unknown_attributes = false # only Part tolerates unknown properties
|
|
149
|
+
attribute :name, :string
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class Car < CouchbaseOrm::Base
|
|
153
|
+
attribute :parts, :array, type: Part # Car's own setting (default: true) is unaffected
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
138
157
|
In the next section, we'll explore enums in CouchbaseOrm and how they can be used to define a fixed set of values for an attribute.
|
|
@@ -141,6 +141,17 @@ CouchbaseOrm handles the storage format for encrypted attributes but does not pe
|
|
|
141
141
|
- All actual encryption/decryption is your application's responsibility
|
|
142
142
|
- Values must be valid Base64-encoded strings
|
|
143
143
|
|
|
144
|
+
The `encrypted$` unwrapping in step 2 above happens for **any** key with that prefix, whether or
|
|
145
|
+
not it maps to a declared `:encrypted` attribute. This means that if you are retiring an encrypted
|
|
146
|
+
attribute with [`raise_on_unknown_attributes = false`](./03-defining-models.md#37-handling-unknown-document-properties)
|
|
147
|
+
instead of `ignored_properties`, a single rule covers both `legacy_field` and
|
|
148
|
+
`encrypted$legacy_field` in the underlying document - you don't need to list both spellings.
|
|
149
|
+
One case only `ignored_properties` can handle: if an `encrypted$`-prefixed key's value is not a
|
|
150
|
+
`{alg:, ciphertext:}`-shaped hash (e.g. it was left over from a different serialization format),
|
|
151
|
+
unwrapping raises before `raise_on_unknown_attributes` ever gets a chance to filter it - only
|
|
152
|
+
naming the exact key with `ignored_properties`, which strips it before unwrapping is attempted,
|
|
153
|
+
handles that case.
|
|
154
|
+
|
|
144
155
|
## 11.4. Considerations and Best Practices
|
|
145
156
|
|
|
146
157
|
When using encrypted attributes in CouchbaseOrm, consider the following best practices:
|
|
@@ -46,3 +46,17 @@ D, [2024-05-24T11:48:00.113166 #234447] DEBUG -- : _update_record - replace user
|
|
|
46
46
|
I, [2024-05-24T11:48:00.115239 #234447] INFO -- : User user-1-vncZNSYZj updated email to john.doe@example.com
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## 12.2. Unknown Attribute Warnings
|
|
50
|
+
|
|
51
|
+
When a model has [`raise_on_unknown_attributes`](./03-defining-models.md#37-handling-unknown-document-properties)
|
|
52
|
+
set to `false`, every `assign_attributes` call (from `new`, `find`, `reload`, ...) that drops an
|
|
53
|
+
undeclared key logs at `DEBUG` level. To avoid flooding your logs on high-traffic models, a `WARN`
|
|
54
|
+
is only emitted the first time a given (model class, property name) pair is seen in the process:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
D, [...] DEBUG -- : User: ignoring unknown properties ["legacy_field"]
|
|
58
|
+
W, [...] WARN -- : User: ignoring unknown document properties legacy_field (raise_on_unknown_attributes is false for this class - they will not be persisted if the document is saved)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That warning tracking is capped so it cannot grow unbounded.
|
|
62
|
+
|
|
@@ -16,6 +16,8 @@ Here are some common issues you may encounter while using CouchbaseOrm:
|
|
|
16
16
|
|
|
17
17
|
5. **Unexpected Behavior**: If you experience unexpected behavior or results, double-check your code logic, query conditions, and attribute assignments. Ensure that you are using the correct methods, parameters, and data types.
|
|
18
18
|
|
|
19
|
+
6. **`ActiveModel::UnknownAttributeError` right after a deploy**: This typically happens during a rolling or canary deploy, when a document written by a pod running newer code carries an attribute a pod still running older code hasn't declared yet. See [Handling Unknown Document Properties](./03-defining-models.md#37-handling-unknown-document-properties) for `raise_on_unknown_attributes`, or `ignored_properties` if you're phasing out a specific, known set of legacy keys instead.
|
|
20
|
+
|
|
19
21
|
## 15.2. Debugging Tips
|
|
20
22
|
|
|
21
23
|
When troubleshooting issues with CouchbaseOrm, consider the following debugging tips:
|
|
@@ -44,12 +44,6 @@ module CouchbaseOrm
|
|
|
44
44
|
def type_for_attribute(attribute)
|
|
45
45
|
attribute_types[attribute]
|
|
46
46
|
end
|
|
47
|
-
|
|
48
|
-
if ActiveModel::VERSION::MAJOR < 6
|
|
49
|
-
def attribute_names
|
|
50
|
-
attribute_types.keys
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
47
|
end
|
|
54
48
|
|
|
55
49
|
def slice(*methods)
|
|
@@ -68,25 +62,5 @@ module CouchbaseOrm
|
|
|
68
62
|
value = send(attr_name)
|
|
69
63
|
value.inspect
|
|
70
64
|
end
|
|
71
|
-
|
|
72
|
-
if ActiveModel::VERSION::MAJOR < 6
|
|
73
|
-
def attribute_names
|
|
74
|
-
self.class.attribute_names
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def has_attribute?(attr_name)
|
|
78
|
-
@attributes.key?(attr_name.to_s)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def attribute_present?(attribute)
|
|
82
|
-
value = send(attribute)
|
|
83
|
-
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def _write_attribute(attr_name, value)
|
|
87
|
-
@attributes.write_from_user(attr_name.to_s, value)
|
|
88
|
-
value
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
65
|
end
|
|
92
66
|
end
|
data/lib/couchbase-orm/base.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true, encoding: ASCII-8BIT
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
require 'set'
|
|
4
5
|
require 'active_model'
|
|
5
6
|
require 'active_support/hash_with_indifferent_access'
|
|
6
7
|
require 'couchbase'
|
|
@@ -26,6 +27,7 @@ require 'couchbase-orm/json_transcoder'
|
|
|
26
27
|
require 'couchbase-orm/timestamps'
|
|
27
28
|
require 'couchbase-orm/active_record_compat'
|
|
28
29
|
require 'couchbase-orm/strict_loading'
|
|
30
|
+
require 'couchbase-orm/unknown_attributes'
|
|
29
31
|
require 'couchbase-orm/json_schema/validation'
|
|
30
32
|
require 'couchbase-orm/utilities/properties_always_exists_in_document'
|
|
31
33
|
|
|
@@ -43,6 +45,7 @@ module CouchbaseOrm
|
|
|
43
45
|
include ActiveRecordCompat
|
|
44
46
|
include StrictLoading
|
|
45
47
|
include Encrypt
|
|
48
|
+
include UnknownAttributes
|
|
46
49
|
|
|
47
50
|
extend Enum
|
|
48
51
|
extend IgnoredProperties
|
|
@@ -157,6 +157,10 @@ module CouchbaseOrm
|
|
|
157
157
|
hash = hash.with_indifferent_access if hash.is_a?(Hash)
|
|
158
158
|
super(hash.except("type"))
|
|
159
159
|
end
|
|
160
|
+
# ActiveModel::AttributeAssignment aliases attributes= to its own
|
|
161
|
+
# assign_attributes at include-time, so without this, `model.attributes = hash`
|
|
162
|
+
# would bypass the "type" stripping above.
|
|
163
|
+
alias_method :attributes=, :assign_attributes
|
|
160
164
|
|
|
161
165
|
# Updates the attributes of the model from the passed-in hash and saves the
|
|
162
166
|
# record. If the object is invalid, the saving will fail and false will be returned.
|
|
@@ -23,14 +23,8 @@ module CouchbaseOrm
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
26
|
+
def method_missing(name, *args, **options, &block)
|
|
27
|
+
@proxyfied.public_send(name, *args, **options, &block)
|
|
34
28
|
end
|
|
35
29
|
end
|
|
36
30
|
end
|
data/lib/couchbase-orm/types.rb
CHANGED
|
@@ -5,13 +5,6 @@ require "couchbase-orm/types/array"
|
|
|
5
5
|
require "couchbase-orm/types/nested"
|
|
6
6
|
require "couchbase-orm/types/encrypted"
|
|
7
7
|
|
|
8
|
-
if ActiveModel::VERSION::MAJOR < 6
|
|
9
|
-
# In Rails 5, the type system cannot allow overriding the default types
|
|
10
|
-
ActiveModel::Type.registry.instance_variable_get(:@registrations).delete_if do |k|
|
|
11
|
-
k.matches?(:date) || k.matches?(:datetime) || k.matches?(:timestamp)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
8
|
ActiveModel::Type.register(:date, CouchbaseOrm::Types::Date)
|
|
16
9
|
ActiveModel::Type.register(:datetime, CouchbaseOrm::Types::DateTime)
|
|
17
10
|
ActiveModel::Type.register(:timestamp, CouchbaseOrm::Types::Timestamp)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
require 'active_support/concern'
|
|
3
|
+
|
|
4
|
+
module CouchbaseOrm
|
|
5
|
+
# Controls what happens when a document (or a Hash/Parameters passed to
|
|
6
|
+
# `new`/`assign_attributes`) carries a key that has no corresponding
|
|
7
|
+
# attribute or writer on the model.
|
|
8
|
+
#
|
|
9
|
+
# By default (`raise_on_unknown_attributes = true`) this is unchanged
|
|
10
|
+
# ActiveModel behaviour: an `ActiveModel::UnknownAttributeError` is
|
|
11
|
+
# raised. Setting it to `false` on a model (it is inherited, like any
|
|
12
|
+
# `class_attribute`) makes unknown keys tolerated instead: they are
|
|
13
|
+
# dropped before assignment and a warning is logged.
|
|
14
|
+
#
|
|
15
|
+
# This exists to survive rolling/canary deploys: a document written by a
|
|
16
|
+
# pod running newer code can carry attributes a pod still running older
|
|
17
|
+
# code does not know about yet. Without this, reading that document on
|
|
18
|
+
# the old code raises and the request fails.
|
|
19
|
+
#
|
|
20
|
+
# A key is considered "known" using the same test ActiveModel itself uses
|
|
21
|
+
# to decide whether to raise (`respond_to?(:"#{key}=")`), not attribute
|
|
22
|
+
# membership - this matters because association writers (`belongs_to`,
|
|
23
|
+
# `has_and_belongs_to_many`) define a setter (e.g. `parent=`) for a name
|
|
24
|
+
# that is not itself a declared attribute (the attribute is `parent_id`).
|
|
25
|
+
# Filtering by attribute name would silently drop those.
|
|
26
|
+
#
|
|
27
|
+
# Unknown keys are only ever dropped from the in-memory assignment. If
|
|
28
|
+
# the model is saved afterwards, `serialized_attributes` only emits
|
|
29
|
+
# declared attributes, so the unknown key disappears from the stored
|
|
30
|
+
# document too - the same trade-off `ignored_properties` already makes.
|
|
31
|
+
module UnknownAttributes
|
|
32
|
+
extend ActiveSupport::Concern
|
|
33
|
+
|
|
34
|
+
# Caps the process-wide memory used to track which (class, key)
|
|
35
|
+
# warnings have already been emitted. Unknown keys come from
|
|
36
|
+
# document content, i.e. are not under the application's control,
|
|
37
|
+
# so this must be bounded. Once the cap is hit, unknown attributes
|
|
38
|
+
# are still filtered (and still logged at debug level) - only the
|
|
39
|
+
# warn-once escalation stops.
|
|
40
|
+
MAX_WARNED = 1_000
|
|
41
|
+
|
|
42
|
+
@warned = Set.new
|
|
43
|
+
|
|
44
|
+
included do
|
|
45
|
+
class_attribute :raise_on_unknown_attributes,
|
|
46
|
+
instance_accessor: false, instance_predicate: false, default: true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Filters out unknown keys before assignment when
|
|
50
|
+
# `raise_on_unknown_attributes` is false; otherwise unchanged.
|
|
51
|
+
def assign_attributes(attributes)
|
|
52
|
+
return super if self.class.raise_on_unknown_attributes
|
|
53
|
+
# Garbage input (nil, an Integer, a bare Object...) must still get
|
|
54
|
+
# ActiveModel's own "you must pass a hash" ArgumentError from
|
|
55
|
+
# `super`, not a NoMethodError from calling each_pair on it below.
|
|
56
|
+
return super unless attributes.respond_to?(:each_pair)
|
|
57
|
+
|
|
58
|
+
unknown = attributes.each_pair.filter_map { |key, _| key unless respond_to?(:"#{key}=") }
|
|
59
|
+
return super if unknown.empty?
|
|
60
|
+
|
|
61
|
+
UnknownAttributes.report(self.class, unknown)
|
|
62
|
+
super(attributes.except(*unknown))
|
|
63
|
+
end
|
|
64
|
+
alias_method :attributes=, :assign_attributes
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
# @api private
|
|
68
|
+
def report(klass, keys)
|
|
69
|
+
CouchbaseOrm.logger.debug { "#{klass.name}: ignoring unknown properties #{keys.inspect}" }
|
|
70
|
+
|
|
71
|
+
newly_warned = @warned.size >= MAX_WARNED ? [] : keys.select { |key| @warned.add?("#{klass.name}##{key}") }
|
|
72
|
+
return if newly_warned.empty?
|
|
73
|
+
|
|
74
|
+
CouchbaseOrm.logger.warn(
|
|
75
|
+
"#{klass.name}: ignoring unknown document properties #{newly_warned.join(', ')} " \
|
|
76
|
+
"(raise_on_unknown_attributes is false for this class - " \
|
|
77
|
+
"they will not be persisted if the document is saved)"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/spec/base_spec.rb
CHANGED
|
@@ -22,6 +22,33 @@ class BaseTestWithIgnoredProperties < CouchbaseOrm::Base
|
|
|
22
22
|
attribute :job, :string
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
class BaseTestWithUnknownAttributesAllowed < CouchbaseOrm::Base
|
|
26
|
+
self.raise_on_unknown_attributes = false
|
|
27
|
+
attribute :name, :string
|
|
28
|
+
attribute :job, :string
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class NestedDocWithUnknownAttributesAllowed < CouchbaseOrm::NestedDocument
|
|
32
|
+
self.raise_on_unknown_attributes = false
|
|
33
|
+
attribute :title, :string
|
|
34
|
+
attribute :value, :integer
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class NestedDocWithDefaultBehavior < CouchbaseOrm::NestedDocument
|
|
38
|
+
attribute :title, :string
|
|
39
|
+
attribute :value, :integer
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ParentDocWithNestedUnknownAllowed < CouchbaseOrm::Base
|
|
43
|
+
attribute :name, :string
|
|
44
|
+
attribute :nested_item, :nested, type: NestedDocWithUnknownAttributesAllowed
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class ParentDocWithNestedDefault < CouchbaseOrm::Base
|
|
48
|
+
attribute :name, :string
|
|
49
|
+
attribute :nested_item, :nested, type: NestedDocWithDefaultBehavior
|
|
50
|
+
end
|
|
51
|
+
|
|
25
52
|
class BaseTestWithPropertiesAlwaysExistsInDocument < CouchbaseOrm::Base
|
|
26
53
|
self.properties_always_exists_in_document = true
|
|
27
54
|
attribute :name, :string
|
|
@@ -349,6 +376,116 @@ describe CouchbaseOrm::Base do
|
|
|
349
376
|
end
|
|
350
377
|
end
|
|
351
378
|
|
|
379
|
+
describe 'handling unknown attributes' do
|
|
380
|
+
context 'when raise_on_unknown_attributes is set to false' do
|
|
381
|
+
it 'returns false when queried' do
|
|
382
|
+
expect(BaseTestWithUnknownAttributesAllowed.raise_on_unknown_attributes).to be(false)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
it 'silently ignores unknown attributes in new' do
|
|
386
|
+
model = BaseTestWithUnknownAttributesAllowed.new(name: 'test', job: 'dev', unknown_attr: 'value')
|
|
387
|
+
expect(model.name).to eq('test')
|
|
388
|
+
expect(model.job).to eq('dev')
|
|
389
|
+
expect(model.respond_to?(:unknown_attr)).to be(false)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
it 'silently ignores unknown attributes in assign_attributes' do
|
|
393
|
+
model = BaseTestWithUnknownAttributesAllowed.new(name: 'test')
|
|
394
|
+
expect {
|
|
395
|
+
model.assign_attributes(name: 'updated', job: 'engineer', foo: 'bar', baz: 'qux')
|
|
396
|
+
}.not_to raise_error
|
|
397
|
+
expect(model.name).to eq('updated')
|
|
398
|
+
expect(model.job).to eq('engineer')
|
|
399
|
+
expect(model.respond_to?(:foo)).to be(false)
|
|
400
|
+
expect(model.respond_to?(:baz)).to be(false)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
it 'only stores known attributes' do
|
|
404
|
+
model = BaseTestWithUnknownAttributesAllowed.new(
|
|
405
|
+
name: 'Alice',
|
|
406
|
+
job: 'Developer',
|
|
407
|
+
unknown_field_1: 'value1',
|
|
408
|
+
unknown_field_2: 'value2'
|
|
409
|
+
)
|
|
410
|
+
# Only known attributes should be stored
|
|
411
|
+
expect(model.name).to eq('Alice')
|
|
412
|
+
expect(model.job).to eq('Developer')
|
|
413
|
+
expect(model.respond_to?(:unknown_field_1)).to be(false)
|
|
414
|
+
expect(model.respond_to?(:unknown_field_2)).to be(false)
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
context 'default behavior (raise_on_unknown_attributes = true)' do
|
|
419
|
+
it 'returns true by default' do
|
|
420
|
+
expect(BaseTest.raise_on_unknown_attributes).to be(true)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
it 'raises ActiveModel::UnknownAttributeError on unknown attributes in new' do
|
|
424
|
+
expect {
|
|
425
|
+
BaseTest.new(name: 'bob', job: 'dev', foo: 'bar')
|
|
426
|
+
}.to raise_error(ActiveModel::UnknownAttributeError)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
it 'raises ActiveModel::UnknownAttributeError on unknown attributes in assign_attributes' do
|
|
430
|
+
model = BaseTest.new(name: 'bob')
|
|
431
|
+
expect {
|
|
432
|
+
model.assign_attributes(job: 'dev', foo: 'bar')
|
|
433
|
+
}.to raise_error(ActiveModel::UnknownAttributeError)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
context 'for NestedDocument classes' do
|
|
438
|
+
it 'returns false when queried on NestedDocument with raise_on_unknown_attributes = false' do
|
|
439
|
+
expect(NestedDocWithUnknownAttributesAllowed.raise_on_unknown_attributes).to be(false)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
it 'returns true by default on NestedDocument' do
|
|
443
|
+
expect(NestedDocWithDefaultBehavior.raise_on_unknown_attributes).to be(true)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it 'silently ignores unknown attributes in NestedDocument when disabled' do
|
|
447
|
+
nested = NestedDocWithUnknownAttributesAllowed.new(title: 'Test', value: 42, unknown: 'ignored')
|
|
448
|
+
expect(nested.title).to eq('Test')
|
|
449
|
+
expect(nested.value).to eq(42)
|
|
450
|
+
expect(nested.respond_to?(:unknown)).to be(false)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it 'raises error for unknown attributes in NestedDocument when enabled' do
|
|
454
|
+
expect {
|
|
455
|
+
NestedDocWithDefaultBehavior.new(title: 'Test', value: 42, unknown: 'error')
|
|
456
|
+
}.to raise_error(ActiveModel::UnknownAttributeError)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
it 'silently ignores unknown attributes in nested documents within parent' do
|
|
460
|
+
parent = ParentDocWithNestedUnknownAllowed.new(name: 'Parent')
|
|
461
|
+
nested = NestedDocWithUnknownAttributesAllowed.new(title: 'Nested', value: 100, extra: 'ignored')
|
|
462
|
+
parent.nested_item = nested
|
|
463
|
+
|
|
464
|
+
expect(parent.nested_item.title).to eq('Nested')
|
|
465
|
+
expect(parent.nested_item.value).to eq(100)
|
|
466
|
+
expect(parent.nested_item.respond_to?(:extra)).to be(false)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
it 'raises error for unknown attributes in nested documents with default behavior' do
|
|
470
|
+
parent = ParentDocWithNestedDefault.new(name: 'Parent')
|
|
471
|
+
expect {
|
|
472
|
+
NestedDocWithDefaultBehavior.new(title: 'Nested', value: 100, extra: 'error')
|
|
473
|
+
}.to raise_error(ActiveModel::UnknownAttributeError)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
it 'works with assign_attributes on NestedDocument' do
|
|
477
|
+
nested = NestedDocWithUnknownAttributesAllowed.new(title: 'Initial')
|
|
478
|
+
expect {
|
|
479
|
+
nested.assign_attributes(title: 'Updated', value: 50, unknown_field: 'ignored')
|
|
480
|
+
}.not_to raise_error
|
|
481
|
+
|
|
482
|
+
expect(nested.title).to eq('Updated')
|
|
483
|
+
expect(nested.value).to eq(50)
|
|
484
|
+
expect(nested.respond_to?(:unknown_field)).to be(false)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
352
489
|
describe '.properties_always_exists_in_document' do
|
|
353
490
|
it 'Uses NOT VALUED when properties_always_exists_in_document = false' do
|
|
354
491
|
where_clause = BaseTest.where(name: nil)
|
|
@@ -3,6 +3,18 @@ require './lib/couchbase-orm/active_record_compat'
|
|
|
3
3
|
class Foo
|
|
4
4
|
include CouchbaseOrm::ActiveRecordCompat
|
|
5
5
|
|
|
6
|
+
def self.attribute_names
|
|
7
|
+
%w[id compute_age]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.attribute_types
|
|
11
|
+
{ 'id' => :string }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def attribute_names
|
|
15
|
+
self.class.attribute_names
|
|
16
|
+
end
|
|
17
|
+
|
|
6
18
|
def compute_age
|
|
7
19
|
10 + 32
|
|
8
20
|
end
|
|
@@ -21,4 +33,65 @@ describe CouchbaseOrm::ActiveRecordCompat do
|
|
|
21
33
|
expect(foo.values_at([:compute_age])).to eq([42])
|
|
22
34
|
end
|
|
23
35
|
end
|
|
36
|
+
|
|
37
|
+
describe '.primary_key' do
|
|
38
|
+
it 'is always "id"' do
|
|
39
|
+
expect(Foo.primary_key).to eq('id')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '.base_class?' do
|
|
44
|
+
it 'is always true' do
|
|
45
|
+
expect(Foo.base_class?).to be true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '.column_names' do
|
|
50
|
+
it 'delegates to .attribute_names' do
|
|
51
|
+
expect(Foo.column_names).to eq(%w[id compute_age])
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '.abstract_class?' do
|
|
56
|
+
it 'is always false' do
|
|
57
|
+
expect(Foo.abstract_class?).to be false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe '.connected?' do
|
|
62
|
+
it 'is always true' do
|
|
63
|
+
expect(Foo.connected?).to be true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe '.table_exists?' do
|
|
68
|
+
it 'is always true' do
|
|
69
|
+
expect(Foo.table_exists?).to be true
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '._reflect_on_association' do
|
|
74
|
+
it 'is always false' do
|
|
75
|
+
expect(Foo._reflect_on_association(:anything)).to be false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe '.type_for_attribute' do
|
|
80
|
+
it 'delegates to .attribute_types' do
|
|
81
|
+
expect(Foo.type_for_attribute('id')).to eq(:string)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe '#_has_attribute?' do
|
|
86
|
+
it 'checks membership in #attribute_names' do
|
|
87
|
+
expect(foo._has_attribute?(:compute_age)).to be true
|
|
88
|
+
expect(foo._has_attribute?(:unknown)).to be false
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#attribute_for_inspect' do
|
|
93
|
+
it 'inspects the value returned by the named method' do
|
|
94
|
+
expect(foo.attribute_for_inspect(:compute_age)).to eq('42')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
24
97
|
end
|
|
@@ -13,4 +13,41 @@ describe 'CouchbaseOrm::Changeable' do
|
|
|
13
13
|
doc = Doc.find(doc.id)
|
|
14
14
|
expect(doc.changes).to be_empty
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
describe 'per-attribute dirty methods generated by create_dirty_methods' do
|
|
18
|
+
# `changes_applied` runs the same in-memory bookkeeping create/update
|
|
19
|
+
# triggers after a save (no DB round trip needed to exercise it).
|
|
20
|
+
let(:doc) do
|
|
21
|
+
Doc.new(title: 'initial').tap do |d|
|
|
22
|
+
d.title = 'Test'
|
|
23
|
+
d.changes_applied
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'exposes the change made in the last save' do
|
|
28
|
+
expect(doc.changed?).to be false # cleared by changes_applied
|
|
29
|
+
expect(doc.title_change).to be_nil # no change pending right now
|
|
30
|
+
expect(doc.will_save_change_to_title?).to be false
|
|
31
|
+
expect(doc.title_previously_was).to eq('initial')
|
|
32
|
+
expect(doc.title_before_last_save).to be_nil # only set by a real save, not by changes_applied alone
|
|
33
|
+
expect(doc.title_previously_changed?).to be true
|
|
34
|
+
expect(doc.title_previous_change).to eq(['initial', 'Test'])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'exposes the change during the save that triggered it, with an explicit from/to' do
|
|
38
|
+
expect(doc.saved_change_to_title?(from: 'initial')).to be true
|
|
39
|
+
expect(doc.saved_change_to_title?(to: 'Test')).to be true
|
|
40
|
+
expect(doc.saved_change_to_title?(from: 'initial', to: 'Test')).to be true
|
|
41
|
+
expect(doc.saved_change_to_title?(from: 'nope')).to be false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'can flag an attribute as about to change, and reset it back' do
|
|
45
|
+
expect { doc.title_will_change! }.not_to raise_error
|
|
46
|
+
expect { doc.reset_title! }.not_to raise_error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'falls back to nil for an attribute that was never saved' do
|
|
50
|
+
expect(Doc.new.title_previously_was).to be_nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
16
53
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require 'couchbase-orm/proxies/bucket_proxy'
|
|
2
|
+
require 'couchbase-orm/proxies/n1ql_proxy'
|
|
3
|
+
require 'couchbase-orm/proxies/results_proxy'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CouchbaseOrm::BucketProxy do
|
|
6
|
+
let(:proxyfied) { double('bucket') }
|
|
7
|
+
|
|
8
|
+
it 'raises when asked to proxy nil' do
|
|
9
|
+
expect { CouchbaseOrm::BucketProxy.new(nil) }.to raise_error(ArgumentError)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'delegates unknown methods to the proxyfied object' do
|
|
13
|
+
allow(proxyfied).to receive(:name).and_return('my_bucket')
|
|
14
|
+
expect(CouchbaseOrm::BucketProxy.new(proxyfied).name).to eq('my_bucket')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'delegates unknown methods with args, kwargs and a block' do
|
|
18
|
+
allow(proxyfied).to receive(:default_collection).with('a', b: 1).and_yield(:yielded)
|
|
19
|
+
CouchbaseOrm::BucketProxy.new(proxyfied).default_collection('a', b: 1) do |arg|
|
|
20
|
+
expect(arg).to eq(:yielded)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'wraps #n1ql in a N1qlProxy' do
|
|
25
|
+
allow(proxyfied).to receive(:n1ql).and_return(:the_n1ql)
|
|
26
|
+
expect(CouchbaseOrm::BucketProxy.new(proxyfied).n1ql).to be_a(CouchbaseOrm::N1qlProxy)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe '#view' do
|
|
30
|
+
it 'wraps the result in a ResultsProxy and caches it for the same design/view' do
|
|
31
|
+
allow(proxyfied).to receive(:view).with('design', 'view').and_return([:row1, :row2])
|
|
32
|
+
proxy = CouchbaseOrm::BucketProxy.new(proxyfied)
|
|
33
|
+
|
|
34
|
+
first = proxy.view('design', 'view')
|
|
35
|
+
second = proxy.view('design', 'view')
|
|
36
|
+
|
|
37
|
+
expect(first).to be_a(CouchbaseOrm::ResultsProxy)
|
|
38
|
+
expect(second).to be(first)
|
|
39
|
+
expect(proxyfied).to have_received(:view).once
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'requeries when the design/view pair changes' do
|
|
43
|
+
allow(proxyfied).to receive(:view).with('design', 'view').and_return([:row1])
|
|
44
|
+
allow(proxyfied).to receive(:view).with('design', 'other').and_return([:row2])
|
|
45
|
+
proxy = CouchbaseOrm::BucketProxy.new(proxyfied)
|
|
46
|
+
|
|
47
|
+
proxy.view('design', 'view')
|
|
48
|
+
proxy.view('design', 'other')
|
|
49
|
+
|
|
50
|
+
expect(proxyfied).to have_received(:view).twice
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/spec/type_encrypted_spec.rb
CHANGED
|
@@ -25,6 +25,16 @@ describe CouchbaseOrm::Types::Encrypted do
|
|
|
25
25
|
let(:the_secret) { "\x17`\x1F\xEE\el\xE0\x9F<\x94\xFE\x8A.\x1A\x92\xB9\xC3@\x86\x9Cp\xBEl\x86\x0E\x8CJ\tB\x97*U)\x96\x06\xA3\xE9\x84\xA6xW%\xDCT\x8C^\xEA\t\xC7\xD8\xFC\xF1\xD3\xD3\xE2\xEA\x89\xCBuUs\xB3\xFF'W>\xDE\x9CP\xA9\xDE%\xA2\xDE\x11\xFD\b\x9C\xD4\x87J,\x91\x02f\x16R\xDE\x908\x05\x1C\xF9\xDF{\x0F\xB3e\xB2\xB2\x96\xD7\xCC\x16As\xD3I\x02w\xE0\x8FL\xC6S\xEFP\xAC\x15\e^\xC4!\x15\"KF1\x17\x06\xA0N\x00\x18\xBA\x87\xEA?H\xD4<\xB5\xBCV\xB50\fc\xC9F\"\xF0B\eg%\x8E\x88\xD0\x9Bc\xE4\x93\t\x98\xC8\x87\xCB4]\xD9K\xA3\xDF\x13Q\xC0T\xCA\x91;\b\x9Cp\xE0\x7FR h\xDA\xB7\xD5\x869\f\xCA\x80\x802\x19\x19\xDD\x9DO\xAE}\xCA\eX\xA3\xA8\xBE\xE1\xBCW0g\x19@5n\r\xD8\xF3\x05\x7F4\x9CI\x1F3\xC0\xBDQJyG\v\xED!s\xD5\xD0&\xC1\x1A\xBC\x17\xFD\x9Cd\xB5\xAF\xB6U\x8A" }
|
|
26
26
|
let(:base64_secret) { Base64.strict_encode64(the_secret) }
|
|
27
27
|
|
|
28
|
+
describe "#serialize" do
|
|
29
|
+
it "returns nil for a nil value" do
|
|
30
|
+
expect(CouchbaseOrm::Types::Encrypted.new.serialize(nil)).to be_nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "returns the value unchanged otherwise (wrapping happens in Encrypt#encode_encrypted_attributes)" do
|
|
34
|
+
expect(CouchbaseOrm::Types::Encrypted.new.serialize(base64_secret)).to eq(base64_secret)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
28
38
|
it "prefix attribute on serialization" do
|
|
29
39
|
obj = TypeEncryptedTest.new(secret: base64_secret, secret2: "a secret")
|
|
30
40
|
expect_serialized_secret(obj)
|
|
@@ -50,6 +60,16 @@ describe CouchbaseOrm::Types::Encrypted do
|
|
|
50
60
|
expect(obj.as_json["secret2"]).to eq "a secret"
|
|
51
61
|
end
|
|
52
62
|
|
|
63
|
+
it "raises when serializing a non-String value for an encrypted attribute" do
|
|
64
|
+
obj = TypeEncryptedTest.new(secret: 12345)
|
|
65
|
+
expect { obj.send(:serialized_attributes) }.to raise_error(/Can not serialize value/)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "raises from #as_json when a non-String value is held by an encrypted attribute" do
|
|
69
|
+
obj = TypeEncryptedTest.new(secret: 12345)
|
|
70
|
+
expect { obj.as_json }.to raise_error(/Can not serialize value/)
|
|
71
|
+
end
|
|
72
|
+
|
|
53
73
|
it "prefix with custom algo" do
|
|
54
74
|
obj = SpecificAlgoTest.new(secret: base64_secret)
|
|
55
75
|
expect(obj.send(:serialized_attributes)["encrypted$secret"]).to eq({alg:"3DES", ciphertext: base64_secret})
|
data/spec/type_nested_spec.rb
CHANGED
|
@@ -18,6 +18,18 @@ class TypeNestedTest < CouchbaseOrm::Base
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
describe CouchbaseOrm::Types::Nested do
|
|
21
|
+
describe "#serialize" do
|
|
22
|
+
it "builds an instance from a raw Hash before serializing it, filtering ignored properties" do
|
|
23
|
+
# `serialize` is normally called with an already-built NestedDocument
|
|
24
|
+
# instance (see the specs below); this exercises its Hash branch
|
|
25
|
+
# directly, using string keys to match the shape a real document
|
|
26
|
+
# (and thus `ignored_properties`, which stores string names) has.
|
|
27
|
+
type = CouchbaseOrm::Types::Nested.new(type: SubTypeWithIgnoredProperties)
|
|
28
|
+
serialized = type.serialize("name" => "Nested", "value" => "Valid", "deprecated_property" => "should be dropped")
|
|
29
|
+
expect(serialized).to eq("name" => "Nested", "value" => "Valid")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
21
33
|
it "should be able to store and retrieve a nested object" do
|
|
22
34
|
obj = TypeNestedTest.new
|
|
23
35
|
obj.main = SubTypeTest.new
|
|
@@ -255,4 +267,84 @@ describe CouchbaseOrm::Types::Nested do
|
|
|
255
267
|
expect(parent.nested.value).to eq("Parent Value")
|
|
256
268
|
end
|
|
257
269
|
end
|
|
270
|
+
|
|
271
|
+
describe "raise_on_unknown_attributes" do
|
|
272
|
+
class SubTypeWithUnknownAttributesAllowed < CouchbaseOrm::NestedDocument
|
|
273
|
+
self.raise_on_unknown_attributes = false
|
|
274
|
+
attribute :label, :string
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
class SubTypeWithDefaultUnknownAttributes < CouchbaseOrm::NestedDocument
|
|
278
|
+
attribute :label, :string
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
class ParentWithNestedUnknownAttributesAllowed < CouchbaseOrm::Base
|
|
282
|
+
self.raise_on_unknown_attributes = false
|
|
283
|
+
attribute :title, :string
|
|
284
|
+
attribute :main, :nested, type: SubTypeWithUnknownAttributesAllowed
|
|
285
|
+
attribute :others, :array, type: SubTypeWithUnknownAttributesAllowed
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
class ParentWithStrictNestedUnknownAttributes < CouchbaseOrm::Base
|
|
289
|
+
self.raise_on_unknown_attributes = false
|
|
290
|
+
attribute :title, :string
|
|
291
|
+
attribute :main, :nested, type: SubTypeWithDefaultUnknownAttributes
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it "tolerates an unknown key in a nested document assigned as a Hash, via Types::Nested#cast" do
|
|
295
|
+
# This is the path the actual document-loading code takes; assigning an
|
|
296
|
+
# already-built NestedDocument *instance* (as opposed to a Hash) never
|
|
297
|
+
# reaches Types::Nested#cast at all.
|
|
298
|
+
parent = ParentWithNestedUnknownAttributesAllowed.new(title: "Parent")
|
|
299
|
+
parent.main = SubTypeWithUnknownAttributesAllowed.new(label: "Main")
|
|
300
|
+
parent.save!
|
|
301
|
+
|
|
302
|
+
doc_id = parent.id
|
|
303
|
+
raw_doc = ParentWithNestedUnknownAttributesAllowed.bucket.default_collection.get(doc_id).content
|
|
304
|
+
raw_doc["main"]["legacy"] = "should be ignored"
|
|
305
|
+
ParentWithNestedUnknownAttributesAllowed.bucket.default_collection.replace(doc_id, raw_doc)
|
|
306
|
+
|
|
307
|
+
parent.reload
|
|
308
|
+
expect(parent.main.label).to eq("Main")
|
|
309
|
+
expect(parent.main.attributes.keys).not_to include("legacy")
|
|
310
|
+
|
|
311
|
+
# Same assertion via `find` - a different code path (the GetResult
|
|
312
|
+
# branch of Document#initialize) than `reload`.
|
|
313
|
+
reloaded = ParentWithNestedUnknownAttributesAllowed.find(doc_id)
|
|
314
|
+
expect(reloaded.main.label).to eq("Main")
|
|
315
|
+
expect(reloaded.main.attributes.keys).not_to include("legacy")
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
it "tolerates an unknown key in a nested document inside a Types::Array, assigned as Hashes" do
|
|
319
|
+
parent = ParentWithNestedUnknownAttributesAllowed.new(title: "Parent")
|
|
320
|
+
parent.others = [
|
|
321
|
+
SubTypeWithUnknownAttributesAllowed.new(label: "First"),
|
|
322
|
+
SubTypeWithUnknownAttributesAllowed.new(label: "Second")
|
|
323
|
+
]
|
|
324
|
+
parent.save!
|
|
325
|
+
|
|
326
|
+
doc_id = parent.id
|
|
327
|
+
raw_doc = ParentWithNestedUnknownAttributesAllowed.bucket.default_collection.get(doc_id).content
|
|
328
|
+
raw_doc["others"][0]["legacy"] = "should be ignored"
|
|
329
|
+
ParentWithNestedUnknownAttributesAllowed.bucket.default_collection.replace(doc_id, raw_doc)
|
|
330
|
+
|
|
331
|
+
parent.reload
|
|
332
|
+
expect(parent.others.map(&:label)).to eq(["First", "Second"])
|
|
333
|
+
expect(parent.others[0].attributes.keys).not_to include("legacy")
|
|
334
|
+
expect(parent.others[1].attributes.keys).not_to include("legacy")
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
it "is per nested class: a tolerant parent does not make a strict nested document tolerant" do
|
|
338
|
+
parent = ParentWithStrictNestedUnknownAttributes.new(title: "Parent")
|
|
339
|
+
parent.main = SubTypeWithDefaultUnknownAttributes.new(label: "Main")
|
|
340
|
+
parent.save!
|
|
341
|
+
|
|
342
|
+
doc_id = parent.id
|
|
343
|
+
raw_doc = ParentWithStrictNestedUnknownAttributes.bucket.default_collection.get(doc_id).content
|
|
344
|
+
raw_doc["main"]["legacy"] = "should raise"
|
|
345
|
+
ParentWithStrictNestedUnknownAttributes.bucket.default_collection.replace(doc_id, raw_doc)
|
|
346
|
+
|
|
347
|
+
expect { parent.reload }.to raise_error(ActiveModel::UnknownAttributeError)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
258
350
|
end
|
|
@@ -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,16 +1,17 @@
|
|
|
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
|
|
8
8
|
- Gauthier Monserand
|
|
9
9
|
- Pierre Merlin
|
|
10
10
|
- Julien Burnet-Fauche
|
|
11
|
+
autorequire:
|
|
11
12
|
bindir: bin
|
|
12
13
|
cert_chain: []
|
|
13
|
-
date:
|
|
14
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
14
15
|
dependencies:
|
|
15
16
|
- !ruby/object:Gem::Dependency
|
|
16
17
|
name: activemodel
|
|
@@ -209,11 +210,13 @@ dependencies:
|
|
|
209
210
|
- !ruby/object:Gem::Version
|
|
210
211
|
version: '0'
|
|
211
212
|
description: A Couchbase ORM for Rails
|
|
213
|
+
email:
|
|
212
214
|
executables: []
|
|
213
215
|
extensions: []
|
|
214
216
|
extra_rdoc_files: []
|
|
215
217
|
files:
|
|
216
218
|
- ".github/workflows/deploy.yml"
|
|
219
|
+
- ".github/workflows/release.yml"
|
|
217
220
|
- ".github/workflows/test.yml"
|
|
218
221
|
- ".gitignore"
|
|
219
222
|
- ".rspec"
|
|
@@ -297,6 +300,7 @@ files:
|
|
|
297
300
|
- lib/couchbase-orm/types/encrypted.rb
|
|
298
301
|
- lib/couchbase-orm/types/nested.rb
|
|
299
302
|
- lib/couchbase-orm/types/timestamp.rb
|
|
303
|
+
- lib/couchbase-orm/unknown_attributes.rb
|
|
300
304
|
- lib/couchbase-orm/utilities/ensure_unique.rb
|
|
301
305
|
- lib/couchbase-orm/utilities/enum.rb
|
|
302
306
|
- lib/couchbase-orm/utilities/has_many.rb
|
|
@@ -334,6 +338,7 @@ files:
|
|
|
334
338
|
- spec/json_schema_spec.rb
|
|
335
339
|
- spec/n1ql_spec.rb
|
|
336
340
|
- spec/persistence_spec.rb
|
|
341
|
+
- spec/proxies/bucket_proxy_spec.rb
|
|
337
342
|
- spec/relation_nested_spec.rb
|
|
338
343
|
- spec/relation_spec.rb
|
|
339
344
|
- spec/support.rb
|
|
@@ -341,6 +346,7 @@ files:
|
|
|
341
346
|
- spec/type_encrypted_spec.rb
|
|
342
347
|
- spec/type_nested_spec.rb
|
|
343
348
|
- spec/type_spec.rb
|
|
349
|
+
- spec/unknown_attributes_spec.rb
|
|
344
350
|
- spec/utilities/ignored_properties_spec.rb
|
|
345
351
|
- spec/utilities/properties_always_exists_in_document_spec.rb
|
|
346
352
|
- spec/views_spec.rb
|
|
@@ -352,6 +358,7 @@ metadata:
|
|
|
352
358
|
bug_tracker_uri: https://github.com/Couchbase-Ecosystem/couchbase-ruby-orm/issues
|
|
353
359
|
documentation_uri: https://www.couchbase-ruby-orm.com/
|
|
354
360
|
homepage_uri: https://github.com/Couchbase-Ecosystem/couchbase-ruby-orm
|
|
361
|
+
post_install_message:
|
|
355
362
|
rdoc_options: []
|
|
356
363
|
require_paths:
|
|
357
364
|
- lib
|
|
@@ -366,7 +373,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
366
373
|
- !ruby/object:Gem::Version
|
|
367
374
|
version: '0'
|
|
368
375
|
requirements: []
|
|
369
|
-
rubygems_version:
|
|
376
|
+
rubygems_version: 3.0.3.1
|
|
377
|
+
signing_key:
|
|
370
378
|
specification_version: 4
|
|
371
379
|
summary: Couchbase ORM for Rails
|
|
372
380
|
test_files:
|
|
@@ -392,6 +400,7 @@ test_files:
|
|
|
392
400
|
- spec/json_schema_spec.rb
|
|
393
401
|
- spec/n1ql_spec.rb
|
|
394
402
|
- spec/persistence_spec.rb
|
|
403
|
+
- spec/proxies/bucket_proxy_spec.rb
|
|
395
404
|
- spec/relation_nested_spec.rb
|
|
396
405
|
- spec/relation_spec.rb
|
|
397
406
|
- spec/support.rb
|
|
@@ -399,6 +408,7 @@ test_files:
|
|
|
399
408
|
- spec/type_encrypted_spec.rb
|
|
400
409
|
- spec/type_nested_spec.rb
|
|
401
410
|
- spec/type_spec.rb
|
|
411
|
+
- spec/unknown_attributes_spec.rb
|
|
402
412
|
- spec/utilities/ignored_properties_spec.rb
|
|
403
413
|
- spec/utilities/properties_always_exists_in_document_spec.rb
|
|
404
414
|
- spec/views_spec.rb
|