@reventlessdev/reventless-core 3.0.0-alpha.237 → 3.0.0-alpha.239
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 +24 -0
- package/package.json +8 -8
- package/src/Message.res +1 -1
- package/src/adapter/Monitoring/Monitoring.res +1 -1
- package/src/admin/Platform_Admin_Structure.res +1 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +2 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate_Callback.res +2 -2
- package/src/components/Api/ApiAllowedStatesHelpers.res +2 -3
- package/src/components/Api/ApiTargetStateHelpers.res +4 -3
- package/src/components/Api/GraphQL_FragmentGenerator.res +122 -12
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +41 -8
- package/src/components/Api/SuryToJsonSchema.res +8 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +8 -4
- package/src/components/Dcb/Dcb_Builder.res +79 -16
- package/src/components/Dcb/Dcb_Builder.res.mjs +41 -5
- package/src/components/EventLog/EventLog.res +1 -1
- package/src/plugin/component/Plugin_Builder.res +7 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -1
- package/src/plugin/component/Plugin_Structure.res +376 -27
- package/src/plugin/component/Plugin_Structure.res.mjs +175 -8
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +1 -1
- package/tests/admin/Platform_BakedManifestTest.res +1 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +1 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +3 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +6 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res +1 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +2 -0
- package/tests/aggregate/AggregateCacheTest.res +1 -1
- package/tests/aggregate/AggregateSnapshotTest.res +1 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res +171 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +108 -0
- package/tests/api/SuryToJsonSchemaTest.res +30 -3
- package/tests/api/SuryToJsonSchemaTest.res.mjs +49 -4
- package/tests/commandgenerator/OwnerStampingTest.res +57 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +19 -0
- package/tests/message/MessageTest.res +1 -1
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +1 -1
- package/tests/plugin/PluginStructureTest.res +268 -4
- package/tests/plugin/PluginStructureTest.res.mjs +284 -3
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res +31 -0
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +73 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +16 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -2
- package/tests/plugin/StateViewSlice/PsShipmentsView.res +26 -0
- package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +101 -0
- package/tests/util/CsvStreamTest.res +1 -1
|
@@ -312,7 +312,7 @@ module Make = (
|
|
|
312
312
|
// sweeps up sibling products. Cross-partition fan-out and capacity reads are
|
|
313
313
|
// preserved (generalised rule 3). Safety guard: if any slice's partition is
|
|
314
314
|
// ambiguous we keep the annotated values for the whole boundary rather than
|
|
315
|
-
// thread a partial inference. See docs/plans/dcb-tag-scope-inference.md.
|
|
315
|
+
// thread a partial inference. See docs/plans/done/dcb-tag-scope-inference.md.
|
|
316
316
|
let inferenceShapes =
|
|
317
317
|
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) =>
|
|
318
318
|
Reventless.DcbTag.sliceShapeFromSchemas(
|
|
@@ -761,28 +761,80 @@ module Make = (
|
|
|
761
761
|
// + AppSync direct invocations (CommandGenerator.payload format)
|
|
762
762
|
let dcbHandlerBase = DcbCommandTopic.makeFilteringHandler(dcbCommandTopic)
|
|
763
763
|
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
//
|
|
764
|
+
// generateCommand for AppSync direct invocations, one per command constructor.
|
|
765
|
+
//
|
|
766
|
+
// Every one of them shares this CommandTopic's publishJsons — the reason a
|
|
767
|
+
// single function used to suffice — and they differ in exactly one argument:
|
|
768
|
+
// the schema. That argument is not only a validator. `makeGenerateCommand`
|
|
769
|
+
// also reads it to find the fields a command marks as its owner, and stamps
|
|
770
|
+
// them with the caller. Handing it the permissive `S.json` (as this did, on
|
|
771
|
+
// the still-true grounds that AppSync has already validated against the SDL)
|
|
772
|
+
// makes that lookup answer "no owner fields" for every command there is — so
|
|
773
|
+
// the write publishes whatever owner the CLIENT sent, silently, and the row
|
|
774
|
+
// is wrong from then on.
|
|
775
|
+
//
|
|
776
|
+
// Per-slice decoding still happens inside `buildSliceHandler`; nothing here
|
|
777
|
+
// re-validates. The schema travels for what it says about ownership.
|
|
778
|
+
let dcbCommandSchemasByTag = {
|
|
779
|
+
let byTag = Dict.make()
|
|
780
|
+
stateChangeSlices->Array.forEach((module(S: StateChangeSlice.T)) => {
|
|
781
|
+
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
782
|
+
if !(ApiNoApiHelpers.isNoApi(commandSchema)) {
|
|
783
|
+
Api_Naming.sliceMutationFields(
|
|
784
|
+
~plugin=apiNamePrefix,
|
|
785
|
+
~slice=S.Spec.name,
|
|
786
|
+
~commandSchema,
|
|
787
|
+
)->Array.forEach(((_, tag)) => {
|
|
788
|
+
// The resolver injects the TAG alone, so two slices claiming one
|
|
789
|
+
// constructor name would make the stamp ambiguous — and the wrong
|
|
790
|
+
// arm of that ambiguity writes a row owned by nobody's decision.
|
|
791
|
+
// Multi-command slices cannot reach here (their mutation fields
|
|
792
|
+
// collide first), which leaves single-command slices, where the
|
|
793
|
+
// field names differ and nothing else would notice.
|
|
794
|
+
switch byTag->Dict.get(tag) {
|
|
795
|
+
| Some(_) =>
|
|
796
|
+
log.error(
|
|
797
|
+
~comp="Dcb_Builder",
|
|
798
|
+
`Two DCB slices in "${name}" both declare the command "${tag}". ` ++
|
|
799
|
+
"Owner stamping resolves a command by that name alone, so it cannot " ++
|
|
800
|
+
"tell them apart. Rename one of the constructors.",
|
|
801
|
+
)
|
|
802
|
+
| None => byTag->Dict.set(tag, commandSchema)
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
}
|
|
806
|
+
})
|
|
807
|
+
byTag
|
|
808
|
+
}
|
|
809
|
+
|
|
768
810
|
let dcbGenerateCommandOutput =
|
|
769
811
|
dcbCommandTopic
|
|
770
812
|
->Component.operations
|
|
771
|
-
->Pulumi.Output.apply(ops =>
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
813
|
+
->Pulumi.Output.apply(ops => {
|
|
814
|
+
let make = commandSchema =>
|
|
815
|
+
CommandGenerator_Callback.makeGenerateCommand(
|
|
816
|
+
~publishJsons=ops.publishJsons,
|
|
817
|
+
~publishJsonsAndWait=?ops.publishJsonsAndWait,
|
|
818
|
+
~serviceName=name,
|
|
819
|
+
~commandSchema,
|
|
820
|
+
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
821
|
+
~stripIdFromParams=false,
|
|
822
|
+
)
|
|
823
|
+
let byTag = Dict.make()
|
|
824
|
+
dcbCommandSchemasByTag
|
|
825
|
+
->Dict.toArray
|
|
826
|
+
->Array.forEach(((tag, schema)) => byTag->Dict.set(tag, make(schema)))
|
|
827
|
+
// A command no slice claims keeps the permissive generator — what every
|
|
828
|
+
// command got before this change — so nothing that reaches this handler
|
|
829
|
+
// by another route starts failing. It also cannot stamp, which is why
|
|
830
|
+
// the unknown case is logged rather than left to look like a success.
|
|
831
|
+
(byTag, make(S.json->S.castToUnknown))
|
|
832
|
+
})
|
|
781
833
|
|
|
782
834
|
let dcbHandler =
|
|
783
835
|
(dcbHandlerBase, inboundReceiversOutput, dcbGenerateCommandOutput)
|
|
784
836
|
->Pulumi.Output.all3
|
|
785
|
-
->Pulumi.Output.apply(((baseHandler, receivers,
|
|
837
|
+
->Pulumi.Output.apply(((baseHandler, receivers, (generatorsByTag, generateFallback))) => {
|
|
786
838
|
let composite = (event, ctx) => {
|
|
787
839
|
let raw: dict<JSON.t> = event->Obj.magic
|
|
788
840
|
switch raw->Dict.get("__inboundTranslation") {
|
|
@@ -810,6 +862,17 @@ module Make = (
|
|
|
810
862
|
switch (raw->Dict.get("command"), raw->Dict.get("arguments")) {
|
|
811
863
|
| (Some(JSON.String(_)), Some(_)) =>
|
|
812
864
|
let payload: CommandGenerator.payload = event->Obj.magic
|
|
865
|
+
let generateCommand = switch generatorsByTag->Dict.get(payload.command) {
|
|
866
|
+
| Some(g) => g
|
|
867
|
+
| None =>
|
|
868
|
+
log.warn(
|
|
869
|
+
~comp="Dcb_Builder",
|
|
870
|
+
`No DCB slice in "${name}" declares the command "${payload.command}", ` ++
|
|
871
|
+
"so it is dispatched without an owner stamp. Any field the command " ++
|
|
872
|
+
"marks as its owner keeps the value the caller sent.",
|
|
873
|
+
)
|
|
874
|
+
generateFallback
|
|
875
|
+
}
|
|
813
876
|
(generateCommand(payload)->Effect.map(msgId => msgId->Obj.magic))->Obj.magic
|
|
814
877
|
| _ => baseHandler(event, ctx)
|
|
815
878
|
}
|
|
@@ -422,13 +422,42 @@ function Make(DcbEventLogStorage) {
|
|
|
422
422
|
]);
|
|
423
423
|
})).apply(pairs => Object.fromEntries(pairs));
|
|
424
424
|
let dcbHandlerBase = DcbCommandTopic.makeFilteringHandler(dcbCommandTopic);
|
|
425
|
-
let
|
|
425
|
+
let byTag = {};
|
|
426
|
+
stateChangeSlices.forEach(S => {
|
|
427
|
+
let commandSchema = S.Spec.commandSchema;
|
|
428
|
+
if (!ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
|
|
429
|
+
Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).forEach(param => {
|
|
430
|
+
let tag = param[1];
|
|
431
|
+
let match = byTag[tag];
|
|
432
|
+
if (match !== undefined) {
|
|
433
|
+
return log.error("Dcb_Builder", undefined, `Two DCB slices in "` + name + `" both declare the command "` + tag + `". ` + "Owner stamping resolves a command by that name alone, so it cannot tell them apart. Rename one of the constructors.");
|
|
434
|
+
} else {
|
|
435
|
+
byTag[tag] = commandSchema;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
let dcbGenerateCommandOutput = Component$ReventlessCore.operations(dcbCommandTopic).apply(ops => {
|
|
443
|
+
let make = commandSchema => CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(ops.publishJsons, ops.publishJsonsAndWait, name, commandSchema, "StateChangeSlice", false);
|
|
444
|
+
let byTag$1 = {};
|
|
445
|
+
Object.entries(byTag).forEach(param => {
|
|
446
|
+
byTag$1[param[0]] = make(param[1]);
|
|
447
|
+
});
|
|
448
|
+
return [
|
|
449
|
+
byTag$1,
|
|
450
|
+
make(S.json)
|
|
451
|
+
];
|
|
452
|
+
});
|
|
426
453
|
let dcbHandler = Pulumi.all([
|
|
427
454
|
dcbHandlerBase,
|
|
428
455
|
inboundReceiversOutput,
|
|
429
456
|
dcbGenerateCommandOutput
|
|
430
457
|
]).apply(param => {
|
|
431
|
-
let
|
|
458
|
+
let match = param[2];
|
|
459
|
+
let generateFallback = match[1];
|
|
460
|
+
let generatorsByTag = match[0];
|
|
432
461
|
let receivers = param[1];
|
|
433
462
|
let baseHandler = param[0];
|
|
434
463
|
return (event, ctx) => {
|
|
@@ -448,11 +477,18 @@ function Make(DcbEventLogStorage) {
|
|
|
448
477
|
}
|
|
449
478
|
let match$1 = event["command"];
|
|
450
479
|
let match$2 = event["arguments"];
|
|
451
|
-
if (
|
|
452
|
-
return
|
|
453
|
-
}
|
|
480
|
+
if (match$1 === undefined) {
|
|
481
|
+
return baseHandler(event, ctx);
|
|
482
|
+
}
|
|
483
|
+
if (typeof match$1 !== "string") {
|
|
484
|
+
return baseHandler(event, ctx);
|
|
485
|
+
}
|
|
486
|
+
if (match$2 === undefined) {
|
|
454
487
|
return baseHandler(event, ctx);
|
|
455
488
|
}
|
|
489
|
+
let g = generatorsByTag[event.command];
|
|
490
|
+
let generateCommand = g !== undefined ? g : (log.warn("Dcb_Builder", undefined, `No DCB slice in "` + name + `" declares the command "` + event.command + `", ` + "so it is dispatched without an owner stamp. Any field the command marks as its owner keeps the value the caller sent."), generateFallback);
|
|
491
|
+
return Effect.map(generateCommand(event), msgId => msgId);
|
|
456
492
|
};
|
|
457
493
|
});
|
|
458
494
|
let dcbResources = Object.values(stateChangeSlicesOutputs).flatMap(outputs => outputs.resources).concat(Object.values(inboundTranslationSlicesOutputs).flatMap(outputs => outputs.resources.concat(outputs.queryDb.resources)));
|
|
@@ -25,7 +25,7 @@ type appendError =
|
|
|
25
25
|
// staleness — consumers ignore a snapshot whose hash differs from their current
|
|
26
26
|
// state schema and fall back to full replay. Snapshots are a read optimization
|
|
27
27
|
// only; the OCC append remains the sole consistency primitive
|
|
28
|
-
// (docs/plans/aggregate-snapshotting.md).
|
|
28
|
+
// (docs/plans/done/aggregate-snapshotting.md).
|
|
29
29
|
type snapshot = {seqNr: int, state: JSON.t, schemaHash: string}
|
|
30
30
|
|
|
31
31
|
type append<'id, 'event> = (int, 'id, array<'event>) => promise<result<unit, appendError>>
|
|
@@ -249,6 +249,12 @@ module Make = (
|
|
|
249
249
|
) => {
|
|
250
250
|
let qn = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
|
|
251
251
|
let subIdField = R.Spec.subIdConfig->Option.map(c => c.subIdField)
|
|
252
|
+
// An ordinary read model's `@index` reached the resolver layer
|
|
253
|
+
// (`QueryDbResolvers_{AppSync,Lambda}` build one per `config.indexes`)
|
|
254
|
+
// and never reached the schema, so AWS carried a resolver for a field
|
|
255
|
+
// its SDL did not declare. The DCB and admin entries have populated this
|
|
256
|
+
// all along; the omission here was the asymmetry, not the rule.
|
|
257
|
+
let indexes = R.Spec.config.indexes
|
|
252
258
|
{
|
|
253
259
|
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
254
260
|
listFieldName: qn.listFieldName,
|
|
@@ -258,6 +264,7 @@ module Make = (
|
|
|
258
264
|
permission: R.Spec.authorization,
|
|
259
265
|
connectionSpec: true,
|
|
260
266
|
subIdField: ?subIdField,
|
|
267
|
+
indexQueries: ?(indexes->Array.length > 0 ? Some(indexes) : None),
|
|
261
268
|
}
|
|
262
269
|
})
|
|
263
270
|
|
|
@@ -194,6 +194,7 @@ function Make(Spec) {
|
|
|
194
194
|
let queryEntriesFromReadModels = readModels.map(R => {
|
|
195
195
|
let qn = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(extra$1, R.Spec.name, undefined);
|
|
196
196
|
let subIdField = Stdlib_Option.map(R.Spec.subIdConfig, c => c.subIdField);
|
|
197
|
+
let indexes = R.Spec.config.indexes;
|
|
197
198
|
return {
|
|
198
199
|
singleFieldName: qn.singleFieldName,
|
|
199
200
|
listFieldName: qn.listFieldName,
|
|
@@ -202,7 +203,8 @@ function Make(Spec) {
|
|
|
202
203
|
authorization: undefined,
|
|
203
204
|
permission: R.Spec.authorization,
|
|
204
205
|
connectionSpec: true,
|
|
205
|
-
subIdField: subIdField
|
|
206
|
+
subIdField: subIdField,
|
|
207
|
+
indexQueries: indexes.length !== 0 ? indexes : undefined
|
|
206
208
|
};
|
|
207
209
|
});
|
|
208
210
|
let queryEntries = queryEntriesFromReadModels.concat(dcbResult.queryEntries);
|