couchbase-orm 1.1.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +45 -0
  3. data/.gitignore +2 -0
  4. data/.travis.yml +3 -2
  5. data/CODEOWNERS +1 -0
  6. data/Gemfile +5 -3
  7. data/LICENSE +201 -24
  8. data/README.md +248 -35
  9. data/ci/run_couchbase.sh +22 -0
  10. data/couchbase-orm.gemspec +26 -20
  11. data/lib/couchbase-orm/active_record_compat.rb +92 -0
  12. data/lib/couchbase-orm/associations.rb +119 -0
  13. data/lib/couchbase-orm/base.rb +143 -166
  14. data/lib/couchbase-orm/changeable.rb +512 -0
  15. data/lib/couchbase-orm/connection.rb +28 -8
  16. data/lib/couchbase-orm/encrypt.rb +48 -0
  17. data/lib/couchbase-orm/error.rb +17 -2
  18. data/lib/couchbase-orm/inspectable.rb +37 -0
  19. data/lib/couchbase-orm/json_schema/json_validation_error.rb +13 -0
  20. data/lib/couchbase-orm/json_schema/loader.rb +47 -0
  21. data/lib/couchbase-orm/json_schema/validation.rb +18 -0
  22. data/lib/couchbase-orm/json_schema/validator.rb +45 -0
  23. data/lib/couchbase-orm/json_schema.rb +9 -0
  24. data/lib/couchbase-orm/json_transcoder.rb +27 -0
  25. data/lib/couchbase-orm/locale/en.yml +5 -0
  26. data/lib/couchbase-orm/n1ql.rb +133 -0
  27. data/lib/couchbase-orm/persistence.rb +61 -52
  28. data/lib/couchbase-orm/proxies/bucket_proxy.rb +36 -0
  29. data/lib/couchbase-orm/proxies/collection_proxy.rb +52 -0
  30. data/lib/couchbase-orm/proxies/n1ql_proxy.rb +40 -0
  31. data/lib/couchbase-orm/proxies/results_proxy.rb +23 -0
  32. data/lib/couchbase-orm/railtie.rb +6 -17
  33. data/lib/couchbase-orm/relation.rb +249 -0
  34. data/lib/couchbase-orm/strict_loading.rb +21 -0
  35. data/lib/couchbase-orm/timestamps/created.rb +20 -0
  36. data/lib/couchbase-orm/timestamps/updated.rb +21 -0
  37. data/lib/couchbase-orm/timestamps.rb +15 -0
  38. data/lib/couchbase-orm/types/array.rb +32 -0
  39. data/lib/couchbase-orm/types/date.rb +9 -0
  40. data/lib/couchbase-orm/types/date_time.rb +14 -0
  41. data/lib/couchbase-orm/types/encrypted.rb +17 -0
  42. data/lib/couchbase-orm/types/nested.rb +43 -0
  43. data/lib/couchbase-orm/types/timestamp.rb +18 -0
  44. data/lib/couchbase-orm/types.rb +20 -0
  45. data/lib/couchbase-orm/utilities/enum.rb +13 -1
  46. data/lib/couchbase-orm/utilities/has_many.rb +72 -36
  47. data/lib/couchbase-orm/utilities/ignored_properties.rb +15 -0
  48. data/lib/couchbase-orm/utilities/index.rb +18 -20
  49. data/lib/couchbase-orm/utilities/properties_always_exists_in_document.rb +16 -0
  50. data/lib/couchbase-orm/utilities/query_helper.rb +148 -0
  51. data/lib/couchbase-orm/utils.rb +25 -0
  52. data/lib/couchbase-orm/version.rb +1 -1
  53. data/lib/couchbase-orm/views.rb +38 -41
  54. data/lib/couchbase-orm.rb +44 -9
  55. data/lib/ext/query_n1ql.rb +124 -0
  56. data/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +3 -2
  57. data/spec/associations_spec.rb +219 -50
  58. data/spec/base_spec.rb +296 -14
  59. data/spec/collection_proxy_spec.rb +29 -0
  60. data/spec/connection_spec.rb +27 -0
  61. data/spec/couchbase-orm/active_record_compat_spec.rb +24 -0
  62. data/spec/couchbase-orm/changeable_spec.rb +16 -0
  63. data/spec/couchbase-orm/json_schema/validation_spec.rb +23 -0
  64. data/spec/couchbase-orm/json_schema/validator_spec.rb +13 -0
  65. data/spec/couchbase-orm/timestamps_spec.rb +85 -0
  66. data/spec/couchbase-orm/timestamps_spec_models.rb +36 -0
  67. data/spec/empty-json-schema/.gitkeep +0 -0
  68. data/spec/enum_spec.rb +34 -0
  69. data/spec/has_many_spec.rb +101 -54
  70. data/spec/index_spec.rb +13 -9
  71. data/spec/json-schema/JsonSchemaBaseTest.json +19 -0
  72. data/spec/json-schema/entity_snakecase.json +20 -0
  73. data/spec/json-schema/loader_spec.rb +42 -0
  74. data/spec/json-schema/specific_path.json +20 -0
  75. data/spec/json_schema_spec.rb +178 -0
  76. data/spec/n1ql_spec.rb +193 -0
  77. data/spec/persistence_spec.rb +49 -9
  78. data/spec/relation_nested_spec.rb +88 -0
  79. data/spec/relation_spec.rb +430 -0
  80. data/spec/support.rb +16 -8
  81. data/spec/type_array_spec.rb +52 -0
  82. data/spec/type_encrypted_spec.rb +114 -0
  83. data/spec/type_nested_spec.rb +191 -0
  84. data/spec/type_spec.rb +317 -0
  85. data/spec/utilities/ignored_properties_spec.rb +20 -0
  86. data/spec/utilities/properties_always_exists_in_document_spec.rb +24 -0
  87. data/spec/views_spec.rb +32 -11
  88. metadata +193 -30
data/spec/n1ql_spec.rb ADDED
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../support", __FILE__)
4
+
5
+ class N1QLTest < CouchbaseOrm::Base
6
+ attribute :name, type: String
7
+ attribute :lastname, type: String
8
+ enum rating: [:awesome, :good, :okay, :bad], default: :okay
9
+
10
+ n1ql :by_custom_rating, emit_key: [:rating], query_fn: proc { |bucket, _values, options|
11
+ cluster.query("SELECT raw meta().id FROM `#{bucket.name}` WHERE type = 'n1_ql_test' AND rating IN [1, 2] ORDER BY name ASC", options)
12
+ }
13
+ n1ql :by_name, emit_key: [:name]
14
+ n1ql :by_lastname, emit_key: [:lastname]
15
+ n1ql :by_rating_emit, emit_key: :rating
16
+
17
+ n1ql :by_custom_rating_values, emit_key: [:rating], query_fn: proc { |bucket, values, options|
18
+ cluster.query("SELECT raw meta().id FROM `#{bucket.name}` where type = 'n1_ql_test' AND rating IN #{quote(values[0])} ORDER BY name ASC", options)
19
+ }
20
+ n1ql :by_rating_reverse, emit_key: :rating, custom_order: "name DESC"
21
+ n1ql :by_rating_without_docs, emit_key: :rating, include_docs: false
22
+
23
+ # This generates both:
24
+ # view :by_rating, emit_key: :rating
25
+ # def self.find_by_rating(rating); end # also provide this helper function
26
+ index_n1ql :rating
27
+ end
28
+
29
+ describe CouchbaseOrm::N1ql do
30
+ before(:each) do
31
+ N1QLTest.delete_all
32
+ end
33
+
34
+ it "should not allow n1ql to override existing methods" do
35
+ expect { N1QLTest.n1ql :all }.to raise_error(ArgumentError)
36
+ end
37
+
38
+ it "should perform a query and return the n1ql" do
39
+ N1QLTest.create! name: :bob
40
+ docs = N1QLTest.all.collect { |ob|
41
+ ob.name
42
+ }
43
+ expect(docs).to eq(%w[bob])
44
+ end
45
+
46
+ it "should query by non-nil value" do
47
+ _anonymous = N1QLTest.create!
48
+ bob = N1QLTest.create! name: :bob
49
+
50
+ expect(N1QLTest.by_name(key: 'bob').to_a).to eq [bob]
51
+ end
52
+
53
+ it "should query by nil value" do
54
+ anonymous = N1QLTest.create! lastname: "Anonymous"
55
+ anonymous_no_property = N1QLTest.create! lastname: "Anonymous without name property"
56
+
57
+ CouchbaseOrm::Connection.bucket.default_collection.mutate_in(anonymous_no_property.id, [
58
+ Couchbase::MutateInSpec.remove("name"),
59
+ ])
60
+
61
+ anonymous_no_property.reload
62
+
63
+ _bob = N1QLTest.create! name: :bob
64
+
65
+ expect(N1QLTest.by_name(key: nil).to_a).to match_array [anonymous, anonymous_no_property]
66
+ end
67
+
68
+ it "should query all when key is not set" do
69
+ anonymous = N1QLTest.create!
70
+ bob = N1QLTest.create! name: :bob
71
+
72
+ expect(N1QLTest.by_name.to_a).to eq [anonymous, bob]
73
+ end
74
+
75
+
76
+ it "should work with other keys" do
77
+ N1QLTest.create! name: :bob, rating: :good
78
+ N1QLTest.create! name: :jane, rating: :awesome
79
+ N1QLTest.create! name: :greg, rating: :bad
80
+
81
+ docs = N1QLTest.by_name(descending: true).collect { |ob|
82
+ ob.name
83
+ }
84
+ expect(docs).to eq(%w[jane greg bob])
85
+
86
+ docs = N1QLTest.by_rating(descending: true).collect { |ob|
87
+ ob.rating
88
+ }
89
+ expect(docs).to eq([4, 2, 1])
90
+ end
91
+
92
+ it "should return matching results" do
93
+ N1QLTest.create! name: :bob, rating: :awesome
94
+ N1QLTest.create! name: :jane, rating: :awesome
95
+ N1QLTest.create! name: :greg, rating: :bad
96
+ N1QLTest.create! name: :mel, rating: :good
97
+
98
+ docs = N1QLTest.find_by_rating(1).collect { |ob|
99
+ ob.name
100
+ }
101
+
102
+ expect(Set.new(docs)).to eq(Set.new(%w[bob jane]))
103
+
104
+ docs = N1QLTest.by_custom_rating().collect { |ob|
105
+ ob.name
106
+ }
107
+
108
+ expect(Set.new(docs)).to eq(Set.new(%w[bob jane mel]))
109
+ end
110
+
111
+ it "should return matching results with reverse order" do
112
+ N1QLTest.create! name: :bob, rating: :awesome
113
+ N1QLTest.create! name: :jane, rating: :awesome
114
+ N1QLTest.create! name: :greg, rating: :bad
115
+ N1QLTest.create! name: :mel, rating: :good
116
+
117
+ docs = N1QLTest.by_rating_reverse(key: 1).collect { |ob|
118
+ ob.name
119
+ }
120
+
121
+ expect(docs).to eq(%w[jane bob])
122
+ end
123
+
124
+ it "should return matching results without full documents" do
125
+ inst_bob = N1QLTest.create! name: :bob, rating: :awesome
126
+ inst_jane = N1QLTest.create! name: :jane, rating: :awesome
127
+ N1QLTest.create! name: :greg, rating: :bad
128
+ N1QLTest.create! name: :mel, rating: :good
129
+
130
+ docs = N1QLTest.by_rating_without_docs(key: 1)
131
+
132
+ expect(Set.new(docs)).to eq(Set.new([inst_bob.id, inst_jane.id]))
133
+ end
134
+
135
+ it "should return matching results with nil usage" do
136
+ N1QLTest.create! name: :bob, lastname: nil
137
+ N1QLTest.create! name: :jane, lastname: "dupond"
138
+
139
+ docs = N1QLTest.by_lastname(key: [nil]).collect { |ob|
140
+ ob.name
141
+ }
142
+ expect(docs).to eq(%w[bob])
143
+ end
144
+
145
+ it "should allow storing quoting chars" do
146
+ special_name = "O'Leary & Sons \"The best\" \\ between backslash \\"
147
+ t = N1QLTest.create! name: special_name, rating: :awesome
148
+ expect(N1QLTest.find(t.id).name).to eq(special_name)
149
+ expect(N1QLTest.by_name(key: special_name).to_a.first).to eq(t)
150
+ expect(N1QLTest.where(name: special_name).to_a.first).to eq(t)
151
+ end
152
+ it "should return matching results with custom n1ql query" do
153
+ N1QLTest.create! name: :bob, rating: :awesome
154
+ N1QLTest.create! name: :jane, rating: :awesome
155
+ N1QLTest.create! name: :greg, rating: :bad
156
+ N1QLTest.create! name: :mel, rating: :good
157
+
158
+
159
+ docs = N1QLTest.by_custom_rating().collect { |ob|
160
+ ob.name
161
+ }
162
+
163
+ expect(Set.new(docs)).to eq(Set.new(%w[bob jane mel]))
164
+
165
+ docs = N1QLTest.by_custom_rating_values(key: [[1, 2]]).collect { |ob|
166
+ ob.name
167
+ }
168
+
169
+ expect(Set.new(docs)).to eq(Set.new(%w[bob jane mel]))
170
+ end
171
+
172
+ it "should log the default scan_consistency when n1ql query is executed" do
173
+ allow(CouchbaseOrm.logger).to receive(:debug)
174
+ N1QLTest.by_rating_reverse()
175
+ expect(CouchbaseOrm.logger).to have_received(:debug).at_least(:once).with("N1QL query: select raw meta().id from `#{CouchbaseOrm::Connection.bucket.name}` where type=\"n1_ql_test\" order by name DESC return 0 rows with scan_consistency : #{described_class::DEFAULT_SCAN_CONSISTENCY}")
176
+ end
177
+
178
+ it "should log the set scan_consistency when n1ql query is executed with a specific scan_consistency" do
179
+ allow(CouchbaseOrm.logger).to receive(:debug)
180
+ default_n1ql_config = CouchbaseOrm::N1ql.config
181
+ CouchbaseOrm::N1ql.config({ scan_consistency: :not_bounded })
182
+ N1QLTest.by_rating_reverse()
183
+ expect(CouchbaseOrm.logger).to have_received(:debug).at_least(:once).with("N1QL query: select raw meta().id from `#{CouchbaseOrm::Connection.bucket.name}` where type=\"n1_ql_test\" order by name DESC return 0 rows with scan_consistency : not_bounded")
184
+
185
+ CouchbaseOrm::N1ql.config(default_n1ql_config)
186
+ N1QLTest.by_rating_reverse()
187
+ expect(CouchbaseOrm.logger).to have_received(:debug).at_least(:once).with("N1QL query: select raw meta().id from `#{CouchbaseOrm::Connection.bucket.name}` where type=\"n1_ql_test\" order by name DESC return 0 rows with scan_consistency : #{described_class::DEFAULT_SCAN_CONSISTENCY}")
188
+ end
189
+
190
+ after(:all) do
191
+ N1QLTest.delete_all
192
+ end
193
+ end
@@ -2,9 +2,12 @@
2
2
 
3
3
  require File.expand_path("../support", __FILE__)
4
4
 
5
+ require "action_controller"
5
6
 
6
7
  class BasicModel < CouchbaseOrm::Base
7
- attribute :name, :address, :age
8
+ attribute :name
9
+ attribute :address
10
+ attribute :age
8
11
  end
9
12
 
10
13
  class ModelWithDefaults < CouchbaseOrm::Base
@@ -14,7 +17,9 @@ class ModelWithDefaults < CouchbaseOrm::Base
14
17
  end
15
18
 
16
19
  class ModelWithCallbacks < CouchbaseOrm::Base
17
- attribute :name, :address, :age
20
+ attribute :name
21
+ attribute :address
22
+ attribute :age
18
23
 
19
24
  before_create :update_name
20
25
  before_save :set_address
@@ -32,7 +37,8 @@ class ModelWithCallbacks < CouchbaseOrm::Base
32
37
  end
33
38
 
34
39
  class ModelWithValidations < CouchbaseOrm::Base
35
- attribute :name, :address, type: String
40
+ attribute :name, type: String
41
+ attribute :address, type: String
36
42
  attribute :age, type: :Integer
37
43
 
38
44
  validates :name, presence: true
@@ -205,11 +211,8 @@ describe CouchbaseOrm::Persistence do
205
211
  expect(model.save!).to be(model)
206
212
 
207
213
  # coercion will fail here
208
- begin
209
- model.age = 'a23'
210
- expect(false).to be(true)
211
- rescue ArgumentError => e
212
- end
214
+ model.age = "a23"
215
+ expect{ model.save! }.to raise_error(CouchbaseOrm::Error::RecordInvalid)
213
216
 
214
217
  model.destroy
215
218
  end
@@ -228,12 +231,24 @@ describe CouchbaseOrm::Persistence do
228
231
 
229
232
  model.reload
230
233
  expect(model.changed?).to be(false)
231
- expect(model.id).to be(id)
234
+ expect(model.id).to eq(id)
232
235
 
233
236
  model.destroy
234
237
  expect(model.destroyed?).to be(true)
235
238
  end
236
239
 
240
+ it "should update with action controler parameters" do
241
+ model = BasicModel.create!
242
+ params = ActionController::Parameters.new({
243
+ name: 'Francesco',
244
+ age: 22,
245
+ foo: 'bar'
246
+ })
247
+ model.update!(params.permit(:name, :age))
248
+ model.reload
249
+ expect(model.age).to eq(22)
250
+ end
251
+
237
252
  it "should update attributes" do
238
253
  model = BasicModel.new
239
254
 
@@ -254,6 +269,31 @@ describe CouchbaseOrm::Persistence do
254
269
  expect(model.destroyed?).to be(true)
255
270
  end
256
271
 
272
+ it "should not allow to update unkown attributes" do
273
+ model = BasicModel.new
274
+
275
+ expect{ model.update_attributes({
276
+ name: 'bob',
277
+ age: 34,
278
+ foo: 'bar'
279
+ }) }.to raise_error(ActiveModel::UnknownAttributeError)
280
+ end
281
+
282
+ it "should not allow to create with unkown attributes" do
283
+ expect{ BasicModel.create({
284
+ name: 'bob',
285
+ age: 34,
286
+ foo: 'bar'
287
+ }) }.to raise_error(ActiveModel::UnknownAttributeError)
288
+ end
289
+
290
+ it "should not allow to update with unkown attributes" do
291
+ model = BasicModel.create!(name: 'bob', age: 34)
292
+ expect{ model.update({
293
+ foo: 'bar'
294
+ }) }.to raise_error(ActiveModel::UnknownAttributeError)
295
+ end
296
+
257
297
  describe BasicModel do
258
298
  it_behaves_like "ActiveModel"
259
299
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true, encoding: ASCII-8BIT
2
+
3
+ require File.expand_path("../support", __FILE__)
4
+
5
+ class NestedModel < CouchbaseOrm::NestedDocument
6
+ attribute :name, :string
7
+ attribute :size, :integer
8
+ attribute :child, :nested, type: NestedModel
9
+ end
10
+
11
+ class RelationParentModel < CouchbaseOrm::Base
12
+ attribute :name, :string
13
+ attribute :sub, :nested, type: NestedModel
14
+ attribute :subs, :array, type: NestedModel
15
+ end
16
+
17
+ describe CouchbaseOrm::Relation do
18
+ before(:each) do
19
+ RelationParentModel.delete_all
20
+ CouchbaseOrm.logger.debug "Cleaned before tests"
21
+ end
22
+
23
+ after(:all) do
24
+ CouchbaseOrm.logger.debug "Cleanup after all tests"
25
+ RelationParentModel.delete_all
26
+ end
27
+
28
+ it "should query on nested array attribute" do
29
+ RelationParentModel.create(name: "parent_without_subs")
30
+ parent = RelationParentModel.create(name: "parent")
31
+ parent.subs = [
32
+ NestedModel.new(name: "sub2"),
33
+ NestedModel.new(name: "sub3")
34
+ ]
35
+ parent.save!
36
+
37
+ expect(RelationParentModel.where(subs: {name: 'sub2'}).first).to eq parent
38
+ expect(RelationParentModel.where(subs: {name: ['sub3', 'subX']}).first).to eq parent
39
+ end
40
+
41
+ it "should query on nested array with multiple attributes" do
42
+ RelationParentModel.create(name: "parent_without_subs")
43
+ parent = RelationParentModel.create(name: "parent")
44
+ parent.subs = [
45
+ NestedModel.new(name: "sub2", size: 2),
46
+ NestedModel.new(name: "sub3", size: 3)
47
+ ]
48
+ parent.save!
49
+
50
+ expect(RelationParentModel.where(subs: {name: 'sub2', size: 2}).first).to eq parent
51
+ expect(RelationParentModel.where(subs: {name: 'sub2', size: 3}).first).to eq nil
52
+ end
53
+
54
+ it "should query by gte function" do
55
+ parent = RelationParentModel.create(name: "parent")
56
+ parent.subs = [
57
+ NestedModel.new(name: "sub2", size: 2),
58
+ NestedModel.new(name: "sub3", size: 3),
59
+ NestedModel.new(name: "sub4", size: 4)
60
+ ]
61
+ parent.save!
62
+ expect(RelationParentModel.where(subs: {size: {_gte: 3, _lt: 4}}).first).to eq parent
63
+ end
64
+
65
+ it "should query by nested attribute" do
66
+ RelationParentModel.create(name: "parent_without_sub")
67
+ parent = RelationParentModel.create(name: "parent")
68
+ parent.sub = NestedModel.new(name: "sub")
69
+ parent.save!
70
+ expect(RelationParentModel.where('sub.name': 'sub').first).to eq parent
71
+ expect(RelationParentModel.where(sub: {name: 'sub'}).first).to eq parent
72
+ expect(RelationParentModel.where(sub: {name: ['sub', 'subX']}).first).to eq parent
73
+ expect(RelationParentModel.where(sub: {name: ['subX']}).first).to be_nil
74
+
75
+ end
76
+
77
+ it "should query by grand child attribute" do
78
+ RelationParentModel.create(name: "parent_without_sub")
79
+ parent = RelationParentModel.create(name: "parent")
80
+ parent.sub = NestedModel.new(name: "sub", child: NestedModel.new(name: "child"))
81
+ parent.save!
82
+
83
+ expect(RelationParentModel.where(sub: {child: {name: 'child'}}).first).to eq parent
84
+ expect(RelationParentModel.where(sub: {child: {name: ['child', 'childX']}}).first).to eq parent
85
+ expect(RelationParentModel.where(sub: {child: {name: ['childX']}}).first).to be_nil
86
+ end
87
+ end
88
+