@reventlessdev/reventless-core 3.0.0-alpha.214 → 3.0.0-alpha.216
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 +15 -0
- package/package.json +7 -7
- package/src/adapter/RuntimeExtension/RuntimeExtension.res +194 -0
- package/src/adapter/RuntimeExtension/RuntimeExtension.res.mjs +60 -0
- package/src/admin/Platform_Admin_Structure.res +4 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
- package/src/components/Aggregate/Aggregate_Callback.res +14 -2
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
- package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
- package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
- package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
- package/src/plugin/component/Plugin_Builder.res +1 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +1 -0
- package/src/plugin/component/Plugin_Structure.res +66 -51
- package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
- package/tests/adapter/RuntimeExtensionHookReachTest.res +84 -0
- package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +95 -0
- package/tests/adapter/RuntimeExtensionTest.res +127 -0
- package/tests/adapter/RuntimeExtensionTest.res.mjs +111 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +1 -0
- package/tests/plugin/PluginStructureTest.res +27 -0
- package/tests/plugin/PluginStructureTest.res.mjs +27 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +8 -0
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Effect from "effect/Effect";
|
|
6
7
|
import * as Stream from "effect/Stream";
|
|
7
8
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
8
9
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
10
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
|
+
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
12
|
+
|
|
13
|
+
let commandOutcomeHook = {
|
|
14
|
+
contents: undefined
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function registerCommandOutcome(hook) {
|
|
18
|
+
commandOutcomeHook.contents = hook;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function clearCommandOutcome() {
|
|
22
|
+
commandOutcomeHook.contents = undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fireCommandOutcome(component, reference, outcome) {
|
|
26
|
+
Stdlib_Option.forEach(commandOutcomeHook.contents, hook => {
|
|
27
|
+
try {
|
|
28
|
+
hook({
|
|
29
|
+
component: component,
|
|
30
|
+
reference: reference,
|
|
31
|
+
outcome: outcome
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
} catch (raw_err) {
|
|
35
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
36
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError(`CommandTopic(` + component + `)`, undefined, `command-outcome hook threw (ignored): ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(err), Stdlib_JsExn.message), "unknown")));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
9
40
|
|
|
10
41
|
let acceptedResultChannel = {
|
|
11
42
|
contents: undefined
|
|
@@ -15,12 +46,23 @@ let rejectedResultChannel = {
|
|
|
15
46
|
contents: undefined
|
|
16
47
|
};
|
|
17
48
|
|
|
18
|
-
function reportAccepted(reference, result) {
|
|
49
|
+
function reportAccepted(component, reference, result) {
|
|
19
50
|
Stdlib_Option.forEach(acceptedResultChannel.contents, cb => cb(reference, result));
|
|
51
|
+
fireCommandOutcome(component, reference, {
|
|
52
|
+
TAG: "OutcomeAccepted",
|
|
53
|
+
entityId: result.entityId,
|
|
54
|
+
eventCount: result.eventCount
|
|
55
|
+
});
|
|
20
56
|
}
|
|
21
57
|
|
|
22
|
-
function reportRejected(reference, result) {
|
|
58
|
+
function reportRejected(component, cause, reference, result) {
|
|
23
59
|
Stdlib_Option.forEach(rejectedResultChannel.contents, cb => cb(reference, result));
|
|
60
|
+
fireCommandOutcome(component, reference, {
|
|
61
|
+
TAG: "OutcomeRejected",
|
|
62
|
+
errorCode: result.errorCode,
|
|
63
|
+
errorDetail: result.errorDetail,
|
|
64
|
+
cause: cause
|
|
65
|
+
});
|
|
24
66
|
}
|
|
25
67
|
|
|
26
68
|
function commandOutcomeToJson(outcome) {
|
|
@@ -179,6 +221,10 @@ function getHandlers(typeName) {
|
|
|
179
221
|
}
|
|
180
222
|
|
|
181
223
|
export {
|
|
224
|
+
commandOutcomeHook,
|
|
225
|
+
registerCommandOutcome,
|
|
226
|
+
clearCommandOutcome,
|
|
227
|
+
fireCommandOutcome,
|
|
182
228
|
acceptedResultChannel,
|
|
183
229
|
rejectedResultChannel,
|
|
184
230
|
reportAccepted,
|
|
@@ -322,6 +322,7 @@ module Make = (
|
|
|
322
322
|
// No append, but the read snapshot is valid — cache it for the next command.
|
|
323
323
|
cachePut((state, headPosition))
|
|
324
324
|
CommandTopic_Helpers.reportAccepted(
|
|
325
|
+
~component=Spec.name,
|
|
325
326
|
cmdJson.meta.msgId,
|
|
326
327
|
switch entityId {
|
|
327
328
|
| Some(eid) => {entityId: eid, eventCount: 0}
|
|
@@ -373,6 +374,7 @@ module Make = (
|
|
|
373
374
|
// next command's delta read picks our events up from `headPosition`.
|
|
374
375
|
cachePut((state, headPosition))
|
|
375
376
|
CommandTopic_Helpers.reportAccepted(
|
|
377
|
+
~component=Spec.name,
|
|
376
378
|
cmdJson.meta.msgId,
|
|
377
379
|
switch entityId {
|
|
378
380
|
| Some(eid) => {entityId: eid, eventCount: rawEvents->Array.length}
|
|
@@ -412,7 +414,12 @@ module Make = (
|
|
|
412
414
|
if errorCode == "Conflict" {
|
|
413
415
|
Metrics.emitCount(~metric="AppendConflict", ~slice=Spec.name)
|
|
414
416
|
}
|
|
417
|
+
// Both arms here are storage, not the model: a surfaced Conflict is
|
|
418
|
+
// contention the retry loop couldn't absorb, and AppendFailed is the
|
|
419
|
+
// store failing. `decide` had already said Ok.
|
|
415
420
|
CommandTopic_Helpers.reportRejected(
|
|
421
|
+
~component=Spec.name,
|
|
422
|
+
~cause=InfrastructureFailure,
|
|
416
423
|
cmdJson.meta.msgId,
|
|
417
424
|
{errorCode, errorDetail: errDetail},
|
|
418
425
|
)
|
|
@@ -433,7 +440,12 @@ module Make = (
|
|
|
433
440
|
payloadDict->Dict.toArray->Array.length == 0
|
|
434
441
|
? ""
|
|
435
442
|
: payloadDict->JSON.Encode.object->JSON.stringify
|
|
436
|
-
CommandTopic_Helpers.reportRejected(
|
|
443
|
+
CommandTopic_Helpers.reportRejected(
|
|
444
|
+
~component=Spec.name,
|
|
445
|
+
~cause=DomainRejection,
|
|
446
|
+
cmdJson.meta.msgId,
|
|
447
|
+
{errorCode, errorDetail},
|
|
448
|
+
)
|
|
437
449
|
EffectLogger.logError(
|
|
438
450
|
~comp,
|
|
439
451
|
`decide rejected: ${errorCode} ${errorDetail}`,
|
|
@@ -173,7 +173,7 @@ function Make(Spec) {
|
|
|
173
173
|
state,
|
|
174
174
|
headPosition
|
|
175
175
|
]);
|
|
176
|
-
CommandTopic_Helpers$ReventlessCore.reportAccepted(cmdJson.meta.msgId, entityId !== undefined ? ({
|
|
176
|
+
CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, cmdJson.meta.msgId, entityId !== undefined ? ({
|
|
177
177
|
entityId: entityId,
|
|
178
178
|
eventCount: 0
|
|
179
179
|
}) : ({
|
|
@@ -223,7 +223,7 @@ function Make(Spec) {
|
|
|
223
223
|
state,
|
|
224
224
|
headPosition
|
|
225
225
|
]);
|
|
226
|
-
CommandTopic_Helpers$ReventlessCore.reportAccepted(cmdJson.meta.msgId, entityId !== undefined ? ({
|
|
226
|
+
CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, cmdJson.meta.msgId, entityId !== undefined ? ({
|
|
227
227
|
entityId: entityId,
|
|
228
228
|
eventCount: rawEvents.length
|
|
229
229
|
}) : ({
|
|
@@ -257,7 +257,7 @@ function Make(Spec) {
|
|
|
257
257
|
if (errorCode === "Conflict") {
|
|
258
258
|
Metrics$ReventlessCore.emitCount("AppendConflict", Spec.name, undefined);
|
|
259
259
|
}
|
|
260
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(cmdJson.meta.msgId, {
|
|
260
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "InfrastructureFailure", cmdJson.meta.msgId, {
|
|
261
261
|
errorCode: errorCode,
|
|
262
262
|
errorDetail: errDetail
|
|
263
263
|
});
|
|
@@ -277,7 +277,7 @@ function Make(Spec) {
|
|
|
277
277
|
let match = Message$ReventlessCore.splitMessage(errorJson);
|
|
278
278
|
let payloadDict = match[1];
|
|
279
279
|
let errorDetail = Object.entries(payloadDict).length === 0 ? "" : JSON.stringify(payloadDict);
|
|
280
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(cmdJson.meta.msgId, {
|
|
280
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "DomainRejection", cmdJson.meta.msgId, {
|
|
281
281
|
errorCode: errorCode,
|
|
282
282
|
errorDetail: errorDetail
|
|
283
283
|
});
|
|
@@ -529,6 +529,7 @@ module Make = (
|
|
|
529
529
|
{
|
|
530
530
|
commandTypes: extractTypes(Scs.Spec.commandSchema),
|
|
531
531
|
eventTypes: extractEventTypes(Scs.Spec.eventSchema->S.castToUnknown),
|
|
532
|
+
errorTypes: extractTypes(Scs.Spec.errorSchema),
|
|
532
533
|
consumedEventTypes: extractEventTypes(Scs.Spec.consumedEventSchema->S.castToUnknown),
|
|
533
534
|
chapter: ?chapterFor(Scs.Spec.name),
|
|
534
535
|
},
|
|
@@ -373,6 +373,7 @@ function Make(Spec) {
|
|
|
373
373
|
stateSchemas[Scs.Spec.name] = {
|
|
374
374
|
commandTypes: DcbTag$Reventless.extractAllVariantNames(Scs.Spec.commandSchema),
|
|
375
375
|
eventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.eventSchema),
|
|
376
|
+
errorTypes: DcbTag$Reventless.extractAllVariantNames(Scs.Spec.errorSchema),
|
|
376
377
|
consumedEventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.consumedEventSchema),
|
|
377
378
|
chapter: chapterByName[Scs.Spec.name]
|
|
378
379
|
};
|
|
@@ -157,6 +157,70 @@ let labelFieldsFromStateSchema = (
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// ── Per-variant event / error field extraction (Phase 6.3) ───────────────────
|
|
161
|
+
// Mirrors toCommandDef/extractCommandDefs for emitted events: name (TAG const),
|
|
162
|
+
// payload JSON Schema, and cross-entity references. Module-level rather than
|
|
163
|
+
// inside `make` because the synthetic Platform_Admin structure — hand-written,
|
|
164
|
+
// because its components never pass through `make` — has to derive its defs the
|
|
165
|
+
// same way, and a second copy of this walk would be free to drift from this one.
|
|
166
|
+
|
|
167
|
+
let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
168
|
+
let mkDef = (~variantName, ~properties) => {
|
|
169
|
+
let references =
|
|
170
|
+
properties
|
|
171
|
+
->Dict.toArray
|
|
172
|
+
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
173
|
+
Reventless.Reference.getTarget(fieldSchema)->Option.map(target => (
|
|
174
|
+
{
|
|
175
|
+
Reventless.Plugin.fieldName,
|
|
176
|
+
entity: target.entity,
|
|
177
|
+
plugin: target.plugin,
|
|
178
|
+
}: Reventless.Plugin.fieldReference
|
|
179
|
+
))
|
|
180
|
+
)
|
|
181
|
+
({
|
|
182
|
+
Reventless.Plugin.name: variantName,
|
|
183
|
+
// Derived for the same reason as `commandDef.schema`: an event's field
|
|
184
|
+
// markers are the write side's half of the same vocabulary.
|
|
185
|
+
schema: v->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
186
|
+
references,
|
|
187
|
+
}: Reventless.Plugin.eventDef)
|
|
188
|
+
}
|
|
189
|
+
switch v {
|
|
190
|
+
| Object({properties}) =>
|
|
191
|
+
properties
|
|
192
|
+
->Dict.get("TAG")
|
|
193
|
+
->Option.flatMap(tagSchema =>
|
|
194
|
+
switch tagSchema {
|
|
195
|
+
| String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties))
|
|
196
|
+
| _ => None
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
// Payload-less event variants (`| Archived`) compile to a bare string literal.
|
|
200
|
+
// DCB-projection lookups can't WHERE-clause on them, so they stay out of
|
|
201
|
+
// producedEventTypes/consumedEventTypes; but the full `events` list carries them
|
|
202
|
+
// so the event graph can still draw the emitted (orphan) event node.
|
|
203
|
+
| String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties=Dict.make()))
|
|
204
|
+
| _ => None
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let extractEventDefs = (eventSchema: S.t<unknown>): array<Reventless.Plugin.eventDef> =>
|
|
209
|
+
switch eventSchema {
|
|
210
|
+
| Union({anyOf}) => anyOf->Array.filterMap(toEventDef)
|
|
211
|
+
| _ => toEventDef(eventSchema)->Option.mapOr([], def => [def])
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Declared errors walk identically to emitted events — an error variant is a
|
|
215
|
+
// variant, and the payload-less form (`| CategoryNotFound`) is the common case
|
|
216
|
+
// the bare-string branch above already handles. Reusing the walk rather than
|
|
217
|
+
// copying it keeps the two from drifting; only the def type differs, so that a
|
|
218
|
+
// consumer (and the SDL) can tell a refusal from a fact.
|
|
219
|
+
let extractErrorDefs = (errorSchema: S.t<unknown>): array<Reventless.Plugin.errorDef> =>
|
|
220
|
+
extractEventDefs(errorSchema)->Array.map(({name, schema, references}) => (
|
|
221
|
+
{name, schema, references}: Reventless.Plugin.errorDef
|
|
222
|
+
))
|
|
223
|
+
|
|
160
224
|
let make = (
|
|
161
225
|
type api role,
|
|
162
226
|
~name: string,
|
|
@@ -330,57 +394,6 @@ let make = (
|
|
|
330
394
|
->Option.mapOr([], def => [def])
|
|
331
395
|
}
|
|
332
396
|
|
|
333
|
-
// ── Per-variant event field extraction (Phase 6.3) ─────────────────────────
|
|
334
|
-
// Mirrors toCommandDef/extractCommandDefs for emitted events: name (TAG const),
|
|
335
|
-
// payload JSON Schema, and cross-entity references.
|
|
336
|
-
|
|
337
|
-
let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
338
|
-
let mkDef = (~variantName, ~properties) => {
|
|
339
|
-
let references =
|
|
340
|
-
properties
|
|
341
|
-
->Dict.toArray
|
|
342
|
-
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
343
|
-
Reventless.Reference.getTarget(fieldSchema)->Option.map(target => (
|
|
344
|
-
{
|
|
345
|
-
Reventless.Plugin.fieldName,
|
|
346
|
-
entity: target.entity,
|
|
347
|
-
plugin: target.plugin,
|
|
348
|
-
}: Reventless.Plugin.fieldReference
|
|
349
|
-
))
|
|
350
|
-
)
|
|
351
|
-
({
|
|
352
|
-
Reventless.Plugin.name: variantName,
|
|
353
|
-
// Derived for the same reason as `commandDef.schema` above: an event's
|
|
354
|
-
// field markers are the write side's half of the same vocabulary.
|
|
355
|
-
schema: v->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
356
|
-
references,
|
|
357
|
-
}: Reventless.Plugin.eventDef)
|
|
358
|
-
}
|
|
359
|
-
switch v {
|
|
360
|
-
| Object({properties}) =>
|
|
361
|
-
properties
|
|
362
|
-
->Dict.get("TAG")
|
|
363
|
-
->Option.flatMap(tagSchema =>
|
|
364
|
-
switch tagSchema {
|
|
365
|
-
| String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties))
|
|
366
|
-
| _ => None
|
|
367
|
-
}
|
|
368
|
-
)
|
|
369
|
-
// Payload-less event variants (`| Archived`) compile to a bare string literal.
|
|
370
|
-
// DCB-projection lookups can't WHERE-clause on them, so they stay out of
|
|
371
|
-
// producedEventTypes/consumedEventTypes; but the full `events` list carries them
|
|
372
|
-
// so the event graph can still draw the emitted (orphan) event node.
|
|
373
|
-
| String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties=Dict.make()))
|
|
374
|
-
| _ => None
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
let extractEventDefs = (eventSchema: S.t<unknown>): array<Reventless.Plugin.eventDef> =>
|
|
379
|
-
switch eventSchema {
|
|
380
|
-
| Union({anyOf}) => anyOf->Array.filterMap(toEventDef)
|
|
381
|
-
| _ => toEventDef(eventSchema)->Option.mapOr([], def => [def])
|
|
382
|
-
}
|
|
383
|
-
|
|
384
397
|
// ── Per-component event type extraction ────────────────────────────────────
|
|
385
398
|
|
|
386
399
|
let scsProduced =
|
|
@@ -704,6 +717,7 @@ let make = (
|
|
|
704
717
|
linkedViews: linkedSvsFor(produced),
|
|
705
718
|
consistencyRead: consistencyReadFor(consumed),
|
|
706
719
|
events: extractEventDefs(SCS.Spec.eventSchema->S.castToUnknown),
|
|
720
|
+
errors: extractErrorDefs(SCS.Spec.errorSchema->S.castToUnknown),
|
|
707
721
|
chapter: chapterOf(SCS.Spec.name),
|
|
708
722
|
}: Reventless.Plugin.writableDef)
|
|
709
723
|
})
|
|
@@ -726,6 +740,7 @@ let make = (
|
|
|
726
740
|
linkedViews: Array.concat(linkedSvsFor(produced), linkedReadModelsFor(A.Spec.name)),
|
|
727
741
|
consistencyRead: None,
|
|
728
742
|
events: extractEventDefs(A.Spec.eventSchema->S.castToUnknown),
|
|
743
|
+
errors: extractErrorDefs(A.Spec.errorSchema->S.castToUnknown),
|
|
729
744
|
chapter: chapterOf(A.Spec.name),
|
|
730
745
|
}: Reventless.Plugin.writableDef)
|
|
731
746
|
})
|
|
@@ -140,6 +140,62 @@ function labelFieldsFromStateSchema(entityName, stateSchema) {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function toEventDef(v) {
|
|
144
|
+
let mkDef = (variantName, properties) => {
|
|
145
|
+
let references = Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
146
|
+
let fieldName = param[0];
|
|
147
|
+
return Stdlib_Option.map(Reference$Reventless.getTarget(param[1]), target => ({
|
|
148
|
+
fieldName: fieldName,
|
|
149
|
+
entity: target.entity,
|
|
150
|
+
plugin: target.plugin
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
name: variantName,
|
|
155
|
+
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
|
|
156
|
+
references: references
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
switch (v.type) {
|
|
160
|
+
case "string" :
|
|
161
|
+
let variantName = v.const;
|
|
162
|
+
if (variantName !== undefined) {
|
|
163
|
+
return mkDef(variantName, {});
|
|
164
|
+
} else {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
case "object" :
|
|
168
|
+
let properties = v.properties;
|
|
169
|
+
return Stdlib_Option.flatMap(properties["TAG"], tagSchema => {
|
|
170
|
+
if (tagSchema.type !== "string") {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let variantName = tagSchema.const;
|
|
174
|
+
if (variantName !== undefined) {
|
|
175
|
+
return mkDef(variantName, properties);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
default:
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractEventDefs(eventSchema) {
|
|
184
|
+
if (eventSchema.type === "union") {
|
|
185
|
+
return Stdlib_Array.filterMap(eventSchema.anyOf, toEventDef);
|
|
186
|
+
} else {
|
|
187
|
+
return Stdlib_Option.mapOr(toEventDef(eventSchema), [], def => [def]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractErrorDefs(errorSchema) {
|
|
192
|
+
return extractEventDefs(errorSchema).map(param => ({
|
|
193
|
+
name: param.name,
|
|
194
|
+
schema: param.schema,
|
|
195
|
+
references: param.references
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
143
199
|
function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionsOpt, extensionPointsOpt, componentChaptersOpt) {
|
|
144
200
|
let aggregates = aggregatesOpt !== undefined ? aggregatesOpt : [];
|
|
145
201
|
let readModels = readModelsOpt !== undefined ? readModelsOpt : [];
|
|
@@ -268,52 +324,6 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
268
324
|
return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandSchema), [], def => [def]);
|
|
269
325
|
}
|
|
270
326
|
};
|
|
271
|
-
let toEventDef = v => {
|
|
272
|
-
let mkDef = (variantName, properties) => {
|
|
273
|
-
let references = Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
274
|
-
let fieldName = param[0];
|
|
275
|
-
return Stdlib_Option.map(Reference$Reventless.getTarget(param[1]), target => ({
|
|
276
|
-
fieldName: fieldName,
|
|
277
|
-
entity: target.entity,
|
|
278
|
-
plugin: target.plugin
|
|
279
|
-
}));
|
|
280
|
-
});
|
|
281
|
-
return {
|
|
282
|
-
name: variantName,
|
|
283
|
-
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
|
|
284
|
-
references: references
|
|
285
|
-
};
|
|
286
|
-
};
|
|
287
|
-
switch (v.type) {
|
|
288
|
-
case "string" :
|
|
289
|
-
let variantName = v.const;
|
|
290
|
-
if (variantName !== undefined) {
|
|
291
|
-
return mkDef(variantName, {});
|
|
292
|
-
} else {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
case "object" :
|
|
296
|
-
let properties = v.properties;
|
|
297
|
-
return Stdlib_Option.flatMap(properties["TAG"], tagSchema => {
|
|
298
|
-
if (tagSchema.type !== "string") {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
let variantName = tagSchema.const;
|
|
302
|
-
if (variantName !== undefined) {
|
|
303
|
-
return mkDef(variantName, properties);
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
default:
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
let extractEventDefs = eventSchema => {
|
|
311
|
-
if (eventSchema.type === "union") {
|
|
312
|
-
return Stdlib_Array.filterMap(eventSchema.anyOf, toEventDef);
|
|
313
|
-
} else {
|
|
314
|
-
return Stdlib_Option.mapOr(toEventDef(eventSchema), [], def => [def]);
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
327
|
let scsProduced = stateChangeSlices.map(SCS => [
|
|
318
328
|
SCS.Spec.name,
|
|
319
329
|
qualify(name, DcbTag$Reventless.extractVariantNames(SCS.Spec.eventSchema))
|
|
@@ -541,6 +551,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
541
551
|
linkedViews: linkedSvsFor(produced),
|
|
542
552
|
consistencyRead: consistencyReadFor(consumed),
|
|
543
553
|
events: extractEventDefs(SCS.Spec.eventSchema),
|
|
554
|
+
errors: extractErrorDefs(SCS.Spec.errorSchema),
|
|
544
555
|
chapter: componentChapters[SCS.Spec.name]
|
|
545
556
|
};
|
|
546
557
|
});
|
|
@@ -555,6 +566,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
555
566
|
linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
|
|
556
567
|
consistencyRead: undefined,
|
|
557
568
|
events: extractEventDefs(A.Spec.eventSchema),
|
|
569
|
+
errors: extractErrorDefs(A.Spec.errorSchema),
|
|
558
570
|
chapter: componentChapters[A.Spec.name]
|
|
559
571
|
};
|
|
560
572
|
});
|
|
@@ -641,6 +653,9 @@ export {
|
|
|
641
653
|
statusFieldFromStateSchema,
|
|
642
654
|
labelFieldSourceToString,
|
|
643
655
|
labelFieldsFromStateSchema,
|
|
656
|
+
toEventDef,
|
|
657
|
+
extractEventDefs,
|
|
658
|
+
extractErrorDefs,
|
|
644
659
|
make,
|
|
645
660
|
}
|
|
646
661
|
/* log Not a pure module */
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
S.enableJson()
|
|
4
|
+
|
|
5
|
+
// The point of the seam, end to end at the hook level: an extension that only
|
|
6
|
+
// ever gets `onColdStart` can reach the framework's runtime callback hooks from
|
|
7
|
+
// there, and they take effect on the requests that follow.
|
|
8
|
+
//
|
|
9
|
+
// `CommandGenerator_Callback.registerCommandInterceptor` stands in for all four
|
|
10
|
+
// registrars — they share the shape (a module-level ref consulted on the hot
|
|
11
|
+
// path), so what makes one reachable makes the others reachable too. What is
|
|
12
|
+
// being proved is the ordering: registration happens inside the cold-start hook,
|
|
13
|
+
// the command runs after it, and the interceptor is consulted.
|
|
14
|
+
|
|
15
|
+
@schema
|
|
16
|
+
type command = Add({@partitionTag productId: string, name: string})
|
|
17
|
+
|
|
18
|
+
let published: ref<array<Message.commandJson>> = ref([])
|
|
19
|
+
let publishJsons: CommandGenerator.publishJsons = async cmds =>
|
|
20
|
+
published := published.contents->Array.concat(cmds)
|
|
21
|
+
|
|
22
|
+
let generateCommand = CommandGenerator_Callback.makeGenerateCommand(
|
|
23
|
+
~publishJsons,
|
|
24
|
+
~serviceName="Product",
|
|
25
|
+
~commandSchema=commandSchema->S.castToUnknown,
|
|
26
|
+
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
27
|
+
~stripIdFromParams=false,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
let addChair = (): CommandGenerator.payload =>
|
|
31
|
+
Obj.magic({
|
|
32
|
+
"command": "Add",
|
|
33
|
+
"arguments": {"productId": "p1", "name": "Chair"},
|
|
34
|
+
"meta": {"ip": ["127.0.0.1"], "user": "test-user", "info": ""},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
let runAdd = async () => await generateCommand(addChair())->Effect.runPromise
|
|
38
|
+
|
|
39
|
+
describe("RuntimeExtension reaches the runtime callback hooks", () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
RuntimeExtension.reset()
|
|
42
|
+
CommandGenerator_Callback.clearCommandInterceptor()
|
|
43
|
+
published := []
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
testPromise("an interceptor registered at cold start denies a later command", async () => {
|
|
47
|
+
let seenAtColdStart: array<string> = []
|
|
48
|
+
module Gatekeeper: RuntimeExtension.Extension = {
|
|
49
|
+
let moduleUrl = "file:///pkg/gatekeeper.res.mjs"
|
|
50
|
+
let onColdStart = (~runtimeKind as _, ~component, ~plugin as _, ~platform as _) => {
|
|
51
|
+
seenAtColdStart->Array.push(component)
|
|
52
|
+
CommandGenerator_Callback.registerCommandInterceptor(async (
|
|
53
|
+
~identity as _,
|
|
54
|
+
~componentName,
|
|
55
|
+
~componentKind as _,
|
|
56
|
+
~tag,
|
|
57
|
+
~args as _,
|
|
58
|
+
) => CommandGenerator_Callback.Deny(`${componentName}.${tag} refused`))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
RuntimeExtension.use(module(Gatekeeper: RuntimeExtension.Extension))
|
|
62
|
+
|
|
63
|
+
RuntimeExtension.notifyColdStart(
|
|
64
|
+
~runtimeKind=ComponentType.StateChangeSlice,
|
|
65
|
+
~component="CatalogStateChanges",
|
|
66
|
+
~plugin=Some("Catalog"),
|
|
67
|
+
~platform=Some("Shop"),
|
|
68
|
+
)
|
|
69
|
+
expect(seenAtColdStart)->toEqual(["CatalogStateChanges"])
|
|
70
|
+
|
|
71
|
+
let denied = switch await runAdd() {
|
|
72
|
+
| _ => None
|
|
73
|
+
| exception exn => exn->JsExn.fromException->Option.flatMap(JsExn.message)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expect(denied)->toEqual(Some("Product.Add refused"))
|
|
77
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
testPromise("with nothing registered the same command publishes untouched", async () => {
|
|
81
|
+
let _ = await runAdd()
|
|
82
|
+
expect(published.contents->Array.length)->toBe(1)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
8
|
+
import * as RuntimeExtension$ReventlessCore from "../../src/adapter/RuntimeExtension/RuntimeExtension.res.mjs";
|
|
9
|
+
import * as CommandGenerator_Callback$ReventlessCore from "../../src/components/CommandGenerator/CommandGenerator_Callback.res.mjs";
|
|
10
|
+
|
|
11
|
+
S.enableJson();
|
|
12
|
+
|
|
13
|
+
let commandSchema = S.schema(s => ({
|
|
14
|
+
TAG: "Add",
|
|
15
|
+
productId: s.m(S.string),
|
|
16
|
+
name: s.m(S.string)
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
let published = {
|
|
20
|
+
contents: []
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function publishJsons(cmds) {
|
|
24
|
+
published.contents = published.contents.concat(cmds);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "Product", commandSchema, "StateChangeSlice", false);
|
|
28
|
+
|
|
29
|
+
function addChair() {
|
|
30
|
+
return {
|
|
31
|
+
command: "Add",
|
|
32
|
+
arguments: {
|
|
33
|
+
productId: "p1",
|
|
34
|
+
name: "Chair"
|
|
35
|
+
},
|
|
36
|
+
meta: {
|
|
37
|
+
ip: ["127.0.0.1"],
|
|
38
|
+
user: "test-user",
|
|
39
|
+
info: ""
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runAdd() {
|
|
45
|
+
return await Effect.runPromise(generateCommand(addChair()));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
globalThis.describe("RuntimeExtension reaches the runtime callback hooks", () => {
|
|
49
|
+
globalThis.beforeEach(() => {
|
|
50
|
+
RuntimeExtension$ReventlessCore.reset();
|
|
51
|
+
CommandGenerator_Callback$ReventlessCore.clearCommandInterceptor();
|
|
52
|
+
published.contents = [];
|
|
53
|
+
});
|
|
54
|
+
globalThis.test("an interceptor registered at cold start denies a later command", async () => {
|
|
55
|
+
let seenAtColdStart = [];
|
|
56
|
+
let onColdStart = (param, component, param$1, param$2) => {
|
|
57
|
+
seenAtColdStart.push(component);
|
|
58
|
+
CommandGenerator_Callback$ReventlessCore.registerCommandInterceptor(async (param, componentName, param$1, tag, param$2) => ({
|
|
59
|
+
TAG: "Deny",
|
|
60
|
+
_0: componentName + `.` + tag + ` refused`
|
|
61
|
+
}));
|
|
62
|
+
};
|
|
63
|
+
let Gatekeeper = {
|
|
64
|
+
moduleUrl: "file:///pkg/gatekeeper.res.mjs",
|
|
65
|
+
onColdStart: onColdStart
|
|
66
|
+
};
|
|
67
|
+
RuntimeExtension$ReventlessCore.use(Gatekeeper);
|
|
68
|
+
RuntimeExtension$ReventlessCore.notifyColdStart("StateChangeSlice", "CatalogStateChanges", "Catalog", "Shop");
|
|
69
|
+
globalThis.expect(seenAtColdStart).toEqual(["CatalogStateChanges"]);
|
|
70
|
+
let denied;
|
|
71
|
+
try {
|
|
72
|
+
await runAdd();
|
|
73
|
+
denied = undefined;
|
|
74
|
+
} catch (raw_exn) {
|
|
75
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
76
|
+
denied = Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message);
|
|
77
|
+
}
|
|
78
|
+
globalThis.expect(denied).toEqual("Product.Add refused");
|
|
79
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
globalThis.test("with nothing registered the same command publishes untouched", async () => {
|
|
82
|
+
await runAdd();
|
|
83
|
+
globalThis.expect(published.contents.length).toBe(1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
commandSchema,
|
|
89
|
+
published,
|
|
90
|
+
publishJsons,
|
|
91
|
+
generateCommand,
|
|
92
|
+
addChair,
|
|
93
|
+
runAdd,
|
|
94
|
+
}
|
|
95
|
+
/* Not a pure module */
|