activemodel-datastore 0.7.0 → 0.9.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/CHANGELOG.md +9 -0
- data/README.md +119 -128
- data/lib/active_model/datastore/carrier_wave_uploader.rb +3 -1
- data/lib/active_model/datastore/connection.rb +6 -2
- data/lib/active_model/datastore/nested_attr.rb +3 -3
- data/lib/active_model/datastore/track_changes.rb +2 -2
- data/lib/active_model/datastore/version.rb +1 -1
- data/lib/active_model/datastore.rb +41 -11
- metadata +6 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3126172160a1df49727e648493401c8448f62a1442cba738dcf139d6406dfdce
|
|
4
|
+
data.tar.gz: 24bc7d6985f16739f16561611255d23ac6862cd72502f50827462a3c0674e6aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc4e6401b2932e11c37b509dc1f98561423b6908d0d11f0465363090239a67ae6f7dc02cc5aecedc6fb72e4e8168f44a6ddc9fe38d49665fc6fc95838e0144a8
|
|
7
|
+
data.tar.gz: 7acf7b5d14306afb3412c7ac667806e05708de0b9a8e8348b172bafa337316f4b11160f68eee3bb1152e27d7fa6ae066d4d28e17f430365b6caf003a3dfdbd1c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
### 0.9.0 / 2026-08-24
|
|
2
|
+
* identify the Datastore operation, entity kind, and failed-attempt elapsed time in retry logs
|
|
3
|
+
* use a configurable retry logger that defaults to Rails.logger and falls back to standard output
|
|
4
|
+
* update the development environment to Ruby 3.4.10 and the example application to Rails 8.1
|
|
5
|
+
* test CarrierWave integration against CarrierWave 3.1
|
|
6
|
+
|
|
7
|
+
### 0.8.0 / 2024-03-05
|
|
8
|
+
* updating GCLOUD_KEYFILE_JSON to work with the latest version of GoogleAuth
|
|
9
|
+
|
|
1
10
|
### 0.7.0 / 2021-11-04
|
|
2
11
|
* adding support for Ruby 3
|
|
3
12
|
|
data/README.md
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
Active Model Datastore
|
|
2
2
|
===================================
|
|
3
3
|
|
|
4
|
-
Makes the [google-cloud-datastore](https://
|
|
5
|
-
|
|
4
|
+
Makes the [google-cloud-datastore](https://cloud.google.com/ruby/docs/reference/google-cloud-datastore/latest)
|
|
5
|
+
gem compliant with [active_model](https://github.com/rails/rails/tree/master/activemodel)
|
|
6
|
+
conventions and compatible with your Rails 5+ applications.
|
|
6
7
|
|
|
7
|
-
Why would you want to use Google's NoSQL
|
|
8
|
-
with Rails?
|
|
8
|
+
Why would you want to use Google's NoSQL
|
|
9
|
+
[Firestore in Datastore mode](https://docs.cloud.google.com/datastore/docs) with Rails?
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Use it when you want a Rails app backed by a fully managed, massively scalable NoSQL database,
|
|
12
|
+
without provisioning database servers or manually sharding data. Datastore stores records as
|
|
13
|
+
entities with flexible properties, so your models do not require a fixed database schema. It
|
|
14
|
+
automatically handles scaling and replication, provides highly available and durable storage, and
|
|
15
|
+
supports indexed queries and ACID transactions.
|
|
14
16
|
|
|
15
17
|
[](https://badge.fury.io/rb/activemodel-datastore)
|
|
16
|
-
[](https://github.com/Agrimatics/activemodel-datastore/actions/workflows/ci.yml)
|
|
17
19
|
|
|
18
20
|
## Table of contents
|
|
19
21
|
|
|
@@ -21,9 +23,9 @@ suited for unstructured or semi-structured application data.
|
|
|
21
23
|
- [Model Example](#model)
|
|
22
24
|
- [Controller Example](#controller)
|
|
23
25
|
- [Retrieving Entities](#queries)
|
|
24
|
-
- [Datastore Consistency](#consistency)
|
|
26
|
+
- [Datastore Consistency and Concurrency](#consistency)
|
|
25
27
|
- [Datastore Indexes](#indexes)
|
|
26
|
-
- [
|
|
28
|
+
- [Firestore Emulator](#emulator)
|
|
27
29
|
- [Example Rails App](#rails)
|
|
28
30
|
- [CarrierWave File Uploads](#carrierwave)
|
|
29
31
|
- [Track Changes](#track_changes)
|
|
@@ -48,12 +50,13 @@ gem 'activemodel-datastore'
|
|
|
48
50
|
|
|
49
51
|
Create a Google Cloud account [here](https://cloud.google.com) and create a project.
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Google Cloud
|
|
55
|
-
credentials
|
|
56
|
-
|
|
53
|
+
Follow the [activation instructions](https://cloud.google.com/datastore/docs/activate) to create a
|
|
54
|
+
Firestore in Datastore mode database for the project.
|
|
55
|
+
|
|
56
|
+
The Google Cloud client libraries use Application Default Credentials (ADC). On Google Cloud,
|
|
57
|
+
credentials are discovered automatically from the service account attached to the application. Grant
|
|
58
|
+
that service account only the access it needs; `roles/datastore.user` provides read/write access to
|
|
59
|
+
Datastore data.
|
|
57
60
|
|
|
58
61
|
Set your project id in an `ENV` variable named `GCLOUD_PROJECT`.
|
|
59
62
|
|
|
@@ -63,22 +66,30 @@ To locate your project ID:
|
|
|
63
66
|
2. From the projects list, select the name of your project.
|
|
64
67
|
3. On the left, click Dashboard. The project name and ID are displayed in the Dashboard.
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
For applications outside Google Cloud, configure ADC for the hosting environment. If a service account
|
|
70
|
+
key is required, the Ruby client supports `GOOGLE_APPLICATION_CREDENTIALS` with the path to its JSON
|
|
71
|
+
file. Active Model Datastore also supports the following environment variables for platforms where
|
|
72
|
+
the JSON must be stored directly in environment variables:
|
|
70
73
|
|
|
71
74
|
```bash
|
|
72
|
-
SERVICE_ACCOUNT_PRIVATE_KEY
|
|
73
|
-
SERVICE_ACCOUNT_CLIENT_EMAIL
|
|
75
|
+
export SERVICE_ACCOUNT_PRIVATE_KEY='-----BEGIN PRIVATE KEY-----\nMIIFfb3...5dmFtABy\n-----END PRIVATE KEY-----\n'
|
|
76
|
+
export SERVICE_ACCOUNT_CLIENT_EMAIL='web-app@app-name.iam.gserviceaccount.com'
|
|
74
77
|
```
|
|
75
78
|
|
|
76
|
-
On Heroku the
|
|
79
|
+
On Heroku the environment variables can be set under **Settings > Config Vars**.
|
|
77
80
|
|
|
78
81
|
Active Model Datastore will then handle the authentication for you, and the datastore instance can
|
|
79
82
|
be accessed with `CloudDatastore.dataset`.
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
Datastore retries use `Rails.logger` automatically when Rails is available, which preserves tagged
|
|
85
|
+
logging such as request IDs. Other applications fall back to standard output. To use another logger,
|
|
86
|
+
configure it during application initialization:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
ActiveModel::Datastore.logger = MyApplication.logger
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
There is an example Puma config file [here](https://github.com/Agrimatics/activemodel-datastore/blob/main/test/support/datastore_example_rails_app/config/puma.rb).
|
|
82
93
|
|
|
83
94
|
## <a name="model"></a>Model Example
|
|
84
95
|
|
|
@@ -96,18 +107,18 @@ class User
|
|
|
96
107
|
end
|
|
97
108
|
```
|
|
98
109
|
|
|
99
|
-
Data objects in
|
|
110
|
+
Data objects in Datastore are known as entities. Entities are of a kind. An entity has one
|
|
100
111
|
or more named properties, each of which can have one or more values. Think of them like this:
|
|
101
112
|
* 'Kind' (which is your table and the name of your Rails model)
|
|
102
113
|
* 'Entity' (which is the record from the table)
|
|
103
114
|
* 'Property' (which is the attribute of the record)
|
|
104
115
|
|
|
105
|
-
The `entity_properties` method defines an Array of properties that belong to the entity in
|
|
106
|
-
|
|
116
|
+
The `entity_properties` method defines an Array of properties that belong to the entity in
|
|
117
|
+
Datastore. Define the attributes of your model using `attr_accessor`. With this approach, Rails
|
|
107
118
|
deals solely with ActiveModel objects. The objects are converted to/from entities automatically
|
|
108
119
|
during save/query operations. You can still use virtual attributes on the model (such as the
|
|
109
120
|
`:state` attribute above) by simply excluding it from `entity_properties`. In this example state
|
|
110
|
-
is available to the model but won't be persisted with the entity in
|
|
121
|
+
is available to the model but won't be persisted with the entity in Datastore.
|
|
111
122
|
|
|
112
123
|
Validations work as you would expect:
|
|
113
124
|
|
|
@@ -225,12 +236,12 @@ end
|
|
|
225
236
|
|
|
226
237
|
## <a name="queries"></a>Retrieving Entities
|
|
227
238
|
|
|
228
|
-
Each entity in
|
|
239
|
+
Each entity in Datastore has a key that uniquely identifies it. The key consists of the
|
|
229
240
|
following components:
|
|
230
241
|
|
|
231
242
|
* the kind of the entity, which is User in these examples
|
|
232
243
|
* an identifier for the individual entity, which can be either a a key name string or an integer numeric ID
|
|
233
|
-
* an optional ancestor path locating the entity within the
|
|
244
|
+
* an optional ancestor path locating the entity within the Datastore hierarchy
|
|
234
245
|
|
|
235
246
|
#### [all(options = {})](http://www.rubydoc.info/gems/activemodel-datastore/ActiveModel%2FDatastore%2FClassMethods:all)
|
|
236
247
|
Queries entities using the provided options. When a limit option is provided queries up to the limit
|
|
@@ -279,34 +290,34 @@ user = User.find_by(name: 'Joe')
|
|
|
279
290
|
user = User.find_by(name: 'Bryce', ancestor: parent_key)
|
|
280
291
|
```
|
|
281
292
|
|
|
282
|
-
|
|
283
|
-
work,
|
|
284
|
-
|
|
285
|
-
## <a name="consistency"></a>Datastore Consistency
|
|
293
|
+
Google documents how [Datastore queries](https://cloud.google.com/datastore/docs/concepts/queries)
|
|
294
|
+
work, including their [restrictions](https://cloud.google.com/datastore/docs/concepts/queries#restrictions_on_queries).
|
|
286
295
|
|
|
287
|
-
|
|
288
|
-
machines and uses synchronous replication over a wide geographic area. Because of this architecture
|
|
289
|
-
it offers a balance of strong and eventual consistency.
|
|
296
|
+
## <a name="consistency"></a>Datastore Consistency and Concurrency
|
|
290
297
|
|
|
291
|
-
|
|
298
|
+
Firestore in Datastore mode is strongly consistent by default. Queries and key lookups reflect
|
|
299
|
+
completed writes, including queries that do not use an ancestor. The legacy Cloud Datastore behavior
|
|
300
|
+
where a newly created entity might not immediately appear in a global query does not apply.
|
|
292
301
|
|
|
293
|
-
|
|
294
|
-
Eventual consistency is a theoretical guarantee that, provided no new updates to an entity are made,
|
|
295
|
-
all reads of the entity will eventually return the last updated value.
|
|
302
|
+
Datastore supports three concurrency modes that determine how concurrent transactions interact:
|
|
296
303
|
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
- `PESSIMISTIC` uses reader/writer locks and is the default for new databases.
|
|
305
|
+
- `OPTIMISTIC` allows concurrent transactions, but only the first conflicting transaction to commit
|
|
306
|
+
succeeds.
|
|
307
|
+
- `OPTIMISTIC_WITH_ENTITY_GROUPS` preserves legacy Cloud Datastore entity-group transaction
|
|
308
|
+
semantics. Transactions are limited to 25 entity groups, writes to an entity group are limited to
|
|
309
|
+
one per second, and queries within transactions must be ancestor queries.
|
|
299
310
|
|
|
300
|
-
|
|
311
|
+
Use the following command to inspect a database's concurrency mode:
|
|
301
312
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
313
|
+
```bash
|
|
314
|
+
gcloud firestore databases describe --project=PROJECT_ID --database=DATABASE_ID
|
|
315
|
+
```
|
|
305
316
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
317
|
+
Entity groups are hierarchies formed by a root entity and its children. They are useful for modeling
|
|
318
|
+
related entities and are required by the transactional restrictions of
|
|
319
|
+
`OPTIMISTIC_WITH_ENTITY_GROUPS`. To create an entity group, specify an ancestor path as part of the
|
|
320
|
+
child entity's key.
|
|
310
321
|
|
|
311
322
|
Before using the `save` method, assign the `parent_key_id` attribute an ID. Let's say that 12345
|
|
312
323
|
represents the ID of the company that the users belong to. The key of the user entity will now
|
|
@@ -314,14 +325,10 @@ look like this:
|
|
|
314
325
|
|
|
315
326
|
`@key=#<Google::Cloud::Datastore::Key @kind="User", @id=1, @parent=#<Google::Cloud::Datastore::Key @kind="ParentUser", @id=12345>>`
|
|
316
327
|
|
|
317
|
-
All of the User entities will now belong to an entity group named ParentUser and can be queried by the
|
|
328
|
+
All of the User entities will now belong to an entity group named ParentUser and can be queried by the
|
|
318
329
|
Company ID. When we query for the users we will provide User.parent_key(12345) as the ancestor option.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
However, there is a small downside. Entities with the same ancestor are limited to 1 write per second.
|
|
323
|
-
Also, the entity group relationship cannot be changed after creating the entity (as you can't modify
|
|
324
|
-
an entity's key after it has been saved).
|
|
330
|
+
The entity group relationship cannot be changed after creating the entity because an entity's key
|
|
331
|
+
cannot be modified after it has been saved.
|
|
325
332
|
|
|
326
333
|
The Users controller would now look like this:
|
|
327
334
|
|
|
@@ -384,24 +391,24 @@ class UsersController < ApplicationController
|
|
|
384
391
|
end
|
|
385
392
|
```
|
|
386
393
|
|
|
387
|
-
See
|
|
394
|
+
See the Datastore documentation for details about
|
|
395
|
+
[transactions, isolation, and concurrency modes](https://cloud.google.com/datastore/docs/concepts/transactions#concurrency_modes).
|
|
388
396
|
|
|
389
397
|
## <a name="indexes"></a>Datastore Indexes
|
|
390
398
|
|
|
391
|
-
Every
|
|
399
|
+
Every Datastore query computes its results using one or more indexes. The
|
|
392
400
|
indexes contain entity keys in a sequence specified by the index's properties and, optionally,
|
|
393
401
|
the entity's ancestors.
|
|
394
402
|
|
|
395
403
|
There are two types of indexes, *built-in* and *composite*.
|
|
396
404
|
|
|
397
405
|
#### Built-in
|
|
398
|
-
By default,
|
|
399
|
-
These single property indexes are suitable for simple types of queries.
|
|
400
|
-
do not count against your index limit.
|
|
406
|
+
By default, Datastore automatically predefines an index for each property of each entity kind.
|
|
407
|
+
These single property indexes are suitable for simple types of queries.
|
|
401
408
|
|
|
402
409
|
#### Composite
|
|
403
|
-
Composite
|
|
404
|
-
queries and are defined in an index.yaml file.
|
|
410
|
+
Composite indexes include multiple property values per indexed entity. Composite indexes support
|
|
411
|
+
complex queries and are defined in an `index.yaml` file.
|
|
405
412
|
|
|
406
413
|
Composite indexes are required for queries of the following form:
|
|
407
414
|
|
|
@@ -413,65 +420,50 @@ Composite indexes are required for queries of the following form:
|
|
|
413
420
|
|
|
414
421
|
*NOTE*: Inequality filters are LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL.
|
|
415
422
|
|
|
416
|
-
Google
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
Add the following line to your ~/.bash_profile:
|
|
445
|
-
|
|
446
|
-
export PATH="~/google-cloud-sdk/platform/cloud-datastore-emulator:$PATH"
|
|
447
|
-
|
|
448
|
-
Restart your shell:
|
|
449
|
-
|
|
450
|
-
exec -l $SHELL
|
|
451
|
-
|
|
452
|
-
To create the local development datastore execute the following from the root of the project:
|
|
453
|
-
|
|
454
|
-
$ cloud_datastore_emulator create tmp/local_datastore
|
|
455
|
-
|
|
456
|
-
To create the local test datastore execute the following from the root of the project:
|
|
457
|
-
|
|
458
|
-
$ cloud_datastore_emulator create tmp/test_datastore
|
|
459
|
-
|
|
460
|
-
To start the local Cloud Datastore emulator:
|
|
461
|
-
|
|
462
|
-
$ cloud_datastore_emulator start --port=8180 tmp/local_datastore
|
|
463
|
-
|
|
423
|
+
See Google's [Datastore index documentation](https://cloud.google.com/datastore/docs/concepts/indexes)
|
|
424
|
+
for more information.
|
|
425
|
+
|
|
426
|
+
## <a name="emulator"></a>Firestore Emulator
|
|
427
|
+
|
|
428
|
+
Install the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) and the Firestore emulator:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
gcloud components install cloud-firestore-emulator
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
The emulator requires Java 21 or later. Add the emulator executable to your shell's `PATH` because
|
|
435
|
+
the test helpers invoke it directly:
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
export PATH="$HOME/google-cloud-sdk/platform/cloud-firestore-emulator:$PATH"
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
The Firestore emulator runs in memory by default, so there is no datastore directory to create.
|
|
442
|
+
Start it in Datastore mode on the development port with:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
cloud_firestore_emulator start --database-mode=datastore-mode --port=8180
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Set `DATASTORE_EMULATOR_HOST=localhost:8180` so the Ruby client connects to the emulator. The gem sets
|
|
449
|
+
this automatically for Rails development and uses port 8181 for Rails tests.
|
|
450
|
+
|
|
464
451
|
## <a name="rails"></a>Example Rails App
|
|
465
452
|
|
|
466
|
-
There is an example Rails
|
|
453
|
+
There is an example Rails 8.1 app in the test directory [here](https://github.com/Agrimatics/activemodel-datastore/tree/main/test/support/datastore_example_rails_app).
|
|
454
|
+
|
|
455
|
+
Start the Firestore emulator in Datastore mode in one terminal:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
cloud_firestore_emulator start --database-mode=datastore-mode --port=8180
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Then start the example application in another terminal:
|
|
467
462
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
$ ./start-local-datastore.sh
|
|
473
|
-
$ rails s
|
|
474
|
-
```
|
|
463
|
+
```bash
|
|
464
|
+
bundle install
|
|
465
|
+
rails server
|
|
466
|
+
```
|
|
475
467
|
|
|
476
468
|
Navigate to http://localhost:3000.
|
|
477
469
|
|
|
@@ -577,13 +569,12 @@ class Recipe
|
|
|
577
569
|
end
|
|
578
570
|
```
|
|
579
571
|
|
|
580
|
-
|
|
572
|
+
Alternatively, `:reject_if` also accepts a symbol naming a method:
|
|
581
573
|
|
|
582
574
|
```ruby
|
|
583
575
|
class Recipe
|
|
584
576
|
def ingredients_attributes=(attributes)
|
|
585
|
-
|
|
586
|
-
assign_nested_attributes(:ingredients, attributes, reject_if: reject_recipes)
|
|
577
|
+
assign_nested_attributes(:ingredients, attributes, reject_if: :reject_recipes)
|
|
587
578
|
end
|
|
588
579
|
|
|
589
580
|
def reject_recipes(attributes)
|
|
@@ -594,12 +585,12 @@ end
|
|
|
594
585
|
|
|
595
586
|
Within the parent model `valid?` will validate the parent and associated children and
|
|
596
587
|
`nested_models` will return the child objects. If the nested form submitted params contained
|
|
597
|
-
a truthy `_destroy` key, the appropriate
|
|
598
|
-
to
|
|
588
|
+
a truthy `_destroy` key, the appropriate nested models will have `marked_for_destruction` set
|
|
589
|
+
to `true`.
|
|
599
590
|
|
|
600
591
|
## <a name="gotchas"></a>Datastore Gotchas
|
|
601
592
|
#### Ordering of query results is undefined when no sort order is specified.
|
|
602
593
|
When a query does not specify a sort order, the results are returned in the order they are retrieved.
|
|
603
|
-
As
|
|
594
|
+
As the Datastore implementation evolves (or if a project's indexes change), this order may change.
|
|
604
595
|
Therefore, if your application requires its query results in a particular order, be sure to specify
|
|
605
596
|
that sort order explicitly in the query.
|
|
@@ -44,7 +44,9 @@ module CarrierWaveUploader
|
|
|
44
44
|
self.class.uploaders.keys.each do |attr_name|
|
|
45
45
|
entity[attr_name] = append_files(entity[attr_name], existing_files[attr_name])
|
|
46
46
|
end
|
|
47
|
-
self.class.retry_on_exception?
|
|
47
|
+
self.class.retry_on_exception?(operation: 'save') do
|
|
48
|
+
CloudDatastore.dataset.save entity
|
|
49
|
+
end
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -21,8 +21,12 @@ module CloudDatastore
|
|
|
21
21
|
ENV['GCLOUD_PROJECT'] ||= 'test-datastore'
|
|
22
22
|
elsif ENV['SERVICE_ACCOUNT_PRIVATE_KEY'].present? &&
|
|
23
23
|
ENV['SERVICE_ACCOUNT_CLIENT_EMAIL'].present?
|
|
24
|
-
ENV['GCLOUD_KEYFILE_JSON'] ||=
|
|
25
|
-
|
|
24
|
+
ENV['GCLOUD_KEYFILE_JSON'] ||=
|
|
25
|
+
'{' \
|
|
26
|
+
'"private_key": "' + ENV['SERVICE_ACCOUNT_PRIVATE_KEY'] + '",' \
|
|
27
|
+
'"client_email": "' + ENV['SERVICE_ACCOUNT_CLIENT_EMAIL'] + '",' \
|
|
28
|
+
'"type": "service_account"' \
|
|
29
|
+
'}'
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
|
|
@@ -78,6 +78,8 @@ module ActiveModel::Datastore
|
|
|
78
78
|
extend ActiveSupport::Concern
|
|
79
79
|
include ActiveModel::Model
|
|
80
80
|
|
|
81
|
+
UNASSIGNABLE_KEYS = %w[id _destroy].freeze
|
|
82
|
+
|
|
81
83
|
included do
|
|
82
84
|
attr_accessor :nested_attributes, :marked_for_destruction, :_destroy
|
|
83
85
|
end
|
|
@@ -173,8 +175,6 @@ module ActiveModel::Datastore
|
|
|
173
175
|
|
|
174
176
|
private
|
|
175
177
|
|
|
176
|
-
UNASSIGNABLE_KEYS = %w[id _destroy].freeze
|
|
177
|
-
|
|
178
178
|
def validate_attributes(attributes)
|
|
179
179
|
attributes = attributes.to_h if attributes.respond_to?(:permitted?)
|
|
180
180
|
unless attributes.is_a?(Hash)
|
|
@@ -248,7 +248,7 @@ module ActiveModel::Datastore
|
|
|
248
248
|
def validate_each(record, attribute, value)
|
|
249
249
|
return unless Array(value).reject(&:valid?).any?
|
|
250
250
|
|
|
251
|
-
record.errors.add(attribute, :invalid, **options
|
|
251
|
+
record.errors.add(attribute, :invalid, **options, value: value)
|
|
252
252
|
end
|
|
253
253
|
end
|
|
254
254
|
end
|
|
@@ -71,7 +71,7 @@ module ActiveModel::Datastore
|
|
|
71
71
|
raise TrackChangesError, 'Object has not been configured for change tracking.'
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
changed = marked_for_destruction?
|
|
74
|
+
changed = marked_for_destruction? || false
|
|
75
75
|
tracked_attributes.each do |attr|
|
|
76
76
|
break if changed
|
|
77
77
|
|
|
@@ -88,7 +88,7 @@ module ActiveModel::Datastore
|
|
|
88
88
|
with_changes = Array(send(attr.to_sym)).select(&:values_changed?)
|
|
89
89
|
send("#{attr}=", with_changes)
|
|
90
90
|
end
|
|
91
|
-
nested_attributes.delete_if { |attr| Array(send(attr.to_sym)).
|
|
91
|
+
nested_attributes.delete_if { |attr| Array(send(attr.to_sym)).empty? }
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
module ClassMethods
|
|
@@ -119,6 +119,14 @@ module ActiveModel::Datastore
|
|
|
119
119
|
include ActiveModel::Datastore::PropertyValues
|
|
120
120
|
include ActiveModel::Datastore::TrackChanges
|
|
121
121
|
|
|
122
|
+
def self.logger
|
|
123
|
+
@logger || (Rails.logger if defined?(Rails) == 'constant')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.logger=(logger)
|
|
127
|
+
@logger = logger
|
|
128
|
+
end
|
|
129
|
+
|
|
122
130
|
included do
|
|
123
131
|
private_class_method :query_options, :query_sort, :query_property_filter, :find_all_entities
|
|
124
132
|
define_model_callbacks :save, :update, :destroy
|
|
@@ -183,7 +191,7 @@ module ActiveModel::Datastore
|
|
|
183
191
|
|
|
184
192
|
run_callbacks :update do
|
|
185
193
|
entity = build_entity
|
|
186
|
-
self.class.retry_on_exception? { CloudDatastore.dataset.save entity }
|
|
194
|
+
self.class.retry_on_exception?(operation: 'save') { CloudDatastore.dataset.save entity }
|
|
187
195
|
end
|
|
188
196
|
end
|
|
189
197
|
|
|
@@ -191,7 +199,7 @@ module ActiveModel::Datastore
|
|
|
191
199
|
run_callbacks :destroy do
|
|
192
200
|
key = CloudDatastore.dataset.key self.class.name, id
|
|
193
201
|
key.parent = self.class.parent_key(parent_key_id) if parent?
|
|
194
|
-
self.class.retry_on_exception? { CloudDatastore.dataset.delete key }
|
|
202
|
+
self.class.retry_on_exception?(operation: 'delete') { CloudDatastore.dataset.delete key }
|
|
195
203
|
end
|
|
196
204
|
end
|
|
197
205
|
|
|
@@ -202,7 +210,9 @@ module ActiveModel::Datastore
|
|
|
202
210
|
|
|
203
211
|
run_callbacks :save do
|
|
204
212
|
entity = build_entity(parent)
|
|
205
|
-
success = self.class.retry_on_exception?
|
|
213
|
+
success = self.class.retry_on_exception?(operation: 'save') do
|
|
214
|
+
CloudDatastore.dataset.save entity
|
|
215
|
+
end
|
|
206
216
|
self.id = entity.key.id if success
|
|
207
217
|
self.parent_key_id = entity.key.parent.id if entity.key.parent.present?
|
|
208
218
|
success
|
|
@@ -229,7 +239,7 @@ module ActiveModel::Datastore
|
|
|
229
239
|
def find_entity(id_or_name, parent = nil)
|
|
230
240
|
key = CloudDatastore.dataset.key name, id_or_name
|
|
231
241
|
key.parent = parent if parent.present?
|
|
232
|
-
retry_on_exception { CloudDatastore.dataset.find key }
|
|
242
|
+
retry_on_exception(operation: 'find') { CloudDatastore.dataset.find key }
|
|
233
243
|
end
|
|
234
244
|
|
|
235
245
|
##
|
|
@@ -290,7 +300,9 @@ module ActiveModel::Datastore
|
|
|
290
300
|
def all(options = {})
|
|
291
301
|
next_cursor = nil
|
|
292
302
|
query = build_query(options)
|
|
293
|
-
query_results = retry_on_exception
|
|
303
|
+
query_results = retry_on_exception(operation: 'run query') do
|
|
304
|
+
CloudDatastore.dataset.run query
|
|
305
|
+
end
|
|
294
306
|
if options[:limit]
|
|
295
307
|
next_cursor = query_results.cursor if query_results.size == options[:limit]
|
|
296
308
|
return from_entities(query_results.all), next_cursor
|
|
@@ -342,7 +354,9 @@ module ActiveModel::Datastore
|
|
|
342
354
|
query.ancestor(args[:ancestor]) if args[:ancestor]
|
|
343
355
|
query.limit(1)
|
|
344
356
|
query.where(args.keys[0].to_s, '=', args.values[0])
|
|
345
|
-
query_results = retry_on_exception
|
|
357
|
+
query_results = retry_on_exception(operation: 'run query') do
|
|
358
|
+
CloudDatastore.dataset.run query
|
|
359
|
+
end
|
|
346
360
|
from_entity(query_results.first)
|
|
347
361
|
end
|
|
348
362
|
|
|
@@ -400,15 +414,23 @@ module ActiveModel::Datastore
|
|
|
400
414
|
query_options(query, options)
|
|
401
415
|
end
|
|
402
416
|
|
|
403
|
-
def retry_on_exception?(max_retry_count = 5)
|
|
417
|
+
def retry_on_exception?(max_retry_count = 5, operation: nil, kind: name)
|
|
404
418
|
retries = 0
|
|
405
419
|
sleep_time = 0.25
|
|
406
420
|
begin
|
|
421
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) if operation
|
|
407
422
|
yield
|
|
408
423
|
rescue Google::Cloud::Error => e
|
|
409
424
|
return false if retries >= max_retry_count
|
|
410
425
|
|
|
411
|
-
|
|
426
|
+
if operation
|
|
427
|
+
elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
|
|
428
|
+
message = "\e[33mDatastore #{operation} failed for #{kind} after #{elapsed_ms} ms: " \
|
|
429
|
+
"#{e.message.inspect}; retrying in #{sleep_time} s\e[0m"
|
|
430
|
+
else
|
|
431
|
+
message = "\e[33mRescued exception #{e.message.inspect}, retrying in #{sleep_time}\e[0m"
|
|
432
|
+
end
|
|
433
|
+
ActiveModel::Datastore.logger ? ActiveModel::Datastore.logger.warn(message) : puts(message)
|
|
412
434
|
# 0.25, 0.5, 1, 2, and 4 second between retries.
|
|
413
435
|
sleep sleep_time
|
|
414
436
|
retries += 1
|
|
@@ -417,15 +439,23 @@ module ActiveModel::Datastore
|
|
|
417
439
|
end
|
|
418
440
|
end
|
|
419
441
|
|
|
420
|
-
def retry_on_exception(max_retry_count = 5)
|
|
442
|
+
def retry_on_exception(max_retry_count = 5, operation: nil, kind: name)
|
|
421
443
|
retries = 0
|
|
422
444
|
sleep_time = 0.25
|
|
423
445
|
begin
|
|
446
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) if operation
|
|
424
447
|
yield
|
|
425
448
|
rescue Google::Cloud::Error => e
|
|
426
449
|
raise e if retries >= max_retry_count
|
|
427
450
|
|
|
428
|
-
|
|
451
|
+
if operation
|
|
452
|
+
elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
|
|
453
|
+
message = "\e[33mDatastore #{operation} failed for #{kind} after #{elapsed_ms} ms: " \
|
|
454
|
+
"#{e.message.inspect}; retrying in #{sleep_time} s\e[0m"
|
|
455
|
+
else
|
|
456
|
+
message = "\e[33mRescued exception #{e.message.inspect}, retrying in #{sleep_time}\e[0m"
|
|
457
|
+
end
|
|
458
|
+
ActiveModel::Datastore.logger ? ActiveModel::Datastore.logger.warn(message) : puts(message)
|
|
429
459
|
# 0.25, 0.5, 1, 2, and 4 second between retries.
|
|
430
460
|
sleep sleep_time
|
|
431
461
|
retries += 1
|
|
@@ -491,7 +521,7 @@ module ActiveModel::Datastore
|
|
|
491
521
|
def find_all_entities(ids_or_names, parent)
|
|
492
522
|
keys = ids_or_names.map { |id| CloudDatastore.dataset.key name, id }
|
|
493
523
|
keys.map { |key| key.parent = parent } if parent.present?
|
|
494
|
-
retry_on_exception { CloudDatastore.dataset.find_all keys }
|
|
524
|
+
retry_on_exception(operation: 'find all') { CloudDatastore.dataset.find_all keys }
|
|
495
525
|
end
|
|
496
526
|
|
|
497
527
|
def build_model(entity)
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activemodel-datastore
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bryce McLean
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activemodel
|
|
@@ -148,14 +147,14 @@ dependencies:
|
|
|
148
147
|
requirements:
|
|
149
148
|
- - "~>"
|
|
150
149
|
- !ruby/object:Gem::Version
|
|
151
|
-
version: '
|
|
150
|
+
version: '3.1'
|
|
152
151
|
type: :development
|
|
153
152
|
prerelease: false
|
|
154
153
|
version_requirements: !ruby/object:Gem::Requirement
|
|
155
154
|
requirements:
|
|
156
155
|
- - "~>"
|
|
157
156
|
- !ruby/object:Gem::Version
|
|
158
|
-
version: '
|
|
157
|
+
version: '3.1'
|
|
159
158
|
description: Makes the google-cloud-datastore gem compliant with active_model conventions
|
|
160
159
|
and compatible with your Rails 5+ applications.
|
|
161
160
|
email:
|
|
@@ -182,10 +181,9 @@ licenses:
|
|
|
182
181
|
- MIT
|
|
183
182
|
metadata:
|
|
184
183
|
homepage_uri: https://github.com/Agrimatics/activemodel-datastore
|
|
185
|
-
changelog_uri: https://github.com/Agrimatics/activemodel-datastore/blob/
|
|
184
|
+
changelog_uri: https://github.com/Agrimatics/activemodel-datastore/blob/main/CHANGELOG.md
|
|
186
185
|
source_code_uri: https://github.com/Agrimatics/activemodel-datastore/
|
|
187
186
|
bug_tracker_uri: https://github.com/Agrimatics/activemodel-datastore/issues
|
|
188
|
-
post_install_message:
|
|
189
187
|
rdoc_options: []
|
|
190
188
|
require_paths:
|
|
191
189
|
- lib
|
|
@@ -200,8 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
200
198
|
- !ruby/object:Gem::Version
|
|
201
199
|
version: '0'
|
|
202
200
|
requirements: []
|
|
203
|
-
rubygems_version:
|
|
204
|
-
signing_key:
|
|
201
|
+
rubygems_version: 4.0.19
|
|
205
202
|
specification_version: 4
|
|
206
203
|
summary: Cloud Datastore integration with Active Model
|
|
207
204
|
test_files: []
|