activemodel-datastore 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c26efa635bdc60b33239d0f6b33cc6e4ac86fb5c752672f25f6969b2c3dda5c
4
- data.tar.gz: 9651de6765b071963bbf0ee9da4680dbb46c5d0b820d463e34db0fc0e3d36612
3
+ metadata.gz: 3126172160a1df49727e648493401c8448f62a1442cba738dcf139d6406dfdce
4
+ data.tar.gz: 24bc7d6985f16739f16561611255d23ac6862cd72502f50827462a3c0674e6aa
5
5
  SHA512:
6
- metadata.gz: 4227107ad98274df5bac98ffb38e7de8ebfc8ec3d61edf4f09497ad1498fe936cf2a4a10a4c583ba43f0288f0f011cd8b20dd454f8d2f677380c6967dc3c4c9c
7
- data.tar.gz: 38d33728917c185c9beaf6c5ecb64e6adcf5e94ab672651ea33a41d22e9a9b6be503ee043b8878fc19aa81b552b7eae662cc568a50bf073853782bd45e568770
6
+ metadata.gz: bc4e6401b2932e11c37b509dc1f98561423b6908d0d11f0465363090239a67ae6f7dc02cc5aecedc6fb72e4e8168f44a6ddc9fe38d49665fc6fc95838e0144a8
7
+ data.tar.gz: 7acf7b5d14306afb3412c7ac667806e05708de0b9a8e8348b172bafa337316f4b11160f68eee3bb1152e27d7fa6ae066d4d28e17f430365b6caf003a3dfdbd1c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  ### 0.8.0 / 2024-03-05
2
8
  * updating GCLOUD_KEYFILE_JSON to work with the latest version of GoogleAuth
3
9
 
data/README.md CHANGED
@@ -1,19 +1,21 @@
1
1
  Active Model Datastore
2
2
  ===================================
3
3
 
4
- Makes the [google-cloud-datastore](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-datastore) gem compliant with [active_model](https://github.com/rails/rails/tree/master/activemodel) conventions and compatible with your Rails 5+ applications.
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 [Cloud Datastore](https://cloud.google.com/datastore)
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
- When you want a Rails app backed by a managed, massively-scalable datastore solution. Cloud Datastore
11
- automatically handles sharding and replication. It is a highly available and durable database that
12
- automatically scales to handle your applications' load. Cloud Datastore is a schemaless database
13
- suited for unstructured or semi-structured application data.
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
  [![Gem Version](https://badge.fury.io/rb/activemodel-datastore.svg)](https://badge.fury.io/rb/activemodel-datastore)
16
- [![Build Status](https://travis-ci.org/Agrimatics/activemodel-datastore.svg?branch=master)](https://travis-ci.org/Agrimatics/activemodel-datastore)
18
+ [![CI](https://github.com/Agrimatics/activemodel-datastore/actions/workflows/ci.yml/badge.svg?branch=main)](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
- - [Datastore Emulator](#emulator)
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
- Google Cloud requires the Project ID and Service Account Credentials to connect to the Datastore API.
52
-
53
- *Follow the [activation instructions](https://cloud.google.com/datastore/docs/activate) to enable the
54
- Google Cloud Datastore API. When running on Google Cloud Platform environments the Service Account
55
- credentials will be discovered automatically. When running on other environments (such as AWS or Heroku)
56
- you need to create a service account with the role of editor and generate json credentials.*
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
- If you have an external application running on a platform outside of Google Cloud you also need to
67
- provide the Service Account credentials. They are specified in two additional `ENV` variables named
68
- `SERVICE_ACCOUNT_CLIENT_EMAIL` and `SERVICE_ACCOUNT_PRIVATE_KEY`. The values for these two `ENV`
69
- variables will be in the downloaded service account json credentials file.
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 = -----BEGIN PRIVATE KEY-----\nMIIFfb3...5dmFtABy\n-----END PRIVATE KEY-----\n
73
- SERVICE_ACCOUNT_CLIENT_EMAIL = web-app@app-name.iam.gserviceaccount.com
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 `ENV` variables can be set under 'Settings' -> 'Config Variables'.
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
- There is an example Puma config file [here](https://github.com/Agrimatics/activemodel-datastore/blob/master/test/support/datastore_example_rails_app/config/puma.rb).
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 Cloud Datastore are known as entities. Entities are of a kind. An entity has one
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 cloud
106
- datastore. Define the attributes of your model using `attr_accessor`. With this approach, Rails
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 datastore.
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 Cloud Datastore has a key that uniquely identifies it. The key consists of the
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 Cloud Datastore hierarchy
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
- Cloud Datastore has documentation on how [Datastore Queries](https://cloud.google.com/datastore/docs/concepts/queries#datastore-basic-query-ruby)
283
- work, and pay special attention to the the [restrictions](https://cloud.google.com/datastore/docs/concepts/queries#restrictions_on_queries).
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
- Cloud Datastore is a non-relational databases, or NoSQL database. It distributes data over many
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
- What is eventual consistency?
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
- It means that an updated entity value may not be immediately visible when executing a query.
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
- In the context of a Rails app, there are times that eventual consistency is not ideal. For example,
298
- let's say you create a user entity with a key that looks like this:
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
- `@key=#<Google::Cloud::Datastore::Key @kind="User", @id=1>`
311
+ Use the following command to inspect a database's concurrency mode:
301
312
 
302
- and then immediately redirect to the index view of users. There is a good chance that your new user
303
- is not yet visible in the list. If you perform a refresh on the index view a second or two later
304
- the user will appear.
313
+ ```bash
314
+ gcloud firestore databases describe --project=PROJECT_ID --database=DATABASE_ID
315
+ ```
305
316
 
306
- "Wait a minute!" you say. "This is crap!" you say. Fear not! We can make the query of users strongly
307
- consistent. We just need to use entity groups and ancestor queries. An entity group is a hierarchy
308
- formed by a root entity and its children. To create an entity group, you specify an ancestor path
309
- for the entity which is a parent key as part of the child key.
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
- *Ancestor queries are always strongly consistent.*
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 here for the Cloud Datastore documentation on [Data Consistency](https://cloud.google.com/datastore/docs/concepts/structuring_for_strong_consistency).
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 cloud datastore query requires an index. Yes, you read that correctly. Every single one. The
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, Cloud Datastore automatically predefines an index for each property of each entity kind.
399
- These single property indexes are suitable for simple types of queries. These indexes are free and
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 index multiple property values per indexed entity. Composite indexes support complex
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 has excellent doc regarding datastore indexes [here](https://cloud.google.com/datastore/docs/concepts/indexes).
417
-
418
- The datastore emulator generates composite indexes in an index.yaml file automatically. The file
419
- can be found in /tmp/local_datastore/WEB-INF/index.yaml. If your localhost Rails app exercises every
420
- possible query the application will issue, using every combination of filter and sort order, the
421
- generated entries will represent your complete set of indexes.
422
-
423
- One thing to note is that the datastore emulator caches indexes. As you add and modify application
424
- code you might find that the local datastore index.yaml contains indexes that are no longer needed.
425
- In this scenario try deleting the index.yaml and restarting the emulator. Navigate through your Rails
426
- app and the index.yaml will be built from scratch.
427
-
428
- ## <a name="emulator"></a>Datastore Emulator
429
-
430
- Install the Google Cloud SDK.
431
-
432
- $ curl https://sdk.cloud.google.com | bash
433
-
434
- You can check the version of the SDK and the components installed with:
435
-
436
- $ gcloud components list
437
-
438
- Install the Cloud Datastore Emulator, which provides local emulation of the production Cloud
439
- Datastore environment and the gRPC API. However, you'll need to do a small amount of configuration
440
- before running the application against the emulator, see [here.](https://cloud.google.com/datastore/docs/tools/datastore-emulator)
441
-
442
- $ gcloud components install cloud-datastore-emulator
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 5 app in the test directory [here](https://github.com/Agrimatics/activemodel-datastore/tree/master/test/support/datastore_example_rails_app).
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
- ```bash
469
- $ bundle
470
- $ cloud_datastore_emulator create tmp/local_datastore
471
- $ cloud_datastore_emulator create tmp/test_datastore
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
- Alternatively,`:reject_if` also accepts a symbol for using methods:
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
- reject_proc = proc { |attributes| attributes['name'].blank? }
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 nested_models will have `marked_for_destruction` set
598
- to True.
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 Cloud Datastore implementation evolves (or if a project's indexes change), this order may change.
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? { CloudDatastore.dataset.save entity }
47
+ self.class.retry_on_exception?(operation: 'save') do
48
+ CloudDatastore.dataset.save entity
49
+ end
48
50
  end
49
51
  end
50
52
 
@@ -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.merge(value: value))
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? ? true : false
74
+ changed = marked_for_destruction? || false
75
75
  tracked_attributes.each do |attr|
76
76
  break if changed
77
77
 
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  module Datastore
3
- VERSION = '0.8.0'
3
+ VERSION = '0.9.0'
4
4
  end
5
5
  end
@@ -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? { CloudDatastore.dataset.save entity }
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 { CloudDatastore.dataset.run query }
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 { CloudDatastore.dataset.run query }
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
- puts "\e[33mRescued exception #{e.message.inspect}, retrying in #{sleep_time}\e[0m"
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
- puts "\e[33mRescued exception #{e.message.inspect}, retrying in #{sleep_time}\e[0m"
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.8.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: 2024-03-05 00:00:00.000000000 Z
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: '2.1'
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: '2.1'
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/master/CHANGELOG.md
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: 3.5.3
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: []