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

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 (79) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/package.json +9 -9
  3. package/src/adapter/Monitoring/Monitoring.res +79 -5
  4. package/src/adapter/Monitoring/Monitoring.res.mjs +42 -2
  5. package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
  6. package/src/admin/Platform_ComponentDefinitionsApi.res +5 -1
  7. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
  8. package/src/admin/Platform_UiSlots.res +21 -0
  9. package/src/admin/Platform_UiSlots.res.mjs +15 -0
  10. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +1 -1
  11. package/src/components/Aggregate/Aggregate_Callback.res +13 -4
  12. package/src/components/Aggregate/Aggregate_Callback.res.mjs +6 -5
  13. package/src/components/Api/GraphQL_FragmentGenerator.res +2 -0
  14. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +4 -0
  15. package/src/components/Api/MCP_SchemaGenerator.res.mjs +2 -2
  16. package/src/components/Api/SchemaType.res +8 -0
  17. package/src/components/Api/SchemaType.res.mjs +15 -5
  18. package/src/components/Api/SuryToJsonSchema.res +70 -12
  19. package/src/components/Api/SuryToJsonSchema.res.mjs +43 -16
  20. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +8 -3
  21. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +1 -1
  22. package/src/plugin/component/Plugin.res +1 -0
  23. package/src/plugin/component/Plugin_Builder.res +2 -0
  24. package/src/plugin/component/Plugin_Builder.res.mjs +3 -2
  25. package/src/plugin/component/Plugin_Structure.res +105 -10
  26. package/src/plugin/component/Plugin_Structure.res.mjs +71 -27
  27. package/src/plugin/lifecycle/PluginsReadModelSpec.res +1 -1
  28. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +1 -1
  29. package/src/util/Validation.res +1 -1
  30. package/src/util/Validation.res.mjs +2 -2
  31. package/tests/adapter/MonitoringTest.res +46 -7
  32. package/tests/adapter/MonitoringTest.res.mjs +47 -5
  33. package/tests/admin/AdminApiSchemaDriftTest.res +1 -0
  34. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +3 -0
  35. package/tests/admin/Platform_Admin_StructureTest.res.mjs +2 -14
  36. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +1 -0
  37. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +2 -0
  38. package/tests/admin/Platform_PluginStructuresApiTest.res +8 -14
  39. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +3 -3
  40. package/tests/aggregate/AggregateCacheTest.res.mjs +1 -1
  41. package/tests/aggregate/AggregateConflictTest.res.mjs +1 -1
  42. package/tests/aggregate/AggregateFixtures.res.mjs +1 -1
  43. package/tests/aggregate/AggregateSnapshotTest.res.mjs +1 -1
  44. package/tests/api/GraphQL_FragmentGeneratorTest.res +6 -4
  45. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +5 -5
  46. package/tests/api/SuryToJsonSchemaTest.res.mjs +57 -57
  47. package/tests/api/TaggedUnionSchemaTest.res.mjs +1 -1
  48. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +1 -1
  49. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +1 -1
  50. package/tests/dcb/DcbFixtures.res.mjs +1 -1
  51. package/tests/eventmapper/EventMapperFixtures.res.mjs +1 -1
  52. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +1 -1
  53. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +1 -1
  54. package/tests/fixtures/plugin-lifecycle/2026-07-22-versionconnected-no-requiredstores.json +1 -1
  55. package/tests/fixtures/plugin-lifecycle/2026-07-22-versionsuperseded-nested-definitions.json +1 -1
  56. package/tests/fixtures/plugin-lifecycle/2026-07-28-versionconnected-requiredstores-only.json +1 -1
  57. package/tests/fixtures/plugin-lifecycle/2026-07-30-versionconnected-declarations-without-annotation.json +1 -1
  58. package/tests/fixtures/plugin-lifecycle/2026-07-30-versiondisconnected-declarations-without-annotation.json +1 -1
  59. package/tests/fixtures/plugin-lifecycle/README.md +12 -0
  60. package/tests/logger/LogFormatTest.res +1 -1
  61. package/tests/logger/LogFormatTest.res.mjs +1 -1
  62. package/tests/plugin/ManifestVisibilityTest.res.mjs +1 -1
  63. package/tests/plugin/PluginDefinitionRequiredScalarsTest.res +1 -1
  64. package/tests/plugin/PluginDefinitionScalars.res +13 -8
  65. package/tests/plugin/PluginDefinitionScalars.res.mjs +13 -5
  66. package/tests/plugin/PluginStructureAccessTest.res.mjs +1 -1
  67. package/tests/plugin/PluginStructureTest.res +184 -0
  68. package/tests/plugin/PluginStructureTest.res.mjs +142 -17
  69. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +1 -1
  70. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +1 -1
  71. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +1 -1
  72. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +1 -1
  73. package/tests/plugin/StateChangeSlice/PsReserveStock.res +14 -1
  74. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +9 -2
  75. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +1 -1
  76. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +1 -1
  77. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +1 -1
  78. package/tests/plugin/pluginDefinitionRequiredScalars.txt +13 -14
  79. package/tests/util/CommandPublisherTest.res.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,64 @@
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.257 (2026-09-07)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **core:** hold a monitoring announcement until someone is listening ([9cc4526](https://github.com/ReventlessDev/reventless-core/commit/9cc4526d3a21ce130e219b6da88fd4f3201974bd))
11
+ * **logging:** log a message's identity, not its serialisation ([cd7467a](https://github.com/ReventlessDev/reventless-core/commit/cd7467abdc59609279f16f358cdd8884cc49f063))
12
+ * **ui:** a declared slot module is named, not only written ([de000ba](https://github.com/ReventlessDev/reventless-core/commit/de000baf428249f64cfc719ed52df3760198b82a))
13
+ * feat(dcb)!: a record inside an array is still a reference and a key ([bcb9b13](https://github.com/ReventlessDev/reventless-core/commit/bcb9b13958323d8319d7fcf25c055800e84150f3))
14
+ ### Features
15
+
16
+ * **core:** a command's lifecycle edge comes from its scenarios, not its claim ([1879d05](https://github.com/ReventlessDev/reventless-core/commit/1879d0570a981e7fae5bcd3808f8a36e898bd0fd))
17
+
18
+ ### BREAKING CHANGES
19
+
20
+ * an `int` field is now `Int` in the generated SDL and
21
+ `"integer"` in JSON Schema, where it was `Float`/`"number"`. A client
22
+ declaring `$retryCount: Float!` against one of these fields must move to
23
+ `Int!`.
24
+
25
+
26
+
27
+ # 3.0.0-alpha.256 (2026-09-04)
28
+
29
+ * feat(spec)!: one optional encoding on the wire, with no annotation ([320f91d](https://github.com/ReventlessDev/reventless-core/commit/320f91daa8bd90812a6e82069e7a1cb473041930))
30
+
31
+ ### BREAKING CHANGES
32
+
33
+ * the two encodings cannot read each other. Stored
34
+ pluginDefinition / pluginStructure payloads and already-deployed plugins must
35
+ go — wipe the platform scope (SEED_RESET_SCOPE=platform), quiesce, and
36
+ redeploy the fleet from one commit. Domain plugin data is untouched.
37
+
38
+ Two guards had to learn the new shape, both of which defined "optional" as
39
+ has.null and so mistook an omitted key for something to invent:
40
+
41
+ - Message.fillMissingDefaults reached `return undefined` only below two arms
42
+ that fire first — an enum's first const, and an object member filled with
43
+ zeros. Absent option<record> therefore healed to a zero-filled record, not
44
+ None: on the real schema, dcbEventLog became Some({name: "", eventTopicArn:
45
+ ""}), which manageSubscriptions would have read as a peer to subscribe to.
46
+ One line, mirroring the has.null guard, above both arms.
47
+ - PluginDefinitionScalars' walker reported 16 optional fields as newly-added
48
+ bare required scalars. With both encodings understood, the golden list is
49
+ unchanged.
50
+
51
+ The frozen lifecycle corpus holds five real payloads in the old encoding.
52
+ Null-valued keys were stripped mechanically — the rewrite round-trips each
53
+ file unchanged before editing, and a key-by-key diff shows null removals and
54
+ nothing else. The README records it beside the account-id redaction and says
55
+ why it is not a regeneration: no fixture was rebuilt from ReScript types, so
56
+ every generation in its table is still pinned.
57
+
58
+ check:graphql is unchanged, as expected: SchemaType.fromSury collapses Null
59
+ and Undefined to the same Nullable, so the emitted schema never distinguished
60
+ them.
61
+
62
+
63
+
6
64
  # 3.0.0-alpha.255 (2026-09-04)
7
65
 
8
66
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.255",
3
+ "version": "3.0.0-alpha.257",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,21 +29,21 @@
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",
32
33
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
33
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.8",
34
+ "@reventlessdev/rescript-node": "2.0.0-alpha.9",
34
35
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
35
- "@reventlessdev/rescript-node": "2.0.0-alpha.8",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
37
- "@reventlessdev/reventless-infra": "3.0.0-alpha.156",
38
- "@reventlessdev/rescript-ssh2": "2.0.0-alpha.8",
39
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.128",
41
- "@reventlessdev/reventless-interop": "3.0.0-alpha.34"
37
+ "@reventlessdev/rescript-ssh2": "2.0.0-alpha.9",
38
+ "@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"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
45
45
  "sury-ppx": "11.0.0-rc.2",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.77"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.78"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "12.3.0"
@@ -100,17 +100,83 @@ module Noop: Backend = {
100
100
 
101
101
  let backend: ref<module(Backend)> = ref(module(Noop: Backend))
102
102
 
103
+ /** Whether `use` has run. Distinguishes "nobody is listening yet" — which is
104
+ what the buffer below exists for — from "a backend that deliberately does
105
+ nothing with this unit", which is the backend's decision to make, not ours. */
106
+ let registered = ref(false)
107
+
108
+ /**
109
+ An announcement made before any backend was registered, held until one is.
110
+
111
+ "Before the build" is not early enough for every provisioning site. A module that
112
+ creates its resources at import time announces them while the deploy program's own
113
+ body — including its `use` call — has not started running, because an imported
114
+ module is evaluated to completion first. The dead-letter sink is such a site, and
115
+ it was the one execution unit in a monitored estate that no alarm ever covered:
116
+ not because a backend declined to handle its kind, but because it was told at a
117
+ moment when the answer to "who is listening" was permanently no one.
118
+
119
+ Buffering makes the seam insensitive to that ordering. What a backend sees is the
120
+ same set of units in the same order; only the moment it hears about the early ones
121
+ moves, to the first instant there is anybody to hear it.
122
+ */
123
+ type announcement = {
124
+ kind: unitKind,
125
+ name: string,
126
+ component: ReventlessInfra.Adapter.resource,
127
+ plugin: option<string>,
128
+ platform: option<string>,
129
+ logLocator: option<Pulumi.Output.t<string>>,
130
+ }
131
+
132
+ let pending: ref<array<announcement>> = ref([])
133
+
134
+ let deliver = (module(B: Backend), a: announcement) =>
135
+ B.onProvisioned(
136
+ ~kind=a.kind,
137
+ ~name=a.name,
138
+ ~component=a.component,
139
+ ~plugin=a.plugin,
140
+ ~platform=a.platform,
141
+ ~logLocator=a.logLocator,
142
+ )
143
+
103
144
  /**
104
145
  Register the monitoring backend. Must run before the platform/plugin build in the
105
146
  deploy program (plain statement order — the registry is consulted lazily at each
106
147
  provisioning site). Called once, by the extension, first.
148
+
149
+ Announcements from sites that ran at import time — before any statement of the
150
+ deploy program — are replayed here, in the order they were made, and the buffer is
151
+ drained so a second registration does not receive them twice.
107
152
  */
108
- let use = (b: module(Backend)) => backend := b
153
+ let use = (b: module(Backend)) => {
154
+ backend := b
155
+ registered := true
156
+ let replay = pending.contents
157
+ pending := []
158
+ replay->Array.forEach(a => deliver(b, a))
159
+ }
160
+
161
+ /** Restores the registry to its initial state: no backend, nothing buffered.
162
+
163
+ A deploy program never needs this — it registers once and runs once. Tests do,
164
+ because the registry and the buffer are module state that outlives a single
165
+ case, and `use(Noop)` is a *registration* rather than an undo: it would leave
166
+ later announcements delivered-and-dropped instead of held. */
167
+ let reset = () => {
168
+ backend := module(Noop: Backend)
169
+ registered := false
170
+ pending := []
171
+ }
109
172
 
110
173
  /**
111
174
  Announce a provisioned execution unit to the registered backend. Called by
112
- runtime backends at their provisioning sites (once per unit). No-op until an
113
- extension registers a backend.
175
+ runtime backends at their provisioning sites (once per unit).
176
+
177
+ Held until an extension registers a backend, rather than dropped — see
178
+ `announcement` above for why that distinction decides whether a unit provisioned
179
+ at import time is ever monitored.
114
180
  */
115
181
  let notify = (
116
182
  ~kind: unitKind,
@@ -118,13 +184,21 @@ let notify = (
118
184
  ~component: ReventlessInfra.Adapter.resource,
119
185
  ~logLocator: option<Pulumi.Output.t<string>>=?,
120
186
  ) => {
121
- module B = unpack(backend.contents)
122
187
  // The owning plugin/platform come from the ambient context the plugin builder publishes
123
188
  // (ResourceAttribution.enter) — populated exactly when this fires, so the call sites need not
124
189
  // thread the names. `None` outside a plugin construct (platform substrate).
190
+ //
191
+ // Read here rather than at delivery, because a buffered announcement is replayed from a deploy
192
+ // program's body, where the ambient context is whatever the *next* construct left behind. The
193
+ // owner belongs to the moment the unit was provisioned.
125
194
  let {ResourceAttribution.plugin: plugin, platform} = ResourceAttribution.current.contents
126
195
  // `~logLocator`, unlike the owner, is NOT ambient: only the provisioning site knows where it
127
196
  // pointed the unit's logs. So it is passed, and optional — a call site with nothing to say omits
128
197
  // it rather than being forced to invent an address.
129
- B.onProvisioned(~kind, ~name, ~component, ~plugin, ~platform, ~logLocator)
198
+ let announcement = {kind, name, component, plugin, platform, logLocator}
199
+ if registered.contents {
200
+ deliver(backend.contents, announcement)
201
+ } else {
202
+ pending := pending.contents->Array.concat([announcement])
203
+ }
130
204
  }
@@ -14,20 +14,60 @@ let backend = {
14
14
  contents: Noop
15
15
  };
16
16
 
17
+ let registered = {
18
+ contents: false
19
+ };
20
+
21
+ let pending = {
22
+ contents: []
23
+ };
24
+
25
+ function deliver(B, a) {
26
+ B.onProvisioned(a.kind, a.name, a.component, a.plugin, a.platform, a.logLocator);
27
+ }
28
+
17
29
  function use(b) {
18
30
  backend.contents = b;
31
+ registered.contents = true;
32
+ let replay = pending.contents;
33
+ pending.contents = [];
34
+ replay.forEach(a => deliver(b, a));
35
+ }
36
+
37
+ function reset() {
38
+ backend.contents = Noop;
39
+ registered.contents = false;
40
+ pending.contents = [];
19
41
  }
20
42
 
21
43
  function notify(kind, name, component, logLocator) {
22
- let B = backend.contents;
23
44
  let match = ResourceAttribution$ReventlessCore.current.contents;
24
- B.onProvisioned(kind, name, component, match.plugin, match.platform, logLocator);
45
+ let announcement_plugin = match.plugin;
46
+ let announcement_platform = match.platform;
47
+ let announcement = {
48
+ kind: kind,
49
+ name: name,
50
+ component: component,
51
+ plugin: announcement_plugin,
52
+ platform: announcement_platform,
53
+ logLocator: logLocator
54
+ };
55
+ if (registered.contents) {
56
+ return deliver(backend.contents, announcement);
57
+ } else {
58
+ pending.contents = pending.contents.concat([announcement]);
59
+ return;
60
+ }
25
61
  }
26
62
 
27
63
  export {
28
64
  Noop,
29
65
  backend,
66
+ registered,
67
+ pending,
68
+ deliver,
30
69
  use,
70
+ reset,
31
71
  notify,
32
72
  }
33
73
  /* No side effect */
@@ -7,7 +7,7 @@ import * as PluginsReadModelSpec$ReventlessCore from "../plugin/lifecycle/Plugin
7
7
 
8
8
  let pluginId = "Platform";
9
9
 
10
- let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandTransition, PluginSpec$ReventlessCore.commandSchema);
10
+ let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandTransition, undefined, PluginSpec$ReventlessCore.commandSchema);
11
11
 
12
12
  let pluginAggregate_producedEventTypes = [];
13
13
 
@@ -18,7 +18,7 @@ open Reventless.Plugin
18
18
  // consumers long after they were being encoded.
19
19
  let sdlTypes: array<string> = [
20
20
  `type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
21
- `type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n requiredAccess: [String!]\n ownerField: String\n}`,
21
+ `type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n allowedStatesSource: String\n apiExposed: Boolean\n requiredAccess: [String!]\n ownerField: String\n}`,
22
22
  `type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
23
23
  // Same fields as Platform_EventDef, kept a distinct type because a refusal is not
24
24
  // a fact: a caller selecting `errors` is asking what a command can be rejected
@@ -84,6 +84,10 @@ let encodeCommandDef = (c: commandDef): JSON.t =>
84
84
  c.allowedStates->Option.mapOr(JSON.Encode.null, encodeStrings),
85
85
  ),
86
86
  ("targetState", c.targetState->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
87
+ (
88
+ "allowedStatesSource",
89
+ c.allowedStatesSource->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
90
+ ),
87
91
  ("apiExposed", c.apiExposed->Option.mapOr(JSON.Encode.null, JSON.Encode.bool)),
88
92
  // Null, not `[]`: a rule asking for nothing checkable and a def written
89
93
  // before the field existed are the same answer to a client — "no key gates
@@ -6,7 +6,7 @@ import * as Plugin$ReventlessCore from "../plugin/component/Plugin.res.mjs";
6
6
 
7
7
  let sdlTypes = [
8
8
  `type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
9
- `type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n requiredAccess: [String!]\n ownerField: String\n}`,
9
+ `type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n allowedStatesSource: String\n apiExposed: Boolean\n requiredAccess: [String!]\n ownerField: String\n}`,
10
10
  `type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
11
11
  `type Platform_ErrorDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
12
12
  `type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n errors: [Platform_ErrorDef!]!\n chapter: String\n}`,
@@ -78,6 +78,10 @@ function encodeCommandDef(c) {
78
78
  "targetState",
79
79
  Stdlib_Option.mapOr(c.targetState, null, prim => prim)
80
80
  ],
81
+ [
82
+ "allowedStatesSource",
83
+ Stdlib_Option.mapOr(c.allowedStatesSource, null, prim => prim)
84
+ ],
81
85
  [
82
86
  "apiExposed",
83
87
  Stdlib_Option.mapOr(c.apiExposed, null, prim => prim)
@@ -0,0 +1,21 @@
1
+ // Where a deployment's slot module goes, and what the shell is told to fetch.
2
+ //
3
+ // Stated once, in core, for the reason `Platform_BakedManifest.defaultKey`
4
+ // gives: four places have to agree on this string — the AWS `BucketObject` key,
5
+ // the `uiSlotsUrl` the AWS deploy writes into `config.json`, the file the local
6
+ // platform serves out of the host-shell `dist/`, and the `uiSlotsUrl` that
7
+ // platform overlays. A platform that spelled it differently from the one that
8
+ // wrote the config key would put the module where nothing imports it, and the
9
+ // symptom is a surface that draws its own regions — indistinguishable from a
10
+ // deployment that declared no slots at all.
11
+ //
12
+ // Both platforms write the module beside `config.json` at what is also the
13
+ // shell's URL root, so the file name and the path the browser asks for are one
14
+ // string.
15
+
16
+ let fileName = "ui-slots.js"
17
+
18
+ /** The `config.json` key naming the module, as `SlotModules.load` reads it. */
19
+ let configKey = "uiSlotsUrl"
20
+
21
+ let url = "/" ++ fileName
@@ -0,0 +1,15 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ let fileName = "ui-slots.js";
5
+
6
+ let url = "/" + fileName;
7
+
8
+ let configKey = "uiSlotsUrl";
9
+
10
+ export {
11
+ fileName,
12
+ configKey,
13
+ url,
14
+ }
15
+ /* No side effect */
@@ -47,7 +47,7 @@ function commandAuthorization(param) {
47
47
  }
48
48
 
49
49
  function commandTransition(param) {
50
- return "Unrestricted";
50
+ return "Undeclared";
51
51
  }
52
52
 
53
53
  let traits = [];
@@ -66,15 +66,25 @@ module Make = (
66
66
  // A domain rejection from Behavior.decide is recorded as `CmdRejected` (no state change,
67
67
  // surviving commands in the batch continue); a successful decide records `CmdOk(events)`
68
68
  // and advances state via `Behavior.evolve`.
69
- let processCommand = (
69
+ //
70
+ // `~seq` is the replayed sequence number the batch decides from, carried in so
71
+ // the line below can name the state rather than serialise it.
72
+ let processCommand = (~seq: int) => (
70
73
  (state, outcomes): (Behavior.state, array<(string, cmdOutcome, Message.meta)>),
71
74
  topicItem: CommandTopic.topicItem<Message.command'<Spec.Id.t, Spec.command>>,
72
75
  ) => {
73
76
  let {reference, command: command'} = topicItem
74
77
  let meta = command'->updateMeta
78
+ let id = command'.id->Spec.Id.toString
79
+ // Identity, not content: the folded state grows with the entity's whole
80
+ // history, so serialising it here makes the line's size a function of how
81
+ // long the system has been in use. The state at decision time is a
82
+ // debugger's subject, not a log's.
83
+ let cmdName =
84
+ command'.command->Message.encode(Spec.commandSchema)->Message.variantNameOfJson->LogFormat.bold
75
85
  EffectLogger.logDebug(
76
86
  ~comp,
77
- `deciding on state: ${state->JSON.stringifyAny->Option.getOr("<unserializable>")}`,
87
+ `deciding: id=${id} seq=${seq->Int.toString} cmd=${cmdName}`,
78
88
  )->Effect.runSync
79
89
  switch Behavior.decide(state, command'.command) {
80
90
  | Ok(generatedEvents) =>
@@ -89,7 +99,6 @@ module Make = (
89
99
  payloadDict->Dict.toArray->Array.length == 0
90
100
  ? ""
91
101
  : payloadDict->JSON.Encode.object->JSON.stringify
92
- let id = command'.id->Spec.Id.toString
93
102
  EffectLogger.logError(
94
103
  ~comp,
95
104
  `decide rejected: ${errorCode} ${errorDetail} id=${id}`,
@@ -336,7 +345,7 @@ module Make = (
336
345
  readState
337
346
  ->Effect.flatMap(((initialState, sequenceNr)) => {
338
347
  let (finalState, outcomes) =
339
- topicItemsForId->Array.reduce((initialState, []), processCommand)
348
+ topicItemsForId->Array.reduce((initialState, []), processCommand(~seq=sequenceNr))
340
349
 
341
350
  let eventsToAppend =
342
351
  outcomes
@@ -29,13 +29,15 @@ function Make(Spec) {
29
29
  Spec.Id.makeFromString(param[0]),
30
30
  param[1]
31
31
  ]));
32
- let processCommand = (param, topicItem) => {
32
+ let processCommand = seq => ((param, topicItem) => {
33
33
  let reference = topicItem.reference;
34
34
  let command$p = topicItem.command;
35
35
  let outcomes = param[1];
36
36
  let state = param[0];
37
37
  let meta = Message$ReventlessCore.deriveMeta(command$p.meta, undefined);
38
- Effect.runSync(EffectLogger$ReventlessCore.logDebug(comp, undefined, `deciding on state: ` + Stdlib_Option.getOr(JSON.stringify(state), "<unserializable>")));
38
+ let id = Spec.Id.toString(command$p.id);
39
+ let cmdName = LogFormat$ReventlessCore.bold(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command$p.command, Spec.commandSchema)));
40
+ Effect.runSync(EffectLogger$ReventlessCore.logDebug(comp, undefined, `deciding: id=` + id + ` seq=` + seq.toString() + ` cmd=` + cmdName));
39
41
  let generatedEvents = Behavior.decide(state, command$p.command);
40
42
  if (generatedEvents.TAG === "Ok") {
41
43
  let generatedEvents$1 = generatedEvents._0;
@@ -57,7 +59,6 @@ function Make(Spec) {
57
59
  let match = Message$ReventlessCore.splitMessage(errorJson);
58
60
  let payloadDict = match[1];
59
61
  let errorDetail = Object.entries(payloadDict).length === 0 ? "" : JSON.stringify(payloadDict);
60
- let id = Spec.Id.toString(command$p.id);
61
62
  Effect.runSync(EffectLogger$ReventlessCore.logError(comp, undefined, `decide rejected: ` + errorCode + ` ` + errorDetail + ` id=` + id));
62
63
  return [
63
64
  state,
@@ -71,7 +72,7 @@ function Make(Spec) {
71
72
  meta
72
73
  ]])
73
74
  ];
74
- };
75
+ });
75
76
  let replayCache = Lru$ReventlessCore.make(100);
76
77
  let resetCache = () => Lru$ReventlessCore.clear(replayCache);
77
78
  let snapshotConfig = Behavior.snapshot;
@@ -249,7 +250,7 @@ function Make(Spec) {
249
250
  let match = Stdlib_Array.reduce(topicItemsForId, [
250
251
  initialState,
251
252
  []
252
- ], processCommand);
253
+ ], processCommand(sequenceNr));
253
254
  let outcomes = match[1];
254
255
  let finalState = match[0];
255
256
  let eventsToAppend = outcomes.map(param => {
@@ -42,6 +42,7 @@ let rec fromSchemaType = (
42
42
  switch st {
43
43
  | ScalarString => `String${bang}`
44
44
  | ScalarNumber => `Float${bang}`
45
+ | ScalarInt => `Int${bang}`
45
46
  | ScalarBoolean => `Boolean${bang}`
46
47
  | ScalarBigInt => `String${bang}`
47
48
  | EntityId => `ID${bang}`
@@ -286,6 +287,7 @@ let rec scalarOfSchemaType = (st: SchemaType.schemaType): string =>
286
287
  switch st {
287
288
  | ScalarString => "String"
288
289
  | ScalarNumber => "Float"
290
+ | ScalarInt => "Int"
289
291
  | ScalarBoolean => "Boolean"
290
292
  | ScalarBigInt => "String"
291
293
  | EntityId => "ID"
@@ -43,6 +43,8 @@ function fromSchemaType(_required, _asInputOpt, _st, collectedTypes, seenTypes)
43
43
  switch (st) {
44
44
  case "ScalarNumber" :
45
45
  return `Float` + bang;
46
+ case "ScalarInt" :
47
+ return `Int` + bang;
46
48
  case "ScalarBoolean" :
47
49
  return `Boolean` + bang;
48
50
  case "EntityId" :
@@ -219,6 +221,8 @@ function scalarOfSchemaType(_st) {
219
221
  switch (st) {
220
222
  case "ScalarNumber" :
221
223
  return "Float";
224
+ case "ScalarInt" :
225
+ return "Int";
222
226
  case "ScalarBoolean" :
223
227
  return "Boolean";
224
228
  case "EntityId" :
@@ -24,7 +24,7 @@ function generateTools(pluginName, mutationEntries) {
24
24
  if (fieldName.length <= 0) {
25
25
  return;
26
26
  }
27
- let inputSchema = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
27
+ let inputSchema = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, schema);
28
28
  let base = entryDescription.length > 0 ? entryDescription : `Execute ` + fieldName + ` on ` + pluginName;
29
29
  let desc = buildDescriptionSuffix(base, entry);
30
30
  tools.push({
@@ -39,7 +39,7 @@ function generateTools(pluginName, mutationEntries) {
39
39
  if (fieldName.length <= 0) {
40
40
  return;
41
41
  }
42
- let inputSchema = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(variantSchema);
42
+ let inputSchema = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, variantSchema);
43
43
  let obj = Stdlib_JSON.Decode.object(inputSchema);
44
44
  let withId;
45
45
  if (obj !== undefined) {
@@ -1,6 +1,11 @@
1
1
  type rec schemaType =
2
2
  | ScalarString
3
3
  | ScalarNumber
4
+ | /** A number the schema says is whole. sury carries the fact on the number's
5
+ `format`, and it was being dropped here — so every `int` in the domain
6
+ reached the SDL as `Float` and JSON Schema as `"number"`, and a quantity
7
+ was a field a client could legally send `2.5` in. */
8
+ ScalarInt
4
9
  | ScalarBoolean
5
10
  | ScalarBigInt
6
11
  | EntityId
@@ -103,6 +108,9 @@ and shapeOf = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): s
103
108
  | String({const: ?Some(_)}) => ScalarString
104
109
  | String(_) if isDateTime(schema) => DateTime
105
110
  | String(_) => isIdFieldName(fieldName) ? EntityId : ScalarString
111
+ // `Int32` and `Port` are both whole-number formats; anything else (or no
112
+ // format at all) is a `float` as far as the schema is concerned.
113
+ | Number({format: ?Some(Int32 | Port)}) => ScalarInt
106
114
  | Number(_) => ScalarNumber
107
115
  | Boolean(_) => ScalarBoolean
108
116
  | BigInt(_) => ScalarBigInt
@@ -85,7 +85,17 @@ function shapeOf(parentName, fieldName, schema) {
85
85
  return "ScalarString";
86
86
  }
87
87
  case "number" :
88
- return "ScalarNumber";
88
+ let match = schema.format;
89
+ if (match === undefined) {
90
+ return "ScalarNumber";
91
+ }
92
+ switch (match) {
93
+ case "int32" :
94
+ case "port" :
95
+ return "ScalarInt";
96
+ case "integer" :
97
+ return "ScalarNumber";
98
+ }
89
99
  case "bigint" :
90
100
  return "ScalarBigInt";
91
101
  case "boolean" :
@@ -168,12 +178,12 @@ function shapeOf(parentName, fieldName, schema) {
168
178
  return $$enum;
169
179
  }
170
180
  }
171
- let match = TaggedUnion$Reventless.classify(schema);
172
- if (match === undefined) {
181
+ let match$1 = TaggedUnion$Reventless.classify(schema);
182
+ if (match$1 === undefined) {
173
183
  return "Unknown";
174
184
  }
175
- let unionName = match[0];
176
- let armTypes = match[1].map(param => {
185
+ let unionName = match$1[0];
186
+ let armTypes = match$1[1].map(param => {
177
187
  let armSchema = param.schema;
178
188
  let tag = param.tag;
179
189
  let memberName = TaggedUnion$Reventless.memberTypeName(unionName, tag);