@reventlessdev/reventless-core 3.0.0-alpha.236 → 3.0.0-alpha.238
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.
- package/CHANGELOG.md +23 -0
- package/package.json +6 -6
- package/src/Message.res +1 -1
- package/src/adapter/Monitoring/Monitoring.res +1 -1
- package/src/admin/Platform_Admin_Structure.res +5 -1
- package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res +4 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +11 -3
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate_Callback.res +2 -2
- package/src/components/Api/ApiAllowedStatesHelpers.res +2 -3
- package/src/components/Api/ApiTargetStateHelpers.res +4 -3
- package/src/components/Api/GraphQL_FragmentGenerator.res +14 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +1 -1
- package/src/components/Api/QueryDbListQuery.res +25 -1
- package/src/components/Api/QueryDbListQuery.res.mjs +11 -4
- package/src/components/Api/SuryToJsonSchema.res +39 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +34 -3
- package/src/components/Dcb/Dcb_Builder.res +79 -16
- package/src/components/Dcb/Dcb_Builder.res.mjs +41 -5
- package/src/components/EventLog/EventLog.res +1 -1
- package/src/plugin/component/Plugin_Structure.res +377 -18
- package/src/plugin/component/Plugin_Structure.res.mjs +204 -9
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +1 -1
- package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +3 -2
- package/tests/admin/Platform_Admin_StructureTest.res +49 -0
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +24 -0
- package/tests/admin/Platform_BakedManifestTest.res +3 -1
- package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +14 -8
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +24 -12
- package/tests/admin/Platform_PluginStructuresApiTest.res +3 -1
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +6 -2
- package/tests/aggregate/AggregateCacheTest.res +1 -1
- package/tests/aggregate/AggregateSnapshotTest.res +1 -1
- package/tests/api/Api_IdsTest.res.mjs +1 -1
- package/tests/api/SuryToJsonSchemaTest.res +141 -1
- package/tests/api/SuryToJsonSchemaTest.res.mjs +269 -54
- package/tests/commandgenerator/OwnerStampingTest.res +57 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +19 -0
- package/tests/message/MessageTest.res +1 -1
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +1 -1
- package/tests/plugin/PluginStructureTest.res +332 -15
- package/tests/plugin/PluginStructureTest.res.mjs +316 -10
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res +31 -0
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +73 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +16 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -2
- package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +3 -2
- package/tests/plugin/StateViewSlice/PsShipmentsView.res +26 -0
- package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +101 -0
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +2 -0
- package/tests/util/CsvStreamTest.res +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
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.238 (2026-08-16)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** stamp the owner from each DCB slice's own command schema ([6edbdf4](https://github.com/ReventlessDev/reventless-core/commit/6edbdf4680f2535597c3b560215aa514ceed27eb))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **core:** check a declared lifecycle edge against the states that exist ([d427d92](https://github.com/ReventlessDev/reventless-core/commit/d427d92f8912b4dc9a9407c3c0cec3200d41a5f1))
|
|
14
|
+
* **core:** report a lifecycle state no command can reach ([eb24dbd](https://github.com/ReventlessDev/reventless-core/commit/eb24dbd90df07185f0f4785c4bb726f5b617e7b2))
|
|
15
|
+
* **ppx:** declare a command's lifecycle edge once, as [@transition](https://github.com/transition) ([dd35130](https://github.com/ReventlessDev/reventless-core/commit/dd3513014f14b71879ee21263c6755d3c3d95096))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 3.0.0-alpha.237 (2026-08-16)
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **core:** exclude retired rows from reads a caller may not widen ([662f31a](https://github.com/ReventlessDev/reventless-core/commit/662f31abb717bda5154199d349da6dcf8e2d3e78))
|
|
23
|
+
* **core:** let [@retired](https://github.com/retired) name a lifecycle state, not only a boolean ([6bb346b](https://github.com/ReventlessDev/reventless-core/commit/6bb346b4f6a5f33826fc24537953482a76067177))
|
|
24
|
+
* **core:** mark the state that retires a row, and allow more than one ([cb1461f](https://github.com/ReventlessDev/reventless-core/commit/cb1461f024d3ca3b53fd9c8b010a054e3fcc4555))
|
|
25
|
+
* **core:** publish queryableDef.retiredField from the [@retired](https://github.com/retired) annotation ([b44436a](https://github.com/ReventlessDev/reventless-core/commit/b44436a997b6c4ff0531f0b07d793cc858eef94a))
|
|
26
|
+
* **spec:** [@retired](https://github.com/retired) state-field annotation and its schema emission ([2d8234b](https://github.com/ReventlessDev/reventless-core/commit/2d8234b6b3dd8f479031a67eb5b4b47b5c0c2ff9))
|
|
27
|
+
|
|
28
|
+
|
|
6
29
|
# 3.0.0-alpha.236 (2026-08-15)
|
|
7
30
|
|
|
8
31
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.238",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -30,20 +30,20 @@
|
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
32
32
|
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.7",
|
|
33
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
34
33
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
35
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
36
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
35
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
36
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
37
37
|
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.7",
|
|
38
38
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.142",
|
|
40
39
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.116",
|
|
41
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.144"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
45
45
|
"sury-ppx": "11.0.0-alpha.2",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.68"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "12.3.0"
|
package/src/Message.res
CHANGED
|
@@ -69,7 +69,7 @@ let toCommandSchema' = (idSchema, commandSchema) =>
|
|
|
69
69
|
// before a nested `@schema` field existed still decodes instead of bricking the
|
|
70
70
|
// aggregate's log replay. These two entry points thread the memoized envelope schema;
|
|
71
71
|
// `Reventless.Message.decode` (the other central decoder) is tolerant at the source.
|
|
72
|
-
// See docs/plans/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
72
|
+
// See docs/plans/done/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
73
73
|
let decodeEvent' = (json, idSchema, eventSchema) =>
|
|
74
74
|
json->parseJsonTolerant(toEventSchema'(idSchema, eventSchema))
|
|
75
75
|
let decodeCommand' = (json, idSchema, commandSchema) =>
|
|
@@ -10,7 +10,7 @@ concern; this only exposes the choke point through which the framework says
|
|
|
10
10
|
"I just provisioned an execution unit" and leaves what to do about it to the
|
|
11
11
|
listener.
|
|
12
12
|
|
|
13
|
-
See `docs/plans/monitoring-hook-seam.md`.
|
|
13
|
+
See `docs/plans/done/monitoring-hook-seam.md`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -122,7 +122,9 @@ let pluginReadModel: queryableDef = {
|
|
|
122
122
|
// Hand-rolled rather than resolved, but the rung is the same one
|
|
123
123
|
// `labelFieldsFromStateSchema` would report for a field literally named `name`.
|
|
124
124
|
labelFieldSource: Some("convention"),
|
|
125
|
-
|
|
125
|
+
// The published wire name stays `status` — it is what the SDL declares and what
|
|
126
|
+
// stored rows carry. Only the key naming it moved.
|
|
127
|
+
lifecycleField: Some("status"),
|
|
126
128
|
visibility: None,
|
|
127
129
|
chapter: None,
|
|
128
130
|
// The admin fragment hand-declares its query names rather than deriving them from
|
|
@@ -137,6 +139,8 @@ let pluginReadModel: queryableDef = {
|
|
|
137
139
|
idFieldSource: None,
|
|
138
140
|
requiredAccess: None,
|
|
139
141
|
ownerField: None,
|
|
142
|
+
retiredField: None,
|
|
143
|
+
retiredValues: None,
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
let structure: pluginStructure = {
|
|
@@ -137,7 +137,7 @@ let pluginReadModel_searchableFields = ["name"];
|
|
|
137
137
|
|
|
138
138
|
let pluginReadModel_labelFieldSource = "convention";
|
|
139
139
|
|
|
140
|
-
let
|
|
140
|
+
let pluginReadModel_lifecycleField = "status";
|
|
141
141
|
|
|
142
142
|
let pluginReadModel_singleQueryField = Api_Naming$ReventlessCore.adminField("Plugin");
|
|
143
143
|
|
|
@@ -150,8 +150,10 @@ let pluginReadModel = {
|
|
|
150
150
|
labelField: "name",
|
|
151
151
|
searchableFields: pluginReadModel_searchableFields,
|
|
152
152
|
labelFieldSource: pluginReadModel_labelFieldSource,
|
|
153
|
-
|
|
153
|
+
lifecycleField: pluginReadModel_lifecycleField,
|
|
154
154
|
ownerField: undefined,
|
|
155
|
+
retiredField: undefined,
|
|
156
|
+
retiredValues: undefined,
|
|
155
157
|
visibility: undefined,
|
|
156
158
|
chapter: undefined,
|
|
157
159
|
singleQueryField: pluginReadModel_singleQueryField,
|
|
@@ -29,7 +29,7 @@ let sdlTypes: array<string> = [
|
|
|
29
29
|
// structures persisted before the fields existed decode as `None`, a hand-rolled
|
|
30
30
|
// def may decline to state them, and a state whose key cannot be resolved has no
|
|
31
31
|
// `idField` to report.
|
|
32
|
-
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n
|
|
32
|
+
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n lifecycleField: String\n visibility: String\n chapter: String\n singleQueryField: String\n idField: String\n idFieldSource: String\n requiredAccess: [String!]\n ownerField: String\n retiredField: String\n retiredValues: [String!]\n}`,
|
|
33
33
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
34
34
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
35
35
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -113,7 +113,7 @@ let encodeQueryableDef = (r: queryableDef): JSON.t =>
|
|
|
113
113
|
"labelFieldSource",
|
|
114
114
|
r.labelFieldSource->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
|
|
115
115
|
),
|
|
116
|
-
("
|
|
116
|
+
("lifecycleField", r.lifecycleField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
117
117
|
// Carried rather than only filtered on: a tooling consumer reading the
|
|
118
118
|
// unfiltered structure needs to know WHICH components are Internal, and a
|
|
119
119
|
// consumer of the filtered query reads null here because nothing Internal
|
|
@@ -128,6 +128,8 @@ let encodeQueryableDef = (r: queryableDef): JSON.t =>
|
|
|
128
128
|
("idFieldSource", r.idFieldSource->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
129
129
|
("requiredAccess", r.requiredAccess->Option.mapOr(JSON.Encode.null, encodeStrings)),
|
|
130
130
|
("ownerField", r.ownerField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
131
|
+
("retiredField", r.retiredField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
132
|
+
("retiredValues", r.retiredValues->Option.mapOr(JSON.Encode.null, encodeStrings)),
|
|
131
133
|
])->JSON.Encode.object
|
|
132
134
|
|
|
133
135
|
let encodeEventDef = (e: eventDef): JSON.t =>
|
|
@@ -10,7 +10,7 @@ let sdlTypes = [
|
|
|
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}`,
|
|
13
|
-
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n
|
|
13
|
+
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n lifecycleField: String\n visibility: String\n chapter: String\n singleQueryField: String\n idField: String\n idFieldSource: String\n requiredAccess: [String!]\n ownerField: String\n retiredField: String\n retiredValues: [String!]\n}`,
|
|
14
14
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
15
15
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
16
16
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -130,8 +130,8 @@ function encodeQueryableDef(r) {
|
|
|
130
130
|
Stdlib_Option.mapOr(r.labelFieldSource, null, prim => prim)
|
|
131
131
|
],
|
|
132
132
|
[
|
|
133
|
-
"
|
|
134
|
-
Stdlib_Option.mapOr(r.
|
|
133
|
+
"lifecycleField",
|
|
134
|
+
Stdlib_Option.mapOr(r.lifecycleField, null, prim => prim)
|
|
135
135
|
],
|
|
136
136
|
[
|
|
137
137
|
"visibility",
|
|
@@ -160,6 +160,14 @@ function encodeQueryableDef(r) {
|
|
|
160
160
|
[
|
|
161
161
|
"ownerField",
|
|
162
162
|
Stdlib_Option.mapOr(r.ownerField, null, prim => prim)
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"retiredField",
|
|
166
|
+
Stdlib_Option.mapOr(r.retiredField, null, prim => prim)
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
"retiredValues",
|
|
170
|
+
Stdlib_Option.mapOr(r.retiredValues, null, encodeStrings)
|
|
163
171
|
]
|
|
164
172
|
]);
|
|
165
173
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// UiFragmentRegistry StateChangeSlice — the write side of the platform UI-fragment
|
|
2
2
|
// registry, extracted off the Plugin aggregate (see
|
|
3
|
-
// docs/plans/event-sourced-fragment-registries.md).
|
|
3
|
+
// docs/plans/done/event-sourced-fragment-registries.md).
|
|
4
4
|
//
|
|
5
5
|
// Commands are driven by the plugin runtime's connect handshake and routed here by the
|
|
6
6
|
// admin PluginExtensionPoint's UI-fragment mapping (RegisterUiFragment on ConnectPlugin,
|
|
@@ -114,7 +114,7 @@ module Make = (
|
|
|
114
114
|
// at append time → the conflict branch invalidates the entry and the retry
|
|
115
115
|
// replays cold. Mirrors the StateChangeSlice decision-model cache, including
|
|
116
116
|
// the fixed capacity (a per-aggregate knob is a future refinement — see
|
|
117
|
-
// docs/plans/aggregate-snapshotting.md).
|
|
117
|
+
// docs/plans/done/aggregate-snapshotting.md).
|
|
118
118
|
let replayCacheCapacity = 100
|
|
119
119
|
let replayCache: Lru.t<string, (Behavior.state, int)> = Lru.make(
|
|
120
120
|
~capacity=replayCacheCapacity,
|
|
@@ -122,7 +122,7 @@ module Make = (
|
|
|
122
122
|
|
|
123
123
|
let resetCache = () => replayCache->Lru.clear
|
|
124
124
|
|
|
125
|
-
// Persisted-snapshot configuration (docs/plans/aggregate-snapshotting.md).
|
|
125
|
+
// Persisted-snapshot configuration (docs/plans/done/aggregate-snapshotting.md).
|
|
126
126
|
// `None` (the default) keeps full replay; `Some({interval, stateSchema})`
|
|
127
127
|
// seeds cold replays from the latest persisted snapshot and writes a fresh
|
|
128
128
|
// one every `interval` events. Snapshots are a read optimization only — the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Read per-variant `allowedStates` metadata that the reventless-ppx
|
|
2
|
-
// `@
|
|
2
|
+
// an `@transition` from-set attaches to a command schema. Consumed by
|
|
3
3
|
// `Plugin_Structure.toCommandDef` when building the `commandDef` records that
|
|
4
4
|
// land in `Platform_ComponentDefinitions` so AutoUI can filter the per-row command
|
|
5
5
|
// menu by the row's status.
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
open ReventlessInfra.Api
|
|
8
8
|
|
|
9
9
|
/** Returns the allowed-state list for a given command-variant name, or None
|
|
10
|
-
when the variant
|
|
11
|
-
— the command shows on every row). */
|
|
10
|
+
when the variant declares no `@transition` (the command shows on every row). */
|
|
12
11
|
let getAllowedStates = (
|
|
13
12
|
commandSchema: S.t<unknown>,
|
|
14
13
|
~variantName: string,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Read per-variant `targetState` metadata that the reventless-ppx
|
|
2
|
-
// `@
|
|
2
|
+
// an `@transition` target attaches to a command schema. Consumed by
|
|
3
3
|
// `Plugin_Structure.toCommandDef` when building the `commandDef` records that
|
|
4
4
|
// land in `Platform_ComponentDefinitions`, so AutoUI's board drag resolver can
|
|
5
5
|
// move a row by a *declared* transition instead of a name-stem guess. Mirrors
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
open ReventlessInfra.Api
|
|
9
9
|
|
|
10
10
|
/** Returns the declared target state for a given command-variant name, or None
|
|
11
|
-
when the variant
|
|
12
|
-
|
|
11
|
+
when the variant declares no target — either because it carries no
|
|
12
|
+
`@transition` at all, or because it declares a from-set only, which is a
|
|
13
|
+
command saying it does not move the row. */
|
|
13
14
|
let getTargetState = (
|
|
14
15
|
commandSchema: S.t<unknown>,
|
|
15
16
|
~variantName: string,
|
|
@@ -436,6 +436,19 @@ let deriveByIdsQueryField = (
|
|
|
436
436
|
): string =>
|
|
437
437
|
` ${listFieldName}ByIds(ids: [String!]!): [${returnTypeName}!]!`
|
|
438
438
|
|
|
439
|
+
// `includeRetired` sits beside the paging arguments rather than inside `filter`,
|
|
440
|
+
// and that placement is the point. `filter` is the caller's description of the
|
|
441
|
+
// rows they want; this is a request to lift a restriction the server placed on
|
|
442
|
+
// them, which the server grants or ignores on its own terms. Putting it in the
|
|
443
|
+
// filter input would also require the field to be a declared filter field, which
|
|
444
|
+
// would publish `<field>Eq` to callers who can only ever get an empty page from
|
|
445
|
+
// it.
|
|
446
|
+
//
|
|
447
|
+
// Emitted unconditionally, on every connection field, whether or not the view
|
|
448
|
+
// declares a retirement flag. A view that declares none ignores it, and the
|
|
449
|
+
// alternative — an argument that appears and disappears with the annotation —
|
|
450
|
+
// makes adding `@retired` a breaking schema change for every client that had
|
|
451
|
+
// already learned the field's shape.
|
|
439
452
|
let deriveConnectionQueryField = (
|
|
440
453
|
~listFieldName: string,
|
|
441
454
|
~singularTypeName: string,
|
|
@@ -443,7 +456,7 @@ let deriveConnectionQueryField = (
|
|
|
443
456
|
~hasOrderBy: bool=false,
|
|
444
457
|
): string => {
|
|
445
458
|
let orderByArg = hasOrderBy ? `, orderBy: ${singularTypeName}OrderBy` : ""
|
|
446
|
-
` ${listFieldName}(filter: ${filterTypeName}${orderByArg}, first: Int, after: String, last: Int, before: String): ${singularTypeName}Connection!`
|
|
459
|
+
` ${listFieldName}(filter: ${filterTypeName}${orderByArg}, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): ${singularTypeName}Connection!`
|
|
447
460
|
}
|
|
448
461
|
|
|
449
462
|
// ── Mutation field derivation ──────────────────────────────────────────────
|
|
@@ -374,7 +374,7 @@ function deriveByIdsQueryField(listFieldName, returnTypeName) {
|
|
|
374
374
|
function deriveConnectionQueryField(listFieldName, singularTypeName, filterTypeName, hasOrderByOpt) {
|
|
375
375
|
let hasOrderBy = hasOrderByOpt !== undefined ? hasOrderByOpt : false;
|
|
376
376
|
let orderByArg = hasOrderBy ? `, orderBy: ` + singularTypeName + `OrderBy` : "";
|
|
377
|
-
return ` ` + listFieldName + `(filter: ` + filterTypeName + orderByArg + `, first: Int, after: String, last: Int, before: String): ` + singularTypeName + `Connection!`;
|
|
377
|
+
return ` ` + listFieldName + `(filter: ` + filterTypeName + orderByArg + `, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): ` + singularTypeName + `Connection!`;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
380
|
function deriveMutationFieldFromObject(fieldName, collectedTypes, seenTypes, variantSchema) {
|
|
@@ -35,6 +35,17 @@ let getId = (item: JSON.t): string =>
|
|
|
35
35
|
// String form of a field for comparison / cursor purposes: the string itself,
|
|
36
36
|
// or a number rendered via Float.toString, else None. The push-down must render
|
|
37
37
|
// the same string in SQL (CAST … AS TEXT) for cursor/order parity.
|
|
38
|
+
// A boolean field's value, or None when the row does not carry one. Separate
|
|
39
|
+
// from `getFieldString` rather than reusing it: JSON `true` stringifies to
|
|
40
|
+
// nothing useful, and a retirement predicate read off a coerced string would
|
|
41
|
+
// treat `"false"` — a legitimate value for a field someone stored as text — as
|
|
42
|
+
// truthy and hide the row.
|
|
43
|
+
let getFieldBool = (item: JSON.t, field: string): option<bool> =>
|
|
44
|
+
item
|
|
45
|
+
->JSON.Decode.object
|
|
46
|
+
->Option.flatMap(d => d->Dict.get(field))
|
|
47
|
+
->Option.flatMap(JSON.Decode.bool)
|
|
48
|
+
|
|
38
49
|
let getFieldString = (item: JSON.t, field: string): option<string> =>
|
|
39
50
|
item
|
|
40
51
|
->JSON.Decode.object
|
|
@@ -97,6 +108,7 @@ let run = (
|
|
|
97
108
|
~labelField: string,
|
|
98
109
|
~decodeLocalId: string => option<string>=Api_Ids.toLocalId,
|
|
99
110
|
~ownerScope: option<(string, string)>=?,
|
|
111
|
+
~retiredScope: option<Reventless.OwnerScope.retiredScope>=?,
|
|
100
112
|
): JSON.t => {
|
|
101
113
|
let filterDict =
|
|
102
114
|
argsDict->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
|
|
@@ -185,7 +197,19 @@ let run = (
|
|
|
185
197
|
getFieldString(item, field)->Option.mapOr(false, v => v == required)
|
|
186
198
|
| None => true
|
|
187
199
|
}
|
|
188
|
-
|
|
200
|
+
// The missing-field rule lives in `OwnerScope.isRetiredValue`, beside the two
|
|
201
|
+
// forms it has to answer for; the note on why it lands the opposite way from
|
|
202
|
+
// `passOwner` is there.
|
|
203
|
+
let passRetired = switch retiredScope {
|
|
204
|
+
| Some(scope) =>
|
|
205
|
+
!(
|
|
206
|
+
scope->Reventless.OwnerScope.isRetiredValue(
|
|
207
|
+
item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(scope.field)),
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
| None => true
|
|
211
|
+
}
|
|
212
|
+
passSearch && passPrefix && passIds && passPerField && passOwner && passRetired
|
|
189
213
|
})
|
|
190
214
|
|
|
191
215
|
let orderByDict = argsDict->Dict.get("orderBy")->Option.flatMap(JSON.Decode.object)
|
|
@@ -5,6 +5,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
|
|
7
7
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
8
9
|
import * as Api_Ids$ReventlessCore from "./Api_Ids.res.mjs";
|
|
9
10
|
|
|
10
11
|
function encodeCursor(value) {
|
|
@@ -19,6 +20,10 @@ function getId(item) {
|
|
|
19
20
|
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d["id"]), Stdlib_JSON.Decode.string), "");
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function getFieldBool(item, field) {
|
|
24
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]), Stdlib_JSON.Decode.bool);
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
function getFieldString(item, field) {
|
|
23
28
|
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]), v => {
|
|
24
29
|
let s = Stdlib_JSON.Decode.string(v);
|
|
@@ -48,7 +53,7 @@ function buildConnection(pageItems, hasNextPage, hasPreviousPage, cursorValueOf)
|
|
|
48
53
|
};
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
function run(items, argsDict, capability, labelField, decodeLocalIdOpt, ownerScope) {
|
|
56
|
+
function run(items, argsDict, capability, labelField, decodeLocalIdOpt, ownerScope, retiredScope) {
|
|
52
57
|
let decodeLocalId = decodeLocalIdOpt !== undefined ? decodeLocalIdOpt : Api_Ids$ReventlessCore.toLocalId;
|
|
53
58
|
let filterDict = Stdlib_Option.getOr(Stdlib_Option.flatMap(argsDict["filter"], Stdlib_JSON.Decode.object), {});
|
|
54
59
|
let search = Stdlib_Option.flatMap(filterDict["search"], Stdlib_JSON.Decode.string);
|
|
@@ -102,8 +107,9 @@ function run(items, argsDict, capability, labelField, decodeLocalIdOpt, ownerSco
|
|
|
102
107
|
} else {
|
|
103
108
|
passOwner = true;
|
|
104
109
|
}
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
let passRetired = retiredScope !== undefined ? !OwnerScope$Reventless.isRetiredValue(retiredScope, Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[retiredScope.field])) : true;
|
|
111
|
+
if (passSearch && passPrefix && passIds && passPerField && passOwner) {
|
|
112
|
+
return passRetired;
|
|
107
113
|
} else {
|
|
108
114
|
return false;
|
|
109
115
|
}
|
|
@@ -214,8 +220,9 @@ export {
|
|
|
214
220
|
decodeCursor,
|
|
215
221
|
defaultListPageSize,
|
|
216
222
|
getId,
|
|
223
|
+
getFieldBool,
|
|
217
224
|
getFieldString,
|
|
218
225
|
buildConnection,
|
|
219
226
|
run,
|
|
220
227
|
}
|
|
221
|
-
/*
|
|
228
|
+
/* OwnerScope-Reventless Not a pure module */
|
|
@@ -101,6 +101,45 @@ let mergeAnnotations = (
|
|
|
101
101
|
}
|
|
102
102
|
| None => ()
|
|
103
103
|
}
|
|
104
|
+
switch spec.retired {
|
|
105
|
+
| Some(r) if r.field === fieldName =>
|
|
106
|
+
// `x-reventless-retired: {label?, showWhenFalse, values?, value?}`. The
|
|
107
|
+
// label is omitted when empty, the way `x-reventless-metric` omits its own,
|
|
108
|
+
// so a consumer deriving one from the field name can tell "not stated" from
|
|
109
|
+
// "stated as empty". `showWhenFalse` always travels: it is a decision the
|
|
110
|
+
// annotation author made either way, and its default is not the consumer's
|
|
111
|
+
// to pick.
|
|
112
|
+
//
|
|
113
|
+
// `values` follows the same omit-rather-than-write-empty rule, and its
|
|
114
|
+
// absence is what says which form this is: absent ⇒ the row is retired when
|
|
115
|
+
// the field is `true`; present ⇒ retired when the field's value is in the
|
|
116
|
+
// set, and the field is the record's lifecycle.
|
|
117
|
+
//
|
|
118
|
+
// The singular `value` rides alongside for one release, and only where the
|
|
119
|
+
// set has exactly one member — there is no singular reading of two. It
|
|
120
|
+
// exists to close a window, not to support two shapes: a consumer pinned
|
|
121
|
+
// before the set degrades to nothing at all rather than to a missing badge.
|
|
122
|
+
// Enforcement never depended on it — the resolvers exclude retired rows
|
|
123
|
+
// whatever the consumer understands — so the whole of the lag is cosmetic.
|
|
124
|
+
// Drop it once no consumer predating the set can be reached.
|
|
125
|
+
let entries = [("showWhenFalse", JSON.Encode.bool(r.showWhenFalse))]
|
|
126
|
+
let entries = switch r.values {
|
|
127
|
+
| Some(vs) =>
|
|
128
|
+
let entries = Array.concat(
|
|
129
|
+
entries,
|
|
130
|
+
[("values", JSON.Encode.array(vs->Array.map(JSON.Encode.string)))],
|
|
131
|
+
)
|
|
132
|
+
switch vs {
|
|
133
|
+
| [only] => Array.concat(entries, [("value", JSON.Encode.string(only))])
|
|
134
|
+
| _ => entries
|
|
135
|
+
}
|
|
136
|
+
| None => entries
|
|
137
|
+
}
|
|
138
|
+
let entries =
|
|
139
|
+
r.label === "" ? entries : Array.concat([("label", JSON.Encode.string(r.label))], entries)
|
|
140
|
+
obj->Dict.set("x-reventless-retired", JSON.Encode.object(Dict.fromArray(entries)))
|
|
141
|
+
| _ => ()
|
|
142
|
+
}
|
|
104
143
|
switch spec.metric->Array.find(((field, _)) => field === fieldName) {
|
|
105
144
|
| Some((_, m)) =>
|
|
106
145
|
// `x-reventless-metric: {aggregate, label}`. Omit an empty label so the
|
|
@@ -90,18 +90,49 @@ function mergeAnnotations(fieldSchema, fieldName, spec) {
|
|
|
90
90
|
obj["x-reventless-semantic"] = semanticId;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
let r = spec.retired;
|
|
94
|
+
if (r !== undefined && r.field === fieldName) {
|
|
95
|
+
let entries = [[
|
|
96
|
+
"showWhenFalse",
|
|
97
|
+
r.showWhenFalse
|
|
98
|
+
]];
|
|
99
|
+
let vs = r.values;
|
|
100
|
+
let entries$1;
|
|
101
|
+
if (vs !== undefined) {
|
|
102
|
+
let entries$2 = entries.concat([[
|
|
103
|
+
"values",
|
|
104
|
+
vs.map(prim => prim)
|
|
105
|
+
]]);
|
|
106
|
+
if (vs.length !== 1) {
|
|
107
|
+
entries$1 = entries$2;
|
|
108
|
+
} else {
|
|
109
|
+
let only = vs[0];
|
|
110
|
+
entries$1 = entries$2.concat([[
|
|
111
|
+
"value",
|
|
112
|
+
only
|
|
113
|
+
]]);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
entries$1 = entries;
|
|
117
|
+
}
|
|
118
|
+
let entries$3 = r.label === "" ? entries$1 : [[
|
|
119
|
+
"label",
|
|
120
|
+
r.label
|
|
121
|
+
]].concat(entries$1);
|
|
122
|
+
obj["x-reventless-retired"] = Object.fromEntries(entries$3);
|
|
123
|
+
}
|
|
93
124
|
let match$4 = spec.metric.find(param => param[0] === fieldName);
|
|
94
125
|
if (match$4 !== undefined) {
|
|
95
126
|
let m = match$4[1];
|
|
96
|
-
let entries = [[
|
|
127
|
+
let entries$4 = [[
|
|
97
128
|
"aggregate",
|
|
98
129
|
m.aggregate
|
|
99
130
|
]];
|
|
100
|
-
let entries$
|
|
131
|
+
let entries$5 = m.label === "" ? entries$4 : entries$4.concat([[
|
|
101
132
|
"label",
|
|
102
133
|
m.label
|
|
103
134
|
]]);
|
|
104
|
-
obj["x-reventless-metric"] = Object.fromEntries(entries$
|
|
135
|
+
obj["x-reventless-metric"] = Object.fromEntries(entries$5);
|
|
105
136
|
}
|
|
106
137
|
return obj;
|
|
107
138
|
}
|
|
@@ -312,7 +312,7 @@ module Make = (
|
|
|
312
312
|
// sweeps up sibling products. Cross-partition fan-out and capacity reads are
|
|
313
313
|
// preserved (generalised rule 3). Safety guard: if any slice's partition is
|
|
314
314
|
// ambiguous we keep the annotated values for the whole boundary rather than
|
|
315
|
-
// thread a partial inference. See docs/plans/dcb-tag-scope-inference.md.
|
|
315
|
+
// thread a partial inference. See docs/plans/done/dcb-tag-scope-inference.md.
|
|
316
316
|
let inferenceShapes =
|
|
317
317
|
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) =>
|
|
318
318
|
Reventless.DcbTag.sliceShapeFromSchemas(
|
|
@@ -761,28 +761,80 @@ module Make = (
|
|
|
761
761
|
// + AppSync direct invocations (CommandGenerator.payload format)
|
|
762
762
|
let dcbHandlerBase = DcbCommandTopic.makeFilteringHandler(dcbCommandTopic)
|
|
763
763
|
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
//
|
|
764
|
+
// generateCommand for AppSync direct invocations, one per command constructor.
|
|
765
|
+
//
|
|
766
|
+
// Every one of them shares this CommandTopic's publishJsons — the reason a
|
|
767
|
+
// single function used to suffice — and they differ in exactly one argument:
|
|
768
|
+
// the schema. That argument is not only a validator. `makeGenerateCommand`
|
|
769
|
+
// also reads it to find the fields a command marks as its owner, and stamps
|
|
770
|
+
// them with the caller. Handing it the permissive `S.json` (as this did, on
|
|
771
|
+
// the still-true grounds that AppSync has already validated against the SDL)
|
|
772
|
+
// makes that lookup answer "no owner fields" for every command there is — so
|
|
773
|
+
// the write publishes whatever owner the CLIENT sent, silently, and the row
|
|
774
|
+
// is wrong from then on.
|
|
775
|
+
//
|
|
776
|
+
// Per-slice decoding still happens inside `buildSliceHandler`; nothing here
|
|
777
|
+
// re-validates. The schema travels for what it says about ownership.
|
|
778
|
+
let dcbCommandSchemasByTag = {
|
|
779
|
+
let byTag = Dict.make()
|
|
780
|
+
stateChangeSlices->Array.forEach((module(S: StateChangeSlice.T)) => {
|
|
781
|
+
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
782
|
+
if !(ApiNoApiHelpers.isNoApi(commandSchema)) {
|
|
783
|
+
Api_Naming.sliceMutationFields(
|
|
784
|
+
~plugin=apiNamePrefix,
|
|
785
|
+
~slice=S.Spec.name,
|
|
786
|
+
~commandSchema,
|
|
787
|
+
)->Array.forEach(((_, tag)) => {
|
|
788
|
+
// The resolver injects the TAG alone, so two slices claiming one
|
|
789
|
+
// constructor name would make the stamp ambiguous — and the wrong
|
|
790
|
+
// arm of that ambiguity writes a row owned by nobody's decision.
|
|
791
|
+
// Multi-command slices cannot reach here (their mutation fields
|
|
792
|
+
// collide first), which leaves single-command slices, where the
|
|
793
|
+
// field names differ and nothing else would notice.
|
|
794
|
+
switch byTag->Dict.get(tag) {
|
|
795
|
+
| Some(_) =>
|
|
796
|
+
log.error(
|
|
797
|
+
~comp="Dcb_Builder",
|
|
798
|
+
`Two DCB slices in "${name}" both declare the command "${tag}". ` ++
|
|
799
|
+
"Owner stamping resolves a command by that name alone, so it cannot " ++
|
|
800
|
+
"tell them apart. Rename one of the constructors.",
|
|
801
|
+
)
|
|
802
|
+
| None => byTag->Dict.set(tag, commandSchema)
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
}
|
|
806
|
+
})
|
|
807
|
+
byTag
|
|
808
|
+
}
|
|
809
|
+
|
|
768
810
|
let dcbGenerateCommandOutput =
|
|
769
811
|
dcbCommandTopic
|
|
770
812
|
->Component.operations
|
|
771
|
-
->Pulumi.Output.apply(ops =>
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
813
|
+
->Pulumi.Output.apply(ops => {
|
|
814
|
+
let make = commandSchema =>
|
|
815
|
+
CommandGenerator_Callback.makeGenerateCommand(
|
|
816
|
+
~publishJsons=ops.publishJsons,
|
|
817
|
+
~publishJsonsAndWait=?ops.publishJsonsAndWait,
|
|
818
|
+
~serviceName=name,
|
|
819
|
+
~commandSchema,
|
|
820
|
+
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
821
|
+
~stripIdFromParams=false,
|
|
822
|
+
)
|
|
823
|
+
let byTag = Dict.make()
|
|
824
|
+
dcbCommandSchemasByTag
|
|
825
|
+
->Dict.toArray
|
|
826
|
+
->Array.forEach(((tag, schema)) => byTag->Dict.set(tag, make(schema)))
|
|
827
|
+
// A command no slice claims keeps the permissive generator — what every
|
|
828
|
+
// command got before this change — so nothing that reaches this handler
|
|
829
|
+
// by another route starts failing. It also cannot stamp, which is why
|
|
830
|
+
// the unknown case is logged rather than left to look like a success.
|
|
831
|
+
(byTag, make(S.json->S.castToUnknown))
|
|
832
|
+
})
|
|
781
833
|
|
|
782
834
|
let dcbHandler =
|
|
783
835
|
(dcbHandlerBase, inboundReceiversOutput, dcbGenerateCommandOutput)
|
|
784
836
|
->Pulumi.Output.all3
|
|
785
|
-
->Pulumi.Output.apply(((baseHandler, receivers,
|
|
837
|
+
->Pulumi.Output.apply(((baseHandler, receivers, (generatorsByTag, generateFallback))) => {
|
|
786
838
|
let composite = (event, ctx) => {
|
|
787
839
|
let raw: dict<JSON.t> = event->Obj.magic
|
|
788
840
|
switch raw->Dict.get("__inboundTranslation") {
|
|
@@ -810,6 +862,17 @@ module Make = (
|
|
|
810
862
|
switch (raw->Dict.get("command"), raw->Dict.get("arguments")) {
|
|
811
863
|
| (Some(JSON.String(_)), Some(_)) =>
|
|
812
864
|
let payload: CommandGenerator.payload = event->Obj.magic
|
|
865
|
+
let generateCommand = switch generatorsByTag->Dict.get(payload.command) {
|
|
866
|
+
| Some(g) => g
|
|
867
|
+
| None =>
|
|
868
|
+
log.warn(
|
|
869
|
+
~comp="Dcb_Builder",
|
|
870
|
+
`No DCB slice in "${name}" declares the command "${payload.command}", ` ++
|
|
871
|
+
"so it is dispatched without an owner stamp. Any field the command " ++
|
|
872
|
+
"marks as its owner keeps the value the caller sent.",
|
|
873
|
+
)
|
|
874
|
+
generateFallback
|
|
875
|
+
}
|
|
813
876
|
(generateCommand(payload)->Effect.map(msgId => msgId->Obj.magic))->Obj.magic
|
|
814
877
|
| _ => baseHandler(event, ctx)
|
|
815
878
|
}
|