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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +171 -0
- data/README.md +28 -2
- data/lib/yes/version.rb +1 -1
- metadata +9 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5eff49dca0523282e364a82fb408cc06d00742f2fca7056b82384ac091c7e107
|
|
4
|
+
data.tar.gz: a2a87f022f74b86b515bc66f8862086a9c036c677e9a1202226361dc5ba48243
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
368
|
+
Register the middlewares:
|
|
369
369
|
|
|
370
370
|
```ruby
|
|
371
371
|
PgEventstore.configure do |config|
|
|
372
|
-
|
|
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
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|