concerns_on_rails 1.28.5 → 1.28.7
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 +243 -0
- data/README.md +198 -62
- data/lib/concerns_on_rails/configuration.rb +35 -0
- 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/idempotentable.rb +76 -15
- data/lib/concerns_on_rails/controllers/includable.rb +88 -18
- data/lib/concerns_on_rails/controllers/localizable.rb +2 -23
- data/lib/concerns_on_rails/controllers/respondable.rb +125 -0
- data/lib/concerns_on_rails/controllers/sortable.rb +208 -26
- data/lib/concerns_on_rails/controllers/throttleable.rb +106 -10
- data/lib/concerns_on_rails/controllers/timezoneable.rb +84 -14
- data/lib/concerns_on_rails/models/activatable.rb +90 -13
- data/lib/concerns_on_rails/models/auditable.rb +49 -5
- data/lib/concerns_on_rails/models/counter_cacheable.rb +94 -17
- data/lib/concerns_on_rails/models/duplicable.rb +13 -1
- data/lib/concerns_on_rails/models/hashable.rb +66 -11
- data/lib/concerns_on_rails/models/maskable.rb +83 -1
- 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/sluggable.rb +18 -0
- data/lib/concerns_on_rails/models/storable.rb +250 -65
- data/lib/concerns_on_rails/models/tokenizable.rb +121 -29
- data/lib/concerns_on_rails/support/include_tree.rb +74 -0
- 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 +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b713f3fd8cd14e5f3a12db7b3c390111118df1fa143d5d833dee009544a8a64
|
|
4
|
+
data.tar.gz: 16f6d5447ac1d44c5be3dc1cdb0113d9a9e64457de14b87d4199bb95993189d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7afa091b97fe9359b7294f965593af629d5d66d797e77a7c3ebde396eddc7fcdf627937fae2c90c2ee0c034c23e91f3713ac3827ab201ee8edda0438094ad89c
|
|
7
|
+
data.tar.gz: 683d74ea9d1b258e1c79bcdcd7243bcbcd22b10ba5c862946267e5baab118c3753c69baf7af496436a7424286cbd33be9f5e6bfba889928f6f212b190d9ed3ca
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,248 @@
|
|
|
1
1
|
<!-- CHANGELOG.md -->
|
|
2
2
|
|
|
3
|
+
## 1.28.7 (2026-09-19)
|
|
4
|
+
|
|
5
|
+
The eight PRs held back from 1.28.6, released as a patch by request. Each carried a
|
|
6
|
+
CRITICAL or a design-level defect found in review; each now carries the fix, and in
|
|
7
|
+
most cases a spec that was verified to fail against the unfixed code. Read the
|
|
8
|
+
`### Fixed` section: several of these defects were live in the PRs' own green CI,
|
|
9
|
+
and two of them are security-shaped.
|
|
10
|
+
|
|
11
|
+
Also in this release: the Rails 8.1 component bumps are unblocked, and `json` is
|
|
12
|
+
pinned below 3 (json 3 removes `JSON.generate(..., quirks_mode:)`, which
|
|
13
|
+
ActiveSupport 7.1 calls, and changes `JSON.parse`'s positional options, which
|
|
14
|
+
ActiveSupport 8.1 uses — with json 3.0.2 the suite fails in Storable's decode path).
|
|
15
|
+
1730 examples, 0 failures.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Controllers::Cacheable**: `etag_with` folds request context into the ETag —
|
|
19
|
+
presets `:locale` / `:format` / `:query`, controller-method Symbols, or a block — so
|
|
20
|
+
locale-, fieldset- or role-dependent representations of one resource never share a
|
|
21
|
+
validator; each source adds its implied `Vary` (`vary:` overrides, `vary: false`
|
|
22
|
+
suppresses), merged with the `http_cache_actions` policy. `stale_resource?` /
|
|
23
|
+
`set_cache_validators` gain a per-call `extras:`. (#48)
|
|
24
|
+
- **Controllers::Authorizable**: denials instrument
|
|
25
|
+
`authorization_denied.concerns_on_rails` (controller, action, actor_id, actor_type,
|
|
26
|
+
rule name, status, message) via the `on_authorization_denied(rule)` override point;
|
|
27
|
+
`authorize_by`/`require_role` accept `name:`. `skip_authorization only:/except:`
|
|
28
|
+
exempts actions from every rule, inherited ones included. `authorized?(action)`
|
|
29
|
+
evaluates the rules without rendering, for view predicates. (#68)
|
|
30
|
+
- **Controllers::Timezoneable**: `persist:` writes a param-chosen zone into the
|
|
31
|
+
`cookie:` cookie; `response_header:` emits the resolved zone (`X-Time-Zone` or a
|
|
32
|
+
custom name) and appends `Vary: Time-Zone`; `time_zone_source` reports which source
|
|
33
|
+
won. (#71)
|
|
34
|
+
- **Controllers::Sortable**: `params[:sort]` accepts JSON:API-style `-key` / `+key`
|
|
35
|
+
per-column direction prefixes, and `sortable_by` accepts rule hashes —
|
|
36
|
+
`key: { column: "table.column", joins:, join: :left|:inner, nulls: :first|:last }` —
|
|
37
|
+
for association-column sorting (lazy LEFT OUTER JOIN by default) and NULLs pinned
|
|
38
|
+
first or last (Rails 6.1+). (#63)
|
|
39
|
+
- **Models::Searchable**: `searchable_by ..., ranked: true` orders `search` results by
|
|
40
|
+
relevance — exact, then prefix, then substring, earlier-declared columns first
|
|
41
|
+
within a tier — via a portable CASE expression; the relation's existing ORDER BY
|
|
42
|
+
becomes the tiebreaker. `search(q, ranked:)` overrides per call and `search_rank(q)`
|
|
43
|
+
exposes the score expression. (#64)
|
|
44
|
+
- **Models::Normalizable**: `with:` accepts an Array of presets/callables applied left
|
|
45
|
+
to right, validated at class load. New presets `:strip`, `:capitalize`, `:titleize`,
|
|
46
|
+
`:parameterize`, `:nullify_blank` and `:url`. `Model.normalize(field, value)` applies
|
|
47
|
+
a field's rule to a bare value for lookups and params. (#66)
|
|
48
|
+
- **Models::Sanitizable**: `sanitized_attributes` and a `sanitized:` serialization
|
|
49
|
+
option — `as_json(sanitized: true | [:fields])` — which composes with
|
|
50
|
+
`only:`/`except:`, is carried into `include:` children, and sanitizes the *serialized*
|
|
51
|
+
value so a Maskable mask survives. `Model.sanitize_all!(*fields)` rewrites legacy rows
|
|
52
|
+
in place for the current scope (by default the `on: :write` fields only), transactional
|
|
53
|
+
via `Support::BatchOps`, refreshing Encryptable blind indexes. (#72)
|
|
54
|
+
- **Models::Storable**: `where_<accessor>(value)` scope per key — equality on a stored
|
|
55
|
+
key via `json_extract` (SQLite), `->>` (PostgreSQL) or `JSON_UNQUOTE(JSON_EXTRACT())`
|
|
56
|
+
(MySQL). Values are cast as the writer stores them; `where_<key>(nil)` matches
|
|
57
|
+
unset/null. Opt out per key or per macro with `query: false`. (#76)
|
|
58
|
+
- **Support::VaryHeader**: shared `Vary` appender used by Timezoneable and Localizable —
|
|
59
|
+
seeds Rails' own `Accept` dimension, appends rather than clobbers, de-duplicates
|
|
60
|
+
case-insensitively and leaves `Vary: *` alone. (#71)
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
- **Controllers::Cacheable**: a response whose ETag varies on a dimension `Vary` cannot
|
|
64
|
+
express — a block or controller-method source, or any source with `vary: false` — is
|
|
65
|
+
now emitted as `Cache-Control: private` regardless of the rule's declared
|
|
66
|
+
`visibility:`. Such a response is not shareable, and there is no `Vary` that makes it
|
|
67
|
+
so. (#48)
|
|
68
|
+
- **Controllers::Sortable**: PostgreSQL uses native `NULLS FIRST/LAST`; every other
|
|
69
|
+
adapter gets the portable `CASE WHEN col IS NULL` equivalent. A `default:` outside the
|
|
70
|
+
allow-list orders the relation without becoming client-selectable, repeated sort keys
|
|
71
|
+
collapse to their first occurrence, and `+` must be percent-encoded as `%2B` (Rack
|
|
72
|
+
decodes a raw `+` to a space). `sort_requests` is the override point; `sort_fields` is
|
|
73
|
+
read-only. (#63)
|
|
74
|
+
- **Controllers::Authorizable**: the denial payload carries `actor_id:`/`actor_type:`
|
|
75
|
+
rather than the `current_user` object — notification payloads are not filtered by
|
|
76
|
+
`config.filter_parameters`. (#68)
|
|
77
|
+
- **Models::Normalizable**: `:url` accepts only `http`/`https`; a value carrying any
|
|
78
|
+
other scheme is returned stripped rather than blessed as normalized. `:titleize` is
|
|
79
|
+
deliberately **not** `String#titleize`. (#66)
|
|
80
|
+
- **Models::Storable**: a `where_<key>` scope whose name is already taken no longer
|
|
81
|
+
aborts the declaration — it is skipped with a deprecator warning, so an existing model
|
|
82
|
+
defining that method still boots after an upgrade. (#76)
|
|
83
|
+
- **Models::Searchable**: a grouped relation is returned unranked, since a rank
|
|
84
|
+
`ORDER BY` over `GROUP BY` is an error on PostgreSQL and on MySQL under
|
|
85
|
+
`ONLY_FULL_GROUP_BY`. (#64)
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
- **Controllers::Authorizable**: `skip_authorization except: []` (or `false`, or `""`)
|
|
89
|
+
exempted **every** action of the controller and all its subclasses — each of those
|
|
90
|
+
values is truthy while matching no real action name, so the `!except.include?(action)`
|
|
91
|
+
test was true everywhere. `except: Rails.env.production? && :destroy` is the realistic
|
|
92
|
+
spelling. Now rejected at class load, along with non-Symbol/String entries; `only:`
|
|
93
|
+
still accepts them, where they are inert. (#68)
|
|
94
|
+
- **Models::Sanitizable**: `serializable_hash` re-read the raw column instead of
|
|
95
|
+
post-processing the serialized value, so on a model including both Maskable and
|
|
96
|
+
Sanitizable it overwrote the mask with sanitized plaintext — order-dependently, and
|
|
97
|
+
therefore silently. (#72)
|
|
98
|
+
- **Controllers::Timezoneable**: the `cookie:` source now works on a real
|
|
99
|
+
`ActionController::Base`. `#cookies` is PRIVATE there, so the `respond_to?(:cookies)`
|
|
100
|
+
guard was always false and the documented cookie source silently did nothing in every
|
|
101
|
+
real Rails app; only the specs' public-`cookies` double made it look alive. Both guards
|
|
102
|
+
now ask `respond_to?(:cookies, true)`. (#71)
|
|
103
|
+
- **Controllers::Timezoneable**: `Vary` is no longer written before the action runs,
|
|
104
|
+
which suppressed Rails' own `Vary: Accept` (`_set_vary_header` only adds it when `Vary`
|
|
105
|
+
is blank) and let a shared cache serve a JSON body to an HTML request. (#71)
|
|
106
|
+
- **Controllers::Sortable**: MySQL is detected by behaviour rather than by adapter name.
|
|
107
|
+
The previous `adapter_name.include?("mysql")` test was false for Trilogy, so a
|
|
108
|
+
`nulls:` rule emitted PostgreSQL syntax against MySQL 8 — a 1064 parse error on every
|
|
109
|
+
request using that sort key. A dotted Symbol column (`sortable_by :"authors.name"`)
|
|
110
|
+
is quoted correctly again; it had regressed to `"posts"."authors.name"`. Sort keys are
|
|
111
|
+
de-duplicated, so `?sort=` with thousands of repeated keys no longer builds thousands
|
|
112
|
+
of ORDER BY terms. (#63)
|
|
113
|
+
- **Models::Normalizable**: `:titleize` no longer destroys data. It was
|
|
114
|
+
`Inflector.titleize`, i.e. `humanize(underscore(v))`, which deleted characters —
|
|
115
|
+
`"Jean-Luc Picard"` → `"Jean Luc Picard"`, `"customer_id"` → `"Customer"` — and ran in
|
|
116
|
+
`before_validation`, so the original was gone. `:url` no longer drops a URL's
|
|
117
|
+
`userinfo` on Ruby's newer `uri` versions. (#66)
|
|
118
|
+
- **Models::Storable**: `serialize :settings, coder: JSON, type: Hash` — the form Rails
|
|
119
|
+
7.1's own deprecation message directs users to — was misclassified as a non-JSON coder,
|
|
120
|
+
so the whole query feature refused to run on a perfectly queryable column. A blank or
|
|
121
|
+
corrupt store value no longer makes every `where_` query raise on SQLite. A read-only
|
|
122
|
+
finder no longer mutates the caller's `Time`. Key names are validated at macro time.
|
|
123
|
+
(#76)
|
|
124
|
+
- **Models::CounterCacheable**: the locking spec added in 1.28.6 matched the SQLite
|
|
125
|
+
transaction statement with `start_with?("begin")`; Rails 7.2+ switched SQLite to
|
|
126
|
+
IMMEDIATE transactions and upcased it, so the assertion silently found nothing on
|
|
127
|
+
Rails 8.x. Test-only.
|
|
128
|
+
|
|
129
|
+
### Internal
|
|
130
|
+
- `json` is pinned to `< 3` in the Gemfile. Verified against a real 8.1.3.1 gemset:
|
|
131
|
+
with json 3.0.2 the suite fails in Storable's decode path; with `json < 3` Rails
|
|
132
|
+
8.1.3.1 is green.
|
|
133
|
+
- `require "active_support/notifications"` added to `authorizable.rb` and
|
|
134
|
+
`error_handleable.rb`, which instrument without requiring it — a direct require of
|
|
135
|
+
either file used to `NameError` on the first event. (#68)
|
|
136
|
+
|
|
137
|
+
## 1.28.6 (2026-09-18)
|
|
138
|
+
|
|
139
|
+
Ten feature PRs deepening existing concerns, released as a patch by request: no new
|
|
140
|
+
concerns and no dependency changes, though several add an optional column or option.
|
|
141
|
+
Every PR was reviewed before merge and carries the review's fixes; the `### Changed`
|
|
142
|
+
and `### Fixed` sections below are those fixes, and they are the ones to read — a few
|
|
143
|
+
tighten validation or start raising where the code used to accept bad input silently.
|
|
144
|
+
Eight further PRs were reviewed in the same pass and held back: they carry defects
|
|
145
|
+
that need more than a patch-sized change.
|
|
146
|
+
|
|
147
|
+
### Added
|
|
148
|
+
- **Models::Maskable**: `masked_attributes` (every declared field masked, keyed like
|
|
149
|
+
`attributes`) and a `masked:` serialization option — `as_json(masked: true)` /
|
|
150
|
+
`to_json(masked: true)` / `serializable_hash(masked: true)` swap the declared fields
|
|
151
|
+
for their masked forms (`masked: [:email]` for a subset; undeclared fields raise).
|
|
152
|
+
Composes with `only:`/`except:`/`methods:`/`include:`; plain serialization is
|
|
153
|
+
unchanged. (#65)
|
|
154
|
+
- **Models::Activatable**: `before_activate` / `after_activate` / `before_deactivate` /
|
|
155
|
+
`after_deactivate` hooks (one transaction with the write; a raising after-hook rolls
|
|
156
|
+
back, a failed update skips it; overriding one moves that direction's batch verb to
|
|
157
|
+
the per-record path) and `activatable_by timestamps:` (`true` → `activated_at` /
|
|
158
|
+
`deactivated_at` stamped on each transition, or a Hash to rename/drop a side). The
|
|
159
|
+
batch fast path stamps too. (#78)
|
|
160
|
+
- **Models::Hashable**: `prefix:` prepends a literal to generated values (Stripe-style
|
|
161
|
+
public IDs such as `ord_k7m3pq9a`; string types only), and `to_param: true` makes the
|
|
162
|
+
hashed field the URL parameter (falls back to the id while blank). Both validated at
|
|
163
|
+
class load. (#70)
|
|
164
|
+
- **Models::Tokenizable**: `tokenizable_by … expires_in:` gives a token a lifetime —
|
|
165
|
+
`<field>_expires_at` is stamped on every generation, `authenticate_by_<field>` refuses
|
|
166
|
+
an expired token, `<field>_expired?` and the `<field>_expired` scope report it. New
|
|
167
|
+
`consume_<field>(value)` (every field) authenticates and revokes in one race-safe step
|
|
168
|
+
for single-use tokens. (#51)
|
|
169
|
+
- **Models::Auditable**: `ConcernsOnRails.setup { |c| c.audit_actor = -> { Current.user&.id } }`
|
|
170
|
+
sets the `"by"` actor once for every audited model that passes no `actor:` (resolved
|
|
171
|
+
per save, `instance_exec`'d on the record); `actor: false` opts a model out, and
|
|
172
|
+
`actor: :method_name` stamps a record method's value. (#50)
|
|
173
|
+
- **Models::CounterCacheable**: `recount_counter_caches!(association, parents:)` repairs
|
|
174
|
+
only the given parents (ids, records or a relation) — locked, zeroed and re-tallied in
|
|
175
|
+
one transaction, other rows untouched — so a post-import fix is O(their children).
|
|
176
|
+
Empty `parents:` is a no-op; the association is required when more than one is
|
|
177
|
+
declared. (#75)
|
|
178
|
+
- **Controllers::Includable**: nested include allow-lists — `includable :author,
|
|
179
|
+
comments: :author` accepts `?include=comments.author` (a path must match every
|
|
180
|
+
segment). `requested_includes(as: :query | :paths | :json)` returns the sanitized
|
|
181
|
+
includes for `includes`/`preload`, JSON:API serializers, or `as_json(include:)`;
|
|
182
|
+
`requested_include_paths` exposes the dotted paths. `default:` loads named paths when
|
|
183
|
+
`?include` is absent (blank opts out); `strategy:` picks
|
|
184
|
+
`includes`/`preload`/`eager_load`. New `Support::IncludeTree`. (#69)
|
|
185
|
+
- **Controllers::Idempotentable**: replays now carry the original response's `Location`,
|
|
186
|
+
`Content-Location`, `ETag`, `Last-Modified` and `Link` headers, so a retried `create`
|
|
187
|
+
still says where the resource lives. `idempotent_actions … headers:` tunes the
|
|
188
|
+
allow-list (`[]` to capture none). Records written before this release replay
|
|
189
|
+
unchanged. (#47)
|
|
190
|
+
- **Controllers::Throttleable**: `throttle_by … if:/unless:` per-request skip conditions
|
|
191
|
+
(Symbol method or callable; both must pass), mirroring Rails 7.2's `rate_limit`. A
|
|
192
|
+
throttled request instruments `rate_limited.concerns_on_rails` (rule, discriminator,
|
|
193
|
+
count/limit/period, reset_at/retry_after, controller, action) through the new public
|
|
194
|
+
`on_rate_limited(rule, result)` hook. (#44)
|
|
195
|
+
- **Controllers::Respondable**: `respondable_by error_format: :problem_details,
|
|
196
|
+
problem_type_base:` makes `render_error` emit RFC 9457 `application/problem+json`
|
|
197
|
+
documents (`type`, `title`, `status`, `detail`, `instance`, plus `code`/`errors`
|
|
198
|
+
extensions). Every concern that renders errors through Respondable follows, so the
|
|
199
|
+
switch is app-wide. The default stays `:envelope`. (#57)
|
|
200
|
+
|
|
201
|
+
### Changed
|
|
202
|
+
- **Controllers::Throttleable**: with several applicable rules the `X-RateLimit-*`
|
|
203
|
+
headers now describe the tightest passing rule (fewest remaining) instead of the last
|
|
204
|
+
one declared, and a tie on remaining is broken by the rule that resets *last* — so the
|
|
205
|
+
advertised `X-RateLimit-Reset` can no longer promise a 60-second wait when an hourly
|
|
206
|
+
rule is the real budget. `result[:discriminator]` is now available to
|
|
207
|
+
`throttled_response`. Note it is raw personal data (an IP or a user id) in the
|
|
208
|
+
instrumentation payload too; override `on_rate_limited` to hash or drop it. (#44)
|
|
209
|
+
- **Controllers::Throttleable**: `if:`/`unless:` conditions are now arity-aware — a
|
|
210
|
+
one-argument callable receives the controller, a zero-arity Proc is still
|
|
211
|
+
`instance_exec`'d. Previously a `->(c) { … }` condition raised `ArgumentError` on
|
|
212
|
+
every request. (#44)
|
|
213
|
+
- **Models::Auditable**: an explicit `actor: nil` now keeps meaning "never record who"
|
|
214
|
+
and no longer falls through to the gem-wide `config.audit_actor`; only omitting
|
|
215
|
+
`actor:` takes the fallback. `config.audit_actor` accepts a non-Proc callable and
|
|
216
|
+
`false` (a synonym for nil), and rejects a lambda that demands arguments at
|
|
217
|
+
configuration time rather than raising inside `before_save`. (#50)
|
|
218
|
+
- **Models::CounterCacheable**: `recount_counter_caches!` now raises `ArgumentError`
|
|
219
|
+
for an association with no declared counter, instead of returning `{}` as a silent
|
|
220
|
+
success; and a scoped repair locks the named parent rows before tallying, so a
|
|
221
|
+
concurrent child write can no longer be lost. (#75)
|
|
222
|
+
- **Controllers::Idempotentable**: response-header capture is case-insensitive, so an
|
|
223
|
+
app that sets `location` rather than `Location` (correct under Rack 3) is captured on
|
|
224
|
+
every supported Rails version, not only 7.1+. `idempotency_scope` is now a documented
|
|
225
|
+
public override point — the default namespace has no per-principal component, so two
|
|
226
|
+
users sharing a client-chosen key can be served each other's cached response. (#47)
|
|
227
|
+
- **Controllers::Respondable**: the problem-details media type is emitted as bare
|
|
228
|
+
`application/problem+json`, with no `charset` parameter — RFC 9457 registers none, and
|
|
229
|
+
strict clients reject the parameterised form. (#57)
|
|
230
|
+
|
|
231
|
+
### Fixed
|
|
232
|
+
- **Models::Activatable**: lifecycle hooks are invoked with `send`, so a `private` hook
|
|
233
|
+
override runs instead of raising `NoMethodError` — which in a batch verb aborted and
|
|
234
|
+
rolled back the whole run. (#78)
|
|
235
|
+
- **Models::Hashable**: the `to_param: true` / Sluggable conflict now raises in *either*
|
|
236
|
+
declaration order. With Hashable declared first, friendly_id's `to_param` used to land
|
|
237
|
+
above Hashable's and silently win, so `to_param: true` did nothing. (#70)
|
|
238
|
+
- **Models::Tokenizable**: `expires_in:` now requires an `ActiveSupport::Duration` or a
|
|
239
|
+
`Numeric`. `expires_in: 2.hours.from_now` was accepted as ~1.8 billion seconds (tokens
|
|
240
|
+
effectively never expired) and `expires_in: "2 hours"` as 2 seconds. A model missing
|
|
241
|
+
both token columns now reports them in one error. (#51)
|
|
242
|
+
- **Controllers::Includable**: `requested_includes(as: :paths)` returns a fresh array on
|
|
243
|
+
the `default:` path; it previously handed back the class attribute itself, so a caller
|
|
244
|
+
mutating the result corrupted every later request in the process. (#69)
|
|
245
|
+
|
|
3
246
|
## 1.28.5 (2026-09-17)
|
|
4
247
|
|
|
5
248
|
Eight feature PRs deepening existing concerns, released as a patch by request:
|