@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +8 -8
  3. package/src/Message.res +1 -1
  4. package/src/adapter/Monitoring/Monitoring.res +1 -1
  5. package/src/admin/Platform_Admin_Structure.res +1 -0
  6. package/src/admin/Platform_Admin_Structure.res.mjs +1 -0
  7. package/src/admin/Platform_ComponentDefinitionsApi.res +2 -1
  8. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
  9. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +1 -1
  10. package/src/components/Aggregate/Aggregate_Callback.res +2 -2
  11. package/src/components/Api/ApiAllowedStatesHelpers.res +2 -3
  12. package/src/components/Api/ApiTargetStateHelpers.res +4 -3
  13. package/src/components/Api/GraphQL_FragmentGenerator.res +122 -12
  14. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +41 -8
  15. package/src/components/Api/SuryToJsonSchema.res +8 -0
  16. package/src/components/Api/SuryToJsonSchema.res.mjs +8 -4
  17. package/src/components/Dcb/Dcb_Builder.res +79 -16
  18. package/src/components/Dcb/Dcb_Builder.res.mjs +41 -5
  19. package/src/components/EventLog/EventLog.res +1 -1
  20. package/src/plugin/component/Plugin_Builder.res +7 -0
  21. package/src/plugin/component/Plugin_Builder.res.mjs +3 -1
  22. package/src/plugin/component/Plugin_Structure.res +376 -27
  23. package/src/plugin/component/Plugin_Structure.res.mjs +175 -8
  24. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +1 -1
  25. package/tests/admin/Platform_BakedManifestTest.res +1 -0
  26. package/tests/admin/Platform_BakedManifestTest.res.mjs +1 -0
  27. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +3 -0
  28. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +6 -0
  29. package/tests/admin/Platform_PluginStructuresApiTest.res +1 -0
  30. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +2 -0
  31. package/tests/aggregate/AggregateCacheTest.res +1 -1
  32. package/tests/aggregate/AggregateSnapshotTest.res +1 -1
  33. package/tests/api/GraphQL_FragmentGeneratorTest.res +171 -0
  34. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +108 -0
  35. package/tests/api/SuryToJsonSchemaTest.res +30 -3
  36. package/tests/api/SuryToJsonSchemaTest.res.mjs +49 -4
  37. package/tests/commandgenerator/OwnerStampingTest.res +57 -0
  38. package/tests/commandgenerator/OwnerStampingTest.res.mjs +19 -0
  39. package/tests/message/MessageTest.res +1 -1
  40. package/tests/plugin/HeartbeatDisconnectGraceTest.res +1 -1
  41. package/tests/plugin/PluginStructureTest.res +268 -4
  42. package/tests/plugin/PluginStructureTest.res.mjs +284 -3
  43. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res +31 -0
  44. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +73 -0
  45. package/tests/plugin/StateChangeSlice/PsShipOrder.res +16 -1
  46. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -2
  47. package/tests/plugin/StateViewSlice/PsShipmentsView.res +26 -0
  48. package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +101 -0
  49. 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
- // Shared generateCommand for AppSync direct invocations — all sync StateChangeSlices
765
- // share the same DCB CommandTopic's publishJsons, so a single function suffices.
766
- // commandSchema validation is skipped (permissive JSON.t schema) because AppSync
767
- // already validates input against the SDL.
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
- CommandGenerator_Callback.makeGenerateCommand(
773
- ~publishJsons=ops.publishJsons,
774
- ~publishJsonsAndWait=?ops.publishJsonsAndWait,
775
- ~serviceName=name,
776
- ~commandSchema=S.json->S.castToUnknown,
777
- ~componentKind=CommandGenerator_Callback.StateChangeSlice,
778
- ~stripIdFromParams=false,
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, generateCommand)) => {
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 dcbGenerateCommandOutput = Component$ReventlessCore.operations(dcbCommandTopic).apply(ops => CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(ops.publishJsons, ops.publishJsonsAndWait, name, S.json, "StateChangeSlice", false));
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 generateCommand = param[2];
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 (typeof match$1 === "string" && match$2 !== undefined) {
452
- return Effect.map(generateCommand(event), msgId => msgId);
453
- } else {
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);