@reventlessdev/reventless-core 3.0.0-alpha.257 → 3.0.0-alpha.259

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +5 -5
  3. package/src/Projection.res +158 -40
  4. package/src/Projection.res.mjs +107 -28
  5. package/src/admin/UiFragmentRegistry/{StateChangeSlice → StateChange}/UiFragmentRegistry.res.mjs +1 -1
  6. package/src/admin/UiFragmentRegistry/{StateChangeSlice → StateChange}/UiFragmentRegistry_Behavior.res.mjs +1 -1
  7. package/src/admin/UiFragmentRegistry/{StateViewSlice → StateView}/UiFragments.res.mjs +1 -1
  8. package/src/admin/UiFragmentRegistry/{StateViewSlice → StateView}/UiFragments_Projection.res.mjs +1 -1
  9. package/src/components/Api/GraphQL_FragmentGenerator.res +12 -1
  10. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +2 -1
  11. package/src/components/Api/SchemaType.res +66 -1
  12. package/src/components/Api/SchemaType.res.mjs +66 -3
  13. package/src/components/Api/SuryToJsonSchema.res +1 -0
  14. package/src/components/Api/SuryToJsonSchema.res.mjs +11 -0
  15. package/src/components/ReadModel/ReadModel_Callback.res +5 -2
  16. package/src/components/ReadModel/ReadModel_Callback.res.mjs +2 -5
  17. package/src/components/StateViewSlice/StateViewSlice_Builder.res +9 -3
  18. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +1 -1
  19. package/src/components/StateViewSlice/StateViewSlice_Callback.res +5 -1
  20. package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +1 -1
  21. package/src/plugin/component/Plugin_Structure.res +9 -46
  22. package/src/plugin/component/Plugin_Structure.res.mjs +8 -40
  23. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +1 -1
  24. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +1 -1
  25. package/tests/api/BrandedMarkerCompositionTest.res +74 -0
  26. package/tests/api/BrandedMarkerCompositionTest.res.mjs +74 -0
  27. package/tests/api/PsBrandedMarkersReadModel.res +24 -0
  28. package/tests/api/PsBrandedMarkersReadModel.res.mjs +87 -0
  29. package/tests/api/SuryToJsonSchemaTest.res +35 -3
  30. package/tests/api/SuryToJsonSchemaTest.res.mjs +35 -4
  31. package/tests/plugin/PluginStructureAccessTest.res.mjs +3 -3
  32. package/tests/plugin/PluginStructureTest.res +5 -5
  33. package/tests/plugin/PluginStructureTest.res.mjs +26 -26
  34. package/tests/plugin/{StateChangeSlice → StateChange}/PsAttachInvoice.res.mjs +1 -1
  35. package/tests/plugin/{StateChangeSlice → StateChange}/PsChangePhoto.res.mjs +1 -1
  36. package/tests/plugin/{StateChangeSlice → StateChange}/PsDispatchShipment.res.mjs +1 -1
  37. package/tests/plugin/{StateChangeSlice → StateChange}/PsGatedCommands.res.mjs +1 -1
  38. package/tests/plugin/{StateChangeSlice → StateChange}/PsPlaceOrder.res.mjs +1 -1
  39. package/tests/plugin/{StateChangeSlice → StateChange}/PsReserveStock.res.mjs +1 -1
  40. package/tests/plugin/{StateChangeSlice → StateChange}/PsShipOrder.res.mjs +1 -1
  41. package/tests/plugin/{StateChangeSlice → StateChange}/PsTransitionSwitch.res.mjs +1 -1
  42. package/tests/plugin/{StateChangeSlice → StateChange}/PsTypoStore.res.mjs +1 -1
  43. package/tests/plugin/{StateChangeSlice → StateChange}/PsUploadAvatar.res.mjs +1 -1
  44. package/tests/plugin/{StateChangeSlice → StateChange}/PsUploadImages.res.mjs +1 -1
  45. package/tests/plugin/{StateViewSlice → StateView}/PsAnnotatedView.res.mjs +1 -1
  46. package/tests/plugin/{StateViewSlice → StateView}/PsAvailableProductsView.res.mjs +1 -1
  47. package/tests/plugin/{StateViewSlice → StateView}/PsCategoriesView.res.mjs +1 -1
  48. package/tests/plugin/{StateViewSlice → StateView}/PsCustomersView.res.mjs +1 -1
  49. package/tests/plugin/{StateViewSlice → StateView}/PsGatedView.res.mjs +1 -1
  50. package/tests/plugin/{StateViewSlice → StateView}/PsOrdersView.res.mjs +1 -1
  51. package/tests/plugin/{StateViewSlice → StateView}/PsShipmentsView.res.mjs +1 -1
  52. package/tests/projection/LifecycleTrailTest.res +139 -0
  53. package/tests/projection/LifecycleTrailTest.res.mjs +280 -0
  54. /package/src/admin/UiFragmentRegistry/{StateChangeSlice → StateChange}/UiFragmentRegistry.res +0 -0
  55. /package/src/admin/UiFragmentRegistry/{StateChangeSlice → StateChange}/UiFragmentRegistry_Behavior.res +0 -0
  56. /package/src/admin/UiFragmentRegistry/{StateViewSlice → StateView}/UiFragments.res +0 -0
  57. /package/src/admin/UiFragmentRegistry/{StateViewSlice → StateView}/UiFragments_Projection.res +0 -0
  58. /package/tests/plugin/{StateChangeSlice → StateChange}/PsAttachInvoice.res +0 -0
  59. /package/tests/plugin/{StateChangeSlice → StateChange}/PsChangePhoto.res +0 -0
  60. /package/tests/plugin/{StateChangeSlice → StateChange}/PsDispatchShipment.res +0 -0
  61. /package/tests/plugin/{StateChangeSlice → StateChange}/PsGatedCommands.res +0 -0
  62. /package/tests/plugin/{StateChangeSlice → StateChange}/PsPlaceOrder.res +0 -0
  63. /package/tests/plugin/{StateChangeSlice → StateChange}/PsReserveStock.res +0 -0
  64. /package/tests/plugin/{StateChangeSlice → StateChange}/PsShipOrder.res +0 -0
  65. /package/tests/plugin/{StateChangeSlice → StateChange}/PsTransitionSwitch.res +0 -0
  66. /package/tests/plugin/{StateChangeSlice → StateChange}/PsTypoStore.res +0 -0
  67. /package/tests/plugin/{StateChangeSlice → StateChange}/PsUploadAvatar.res +0 -0
  68. /package/tests/plugin/{StateChangeSlice → StateChange}/PsUploadImages.res +0 -0
  69. /package/tests/plugin/{StateViewSlice → StateView}/PsAnnotatedView.res +0 -0
  70. /package/tests/plugin/{StateViewSlice → StateView}/PsAvailableProductsView.res +0 -0
  71. /package/tests/plugin/{StateViewSlice → StateView}/PsCategoriesView.res +0 -0
  72. /package/tests/plugin/{StateViewSlice → StateView}/PsCustomersView.res +0 -0
  73. /package/tests/plugin/{StateViewSlice → StateView}/PsGatedView.res +0 -0
  74. /package/tests/plugin/{StateViewSlice → StateView}/PsOrdersView.res +0 -0
  75. /package/tests/plugin/{StateViewSlice → StateView}/PsShipmentsView.res +0 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.259 (2026-09-08)
7
+
8
+ ### Features
9
+
10
+ * a slice folder is named for its kind, without the Slice ([a400555](https://github.com/ReventlessDev/reventless-core/commit/a400555121b023823983ebe846e99cda27063c1f))
11
+ * **core:** a reference names a row and shows its picture ([2b40d04](https://github.com/ReventlessDev/reventless-core/commit/2b40d0418bba2bf83463f446e94e372ea0c9898c))
12
+ * **core:** a state view records when it reached each state ([5216f71](https://github.com/ReventlessDev/reventless-core/commit/5216f71b8dcafcb2f0c991923ebcf2438b27e347))
13
+ * **ppx:** a marker composes onto a branded type instead of replacing it ([250c455](https://github.com/ReventlessDev/reventless-core/commit/250c4559aec142558adadcb7f539fea607da5032))
14
+ * **spec:** an instant is a type, and a calendar day is a different one ([a60bebc](https://github.com/ReventlessDev/reventless-core/commit/a60bebc20fe5f3413f9252be36f94ba2a9fb620d))
15
+
16
+
17
+ # 3.0.0-alpha.258 (2026-09-07)
18
+
19
+ ### Bug Fixes
20
+
21
+ * **core:** a state view slice composes its [@display](https://github.com/display)Name like a read model ([851aa1d](https://github.com/ReventlessDev/reventless-core/commit/851aa1d12ddae9dd39f91084f5719cd98fe1cbd4))
22
+
23
+
6
24
  # 3.0.0-alpha.257 (2026-09-07)
7
25
 
8
26
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.257",
3
+ "version": "3.0.0-alpha.259",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,16 +29,16 @@
29
29
  "sury": "11.0.0-rc.2",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
32
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.9",
33
32
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
34
33
  "@reventlessdev/rescript-node": "2.0.0-alpha.9",
34
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.9",
35
35
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.9",
38
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.160",
38
40
  "@reventlessdev/reventless-interop": "3.0.0-alpha.35",
39
- "@reventlessdev/reventless-spec": "3.0.0-alpha.130",
40
- "@reventlessdev/reventless-infra": "3.0.0-alpha.158",
41
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0"
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.132"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
@@ -21,54 +21,172 @@ module Mapping = {
21
21
  // hot path at the default Info level.
22
22
  let logAction = makeStr => log.debugLazy(~comp="Projection", makeStr)
23
23
 
24
- // ── @displayName overlay ────────────────────────────────────────────────────
25
- // When a read model's state schema carries DisplayName metadata, every state
26
- // written by a projection action gets its [displayName] field overwritten with
27
- // the composite label derived from the annotated source fields. The overlay
28
- // encodes via sury, mutates the JSON dict, and parses back — unlocks fine
29
- // work on partial/option fields without duplicating the record shape here.
30
-
31
- let overlayDisplayName = (
32
- state: 'state,
33
- stateSchema: S.t<'state>,
34
- spec: Reventless.DisplayName.displayNameSpec,
35
- ): 'state => {
36
- let json = state->Reventless.Util_Sury.toJson(stateSchema)
37
- switch json->JSON.Decode.object {
38
- | None => state
39
- | Some(stateDict) =>
40
- let label = Reventless.DisplayName.computeLabel(spec, stateDict)
41
- stateDict->Dict.set("displayName", JSON.Encode.string(label))
42
- stateDict->JSON.Encode.object->Reventless.Util_Sury.fromJson(stateSchema)
24
+ // ── Schema-driven state overlays ────────────────────────────────────────────
25
+ // Two things every state a projection writes carries that the domain does not
26
+ // write: the composite `@displayName` label, and a lifecycle trail entry for an
27
+ // action that moved the row's lifecycle field. Both are read off the state
28
+ // schema and applied to one JSON round trip per written state.
29
+
30
+ type overlays = {
31
+ displayName: option<Reventless.DisplayName.displayNameSpec>,
32
+ // (trail field, lifecycle field). A trail with no lifecycle field to follow
33
+ // records nothing — there is no value to append.
34
+ trail: option<(string, string)>,
35
+ }
36
+
37
+ let overlaysFor = (stateSchema: S.t<unknown>): option<overlays> => {
38
+ let displayName = Reventless.DisplayName.getSpec(stateSchema)
39
+ let trail = switch (
40
+ Reventless.Lifecycle.Trail.fieldName(stateSchema),
41
+ Reventless.Lifecycle.fieldName(stateSchema),
42
+ ) {
43
+ | (Some(trailField), Some(lifecycleField)) => Some((trailField, lifecycleField))
44
+ | _ => None
45
+ }
46
+ displayName->Option.isNone && trail->Option.isNone ? None : Some({displayName, trail})
47
+ }
48
+
49
+ // `priorLifecycle` is what the row's lifecycle field held before the action.
50
+ // `None` where the row is created — that is a change from nothing, and it opens
51
+ // the trail. `at` is the envelope's own time, never a clock: a projection that
52
+ // read one would produce a different trail on every replay.
53
+ let applyOverlays = (
54
+ stateDict: dict<JSON.t>,
55
+ {displayName, trail}: overlays,
56
+ ~priorLifecycle: option<JSON.t>,
57
+ ~at: string,
58
+ ): unit => {
59
+ displayName->Option.forEach(spec =>
60
+ stateDict->Dict.set(
61
+ "displayName",
62
+ JSON.Encode.string(Reventless.DisplayName.computeLabel(spec, stateDict)),
63
+ )
64
+ )
65
+ trail->Option.forEach(((trailField, lifecycleField)) =>
66
+ switch stateDict->Dict.get(lifecycleField) {
67
+ | Some(value) if Some(value) != priorLifecycle =>
68
+ stateDict->Reventless.Lifecycle.Trail.record(~field=trailField, ~state=value, ~at)
69
+ | _ => ()
70
+ }
71
+ )
72
+ }
73
+
74
+ // The action shapes, once, over a lens onto whatever a state is on this path:
75
+ // a typed record encoded through sury, or JSON that already is one.
76
+ //
77
+ // Multi-state actions get the label but no trail entry: pairing a before-row
78
+ // with an after-row needs the sub-id, which is not resolved until the action is
79
+ // applied, and pairing by position would credit one row's transition to another.
80
+ let rewriteActionWith = (
81
+ action: action<'id, 'state>,
82
+ ~overlays: overlays,
83
+ ~toDict: 'state => option<dict<JSON.t>>,
84
+ ~fromDict: dict<JSON.t> => 'state,
85
+ ~at: string,
86
+ ): action<'id, 'state> => {
87
+ // Read before the update function runs, so a projection that returns its
88
+ // argument mutated cannot make the prior state look like the new one.
89
+ let priorLifecycleOf = state =>
90
+ switch overlays.trail {
91
+ | None => None
92
+ | Some((_, lifecycleField)) => toDict(state)->Option.flatMap(Dict.get(_, lifecycleField))
93
+ }
94
+ let apply = (state, ~overlays, ~priorLifecycle) =>
95
+ switch toDict(state) {
96
+ | None => state
97
+ | Some(stateDict) =>
98
+ applyOverlays(stateDict, overlays, ~priorLifecycle, ~at)
99
+ fromDict(stateDict)
100
+ }
101
+ // `Set` opens a trail rather than extending one: it replaces the whole row,
102
+ // the trail included, so there is no prior value left to compare against.
103
+ let created = state => apply(state, ~overlays, ~priorLifecycle=None)
104
+ let updated = (fn, state) =>
105
+ apply(fn(state), ~overlays, ~priorLifecycle=priorLifecycleOf(state))
106
+ let labelled = state => apply(state, ~overlays={...overlays, trail: None}, ~priorLifecycle=None)
107
+ switch action {
108
+ | Create(id, state) => Create(id, created(state))
109
+ | CreateMany(pairs) => CreateMany(pairs->Array.map(((id, s)) => (id, created(s))))
110
+ | Set(id, state) => Set(id, created(state))
111
+ | SetMany(ids, fn) => SetMany(ids, id => created(fn(id)))
112
+ | Update(id, fn) => Update(id, state => updated(fn, state))
113
+ | UpdateMany(ids, fn) => UpdateMany(ids, (id, s) => updated(state => fn(id, state), s))
114
+ | UpdateWithDefault(id, default, fn) =>
115
+ UpdateWithDefault(id, created(default), state => updated(fn, state))
116
+ | UpdateManyWithDefault(ids, defFn, fn) =>
117
+ UpdateManyWithDefault(
118
+ ids,
119
+ id => created(defFn(id)),
120
+ (id, s) => updated(state => fn(id, state), s),
121
+ )
122
+ | CreateMultiState(id, states) => CreateMultiState(id, states->Array.map(labelled))
123
+ | UpdateMultiState(id, fn) => UpdateMultiState(id, states => states->fn->Array.map(labelled))
124
+ | UpdateManyMultiStates(ids, fn) =>
125
+ UpdateManyMultiStates(ids, (id, states) => fn(id, states)->Array.map(labelled))
126
+ | Delete(_) | DeleteMany(_) | DeleteIf(_, _) | DeleteManyIf(_, _) | Ignore => action
43
127
  }
44
128
  }
45
129
 
46
130
  let rewriteAction = (
47
131
  action: action<'id, 'state>,
132
+ ~at: string,
48
133
  stateSchema: S.t<'state>,
49
134
  ): action<'id, 'state> =>
50
- switch Reventless.DisplayName.getSpec(stateSchema->S.castToUnknown) {
135
+ switch overlaysFor(stateSchema->S.castToUnknown) {
51
136
  | None => action
52
- | Some(spec) =>
53
- let overlay = state => overlayDisplayName(state, stateSchema, spec)
54
- switch action {
55
- | Create(id, state) => Create(id, overlay(state))
56
- | CreateMany(pairs) => CreateMany(pairs->Array.map(((id, s)) => (id, overlay(s))))
57
- | Set(id, state) => Set(id, overlay(state))
58
- | SetMany(ids, fn) => SetMany(ids, id => overlay(fn(id)))
59
- | Update(id, fn) => Update(id, state => overlay(fn(state)))
60
- | UpdateMany(ids, fn) => UpdateMany(ids, (id, s) => overlay(fn(id, s)))
61
- | UpdateWithDefault(id, default, fn) =>
62
- UpdateWithDefault(id, overlay(default), state => overlay(fn(state)))
63
- | UpdateManyWithDefault(ids, defFn, fn) =>
64
- UpdateManyWithDefault(ids, id => overlay(defFn(id)), (id, s) => overlay(fn(id, s)))
65
- | CreateMultiState(id, states) => CreateMultiState(id, states->Array.map(overlay))
66
- | UpdateMultiState(id, fn) =>
67
- UpdateMultiState(id, states => states->fn->Array.map(overlay))
68
- | UpdateManyMultiStates(ids, fn) =>
69
- UpdateManyMultiStates(ids, (id, states) => fn(id, states)->Array.map(overlay))
70
- | Delete(_) | DeleteMany(_) | DeleteIf(_, _) | DeleteManyIf(_, _) | Ignore => action
71
- }
137
+ | Some(overlays) =>
138
+ action->rewriteActionWith(
139
+ ~overlays,
140
+ ~at,
141
+ ~toDict=state => state->Reventless.Util_Sury.toJson(stateSchema)->JSON.Decode.object,
142
+ ~fromDict=stateDict =>
143
+ stateDict->JSON.Encode.object->Reventless.Util_Sury.fromJson(stateSchema),
144
+ )
145
+ }
146
+
147
+ // The trail alone, for the two callers that must not compose `displayName`.
148
+ let trailOverlays = (stateSchema: option<S.t<unknown>>): option<overlays> =>
149
+ switch stateSchema->Option.flatMap(overlaysFor) {
150
+ | Some({trail: Some(_)} as overlays) => Some({...overlays, displayName: None})
151
+ | _ => None
152
+ }
153
+
154
+ /**
155
+ The rewrite a test harness applies: the trail and nothing else. `displayName`
156
+ lives in the state schema and not in the record, so composing it would produce a
157
+ state no expectation can spell.
158
+ */
159
+ let rewriteTrail = (
160
+ action: action<'id, 'state>,
161
+ ~at: string,
162
+ stateSchema: S.t<'state>,
163
+ ): action<'id, 'state> =>
164
+ switch trailOverlays(Some(stateSchema->S.castToUnknown)) {
165
+ | None => action
166
+ | Some(overlays) =>
167
+ action->rewriteActionWith(
168
+ ~overlays,
169
+ ~at,
170
+ ~toDict=state => state->Reventless.Util_Sury.toJson(stateSchema)->JSON.Decode.object,
171
+ ~fromDict=stateDict =>
172
+ stateDict->JSON.Encode.object->Reventless.Util_Sury.fromJson(stateSchema),
173
+ )
174
+ }
175
+
176
+ /**
177
+ The same, for the JSON-level pipeline a deployed state-view slice assembles,
178
+ whose states are already JSON. `@displayName` is composed at save there
179
+ (`ProjectionEntryPoint_Ops.withDisplayName`).
180
+ */
181
+ let rewriteJsonAction = (
182
+ action: action<'id, JSON.t>,
183
+ ~at: string,
184
+ ~stateSchema: option<S.t<unknown>>,
185
+ ): action<'id, JSON.t> =>
186
+ switch trailOverlays(stateSchema) {
187
+ | None => action
188
+ | Some(overlays) =>
189
+ action->rewriteActionWith(~overlays, ~at, ~toDict=JSON.Decode.object, ~fromDict=JSON.Encode.object)
72
190
  }
73
191
 
74
192
  let applyChanges = async (
@@ -11,6 +11,7 @@ import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
11
11
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
12
12
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
13
13
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
14
+ import * as Lifecycle$Reventless from "@reventlessdev/reventless-spec/src/types/Lifecycle.res.mjs";
14
15
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
15
16
  import * as Logger$ReventlessCore from "./util/Logger.res.mjs";
16
17
  import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/components/DisplayName.res.mjs";
@@ -37,23 +38,59 @@ function logAction(makeStr) {
37
38
  log.debugLazy("Projection", makeStr);
38
39
  }
39
40
 
40
- function overlayDisplayName(state, stateSchema, spec) {
41
- let json = Util_Sury$Reventless.toJson(state, stateSchema);
42
- let stateDict = Stdlib_JSON.Decode.object(json);
43
- if (stateDict === undefined) {
44
- return state;
41
+ function overlaysFor(stateSchema) {
42
+ let displayName = DisplayName$Reventless.getSpec(stateSchema);
43
+ let match = Lifecycle$Reventless.Trail.fieldName(stateSchema);
44
+ let match$1 = Lifecycle$Reventless.fieldName(stateSchema);
45
+ let trail = match !== undefined && match$1 !== undefined ? [
46
+ match,
47
+ match$1
48
+ ] : undefined;
49
+ if (Stdlib_Option.isNone(displayName) && Stdlib_Option.isNone(trail)) {
50
+ return;
51
+ } else {
52
+ return {
53
+ displayName: displayName,
54
+ trail: trail
55
+ };
45
56
  }
46
- let label = DisplayName$Reventless.computeLabel(spec, stateDict);
47
- stateDict["displayName"] = label;
48
- return Util_Sury$Reventless.fromJson(stateDict, stateSchema);
49
57
  }
50
58
 
51
- function rewriteAction(action, stateSchema) {
52
- let spec = DisplayName$Reventless.getSpec(stateSchema);
53
- if (spec === undefined) {
54
- return action;
55
- }
56
- let overlay = state => overlayDisplayName(state, stateSchema, spec);
59
+ function applyOverlays(stateDict, param, priorLifecycle, at) {
60
+ Stdlib_Option.forEach(param.displayName, spec => {
61
+ stateDict["displayName"] = DisplayName$Reventless.computeLabel(spec, stateDict);
62
+ });
63
+ Stdlib_Option.forEach(param.trail, param => {
64
+ let value = stateDict[param[1]];
65
+ if (value !== undefined && Primitive_object.notequal(value, priorLifecycle)) {
66
+ return Lifecycle$Reventless.Trail.record(stateDict, param[0], value, at);
67
+ }
68
+ });
69
+ }
70
+
71
+ function rewriteActionWith(action, overlays, toDict, fromDict, at) {
72
+ let priorLifecycleOf = state => {
73
+ let match = overlays.trail;
74
+ if (match === undefined) {
75
+ return;
76
+ }
77
+ let lifecycleField = match[1];
78
+ return Stdlib_Option.flatMap(toDict(state), __x => __x[lifecycleField]);
79
+ };
80
+ let apply = (state, overlays, priorLifecycle) => {
81
+ let stateDict = toDict(state);
82
+ if (stateDict !== undefined) {
83
+ applyOverlays(stateDict, overlays, priorLifecycle, at);
84
+ return fromDict(stateDict);
85
+ } else {
86
+ return state;
87
+ }
88
+ };
89
+ let updated = (fn, state) => apply(fn(state), overlays, priorLifecycleOf(state));
90
+ let labelled = state => apply(state, {
91
+ displayName: overlays.displayName,
92
+ trail: undefined
93
+ }, undefined);
57
94
  if (typeof action !== "object") {
58
95
  return action;
59
96
  }
@@ -62,14 +99,14 @@ function rewriteAction(action, stateSchema) {
62
99
  return {
63
100
  TAG: "Create",
64
101
  _0: action._0,
65
- _1: overlayDisplayName(action._1, stateSchema, spec)
102
+ _1: apply(action._1, overlays, undefined)
66
103
  };
67
104
  case "CreateMany" :
68
105
  return {
69
106
  TAG: "CreateMany",
70
107
  _0: action._0.map(param => [
71
108
  param[0],
72
- overlayDisplayName(param[1], stateSchema, spec)
109
+ apply(param[1], overlays, undefined)
73
110
  ])
74
111
  };
75
112
  case "Update" :
@@ -77,22 +114,22 @@ function rewriteAction(action, stateSchema) {
77
114
  return {
78
115
  TAG: "Update",
79
116
  _0: action._0,
80
- _1: state => overlayDisplayName(fn(state), stateSchema, spec)
117
+ _1: state => updated(fn, state)
81
118
  };
82
119
  case "UpdateMany" :
83
120
  let fn$1 = action._1;
84
121
  return {
85
122
  TAG: "UpdateMany",
86
123
  _0: action._0,
87
- _1: (id, s) => overlayDisplayName(fn$1(id, s), stateSchema, spec)
124
+ _1: (id, s) => updated(state => fn$1(id, state), s)
88
125
  };
89
126
  case "UpdateWithDefault" :
90
127
  let fn$2 = action._2;
91
128
  return {
92
129
  TAG: "UpdateWithDefault",
93
130
  _0: action._0,
94
- _1: overlayDisplayName(action._1, stateSchema, spec),
95
- _2: state => overlayDisplayName(fn$2(state), stateSchema, spec)
131
+ _1: apply(action._1, overlays, undefined),
132
+ _2: state => updated(fn$2, state)
96
133
  };
97
134
  case "UpdateManyWithDefault" :
98
135
  let fn$3 = action._2;
@@ -100,47 +137,84 @@ function rewriteAction(action, stateSchema) {
100
137
  return {
101
138
  TAG: "UpdateManyWithDefault",
102
139
  _0: action._0,
103
- _1: id => overlayDisplayName(defFn(id), stateSchema, spec),
104
- _2: (id, s) => overlayDisplayName(fn$3(id, s), stateSchema, spec)
140
+ _1: id => apply(defFn(id), overlays, undefined),
141
+ _2: (id, s) => updated(state => fn$3(id, state), s)
105
142
  };
106
143
  case "Set" :
107
144
  return {
108
145
  TAG: "Set",
109
146
  _0: action._0,
110
- _1: overlayDisplayName(action._1, stateSchema, spec)
147
+ _1: apply(action._1, overlays, undefined)
111
148
  };
112
149
  case "SetMany" :
113
150
  let fn$4 = action._1;
114
151
  return {
115
152
  TAG: "SetMany",
116
153
  _0: action._0,
117
- _1: id => overlayDisplayName(fn$4(id), stateSchema, spec)
154
+ _1: id => apply(fn$4(id), overlays, undefined)
118
155
  };
119
156
  case "CreateMultiState" :
120
157
  return {
121
158
  TAG: "CreateMultiState",
122
159
  _0: action._0,
123
- _1: action._1.map(overlay)
160
+ _1: action._1.map(labelled)
124
161
  };
125
162
  case "UpdateMultiState" :
126
163
  let fn$5 = action._1;
127
164
  return {
128
165
  TAG: "UpdateMultiState",
129
166
  _0: action._0,
130
- _1: states => fn$5(states).map(overlay)
167
+ _1: states => fn$5(states).map(labelled)
131
168
  };
132
169
  case "UpdateManyMultiStates" :
133
170
  let fn$6 = action._1;
134
171
  return {
135
172
  TAG: "UpdateManyMultiStates",
136
173
  _0: action._0,
137
- _1: (id, states) => fn$6(id, states).map(overlay)
174
+ _1: (id, states) => fn$6(id, states).map(labelled)
138
175
  };
139
176
  default:
140
177
  return action;
141
178
  }
142
179
  }
143
180
 
181
+ function rewriteAction(action, at, stateSchema) {
182
+ let overlays = overlaysFor(stateSchema);
183
+ if (overlays !== undefined) {
184
+ return rewriteActionWith(action, overlays, state => Stdlib_JSON.Decode.object(Util_Sury$Reventless.toJson(state, stateSchema)), stateDict => Util_Sury$Reventless.fromJson(stateDict, stateSchema), at);
185
+ } else {
186
+ return action;
187
+ }
188
+ }
189
+
190
+ function trailOverlays(stateSchema) {
191
+ let overlays = Stdlib_Option.flatMap(stateSchema, overlaysFor);
192
+ if (overlays !== undefined && overlays.trail !== undefined) {
193
+ return {
194
+ displayName: undefined,
195
+ trail: overlays.trail
196
+ };
197
+ }
198
+ }
199
+
200
+ function rewriteTrail(action, at, stateSchema) {
201
+ let overlays = trailOverlays(stateSchema);
202
+ if (overlays !== undefined) {
203
+ return rewriteActionWith(action, overlays, state => Stdlib_JSON.Decode.object(Util_Sury$Reventless.toJson(state, stateSchema)), stateDict => Util_Sury$Reventless.fromJson(stateDict, stateSchema), at);
204
+ } else {
205
+ return action;
206
+ }
207
+ }
208
+
209
+ function rewriteJsonAction(action, at, stateSchema) {
210
+ let overlays = trailOverlays(stateSchema);
211
+ if (overlays !== undefined) {
212
+ return rewriteActionWith(action, overlays, Stdlib_JSON.Decode.object, prim => prim, at);
213
+ } else {
214
+ return action;
215
+ }
216
+ }
217
+
144
218
  async function applyChanges(action, id, beforeStates, afterStates, param, param$1) {
145
219
  let getSubId = param$1.getSubId;
146
220
  let subIdField = param$1.subIdField;
@@ -714,8 +788,13 @@ export {
714
788
  log,
715
789
  Mapping,
716
790
  logAction,
717
- overlayDisplayName,
791
+ overlaysFor,
792
+ applyOverlays,
793
+ rewriteActionWith,
718
794
  rewriteAction,
795
+ trailOverlays,
796
+ rewriteTrail,
797
+ rewriteJsonAction,
719
798
  applyChanges,
720
799
  stateToString,
721
800
  statesToString,
@@ -58,7 +58,7 @@ let Id;
58
58
 
59
59
  let consumedEventSchema = eventSchema;
60
60
 
61
- let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs";
61
+ let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res.mjs";
62
62
 
63
63
  let readConsistency = "EscalateOnRetry";
64
64
 
@@ -68,7 +68,7 @@ let Spec;
68
68
 
69
69
  let initialState;
70
70
 
71
- let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res.mjs";
71
+ let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry_Behavior.res.mjs";
72
72
 
73
73
  export {
74
74
  Spec,
@@ -42,7 +42,7 @@ let Id;
42
42
 
43
43
  let subIdConfig;
44
44
 
45
- let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs";
45
+ let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
46
46
 
47
47
  let authorization = "AllowAuthenticated";
48
48
 
@@ -45,7 +45,7 @@ function project(param) {
45
45
 
46
46
  let Spec;
47
47
 
48
- let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs";
48
+ let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments_Projection.res.mjs";
49
49
 
50
50
  export {
51
51
  Spec,
@@ -47,6 +47,7 @@ let rec fromSchemaType = (
47
47
  | ScalarBigInt => `String${bang}`
48
48
  | EntityId => `ID${bang}`
49
49
  | DateTime => `String${bang}`
50
+ | CalendarDate => `String${bang}`
50
51
  | Nullable(inner) => fromSchemaType(~required=false, ~asInput, inner, collectedTypes, seenTypes)
51
52
  | ArrayOf(item) =>
52
53
  let itemType = fromSchemaType(~required=true, ~asInput, item, collectedTypes, seenTypes)
@@ -594,8 +595,18 @@ let deriveByIdsQueryField = (
594
595
  // is not whether the door exists but whether a *retired* row comes through it —
595
596
  // without it this is a cheap label read that narrows exactly as every other door
596
597
  // does.
598
+ // `image` travels beside `label` for the reason the label does: a card or a tile
599
+ // drawing a reference wants the row's picture as well as its name, and the
600
+ // alternative is a second query per grid asking the referenced view for one
601
+ // field. It stays inside the narrowness above — a ref path is what a reference
602
+ // already publishes, and a caller still cannot ask for anything else — and it is
603
+ // `null` for the views (most of them) that declare no picture.
604
+ //
605
+ // Emitted unconditionally for the same reason the door itself is: a field that
606
+ // appears once a view adopts an image would make adopting one a breaking schema
607
+ // change for every client that had learned this type's shape.
597
608
  let deriveRefTypeSdl = (~returnTypeName: string): string =>
598
- `type ${returnTypeName}Ref {\n id: ID!\n label: String!\n retired: Boolean!\n retiredState: String\n}`
609
+ `type ${returnTypeName}Ref {\n id: ID!\n label: String!\n image: String\n retired: Boolean!\n retiredState: String\n}`
599
610
 
600
611
  // `retiredState` is null in two cases that do not need telling apart by a
601
612
  // consumer: a live row, and a boolean-form retirement, where the field is the
@@ -52,6 +52,7 @@ function fromSchemaType(_required, _asInputOpt, _st, collectedTypes, seenTypes)
52
52
  case "ScalarString" :
53
53
  case "ScalarBigInt" :
54
54
  case "DateTime" :
55
+ case "CalendarDate" :
55
56
  case "Unknown" :
56
57
  return `String` + bang;
57
58
  }
@@ -455,7 +456,7 @@ function deriveByIdsQueryField(listFieldName, returnTypeName) {
455
456
  }
456
457
 
457
458
  function deriveRefTypeSdl(returnTypeName) {
458
- return `type ` + returnTypeName + `Ref {\n id: ID!\n label: String!\n retired: Boolean!\n retiredState: String\n}`;
459
+ return `type ` + returnTypeName + `Ref {\n id: ID!\n label: String!\n image: String\n retired: Boolean!\n retiredState: String\n}`;
459
460
  }
460
461
 
461
462
  function deriveRefsQueryField(listFieldName, returnTypeName) {