couchbase-orm 1.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/test.yml +45 -0
- data/.gitignore +2 -0
- data/.travis.yml +5 -4
- data/CODEOWNERS +1 -0
- data/Gemfile +5 -3
- data/README.md +237 -31
- data/ci/run_couchbase.sh +22 -0
- data/couchbase-orm.gemspec +26 -20
- data/lib/couchbase-orm/active_record_compat.rb +92 -0
- data/lib/couchbase-orm/associations.rb +119 -0
- data/lib/couchbase-orm/base.rb +143 -166
- data/lib/couchbase-orm/changeable.rb +512 -0
- data/lib/couchbase-orm/connection.rb +28 -8
- data/lib/couchbase-orm/encrypt.rb +48 -0
- data/lib/couchbase-orm/error.rb +17 -2
- data/lib/couchbase-orm/inspectable.rb +37 -0
- data/lib/couchbase-orm/json_schema/json_validation_error.rb +13 -0
- data/lib/couchbase-orm/json_schema/loader.rb +47 -0
- data/lib/couchbase-orm/json_schema/validation.rb +18 -0
- data/lib/couchbase-orm/json_schema/validator.rb +45 -0
- data/lib/couchbase-orm/json_schema.rb +9 -0
- data/lib/couchbase-orm/json_transcoder.rb +27 -0
- data/lib/couchbase-orm/locale/en.yml +5 -0
- data/lib/couchbase-orm/n1ql.rb +133 -0
- data/lib/couchbase-orm/persistence.rb +67 -50
- data/lib/couchbase-orm/proxies/bucket_proxy.rb +36 -0
- data/lib/couchbase-orm/proxies/collection_proxy.rb +52 -0
- data/lib/couchbase-orm/proxies/n1ql_proxy.rb +40 -0
- data/lib/couchbase-orm/proxies/results_proxy.rb +23 -0
- data/lib/couchbase-orm/railtie.rb +6 -17
- data/lib/couchbase-orm/relation.rb +249 -0
- data/lib/couchbase-orm/strict_loading.rb +21 -0
- data/lib/couchbase-orm/timestamps/created.rb +20 -0
- data/lib/couchbase-orm/timestamps/updated.rb +21 -0
- data/lib/couchbase-orm/timestamps.rb +15 -0
- data/lib/couchbase-orm/types/array.rb +32 -0
- data/lib/couchbase-orm/types/date.rb +9 -0
- data/lib/couchbase-orm/types/date_time.rb +14 -0
- data/lib/couchbase-orm/types/encrypted.rb +17 -0
- data/lib/couchbase-orm/types/nested.rb +43 -0
- data/lib/couchbase-orm/types/timestamp.rb +18 -0
- data/lib/couchbase-orm/types.rb +20 -0
- data/lib/couchbase-orm/utilities/enum.rb +13 -1
- data/lib/couchbase-orm/utilities/has_many.rb +72 -36
- data/lib/couchbase-orm/utilities/ignored_properties.rb +15 -0
- data/lib/couchbase-orm/utilities/index.rb +23 -8
- data/lib/couchbase-orm/utilities/properties_always_exists_in_document.rb +16 -0
- data/lib/couchbase-orm/utilities/query_helper.rb +148 -0
- data/lib/couchbase-orm/utils.rb +25 -0
- data/lib/couchbase-orm/version.rb +1 -1
- data/lib/couchbase-orm/views.rb +38 -41
- data/lib/couchbase-orm.rb +44 -9
- data/lib/ext/query_n1ql.rb +124 -0
- data/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +3 -2
- data/spec/associations_spec.rb +219 -50
- data/spec/base_spec.rb +296 -14
- data/spec/collection_proxy_spec.rb +29 -0
- data/spec/connection_spec.rb +27 -0
- data/spec/couchbase-orm/active_record_compat_spec.rb +24 -0
- data/spec/couchbase-orm/changeable_spec.rb +16 -0
- data/spec/couchbase-orm/json_schema/validation_spec.rb +23 -0
- data/spec/couchbase-orm/json_schema/validator_spec.rb +13 -0
- data/spec/couchbase-orm/timestamps_spec.rb +85 -0
- data/spec/couchbase-orm/timestamps_spec_models.rb +36 -0
- data/spec/empty-json-schema/.gitkeep +0 -0
- data/spec/enum_spec.rb +34 -0
- data/spec/has_many_spec.rb +101 -54
- data/spec/index_spec.rb +55 -8
- data/spec/json-schema/JsonSchemaBaseTest.json +19 -0
- data/spec/json-schema/entity_snakecase.json +20 -0
- data/spec/json-schema/loader_spec.rb +42 -0
- data/spec/json-schema/specific_path.json +20 -0
- data/spec/json_schema_spec.rb +178 -0
- data/spec/n1ql_spec.rb +193 -0
- data/spec/persistence_spec.rb +49 -9
- data/spec/relation_nested_spec.rb +88 -0
- data/spec/relation_spec.rb +430 -0
- data/spec/support.rb +16 -8
- data/spec/type_array_spec.rb +52 -0
- data/spec/type_encrypted_spec.rb +114 -0
- data/spec/type_nested_spec.rb +191 -0
- data/spec/type_spec.rb +317 -0
- data/spec/utilities/ignored_properties_spec.rb +20 -0
- data/spec/utilities/properties_always_exists_in_document_spec.rb +24 -0
- data/spec/views_spec.rb +34 -13
- metadata +192 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 65b1d614fe648dace9781d689ac5583108b6acfcf216f6da479a66d710608048
|
4
|
+
data.tar.gz: 4cbb6ff2c0e59445ae03d908c07b2d21bbc12f34f3cf02d68248390b66bea3c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b12fc3d20ac1acc47bac8b784b9f5de3b5113e916c4b68c3181684bc58cffa143202d122177b62821afea4d3af9e54d9a0c26d5a16a441b757cefcd4a3465435
|
7
|
+
data.tar.gz: e1a99aeaa12090047188a5f93b652e29d5e173e6a0f12422e1f42a252bda3229651cc9f550ac33dbeb0784f564e71b620a50c6967b375e882db3d005e2b0358d
|
@@ -0,0 +1,45 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [master]
|
6
|
+
pull_request:
|
7
|
+
branches: [master]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
strategy:
|
12
|
+
matrix:
|
13
|
+
include:
|
14
|
+
- ruby: '3.2'
|
15
|
+
gemfile: '7.1.0'
|
16
|
+
couchbase: '7.1.1'
|
17
|
+
- ruby: '3.0'
|
18
|
+
gemfile: '7.0.0'
|
19
|
+
couchbase: '6.6.5'
|
20
|
+
- ruby: '3.0'
|
21
|
+
gemfile: '7.0.0'
|
22
|
+
couchbase: '7.1.0'
|
23
|
+
- ruby: '2.7'
|
24
|
+
gemfile: '7.0.0'
|
25
|
+
couchbase: '7.1.0'
|
26
|
+
fail-fast: false
|
27
|
+
runs-on: ubuntu-20.04
|
28
|
+
name: ${{ matrix.ruby }} rails-${{ matrix.gemfile }} couchbase-${{ matrix.couchbase }}
|
29
|
+
steps:
|
30
|
+
- uses: actions/checkout@v3
|
31
|
+
- run: sudo apt-get update && sudo apt-get install libevent-dev libev-dev python-httplib2
|
32
|
+
- uses: ruby/setup-ruby@v1
|
33
|
+
with:
|
34
|
+
ruby-version: ${{ matrix.ruby }}
|
35
|
+
bundler-cache: true
|
36
|
+
- run: sudo ./ci/run_couchbase.sh $COUCHBASE_VERSION $COUCHBASE_BUCKET $COUCHBASE_USER $COUCHBASE_PASSWORD
|
37
|
+
- run: bundle exec rspec
|
38
|
+
env:
|
39
|
+
ACTIVE_MODEL_VERSION: ${{ matrix.gemfile }}
|
40
|
+
BUNDLE_JOBS: 4
|
41
|
+
BUNDLE_PATH: vendor/bundle
|
42
|
+
COUCHBASE_BUCKET: default
|
43
|
+
COUCHBASE_USER: tester
|
44
|
+
COUCHBASE_PASSWORD: password123
|
45
|
+
COUCHBASE_VERSION: ${{ matrix.couchbase }}
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
3
|
- ruby-2.4.2
|
4
|
-
- ruby-2.
|
4
|
+
- ruby-2.5.7
|
5
5
|
- ruby-head
|
6
|
-
- jruby-9.
|
6
|
+
- jruby-9.2.9.0
|
7
7
|
- jruby-head
|
8
8
|
- rubinius
|
9
9
|
- rubinius-3.86
|
@@ -14,8 +14,8 @@ before_install:
|
|
14
14
|
- git submodule update --init --recursive
|
15
15
|
- gem install ffi
|
16
16
|
- sudo apt-get install libev-dev python-httplib2
|
17
|
-
- sudo wget https://packages.couchbase.com/releases/5.
|
18
|
-
- sudo dpkg -i couchbase-server-enterprise_5.
|
17
|
+
- sudo wget https://packages.couchbase.com/releases/5.1.0/couchbase-server-enterprise_5.1.0-ubuntu14.04_amd64.deb
|
18
|
+
- sudo dpkg -i couchbase-server-enterprise_5.1.0-ubuntu14.04_amd64.deb
|
19
19
|
- sleep 8
|
20
20
|
- sudo service couchbase-server status
|
21
21
|
- /opt/couchbase/bin/couchbase-cli cluster-init -c 127.0.0.1:8091 --cluster-username=admin --cluster-password=password --cluster-ramsize=320 --cluster-index-ramsize=256 --cluster-fts-ramsize=256 --services=data,index,query,fts
|
@@ -24,6 +24,7 @@ before_install:
|
|
24
24
|
- /opt/couchbase/bin/couchbase-cli bucket-create -c 127.0.0.1:8091 -u admin -p password --bucket=default --bucket-type=couchbase --bucket-ramsize=160 --bucket-replica=0 --wait
|
25
25
|
- sleep 1
|
26
26
|
- /opt/couchbase/bin/couchbase-cli user-manage -c 127.0.0.1:8091 -u admin -p password --set --rbac-username tester --rbac-password password123 --rbac-name "Auto Tester" --roles admin --auth-domain local
|
27
|
+
- curl http://admin:password@localhost:8093/query/service -d 'statement=CREATE INDEX `default_type` ON `default`(`type`)'
|
27
28
|
- export TRAVIS_TEST=true
|
28
29
|
matrix:
|
29
30
|
allow_failures:
|
data/CODEOWNERS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* @doctolib/couchbase-guild
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# Couchbase ORM for Rails
|
2
2
|
|
3
|
-
[![Build Status](https://secure.travis-ci.org/acaprojects/couchbase-orm.svg)](http://travis-ci.org/acaprojects/couchbase-orm)
|
4
|
-
|
5
3
|
## Rails integration
|
6
4
|
|
7
5
|
To generate config you can use `rails generate couchbase_orm:config`:
|
@@ -11,8 +9,9 @@ To generate config you can use `rails generate couchbase_orm:config`:
|
|
11
9
|
|
12
10
|
It will generate this `config/couchbase.yml` for you:
|
13
11
|
|
12
|
+
```yaml
|
14
13
|
common: &common
|
15
|
-
|
14
|
+
connection_string: couchbase://localhost
|
16
15
|
username: dev_user
|
17
16
|
password: dev_password
|
18
17
|
|
@@ -26,10 +25,25 @@ It will generate this `config/couchbase.yml` for you:
|
|
26
25
|
|
27
26
|
# set these environment variables on your production server
|
28
27
|
production:
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
connection_string: <%= ENV['COUCHBASE_CONNECTION_STRING'] %>
|
29
|
+
bucket: <%= ENV['COUCHBASE_BUCKET'] %>
|
30
|
+
username: <%= ENV['COUCHBASE_USER'] %>
|
31
|
+
password: <%= ENV['COUCHBASE_PASSWORD'] %>
|
32
|
+
```
|
33
|
+
|
34
|
+
## Setup without Rails
|
35
|
+
|
36
|
+
If you are not using Rails, you can configure couchbase-orm with an initializer:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
# config/initializers/couchbase_orm.rb
|
40
|
+
CouchbaseOrm::Connection.config = {
|
41
|
+
connection_string: "couchbase://localhost"
|
42
|
+
username: "dev_user"
|
43
|
+
password: "dev_password"
|
44
|
+
bucket: "dev_bucket"
|
45
|
+
}
|
46
|
+
```
|
33
47
|
|
34
48
|
Views are generated on application load if they don't exist or mismatch.
|
35
49
|
This works fine in production however by default in development models are lazy loaded.
|
@@ -37,16 +51,15 @@ This works fine in production however by default in development models are lazy
|
|
37
51
|
# config/environments/development.rb
|
38
52
|
config.eager_load = true
|
39
53
|
|
40
|
-
|
41
54
|
## Examples
|
42
55
|
|
43
56
|
```ruby
|
44
57
|
require 'couchbase-orm'
|
45
58
|
|
46
59
|
class Post < CouchbaseOrm::Base
|
47
|
-
attribute :title,
|
48
|
-
attribute :body,
|
49
|
-
attribute :draft,
|
60
|
+
attribute :title, :string
|
61
|
+
attribute :body, :string
|
62
|
+
attribute :draft, :boolean
|
50
63
|
end
|
51
64
|
|
52
65
|
p = Post.new(id: 'hello-world',
|
@@ -69,16 +82,45 @@ You can also let the library generate the unique identifier for you:
|
|
69
82
|
p.id #=> "post-abcDE34"
|
70
83
|
```
|
71
84
|
|
72
|
-
You can define connection options on per model basis:
|
85
|
+
<!-- You can define connection options on per model basis:
|
73
86
|
|
74
87
|
```ruby
|
75
88
|
class Post < CouchbaseOrm::Base
|
76
|
-
attribute :title,
|
77
|
-
attribute :body,
|
78
|
-
attribute :draft,
|
89
|
+
attribute :title, :string
|
90
|
+
attribute :body, :string
|
91
|
+
attribute :draft, :boolean
|
79
92
|
|
80
93
|
connect bucket: 'blog', password: ENV['BLOG_BUCKET_PASSWORD']
|
81
94
|
end
|
95
|
+
``` -->
|
96
|
+
|
97
|
+
## Typing
|
98
|
+
|
99
|
+
The following types have been tested :
|
100
|
+
|
101
|
+
- :string
|
102
|
+
- :integer
|
103
|
+
- :float
|
104
|
+
- :boolean
|
105
|
+
- :date
|
106
|
+
- :datetime (stored as iso8601, use precision: n to store more decimal precision)
|
107
|
+
- :timestamp (stored as integer)
|
108
|
+
- :encrypted
|
109
|
+
- see <https://docs.couchbase.com/couchbase-lite/current/c/field-level-encryption.html>
|
110
|
+
- You must store a string that can be encoded in json (not binary data), use base64 if needed
|
111
|
+
- :array (see below)
|
112
|
+
- :nested (see below)
|
113
|
+
|
114
|
+
You can register other types in ActiveModel registry :
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime
|
118
|
+
def serialize(value)
|
119
|
+
value&.iso8601(3)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
ActiveModel::Type.register(:datetime3decimal, DateTimeWith3Decimal)
|
82
124
|
```
|
83
125
|
|
84
126
|
## Validations
|
@@ -89,7 +131,8 @@ context of rails application. You can also enforce types using ruby
|
|
89
131
|
|
90
132
|
```ruby
|
91
133
|
class Comment < Couchbase::Model
|
92
|
-
attribute :author, :
|
134
|
+
attribute :author, :string
|
135
|
+
attribute :body, :string
|
93
136
|
|
94
137
|
validates_presence_of :author, :body
|
95
138
|
end
|
@@ -102,7 +145,8 @@ can then be used for filtering results or ordering.
|
|
102
145
|
|
103
146
|
```ruby
|
104
147
|
class Comment < CouchbaseOrm::Base
|
105
|
-
attribute :author
|
148
|
+
attribute :author :string
|
149
|
+
attribute :body, :string
|
106
150
|
view :all # => emits :id and will return all comments
|
107
151
|
view :by_author, emit_key: :author
|
108
152
|
|
@@ -110,15 +154,12 @@ can then be used for filtering results or ordering.
|
|
110
154
|
# * the by_author view above
|
111
155
|
# * def find_by_author(author); end
|
112
156
|
index_view :author
|
113
|
-
|
157
|
+
|
114
158
|
# You can make compound keys by passing an array to :emit_key
|
115
159
|
# this allow to query by read/unread comments
|
116
160
|
view :by_read, emit_key: [:user_id, :read]
|
117
161
|
# this allow to query by view_count
|
118
162
|
view :by_view_count, emit_key: [:user_id, :view_count]
|
119
|
-
|
120
|
-
|
121
|
-
|
122
163
|
|
123
164
|
validates_presence_of :author, :body
|
124
165
|
end
|
@@ -128,20 +169,129 @@ You can use `Comment.find_by_author('name')` to obtain all the comments by
|
|
128
169
|
a particular author. The same thing, using the view directly would be:
|
129
170
|
`Comment.by_author(key: 'name')`
|
130
171
|
|
131
|
-
When using a compound key, the usage is the same, you just give the full key :
|
172
|
+
When using a compound key, the usage is the same, you just give the full key :
|
132
173
|
|
133
174
|
```ruby
|
134
175
|
Comment.by_read(key: '["'+user_id+'",false]') # gives all unread comments for one particular user
|
135
|
-
|
176
|
+
|
136
177
|
# or even a range !
|
178
|
+
|
179
|
+
Comment.by_view_count(startkey: '["'+user_id+'",10]', endkey: '["'+user_id+'",20]')
|
137
180
|
|
138
|
-
|
181
|
+
# gives all comments that have been seen more than 10 times but less than 20
|
139
182
|
```
|
140
|
-
|
141
|
-
Check this couchbase help page to learn more on what's possible with compound keys : https://developer.couchbase.com/documentation/server/3.x/admin/Views/views-translateSQL.html
|
183
|
+
Check this couchbase help page to learn more on what's possible with compound keys : <https://developer.couchbase.com/documentation/server/3.x/admin/Views/views-translateSQL.html>
|
142
184
|
|
143
185
|
Ex : Compound keys allows to decide the order of the results, and you can reverse it by passing `descending: true`
|
144
186
|
|
187
|
+
```ruby
|
188
|
+
class Comment < CouchbaseOrm::Base19
|
189
|
+
self.ignored_properties = [:old_name] # ignore old_name property in the model
|
190
|
+
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
|
191
|
+
end
|
192
|
+
```
|
193
|
+
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.
|
194
|
+
|
195
|
+
WARNING: If a document exists without a property, the query will failed! So you must be sure that all documents have all properties.
|
196
|
+
|
197
|
+
|
198
|
+
## N1ql
|
199
|
+
|
200
|
+
Like views, it's possible to use N1QL to process some requests used for filtering results or ordering.
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
class Comment < CouchbaseOrm::Base
|
204
|
+
attribute :author, :string
|
205
|
+
attribute :body, :string
|
206
|
+
n1ql :by_author, emit_key: :author
|
207
|
+
|
208
|
+
# Generates two functions:
|
209
|
+
# * the by_author view above
|
210
|
+
# * def find_by_author(author); end
|
211
|
+
index_n1ql :author
|
212
|
+
|
213
|
+
# You can make compound keys by passing an array to :emit_key
|
214
|
+
# this allow to query by read/unread comments
|
215
|
+
n1ql :by_read, emit_key: [:user_id, :read]
|
216
|
+
# this allow to query by view_count
|
217
|
+
n1ql :by_view_count, emit_key: [:user_id, :view_count]
|
218
|
+
|
219
|
+
validates_presence_of :author, :body
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
## Basic Active Record like query engine
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
class Comment < CouchbaseOrm::Base
|
227
|
+
attribute :title, :string
|
228
|
+
attribute :author, :string
|
229
|
+
attribute :category, :string
|
230
|
+
attribute :ratings, :number
|
231
|
+
end
|
232
|
+
|
233
|
+
Comment.where(author: "Anne McCaffrey", category: ['S-F', 'Fantasy']).not(ratings: 0).order(:title).limit(10)
|
234
|
+
|
235
|
+
# Relation can be composed as in AR:
|
236
|
+
|
237
|
+
amc_comments = Comment.where(author: "Anne McCaffrey")
|
238
|
+
|
239
|
+
amc_comments.count
|
240
|
+
|
241
|
+
amc_sf_comments = amc_comments.where(category: 'S-F')
|
242
|
+
|
243
|
+
# pluck is available, but will query all object fields first
|
244
|
+
|
245
|
+
Comment.pluck(:title, :ratings)
|
246
|
+
|
247
|
+
# To load the ids without loading the models
|
248
|
+
|
249
|
+
Comment.where(author: "David Eddings").ids
|
250
|
+
|
251
|
+
# To delete all the models of a relation
|
252
|
+
|
253
|
+
Comment.where(ratings: 0).delete_all
|
254
|
+
```
|
255
|
+
|
256
|
+
## scopes
|
257
|
+
|
258
|
+
Scopes can be written as class method, scope method is not implemented yet.
|
259
|
+
They can be chained as in AR or mixed with relation methods.
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
class Comment < CouchbaseOrm::Base
|
263
|
+
attribute :title, :string
|
264
|
+
attribute :author, :string
|
265
|
+
attribute :category, :string
|
266
|
+
attribute :ratings, :number
|
267
|
+
|
268
|
+
def self.by_author(author)
|
269
|
+
where(author: author)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
Comment.by_author("Anne McCaffrey").where(category: 'S-F').not(ratings: 0).order(:title).limit(10)
|
274
|
+
```
|
275
|
+
|
276
|
+
## Operators
|
277
|
+
|
278
|
+
Several operators are available to filter numerical results : \_gt, \_lt, \_gte, \_lte, \_ne
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
Comment.where(ratings: {_gt: 3})
|
282
|
+
```
|
283
|
+
|
284
|
+
## Range in the where
|
285
|
+
|
286
|
+
You can specify a Range of date or intger in the where clause
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
Person.where(birth_date: DateTime.new(1980, 1, 1)..DateTime.new(1990, 1, 1))
|
290
|
+
Person.where(age: 10..20)
|
291
|
+
|
292
|
+
Person.where(age: 10...20) # to exclude the upper bound
|
293
|
+
```
|
294
|
+
|
145
295
|
## Associations and Indexes
|
146
296
|
|
147
297
|
There are common active record helpers available for use `belongs_to` and `has_many`
|
@@ -155,21 +305,77 @@ There are common active record helpers available for use `belongs_to` and `has_m
|
|
155
305
|
has_many :comments, dependent: :destroy
|
156
306
|
|
157
307
|
# You can ensure an attribute is unique for this model
|
158
|
-
attribute :email,
|
308
|
+
attribute :email, :string
|
159
309
|
ensure_unique :email
|
160
310
|
end
|
161
311
|
```
|
162
312
|
|
313
|
+
By default, `has_many` uses a view for association,
|
314
|
+
but you can define a `type` option to specify an association using N1QL instead:
|
315
|
+
|
316
|
+
```ruby
|
317
|
+
class Comment < CouchbaseOrm::Base
|
318
|
+
belongs_to :author
|
319
|
+
end
|
320
|
+
|
321
|
+
class Author < CouchbaseOrm::Base
|
322
|
+
has_many :comments, type: :n1ql, dependent: :destroy
|
323
|
+
end
|
324
|
+
```
|
325
|
+
|
326
|
+
## Nested
|
327
|
+
|
328
|
+
Attributes can be of type nested, they must specify a type of NestedDocument.
|
329
|
+
The NestedValidation triggers nested validation on parent validation.
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
class Address < CouchbaseOrm::NestedDocument
|
333
|
+
attribute :road, :string
|
334
|
+
attribute :city, :string
|
335
|
+
validates :road, :city, presence: true
|
336
|
+
end
|
337
|
+
|
338
|
+
class Author < CouchbaseOrm::Base
|
339
|
+
attribute :address, :nested, type: Address
|
340
|
+
validates :address, nested: true
|
341
|
+
end
|
342
|
+
```
|
343
|
+
|
344
|
+
Model can be queried using the nested attributes
|
345
|
+
|
346
|
+
```ruby
|
347
|
+
Author.where(address: {road: '1 rue de la paix', city: 'Paris'})
|
348
|
+
```
|
349
|
+
|
350
|
+
## Array
|
351
|
+
|
352
|
+
Attributes can be of type array, they must contain something that can be serialized and deserialized to/from JSON.
|
353
|
+
You can enforce the type of array elements. The type can be a NestedDocument
|
354
|
+
|
355
|
+
```ruby
|
356
|
+
class Book < CouchbaseOrm::NestedDocument
|
357
|
+
attribute :name, :string
|
358
|
+
validates :name, presence: true
|
359
|
+
end
|
360
|
+
|
361
|
+
class Author < CouchbaseOrm::Base
|
362
|
+
attribute things, :array
|
363
|
+
attribute flags, :array, type: :string
|
364
|
+
attribute books, :array, type: Book
|
365
|
+
|
366
|
+
validates :books, nested: true
|
367
|
+
end
|
368
|
+
```
|
163
369
|
|
164
370
|
## Performance Comparison with Couchbase-Ruby-Model
|
165
371
|
|
166
372
|
Basically we migrated an application from [Couchbase Ruby Model](https://github.com/couchbase/couchbase-ruby-model)
|
167
373
|
to [Couchbase-ORM](https://github.com/acaprojects/couchbase-orm) (this project)
|
168
374
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
375
|
+
- Rails 5 production
|
376
|
+
- Puma as the webserver
|
377
|
+
- Running on a 2015 Macbook Pro
|
378
|
+
- Performance test: `siege -c250 -r10 http://localhost:3000/auth/authority`
|
173
379
|
|
174
380
|
The request above pulls the same database document each time and returns it. A simple O(1) operation.
|
175
381
|
|
data/ci/run_couchbase.sh
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
set -x
|
2
|
+
set -e
|
3
|
+
|
4
|
+
VERSION=$1
|
5
|
+
BUCKET=$2
|
6
|
+
USER=$3
|
7
|
+
PASSWORD=$4
|
8
|
+
|
9
|
+
|
10
|
+
wget https://packages.couchbase.com/releases/$VERSION/couchbase-server-enterprise_$VERSION-ubuntu20.04_amd64.deb
|
11
|
+
dpkg -i couchbase-server-enterprise_$VERSION-ubuntu20.04_amd64.deb
|
12
|
+
sleep 8
|
13
|
+
sudo service couchbase-server status
|
14
|
+
/opt/couchbase/bin/couchbase-cli cluster-init -c 127.0.0.1:8091 --cluster-username=admin --cluster-password=password --cluster-ramsize=320 --cluster-index-ramsize=256 --cluster-fts-ramsize=256 --services=data,index,query,fts
|
15
|
+
sleep 5
|
16
|
+
/opt/couchbase/bin/couchbase-cli server-info -c 127.0.0.1:8091 -u admin -p password
|
17
|
+
/opt/couchbase/bin/couchbase-cli bucket-create -c 127.0.0.1:8091 -u admin -p password --bucket=$BUCKET --bucket-type=couchbase --bucket-ramsize=160 --bucket-replica=0 --wait
|
18
|
+
sleep 1
|
19
|
+
/opt/couchbase/bin/couchbase-cli user-manage -c 127.0.0.1:8091 -u admin -p password --set --rbac-username $USER --rbac-password $PASSWORD --rbac-name "Auto Tester" --roles admin --auth-domain local
|
20
|
+
curl http://admin:password@localhost:8093/query/service -d "statement=CREATE INDEX \`default_type\` ON \`$BUCKET\`(\`type\`)"
|
21
|
+
curl http://admin:password@localhost:8093/query/service -d "statement=CREATE INDEX \`default_rating\` ON \`$BUCKET\`(\`rating\`)"
|
22
|
+
curl http://admin:password@localhost:8093/query/service -d "statement=CREATE INDEX \`default_name\` ON \`$BUCKET\`(\`name\`)"
|
data/couchbase-orm.gemspec
CHANGED
@@ -1,27 +1,33 @@
|
|
1
|
-
require File.expand_path(
|
1
|
+
require File.expand_path('../lib/couchbase-orm/version', __FILE__)
|
2
2
|
|
3
3
|
Gem::Specification.new do |gem|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
gem.description = "A Couchbase ORM for Rails"
|
4
|
+
gem.name = 'couchbase-orm'
|
5
|
+
gem.version = CouchbaseOrm::VERSION
|
6
|
+
gem.license = 'MIT'
|
7
|
+
gem.authors = ['Stephen von Takach', 'Gauthier Monserand', 'Pierre Merlin', 'Julien Burnet-Fauche']
|
8
|
+
gem.homepage = 'https://github.com/doctolib/couchbase-orm'
|
9
|
+
gem.summary = 'Couchbase ORM for Rails'
|
10
|
+
gem.description = 'A Couchbase ORM for Rails'
|
12
11
|
|
13
|
-
|
14
|
-
|
12
|
+
gem.required_ruby_version = '>= 2.7.0'
|
13
|
+
gem.require_paths = ['lib']
|
15
14
|
|
16
|
-
|
17
|
-
gem.add_runtime_dependency 'activemodel', '>= 4.0', '< 6.0'
|
18
|
-
gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base
|
15
|
+
gem.add_runtime_dependency 'activemodel', ENV['ACTIVE_MODEL_VERSION'] || '>= 5.2'
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
gem.add_development_dependency 'minitest', '~> 5.10'
|
17
|
+
gem.add_runtime_dependency 'couchbase', '>= 3.4.2'
|
18
|
+
gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base
|
19
|
+
gem.add_runtime_dependency 'json-schema', '>= 3' # validating JSON against a schema
|
24
20
|
|
25
|
-
|
26
|
-
|
21
|
+
gem.add_development_dependency 'rake', '~> 12.2'
|
22
|
+
gem.add_development_dependency 'rspec', '~> 3.7'
|
23
|
+
gem.add_development_dependency 'yard', '~> 0.9'
|
24
|
+
gem.add_development_dependency 'pry'
|
25
|
+
gem.add_development_dependency 'pry-stack_explorer'
|
26
|
+
gem.add_development_dependency 'simplecov'
|
27
|
+
gem.add_development_dependency 'actionpack'
|
28
|
+
gem.add_development_dependency 'timecop'
|
29
|
+
gem.add_development_dependency 'base64'
|
30
|
+
|
31
|
+
gem.files = `git ls-files`.split("\n")
|
32
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
27
33
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_model'
|
3
|
+
require 'active_support/hash_with_indifferent_access'
|
4
|
+
|
5
|
+
module CouchbaseOrm
|
6
|
+
|
7
|
+
# Set of methods defined in ActiveRecord and needed for CouchbaseOrm
|
8
|
+
# try to avoid dependencies on too many active record classes
|
9
|
+
# by exemple we don't want to go down to the concept of tables
|
10
|
+
module ActiveRecordCompat
|
11
|
+
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def primary_key
|
17
|
+
'id'
|
18
|
+
end
|
19
|
+
|
20
|
+
def base_class?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def column_names # can't be an alias for now
|
25
|
+
attribute_names
|
26
|
+
end
|
27
|
+
|
28
|
+
def abstract_class?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def connected?
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
def table_exists?
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def _reflect_on_association(_attribute)
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def type_for_attribute(attribute)
|
45
|
+
attribute_types[attribute]
|
46
|
+
end
|
47
|
+
|
48
|
+
if ActiveModel::VERSION::MAJOR < 6
|
49
|
+
def attribute_names
|
50
|
+
attribute_types.keys
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def slice(*methods)
|
56
|
+
HashWithIndifferentAccess.new(methods.flatten.to_h { |method| [method, public_send(method)] })
|
57
|
+
end
|
58
|
+
|
59
|
+
def values_at(*methods)
|
60
|
+
methods.flatten.map! { |method| public_send(method) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def _has_attribute?(attr_name)
|
64
|
+
attribute_names.include?(attr_name.to_s)
|
65
|
+
end
|
66
|
+
|
67
|
+
def attribute_for_inspect(attr_name)
|
68
|
+
value = send(attr_name)
|
69
|
+
value.inspect
|
70
|
+
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
|
+
end
|
92
|
+
end
|