yes 1.3.0 → 2.2.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 +171 -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: 5eff49dca0523282e364a82fb408cc06d00742f2fca7056b82384ac091c7e107
4
+ data.tar.gz: a2a87f022f74b86b515bc66f8862086a9c036c677e9a1202226361dc5ba48243
5
5
  SHA512:
6
- metadata.gz: 59af9e5d4773472bc52a809de3b4439ebbbb08fdccee010c9e324abacc5283820858bf7c337ab6b0ad7b956064a08536f1a6ebfe20ec5190a36cf52d120a03d7
7
- data.tar.gz: b4456e11e06600b7123cc3640f6b6d32cead2d5199476a2593e87acb38e5608737bf7473c9a6dc2ef680561fe420f8dcdccc90d073a4ed33e2ae866c89b897a1
6
+ metadata.gz: b28a95c77f846978113afb0e33996670b693413d972f9b8b20b5734bcafdf783cdec7c5122865e691e37becd8934ae9b800b63fe0e8e74bd458b7015b52eff96
7
+ data.tar.gz: 89dd8e95c3ef2cedc6f796e3052886c5948c865f8f8e844a9d23e9ca6d2a00dab1aae61f6f1fc8802ba9f92177ad92d9fef9b54149fadbbdd5e13c1486f426dd
data/CHANGELOG.md CHANGED
@@ -2,6 +2,177 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.2.0] - 2026-09-01
6
+
7
+ ### yes-core
8
+
9
+ #### Added
10
+ - `Authorization::LookupCache` — scoped memoization for the read-only lookups an
11
+ authorization pass repeats.
12
+
13
+ Caching is opt-in per scope: outside `LookupCache.with_scope` its `fetch` just yields,
14
+ so nothing changes for callers that do not open a scope. The store lives in
15
+ `ActiveSupport::IsolatedExecutionState`, so it is per thread/fiber, and `with_scope`
16
+ clears it on the way out — including when the block raises — so nothing leaks into the
17
+ next request. Nested scopes reuse the outermost cache.
18
+
19
+ #### Changed
20
+ - `Authorization::CommandCerbosAuthorizer` resolves the principal data and the
21
+ authorized resource through `LookupCache`.
22
+
23
+ Authorizing a batch of commands used to repeat both lookups once per command. The
24
+ principal data is derived purely from the request's auth data, and the commands of a
25
+ batch commonly act on the same resource, so both were re-read from the database for
26
+ every command — for an N-command batch, N times the queries for N identical results.
27
+ In production traces the two lookups accounted for the large majority of the authorize
28
+ span, dwarfing the Cerbos call itself.
29
+
30
+ This is safe because a batch is authorized in full before any of its commands is
31
+ executed, so no write can invalidate either lookup while the pass is running. Cached
32
+ values are shared between the commands of a pass and must be treated as read-only;
33
+ the per-command Cerbos payload is still built, and checked, per command.
34
+
35
+ - The `Cerbos Authorize Command` span now tracks SQL, so the time it spends in-process
36
+ can be attributed to queries rather than guessed at.
37
+
38
+ ### yes-command-api
39
+
40
+ #### Changed
41
+ - `Commands::BatchAuthorizer` authorizes a batch inside a single
42
+ `Yes::Core::Authorization::LookupCache` scope, which is what lets the authorizers of
43
+ one batch share their principal and resource lookups.
44
+
45
+ ## [2.1.1] - 2026-08-22
46
+
47
+ ### yes-core
48
+
49
+ - `Types::UUID` now accepts any RFC 9562 UUID version (1-8), not only v4.
50
+
51
+ pg_eventstore 3.0.0 moved event-id generation off the database's `gen_random_uuid()`
52
+ (always v4) to `SecureRandom.uuid_v7`. Those ids reach the gem as `causation_id` /
53
+ `correlation_id`, and a v4-only pattern makes `TransactionDetails.new` raise
54
+ `Dry::Struct::Error` — which fails the event handler and kills the subscription once
55
+ its restarts are exhausted.
56
+
57
+ Still a real constraint: version must be 1-8 and the variant nibble 8/9/a/b, so
58
+ arbitrary hex in UUID shape is rejected as before.
59
+
60
+ **Required for anything running pg_eventstore 3.x.**
61
+
62
+ - Register a `failed_subscription_notifier` so a dead subscription reports to Sentry.
63
+
64
+ `config.failed_subscription_notifier` is pg_eventstore's only death signal — it is
65
+ called once, when a subscription exhausts its restarts and stays dead. Without it that
66
+ death is silent: per-failure errors are only recorded on the subscription row and are
67
+ never raised, so a subscription can stop processing indefinitely with no alert.
68
+
69
+ Registered only when the host application has loaded Sentry.
70
+
71
+ Shipped alongside the UUID fix deliberately: that fix addresses a bug which *kills*
72
+ subscriptions, and this is what tells you when one has died.
73
+
74
+ ## [2.1.0] - 2026-07-31
75
+
76
+ ### yes-core
77
+
78
+ #### Added
79
+ - `Middlewares::WriteEncryptor` — encrypts on `#serialize` exactly like `Middlewares::Encryptor`
80
+ (it subclasses it), but its `#deserialize` is a no-op.
81
+ - `Middlewares.register_encryptor(key_repository, config:)` — registers `:encryptor` and
82
+ `:write_encryptor` together. Host applications should call this instead of assigning
83
+ `config.middlewares[:encryptor]` by hand; registering the decrypting encryptor on its own doubles
84
+ the encryptor round trips of every encrypted append.
85
+ - `Middlewares.for_write` — the middleware keys to pass to `#append_to_stream`: every configured
86
+ middleware, with `:encryptor` swapped for `:write_encryptor`. Derived from the live config rather
87
+ than hard-coded, because `PgEventstore::Client` resolves a passed list with
88
+ `config.middlewares.slice(*list)`, which silently drops unregistered names — a literal list could
89
+ therefore resolve to one with no encryptor at all and write plaintext at rest. Falls back to the
90
+ full list when `:write_encryptor` is missing, and the railtie warns about that at boot.
91
+ - `Middlewares::ENCRYPTOR` / `Middlewares::WRITE_ENCRYPTOR` config-key constants, and
92
+ `DataEncryptor::CIPHERTEXT_KEY` for the `es_encrypted` data key.
93
+
94
+ #### Changed
95
+ - Every write site now appends with `middlewares: Middlewares.for_write`, so an append no longer
96
+ decrypts the event it returns: `CommandHandling::EventPublisher`,
97
+ `CommandHandling::CommandGroupExecutor`, `Commands::Stateless::Handler` and
98
+ `TestSupport::EventHelpers#append_event`. This covers both `PgEventstore#multiple` paths, whose
99
+ sub-events publish through those same call sites.
100
+
101
+ pg_eventstore 3.0 runs every registered middleware's `#deserialize` on the events returned by
102
+ `#append_to_stream`, not only on reads. Nothing in yes-core reads `data` off that returned event —
103
+ `otl_record_response` records only type, revision, stream and positions, and `ReadModelUpdater`
104
+ always receives the command payload on the write path — so each encrypted append was paying an
105
+ uncached key lookup plus a decrypt against the encryptor service for a payload it discarded. Those
106
+ calls also ran inside the SERIALIZABLE transaction opened by `#multiple`, widening the window for
107
+ `PG::TRSerializationFailure` and its retries.
108
+
109
+ ⚠️ Consumers that relied on the appended event coming back decrypted must read the event instead.
110
+
111
+ #### Fixed
112
+ - `Middlewares::Encryptor#serialize` is now idempotent: it returns the event untouched when the data
113
+ is already encrypted. Required because the default middleware list holds two serialize-capable
114
+ encryptors once `:write_encryptor` is registered, so an append that omits `middlewares:` would
115
+ otherwise encrypt twice — the second pass encrypting the first pass's sentinels and overwriting the
116
+ real ciphertext irrecoverably. It also makes re-appending an event that was read at rest
117
+ (`middlewares: Middlewares.without(:encryptor)`) safe, which it was not before.
118
+
119
+ ## [2.0.0] - 2026-07-28
120
+
121
+ Major bump because `yes-core` now requires `pg_eventstore` v3, whose schema is
122
+ incompatible with v1. Released as 2.0.0 rather than 1.5.0 deliberately: consumers
123
+ constrain these gems at `~> 1.3`, which 1.5.0 would satisfy, so a minor bump could
124
+ be pulled in by an unrelated `bundle update` and put v3 code against a v1 store.
125
+ 2.0.0 makes that impossible.
126
+
127
+ ⚠️ **Do not adopt until your event store has been migrated to v3.** Migrating is a
128
+ one-way, downtime-requiring operation — see the `pg_eventstore` upgrade notes.
129
+
130
+ ### yes-core
131
+
132
+ #### Changed
133
+ - **Breaking change**: `pg_eventstore` dependency `~> 1.0` → `~> 3.0`.
134
+ - **Breaking change**: OpenTelemetry span attribute `event.link_id` is now
135
+ `event.link_global_position`, in `Commands::Stateless::Handler` and
136
+ `CommandHandling::EventPublisher`. `pg_eventstore` v3 drops `events.link_id`
137
+ (migration 13) in favour of the bigint `link_global_position`, so `Event#link_id`
138
+ raises `NoMethodError`. Mirrors the same change in `yousty-eventsourcing` 16.0.0.
139
+ **Update any dashboards or trace queries keyed on `event.link_id`.**
140
+
141
+ ### yes-auth
142
+
143
+ #### Changed
144
+ - **Breaking change**: `yes-core` dependency `~> 1.0` → `~> 2.0`, required to stay
145
+ resolvable alongside yes-core 2.0.0.
146
+
147
+ ## [1.4.0] - 2026-06-24
148
+
149
+ ### yes-command-api
150
+
151
+ #### Added
152
+ - 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.
153
+
154
+ ### yes-core
155
+
156
+ #### Added
157
+ - `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.
158
+ - `Yes::Core::ActiveJobSerializers::CommandGroupSerializer` now serializes aggregate-DSL `CommandGroup`s as well as the legacy stateless `Group`, so groups survive the async command queue.
159
+ - `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.
160
+
161
+ #### Changed
162
+ - `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).
163
+
164
+ ## [1.3.1] - 2026-06-16
165
+
166
+ ### yes-auth
167
+
168
+ #### Added
169
+ - Rebuild the principals mirror rows on resource-access `Restored` events, so a restored read/write resource access re-materializes the principal it grants.
170
+
171
+ ### yes-core
172
+
173
+ #### Fixed
174
+ - `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.
175
+
5
176
  ## [1.3.0] - 2026-05-18
6
177
 
7
178
  ### 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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.0
68
68
  description: Event sourcing framework for Ruby on Rails applications
69
69
  email:
70
70
  - nico.ritsche@yousty.ch