yes 1.3.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +192 -0
  3. data/README.md +28 -2
  4. data/lib/yes/version.rb +1 -1
  5. metadata +9 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f6e32695a2368de9dbfcae480f960fba0e04261861593b172efb09c7122b723
4
- data.tar.gz: c71415bdf839dd8eab46c16e3b11b1220ba9bfd04f8d0313f8a3194d197a6f0e
3
+ metadata.gz: 91e44ef887d4626f5a871e1a4ddccf41f45e58ff6c9e7d21500e3e04c6a252f6
4
+ data.tar.gz: 076b44cc8d84038b6720b9cf67f2404ff8f4a27f6e3b78e76778cf8de8f59785
5
5
  SHA512:
6
- metadata.gz: 59af9e5d4773472bc52a809de3b4439ebbbb08fdccee010c9e324abacc5283820858bf7c337ab6b0ad7b956064a08536f1a6ebfe20ec5190a36cf52d120a03d7
7
- data.tar.gz: b4456e11e06600b7123cc3640f6b6d32cead2d5199476a2593e87acb38e5608737bf7473c9a6dc2ef680561fe420f8dcdccc90d073a4ed33e2ae866c89b897a1
6
+ metadata.gz: 6b81da070eceb08e313a7c102758f77a622a4d2e1c61505c4975b041c381338319e1f2412e3b2e2386dad651d3cbccc2b1e6fd8751a63f59b0240fc1a6f1aa57
7
+ data.tar.gz: c45327b8bc188c4860e9befcdb07358ca328daa5010339d89abac247a0533090c04b4638435aef7a93bcda4fb9dcce7c941c4c6fd409f8119360ef4547e3a3b5
data/CHANGELOG.md CHANGED
@@ -2,6 +2,198 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.3.0] - 2026-09-03
6
+
7
+ ### yes-core
8
+
9
+ #### Added
10
+ - `CommandHandling::RevisionConflictBackoff` and `RevisionConflictWaiting`: an
11
+ exponential, jittered backoff (10 ms → 1 s per attempt, 2 s in total) applied
12
+ between revision-conflict retries while the read model still lags the stream.
13
+
14
+ #### Changed
15
+ - `CommandExecutor` and `CommandGroupExecutor` wait for the read model to catch up
16
+ before retrying a `PgEventstore::WrongExpectedRevisionError` instead of retrying
17
+ immediately; conflicts the same process caused still retry at once.
18
+ - The `ConcurrentUpdateError` retries take their delay from
19
+ `RevisionConflictBackoff.schedule`; timing unchanged.
20
+
21
+ #### Fixed
22
+ - `EventPublisher#verify_external_revisions!` and `Stateless::Handler#revision_error!`
23
+ raise `WrongExpectedRevisionError` with pg_eventstore's field order: `revision` is the
24
+ store's value, `expected_revision` the caller's. Both were inverted.
25
+
26
+ ## [2.2.0] - 2026-09-01
27
+
28
+ ### yes-core
29
+
30
+ #### Added
31
+ - `Authorization::LookupCache` — scoped memoization for the read-only lookups an
32
+ authorization pass repeats.
33
+
34
+ Caching is opt-in per scope: outside `LookupCache.with_scope` its `fetch` just yields,
35
+ so nothing changes for callers that do not open a scope. The store lives in
36
+ `ActiveSupport::IsolatedExecutionState`, so it is per thread/fiber, and `with_scope`
37
+ clears it on the way out — including when the block raises — so nothing leaks into the
38
+ next request. Nested scopes reuse the outermost cache.
39
+
40
+ #### Changed
41
+ - `Authorization::CommandCerbosAuthorizer` resolves the principal data and the
42
+ authorized resource through `LookupCache`.
43
+
44
+ Authorizing a batch of commands used to repeat both lookups once per command. The
45
+ principal data is derived purely from the request's auth data, and the commands of a
46
+ batch commonly act on the same resource, so both were re-read from the database for
47
+ every command — for an N-command batch, N times the queries for N identical results.
48
+ In production traces the two lookups accounted for the large majority of the authorize
49
+ span, dwarfing the Cerbos call itself.
50
+
51
+ This is safe because a batch is authorized in full before any of its commands is
52
+ executed, so no write can invalidate either lookup while the pass is running. Cached
53
+ values are shared between the commands of a pass and must be treated as read-only;
54
+ the per-command Cerbos payload is still built, and checked, per command.
55
+
56
+ - The `Cerbos Authorize Command` span now tracks SQL, so the time it spends in-process
57
+ can be attributed to queries rather than guessed at.
58
+
59
+ ### yes-command-api
60
+
61
+ #### Changed
62
+ - `Commands::BatchAuthorizer` authorizes a batch inside a single
63
+ `Yes::Core::Authorization::LookupCache` scope, which is what lets the authorizers of
64
+ one batch share their principal and resource lookups.
65
+
66
+ ## [2.1.1] - 2026-08-22
67
+
68
+ ### yes-core
69
+
70
+ - `Types::UUID` now accepts any RFC 9562 UUID version (1-8), not only v4.
71
+
72
+ pg_eventstore 3.0.0 moved event-id generation off the database's `gen_random_uuid()`
73
+ (always v4) to `SecureRandom.uuid_v7`. Those ids reach the gem as `causation_id` /
74
+ `correlation_id`, and a v4-only pattern makes `TransactionDetails.new` raise
75
+ `Dry::Struct::Error` — which fails the event handler and kills the subscription once
76
+ its restarts are exhausted.
77
+
78
+ Still a real constraint: version must be 1-8 and the variant nibble 8/9/a/b, so
79
+ arbitrary hex in UUID shape is rejected as before.
80
+
81
+ **Required for anything running pg_eventstore 3.x.**
82
+
83
+ - Register a `failed_subscription_notifier` so a dead subscription reports to Sentry.
84
+
85
+ `config.failed_subscription_notifier` is pg_eventstore's only death signal — it is
86
+ called once, when a subscription exhausts its restarts and stays dead. Without it that
87
+ death is silent: per-failure errors are only recorded on the subscription row and are
88
+ never raised, so a subscription can stop processing indefinitely with no alert.
89
+
90
+ Registered only when the host application has loaded Sentry.
91
+
92
+ Shipped alongside the UUID fix deliberately: that fix addresses a bug which *kills*
93
+ subscriptions, and this is what tells you when one has died.
94
+
95
+ ## [2.1.0] - 2026-07-31
96
+
97
+ ### yes-core
98
+
99
+ #### Added
100
+ - `Middlewares::WriteEncryptor` — encrypts on `#serialize` exactly like `Middlewares::Encryptor`
101
+ (it subclasses it), but its `#deserialize` is a no-op.
102
+ - `Middlewares.register_encryptor(key_repository, config:)` — registers `:encryptor` and
103
+ `:write_encryptor` together. Host applications should call this instead of assigning
104
+ `config.middlewares[:encryptor]` by hand; registering the decrypting encryptor on its own doubles
105
+ the encryptor round trips of every encrypted append.
106
+ - `Middlewares.for_write` — the middleware keys to pass to `#append_to_stream`: every configured
107
+ middleware, with `:encryptor` swapped for `:write_encryptor`. Derived from the live config rather
108
+ than hard-coded, because `PgEventstore::Client` resolves a passed list with
109
+ `config.middlewares.slice(*list)`, which silently drops unregistered names — a literal list could
110
+ therefore resolve to one with no encryptor at all and write plaintext at rest. Falls back to the
111
+ full list when `:write_encryptor` is missing, and the railtie warns about that at boot.
112
+ - `Middlewares::ENCRYPTOR` / `Middlewares::WRITE_ENCRYPTOR` config-key constants, and
113
+ `DataEncryptor::CIPHERTEXT_KEY` for the `es_encrypted` data key.
114
+
115
+ #### Changed
116
+ - Every write site now appends with `middlewares: Middlewares.for_write`, so an append no longer
117
+ decrypts the event it returns: `CommandHandling::EventPublisher`,
118
+ `CommandHandling::CommandGroupExecutor`, `Commands::Stateless::Handler` and
119
+ `TestSupport::EventHelpers#append_event`. This covers both `PgEventstore#multiple` paths, whose
120
+ sub-events publish through those same call sites.
121
+
122
+ pg_eventstore 3.0 runs every registered middleware's `#deserialize` on the events returned by
123
+ `#append_to_stream`, not only on reads. Nothing in yes-core reads `data` off that returned event —
124
+ `otl_record_response` records only type, revision, stream and positions, and `ReadModelUpdater`
125
+ always receives the command payload on the write path — so each encrypted append was paying an
126
+ uncached key lookup plus a decrypt against the encryptor service for a payload it discarded. Those
127
+ calls also ran inside the SERIALIZABLE transaction opened by `#multiple`, widening the window for
128
+ `PG::TRSerializationFailure` and its retries.
129
+
130
+ ⚠️ Consumers that relied on the appended event coming back decrypted must read the event instead.
131
+
132
+ #### Fixed
133
+ - `Middlewares::Encryptor#serialize` is now idempotent: it returns the event untouched when the data
134
+ is already encrypted. Required because the default middleware list holds two serialize-capable
135
+ encryptors once `:write_encryptor` is registered, so an append that omits `middlewares:` would
136
+ otherwise encrypt twice — the second pass encrypting the first pass's sentinels and overwriting the
137
+ real ciphertext irrecoverably. It also makes re-appending an event that was read at rest
138
+ (`middlewares: Middlewares.without(:encryptor)`) safe, which it was not before.
139
+
140
+ ## [2.0.0] - 2026-07-28
141
+
142
+ Major bump because `yes-core` now requires `pg_eventstore` v3, whose schema is
143
+ incompatible with v1. Released as 2.0.0 rather than 1.5.0 deliberately: consumers
144
+ constrain these gems at `~> 1.3`, which 1.5.0 would satisfy, so a minor bump could
145
+ be pulled in by an unrelated `bundle update` and put v3 code against a v1 store.
146
+ 2.0.0 makes that impossible.
147
+
148
+ ⚠️ **Do not adopt until your event store has been migrated to v3.** Migrating is a
149
+ one-way, downtime-requiring operation — see the `pg_eventstore` upgrade notes.
150
+
151
+ ### yes-core
152
+
153
+ #### Changed
154
+ - **Breaking change**: `pg_eventstore` dependency `~> 1.0` → `~> 3.0`.
155
+ - **Breaking change**: OpenTelemetry span attribute `event.link_id` is now
156
+ `event.link_global_position`, in `Commands::Stateless::Handler` and
157
+ `CommandHandling::EventPublisher`. `pg_eventstore` v3 drops `events.link_id`
158
+ (migration 13) in favour of the bigint `link_global_position`, so `Event#link_id`
159
+ raises `NoMethodError`.
160
+ **Update any dashboards or trace queries keyed on `event.link_id`.**
161
+
162
+ ### yes-auth
163
+
164
+ #### Changed
165
+ - **Breaking change**: `yes-core` dependency `~> 1.0` → `~> 2.0`, required to stay
166
+ resolvable alongside yes-core 2.0.0.
167
+
168
+ ## [1.4.0] - 2026-06-24
169
+
170
+ ### yes-command-api
171
+
172
+ #### Added
173
+ - Dispatch aggregate-DSL command groups over the HTTP command API. The deserializer now resolves the `<Context>::<Subject>::CommandGroups::<Name>::Command` class-name convention (generated by the `command_group` macro) in addition to the legacy top-level group, V2, and V1 conventions. The controller expands a `Yes::Core::Commands::CommandGroup` into its sub-commands for batch authorization and validation, while still passing the wrapped group to the command bus so the Processor dispatches it as one atomic unit.
174
+
175
+ ### yes-core
176
+
177
+ #### Added
178
+ - `Yes::Core::Commands::CommandGroup#to_h` now returns the flat input payload merged with reserved keys, so a group round-trips cleanly through `Class.new(to_h)` (used by the command bus' `add_metadata` and by the ActiveJob serializer). Reserved keys (`origin`, `batch_id`, `command_id`, `metadata`, `transaction`) now propagate to the aggregate group method through that round-trip.
179
+ - `Yes::Core::ActiveJobSerializers::CommandGroupSerializer` now serializes aggregate-DSL `CommandGroup`s as well as the legacy stateless `Group`, so groups survive the async command queue.
180
+ - `Yes::Core::Configuration#command_group_guard_evaluator_class` resolves a command group's guard evaluator from the `:command_group_guard_evaluator` registry, and `Processor#guard_evaluator_exists?` uses it for `CommandGroup` instances. A group registers its guard evaluator under that dedicated registry type, so without this the existence check raised `UnregisteredCommand` when a group was dispatched.
181
+
182
+ #### Changed
183
+ - `Yes::Core::Commands::Processor#run_command` no longer special-cases `CommandGroup` payloads; the flat `#to_h` makes a single code path correct for both single commands and groups (drops the `reinstantiate_with_reserved_keys` helper).
184
+
185
+ ## [1.3.1] - 2026-06-16
186
+
187
+ ### yes-auth
188
+
189
+ #### Added
190
+ - Rebuild the principals mirror rows on resource-access `Restored` events, so a restored read/write resource access re-materializes the principal it grants.
191
+
192
+ ### yes-core
193
+
194
+ #### Fixed
195
+ - `Yes::Core::Utils::HashUtils.deep_flatten_hash` now applies the `prefix` to array-valued keys, consistently with scalar and nested-hash keys. Previously an array value was keyed by its bare name (e.g. `deep_flatten_hash({ tags: [...] }, 'span')` produced `"tags"` instead of `"span.tags"`), so callers passing a prefix got a mix of namespaced and un-namespaced keys. Callers that pass no prefix are unaffected.
196
+
5
197
  ## [1.3.0] - 2026-05-18
6
198
 
7
199
  ### yes-core
data/README.md CHANGED
@@ -365,14 +365,18 @@ command :change, :ssn, :string, encrypt: true
365
365
 
366
366
  Encryption is performed by a PgEventstore middleware that delegates the actual key management and cryptography to a `key_repository` object you provide. *Yes* does not ship a concrete implementation — you plug in any object that satisfies the interface below.
367
367
 
368
- Register the middleware:
368
+ Register the middlewares:
369
369
 
370
370
  ```ruby
371
371
  PgEventstore.configure do |config|
372
- config.middlewares[:encryptor] = Yes::Core::Middlewares::Encryptor.new(key_repository)
372
+ Yes::Core::Middlewares.register_encryptor(key_repository, config:)
373
373
  end
374
374
  ```
375
375
 
376
+ This registers two middlewares against your repository: `:encryptor`, which decrypts events as they are read, and `:write_encryptor`, which encrypts identically but does not decrypt. pg_eventstore runs `#deserialize` on the event returned by `#append_to_stream` too, and nothing on the write path reads that event's data — so *Yes* appends with `middlewares: Yes::Core::Middlewares.for_write`, and your `key_repository` is never asked to decrypt a payload that is about to be discarded. Always register through `register_encryptor`: registering `:encryptor` alone silently costs a key lookup and a decrypt on every encrypted write.
377
+
378
+ One consequence worth knowing: **the event returned by a command is encrypted**. Read the event back when you need its plaintext.
379
+
376
380
  The `key_repository` must respond to the following methods, each returning a [`Dry::Monads::Result`](https://dry-rb.org/gems/dry-monads/) (or any object responding to `success?`, `failure?`, and `value!`):
377
381
 
378
382
  | Method | Purpose | Returns (on success) |
@@ -833,6 +837,28 @@ A `command_group :foo` macro on `Context::Aggregate` generates:
833
837
 
834
838
  The legacy stateless `Yes::Core::Commands::Group` / `Yes::Core::Commands::Stateless::GroupHandler` are untouched and continue to serve cross-aggregate use cases declared outside the aggregate DSL.
835
839
 
840
+ **Invoking via the Command API:** command groups are dispatchable over HTTP exactly like regular commands. POST to `/v1/commands` with the standard request shape — `command` is the group name (camelized), `data` is the flat payload (same as the Ruby invocation form):
841
+
842
+ ```json
843
+ {
844
+ "commands": [
845
+ {
846
+ "context": "Companies",
847
+ "subject": "Apprenticeship",
848
+ "command": "CreateApprenticeship",
849
+ "data": {
850
+ "company_id": "...",
851
+ "user_id": "...",
852
+ "name": "Acme Apprenticeship",
853
+ "description": "Software dev role"
854
+ }
855
+ }
856
+ ]
857
+ }
858
+ ```
859
+
860
+ The Deserializer resolves the group's `Command` class via the registered `Context::Aggregate::CommandGroups::<Name>::Command` namespace. Authorization and validation run per-sub-command (each sub-command's existing `Authorizer` and `Validator` are invoked, exactly like the legacy stateless `Group` flow), and the group dispatches as a single atomic unit through the bus.
861
+
836
862
  ### Read Models
837
863
 
838
864
  Each aggregate automatically gets a corresponding read model (ActiveRecord model) that persists its current state. This is how you access attribute values from an aggregate.
data/lib/yes/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yes
4
- VERSION = '1.3.0'
4
+ VERSION = '2.3.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: 'yes'
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nico Ritsche
@@ -15,56 +15,56 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 1.3.0
18
+ version: 2.3.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 1.3.0
25
+ version: 2.3.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: yes-command-api
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 1.3.0
32
+ version: 2.3.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 1.3.0
39
+ version: 2.3.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: yes-core
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 1.3.0
46
+ version: 2.3.0
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 1.3.0
53
+ version: 2.3.0
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: yes-read-api
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - '='
59
59
  - !ruby/object:Gem::Version
60
- version: 1.3.0
60
+ version: 2.3.0
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - '='
66
66
  - !ruby/object:Gem::Version
67
- version: 1.3.0
67
+ version: 2.3.0
68
68
  description: Event sourcing framework for Ruby on Rails applications
69
69
  email:
70
70
  - nico.ritsche@yousty.ch