concerns_on_rails 1.28.6 → 1.28.8
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 +259 -0
- data/README.md +177 -42
- data/lib/concerns_on_rails/controllers/authorizable.rb +152 -17
- data/lib/concerns_on_rails/controllers/cacheable.rb +141 -5
- data/lib/concerns_on_rails/controllers/error_handleable.rb +1 -0
- data/lib/concerns_on_rails/controllers/filterable.rb +213 -10
- data/lib/concerns_on_rails/controllers/localizable.rb +2 -23
- data/lib/concerns_on_rails/controllers/respondable.rb +120 -3
- data/lib/concerns_on_rails/controllers/sortable.rb +208 -26
- data/lib/concerns_on_rails/controllers/timezoneable.rb +84 -14
- data/lib/concerns_on_rails/controllers/webhook_verifiable.rb +152 -2
- data/lib/concerns_on_rails/encryption.rb +59 -0
- data/lib/concerns_on_rails/models/duplicable.rb +9 -1
- data/lib/concerns_on_rails/models/encryptable.rb +196 -18
- data/lib/concerns_on_rails/models/lockable.rb +138 -16
- data/lib/concerns_on_rails/models/normalizable.rb +100 -10
- data/lib/concerns_on_rails/models/sanitizable.rb +146 -1
- data/lib/concerns_on_rails/models/searchable.rb +87 -13
- data/lib/concerns_on_rails/models/stateable.rb +84 -13
- data/lib/concerns_on_rails/models/storable.rb +250 -65
- data/lib/concerns_on_rails/support/encryptor.rb +33 -11
- data/lib/concerns_on_rails/support/error_envelope.rb +20 -4
- data/lib/concerns_on_rails/support/vary_header.rb +54 -0
- data/lib/concerns_on_rails/version.rb +1 -1
- data/lib/concerns_on_rails.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ae4fb29ea12389fc8aaff341a565a482bf3c8ef99fdcfc7791c64c7302d1ca5
|
|
4
|
+
data.tar.gz: 02ea08bceb25e2a479e27ef00342c69cfeac2ffc8129b689269f423819c0c0ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ac2edd1717ef0e4c157b851e75554ba057321e1ae9f283bcdb015e9e6d49d63f1f4bf9bc9ee4419e0b28a111487147516957a03b17ae3915c7ac23f8a721109
|
|
7
|
+
data.tar.gz: 826dd0234e22faa23b089fbedd7900c8bc847f8f7af5670c6a6b0dafc22ec838b29e0cf196b0269b8dde798302d43c677eb4c93d60396db207d6e733d0f16ff2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,264 @@
|
|
|
1
1
|
<!-- CHANGELOG.md -->
|
|
2
2
|
|
|
3
|
+
## 1.28.8 (2026-09-19)
|
|
4
|
+
|
|
5
|
+
The last six open feature PRs, released as a patch by request. These had never been
|
|
6
|
+
reviewed — they were excluded from the previous two waves because they would not merge —
|
|
7
|
+
so each was rebased onto master, reviewed adversarially, and fixed. **Five of the six
|
|
8
|
+
carried a CRITICAL defect**, two of which could destroy data. The `### Fixed` section is
|
|
9
|
+
the important one; several entries describe behaviour that never worked as documented.
|
|
10
|
+
|
|
11
|
+
Also fixes a live defect in `Support::ErrorEnvelope` found during the review, which
|
|
12
|
+
affected eight concerns on 1.28.7 and earlier. 1828 examples, 0 failures.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **Models::Encryptable**: key rotation. `ConcernsOnRails.configure_encryption { |c|
|
|
16
|
+
c.key_id = 1; c.previous_keys = { 0 => old } }` — new writes stamp `key_id`, reads
|
|
17
|
+
decrypt with whichever configured key the envelope names (unknown id → `DecryptionError`
|
|
18
|
+
naming it), and blind-index lookups match current + previous digests.
|
|
19
|
+
`Model.needs_reencryption(*fields)`, `Model.reencrypt_all!(*fields)` /
|
|
20
|
+
`record.reencrypt!`, and `<field>_key_id`. Per-field `key:` fields sit outside rotation.
|
|
21
|
+
The envelope format is unchanged: 1.28.x already wrote and authenticated a `key_id` byte,
|
|
22
|
+
so every existing row is a key-id-0 envelope and decrypts untouched after upgrading. (#77)
|
|
23
|
+
- **Models::Lockable**: `lockable_by … unlock_token:` mints a URL-safe token in the same
|
|
24
|
+
write as the lock, for self-service unlock links without Devise.
|
|
25
|
+
`User.unlock_by_token(token)` consumes it once — constant-time compare, one conditional
|
|
26
|
+
UPDATE decides the winner, hooks fire — and the token lives exactly as long as the lock.
|
|
27
|
+
Cleared on every unlock path, reset by Duplicable, and registered with the
|
|
28
|
+
filter_parameters registry. (#61)
|
|
29
|
+
- **Models::Stateable**: `stateable_by … timestamps: true` (or a list of states) stamps
|
|
30
|
+
`<state>_at` in the same write as the state change, for guarded events, direct setters,
|
|
31
|
+
`transition_to!` and `transition_all`. Per-event `before_<event>` / `after_<event>` hooks
|
|
32
|
+
fire inside the generic `before_transition` / `after_transition` pair and the same
|
|
33
|
+
transaction, and may be declared `private`. (#49)
|
|
34
|
+
- **Controllers::Filterable**: `filter_by … operators: true` (or a subset) adds `not`,
|
|
35
|
+
`gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `null`, `contains` and `starts_with` to
|
|
36
|
+
direct-where filters, read as a suffix (`?price_gte=10`) or in bracket form
|
|
37
|
+
(`?price[gte]=10`). Every operator comes from a frozen allow-list mapped to a fixed Arel
|
|
38
|
+
node, so no param string reaches SQL. Values are cast through the column's type or the
|
|
39
|
+
new `type:` option, which also pre-casts what a `with:` lambda receives. Opt-in per
|
|
40
|
+
filter; existing filters are unchanged. (#46)
|
|
41
|
+
- **Controllers::WebhookVerifiable**: `verify_webhook … replay:` / `replay_ttl:` rejects a
|
|
42
|
+
redelivery with 409 `webhook_replayed` — replay protection for the schemes that carry no
|
|
43
|
+
timestamp (GitHub, Shopify, plain HMAC). The check runs only after the signature
|
|
44
|
+
verifies. A short claim is taken before the action and promoted on completion, and
|
|
45
|
+
released on a 5xx so a transient failure does not burn the delivery id. (#60)
|
|
46
|
+
- **Controllers::Respondable**: `render_success` accepts `location:` and `headers:`, and
|
|
47
|
+
gains `render_created(data:, location:)` (201 + `Location`) and `render_invalid(record)`
|
|
48
|
+
(422 + the record's `full_messages`). Header names and values are stripped of bytes a
|
|
49
|
+
header line cannot carry, and a `Link` value is appended to an existing one — matched
|
|
50
|
+
case-insensitively — rather than replacing it. Works in both the envelope and the
|
|
51
|
+
problem-details format. (#105, replacing #74)
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- **Controllers::Filterable**: an uncastable comparison value (`?price_gte=abc`) returns
|
|
55
|
+
`none` rather than matching. Fail-closed was chosen because ignoring the filter would
|
|
56
|
+
return the very rows the client tried to exclude; it matches what plain `where` already
|
|
57
|
+
does with the same input, and is scoped to the four new comparison operators so no
|
|
58
|
+
pre-existing filter changes on upgrade. `contains` / `starts_with` are case-INsensitive
|
|
59
|
+
on every adapter — there is no `case_sensitive:` option. (#46)
|
|
60
|
+
- **Models::Stateable**: `timestamps:` refuses to derive a stamp column from a state named
|
|
61
|
+
`created` or `updated`, which would otherwise target Rails' own `created_at`/`updated_at`.
|
|
62
|
+
Stamp columns are NOT affixed by `prefix:`/`suffix:`, so a state named `published` or
|
|
63
|
+
`deleted` collides with Publishable's and SoftDeletable's columns — rename the state or
|
|
64
|
+
leave it out of `timestamps:`. (#49)
|
|
65
|
+
- **Models::Encryptable**: `record.reencrypt!` rewrites through a guarded `UPDATE` that
|
|
66
|
+
matches the ciphertext read at load, then reloads. A row written by someone else in the
|
|
67
|
+
meantime is left alone instead of being reverted, and a field with unsaved changes is
|
|
68
|
+
skipped rather than silently committed. (#77)
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
- **Support::ErrorEnvelope**: the `errors:` keyword was passed to a host app's
|
|
72
|
+
`render_error` whenever details were present, but several concerns document the override
|
|
73
|
+
contract as `render_error(message:, status:, code:)`. Such an app got `ArgumentError:
|
|
74
|
+
unknown keyword: :errors` at request time — a 500 instead of the 4xx, on exactly the path
|
|
75
|
+
that has something to report. The previous guard tested `details` alone, so it covered
|
|
76
|
+
only the empty case, i.e. the one that was never broken. This reached every concern that
|
|
77
|
+
funnels through the envelope, most commonly ErrorHandleable's rescued `RecordInvalid`.
|
|
78
|
+
- **Models::Encryptable**: `previous_keys=` printed the offending hash's keys on a bad
|
|
79
|
+
shape, so an inverted `{ ENV["KEY_V1"] => 0 }` raised **with the live key in the exception
|
|
80
|
+
message** — into logs, backtraces and error trackers. Only the value's class is reported
|
|
81
|
+
now. (#77)
|
|
82
|
+
- **Models::Encryptable**: `needs_reencryption` detected MySQL by adapter name, so Trilogy
|
|
83
|
+
(Rails 7.1+) and MariaDB fell through to a case-folding comparison and returned **zero
|
|
84
|
+
rows**. `reencrypt_all!` then reported 0, an operator would conclude the rotation was
|
|
85
|
+
complete and drop the old key, and every pre-rotation row would become permanently
|
|
86
|
+
undecryptable. (#77)
|
|
87
|
+
- **Models::Lockable**: a `before_unlock` / `after_unlock` hook that vetoed the unlock
|
|
88
|
+
permanently burned the token while leaving the account locked — the claim ran in a bare
|
|
89
|
+
transaction that joined the enclosing one, so `unlock_access!`'s savepoint rolled back
|
|
90
|
+
while the claim committed. The user's emailed link was dead with no way back. The claim
|
|
91
|
+
now takes its own savepoint and is rolled back with the unlock. (#61)
|
|
92
|
+
- **Models::Lockable**: `unlock_by_token` no longer honours a token once the lock has
|
|
93
|
+
lapsed, and no longer uses `unscoped` — a mailed link could otherwise reach a row hidden
|
|
94
|
+
behind a `default_scope` (a soft-deleted or deactivated account). (#61)
|
|
95
|
+
- **Controllers::WebhookVerifiable**: a store answering `#write` but not `#read` passed
|
|
96
|
+
declaration-time validation and then provided **no replay protection at all**, silently —
|
|
97
|
+
the endpoint looked protected and accepted every redelivery. Both methods are now
|
|
98
|
+
required. (#60)
|
|
99
|
+
- **Controllers::WebhookVerifiable**: the Stripe replay key hashed the raw
|
|
100
|
+
`Stripe-Signature` header, which is *parsed* rather than compared — appending an unknown
|
|
101
|
+
`v0=…` pair or re-spacing the commas produced a still-valid header with a fresh key,
|
|
102
|
+
defeating the protection. Stripe now keys off the signed `"<timestamp>.<body>"` payload.
|
|
103
|
+
(#60)
|
|
104
|
+
- **Controllers::WebhookVerifiable**: `replay: false` raised at class load, so
|
|
105
|
+
`replay: Rails.env.production?` broke controller loading in development and test. It is
|
|
106
|
+
now a synonym for "off". (#60)
|
|
107
|
+
- **Controllers::Respondable**: caller-supplied header **names** reached the response
|
|
108
|
+
unsanitized, so a CR/LF in an interpolated name was response splitting. Names now go
|
|
109
|
+
through the same filter as values, widened from CR/LF to the full illegal-byte range.
|
|
110
|
+
(#105)
|
|
111
|
+
- **Controllers::Filterable**: a bracket-form operator with a blank value
|
|
112
|
+
(`?price[gte]=`) became `price > NULL` and returned **nothing**, while the documented
|
|
113
|
+
equivalent `?price_gte=` correctly returned everything — an empty min/max box silently
|
|
114
|
+
emptied the result set. Both forms now skip blanks, and both honour a boolean `false`.
|
|
115
|
+
(#46)
|
|
116
|
+
- **Controllers::Filterable**: `?stock_gt=twelve` silently became `stock > 0` and returned
|
|
117
|
+
the stocked rows (`ActiveModel::Type::Integer#cast("twelve")` is `0`, not `nil`), while
|
|
118
|
+
the same typo on a datetime column returned none — one malformed request answered two
|
|
119
|
+
opposite ways. (#46)
|
|
120
|
+
- **Controllers::Filterable**: LIKE escaping no longer relies on `sanitize_sql_like`, which
|
|
121
|
+
is not public before Rails 5.1 while the gemspec claims `>= 5.0`. (#46)
|
|
122
|
+
|
|
123
|
+
### Internal
|
|
124
|
+
- Docs corrected: "Never `update_column(s)` an encrypted field — those bypass the type and
|
|
125
|
+
write raw plaintext" was **false** (`update_columns` serializes through the attribute
|
|
126
|
+
type), and `reencrypt_all!` is built on it. (#77)
|
|
127
|
+
|
|
128
|
+
## 1.28.7 (2026-09-19)
|
|
129
|
+
|
|
130
|
+
The eight PRs held back from 1.28.6, released as a patch by request. Each carried a
|
|
131
|
+
CRITICAL or a design-level defect found in review; each now carries the fix, and in
|
|
132
|
+
most cases a spec that was verified to fail against the unfixed code. Read the
|
|
133
|
+
`### Fixed` section: several of these defects were live in the PRs' own green CI,
|
|
134
|
+
and two of them are security-shaped.
|
|
135
|
+
|
|
136
|
+
Also in this release: the Rails 8.1 component bumps are unblocked, and `json` is
|
|
137
|
+
pinned below 3 (json 3 removes `JSON.generate(..., quirks_mode:)`, which
|
|
138
|
+
ActiveSupport 7.1 calls, and changes `JSON.parse`'s positional options, which
|
|
139
|
+
ActiveSupport 8.1 uses — with json 3.0.2 the suite fails in Storable's decode path).
|
|
140
|
+
1730 examples, 0 failures.
|
|
141
|
+
|
|
142
|
+
### Added
|
|
143
|
+
- **Controllers::Cacheable**: `etag_with` folds request context into the ETag —
|
|
144
|
+
presets `:locale` / `:format` / `:query`, controller-method Symbols, or a block — so
|
|
145
|
+
locale-, fieldset- or role-dependent representations of one resource never share a
|
|
146
|
+
validator; each source adds its implied `Vary` (`vary:` overrides, `vary: false`
|
|
147
|
+
suppresses), merged with the `http_cache_actions` policy. `stale_resource?` /
|
|
148
|
+
`set_cache_validators` gain a per-call `extras:`. (#48)
|
|
149
|
+
- **Controllers::Authorizable**: denials instrument
|
|
150
|
+
`authorization_denied.concerns_on_rails` (controller, action, actor_id, actor_type,
|
|
151
|
+
rule name, status, message) via the `on_authorization_denied(rule)` override point;
|
|
152
|
+
`authorize_by`/`require_role` accept `name:`. `skip_authorization only:/except:`
|
|
153
|
+
exempts actions from every rule, inherited ones included. `authorized?(action)`
|
|
154
|
+
evaluates the rules without rendering, for view predicates. (#68)
|
|
155
|
+
- **Controllers::Timezoneable**: `persist:` writes a param-chosen zone into the
|
|
156
|
+
`cookie:` cookie; `response_header:` emits the resolved zone (`X-Time-Zone` or a
|
|
157
|
+
custom name) and appends `Vary: Time-Zone`; `time_zone_source` reports which source
|
|
158
|
+
won. (#71)
|
|
159
|
+
- **Controllers::Sortable**: `params[:sort]` accepts JSON:API-style `-key` / `+key`
|
|
160
|
+
per-column direction prefixes, and `sortable_by` accepts rule hashes —
|
|
161
|
+
`key: { column: "table.column", joins:, join: :left|:inner, nulls: :first|:last }` —
|
|
162
|
+
for association-column sorting (lazy LEFT OUTER JOIN by default) and NULLs pinned
|
|
163
|
+
first or last (Rails 6.1+). (#63)
|
|
164
|
+
- **Models::Searchable**: `searchable_by ..., ranked: true` orders `search` results by
|
|
165
|
+
relevance — exact, then prefix, then substring, earlier-declared columns first
|
|
166
|
+
within a tier — via a portable CASE expression; the relation's existing ORDER BY
|
|
167
|
+
becomes the tiebreaker. `search(q, ranked:)` overrides per call and `search_rank(q)`
|
|
168
|
+
exposes the score expression. (#64)
|
|
169
|
+
- **Models::Normalizable**: `with:` accepts an Array of presets/callables applied left
|
|
170
|
+
to right, validated at class load. New presets `:strip`, `:capitalize`, `:titleize`,
|
|
171
|
+
`:parameterize`, `:nullify_blank` and `:url`. `Model.normalize(field, value)` applies
|
|
172
|
+
a field's rule to a bare value for lookups and params. (#66)
|
|
173
|
+
- **Models::Sanitizable**: `sanitized_attributes` and a `sanitized:` serialization
|
|
174
|
+
option — `as_json(sanitized: true | [:fields])` — which composes with
|
|
175
|
+
`only:`/`except:`, is carried into `include:` children, and sanitizes the *serialized*
|
|
176
|
+
value so a Maskable mask survives. `Model.sanitize_all!(*fields)` rewrites legacy rows
|
|
177
|
+
in place for the current scope (by default the `on: :write` fields only), transactional
|
|
178
|
+
via `Support::BatchOps`, refreshing Encryptable blind indexes. (#72)
|
|
179
|
+
- **Models::Storable**: `where_<accessor>(value)` scope per key — equality on a stored
|
|
180
|
+
key via `json_extract` (SQLite), `->>` (PostgreSQL) or `JSON_UNQUOTE(JSON_EXTRACT())`
|
|
181
|
+
(MySQL). Values are cast as the writer stores them; `where_<key>(nil)` matches
|
|
182
|
+
unset/null. Opt out per key or per macro with `query: false`. (#76)
|
|
183
|
+
- **Support::VaryHeader**: shared `Vary` appender used by Timezoneable and Localizable —
|
|
184
|
+
seeds Rails' own `Accept` dimension, appends rather than clobbers, de-duplicates
|
|
185
|
+
case-insensitively and leaves `Vary: *` alone. (#71)
|
|
186
|
+
|
|
187
|
+
### Changed
|
|
188
|
+
- **Controllers::Cacheable**: a response whose ETag varies on a dimension `Vary` cannot
|
|
189
|
+
express — a block or controller-method source, or any source with `vary: false` — is
|
|
190
|
+
now emitted as `Cache-Control: private` regardless of the rule's declared
|
|
191
|
+
`visibility:`. Such a response is not shareable, and there is no `Vary` that makes it
|
|
192
|
+
so. (#48)
|
|
193
|
+
- **Controllers::Sortable**: PostgreSQL uses native `NULLS FIRST/LAST`; every other
|
|
194
|
+
adapter gets the portable `CASE WHEN col IS NULL` equivalent. A `default:` outside the
|
|
195
|
+
allow-list orders the relation without becoming client-selectable, repeated sort keys
|
|
196
|
+
collapse to their first occurrence, and `+` must be percent-encoded as `%2B` (Rack
|
|
197
|
+
decodes a raw `+` to a space). `sort_requests` is the override point; `sort_fields` is
|
|
198
|
+
read-only. (#63)
|
|
199
|
+
- **Controllers::Authorizable**: the denial payload carries `actor_id:`/`actor_type:`
|
|
200
|
+
rather than the `current_user` object — notification payloads are not filtered by
|
|
201
|
+
`config.filter_parameters`. (#68)
|
|
202
|
+
- **Models::Normalizable**: `:url` accepts only `http`/`https`; a value carrying any
|
|
203
|
+
other scheme is returned stripped rather than blessed as normalized. `:titleize` is
|
|
204
|
+
deliberately **not** `String#titleize`. (#66)
|
|
205
|
+
- **Models::Storable**: a `where_<key>` scope whose name is already taken no longer
|
|
206
|
+
aborts the declaration — it is skipped with a deprecator warning, so an existing model
|
|
207
|
+
defining that method still boots after an upgrade. (#76)
|
|
208
|
+
- **Models::Searchable**: a grouped relation is returned unranked, since a rank
|
|
209
|
+
`ORDER BY` over `GROUP BY` is an error on PostgreSQL and on MySQL under
|
|
210
|
+
`ONLY_FULL_GROUP_BY`. (#64)
|
|
211
|
+
|
|
212
|
+
### Fixed
|
|
213
|
+
- **Controllers::Authorizable**: `skip_authorization except: []` (or `false`, or `""`)
|
|
214
|
+
exempted **every** action of the controller and all its subclasses — each of those
|
|
215
|
+
values is truthy while matching no real action name, so the `!except.include?(action)`
|
|
216
|
+
test was true everywhere. `except: Rails.env.production? && :destroy` is the realistic
|
|
217
|
+
spelling. Now rejected at class load, along with non-Symbol/String entries; `only:`
|
|
218
|
+
still accepts them, where they are inert. (#68)
|
|
219
|
+
- **Models::Sanitizable**: `serializable_hash` re-read the raw column instead of
|
|
220
|
+
post-processing the serialized value, so on a model including both Maskable and
|
|
221
|
+
Sanitizable it overwrote the mask with sanitized plaintext — order-dependently, and
|
|
222
|
+
therefore silently. (#72)
|
|
223
|
+
- **Controllers::Timezoneable**: the `cookie:` source now works on a real
|
|
224
|
+
`ActionController::Base`. `#cookies` is PRIVATE there, so the `respond_to?(:cookies)`
|
|
225
|
+
guard was always false and the documented cookie source silently did nothing in every
|
|
226
|
+
real Rails app; only the specs' public-`cookies` double made it look alive. Both guards
|
|
227
|
+
now ask `respond_to?(:cookies, true)`. (#71)
|
|
228
|
+
- **Controllers::Timezoneable**: `Vary` is no longer written before the action runs,
|
|
229
|
+
which suppressed Rails' own `Vary: Accept` (`_set_vary_header` only adds it when `Vary`
|
|
230
|
+
is blank) and let a shared cache serve a JSON body to an HTML request. (#71)
|
|
231
|
+
- **Controllers::Sortable**: MySQL is detected by behaviour rather than by adapter name.
|
|
232
|
+
The previous `adapter_name.include?("mysql")` test was false for Trilogy, so a
|
|
233
|
+
`nulls:` rule emitted PostgreSQL syntax against MySQL 8 — a 1064 parse error on every
|
|
234
|
+
request using that sort key. A dotted Symbol column (`sortable_by :"authors.name"`)
|
|
235
|
+
is quoted correctly again; it had regressed to `"posts"."authors.name"`. Sort keys are
|
|
236
|
+
de-duplicated, so `?sort=` with thousands of repeated keys no longer builds thousands
|
|
237
|
+
of ORDER BY terms. (#63)
|
|
238
|
+
- **Models::Normalizable**: `:titleize` no longer destroys data. It was
|
|
239
|
+
`Inflector.titleize`, i.e. `humanize(underscore(v))`, which deleted characters —
|
|
240
|
+
`"Jean-Luc Picard"` → `"Jean Luc Picard"`, `"customer_id"` → `"Customer"` — and ran in
|
|
241
|
+
`before_validation`, so the original was gone. `:url` no longer drops a URL's
|
|
242
|
+
`userinfo` on Ruby's newer `uri` versions. (#66)
|
|
243
|
+
- **Models::Storable**: `serialize :settings, coder: JSON, type: Hash` — the form Rails
|
|
244
|
+
7.1's own deprecation message directs users to — was misclassified as a non-JSON coder,
|
|
245
|
+
so the whole query feature refused to run on a perfectly queryable column. A blank or
|
|
246
|
+
corrupt store value no longer makes every `where_` query raise on SQLite. A read-only
|
|
247
|
+
finder no longer mutates the caller's `Time`. Key names are validated at macro time.
|
|
248
|
+
(#76)
|
|
249
|
+
- **Models::CounterCacheable**: the locking spec added in 1.28.6 matched the SQLite
|
|
250
|
+
transaction statement with `start_with?("begin")`; Rails 7.2+ switched SQLite to
|
|
251
|
+
IMMEDIATE transactions and upcased it, so the assertion silently found nothing on
|
|
252
|
+
Rails 8.x. Test-only.
|
|
253
|
+
|
|
254
|
+
### Internal
|
|
255
|
+
- `json` is pinned to `< 3` in the Gemfile. Verified against a real 8.1.3.1 gemset:
|
|
256
|
+
with json 3.0.2 the suite fails in Storable's decode path; with `json < 3` Rails
|
|
257
|
+
8.1.3.1 is green.
|
|
258
|
+
- `require "active_support/notifications"` added to `authorizable.rb` and
|
|
259
|
+
`error_handleable.rb`, which instrument without requiring it — a direct require of
|
|
260
|
+
either file used to `NameError` on the first event. (#68)
|
|
261
|
+
|
|
3
262
|
## 1.28.6 (2026-09-18)
|
|
4
263
|
|
|
5
264
|
Ten feature PRs deepening existing concerns, released as a patch by request: no new
|