@reventlessdev/reventless-core 3.0.0-alpha.260 → 3.0.0-alpha.261
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 +8 -0
- package/package.json +9 -9
- package/src/Behavior.res +1 -4
- package/src/Message.res +17 -14
- package/src/Projection.res +51 -25
- package/src/RequestContext.res +8 -9
- package/src/ResourceAttribution.res +28 -14
- package/src/adapter/Adapter.res +48 -4
- package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
- package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
- package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
- package/src/adapter/Monitoring/Monitoring.res +16 -17
- package/src/adapter/QueryInterception/QueryInterception.res +0 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
- package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
- package/src/admin/Platform_Admin.res +17 -6
- package/src/admin/Platform_AdminApi.res +14 -9
- package/src/admin/Platform_Admin_Callback.res +1 -1
- package/src/admin/Platform_BakedManifest.res +34 -34
- package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
- package/src/admin/Platform_PluginStructuresApi.res +15 -12
- package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate.res +13 -12
- package/src/components/Aggregate/Aggregate_Builder.res +22 -21
- package/src/components/Aggregate/Aggregate_Callback.res +91 -86
- package/src/components/Api/ApiNoApiHelpers.res +1 -3
- package/src/components/Api/Api_Ids.res +0 -1
- package/src/components/Api/Api_Naming.res +41 -18
- package/src/components/Api/GraphQL_FragmentGenerator.res +38 -51
- package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
- package/src/components/Api/GraphQL_Stitcher.res +9 -10
- package/src/components/Api/MCP_SchemaGenerator.res +8 -12
- package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
- package/src/components/Api/QueryDbListQuery.res +13 -6
- package/src/components/Api/SchemaType.res +28 -23
- package/src/components/Api/SuryToJsonSchema.res +9 -10
- package/src/components/AutomationSlice/AutomationSlice.res +4 -4
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
- package/src/components/Builder_Helpers.res +12 -23
- package/src/components/CommandGenerator/CommandGenerator.res +6 -1
- package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
- package/src/components/CommandTopic/CommandTopic.res +3 -3
- package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
- package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
- package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
- package/src/components/Counter/Counter_Builder.res +1 -6
- package/src/components/Counter/Counter_Callback.res +3 -3
- package/src/components/Dcb/Dcb_Builder.res +970 -956
- package/src/components/DcbEventLog/DcbEventLog.res +4 -4
- package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
- package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
- package/src/components/EventCollector/EventCollector_Builder.res +2 -1
- package/src/components/EventLog/EventLog.res +5 -1
- package/src/components/EventLog/EventLog_Operations.res +12 -8
- package/src/components/EventMapper/EventMapper.res +8 -10
- package/src/components/EventMapper/EventMapper_Builder.res +4 -1
- package/src/components/EventMapper/EventMapper_Callback.res +46 -31
- package/src/components/EventTopic/EventTopic.res +3 -3
- package/src/components/EventTopic/EventTopic_Builder.res +1 -1
- package/src/components/Extension/Extension.res +4 -1
- package/src/components/Extension/Extension_Operations.res +11 -3
- package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
- package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
- package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
- package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
- package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
- package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
- package/src/components/QueryDb/QueryDb_Operations.res +10 -10
- package/src/components/ReadModel/ReadModel.res +3 -3
- package/src/components/ReadModel/ReadModel_Builder.res +1 -2
- package/src/components/ReadModel/ReadModel_Callback.res +2 -1
- package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
- package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
- package/src/components/StateTopic.res +1 -3
- package/src/components/StateViewSlice/StateViewSlice.res +4 -4
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
- package/src/components/Task/Task.res +3 -3
- package/src/components/Task/Task_Adapter.res +4 -1
- package/src/components/Task/Task_Builder.res +24 -21
- package/src/plugin/api/PluginBaseFragment.res +9 -3
- package/src/plugin/component/Capability_Inference.res +8 -10
- package/src/plugin/component/Plugin.res +6 -6
- package/src/plugin/component/Plugin_Builder.res +54 -50
- package/src/plugin/component/Plugin_Callback.res +10 -6
- package/src/plugin/component/Plugin_Helpers.res +264 -230
- package/src/plugin/component/Plugin_Structure.res +370 -324
- package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
- package/src/plugin/component/SchemaWalker.res +11 -12
- package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
- package/src/plugin/lifecycle/PluginBehavior.res +3 -4
- package/src/plugin/lifecycle/PluginSpec.res +1 -2
- package/src/plugin/lifecycle/PluginsProjection.res +16 -5
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
- package/src/util/CommandPublisher.res +9 -2
- package/src/util/CsvStream.res +3 -3
- package/src/util/Interstack.res +7 -15
- package/src/util/Interstack.resi +3 -3
- package/src/util/LogFormat.res +5 -9
- package/src/util/Logger.res +1 -2
- package/src/util/Lru.res +2 -1
- package/src/util/Lru.resi +2 -3
- package/src/util/ScheduleOps.res +24 -27
- package/src/util/StorePrefixCollision.res +6 -9
- package/src/util/Util_Promise.res +4 -1
- package/src/util/Util_Promise.res.mjs +1 -1
- package/tests/IdentityTest.res +166 -113
- package/tests/ResourceAttributionTest.res +28 -25
- package/tests/StorePrefixCollisionTest.res +2 -1
- package/tests/TaskBucketNamingTest.res +3 -2
- package/tests/adapter/EventLogProvisioningTest.res +51 -48
- package/tests/adapter/MonitoringTest.res +81 -38
- package/tests/adapter/QueryInterceptionTest.res +2 -2
- package/tests/adapter/RefusalVocabularyTest.res +31 -23
- package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
- package/tests/adapter/RuntimeExtensionTest.res +5 -3
- package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
- package/tests/admin/Platform_Admin_StructureTest.res +10 -11
- package/tests/admin/Platform_BakedManifestTest.res +31 -35
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
- package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
- package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
- package/tests/aggregate/AggregateCacheTest.res +10 -12
- package/tests/aggregate/AggregateCallbackTest.res +75 -46
- package/tests/aggregate/AggregateCausationTest.res +19 -20
- package/tests/aggregate/AggregateConflictTest.res +41 -19
- package/tests/aggregate/AggregateFixtures.res +15 -5
- package/tests/aggregate/AggregateRejectionTest.res +97 -80
- package/tests/aggregate/AggregateSnapshotTest.res +44 -16
- package/tests/api/Api_IdsTest.res +10 -4
- package/tests/api/BrandedMarkerCompositionTest.res +48 -36
- package/tests/api/GeolocationSemanticTest.res +3 -3
- package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
- package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
- package/tests/api/StorageRefFieldsTest.res +30 -18
- package/tests/api/SuryToJsonSchemaTest.res +1315 -1014
- package/tests/api/TaggedUnionSchemaTest.res +17 -24
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
- package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
- package/tests/commandgenerator/OwnerStampingTest.res +91 -76
- package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
- package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
- package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
- package/tests/counter/CounterCallbackTest.res +40 -34
- package/tests/counter/CounterFixtures.res +4 -4
- package/tests/dcb/DcbDecodeTest.res +115 -84
- package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
- package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
- package/tests/dcb/DcbFixtures.res +4 -5
- package/tests/dcb/DcbScopeInferenceTest.res +173 -114
- package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
- package/tests/dcb/DcbTagTest.res +22 -11
- package/tests/dcb/DcbValidationTest.res +288 -218
- package/tests/eventlog/EventLogFixtures.res +4 -4
- package/tests/eventlog/EventLogOperationsTest.res +102 -67
- package/tests/eventlog/EventLogRetryTest.res +104 -65
- package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
- package/tests/eventmapper/EventMapperFixtures.res +15 -23
- package/tests/eventmapper/EventMapperTest.res +54 -38
- package/tests/eventtopic/EventTopicFixtures.res +3 -4
- package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
- package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
- package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
- package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
- package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
- package/tests/logger/LogFormatTest.res +104 -82
- package/tests/logger/LoggerLazyTest.res +21 -12
- package/tests/message/MessageTest.res +96 -93
- package/tests/message/MetaEnvelopeTest.res +26 -30
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
- package/tests/plugin/ManifestVisibilityTest.res +20 -12
- package/tests/plugin/PluginDefinitionScalars.res +1 -2
- package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
- package/tests/plugin/PluginSpecExperiment.res +0 -1
- package/tests/plugin/PluginStructureTest.res +1529 -1045
- package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
- package/tests/plugin/SchemaWalkerTest.res +116 -70
- package/tests/plugin/StateChange/PsReserveStock.res +4 -4
- package/tests/plugin/StateView/PsAnnotatedView.res +4 -4
- package/tests/plugin/StateView/PsCustomersView.res +4 -4
- package/tests/plugin/StateView/PsShipmentsView.res +3 -2
- package/tests/projection/LifecycleTrailTest.res +1 -4
- package/tests/projection/ProjectionOptimizeTest.res +13 -9
- package/tests/querydb/QueryDbFixtures.res +0 -1
- package/tests/querydb/QueryDbOperationsTest.res +99 -64
- package/tests/querydb/TaggedUnionStampTest.res +10 -8
- package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
- package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
- package/tests/util/CommandPublisherTest.res +0 -1
- package/tests/util/CsvStreamTest.res +3 -3
- package/tests/util/LruTest.res +84 -56
|
@@ -27,7 +27,14 @@ module PsPlaceOrderSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
27
27
|
}
|
|
28
28
|
let isAsync = false
|
|
29
29
|
type component = scsComponent
|
|
30
|
-
let make = (
|
|
30
|
+
let make = (
|
|
31
|
+
~dcbEventLog as _,
|
|
32
|
+
~publishJsons as _,
|
|
33
|
+
~tagKeysByEventType as _=?,
|
|
34
|
+
~crossPartitionTagKeys as _=?,
|
|
35
|
+
~runtime as _=?,
|
|
36
|
+
~opts as _=?,
|
|
37
|
+
): component => Obj.magic(0)
|
|
31
38
|
}
|
|
32
39
|
module PsShipOrderSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
33
40
|
module Spec = PsShipOrder
|
|
@@ -40,7 +47,14 @@ module PsShipOrderSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
40
47
|
}
|
|
41
48
|
let isAsync = false
|
|
42
49
|
type component = scsComponent
|
|
43
|
-
let make = (
|
|
50
|
+
let make = (
|
|
51
|
+
~dcbEventLog as _,
|
|
52
|
+
~publishJsons as _,
|
|
53
|
+
~tagKeysByEventType as _=?,
|
|
54
|
+
~crossPartitionTagKeys as _=?,
|
|
55
|
+
~runtime as _=?,
|
|
56
|
+
~opts as _=?,
|
|
57
|
+
): component => Obj.magic(0)
|
|
44
58
|
}
|
|
45
59
|
module PsOrdersViewSlice: ReventlessInfra.StateViewSlice.T = {
|
|
46
60
|
module Spec = PsOrdersView
|
|
@@ -99,7 +113,14 @@ module PsAttachInvoiceSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
99
113
|
}
|
|
100
114
|
let isAsync = false
|
|
101
115
|
type component = scsComponent
|
|
102
|
-
let make = (
|
|
116
|
+
let make = (
|
|
117
|
+
~dcbEventLog as _,
|
|
118
|
+
~publishJsons as _,
|
|
119
|
+
~tagKeysByEventType as _=?,
|
|
120
|
+
~crossPartitionTagKeys as _=?,
|
|
121
|
+
~runtime as _=?,
|
|
122
|
+
~opts as _=?,
|
|
123
|
+
): component => Obj.magic(0)
|
|
103
124
|
}
|
|
104
125
|
|
|
105
126
|
module PsUploadAvatarSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
@@ -113,7 +134,14 @@ module PsUploadAvatarSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
113
134
|
}
|
|
114
135
|
let isAsync = false
|
|
115
136
|
type component = scsComponent
|
|
116
|
-
let make = (
|
|
137
|
+
let make = (
|
|
138
|
+
~dcbEventLog as _,
|
|
139
|
+
~publishJsons as _,
|
|
140
|
+
~tagKeysByEventType as _=?,
|
|
141
|
+
~crossPartitionTagKeys as _=?,
|
|
142
|
+
~runtime as _=?,
|
|
143
|
+
~opts as _=?,
|
|
144
|
+
): component => Obj.magic(0)
|
|
117
145
|
}
|
|
118
146
|
|
|
119
147
|
module PsUploadImagesSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
@@ -127,7 +155,14 @@ module PsUploadImagesSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
127
155
|
}
|
|
128
156
|
let isAsync = false
|
|
129
157
|
type component = scsComponent
|
|
130
|
-
let make = (
|
|
158
|
+
let make = (
|
|
159
|
+
~dcbEventLog as _,
|
|
160
|
+
~publishJsons as _,
|
|
161
|
+
~tagKeysByEventType as _=?,
|
|
162
|
+
~crossPartitionTagKeys as _=?,
|
|
163
|
+
~runtime as _=?,
|
|
164
|
+
~opts as _=?,
|
|
165
|
+
): component => Obj.magic(0)
|
|
131
166
|
}
|
|
132
167
|
|
|
133
168
|
module PsTypoStoreSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
@@ -141,7 +176,14 @@ module PsTypoStoreSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
141
176
|
}
|
|
142
177
|
let isAsync = false
|
|
143
178
|
type component = scsComponent
|
|
144
|
-
let make = (
|
|
179
|
+
let make = (
|
|
180
|
+
~dcbEventLog as _,
|
|
181
|
+
~publishJsons as _,
|
|
182
|
+
~tagKeysByEventType as _=?,
|
|
183
|
+
~crossPartitionTagKeys as _=?,
|
|
184
|
+
~runtime as _=?,
|
|
185
|
+
~opts as _=?,
|
|
186
|
+
): component => Obj.magic(0)
|
|
145
187
|
}
|
|
146
188
|
|
|
147
189
|
module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
@@ -155,7 +197,14 @@ module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
155
197
|
}
|
|
156
198
|
let isAsync = false
|
|
157
199
|
type component = scsComponent
|
|
158
|
-
let make = (
|
|
200
|
+
let make = (
|
|
201
|
+
~dcbEventLog as _,
|
|
202
|
+
~publishJsons as _,
|
|
203
|
+
~tagKeysByEventType as _=?,
|
|
204
|
+
~crossPartitionTagKeys as _=?,
|
|
205
|
+
~runtime as _=?,
|
|
206
|
+
~opts as _=?,
|
|
207
|
+
): component => Obj.magic(0)
|
|
159
208
|
}
|
|
160
209
|
|
|
161
210
|
module PsReserveStockSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
@@ -169,7 +218,14 @@ module PsReserveStockSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
169
218
|
}
|
|
170
219
|
let isAsync = false
|
|
171
220
|
type component = scsComponent
|
|
172
|
-
let make = (
|
|
221
|
+
let make = (
|
|
222
|
+
~dcbEventLog as _,
|
|
223
|
+
~publishJsons as _,
|
|
224
|
+
~tagKeysByEventType as _=?,
|
|
225
|
+
~crossPartitionTagKeys as _=?,
|
|
226
|
+
~runtime as _=?,
|
|
227
|
+
~opts as _=?,
|
|
228
|
+
): component => Obj.magic(0)
|
|
173
229
|
}
|
|
174
230
|
|
|
175
231
|
module PsDispatchShipmentSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
@@ -183,7 +239,14 @@ module PsDispatchShipmentSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
183
239
|
}
|
|
184
240
|
let isAsync = false
|
|
185
241
|
type component = scsComponent
|
|
186
|
-
let make = (
|
|
242
|
+
let make = (
|
|
243
|
+
~dcbEventLog as _,
|
|
244
|
+
~publishJsons as _,
|
|
245
|
+
~tagKeysByEventType as _=?,
|
|
246
|
+
~crossPartitionTagKeys as _=?,
|
|
247
|
+
~runtime as _=?,
|
|
248
|
+
~opts as _=?,
|
|
249
|
+
): component => Obj.magic(0)
|
|
187
250
|
}
|
|
188
251
|
module PsTransitionSwitchSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
189
252
|
module Spec = PsTransitionSwitch
|
|
@@ -196,7 +259,14 @@ module PsTransitionSwitchSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
196
259
|
}
|
|
197
260
|
let isAsync = false
|
|
198
261
|
type component = scsComponent
|
|
199
|
-
let make = (
|
|
262
|
+
let make = (
|
|
263
|
+
~dcbEventLog as _,
|
|
264
|
+
~publishJsons as _,
|
|
265
|
+
~tagKeysByEventType as _=?,
|
|
266
|
+
~crossPartitionTagKeys as _=?,
|
|
267
|
+
~runtime as _=?,
|
|
268
|
+
~opts as _=?,
|
|
269
|
+
): component => Obj.magic(0)
|
|
200
270
|
}
|
|
201
271
|
module PsShipmentsViewSlice: ReventlessInfra.StateViewSlice.T = {
|
|
202
272
|
module Spec = PsShipmentsView
|
|
@@ -241,7 +311,7 @@ describe("lifecycle topology", () => {
|
|
|
241
311
|
references: [],
|
|
242
312
|
allowedStates,
|
|
243
313
|
targetState,
|
|
244
|
-
allowedStatesSource: ?allowedStates->Option.map(_ => "declared"),
|
|
314
|
+
allowedStatesSource: ?(allowedStates->Option.map(_ => "declared")),
|
|
245
315
|
apiExposed: None,
|
|
246
316
|
requiredAccess: None,
|
|
247
317
|
ownerField: None,
|
|
@@ -251,7 +321,9 @@ describe("lifecycle topology", () => {
|
|
|
251
321
|
// `Cancelled` is in the enum and nothing declares an edge into it.
|
|
252
322
|
let findings = Plugin_Structure.lifecycleTopologyFindings(
|
|
253
323
|
~writables=[
|
|
254
|
-
writableWith([
|
|
324
|
+
writableWith([
|
|
325
|
+
command(~name="Ship", ~allowedStates=["Placed"], ~targetState="Shipped", ()),
|
|
326
|
+
]),
|
|
255
327
|
],
|
|
256
328
|
~lifecycleStatesByView=ordersStates,
|
|
257
329
|
)
|
|
@@ -360,7 +432,7 @@ describe("the harvested model against the switch", () => {
|
|
|
360
432
|
): Reventless.Plugin.derivedEdge => {
|
|
361
433
|
component: "TransitionSwitch",
|
|
362
434
|
command,
|
|
363
|
-
|
|
435
|
+
?level,
|
|
364
436
|
allowedStates,
|
|
365
437
|
targets,
|
|
366
438
|
}
|
|
@@ -406,7 +478,10 @@ describe("the harvested model against the switch", () => {
|
|
|
406
478
|
// cannot express. The harvest reports that as a contradiction; here the
|
|
407
479
|
// choice is between the declaration and picking one of the two arbitrarily.
|
|
408
480
|
expect(
|
|
409
|
-
edgeOf(
|
|
481
|
+
edgeOf(
|
|
482
|
+
[edge(~command="Book", ~allowedStates=["Booked"], ~targets=["Booked", "Abandoned"])],
|
|
483
|
+
"Book",
|
|
484
|
+
),
|
|
410
485
|
)->toEqual(Some((Some(["Booked"]), Some("Booked"), Some("derived"))))
|
|
411
486
|
})
|
|
412
487
|
|
|
@@ -560,231 +635,334 @@ describe("declared-edge cross-check", () => {
|
|
|
560
635
|
|
|
561
636
|
describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
562
637
|
describe("stateChangeSlices", () => {
|
|
563
|
-
testSync(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
testSync(
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
638
|
+
testSync(
|
|
639
|
+
"produces two SCS entries in declaration order",
|
|
640
|
+
() => {
|
|
641
|
+
expect(structure.stateChangeSlices->Array.length)->toBe(2)
|
|
642
|
+
},
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
testSync(
|
|
646
|
+
"PlaceOrder: producedEventTypes contains OrderPlaced (qualified)",
|
|
647
|
+
() => {
|
|
648
|
+
let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
649
|
+
expect(placeOrder.producedEventTypes)->toEqual(["TestPlugin.OrderPlaced"])
|
|
650
|
+
},
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
testSync(
|
|
654
|
+
"PlaceOrder: consumedEventTypes contains CatalogProductSynced qualified (payload-less variants excluded)",
|
|
655
|
+
() => {
|
|
656
|
+
let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
657
|
+
expect(placeOrder.consumedEventTypes)->toEqual(["TestPlugin.CatalogProductSynced"])
|
|
658
|
+
},
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
testSync(
|
|
662
|
+
"PlaceOrder: linkedViews contains Orders (consumes its OrderPlaced)",
|
|
663
|
+
() => {
|
|
664
|
+
let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
665
|
+
expect(placeOrder.linkedViews)->toEqual(["Orders"])
|
|
666
|
+
},
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
testSync(
|
|
670
|
+
"PlaceOrder: consistencyRead is AvailableProducts (only SVS consuming CatalogProductSynced)",
|
|
671
|
+
() => {
|
|
672
|
+
let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
673
|
+
expect(placeOrder.consistencyRead)->toEqual(Some("AvailableProducts"))
|
|
674
|
+
},
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
testSync(
|
|
678
|
+
"PlaceOrder command: level Collection (creation command), aggregateIdField orderId for UUID injection",
|
|
679
|
+
() => {
|
|
680
|
+
let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
681
|
+
let cmd = placeOrder.commands->Array.getUnsafe(0)
|
|
682
|
+
expect((cmd.level, cmd.aggregateIdField))->toEqual((
|
|
683
|
+
Reventless.Plugin.Collection,
|
|
684
|
+
Some("orderId"),
|
|
685
|
+
))
|
|
686
|
+
},
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
testSync(
|
|
690
|
+
"ShipOrder: producedEventTypes contains OrderShipped (qualified)",
|
|
691
|
+
() => {
|
|
692
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
693
|
+
expect(shipOrder.producedEventTypes)->toEqual(["TestPlugin.OrderShipped"])
|
|
694
|
+
},
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
testSync(
|
|
698
|
+
"ShipOrder: consumedEventTypes is empty (all consumed events are payload-less literals)",
|
|
699
|
+
() => {
|
|
700
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
701
|
+
expect(shipOrder.consumedEventTypes)->toEqual([])
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
testSync(
|
|
706
|
+
"ShipOrder: linkedViews contains Orders (consumes its OrderShipped)",
|
|
707
|
+
() => {
|
|
708
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
709
|
+
expect(shipOrder.linkedViews)->toEqual(["Orders"])
|
|
710
|
+
},
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
testSync(
|
|
714
|
+
"ShipOrder: consistencyRead is None (empty consumedEventTypes cannot overlap any SVS)",
|
|
715
|
+
() => {
|
|
716
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
717
|
+
expect(shipOrder.consistencyRead)->toEqual(None)
|
|
718
|
+
},
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
testSync(
|
|
722
|
+
"ShipOrder command: level Instance, aggregateIdField orderId",
|
|
723
|
+
() => {
|
|
724
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
725
|
+
let cmd = shipOrder.commands->Array.getUnsafe(0)
|
|
726
|
+
expect((cmd.level, cmd.aggregateIdField))->toEqual((
|
|
727
|
+
Reventless.Plugin.Instance,
|
|
728
|
+
Some("orderId"),
|
|
729
|
+
))
|
|
730
|
+
},
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
testSync(
|
|
734
|
+
"ShipOrder: payload-less command CancelShipment is surfaced in commands",
|
|
735
|
+
() => {
|
|
736
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
737
|
+
expect(shipOrder.commands->Array.map(c => c.name))->toEqual(["ShipOrder", "CancelShipment"])
|
|
738
|
+
},
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
testSync(
|
|
742
|
+
"ShipOrder: the @noApi CancelShipment is tagged apiExposed=false; ShipOrder stays exposed",
|
|
743
|
+
() => {
|
|
744
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
745
|
+
let byName = name => shipOrder.commands->Array.find(c => c.name == name)
|
|
746
|
+
expect((
|
|
747
|
+
byName("ShipOrder")->Option.flatMap(c => c.apiExposed),
|
|
748
|
+
byName("CancelShipment")->Option.flatMap(c => c.apiExposed),
|
|
749
|
+
))->toEqual((Some(true), Some(false)))
|
|
750
|
+
},
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
testSync(
|
|
754
|
+
"ShipOrder: the switch's target flows through to commandDef.targetState",
|
|
755
|
+
() => {
|
|
756
|
+
// End-to-end: the spec's `commandTransition`, evaluated per constructor
|
|
757
|
+
// against a synthetic command → commandDef. `CancelShipment` answers
|
|
758
|
+
// `Unrestricted` and carries None (resolver falls back to name-stem).
|
|
759
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
760
|
+
let byName = name => shipOrder.commands->Array.find(c => c.name == name)
|
|
761
|
+
expect((
|
|
762
|
+
byName("ShipOrder")->Option.flatMap(c => c.targetState),
|
|
763
|
+
byName("CancelShipment")->Option.flatMap(c => c.targetState),
|
|
764
|
+
))->toEqual((Some("Shipped"), None))
|
|
765
|
+
},
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
testSync(
|
|
769
|
+
"ShipOrder: the switch's from-set flows through to commandDef.allowedStates",
|
|
770
|
+
() => {
|
|
771
|
+
// The other half of the same declaration. An edge is one value, so a
|
|
772
|
+
// command that declares a target cannot end up without the states it may
|
|
773
|
+
// run from — the two fields are read off it together.
|
|
774
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
775
|
+
let byName = name => shipOrder.commands->Array.find(c => c.name == name)
|
|
776
|
+
expect((
|
|
777
|
+
byName("ShipOrder")->Option.flatMap(c => c.allowedStates),
|
|
778
|
+
byName("CancelShipment")->Option.flatMap(c => c.allowedStates),
|
|
779
|
+
))->toEqual((Some(["Placed"]), None))
|
|
780
|
+
},
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
testSync(
|
|
784
|
+
"ShipOrder: the @noApi variant carries no callable mutation field (no sibling leak)",
|
|
785
|
+
() => {
|
|
786
|
+
// Regression: for a single-exposed-command slice, `mutationFieldFor`
|
|
787
|
+
// resolves every variant — including the @noApi one — to the slice's one
|
|
788
|
+
// mutation field. The non-exposed variant must not carry that sibling
|
|
789
|
+
// field; it emits an empty sentinel instead, while the exposed command
|
|
790
|
+
// keeps a real, non-empty field.
|
|
791
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
792
|
+
let byName = name => shipOrder.commands->Array.find(c => c.name == name)
|
|
793
|
+
let shipField = byName("ShipOrder")->Option.map(c => c.mutationField)->Option.getOr("")
|
|
794
|
+
let cancelField =
|
|
795
|
+
byName("CancelShipment")->Option.map(c => c.mutationField)->Option.getOr("x")
|
|
796
|
+
expect((shipField->String.length > 0, cancelField))->toEqual((true, ""))
|
|
797
|
+
},
|
|
798
|
+
)
|
|
675
799
|
|
|
676
800
|
// A consumer that builds its own mutation document declares one variable
|
|
677
801
|
// per argument, and JSON Schema does not carry the GraphQL type those
|
|
678
802
|
// variables need — `orderId` reads as a plain string there while the server
|
|
679
803
|
// declares `ID!`. So the rendered type rides along on the property.
|
|
680
|
-
testSync(
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
804
|
+
testSync(
|
|
805
|
+
"ShipOrder: the command schema publishes each argument's GraphQL type",
|
|
806
|
+
() => {
|
|
807
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
808
|
+
let ship = shipOrder.commands->Array.find(c => c.name == "ShipOrder")->Option.getOrThrow
|
|
809
|
+
expect(ship.schema->String.includes(`"x-reventless-graphql-type":"ID!"`))->toBe(true)
|
|
810
|
+
},
|
|
811
|
+
)
|
|
685
812
|
|
|
686
813
|
// The names are composed from the mutation field, and a `@noApi` variant
|
|
687
814
|
// has none — publishing a type derived from the empty sentinel would name
|
|
688
815
|
// types no schema declares.
|
|
689
|
-
testSync(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
shipOrder.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
816
|
+
testSync(
|
|
817
|
+
"CancelShipment: a @noApi variant publishes no argument types",
|
|
818
|
+
() => {
|
|
819
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
820
|
+
let cancel =
|
|
821
|
+
shipOrder.commands->Array.find(c => c.name == "CancelShipment")->Option.getOrThrow
|
|
822
|
+
expect(cancel.schema->String.includes("x-reventless-graphql-type"))->toBe(false)
|
|
823
|
+
},
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
testSync(
|
|
827
|
+
"ShipOrder: payload-less event ShipmentVoided is surfaced in events",
|
|
828
|
+
() => {
|
|
829
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
830
|
+
expect(shipOrder.events->Array.map(e => e.name))->toEqual([
|
|
831
|
+
"OrderShipped",
|
|
832
|
+
"ShipmentVoided",
|
|
833
|
+
])
|
|
834
|
+
},
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
testSync(
|
|
838
|
+
"ShipOrder: producedEventTypes still excludes the payload-less ShipmentVoided (DCB filter intact)",
|
|
839
|
+
() => {
|
|
840
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
841
|
+
expect(shipOrder.producedEventTypes)->toEqual(["TestPlugin.OrderShipped"])
|
|
842
|
+
},
|
|
843
|
+
)
|
|
705
844
|
|
|
706
845
|
// A slice's declared errors are the third family of its contract, beside the
|
|
707
846
|
// commands it accepts and the events it emits — what a caller has to handle
|
|
708
847
|
// when a decision is refused.
|
|
709
|
-
testSync(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
848
|
+
testSync(
|
|
849
|
+
"PlaceOrder: the single payload-less error variant is surfaced",
|
|
850
|
+
() => {
|
|
851
|
+
let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
852
|
+
expect(placeOrder.errors->Array.map(e => e.name))->toEqual(["AlreadyPlaced"])
|
|
853
|
+
},
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
testSync(
|
|
857
|
+
"ShipOrder: both error variants surface, in declaration order",
|
|
858
|
+
() => {
|
|
859
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
860
|
+
expect(shipOrder.errors->Array.map(e => e.name))->toEqual(["OrderNotFound", "NotShippable"])
|
|
861
|
+
},
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
testSync(
|
|
865
|
+
"ShipOrder: a payload-carrying error carries its field schema",
|
|
866
|
+
() => {
|
|
867
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
868
|
+
let notShippable =
|
|
869
|
+
shipOrder.errors->Array.find(e => e.name == "NotShippable")->Option.getOrThrow
|
|
870
|
+
expect(notShippable.schema->String.includes("\"reason\""))->toBe(true)
|
|
871
|
+
},
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
testSync(
|
|
875
|
+
"ShipOrder: a payload-less error carries no fields and no references",
|
|
876
|
+
() => {
|
|
877
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
878
|
+
let notFound =
|
|
879
|
+
shipOrder.errors->Array.find(e => e.name == "OrderNotFound")->Option.getOrThrow
|
|
880
|
+
expect((
|
|
881
|
+
notFound.schema->String.includes("\"reason\""),
|
|
882
|
+
notFound.references->Array.length,
|
|
883
|
+
))->toEqual((false, 0))
|
|
884
|
+
},
|
|
885
|
+
)
|
|
732
886
|
})
|
|
733
887
|
|
|
734
888
|
describe("stateViewSlices", () => {
|
|
735
|
-
testSync(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
889
|
+
testSync(
|
|
890
|
+
"produces four SVS entries in declaration order",
|
|
891
|
+
() => {
|
|
892
|
+
expect(structure.stateViewSlices->Array.length)->toBe(4)
|
|
893
|
+
},
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
testSync(
|
|
897
|
+
"OrdersView: consumedEventTypes contains the three order events (qualified)",
|
|
898
|
+
() => {
|
|
899
|
+
let ordersView = structure.stateViewSlices->Array.getUnsafe(0)
|
|
900
|
+
expect(ordersView.consumedEventTypes)->toEqual([
|
|
901
|
+
"TestPlugin.OrderPlaced",
|
|
902
|
+
"TestPlugin.OrderShipped",
|
|
903
|
+
"TestPlugin.OrderCancelled",
|
|
904
|
+
])
|
|
905
|
+
},
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
testSync(
|
|
909
|
+
"OrdersView: linkedWriteSide contains both SCS producing order events",
|
|
910
|
+
() => {
|
|
911
|
+
let ordersView = structure.stateViewSlices->Array.getUnsafe(0)
|
|
912
|
+
expect(ordersView.linkedWriteSide)->toEqual(["PlaceOrder", "ShipOrder"])
|
|
913
|
+
},
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
testSync(
|
|
917
|
+
"AvailableProductsView: consumedEventTypes contains catalog events (qualified)",
|
|
918
|
+
() => {
|
|
919
|
+
let apv = structure.stateViewSlices->Array.getUnsafe(1)
|
|
920
|
+
expect(apv.consumedEventTypes)->toEqual([
|
|
921
|
+
"TestPlugin.CatalogProductSynced",
|
|
922
|
+
"TestPlugin.CatalogProductPriceChanged",
|
|
923
|
+
])
|
|
924
|
+
},
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
testSync(
|
|
928
|
+
"AvailableProductsView: linkedWriteSide is empty (no SCS produces these events)",
|
|
929
|
+
() => {
|
|
930
|
+
let apv = structure.stateViewSlices->Array.getUnsafe(1)
|
|
931
|
+
expect(apv.linkedWriteSide)->toEqual([])
|
|
932
|
+
},
|
|
933
|
+
)
|
|
765
934
|
})
|
|
766
935
|
|
|
767
936
|
describe("labelField / searchableFields", () => {
|
|
768
|
-
testSync(
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
937
|
+
testSync(
|
|
938
|
+
"OrdersView: no @displayName → first non-`*Id` string field wins (customerName)",
|
|
939
|
+
() => {
|
|
940
|
+
let ordersView = structure.stateViewSlices->Array.getUnsafe(0)
|
|
941
|
+
expect((ordersView.labelField, ordersView.searchableFields))->toEqual((
|
|
942
|
+
"customerName",
|
|
943
|
+
["customerName"],
|
|
944
|
+
))
|
|
945
|
+
},
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
testSync(
|
|
949
|
+
"AvailableProductsView: no @displayName → first non-`*Id` string field wins (name)",
|
|
950
|
+
() => {
|
|
951
|
+
let apv = structure.stateViewSlices->Array.getUnsafe(1)
|
|
952
|
+
expect((apv.labelField, apv.searchableFields))->toEqual(("name", ["name"]))
|
|
953
|
+
},
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
testSync(
|
|
957
|
+
"Customers: composite @displayName → labelField=displayName, searchableFields=raw source fields in declaration order",
|
|
958
|
+
() => {
|
|
959
|
+
let customers = structure.stateViewSlices->Array.getUnsafe(2)
|
|
960
|
+
expect((customers.labelField, customers.searchableFields))->toEqual((
|
|
961
|
+
"displayName",
|
|
962
|
+
["firstName", "lastName"],
|
|
963
|
+
))
|
|
964
|
+
},
|
|
965
|
+
)
|
|
788
966
|
})
|
|
789
967
|
|
|
790
968
|
// What a field *is* comes from `SchemaType`, the IR every other schema
|
|
@@ -798,149 +976,204 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
798
976
|
(r.field, r.searchableFields)
|
|
799
977
|
}
|
|
800
978
|
|
|
801
|
-
testSync(
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
979
|
+
testSync(
|
|
980
|
+
"a DateTime is not a name — a state whose only string is one falls to id",
|
|
981
|
+
() => {
|
|
982
|
+
// The shipped `Orders` case: `placedAt` was added so date views have a
|
|
983
|
+
// field to key off, and a positional rule let it rename the entity.
|
|
984
|
+
let schema = S.schema(
|
|
985
|
+
s =>
|
|
986
|
+
{
|
|
987
|
+
"orderId": s.matches(Reventless.Reference.to_("Order")),
|
|
988
|
+
"placedAt": s.matches(Reventless.DateTime.schema),
|
|
989
|
+
"shippedAt": s.matches(Reventless.DateTime.schema),
|
|
990
|
+
},
|
|
991
|
+
)
|
|
992
|
+
expect(labelOf(schema))->toEqual(("id", []))
|
|
993
|
+
},
|
|
994
|
+
)
|
|
813
995
|
|
|
814
|
-
testSync(
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
996
|
+
testSync(
|
|
997
|
+
"a DateTime is skipped, not fatal — a later plain string still wins",
|
|
998
|
+
() => {
|
|
999
|
+
let schema = S.schema(
|
|
1000
|
+
s =>
|
|
1001
|
+
{
|
|
1002
|
+
"placedAt": s.matches(Reventless.DateTime.schema),
|
|
1003
|
+
"reference": s.matches(S.string),
|
|
1004
|
+
},
|
|
1005
|
+
)
|
|
1006
|
+
expect(labelOf(schema))->toEqual(("reference", ["reference"]))
|
|
1007
|
+
},
|
|
1008
|
+
)
|
|
823
1009
|
|
|
824
|
-
testSync(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1010
|
+
testSync(
|
|
1011
|
+
"an optional string is still the entity's name",
|
|
1012
|
+
() => {
|
|
1013
|
+
let schema = S.schema(
|
|
1014
|
+
s =>
|
|
1015
|
+
{
|
|
1016
|
+
"nickname": s.matches(S.option(S.string)),
|
|
1017
|
+
},
|
|
1018
|
+
)
|
|
1019
|
+
expect(labelOf(schema))->toEqual(("nickname", ["nickname"]))
|
|
1020
|
+
},
|
|
1021
|
+
)
|
|
832
1022
|
|
|
833
|
-
testSync(
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1023
|
+
testSync(
|
|
1024
|
+
"a reference is not a name even when its field is not called `*Id`",
|
|
1025
|
+
() => {
|
|
1026
|
+
let schema = S.schema(
|
|
1027
|
+
s =>
|
|
1028
|
+
{
|
|
1029
|
+
"customer": s.matches(Reventless.Reference.to_("Customer")),
|
|
1030
|
+
"note": s.matches(S.string),
|
|
1031
|
+
},
|
|
1032
|
+
)
|
|
1033
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
1034
|
+
},
|
|
1035
|
+
)
|
|
842
1036
|
|
|
843
|
-
testSync(
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1037
|
+
testSync(
|
|
1038
|
+
"a DCB-tagged string is not a name",
|
|
1039
|
+
() => {
|
|
1040
|
+
let schema = S.schema(
|
|
1041
|
+
s =>
|
|
1042
|
+
{
|
|
1043
|
+
"partition": s.matches(Reventless.DcbTag.string),
|
|
1044
|
+
"note": s.matches(S.string),
|
|
1045
|
+
},
|
|
1046
|
+
)
|
|
1047
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
1048
|
+
},
|
|
1049
|
+
)
|
|
852
1050
|
|
|
853
|
-
testSync(
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1051
|
+
testSync(
|
|
1052
|
+
"the `*Id` test is case-insensitive — `productID` is a reference",
|
|
1053
|
+
() => {
|
|
1054
|
+
let schema = S.schema(
|
|
1055
|
+
s =>
|
|
1056
|
+
{
|
|
1057
|
+
"productID": s.matches(S.string),
|
|
1058
|
+
"note": s.matches(S.string),
|
|
1059
|
+
},
|
|
1060
|
+
)
|
|
1061
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
1062
|
+
},
|
|
1063
|
+
)
|
|
862
1064
|
|
|
863
|
-
testSync(
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1065
|
+
testSync(
|
|
1066
|
+
"a semantic-carrying string is a value, not a name",
|
|
1067
|
+
() => {
|
|
1068
|
+
// `imageUrl` names a stored object. A bucket key read as a product's name
|
|
1069
|
+
// is what the previous rule did whenever no plain string preceded it.
|
|
1070
|
+
let schema = S.schema(
|
|
1071
|
+
s =>
|
|
1072
|
+
{
|
|
1073
|
+
"imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
|
|
1074
|
+
"note": s.matches(S.string),
|
|
1075
|
+
},
|
|
1076
|
+
)
|
|
1077
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
1078
|
+
},
|
|
1079
|
+
)
|
|
874
1080
|
|
|
875
|
-
testSync(
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1081
|
+
testSync(
|
|
1082
|
+
"a state whose only string is a semantic falls to id",
|
|
1083
|
+
() => {
|
|
1084
|
+
let schema = S.schema(
|
|
1085
|
+
s =>
|
|
1086
|
+
{
|
|
1087
|
+
"imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
|
|
1088
|
+
},
|
|
1089
|
+
)
|
|
1090
|
+
expect(labelOf(schema))->toEqual(("id", []))
|
|
1091
|
+
},
|
|
1092
|
+
)
|
|
883
1093
|
|
|
884
|
-
testSync(
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1094
|
+
testSync(
|
|
1095
|
+
"`id` itself is still excluded by name",
|
|
1096
|
+
() => {
|
|
1097
|
+
// `SchemaType` reads a name that short as an ordinary string, so the
|
|
1098
|
+
// exclusion cannot come from the IR.
|
|
1099
|
+
let schema = S.schema(
|
|
1100
|
+
s =>
|
|
1101
|
+
{
|
|
1102
|
+
"id": s.matches(S.string),
|
|
1103
|
+
"note": s.matches(S.string),
|
|
1104
|
+
},
|
|
1105
|
+
)
|
|
1106
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
1107
|
+
},
|
|
1108
|
+
)
|
|
895
1109
|
})
|
|
896
1110
|
|
|
897
1111
|
describe("labelField — the conventional-name rung", () => {
|
|
898
1112
|
let labelOf = schema => {
|
|
899
|
-
let r = Plugin_Structure.labelFieldsFromStateSchema(
|
|
1113
|
+
let r = Plugin_Structure.labelFieldsFromStateSchema(
|
|
1114
|
+
~entityName="Test",
|
|
1115
|
+
schema->S.castToUnknown,
|
|
1116
|
+
)
|
|
900
1117
|
(r.field, r.searchableFields)
|
|
901
1118
|
}
|
|
902
1119
|
|
|
903
|
-
testSync(
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1120
|
+
testSync(
|
|
1121
|
+
"a field named `name` beats an earlier string",
|
|
1122
|
+
() => {
|
|
1123
|
+
let schema = S.schema(
|
|
1124
|
+
s =>
|
|
1125
|
+
{
|
|
1126
|
+
"sku": s.matches(S.string),
|
|
1127
|
+
"name": s.matches(S.string),
|
|
1128
|
+
},
|
|
1129
|
+
)
|
|
1130
|
+
expect(labelOf(schema))->toEqual(("name", ["name"]))
|
|
1131
|
+
},
|
|
1132
|
+
)
|
|
912
1133
|
|
|
913
|
-
testSync(
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1134
|
+
testSync(
|
|
1135
|
+
"`title` counts too, case-insensitively",
|
|
1136
|
+
() => {
|
|
1137
|
+
let schema = S.schema(
|
|
1138
|
+
s =>
|
|
1139
|
+
{
|
|
1140
|
+
"body": s.matches(S.string),
|
|
1141
|
+
"Title": s.matches(S.string),
|
|
1142
|
+
},
|
|
1143
|
+
)
|
|
1144
|
+
expect(labelOf(schema))->toEqual(("Title", ["Title"]))
|
|
1145
|
+
},
|
|
1146
|
+
)
|
|
922
1147
|
|
|
923
|
-
testSync(
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1148
|
+
testSync(
|
|
1149
|
+
"the match is exact — `customerName` is a customer's name, not this record's",
|
|
1150
|
+
() => {
|
|
1151
|
+
// The shipped `OrdersView` fixture depends on this: it has exactly one
|
|
1152
|
+
// eligible field and must be picked by position, not by convention.
|
|
1153
|
+
let schema = S.schema(
|
|
1154
|
+
s =>
|
|
1155
|
+
{
|
|
1156
|
+
"customerName": s.matches(S.string),
|
|
1157
|
+
"note": s.matches(S.string),
|
|
1158
|
+
},
|
|
1159
|
+
)
|
|
1160
|
+
expect(labelOf(schema))->toEqual(("customerName", ["customerName"]))
|
|
1161
|
+
},
|
|
1162
|
+
)
|
|
934
1163
|
|
|
935
|
-
testSync(
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1164
|
+
testSync(
|
|
1165
|
+
"an optional `name` still outranks an earlier string",
|
|
1166
|
+
() => {
|
|
1167
|
+
let schema = S.schema(
|
|
1168
|
+
s =>
|
|
1169
|
+
{
|
|
1170
|
+
"sku": s.matches(S.string),
|
|
1171
|
+
"name": s.matches(S.option(S.string)),
|
|
1172
|
+
},
|
|
1173
|
+
)
|
|
1174
|
+
expect(labelOf(schema))->toEqual(("name", ["name"]))
|
|
1175
|
+
},
|
|
1176
|
+
)
|
|
944
1177
|
})
|
|
945
1178
|
|
|
946
1179
|
// The rung is published because the four are not equally believable: a
|
|
@@ -953,54 +1186,72 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
953
1186
|
schema->S.castToUnknown,
|
|
954
1187
|
).source->Plugin_Structure.labelFieldSourceToString
|
|
955
1188
|
|
|
956
|
-
testSync(
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1189
|
+
testSync(
|
|
1190
|
+
"a @displayName spec is a declaration",
|
|
1191
|
+
() => {
|
|
1192
|
+
let customers = structure.stateViewSlices->Array.getUnsafe(2)
|
|
1193
|
+
expect((customers.labelField, customers.labelFieldSource))->toEqual((
|
|
1194
|
+
"displayName",
|
|
1195
|
+
Some("annotation"),
|
|
1196
|
+
))
|
|
1197
|
+
},
|
|
1198
|
+
)
|
|
963
1199
|
|
|
964
|
-
testSync(
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
// declaration.
|
|
978
|
-
let schema = S.schema(s =>
|
|
979
|
-
{
|
|
980
|
-
"customerName": s.matches(S.string),
|
|
981
|
-
"note": s.matches(S.string),
|
|
982
|
-
}
|
|
983
|
-
)
|
|
984
|
-
expect(sourceOf(schema))->toEqual("position")
|
|
985
|
-
})
|
|
1200
|
+
testSync(
|
|
1201
|
+
"a field named `name` is a guess the consumer can also make",
|
|
1202
|
+
() => {
|
|
1203
|
+
let schema = S.schema(
|
|
1204
|
+
s =>
|
|
1205
|
+
{
|
|
1206
|
+
"sku": s.matches(S.string),
|
|
1207
|
+
"name": s.matches(S.string),
|
|
1208
|
+
},
|
|
1209
|
+
)
|
|
1210
|
+
expect(sourceOf(schema))->toEqual("convention")
|
|
1211
|
+
},
|
|
1212
|
+
)
|
|
986
1213
|
|
|
987
|
-
testSync(
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1214
|
+
testSync(
|
|
1215
|
+
"declaration order is a guess only this side can make",
|
|
1216
|
+
() => {
|
|
1217
|
+
// `customerName` is eligible and unconventional, so it wins by position —
|
|
1218
|
+
// and a consumer told only "customerName" could not tell that from a
|
|
1219
|
+
// declaration.
|
|
1220
|
+
let schema = S.schema(
|
|
1221
|
+
s =>
|
|
1222
|
+
{
|
|
1223
|
+
"customerName": s.matches(S.string),
|
|
1224
|
+
"note": s.matches(S.string),
|
|
1225
|
+
},
|
|
1226
|
+
)
|
|
1227
|
+
expect(sourceOf(schema))->toEqual("position")
|
|
1228
|
+
},
|
|
1229
|
+
)
|
|
996
1230
|
|
|
997
|
-
testSync(
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1231
|
+
testSync(
|
|
1232
|
+
"no candidate at all is the state saying it has no human field",
|
|
1233
|
+
() => {
|
|
1234
|
+
let schema = S.schema(
|
|
1235
|
+
s =>
|
|
1236
|
+
{
|
|
1237
|
+
"orderId": s.matches(Reventless.Reference.to_("Order")),
|
|
1238
|
+
"placedAt": s.matches(Reventless.DateTime.schema),
|
|
1239
|
+
},
|
|
1001
1240
|
)
|
|
1002
|
-
|
|
1003
|
-
|
|
1241
|
+
expect(sourceOf(schema))->toEqual("fallback")
|
|
1242
|
+
},
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
testSync(
|
|
1246
|
+
"every built def states one",
|
|
1247
|
+
() => {
|
|
1248
|
+
let stated =
|
|
1249
|
+
Array.concat(structure.readModels, structure.stateViewSlices)->Array.every(
|
|
1250
|
+
q => q.labelFieldSource->Option.isSome,
|
|
1251
|
+
)
|
|
1252
|
+
expect(stated)->toEqual(true)
|
|
1253
|
+
},
|
|
1254
|
+
)
|
|
1004
1255
|
})
|
|
1005
1256
|
|
|
1006
1257
|
// The singular is published because it is not derivable from the plural without
|
|
@@ -1016,25 +1267,33 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1016
1267
|
|
|
1017
1268
|
// "strip a trailing `s`" would publish `TestPlugin_Categorie`, a field the
|
|
1018
1269
|
// generated SDL does not contain.
|
|
1019
|
-
testSync(
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1270
|
+
testSync(
|
|
1271
|
+
"an `-ies` plural publishes the `y` singular, not a truncation",
|
|
1272
|
+
() => {
|
|
1273
|
+
expect((categories.queryField, categories.singleQueryField))->toEqual((
|
|
1274
|
+
"TestPlugin_Categories",
|
|
1275
|
+
Some("TestPlugin_Category"),
|
|
1276
|
+
))
|
|
1277
|
+
},
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
testSync(
|
|
1281
|
+
"every built def states the name Api_Naming returned",
|
|
1282
|
+
() => {
|
|
1283
|
+
let stated =
|
|
1284
|
+
structure.stateViewSlices->Array.every(
|
|
1285
|
+
q =>
|
|
1286
|
+
q.singleQueryField ==
|
|
1287
|
+
Some(
|
|
1288
|
+
Api_Naming.queryFieldNamesForStateView(
|
|
1289
|
+
~plugin="TestPlugin",
|
|
1290
|
+
~viewName=q.name,
|
|
1291
|
+
).singleFieldName,
|
|
1292
|
+
),
|
|
1034
1293
|
)
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1294
|
+
expect(stated)->toEqual(true)
|
|
1295
|
+
},
|
|
1296
|
+
)
|
|
1038
1297
|
})
|
|
1039
1298
|
|
|
1040
1299
|
// Which field identifies a row, and whether the author said so or this side
|
|
@@ -1050,37 +1309,45 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1050
1309
|
|
|
1051
1310
|
// Orders holds `orderId` + `customerName`; the component name matches the
|
|
1052
1311
|
// former, so the rung is the one a client could also reach on its own.
|
|
1053
|
-
testSync(
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1312
|
+
testSync(
|
|
1313
|
+
"a name-matching `*Id` field is a guess the consumer can also make",
|
|
1314
|
+
() =>
|
|
1315
|
+
expect(keyOf(structure.stateViewSlices->Array.getUnsafe(0)))->toEqual((
|
|
1316
|
+
Some("orderId"),
|
|
1317
|
+
Some("convention"),
|
|
1318
|
+
)),
|
|
1058
1319
|
)
|
|
1059
1320
|
|
|
1060
1321
|
// AvailableProducts: the name would ask for `availableProductId`, which is not
|
|
1061
1322
|
// a field — `productId` wins because it is the only candidate there is.
|
|
1062
|
-
testSync(
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1323
|
+
testSync(
|
|
1324
|
+
"a sole `*Id` field is a fact only this side can see",
|
|
1325
|
+
() =>
|
|
1326
|
+
expect(keyOf(structure.stateViewSlices->Array.getUnsafe(1)))->toEqual((
|
|
1327
|
+
Some("productId"),
|
|
1328
|
+
Some("sole"),
|
|
1329
|
+
)),
|
|
1067
1330
|
)
|
|
1068
1331
|
|
|
1069
|
-
testSync(
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1332
|
+
testSync(
|
|
1333
|
+
"an `-ies` component name singularises to its key",
|
|
1334
|
+
() =>
|
|
1335
|
+
expect(keyOf(withCategories.stateViewSlices->Array.getUnsafe(0)))->toEqual((
|
|
1336
|
+
Some("categoryId"),
|
|
1337
|
+
Some("convention"),
|
|
1338
|
+
)),
|
|
1074
1339
|
)
|
|
1075
1340
|
|
|
1076
1341
|
// AnnotatedView declares `@id itemId` beside an `@index` field `ownerId`, and
|
|
1077
1342
|
// its name matches neither — inference alone would decline here, so reporting
|
|
1078
1343
|
// `itemId` proves the declaration produced it.
|
|
1079
|
-
testSync(
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1344
|
+
testSync(
|
|
1345
|
+
"a declared @id is used where inference would decline",
|
|
1346
|
+
() =>
|
|
1347
|
+
expect(keyOf(structure.stateViewSlices->Array.getUnsafe(3)))->toEqual((
|
|
1348
|
+
Some("itemId"),
|
|
1349
|
+
Some("annotation"),
|
|
1350
|
+
)),
|
|
1084
1351
|
)
|
|
1085
1352
|
})
|
|
1086
1353
|
|
|
@@ -1089,7 +1356,9 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1089
1356
|
Plugin_Structure.lifecycleFieldFromStateSchema(schema->S.castToUnknown)
|
|
1090
1357
|
|
|
1091
1358
|
let withLifecycle = (schema, ~field) =>
|
|
1092
|
-
schema
|
|
1359
|
+
schema
|
|
1360
|
+
->S.castToUnknown
|
|
1361
|
+
->S.Metadata.set(
|
|
1093
1362
|
~id=Reventless.StateAnnotations.stateAnnotationsId,
|
|
1094
1363
|
{
|
|
1095
1364
|
ids: [],
|
|
@@ -1114,63 +1383,85 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1114
1383
|
},
|
|
1115
1384
|
)
|
|
1116
1385
|
|
|
1117
|
-
testSync(
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1386
|
+
testSync(
|
|
1387
|
+
"a `lifecycle` field holding a closed set of values is the lifecycle field",
|
|
1388
|
+
() => {
|
|
1389
|
+
let schema = S.schema(
|
|
1390
|
+
s =>
|
|
1391
|
+
{
|
|
1392
|
+
"lifecycle": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
|
|
1393
|
+
},
|
|
1394
|
+
)
|
|
1395
|
+
expect(lifecycleOf(schema))->toEqual(Some("lifecycle"))
|
|
1396
|
+
},
|
|
1397
|
+
)
|
|
1125
1398
|
|
|
1126
|
-
testSync(
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1399
|
+
testSync(
|
|
1400
|
+
"an optional one counts too",
|
|
1401
|
+
() => {
|
|
1402
|
+
let schema = S.schema(
|
|
1403
|
+
s =>
|
|
1404
|
+
{
|
|
1405
|
+
"lifecycle": s.matches(
|
|
1406
|
+
S.option(S.union([S.literal("Placed"), S.literal("Shipped")])),
|
|
1407
|
+
),
|
|
1408
|
+
},
|
|
1409
|
+
)
|
|
1410
|
+
expect(lifecycleOf(schema))->toEqual(Some("lifecycle"))
|
|
1411
|
+
},
|
|
1412
|
+
)
|
|
1134
1413
|
|
|
1135
|
-
testSync(
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1414
|
+
testSync(
|
|
1415
|
+
"free text named `lifecycle` is not a lifecycle",
|
|
1416
|
+
() => {
|
|
1417
|
+
// `allowedStates` filtering needs states to compare against; a string
|
|
1418
|
+
// field named `lifecycle` gives a command menu nothing to match.
|
|
1419
|
+
let schema = S.schema(s => {"lifecycle": s.matches(S.string)})
|
|
1420
|
+
expect(lifecycleOf(schema))->toEqual(None)
|
|
1421
|
+
},
|
|
1422
|
+
)
|
|
1141
1423
|
|
|
1142
1424
|
// The one behaviour the rename deliberately changes, so it is asserted rather
|
|
1143
1425
|
// than assumed. `status` is a promiscuous name — geocoding progress, todo-queue
|
|
1144
1426
|
// progress, translation audit outcome — and a convention keyed on it guessed
|
|
1145
1427
|
// often. A record whose lifecycle really does live in a field called `status`
|
|
1146
1428
|
// says so with `@lifecycle`.
|
|
1147
|
-
testSync(
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1429
|
+
testSync(
|
|
1430
|
+
"an unannotated field named `status` resolves to None",
|
|
1431
|
+
() => {
|
|
1432
|
+
let schema = S.schema(
|
|
1433
|
+
s =>
|
|
1434
|
+
{
|
|
1435
|
+
"status": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
|
|
1436
|
+
},
|
|
1437
|
+
)
|
|
1438
|
+
expect(lifecycleOf(schema))->toEqual(None)
|
|
1439
|
+
},
|
|
1440
|
+
)
|
|
1155
1441
|
|
|
1156
|
-
testSync(
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1442
|
+
testSync(
|
|
1443
|
+
"the annotation names a field the convention would never reach",
|
|
1444
|
+
() => {
|
|
1445
|
+
let schema = S.schema(
|
|
1446
|
+
s =>
|
|
1447
|
+
{
|
|
1448
|
+
"locationStatus": s.matches(S.union([S.literal("Pending"), S.literal("Located")])),
|
|
1449
|
+
},
|
|
1450
|
+
)
|
|
1451
|
+
expect(lifecycleOf(schema->withLifecycle(~field="locationStatus")))->toEqual(
|
|
1452
|
+
Some("locationStatus"),
|
|
1453
|
+
)
|
|
1454
|
+
},
|
|
1455
|
+
)
|
|
1166
1456
|
})
|
|
1167
1457
|
|
|
1168
1458
|
describe("retiredField — annotation and nothing else", () => {
|
|
1169
|
-
let retiredOf = schema =>
|
|
1170
|
-
Plugin_Structure.retiredFieldFromStateSchema(schema->S.castToUnknown)
|
|
1459
|
+
let retiredOf = schema => Plugin_Structure.retiredFieldFromStateSchema(schema->S.castToUnknown)
|
|
1171
1460
|
|
|
1172
1461
|
let withRetired = (schema, ~field) =>
|
|
1173
|
-
schema
|
|
1462
|
+
schema
|
|
1463
|
+
->S.castToUnknown
|
|
1464
|
+
->S.Metadata.set(
|
|
1174
1465
|
~id=Reventless.StateAnnotations.stateAnnotationsId,
|
|
1175
1466
|
{
|
|
1176
1467
|
ids: [],
|
|
@@ -1191,84 +1482,108 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1191
1482
|
groupBy: None,
|
|
1192
1483
|
visibility: None,
|
|
1193
1484
|
live: None,
|
|
1194
|
-
retired: Some({
|
|
1485
|
+
retired: Some({
|
|
1486
|
+
field,
|
|
1487
|
+
label: "",
|
|
1488
|
+
showWhenFalse: false,
|
|
1489
|
+
values: None,
|
|
1490
|
+
namedWhenRetired: false,
|
|
1491
|
+
}),
|
|
1195
1492
|
},
|
|
1196
1493
|
)
|
|
1197
1494
|
|
|
1198
1495
|
// The state form. The value is published beside the field so a client holding
|
|
1199
1496
|
// the def holds the whole predicate — two places deriving one comparison is
|
|
1200
1497
|
// how they come to disagree about which rows a caller may see.
|
|
1201
|
-
testSync(
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1498
|
+
testSync(
|
|
1499
|
+
"publishes the retirement state beside the field",
|
|
1500
|
+
() => {
|
|
1501
|
+
let withState = schema =>
|
|
1502
|
+
schema
|
|
1503
|
+
->S.castToUnknown
|
|
1504
|
+
->S.Metadata.set(
|
|
1505
|
+
~id=Reventless.StateAnnotations.stateAnnotationsId,
|
|
1506
|
+
{
|
|
1507
|
+
ids: [],
|
|
1508
|
+
compositeIds: [],
|
|
1509
|
+
subIds: [],
|
|
1510
|
+
compositeSubIds: [],
|
|
1511
|
+
indexes: [],
|
|
1512
|
+
hidden: [],
|
|
1513
|
+
summary: [],
|
|
1514
|
+
drillTargets: [],
|
|
1515
|
+
drillTargetKeys: [],
|
|
1516
|
+
collapsed: [],
|
|
1517
|
+
scan: [],
|
|
1518
|
+
scanSort: [],
|
|
1519
|
+
semantic: [],
|
|
1520
|
+
metric: [],
|
|
1521
|
+
lifecycle: Some("accountStatus"),
|
|
1522
|
+
groupBy: None,
|
|
1523
|
+
visibility: None,
|
|
1524
|
+
live: None,
|
|
1525
|
+
retired: Some({
|
|
1526
|
+
field: "accountStatus",
|
|
1527
|
+
label: "",
|
|
1528
|
+
showWhenFalse: false,
|
|
1529
|
+
values: Some(["Deactivated", "Closed"]),
|
|
1530
|
+
namedWhenRetired: false,
|
|
1531
|
+
}),
|
|
1532
|
+
},
|
|
1533
|
+
)
|
|
1534
|
+
let schema = S.schema(s => {"accountStatus": s.matches(S.string)})->withState
|
|
1535
|
+
expect((
|
|
1536
|
+
Plugin_Structure.retiredFieldFromStateSchema(schema),
|
|
1537
|
+
Plugin_Structure.retiredValuesFromStateSchema(schema),
|
|
1538
|
+
))->toEqual((Some("accountStatus"), Some(["Deactivated", "Closed"])))
|
|
1539
|
+
},
|
|
1540
|
+
)
|
|
1239
1541
|
|
|
1240
1542
|
// Absent is what says "boolean form", so it has to stay tellable from a state
|
|
1241
1543
|
// form — including one naming nothing, which is `Some([])`.
|
|
1242
|
-
testSync(
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1544
|
+
testSync(
|
|
1545
|
+
"publishes no states on the boolean form",
|
|
1546
|
+
() => {
|
|
1547
|
+
let schema = S.schema(s => {"archived": s.matches(S.bool)})
|
|
1548
|
+
expect(
|
|
1549
|
+
Plugin_Structure.retiredValuesFromStateSchema(schema->withRetired(~field="archived")),
|
|
1550
|
+
)->toEqual(None)
|
|
1551
|
+
},
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
testSync(
|
|
1555
|
+
"names the annotated field",
|
|
1556
|
+
() => {
|
|
1557
|
+
let schema = S.schema(s => {"archived": s.matches(S.bool)})
|
|
1558
|
+
expect(retiredOf(schema->withRetired(~field="archived")))->toEqual(Some("archived"))
|
|
1559
|
+
},
|
|
1560
|
+
)
|
|
1253
1561
|
|
|
1254
1562
|
// The whole reason this has no convention rung. `lifecycleField` may still fall
|
|
1255
1563
|
// back to a field literally named `lifecycle` because guessing wrong there makes
|
|
1256
1564
|
// a command menu filter oddly; guessing wrong here makes rows disappear for every caller who is not
|
|
1257
1565
|
// elevated, so an unannotated boolean stays as visible as it was.
|
|
1258
|
-
testSync(
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1566
|
+
testSync(
|
|
1567
|
+
"declines a conventionally-named boolean nobody annotated",
|
|
1568
|
+
() => {
|
|
1569
|
+
let schema = S.schema(
|
|
1570
|
+
s =>
|
|
1571
|
+
{
|
|
1572
|
+
"archived": s.matches(S.bool),
|
|
1573
|
+
"deactivated": s.matches(S.bool),
|
|
1574
|
+
},
|
|
1575
|
+
)
|
|
1576
|
+
expect(retiredOf(schema))->toEqual(None)
|
|
1577
|
+
},
|
|
1578
|
+
)
|
|
1267
1579
|
|
|
1268
|
-
testSync(
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1580
|
+
testSync(
|
|
1581
|
+
"declines a schema carrying no annotation spec at all",
|
|
1582
|
+
() => {
|
|
1583
|
+
let schema = S.schema(s => {"id": s.matches(S.string)})
|
|
1584
|
+
expect(retiredOf(schema))->toEqual(None)
|
|
1585
|
+
},
|
|
1586
|
+
)
|
|
1272
1587
|
})
|
|
1273
1588
|
|
|
1274
1589
|
// A misspelled retirement state used to be fatal or merely logged depending on
|
|
@@ -1278,7 +1593,9 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1278
1593
|
// a build, so this rung was raised to match the other.
|
|
1279
1594
|
describe("@retired state names are checked against the field's own enum", () => {
|
|
1280
1595
|
let withRetiredStates = (schema, ~field, ~lifecycle, ~values) =>
|
|
1281
|
-
schema
|
|
1596
|
+
schema
|
|
1597
|
+
->S.castToUnknown
|
|
1598
|
+
->S.Metadata.set(
|
|
1282
1599
|
~id=Reventless.StateAnnotations.stateAnnotationsId,
|
|
1283
1600
|
{
|
|
1284
1601
|
ids: [],
|
|
@@ -1304,73 +1621,93 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1304
1621
|
)
|
|
1305
1622
|
|
|
1306
1623
|
let shelf = values =>
|
|
1307
|
-
S.schema(
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1624
|
+
S.schema(
|
|
1625
|
+
s =>
|
|
1626
|
+
{
|
|
1627
|
+
"shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
|
|
1628
|
+
},
|
|
1311
1629
|
)->withRetiredStates(~field="shelf", ~lifecycle=Some("shelf"), ~values=Some(values))
|
|
1312
1630
|
|
|
1313
1631
|
let check = schema => Plugin_Structure.checkRetiredValue(~entityName="Products", schema)
|
|
1314
1632
|
|
|
1315
|
-
testSync(
|
|
1316
|
-
|
|
1317
|
-
|
|
1633
|
+
testSync(
|
|
1634
|
+
"a declared state passes",
|
|
1635
|
+
() => {
|
|
1636
|
+
expect(check(shelf(["Archived"])))->toEqual(Plugin_Structure.Checked([]))
|
|
1637
|
+
},
|
|
1638
|
+
)
|
|
1318
1639
|
|
|
1319
1640
|
// Reported per state rather than as a set: one wrong entry among several
|
|
1320
1641
|
// still narrows something, so the symptom is a subset of rows leaking.
|
|
1321
|
-
testSync(
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1642
|
+
testSync(
|
|
1643
|
+
"each undeclared state is reported on its own",
|
|
1644
|
+
() => {
|
|
1645
|
+
switch check(shelf(["Archived", "Discontinud", "Withdrawn"])) {
|
|
1646
|
+
| Checked(failures) =>
|
|
1647
|
+
expect(failures->Array.length)->toBe(2)
|
|
1648
|
+
expect(failures->Array.some(f => f->String.includes("Discontinud")))->toBe(true)
|
|
1649
|
+
expect(failures->Array.some(f => f->String.includes("Withdrawn")))->toBe(true)
|
|
1650
|
+
// Naming what the field does declare is what turns the failure into a fix.
|
|
1651
|
+
expect(failures->Array.every(f => f->String.includes("Listed, Archived")))->toBe(true)
|
|
1652
|
+
| other => expect(other)->toEqual(Plugin_Structure.Checked([]))
|
|
1653
|
+
}
|
|
1654
|
+
},
|
|
1655
|
+
)
|
|
1332
1656
|
|
|
1333
1657
|
// The condition that came with promoting this: a fatal rule that is silent
|
|
1334
1658
|
// when it cannot run looks exactly like one that passed.
|
|
1335
|
-
testSync(
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1659
|
+
testSync(
|
|
1660
|
+
"a field with no cases to compare against says so instead of passing",
|
|
1661
|
+
() => {
|
|
1662
|
+
let schema =
|
|
1663
|
+
S.schema(s => {"shelf": s.matches(S.string)})->withRetiredStates(
|
|
1664
|
+
~field="shelf",
|
|
1665
|
+
~lifecycle=Some("shelf"),
|
|
1666
|
+
~values=Some(["Archived"]),
|
|
1667
|
+
)
|
|
1668
|
+
switch check(schema) {
|
|
1669
|
+
| Unchecked(why) => expect(why->String.includes("no cases"))->toBe(true)
|
|
1670
|
+
| other => expect(other)->toEqual(Plugin_Structure.Unchecked(""))
|
|
1671
|
+
}
|
|
1672
|
+
},
|
|
1673
|
+
)
|
|
1347
1674
|
|
|
1348
1675
|
// The boolean form names no state, so there is nothing to compare — which is
|
|
1349
1676
|
// not the same as having failed to compare it.
|
|
1350
|
-
testSync(
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1677
|
+
testSync(
|
|
1678
|
+
"the boolean form is not a skip",
|
|
1679
|
+
() => {
|
|
1680
|
+
let schema =
|
|
1681
|
+
S.schema(s => {"archived": s.matches(S.bool)})->withRetiredStates(
|
|
1682
|
+
~field="archived",
|
|
1683
|
+
~lifecycle=None,
|
|
1684
|
+
~values=None,
|
|
1685
|
+
)
|
|
1686
|
+
expect(Plugin_Structure.checkRetiredValue(~entityName="Products", schema))->toEqual(
|
|
1687
|
+
Plugin_Structure.NotDeclared,
|
|
1356
1688
|
)
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
)
|
|
1360
|
-
})
|
|
1689
|
+
},
|
|
1690
|
+
)
|
|
1361
1691
|
|
|
1362
1692
|
// The second rule stays a warning: retirement still works, and which field
|
|
1363
1693
|
// carries it is a modelling judgement rather than a name that is wrong.
|
|
1364
|
-
testSync(
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1694
|
+
testSync(
|
|
1695
|
+
"a state on a non-lifecycle field is not a failure",
|
|
1696
|
+
() => {
|
|
1697
|
+
let schema = S.schema(
|
|
1698
|
+
s =>
|
|
1699
|
+
{
|
|
1700
|
+
"shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
|
|
1701
|
+
"lifecycle": s.matches(S.union([S.literal("Draft"), S.literal("Live")])),
|
|
1702
|
+
},
|
|
1703
|
+
)->withRetiredStates(
|
|
1704
|
+
~field="shelf",
|
|
1705
|
+
~lifecycle=Some("lifecycle"),
|
|
1706
|
+
~values=Some(["Archived"]),
|
|
1707
|
+
)
|
|
1708
|
+
expect(check(schema))->toEqual(Plugin_Structure.Checked([]))
|
|
1709
|
+
},
|
|
1710
|
+
)
|
|
1374
1711
|
})
|
|
1375
1712
|
|
|
1376
1713
|
// Shared by the read-side and write-side schema cases below, which ask the
|
|
@@ -1395,126 +1732,165 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1395
1732
|
// annotated state types. Plugin_Structure now uses SuryToJsonSchema.deriveObjectSchema
|
|
1396
1733
|
// (annotation-aware) instead of S.toJSONSchema (metadata-blind).
|
|
1397
1734
|
describe("queryableDef.schema propagates x-reventless-* annotations", () => {
|
|
1398
|
-
let parseSchema = (svs: Reventless.Plugin.queryableDef): JSON.t =>
|
|
1399
|
-
svs.schema->JSON.parseOrThrow
|
|
1735
|
+
let parseSchema = (svs: Reventless.Plugin.queryableDef): JSON.t => svs.schema->JSON.parseOrThrow
|
|
1400
1736
|
|
|
1401
1737
|
let annotatedSchema = structure.stateViewSlices->Array.getUnsafe(3)->parseSchema
|
|
1402
1738
|
|
|
1403
|
-
testSync(
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
->
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
testSync(
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1739
|
+
testSync(
|
|
1740
|
+
"itemId carries x-reventless-id",
|
|
1741
|
+
() => {
|
|
1742
|
+
expect(
|
|
1743
|
+
annotatedSchema
|
|
1744
|
+
->getPropertyOf("itemId")
|
|
1745
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-id"))
|
|
1746
|
+
->Option.flatMap(JSON.Decode.bool),
|
|
1747
|
+
)->toBe(Some(true))
|
|
1748
|
+
},
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
testSync(
|
|
1752
|
+
"version carries x-reventless-subId",
|
|
1753
|
+
() => {
|
|
1754
|
+
expect(
|
|
1755
|
+
annotatedSchema
|
|
1756
|
+
->getPropertyOf("version")
|
|
1757
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-subId"))
|
|
1758
|
+
->Option.flatMap(JSON.Decode.bool),
|
|
1759
|
+
)->toBe(Some(true))
|
|
1760
|
+
},
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
testSync(
|
|
1764
|
+
"ownerId carries x-reventless-index = byOwner",
|
|
1765
|
+
() => {
|
|
1766
|
+
expect(
|
|
1767
|
+
annotatedSchema
|
|
1768
|
+
->getPropertyOf("ownerId")
|
|
1769
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-index"))
|
|
1770
|
+
->Option.flatMap(JSON.Decode.string),
|
|
1771
|
+
)->toBe(Some("byOwner"))
|
|
1772
|
+
},
|
|
1773
|
+
)
|
|
1774
|
+
|
|
1775
|
+
testSync(
|
|
1776
|
+
"@semantic(\"currency\") flows through the PPX to x-reventless-semantic",
|
|
1777
|
+
() => {
|
|
1778
|
+
expect(
|
|
1779
|
+
annotatedSchema
|
|
1780
|
+
->getPropertyOf("total")
|
|
1781
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
|
|
1782
|
+
->Option.flatMap(JSON.Decode.string),
|
|
1783
|
+
)->toBe(Some("currency"))
|
|
1784
|
+
},
|
|
1785
|
+
)
|
|
1786
|
+
|
|
1787
|
+
testSync(
|
|
1788
|
+
"@sensitive flows through the PPX to x-reventless-sensitive",
|
|
1789
|
+
() => {
|
|
1790
|
+
expect(
|
|
1791
|
+
annotatedSchema
|
|
1792
|
+
->getPropertyOf("contact")
|
|
1793
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-sensitive"))
|
|
1794
|
+
->Option.flatMap(JSON.Decode.bool),
|
|
1795
|
+
)->toBe(Some(true))
|
|
1796
|
+
},
|
|
1797
|
+
)
|
|
1447
1798
|
|
|
1448
1799
|
// The case the marker exists for. A renderer composes from an event payload,
|
|
1449
1800
|
// and the annotation path `deriveObjectSchema` reads for state is
|
|
1450
1801
|
// state-only — so a marker that lived there would be absent exactly where
|
|
1451
1802
|
// the value is about to be interpolated into a message.
|
|
1452
|
-
testSync(
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1803
|
+
testSync(
|
|
1804
|
+
"@sensitive reaches an event variant's payload, not only state",
|
|
1805
|
+
() =>
|
|
1806
|
+
expect(
|
|
1807
|
+
Reventless.Sensitive.variantFieldNames(
|
|
1808
|
+
PsAnnotatedView.consumedEventSchema->S.castToUnknown,
|
|
1809
|
+
~variant="ItemRecorded",
|
|
1810
|
+
),
|
|
1811
|
+
)->toEqual(["contact"]),
|
|
1459
1812
|
)
|
|
1460
1813
|
|
|
1461
1814
|
// JSON Schema's own keyword rather than an `x-reventless-` one, so a client
|
|
1462
1815
|
// that knows nothing about Reventless reads it.
|
|
1463
|
-
testSync(
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
annotatedSchema
|
|
1474
|
-
->getPropertyOf("quantity")
|
|
1475
|
-
->Option.flatMap(s => getProperty(s, "type"))
|
|
1476
|
-
->Option.flatMap(JSON.Decode.string),
|
|
1477
|
-
)->toBe(Some("integer"))
|
|
1478
|
-
})
|
|
1479
|
-
|
|
1480
|
-
testSync("@metric flows through the PPX to x-reventless-metric {aggregate,label}", () => {
|
|
1481
|
-
let metricObj =
|
|
1482
|
-
annotatedSchema
|
|
1483
|
-
->getPropertyOf("total")
|
|
1484
|
-
->Option.flatMap(s => getProperty(s, "x-reventless-metric"))
|
|
1485
|
-
expect((
|
|
1486
|
-
metricObj->Option.flatMap(m => getProperty(m, "aggregate"))->Option.flatMap(JSON.Decode.string),
|
|
1487
|
-
metricObj->Option.flatMap(m => getProperty(m, "label"))->Option.flatMap(JSON.Decode.string),
|
|
1488
|
-
))->toEqual((Some("sum"), Some("Revenue")))
|
|
1489
|
-
})
|
|
1816
|
+
testSync(
|
|
1817
|
+
"@default flows through the PPX to JSON Schema's `default`",
|
|
1818
|
+
() => {
|
|
1819
|
+
expect(
|
|
1820
|
+
annotatedSchema
|
|
1821
|
+
->getPropertyOf("quantity")
|
|
1822
|
+
->Option.flatMap(s => getProperty(s, "default")),
|
|
1823
|
+
)->toEqual(Some(JSON.Encode.int(1)))
|
|
1824
|
+
},
|
|
1825
|
+
)
|
|
1490
1826
|
|
|
1491
|
-
testSync(
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1827
|
+
testSync(
|
|
1828
|
+
"@default leaves the field's own shape alone",
|
|
1829
|
+
() => {
|
|
1830
|
+
expect(
|
|
1831
|
+
annotatedSchema
|
|
1832
|
+
->getPropertyOf("quantity")
|
|
1833
|
+
->Option.flatMap(s => getProperty(s, "type"))
|
|
1834
|
+
->Option.flatMap(JSON.Decode.string),
|
|
1835
|
+
)->toBe(Some("integer"))
|
|
1836
|
+
},
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
testSync(
|
|
1840
|
+
"@metric flows through the PPX to x-reventless-metric {aggregate,label}",
|
|
1841
|
+
() => {
|
|
1842
|
+
let metricObj =
|
|
1843
|
+
annotatedSchema
|
|
1844
|
+
->getPropertyOf("total")
|
|
1845
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-metric"))
|
|
1846
|
+
expect((
|
|
1847
|
+
metricObj
|
|
1848
|
+
->Option.flatMap(m => getProperty(m, "aggregate"))
|
|
1849
|
+
->Option.flatMap(JSON.Decode.string),
|
|
1850
|
+
metricObj
|
|
1851
|
+
->Option.flatMap(m => getProperty(m, "label"))
|
|
1852
|
+
->Option.flatMap(JSON.Decode.string),
|
|
1853
|
+
))->toEqual((Some("sum"), Some("Revenue")))
|
|
1854
|
+
},
|
|
1855
|
+
)
|
|
1856
|
+
|
|
1857
|
+
testSync(
|
|
1858
|
+
"unannotated field 'name' has no x-reventless-* keys",
|
|
1859
|
+
() => {
|
|
1860
|
+
let nameField = annotatedSchema->getPropertyOf("name")
|
|
1861
|
+
expect((
|
|
1862
|
+
nameField->Option.flatMap(s => getProperty(s, "x-reventless-id")),
|
|
1863
|
+
nameField->Option.flatMap(s => getProperty(s, "x-reventless-subId")),
|
|
1864
|
+
nameField->Option.flatMap(s => getProperty(s, "x-reventless-index")),
|
|
1865
|
+
))->toEqual((None, None, None))
|
|
1866
|
+
},
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
testSync(
|
|
1870
|
+
"unannotated state-view slice (Orders) emits no x-reventless-* keys",
|
|
1871
|
+
() => {
|
|
1872
|
+
let ordersSchema = structure.stateViewSlices->Array.getUnsafe(0)->parseSchema
|
|
1873
|
+
let orderIdField = ordersSchema->getPropertyOf("orderId")
|
|
1874
|
+
expect(orderIdField->Option.flatMap(s => getProperty(s, "x-reventless-id")))->toBe(None)
|
|
1875
|
+
},
|
|
1876
|
+
)
|
|
1877
|
+
|
|
1878
|
+
testSync(
|
|
1879
|
+
"@live(false) flows through the PPX to top-level x-reventless-live",
|
|
1880
|
+
() => {
|
|
1881
|
+
expect(
|
|
1882
|
+
getProperty(annotatedSchema, "x-reventless-live")->Option.flatMap(JSON.Decode.bool),
|
|
1883
|
+
)->toBe(Some(false))
|
|
1884
|
+
},
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
testSync(
|
|
1888
|
+
"unannotated state-view slice (Orders) has no top-level x-reventless-live",
|
|
1889
|
+
() => {
|
|
1890
|
+
let ordersSchema = structure.stateViewSlices->Array.getUnsafe(0)->parseSchema
|
|
1891
|
+
expect(getProperty(ordersSchema, "x-reventless-live"))->toBe(None)
|
|
1892
|
+
},
|
|
1893
|
+
)
|
|
1518
1894
|
})
|
|
1519
1895
|
|
|
1520
1896
|
// The generator captures each component's chapter (source-folder grouping band)
|
|
@@ -1530,25 +1906,37 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1530
1906
|
~componentChapters=Dict.fromArray([("PlaceOrder", "Order"), ("Orders", "Order")]),
|
|
1531
1907
|
)
|
|
1532
1908
|
|
|
1533
|
-
testSync(
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1909
|
+
testSync(
|
|
1910
|
+
"a chaptered state-change slice carries Some(chapter)",
|
|
1911
|
+
() => {
|
|
1912
|
+
let placeOrder = chaptered.stateChangeSlices->Array.getUnsafe(0)
|
|
1913
|
+
expect(placeOrder.chapter)->toEqual(Some("Order"))
|
|
1914
|
+
},
|
|
1915
|
+
)
|
|
1537
1916
|
|
|
1538
|
-
testSync(
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1917
|
+
testSync(
|
|
1918
|
+
"a chaptered state-view slice carries Some(chapter)",
|
|
1919
|
+
() => {
|
|
1920
|
+
let orders = chaptered.stateViewSlices->Array.getUnsafe(0)
|
|
1921
|
+
expect(orders.chapter)->toEqual(Some("Order"))
|
|
1922
|
+
},
|
|
1923
|
+
)
|
|
1542
1924
|
|
|
1543
|
-
testSync(
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1925
|
+
testSync(
|
|
1926
|
+
"a component absent from the map carries None",
|
|
1927
|
+
() => {
|
|
1928
|
+
let shipOrder = chaptered.stateChangeSlices->Array.getUnsafe(1)
|
|
1929
|
+
expect(shipOrder.chapter)->toEqual(None)
|
|
1930
|
+
},
|
|
1931
|
+
)
|
|
1547
1932
|
|
|
1548
|
-
testSync(
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1933
|
+
testSync(
|
|
1934
|
+
"no ~componentChapters → every def is chapterless (None)",
|
|
1935
|
+
() => {
|
|
1936
|
+
let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
1937
|
+
expect(placeOrder.chapter)->toEqual(None)
|
|
1938
|
+
},
|
|
1939
|
+
)
|
|
1552
1940
|
})
|
|
1553
1941
|
|
|
1554
1942
|
// A field typed as a storage ref states that the deployment needs that store
|
|
@@ -1561,120 +1949,152 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1561
1949
|
)
|
|
1562
1950
|
let stores = withStores.requiredStores->Option.getOr([])
|
|
1563
1951
|
|
|
1564
|
-
testSync(
|
|
1565
|
-
|
|
1952
|
+
testSync(
|
|
1953
|
+
"an unqualified store resolves against the declaring plugin",
|
|
1954
|
+
() => expect(stores->Array.includes("TestPlugin.documents"))->toBe(true),
|
|
1566
1955
|
)
|
|
1567
1956
|
|
|
1568
|
-
testSync(
|
|
1569
|
-
|
|
1957
|
+
testSync(
|
|
1958
|
+
"a qualified store keeps its own plugin",
|
|
1959
|
+
() => expect(stores->Array.includes("branding.logos"))->toBe(true),
|
|
1570
1960
|
)
|
|
1571
1961
|
|
|
1572
1962
|
// `documents` is declared on both a command field and an event field.
|
|
1573
|
-
testSync(
|
|
1574
|
-
|
|
1963
|
+
testSync(
|
|
1964
|
+
"a store declared by several fields is collected once",
|
|
1965
|
+
() => expect(stores->Array.filter(s => s == "TestPlugin.documents")->Array.length)->toBe(1),
|
|
1575
1966
|
)
|
|
1576
1967
|
|
|
1577
|
-
testSync("only the declared stores are collected", () =>
|
|
1578
|
-
expect(stores->Array.length)->toBe(2)
|
|
1579
|
-
)
|
|
1968
|
+
testSync("only the declared stores are collected", () => expect(stores->Array.length)->toBe(2))
|
|
1580
1969
|
|
|
1581
|
-
testSync(
|
|
1582
|
-
|
|
1970
|
+
testSync(
|
|
1971
|
+
"a plugin declaring no stores collects none — not None",
|
|
1972
|
+
() => expect(structure.requiredStores)->toEqual(Some([])),
|
|
1583
1973
|
)
|
|
1584
1974
|
|
|
1585
|
-
testSync(
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1975
|
+
testSync(
|
|
1976
|
+
"each requirement keeps its declaring (component, field) sites",
|
|
1977
|
+
() => {
|
|
1978
|
+
// `documents` is declared on a command field AND an event field of the
|
|
1979
|
+
// same component under the same field name — one declaration site, so
|
|
1980
|
+
// the identical triples collapse to one entry.
|
|
1981
|
+
// `annotation` is the source text, not the key: the own-plugin store is
|
|
1982
|
+
// written bare and the foreign one qualified, which is exactly the
|
|
1983
|
+
// distinction no downstream comparison can recover.
|
|
1984
|
+
let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
|
|
1985
|
+
{
|
|
1986
|
+
store: "TestPlugin.documents",
|
|
1987
|
+
component: "AttachInvoice",
|
|
1988
|
+
field: "documentUrl",
|
|
1989
|
+
annotation: Some("documents"),
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
store: "branding.logos",
|
|
1993
|
+
component: "AttachInvoice",
|
|
1994
|
+
field: "logoUrl",
|
|
1995
|
+
annotation: Some("branding.logos"),
|
|
1996
|
+
},
|
|
1997
|
+
]
|
|
1998
|
+
expect(withStores.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
1999
|
+
},
|
|
2000
|
+
)
|
|
1608
2001
|
|
|
1609
2002
|
// The manifest is a rendering of the structure. Its key set must be
|
|
1610
2003
|
// byte-identical to what the deployed plugin reports as `requiredStores`,
|
|
1611
2004
|
// because the deploy-time coverage assertion compares against exactly that
|
|
1612
2005
|
// — this equality is the whole contract of `capabilities.json`.
|
|
1613
|
-
describe(
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2006
|
+
describe(
|
|
2007
|
+
"capability manifest",
|
|
2008
|
+
() => {
|
|
2009
|
+
let manifest = Reventless.CapabilityManifest.fromStructure(withStores)
|
|
2010
|
+
|
|
2011
|
+
testSync(
|
|
2012
|
+
"keys are byte-identical to requiredStores",
|
|
2013
|
+
() =>
|
|
2014
|
+
expect(manifest.capabilities->Array.map(c => c.key))->toEqual(
|
|
2015
|
+
withStores.requiredStores->Option.getOr([]),
|
|
2016
|
+
),
|
|
1619
2017
|
)
|
|
1620
|
-
)
|
|
1621
|
-
|
|
1622
|
-
testSync("each entry carries its provenance", () => {
|
|
1623
|
-
let entry =
|
|
1624
|
-
manifest.capabilities->Array.find(c => c.key == "TestPlugin.documents")->Option.getOrThrow
|
|
1625
|
-
let expected: array<Reventless.CapabilityManifest.provenance> = [
|
|
1626
|
-
{component: "AttachInvoice", field: "documentUrl", annotation: "documents"},
|
|
1627
|
-
]
|
|
1628
|
-
expect(entry.declaredBy)->toEqual(expected)
|
|
1629
|
-
})
|
|
1630
|
-
|
|
1631
|
-
testSync("a structure declaring nothing yields an empty list, not a missing manifest", () =>
|
|
1632
|
-
expect(Reventless.CapabilityManifest.fromStructure(structure).capabilities)->toEqual([])
|
|
1633
|
-
)
|
|
1634
|
-
|
|
1635
|
-
// A capability a slice declares rides the same manifest as the stores, so
|
|
1636
|
-
// the platform generator reads one file. It carries no `field`: nothing
|
|
1637
|
-
// annotated it, and naming one would be a fiction.
|
|
1638
|
-
describe("declared capabilities", () => {
|
|
1639
|
-
let withCapability = {
|
|
1640
|
-
...structure,
|
|
1641
|
-
requiredCapabilities: Some([
|
|
1642
|
-
({capability: "Geocoding", component: "GeocodeAddress"}: Reventless.Plugin.requiredCapabilityDeclaration),
|
|
1643
|
-
{capability: "Geocoding", component: "GeocodeDropOff"},
|
|
1644
|
-
]),
|
|
1645
|
-
traitDeclarations: None,
|
|
1646
|
-
}
|
|
1647
|
-
let entries = Reventless.CapabilityManifest.fromStructure(withCapability).capabilities
|
|
1648
2018
|
|
|
1649
|
-
testSync(
|
|
1650
|
-
|
|
2019
|
+
testSync(
|
|
2020
|
+
"each entry carries its provenance",
|
|
2021
|
+
() => {
|
|
2022
|
+
let entry =
|
|
2023
|
+
manifest.capabilities
|
|
2024
|
+
->Array.find(c => c.key == "TestPlugin.documents")
|
|
2025
|
+
->Option.getOrThrow
|
|
2026
|
+
let expected: array<Reventless.CapabilityManifest.provenance> = [
|
|
2027
|
+
{component: "AttachInvoice", field: "documentUrl", annotation: "documents"},
|
|
2028
|
+
]
|
|
2029
|
+
expect(entry.declaredBy)->toEqual(expected)
|
|
2030
|
+
},
|
|
1651
2031
|
)
|
|
1652
2032
|
|
|
1653
|
-
testSync(
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2033
|
+
testSync(
|
|
2034
|
+
"a structure declaring nothing yields an empty list, not a missing manifest",
|
|
2035
|
+
() =>
|
|
2036
|
+
expect(
|
|
2037
|
+
Reventless.CapabilityManifest.fromStructure(structure).capabilities,
|
|
2038
|
+
)->toEqual([]),
|
|
1658
2039
|
)
|
|
1659
2040
|
|
|
1660
|
-
//
|
|
1661
|
-
//
|
|
1662
|
-
//
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2041
|
+
// A capability a slice declares rides the same manifest as the stores, so
|
|
2042
|
+
// the platform generator reads one file. It carries no `field`: nothing
|
|
2043
|
+
// annotated it, and naming one would be a fiction.
|
|
2044
|
+
describe(
|
|
2045
|
+
"declared capabilities",
|
|
2046
|
+
() => {
|
|
2047
|
+
let withCapability = {
|
|
1666
2048
|
...structure,
|
|
1667
|
-
requiredCapabilities: Some([
|
|
2049
|
+
requiredCapabilities: Some([
|
|
2050
|
+
(
|
|
2051
|
+
{
|
|
2052
|
+
capability: "Geocoding",
|
|
2053
|
+
component: "GeocodeAddress",
|
|
2054
|
+
}: Reventless.Plugin.requiredCapabilityDeclaration
|
|
2055
|
+
),
|
|
2056
|
+
{capability: "Geocoding", component: "GeocodeDropOff"},
|
|
2057
|
+
]),
|
|
1668
2058
|
traitDeclarations: None,
|
|
1669
|
-
}
|
|
1670
|
-
|
|
2059
|
+
}
|
|
2060
|
+
let entries = Reventless.CapabilityManifest.fromStructure(withCapability).capabilities
|
|
2061
|
+
|
|
2062
|
+
testSync(
|
|
2063
|
+
"two components needing one capability yield one entry",
|
|
2064
|
+
() => expect(entries->Array.map(e => e.key))->toEqual(["Geocoding"]),
|
|
2065
|
+
)
|
|
2066
|
+
|
|
2067
|
+
testSync(
|
|
2068
|
+
"both declaring components are kept, with no field",
|
|
2069
|
+
() =>
|
|
2070
|
+
expect((entries->Array.getUnsafe(0)).declaredBy)->toEqual([
|
|
2071
|
+
({component: "GeocodeAddress"}: Reventless.CapabilityManifest.provenance),
|
|
2072
|
+
{component: "GeocodeDropOff"},
|
|
2073
|
+
]),
|
|
2074
|
+
)
|
|
2075
|
+
|
|
2076
|
+
// The generator downstream renders a real `Platform.capability` arm, and
|
|
2077
|
+
// a name this build cannot map has no arm to render — so it is dropped
|
|
2078
|
+
// here rather than passed through as an unrenderable key.
|
|
2079
|
+
testSync(
|
|
2080
|
+
"a capability this build does not know is dropped",
|
|
2081
|
+
() =>
|
|
2082
|
+
expect(
|
|
2083
|
+
Reventless.CapabilityManifest.fromStructure({
|
|
2084
|
+
...structure,
|
|
2085
|
+
requiredCapabilities: Some([{capability: "Telepathy", component: "Guess"}]),
|
|
2086
|
+
traitDeclarations: None,
|
|
2087
|
+
}).capabilities,
|
|
2088
|
+
)->toEqual([]),
|
|
2089
|
+
)
|
|
2090
|
+
},
|
|
1671
2091
|
)
|
|
1672
|
-
})
|
|
1673
2092
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2093
|
+
testSync(
|
|
2094
|
+
"rendering is deterministic and newline-terminated",
|
|
2095
|
+
() => {
|
|
2096
|
+
let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
|
|
2097
|
+
expect(rendered)->toBe(`{
|
|
1678
2098
|
"capabilities": [
|
|
1679
2099
|
{
|
|
1680
2100
|
"kind": "ObjectStore",
|
|
@@ -1700,10 +2120,11 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1700
2120
|
}
|
|
1701
2121
|
]
|
|
1702
2122
|
}
|
|
1703
|
-
|
|
2123
|
+
`)
|
|
2124
|
+
},
|
|
1704
2125
|
)
|
|
1705
|
-
}
|
|
1706
|
-
|
|
2126
|
+
},
|
|
2127
|
+
)
|
|
1707
2128
|
})
|
|
1708
2129
|
|
|
1709
2130
|
// A field typed uploadable declares its store by being named. The collection
|
|
@@ -1717,23 +2138,27 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1717
2138
|
)
|
|
1718
2139
|
let stores = withUploads.requiredStores->Option.getOr([])
|
|
1719
2140
|
|
|
1720
|
-
testSync(
|
|
1721
|
-
|
|
2141
|
+
testSync(
|
|
2142
|
+
"the derived store is the field name, pluralised",
|
|
2143
|
+
() => expect(stores->Array.includes("TestPlugin.productImages"))->toBe(true),
|
|
1722
2144
|
)
|
|
1723
2145
|
|
|
1724
|
-
testSync(
|
|
1725
|
-
|
|
2146
|
+
testSync(
|
|
2147
|
+
"an optional field still declares its store",
|
|
2148
|
+
() => expect(stores->Array.includes("TestPlugin.categoryImages"))->toBe(true),
|
|
1726
2149
|
)
|
|
1727
2150
|
|
|
1728
2151
|
// The array marker lives on the element type. Reading the field schema
|
|
1729
2152
|
// directly answers `None`, which is the same silence as never declaring —
|
|
1730
2153
|
// so this asserts collection, not just that the fixture compiled.
|
|
1731
|
-
testSync(
|
|
1732
|
-
|
|
2154
|
+
testSync(
|
|
2155
|
+
"an array field declares through its element",
|
|
2156
|
+
() => expect(stores->Array.includes("TestPlugin.datasheets"))->toBe(true),
|
|
1733
2157
|
)
|
|
1734
2158
|
|
|
1735
|
-
testSync(
|
|
1736
|
-
|
|
2159
|
+
testSync(
|
|
2160
|
+
"an explicit @storageRef overrides the derived name",
|
|
2161
|
+
() => expect(stores->Array.includes("branding.logos"))->toBe(true),
|
|
1737
2162
|
)
|
|
1738
2163
|
|
|
1739
2164
|
testSync("nothing else is collected", () => expect(stores->Array.length)->toBe(4))
|
|
@@ -1741,39 +2166,43 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1741
2166
|
// The event field derives the same store as the command field of the same
|
|
1742
2167
|
// name, so the two collapse to one declaration site — the behaviour the
|
|
1743
2168
|
// annotation form already had, reached without an annotation.
|
|
1744
|
-
testSync(
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2169
|
+
testSync(
|
|
2170
|
+
"each requirement keeps its declaring sites",
|
|
2171
|
+
() => {
|
|
2172
|
+
let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
|
|
2173
|
+
{
|
|
2174
|
+
store: "TestPlugin.categoryImages",
|
|
2175
|
+
component: "UploadImages",
|
|
2176
|
+
field: "categoryImage",
|
|
2177
|
+
annotation: Some("categoryImages"),
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
store: "TestPlugin.datasheets",
|
|
2181
|
+
component: "UploadImages",
|
|
2182
|
+
field: "datasheets",
|
|
2183
|
+
annotation: Some("datasheets"),
|
|
2184
|
+
},
|
|
2185
|
+
{
|
|
2186
|
+
store: "TestPlugin.productImages",
|
|
2187
|
+
component: "UploadImages",
|
|
2188
|
+
field: "productImage",
|
|
2189
|
+
annotation: Some("productImages"),
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
store: "branding.logos",
|
|
2193
|
+
component: "UploadImages",
|
|
2194
|
+
field: "logo",
|
|
2195
|
+
annotation: Some("branding.logos"),
|
|
2196
|
+
},
|
|
2197
|
+
]
|
|
2198
|
+
expect(withUploads.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
2199
|
+
},
|
|
2200
|
+
)
|
|
1773
2201
|
|
|
1774
2202
|
let commandJson =
|
|
1775
|
-
(
|
|
1776
|
-
|
|
2203
|
+
(
|
|
2204
|
+
(withUploads.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
|
|
2205
|
+
).schema->JSON.parseOrThrow
|
|
1777
2206
|
|
|
1778
2207
|
let semanticOf = field =>
|
|
1779
2208
|
commandJson
|
|
@@ -1783,24 +2212,28 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1783
2212
|
|
|
1784
2213
|
// The whole point of the family: one declaration states the content type
|
|
1785
2214
|
// too, so the UI stops learning "this is an image" from the field's name.
|
|
1786
|
-
testSync(
|
|
1787
|
-
|
|
2215
|
+
testSync(
|
|
2216
|
+
"the wire carries the content half, not just the store",
|
|
2217
|
+
() => expect(semanticOf("productImage"))->toBe(Some("uploadableImage")),
|
|
1788
2218
|
)
|
|
1789
2219
|
|
|
1790
2220
|
// The override replaces the *store*, not the type's semantic: a field
|
|
1791
2221
|
// pointing at another plugin's store is still an image.
|
|
1792
|
-
testSync(
|
|
1793
|
-
|
|
2222
|
+
testSync(
|
|
2223
|
+
"an overridden field keeps its uploadable semantic",
|
|
2224
|
+
() => expect(semanticOf("logo"))->toBe(Some("uploadableImage")),
|
|
1794
2225
|
)
|
|
1795
2226
|
|
|
1796
|
-
testSync(
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2227
|
+
testSync(
|
|
2228
|
+
"the non-image arm carries its own id, on the element",
|
|
2229
|
+
() =>
|
|
2230
|
+
expect(
|
|
2231
|
+
commandJson
|
|
2232
|
+
->getPropertyOf("datasheets")
|
|
2233
|
+
->Option.flatMap(s => getProperty(s, "items"))
|
|
2234
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
|
|
2235
|
+
->Option.flatMap(JSON.Decode.string),
|
|
2236
|
+
)->toBe(Some("uploadableFile")),
|
|
1804
2237
|
)
|
|
1805
2238
|
})
|
|
1806
2239
|
|
|
@@ -1819,49 +2252,58 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1819
2252
|
| _ => "(not a JS error)"
|
|
1820
2253
|
}
|
|
1821
2254
|
|
|
1822
|
-
testSync(
|
|
1823
|
-
|
|
2255
|
+
testSync(
|
|
2256
|
+
"two stores one transposition apart fail the build",
|
|
2257
|
+
() => expect(failureText() != "")->toBe(true),
|
|
1824
2258
|
)
|
|
1825
2259
|
|
|
1826
|
-
testSync(
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
text
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2260
|
+
testSync(
|
|
2261
|
+
"the failure names both stores and both fields",
|
|
2262
|
+
() => {
|
|
2263
|
+
let text = failureText()
|
|
2264
|
+
expect([
|
|
2265
|
+
text->String.includes("TestPlugin.productImages"),
|
|
2266
|
+
text->String.includes("TestPlugin.productImgaes"),
|
|
2267
|
+
text->String.includes("TypoStore.productImage"),
|
|
2268
|
+
text->String.includes("@storageRef"),
|
|
2269
|
+
])->toEqual([true, true, true, true])
|
|
2270
|
+
},
|
|
2271
|
+
)
|
|
1835
2272
|
|
|
1836
2273
|
// Every legitimate pair in the other fixtures has to stay legitimate — a
|
|
1837
2274
|
// check that fails on real designs is worse than the typo it catches.
|
|
1838
|
-
testSync(
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2275
|
+
testSync(
|
|
2276
|
+
"the four stores of the uploadable fixture do not collide",
|
|
2277
|
+
() =>
|
|
2278
|
+
expect(
|
|
2279
|
+
Capability_Inference.collisions(
|
|
2280
|
+
Plugin_Structure.make(
|
|
2281
|
+
~name="TestPlugin",
|
|
2282
|
+
~stateChangeSlices=[module(PsUploadImagesSlice)],
|
|
2283
|
+
).requiredStoreDeclarations->Option.getOr([]),
|
|
2284
|
+
),
|
|
2285
|
+
)->toEqual([]),
|
|
2286
|
+
)
|
|
2287
|
+
|
|
2288
|
+
testSync(
|
|
2289
|
+
"one edit, transpositions included; two edits not",
|
|
2290
|
+
() =>
|
|
2291
|
+
expect(
|
|
2292
|
+
[
|
|
2293
|
+
("productImages", "productImgaes"),
|
|
2294
|
+
("productImages", "productImage"),
|
|
2295
|
+
("logos", "logo"),
|
|
2296
|
+
("documents", "logos"),
|
|
2297
|
+
("productImages", "productImgeas"),
|
|
2298
|
+
]->Array.map(((a, b)) => Capability_Inference.withinOneEdit(a, b)),
|
|
2299
|
+
)->toEqual([true, true, true, false, false]),
|
|
1859
2300
|
)
|
|
1860
2301
|
|
|
1861
2302
|
// Reached only through an explicit override: two *derived* names cannot
|
|
1862
2303
|
// differ by number, because each is pluralised through its singular stem.
|
|
1863
|
-
testSync(
|
|
1864
|
-
|
|
2304
|
+
testSync(
|
|
2305
|
+
"a singular/plural pair collides even at two edits apart",
|
|
2306
|
+
() => expect(Capability_Inference.sameStem("productImage", "productImages"))->toBe(true),
|
|
1865
2307
|
)
|
|
1866
2308
|
})
|
|
1867
2309
|
|
|
@@ -1877,62 +2319,75 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1877
2319
|
~stateChangeSlices=[module(PsUploadAvatarSlice)],
|
|
1878
2320
|
)
|
|
1879
2321
|
|
|
1880
|
-
testSync(
|
|
1881
|
-
|
|
2322
|
+
testSync(
|
|
2323
|
+
"an optional storage ref still requires its store",
|
|
2324
|
+
() => expect(withOptional.requiredStores)->toEqual(Some(["TestPlugin.avatars"])),
|
|
1882
2325
|
)
|
|
1883
2326
|
|
|
1884
2327
|
// Declared on the command and on the event under one field name — the same
|
|
1885
2328
|
// collapse the required case makes, so optionality does not change arity.
|
|
1886
|
-
testSync(
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2329
|
+
testSync(
|
|
2330
|
+
"and still carries one declaring site",
|
|
2331
|
+
() => {
|
|
2332
|
+
let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
|
|
2333
|
+
{
|
|
2334
|
+
store: "TestPlugin.avatars",
|
|
2335
|
+
component: "UploadAvatar",
|
|
2336
|
+
field: "avatarUrl",
|
|
2337
|
+
annotation: Some("avatars"),
|
|
2338
|
+
},
|
|
2339
|
+
]
|
|
2340
|
+
expect(withOptional.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
2341
|
+
},
|
|
2342
|
+
)
|
|
1897
2343
|
|
|
1898
|
-
testSync(
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2344
|
+
testSync(
|
|
2345
|
+
"an optional reference is still collected",
|
|
2346
|
+
() => {
|
|
2347
|
+
let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
|
|
2348
|
+
let expected: array<Reventless.Plugin.fieldReference> = [
|
|
2349
|
+
{fieldName: "referredBy", entity: "Customers", plugin: None},
|
|
2350
|
+
]
|
|
2351
|
+
expect(cmd.references)->toEqual(expected)
|
|
2352
|
+
},
|
|
2353
|
+
)
|
|
1905
2354
|
|
|
1906
2355
|
// What `requiredStores` knows, the wire has to carry. A reader deciding
|
|
1907
2356
|
// which command fills a storage-ref field, or which store's endpoint an
|
|
1908
2357
|
// upload goes to, has only the command schema to go on.
|
|
1909
|
-
testSync(
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
cmd.
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2358
|
+
testSync(
|
|
2359
|
+
"the command schema carries the field's storage-ref marker",
|
|
2360
|
+
() => {
|
|
2361
|
+
let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
|
|
2362
|
+
let target =
|
|
2363
|
+
cmd.schema
|
|
2364
|
+
->JSON.parseOrThrow
|
|
2365
|
+
->getPropertyOf("avatarUrl")
|
|
2366
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-semantic-target"))
|
|
2367
|
+
->Option.flatMap(t => getProperty(t, "store"))
|
|
2368
|
+
->Option.flatMap(JSON.Decode.string)
|
|
2369
|
+
expect(target)->toBe(Some("avatars"))
|
|
2370
|
+
},
|
|
2371
|
+
)
|
|
1920
2372
|
|
|
1921
2373
|
// A form submits what a schema says is required. `avatarUrl?` is optional in
|
|
1922
2374
|
// the spec, so listing it would make the picture mandatory in every UI that
|
|
1923
2375
|
// renders the command — the failure this pairs with, since carrying the
|
|
1924
2376
|
// marker is worth nothing if the field cannot be left empty.
|
|
1925
|
-
testSync(
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
cmd.
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2377
|
+
testSync(
|
|
2378
|
+
"an optional command argument is not required",
|
|
2379
|
+
() => {
|
|
2380
|
+
let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
|
|
2381
|
+
let required =
|
|
2382
|
+
cmd.schema
|
|
2383
|
+
->JSON.parseOrThrow
|
|
2384
|
+
->getProperty("required")
|
|
2385
|
+
->Option.flatMap(JSON.Decode.array)
|
|
2386
|
+
->Option.getOr([])
|
|
2387
|
+
->Array.filterMap(JSON.Decode.string)
|
|
2388
|
+
expect(required)->toEqual(["customerId"])
|
|
2389
|
+
},
|
|
2390
|
+
)
|
|
1936
2391
|
})
|
|
1937
2392
|
|
|
1938
2393
|
// Declarations are authoritative; the name heuristics are a lint. A
|
|
@@ -1949,52 +2404,66 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1949
2404
|
PsChangePhoto.eventSchema->S.castToUnknown,
|
|
1950
2405
|
)
|
|
1951
2406
|
|
|
1952
|
-
testSync(
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
// @storageRef — only the undeclared `photoUrl` warns.
|
|
1962
|
-
let expected: array<Capability_Inference.warning> = [
|
|
1963
|
-
{component: "ChangePhoto", field: "photoUrl"},
|
|
1964
|
-
]
|
|
1965
|
-
expect(eventWarnings)->toEqual(expected)
|
|
1966
|
-
})
|
|
2407
|
+
testSync(
|
|
2408
|
+
"a heuristic-only match produces a warning",
|
|
2409
|
+
() => {
|
|
2410
|
+
let expected: array<Capability_Inference.warning> = [
|
|
2411
|
+
{component: "ChangePhoto", field: "photoUrl"},
|
|
2412
|
+
]
|
|
2413
|
+
expect(commandWarnings)->toEqual(expected)
|
|
2414
|
+
},
|
|
2415
|
+
)
|
|
1967
2416
|
|
|
1968
|
-
testSync(
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
)
|
|
1996
|
-
|
|
1997
|
-
|
|
2417
|
+
testSync(
|
|
2418
|
+
"a declared field with a heuristic name does not warn",
|
|
2419
|
+
() => {
|
|
2420
|
+
// The event's `thumbnail` matches the name heuristic but carries
|
|
2421
|
+
// @storageRef — only the undeclared `photoUrl` warns.
|
|
2422
|
+
let expected: array<Capability_Inference.warning> = [
|
|
2423
|
+
{component: "ChangePhoto", field: "photoUrl"},
|
|
2424
|
+
]
|
|
2425
|
+
expect(eventWarnings)->toEqual(expected)
|
|
2426
|
+
},
|
|
2427
|
+
)
|
|
2428
|
+
|
|
2429
|
+
testSync(
|
|
2430
|
+
"a heuristic-only match reaches no manifest entry",
|
|
2431
|
+
() => {
|
|
2432
|
+
let withHeuristicField = Plugin_Structure.make(
|
|
2433
|
+
~name="TestPlugin",
|
|
2434
|
+
~stateChangeSlices=[module(PsChangePhotoSlice)],
|
|
2435
|
+
)
|
|
2436
|
+
// Only the *declared* `thumbnail` store is collected; `photoUrl` — the
|
|
2437
|
+
// heuristic-only match — provisions nothing.
|
|
2438
|
+
expect(withHeuristicField.requiredStores)->toEqual(Some(["TestPlugin.productPhotos"]))
|
|
2439
|
+
let manifest = Reventless.CapabilityManifest.fromStructure(withHeuristicField)
|
|
2440
|
+
expect(
|
|
2441
|
+
manifest.capabilities->Array.every(
|
|
2442
|
+
entry => entry.declaredBy->Array.every(site => site.field != Some("photoUrl")),
|
|
2443
|
+
),
|
|
2444
|
+
)->toBe(true)
|
|
2445
|
+
},
|
|
2446
|
+
)
|
|
2447
|
+
|
|
2448
|
+
testSync(
|
|
2449
|
+
"the warning names the field and the settling annotation",
|
|
2450
|
+
() => {
|
|
2451
|
+
let text = commandWarnings->Array.getUnsafe(0)->Capability_Inference.message
|
|
2452
|
+
expect(text->String.includes("ChangePhoto.photoUrl"))->toBe(true)
|
|
2453
|
+
expect(text->String.includes("@storageRef"))->toBe(true)
|
|
2454
|
+
},
|
|
2455
|
+
)
|
|
2456
|
+
|
|
2457
|
+
testSync(
|
|
2458
|
+
"suffix and exact name rules match; ordinary names do not",
|
|
2459
|
+
() => {
|
|
2460
|
+
expect(
|
|
2461
|
+
["invoiceFileRef", "upload", "thumbnail", "customerName", "documentUrl"]->Array.map(
|
|
2462
|
+
Capability_Inference.nameMatches,
|
|
2463
|
+
),
|
|
2464
|
+
)->toEqual([true, true, true, false, false])
|
|
2465
|
+
},
|
|
2466
|
+
)
|
|
1998
2467
|
})
|
|
1999
2468
|
|
|
2000
2469
|
// A plural reference is declared exactly like a singular one, and the ppx puts
|
|
@@ -2016,57 +2485,72 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
2016
2485
|
// scalar one is here so a fix that reaches them by breaking it cannot pass.
|
|
2017
2486
|
// The nested one rides alongside, named by its path rather than by the field
|
|
2018
2487
|
// that holds it.
|
|
2019
|
-
testSync(
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
(
|
|
2023
|
-
(
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2488
|
+
testSync(
|
|
2489
|
+
"a command collects its array refs alongside its scalar one",
|
|
2490
|
+
() => {
|
|
2491
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
2492
|
+
expect(cmd.references->referencesOf)->toEqual([
|
|
2493
|
+
("customerId", "Customers"),
|
|
2494
|
+
("productIds", "AvailableProducts"),
|
|
2495
|
+
("warehouseIds", "Warehouses"),
|
|
2496
|
+
("lineItems[].productId", "AvailableProducts"),
|
|
2497
|
+
])
|
|
2498
|
+
},
|
|
2499
|
+
)
|
|
2028
2500
|
|
|
2029
2501
|
// A reference declared on a field of a record the command holds belongs to
|
|
2030
2502
|
// *that* field, so it is named by its path: `[]` for "each element of", `.`
|
|
2031
2503
|
// for "property of". Attributing it to `lineItems` would name a field that
|
|
2032
2504
|
// declares nothing, and dropping it would leave the consuming side free to
|
|
2033
2505
|
// guess an entity from the name.
|
|
2034
|
-
testSync(
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
cmd.
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2506
|
+
testSync(
|
|
2507
|
+
"a reference on a nested record's field is named by its path",
|
|
2508
|
+
() => {
|
|
2509
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
2510
|
+
expect(
|
|
2511
|
+
cmd.references->Array.find(({fieldName}) => fieldName == "lineItems[].productId"),
|
|
2512
|
+
)->toEqual(
|
|
2513
|
+
Some({
|
|
2514
|
+
Reventless.Plugin.fieldName: "lineItems[].productId",
|
|
2515
|
+
entity: "AvailableProducts",
|
|
2516
|
+
plugin: None,
|
|
2517
|
+
}),
|
|
2518
|
+
)
|
|
2519
|
+
},
|
|
2520
|
+
)
|
|
2046
2521
|
|
|
2047
2522
|
// The other half of the path contract: a flat field keeps its bare name, so
|
|
2048
2523
|
// nothing reading today's paths has to learn the new form to go on working.
|
|
2049
|
-
testSync(
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2524
|
+
testSync(
|
|
2525
|
+
"a flat reference in the same variant keeps its bare name",
|
|
2526
|
+
() => {
|
|
2527
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
2528
|
+
expect(cmd.references->Array.some(({fieldName}) => fieldName == "customerId"))->toBe(true)
|
|
2529
|
+
},
|
|
2530
|
+
)
|
|
2053
2531
|
|
|
2054
2532
|
// Optionality is a statement about presence. An optional array puts the
|
|
2055
2533
|
// element two wrappers down, which is the shape most likely to be missed by
|
|
2056
2534
|
// an unwrap that only handles one.
|
|
2057
|
-
testSync(
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
cmd.
|
|
2061
|
-
|
|
2062
|
-
|
|
2535
|
+
testSync(
|
|
2536
|
+
"an optional array ref is collected under the field's own name",
|
|
2537
|
+
() => {
|
|
2538
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
2539
|
+
expect(cmd.references->Array.find(({fieldName}) => fieldName == "warehouseIds"))->toEqual(
|
|
2540
|
+
Some({Reventless.Plugin.fieldName: "warehouseIds", entity: "Warehouses", plugin: None}),
|
|
2541
|
+
)
|
|
2542
|
+
},
|
|
2543
|
+
)
|
|
2063
2544
|
|
|
2064
2545
|
// The event walk is a separate function over the same field dicts. Left to
|
|
2065
2546
|
// itself it would be fixed by accident or not at all.
|
|
2066
|
-
testSync(
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2547
|
+
testSync(
|
|
2548
|
+
"an event collects its array ref too",
|
|
2549
|
+
() => {
|
|
2550
|
+
let evt = slice.events->Array.getUnsafe(0)
|
|
2551
|
+
expect(evt.references->referencesOf)->toEqual([("productIds", "AvailableProducts")])
|
|
2552
|
+
},
|
|
2553
|
+
)
|
|
2070
2554
|
|
|
2071
2555
|
// A client assembling this mutation declares a variable per argument, and
|
|
2072
2556
|
// for `lineItems` it needs the *element* input type by name. The rendered
|
|
@@ -2076,28 +2560,34 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
2076
2560
|
//
|
|
2077
2561
|
// The name is composed from the mutation field by a server-side convention,
|
|
2078
2562
|
// so nothing downstream of the JSON Schema could reconstruct it.
|
|
2079
|
-
testSync(
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
cmd.
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2563
|
+
testSync(
|
|
2564
|
+
"a nested input object publishes the type name the SDL declares",
|
|
2565
|
+
() => {
|
|
2566
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
2567
|
+
let itemName =
|
|
2568
|
+
cmd.schema
|
|
2569
|
+
->JSON.parseOrThrow
|
|
2570
|
+
->getPropertyOf("lineItems")
|
|
2571
|
+
->Option.flatMap(s => getProperty(s, "items"))
|
|
2572
|
+
->Option.flatMap(s => getProperty(s, "x-reventless-graphql-input"))
|
|
2573
|
+
->Option.flatMap(JSON.Decode.string)
|
|
2574
|
+
expect(itemName)->toEqual(Some(cmd.mutationField ++ "LineItems"))
|
|
2575
|
+
},
|
|
2576
|
+
)
|
|
2090
2577
|
|
|
2091
2578
|
// The declared entity, not the one `productIds` reads like. Before the
|
|
2092
2579
|
// element unwrap this field resolved by heuristic to `Products`.
|
|
2093
|
-
testSync(
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
cmd.
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2580
|
+
testSync(
|
|
2581
|
+
"the declared entity wins over the one the field name suggests",
|
|
2582
|
+
() => {
|
|
2583
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
2584
|
+
expect(
|
|
2585
|
+
cmd.references
|
|
2586
|
+
->Array.find(({fieldName}) => fieldName == "productIds")
|
|
2587
|
+
->Option.map(({entity}) => entity),
|
|
2588
|
+
)->toEqual(Some("AvailableProducts"))
|
|
2589
|
+
},
|
|
2590
|
+
)
|
|
2101
2591
|
})
|
|
2102
2592
|
})
|
|
2103
2593
|
|
|
@@ -2118,30 +2608,29 @@ describe("the port's translation table", () => {
|
|
|
2118
2608
|
|
|
2119
2609
|
testSync("accepts a many-to-one declaration", () =>
|
|
2120
2610
|
expect(
|
|
2121
|
-
failures(
|
|
2611
|
+
failures(
|
|
2612
|
+
~declared=[published("ProductWithdrawn", ["Archived", "Discontinued"])],
|
|
2613
|
+
)->Array.length,
|
|
2122
2614
|
)->toBe(0)
|
|
2123
2615
|
)
|
|
2124
2616
|
|
|
2125
2617
|
testSync("rejects a published name the extension point does not declare", () =>
|
|
2126
|
-
expect(
|
|
2127
|
-
failures(~declared=[published("ProductRetired", ["Archived"])])->Array.length,
|
|
2128
|
-
)->toBe(1)
|
|
2618
|
+
expect(failures(~declared=[published("ProductRetired", ["Archived"])])->Array.length)->toBe(1)
|
|
2129
2619
|
)
|
|
2130
2620
|
|
|
2131
2621
|
testSync("rejects a source name the delegate does not declare", () =>
|
|
2132
|
-
expect(
|
|
2133
|
-
failures(~declared=[published("ProductWithdrawn", ["Archivd"])])->Array.length,
|
|
2134
|
-
)->toBe(1)
|
|
2622
|
+
expect(failures(~declared=[published("ProductWithdrawn", ["Archivd"])])->Array.length)->toBe(1)
|
|
2135
2623
|
)
|
|
2136
|
-
|
|
2137
2624
|
})
|
|
2138
2625
|
|
|
2139
2626
|
describe("the translation table against the probe", () => {
|
|
2140
2627
|
let declared = [
|
|
2141
|
-
(
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2628
|
+
(
|
|
2629
|
+
{
|
|
2630
|
+
ReventlessInfra.ExtensionPointMapping.name: "ProductWithdrawn",
|
|
2631
|
+
fromEventTypes: ["Archived", "Discontinued"],
|
|
2632
|
+
}: ReventlessInfra.ExtensionPointMapping.publishedEvent
|
|
2633
|
+
),
|
|
2145
2634
|
]
|
|
2146
2635
|
let drift = (~observed, ~followed) =>
|
|
2147
2636
|
Plugin_Structure.translationTableDrift(~label="EP", ~declared, ~observed, ~followed)
|
|
@@ -2156,10 +2645,7 @@ describe("the translation table against the probe", () => {
|
|
|
2156
2645
|
|
|
2157
2646
|
// The probe watched the arm publish it, so the declaration is stale.
|
|
2158
2647
|
testSync("fails on an edge the probe saw and the declaration omits", () => {
|
|
2159
|
-
let (failures, _) = drift(
|
|
2160
|
-
~observed=[("Archived", "ProductRelisted")],
|
|
2161
|
-
~followed=["Archived"],
|
|
2162
|
-
)
|
|
2648
|
+
let (failures, _) = drift(~observed=[("Archived", "ProductRelisted")], ~followed=["Archived"])
|
|
2163
2649
|
expect(failures->Array.length)->toBe(1)
|
|
2164
2650
|
})
|
|
2165
2651
|
|
|
@@ -2204,9 +2690,7 @@ describe("the subscriber's half of the table", () => {
|
|
|
2204
2690
|
)
|
|
2205
2691
|
|
|
2206
2692
|
testSync("rejects a command neither side declares", () =>
|
|
2207
|
-
expect(
|
|
2208
|
-
failures([handled("ProductBecameAvailable", ["SyncNewProdct"])])->Array.length,
|
|
2209
|
-
)->toBe(1)
|
|
2693
|
+
expect(failures([handled("ProductBecameAvailable", ["SyncNewProdct"])])->Array.length)->toBe(1)
|
|
2210
2694
|
)
|
|
2211
2695
|
})
|
|
2212
2696
|
|