phlex-reactive 0.2.2 → 0.2.3

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: 11c82575cc72017be4f81341c0c2f999d626fe88ed8b81a20cfde246a412fe12
4
- data.tar.gz: '0996ececdc33cd2220a7a6517d49d1a59c5c583af891b937f1df32f433189a65'
3
+ metadata.gz: 5644272b11bc8ff62496e05c81f01a92b1925ffe2b471ab123c05df573ee1343
4
+ data.tar.gz: 56d13ad853e1b6da4e40e8e239838514171b9e908a59ad6db60f6f4a4b199b0e
5
5
  SHA512:
6
- metadata.gz: 24b3704443cd08380e30bb2bd4340d266a75a39beb80247a4ba26327a4c6f6429b0017fc94618b4063dc4f00a7a86f49ab88074c008d9be1c2ead0b266b5c8cd
7
- data.tar.gz: 4f858ded4f109ce0350603140572b4d9c694acd981785ac1913c0cf4c48630e6927d223073cd89fbb1f6a72e16bf7cea0603079661fff7b80740a4389ccbe732
6
+ metadata.gz: a489e7a7cdce253735c59c4b5196c52d2051e83f7fb783e7c892cbbd8b9be98934141392d7a84b58b938e80bfd5aba6e4bfe1995888b2b322d232a3fdc207a4a
7
+ data.tar.gz: b9126364a92d1be370e6db639969803c25936e61910967fe05cac00ce908216dd5fa6f6f2f42f16d3c3c23c813d4e2e1c54614babb96f162f77bd71b6e3a5f6b
data/CHANGELOG.md CHANGED
@@ -8,6 +8,21 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
9
  ### Fixed
10
10
 
11
+ - **Record-backed components silently lost `reactive_state` every action.** When
12
+ a component declared BOTH `reactive_record` and `reactive_state`, the state
13
+ branch was dead: `reactive_token` signed only the record GID and
14
+ `from_identity` rebuilt with only the record — so the listed instance vars
15
+ (e.g. `attribute`, `editing`) reset to their `initialize` defaults on every
16
+ action. This broke the documented `inline_edit` example (clicking "edit"
17
+ couldn't stay in edit mode; `save` wrote the wrong/blank column) and quietly
18
+ voided the docs' promise that `@attribute` is signed/tamper-proof.
19
+ `reactive_token` now signs the record GID AND the declared state into one
20
+ `MessageVerifier` payload, and `from_identity` restores both — record + state
21
+ are composable, and the state stays tamper-proof. Record-only (`{c, gid}`) and
22
+ state-only (`{c, s}`) token shapes are unchanged; a signed `false` survives the
23
+ round trip (only a genuinely absent value falls back to the `initialize`
24
+ default). No API changes. Closes #6.
25
+
11
26
  - **Record-backed component built with a different init keyword by the action
12
27
  endpoint vs the broadcast path.** The click path (`Component.from_identity`)
13
28
  builds with `reactive_record_key` (the `reactive_record :name`), but the
@@ -20,8 +20,12 @@ module Phlex
20
20
  # can neither forge the component class nor swap the record. State =
21
21
  # the database.
22
22
  # * State-backed (record-less, e.g. a counter): reactive_state :count
23
- # signs the listed instance variables. Use only when there is genuinely
24
- # no record to re-find.
23
+ # signs the listed instance variables. Use when there is genuinely no
24
+ # record to re-find.
25
+ # * Both (the inline_edit pattern): reactive_record :record plus
26
+ # reactive_state :attribute, :editing signs the record's GlobalID AND
27
+ # the transient mode in one token, so "which field / what mode" survives
28
+ # every action round trip and stays tamper-proof.
25
29
  #
26
30
  # Actions are DEFAULT-DENY: only methods declared with `action :name` may be
27
31
  # invoked. The signature proves the token is ours, NOT that this user may
@@ -102,17 +106,35 @@ module Phlex
102
106
 
103
107
  # Rebuild a component instance from a verified identity payload. Called
104
108
  # by the action endpoint after the token signature is verified.
109
+ #
110
+ # A component may carry a record (re-found via GlobalID), signed state
111
+ # (instance vars listed in reactive_state), or BOTH (the inline_edit
112
+ # pattern: a record plus "which field / what mode"). We assemble the
113
+ # init kwargs from whichever identity pieces are declared.
105
114
  def from_identity(payload)
115
+ kwargs = {}
116
+
106
117
  if reactive_record_key
107
118
  record = GlobalID::Locator.locate(payload.fetch("gid"))
108
119
  raise(ActiveRecord::RecordNotFound, "reactive record missing") unless record
109
120
 
110
- new(reactive_record_key => record)
111
- else
121
+ kwargs[reactive_record_key] = record
122
+ end
123
+
124
+ if reactive_state_keys.any?
112
125
  state = payload.fetch("s", {})
113
- kwargs = reactive_state_keys.to_h { |k| [k, state[k.to_s]] }.compact
114
- new(**kwargs)
126
+ reactive_state_keys.each do |key|
127
+ # Use key presence, not the value: a signed `nil` (nullable state)
128
+ # must round-trip distinctly. Only a genuinely absent key falls
129
+ # back to the component's initialize default; `false` and `nil`
130
+ # both survive.
131
+ next unless state.key?(key.to_s)
132
+
133
+ kwargs[key] = state[key.to_s]
134
+ end
115
135
  end
136
+
137
+ new(**kwargs)
116
138
  end
117
139
  end
118
140
 
@@ -164,18 +186,25 @@ module Phlex
164
186
 
165
187
  private
166
188
 
167
- # Signed { c, gid } (record-backed) or { c, s } (state-backed).
189
+ # Signed identity payload: the class name plus whichever identity pieces
190
+ # the component declares — a record GlobalID (`gid`), signed state (`s`),
191
+ # or both. Keeping them in ONE MessageVerifier payload makes the state
192
+ # (e.g. which column an inline_edit may write) tamper-proof alongside the
193
+ # record. Record-only ({c, gid}) and state-only ({c, s}) shapes are
194
+ # unchanged.
168
195
  def reactive_token
169
- payload =
170
- if self.class.reactive_record_key
171
- record = instance_variable_get(:"@#{self.class.reactive_record_key}")
172
- {"c" => self.class.name, "gid" => record.to_gid.to_s}
173
- else
174
- state = self.class.reactive_state_keys.to_h do |k|
175
- [k.to_s, instance_variable_get(:"@#{k}").as_json]
176
- end
177
- {"c" => self.class.name, "s" => state}
196
+ payload = {"c" => self.class.name}
197
+
198
+ if self.class.reactive_record_key
199
+ record = instance_variable_get(:"@#{self.class.reactive_record_key}")
200
+ payload["gid"] = record.to_gid.to_s
201
+ end
202
+
203
+ if self.class.reactive_state_keys.any?
204
+ payload["s"] = self.class.reactive_state_keys.to_h do |k|
205
+ [k.to_s, instance_variable_get(:"@#{k}").as_json]
178
206
  end
207
+ end
179
208
 
180
209
  Phlex::Reactive.sign(payload)
181
210
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.2.2"
5
+ VERSION = "0.2.3"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson