concerns_on_rails 1.28.4 → 1.28.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 615b4ddcff3ebdffc7e32f618731510bf5e386fa015cb0a1ae958a4e6fb04d1c
4
- data.tar.gz: 723b3ae2077f43fc5771947d2e610f011ca83ee8215b78fb0894a04400362220
3
+ metadata.gz: d01a41891ae267bab705b85ab63d6555e7057719a218738bd82c858c63febfa9
4
+ data.tar.gz: 3bcaebd299e65af41029baebb90ba1670a3502fde222071bffeeef86a4e1fb7b
5
5
  SHA512:
6
- metadata.gz: 352ff281ce000213b296a1b8ccae97478d6a6726644fb6bcf69c5f4e05d94c57e674f5cff59f3ed0ff8d90891c90b6ebc53affdec5f47ea64102ca88a3c40a0e
7
- data.tar.gz: 595236a2464439aa26d10ed118fc5d307ca47022cac8e5c9cae6077419da39dc7a3fc2d085534bfb2d36bdf3625c99442054dc7fd5868be71b9034923aa66e11
6
+ metadata.gz: 602d50edd8fd84224c73a6ddc09e756bf322f4b3b4e526d34e55774caa88594c54c3ba05d1920005c2152986035b230e2f982e5b6916c2a3a2ec402e1ac2c3c6
7
+ data.tar.gz: 9ab3f9c635ab132b284d673861c3a3a8282f93aa370f6f75cb1890a1205b9e3c924705b52afd7d182a94fb5247fdc38f51e94bcc4d2a0b1c8de39ea53ee03b54
data/CHANGELOG.md CHANGED
@@ -1,5 +1,174 @@
1
1
  <!-- CHANGELOG.md -->
2
2
 
3
+ ## 1.28.6 (2026-09-18)
4
+
5
+ Ten feature PRs deepening existing concerns, released as a patch by request: no new
6
+ concerns and no dependency changes, though several add an optional column or option.
7
+ Every PR was reviewed before merge and carries the review's fixes; the `### Changed`
8
+ and `### Fixed` sections below are those fixes, and they are the ones to read — a few
9
+ tighten validation or start raising where the code used to accept bad input silently.
10
+ Eight further PRs were reviewed in the same pass and held back: they carry defects
11
+ that need more than a patch-sized change.
12
+
13
+ ### Added
14
+ - **Models::Maskable**: `masked_attributes` (every declared field masked, keyed like
15
+ `attributes`) and a `masked:` serialization option — `as_json(masked: true)` /
16
+ `to_json(masked: true)` / `serializable_hash(masked: true)` swap the declared fields
17
+ for their masked forms (`masked: [:email]` for a subset; undeclared fields raise).
18
+ Composes with `only:`/`except:`/`methods:`/`include:`; plain serialization is
19
+ unchanged. (#65)
20
+ - **Models::Activatable**: `before_activate` / `after_activate` / `before_deactivate` /
21
+ `after_deactivate` hooks (one transaction with the write; a raising after-hook rolls
22
+ back, a failed update skips it; overriding one moves that direction's batch verb to
23
+ the per-record path) and `activatable_by timestamps:` (`true` → `activated_at` /
24
+ `deactivated_at` stamped on each transition, or a Hash to rename/drop a side). The
25
+ batch fast path stamps too. (#78)
26
+ - **Models::Hashable**: `prefix:` prepends a literal to generated values (Stripe-style
27
+ public IDs such as `ord_k7m3pq9a`; string types only), and `to_param: true` makes the
28
+ hashed field the URL parameter (falls back to the id while blank). Both validated at
29
+ class load. (#70)
30
+ - **Models::Tokenizable**: `tokenizable_by … expires_in:` gives a token a lifetime —
31
+ `<field>_expires_at` is stamped on every generation, `authenticate_by_<field>` refuses
32
+ an expired token, `<field>_expired?` and the `<field>_expired` scope report it. New
33
+ `consume_<field>(value)` (every field) authenticates and revokes in one race-safe step
34
+ for single-use tokens. (#51)
35
+ - **Models::Auditable**: `ConcernsOnRails.setup { |c| c.audit_actor = -> { Current.user&.id } }`
36
+ sets the `"by"` actor once for every audited model that passes no `actor:` (resolved
37
+ per save, `instance_exec`'d on the record); `actor: false` opts a model out, and
38
+ `actor: :method_name` stamps a record method's value. (#50)
39
+ - **Models::CounterCacheable**: `recount_counter_caches!(association, parents:)` repairs
40
+ only the given parents (ids, records or a relation) — locked, zeroed and re-tallied in
41
+ one transaction, other rows untouched — so a post-import fix is O(their children).
42
+ Empty `parents:` is a no-op; the association is required when more than one is
43
+ declared. (#75)
44
+ - **Controllers::Includable**: nested include allow-lists — `includable :author,
45
+ comments: :author` accepts `?include=comments.author` (a path must match every
46
+ segment). `requested_includes(as: :query | :paths | :json)` returns the sanitized
47
+ includes for `includes`/`preload`, JSON:API serializers, or `as_json(include:)`;
48
+ `requested_include_paths` exposes the dotted paths. `default:` loads named paths when
49
+ `?include` is absent (blank opts out); `strategy:` picks
50
+ `includes`/`preload`/`eager_load`. New `Support::IncludeTree`. (#69)
51
+ - **Controllers::Idempotentable**: replays now carry the original response's `Location`,
52
+ `Content-Location`, `ETag`, `Last-Modified` and `Link` headers, so a retried `create`
53
+ still says where the resource lives. `idempotent_actions … headers:` tunes the
54
+ allow-list (`[]` to capture none). Records written before this release replay
55
+ unchanged. (#47)
56
+ - **Controllers::Throttleable**: `throttle_by … if:/unless:` per-request skip conditions
57
+ (Symbol method or callable; both must pass), mirroring Rails 7.2's `rate_limit`. A
58
+ throttled request instruments `rate_limited.concerns_on_rails` (rule, discriminator,
59
+ count/limit/period, reset_at/retry_after, controller, action) through the new public
60
+ `on_rate_limited(rule, result)` hook. (#44)
61
+ - **Controllers::Respondable**: `respondable_by error_format: :problem_details,
62
+ problem_type_base:` makes `render_error` emit RFC 9457 `application/problem+json`
63
+ documents (`type`, `title`, `status`, `detail`, `instance`, plus `code`/`errors`
64
+ extensions). Every concern that renders errors through Respondable follows, so the
65
+ switch is app-wide. The default stays `:envelope`. (#57)
66
+
67
+ ### Changed
68
+ - **Controllers::Throttleable**: with several applicable rules the `X-RateLimit-*`
69
+ headers now describe the tightest passing rule (fewest remaining) instead of the last
70
+ one declared, and a tie on remaining is broken by the rule that resets *last* — so the
71
+ advertised `X-RateLimit-Reset` can no longer promise a 60-second wait when an hourly
72
+ rule is the real budget. `result[:discriminator]` is now available to
73
+ `throttled_response`. Note it is raw personal data (an IP or a user id) in the
74
+ instrumentation payload too; override `on_rate_limited` to hash or drop it. (#44)
75
+ - **Controllers::Throttleable**: `if:`/`unless:` conditions are now arity-aware — a
76
+ one-argument callable receives the controller, a zero-arity Proc is still
77
+ `instance_exec`'d. Previously a `->(c) { … }` condition raised `ArgumentError` on
78
+ every request. (#44)
79
+ - **Models::Auditable**: an explicit `actor: nil` now keeps meaning "never record who"
80
+ and no longer falls through to the gem-wide `config.audit_actor`; only omitting
81
+ `actor:` takes the fallback. `config.audit_actor` accepts a non-Proc callable and
82
+ `false` (a synonym for nil), and rejects a lambda that demands arguments at
83
+ configuration time rather than raising inside `before_save`. (#50)
84
+ - **Models::CounterCacheable**: `recount_counter_caches!` now raises `ArgumentError`
85
+ for an association with no declared counter, instead of returning `{}` as a silent
86
+ success; and a scoped repair locks the named parent rows before tallying, so a
87
+ concurrent child write can no longer be lost. (#75)
88
+ - **Controllers::Idempotentable**: response-header capture is case-insensitive, so an
89
+ app that sets `location` rather than `Location` (correct under Rack 3) is captured on
90
+ every supported Rails version, not only 7.1+. `idempotency_scope` is now a documented
91
+ public override point — the default namespace has no per-principal component, so two
92
+ users sharing a client-chosen key can be served each other's cached response. (#47)
93
+ - **Controllers::Respondable**: the problem-details media type is emitted as bare
94
+ `application/problem+json`, with no `charset` parameter — RFC 9457 registers none, and
95
+ strict clients reject the parameterised form. (#57)
96
+
97
+ ### Fixed
98
+ - **Models::Activatable**: lifecycle hooks are invoked with `send`, so a `private` hook
99
+ override runs instead of raising `NoMethodError` — which in a batch verb aborted and
100
+ rolled back the whole run. (#78)
101
+ - **Models::Hashable**: the `to_param: true` / Sluggable conflict now raises in *either*
102
+ declaration order. With Hashable declared first, friendly_id's `to_param` used to land
103
+ above Hashable's and silently win, so `to_param: true` did nothing. (#70)
104
+ - **Models::Tokenizable**: `expires_in:` now requires an `ActiveSupport::Duration` or a
105
+ `Numeric`. `expires_in: 2.hours.from_now` was accepted as ~1.8 billion seconds (tokens
106
+ effectively never expired) and `expires_in: "2 hours"` as 2 seconds. A model missing
107
+ both token columns now reports them in one error. (#51)
108
+ - **Controllers::Includable**: `requested_includes(as: :paths)` returns a fresh array on
109
+ the `default:` path; it previously handed back the class attribute itself, so a caller
110
+ mutating the result corrupted every later request in the process. (#69)
111
+
112
+ ## 1.28.5 (2026-09-17)
113
+
114
+ Eight feature PRs deepening existing concerns, released as a patch by request:
115
+ no new concerns and no dependency changes, though several add an optional
116
+ column or option. Every PR was reviewed before merge and carries the review's
117
+ fixes; the notes below call out the ones that change a documented behaviour.
118
+ 1522 examples, 0 failures.
119
+
120
+ ### Added
121
+ - **Models::Schedulable**: `overlapping(from, to)` scope and `overlaps?`
122
+ predicate for booking-clash checks. Open-ended windows (a `NULL` start or
123
+ end), touching intervals, reversed arguments and Range forms behave
124
+ identically in SQL and in Ruby. Affixable like the other scopes. (#58)
125
+ - **Models::Duplicable**: per-call `only:` / `except:` association selection —
126
+ `invoice.duplicate!(except: :line_items)`. The macro's list stays the
127
+ ceiling, so a controller param cannot smuggle in an undeclared association.
128
+ An explicit `nil` counts as passed, not absent, so an empty checkbox list
129
+ copies nothing rather than everything. (#73)
130
+ - **Models::Monetizable**: scope-aware `sum_`, `average_`, `minimum_` and
131
+ `maximum_<name>` aggregates plus their `formatted_` twins, with per-call
132
+ formatting overrides. BigDecimal throughout; a grouped relation returns a
133
+ Hash of converted values instead of raising. (#67)
134
+ - **Models::Taggable**: `tag_counts` — tag to record count in one `GROUP BY`
135
+ query, relation-aware, with `limit:` for the top N. A `select`, `group` or
136
+ `order` on the relation is stripped; a `limit`/`offset` window is honoured by
137
+ resolving it to ids first. (#56)
138
+ - **Models::Expirable**: `before_expire` / `after_expire` lifecycle hooks,
139
+ `expire_in!(duration)` and `clear_expiry!`. The hooks fire when a write
140
+ actually expires the record; a future time only schedules expiry and fires
141
+ nothing, so `after_expire { account.downgrade! }` is safe next to
142
+ `trial.expire_in!(14.days)`. Overriding either hook moves `expire_all` to the
143
+ per-record path. (#54)
144
+ - **Controllers::SecureHeadable**: HSTS, cross-origin (COOP/COEP/CORP) and
145
+ Permissions-Policy presets, plus the `:recommended` and
146
+ `:cross_origin_isolation` bundles. `:recommended` uses `(self)`-scoped
147
+ permission values, so it denies third-party frames without disabling the
148
+ app's own camera, microphone, geolocation or Payment Request. HSTS is skipped
149
+ on a plaintext request (RFC 6797 §7.2) and never overwrites a stricter value
150
+ already on the response. (#55)
151
+ - **Models::Addressable**: `address_fingerprint` (SHA-256 of the normalized
152
+ address), `same_address_as?`, `address_parts_changed?` and a `fingerprint:`
153
+ column option with a `with_address` finder for deduplication reports. The
154
+ column is stamped in `before_save`, after every `before_validation`, so a
155
+ sibling concern rewriting a mapped column cannot desync it. (#79)
156
+ - **Controllers::Localizable**: `Content-Language` response header and
157
+ `Vary: Accept-Language`, both written before the action so a
158
+ `rescue_from`-rendered error carries them. Rails' own `Vary: Accept` is
159
+ preserved rather than suppressed, and `Vary` is advertised only when
160
+ `Accept-Language` can actually change the resolved locale. (#53)
161
+
162
+ ### Notes
163
+ - **Addressable** defines `address_changed?` only when the model has no
164
+ `address` column of its own, so it never shadows ActiveModel's dirty
165
+ predicate; `address_parts_changed?` is always available.
166
+ - Existing rows keep a `NULL` address fingerprint until they are re-saved.
167
+ Backfill with `Model.find_each(&:save)`; `update_columns`, `insert_all` and
168
+ `upsert_all` bypass callbacks and leave it stale.
169
+ - The address digest is unkeyed, so treat the column as revealing the address.
170
+ Do not pair it with an `encryptable` address column.
171
+
3
172
  ## 1.28.4 (2026-09-16)
4
173
 
5
174
  Eleven bug-fix PRs (#91–#101) from the audit of the shipped gem, released as a