statecraft 0.7.0 → 0.7.1
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/README.md +12 -3
- data/lib/statecraft/errors.rb +12 -5
- data/lib/statecraft/pipeline/versioning.rb +11 -1
- data/lib/statecraft/pipeline.rb +3 -1
- data/lib/statecraft/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b91fc7675cab350149a2e58679893b18ccdfa779cc84e2eb5a7d28c5591c265c
|
|
4
|
+
data.tar.gz: c751611f88af3a85738877185b68de734578a353818aa130c19206d40b498b82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 42c744386532cf91f06374dc3695c4e2ee3e64602fbb428045e3b588c27dc32843959552dea944be5d98834f28036baf1d712da2c25008dba8b16766ca23310b
|
|
7
|
+
data.tar.gz: 86822e6c1fba14b00e6a8e18efc586a7f92f35280490a168e4d96d063a6d3ecd471eac6fe877c512b9972359d1d14f4d344521dc6f0fea56235075df2bae975b
|
data/README.md
CHANGED
|
@@ -263,8 +263,12 @@ end
|
|
|
263
263
|
```
|
|
264
264
|
|
|
265
265
|
`seen:` rides all four surface forms and the helper verbs. A string token
|
|
266
|
-
straight from params is fine — the pipeline normalizes it with `Integer()
|
|
267
|
-
|
|
266
|
+
straight from params is fine — the pipeline normalizes it with `Integer()`.
|
|
267
|
+
The field comes back from the browser, so it is hostile input: a token that
|
|
268
|
+
cannot be read at all — a tampered `seen=abc`, an array from `seen[]=1` —
|
|
269
|
+
is refused as `StaleTransition` too, with `expected_version: nil` because
|
|
270
|
+
nothing was compared. A broken or forged form never turns into a 500.
|
|
271
|
+
The honest limits: staleness
|
|
268
272
|
exists only where a token was given — without `seen:` a version mismatch
|
|
269
273
|
is the ordinary `TransitionConflict`; the early deterministic check runs
|
|
270
274
|
only under `lock: true`, where the reload holds the row's real version —
|
|
@@ -513,6 +517,11 @@ statecraft's CAS on the state column.
|
|
|
513
517
|
Outside Rails, the same five steps work by hand — pair the runbook above
|
|
514
518
|
with the reference schema in [Outside Rails](#outside-rails).
|
|
515
519
|
|
|
520
|
+
Once the conversion has settled, one more `add_column` buys protection
|
|
521
|
+
statesman never had: see [Stale transitions](#stale-transitions-versioning-against-aba)
|
|
522
|
+
— the version column is a constant default, so adding it costs a
|
|
523
|
+
metadata-only migration on PostgreSQL 11+.
|
|
524
|
+
|
|
516
525
|
## PII and erasure
|
|
517
526
|
|
|
518
527
|
Metadata is the only place personal data can live — `from_state`, `to_state`
|
|
@@ -732,7 +741,7 @@ possibility-times-permission intersection:
|
|
|
732
741
|
<!-- readme: preview-pattern -->
|
|
733
742
|
```erb
|
|
734
743
|
<%= form_with url: preview_admin_order_path(@order), method: :post, local: true do %>
|
|
735
|
-
<input type="hidden" name="seen" value="<%= @order
|
|
744
|
+
<input type="hidden" name="seen" value="<%= @order.state_version %>">
|
|
736
745
|
<fieldset>
|
|
737
746
|
<legend>Metadata for the next action</legend>
|
|
738
747
|
<label>
|
data/lib/statecraft/errors.rb
CHANGED
|
@@ -46,18 +46,25 @@ module Statecraft
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# The refusal of a seen: token: the snapshot the caller acted on is
|
|
50
|
-
#
|
|
51
|
-
#
|
|
49
|
+
# The refusal of a seen: token: the snapshot the caller acted on is not
|
|
50
|
+
# the row's version — or could not be read at all, which is the same
|
|
51
|
+
# answer with nothing to compare (expected_version stays nil). A subclass
|
|
52
|
+
# of TransitionConflict, so existing rescues keep catching it;
|
|
53
|
+
# controllers map it to 409 specifically.
|
|
52
54
|
class StaleTransition < TransitionConflict
|
|
53
55
|
attr_reader :expected_version, :seen
|
|
54
56
|
|
|
55
57
|
def initialize(record:, expected_from:, expected_version:, seen:)
|
|
56
58
|
@expected_version = expected_version
|
|
57
59
|
@seen = seen
|
|
60
|
+
reason =
|
|
61
|
+
if expected_version.nil?
|
|
62
|
+
"the token #{seen.inspect} is not a readable version"
|
|
63
|
+
else
|
|
64
|
+
"the caller saw version #{seen.inspect}, but the row has moved on"
|
|
65
|
+
end
|
|
58
66
|
super(record: record, expected_from: expected_from,
|
|
59
|
-
message: "stale transition for #{record.class.name}##{record.id}: "
|
|
60
|
-
"the caller saw version #{seen.inspect}, but the row has moved on")
|
|
67
|
+
message: "stale transition for #{record.class.name}##{record.id}: #{reason}")
|
|
61
68
|
end
|
|
62
69
|
end
|
|
63
70
|
|
|
@@ -22,6 +22,10 @@ module Statecraft
|
|
|
22
22
|
column
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# The token arrives from a form field, so it is hostile input: a
|
|
26
|
+
# tampered or broken form must not escape the gem's own hierarchy.
|
|
27
|
+
# An unreadable token IS an invalid snapshot — the same refusal a
|
|
28
|
+
# stale one gets, with no version to compare against.
|
|
25
29
|
def normalize_seen(raw_seen)
|
|
26
30
|
return nil if raw_seen.nil?
|
|
27
31
|
|
|
@@ -30,7 +34,13 @@ module Statecraft
|
|
|
30
34
|
"seen: requires versioning: true on the mounting of #{record.class.name}; " \
|
|
31
35
|
"add the option or drop the token"
|
|
32
36
|
end
|
|
33
|
-
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
Integer(raw_seen)
|
|
40
|
+
rescue ArgumentError, TypeError
|
|
41
|
+
raise StaleTransition.new(record: record, expected_from: current_state,
|
|
42
|
+
expected_version: nil, seen: raw_seen)
|
|
43
|
+
end
|
|
34
44
|
end
|
|
35
45
|
|
|
36
46
|
# What the CAS requires the row's version to be: the caller's seen:
|
data/lib/statecraft/pipeline.rb
CHANGED
|
@@ -65,10 +65,12 @@ module Statecraft
|
|
|
65
65
|
|
|
66
66
|
assert_single_column_primary_key
|
|
67
67
|
@raw_seen = seen
|
|
68
|
-
@seen = normalize_seen(seen)
|
|
69
68
|
metadata = Metadata.normalize(raw_metadata)
|
|
70
69
|
started_at = Time.current
|
|
71
70
|
begin
|
|
71
|
+
# Inside the rescue: an unreadable token is a refusal like any
|
|
72
|
+
# other and belongs in the failure telemetry.
|
|
73
|
+
@seen = normalize_seen(seen)
|
|
72
74
|
edge, event, bypass = edge_resolver.call(current_state)
|
|
73
75
|
assert_clean_when_locked(edge)
|
|
74
76
|
frame = open_frame(edge, event)
|
data/lib/statecraft/version.rb
CHANGED