@reventlessdev/reventless-core 3.0.0-alpha.261 → 3.0.0-alpha.263
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 +31 -0
- package/package.json +9 -9
- package/src/adapter/Secrets/Secrets_Node_Backend.res +60 -0
- package/src/adapter/Secrets/Secrets_Node_Backend.res.mjs +69 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
- package/src/components/Aggregate/Aggregate_Builder.res.mjs +1 -1
- package/src/components/CommandGenerator/CommandGenerator.res +4 -4
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +13 -20
- package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +3 -24
- package/src/components/Dcb/Dcb_Builder.res +45 -49
- package/src/components/Dcb/Dcb_Builder.res.mjs +31 -40
- package/src/components/StateChangeSlice/StateChangeSlice.res +2 -0
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +6 -0
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res.mjs +4 -4
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +51 -22
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +47 -25
- package/src/plugin/component/Plugin_Structure.res +101 -4
- package/src/plugin/component/Plugin_Structure.res.mjs +82 -9
- package/tests/Secrets_Node_BackendTest.res +141 -0
- package/tests/Secrets_Node_BackendTest.res.mjs +152 -0
- package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +1 -1
- package/tests/admin/Platform_Admin_StructureTest.res +12 -3
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +4 -3
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res +1 -1
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res.mjs +1 -1
- package/tests/commandgenerator/CommandGeneratorFixtures.res +2 -0
- package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +17 -3
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +2 -2
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +1 -1
- package/tests/dcb/DcbFixtures.res +18 -1
- package/tests/dcb/DcbFixtures.res.mjs +14 -0
- package/tests/dcb/DcbScopeInferenceTest.res +234 -0
- package/tests/dcb/DcbScopeInferenceTest.res.mjs +425 -13
- package/tests/dcb/DcbStateChangeSliceTest.res.mjs +22 -22
- package/tests/dcb/DcbTagTest.res +81 -68
- package/tests/dcb/DcbTagTest.res.mjs +88 -89
- package/tests/plugin/PluginStructureAccessTest.res +1 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +1 -1
- package/tests/plugin/PluginStructureTest.res +47 -0
- package/tests/plugin/PluginStructureTest.res.mjs +50 -10
|
@@ -345,6 +345,40 @@ let reportTranslationTables = (
|
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
let describePartition = (pt: Reventless.DcbTag.derivedPartitionTag) =>
|
|
349
|
+
switch pt {
|
|
350
|
+
| Simple({key}) => key
|
|
351
|
+
| Composite({keys}) => keys->Array.join(" + ")
|
|
352
|
+
| ByEventType(_) => "a key per event type"
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// An extension groups the commands it sends a DCB slice by the slice's partition
|
|
356
|
+
// key, but it infers that key from the slice alone: the command and the events,
|
|
357
|
+
// not what the slice reads. Where the plugin needs those reads — or the chapter —
|
|
358
|
+
// to decide, the two can differ, and the extension would fail or group commands
|
|
359
|
+
// by the wrong id at run time. `expected` is `None` when the plugin itself cannot
|
|
360
|
+
// derive a partition; that failure is the builder's to report.
|
|
361
|
+
let extensionPartitionFailure = (
|
|
362
|
+
~label: string,
|
|
363
|
+
~delegate: string,
|
|
364
|
+
~expected: option<Reventless.DcbTag.derivedPartitionTag>,
|
|
365
|
+
~actual: result<Reventless.DcbTag.derivedPartitionTag, string>,
|
|
366
|
+
): option<string> =>
|
|
367
|
+
switch (expected, actual) {
|
|
368
|
+
| (None, _) => None
|
|
369
|
+
| (Some(e), Ok(a)) if e == a => None
|
|
370
|
+
| (Some(e), outcome) =>
|
|
371
|
+
let seen = switch outcome {
|
|
372
|
+
| Ok(a) => `groups its commands by ${a->describePartition}`
|
|
373
|
+
| Error(reason) => `cannot tell which key to group its commands by (${reason})`
|
|
374
|
+
}
|
|
375
|
+
Some(
|
|
376
|
+
`${label}: the extension ${seen}, but the plugin partitions ${delegate} by ${e->describePartition}. ` ++
|
|
377
|
+
`An extension sees only the slice's command and events, not what it reads or its chapter — ` ++
|
|
378
|
+
`add @partitionTag ${e->describePartition} to ${delegate}'s event.`,
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
348
382
|
// A command's declared edge names states belonging to another component's enum,
|
|
349
383
|
// and nothing forces a spec to pick the one its linked view declares. Both sides
|
|
350
384
|
// are in hand here. A state the linked views do not declare raises (this runs at assembly,
|
|
@@ -636,7 +670,15 @@ let isCreateCommandName = name =>
|
|
|
636
670
|
name->String.startsWith(p)
|
|
637
671
|
)
|
|
638
672
|
|
|
639
|
-
|
|
673
|
+
// A slice command's id field is the one carrying its slice's partition key, as the
|
|
674
|
+
// boundary infers it; a `@partitionTag` or the first tagged field stands in only
|
|
675
|
+
// where no key was inferred.
|
|
676
|
+
let commandLevelAndId = (
|
|
677
|
+
~isAggregate,
|
|
678
|
+
~partitionKey: option<string>=?,
|
|
679
|
+
~variantName,
|
|
680
|
+
properties: dict<S.t<unknown>>,
|
|
681
|
+
) =>
|
|
640
682
|
if isAggregate {
|
|
641
683
|
if isCreateCommandName(variantName) {
|
|
642
684
|
(Reventless.Plugin.Collection, None)
|
|
@@ -651,9 +693,18 @@ let commandLevelAndId = (~isAggregate, ~variantName, properties: dict<S.t<unknow
|
|
|
651
693
|
fieldName != "TAG" &&
|
|
652
694
|
(Reventless.DcbTag.isTagged(fieldSchema) || Reventless.DcbTag.isTaggedArray(fieldSchema))
|
|
653
695
|
)
|
|
696
|
+
let keyOf = ((fieldName, fieldSchema)) =>
|
|
697
|
+
Reventless.DcbTag.isTaggedArray(fieldSchema)
|
|
698
|
+
? Reventless.DcbTag.resolveArrayTagKey(fieldName, fieldSchema)
|
|
699
|
+
: Reventless.DcbTag.resolveTagKey(fieldName, fieldSchema)
|
|
654
700
|
let taggedField =
|
|
655
|
-
|
|
656
|
-
->Array.find(
|
|
701
|
+
partitionKey
|
|
702
|
+
->Option.flatMap(key => taggedFields->Array.find(field => keyOf(field) == key))
|
|
703
|
+
->Option.orElse(
|
|
704
|
+
taggedFields->Array.find(((_, fieldSchema)) =>
|
|
705
|
+
Reventless.DcbTag.isPartitionTag(fieldSchema)
|
|
706
|
+
),
|
|
707
|
+
)
|
|
657
708
|
->Option.orElse(taggedFields->Array.get(0))
|
|
658
709
|
switch taggedField {
|
|
659
710
|
| Some((fieldName, _)) =>
|
|
@@ -700,6 +751,7 @@ let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
|
|
|
700
751
|
|
|
701
752
|
let toCommandDef = (
|
|
702
753
|
~isAggregate,
|
|
754
|
+
~partitionKey: option<string>=?,
|
|
703
755
|
~mutationFieldFor: string => string,
|
|
704
756
|
~parentSchema: S.t<unknown>,
|
|
705
757
|
// The PPX-generated `command => permission`. Per VARIANT, not per component:
|
|
@@ -727,7 +779,12 @@ let toCommandDef = (
|
|
|
727
779
|
// empty for a payload-less variant (e.g. `| Archive`), which compiles to a bare
|
|
728
780
|
// `S.literal("Archive")` string rather than an `{TAG, ...}` object.
|
|
729
781
|
let mkDef = (~variantName, ~properties) => {
|
|
730
|
-
let (guessedLevel, aggregateIdField) = commandLevelAndId(
|
|
782
|
+
let (guessedLevel, aggregateIdField) = commandLevelAndId(
|
|
783
|
+
~isAggregate,
|
|
784
|
+
~partitionKey?,
|
|
785
|
+
~variantName,
|
|
786
|
+
properties,
|
|
787
|
+
)
|
|
731
788
|
let references = extractReferences(properties)
|
|
732
789
|
// Evaluated against a synthetic value per constructor, the same shape the
|
|
733
790
|
// resolver builds at call time: a payload-bearing variant compiles to
|
|
@@ -867,6 +924,7 @@ let toCommandDef = (
|
|
|
867
924
|
|
|
868
925
|
let extractCommandDefs = (
|
|
869
926
|
~isAggregate,
|
|
927
|
+
~partitionKey: option<string>=?,
|
|
870
928
|
~mutationFieldFor: string => string,
|
|
871
929
|
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
872
930
|
~commandTransition: unknown => Reventless.Transition.t<string>,
|
|
@@ -878,6 +936,7 @@ let extractCommandDefs = (
|
|
|
878
936
|
anyOf->Array.filterMap(v =>
|
|
879
937
|
toCommandDef(
|
|
880
938
|
~isAggregate,
|
|
939
|
+
~partitionKey?,
|
|
881
940
|
~mutationFieldFor,
|
|
882
941
|
~parentSchema=commandSchema,
|
|
883
942
|
~commandAuthorization,
|
|
@@ -890,6 +949,7 @@ let extractCommandDefs = (
|
|
|
890
949
|
// Single-variant command types compile to a bare Object schema, not a Union.
|
|
891
950
|
toCommandDef(
|
|
892
951
|
~isAggregate,
|
|
952
|
+
~partitionKey?,
|
|
893
953
|
~mutationFieldFor,
|
|
894
954
|
~parentSchema=commandSchema,
|
|
895
955
|
~commandAuthorization,
|
|
@@ -1445,6 +1505,30 @@ let make = (
|
|
|
1445
1505
|
|
|
1446
1506
|
// ── Build writable defs ────────────────────────────────────────────────────
|
|
1447
1507
|
|
|
1508
|
+
// Non-throwing: this is a description of the plugin, and the builder is what
|
|
1509
|
+
// refuses a boundary whose partitions do not resolve.
|
|
1510
|
+
let dcbSliceSchemas =
|
|
1511
|
+
stateChangeSlices->Array.map((module(SCS: ReventlessInfra.StateChangeSlice.T)) => {
|
|
1512
|
+
Reventless.DcbTag.name: SCS.Spec.name,
|
|
1513
|
+
commandSchema: SCS.Spec.commandSchema->S.castToUnknown,
|
|
1514
|
+
consumedEventSchema: SCS.Spec.consumedEventSchema->S.castToUnknown,
|
|
1515
|
+
eventSchema: SCS.Spec.eventSchema->S.castToUnknown,
|
|
1516
|
+
moduleUrl: SCS.Spec.moduleUrl,
|
|
1517
|
+
})
|
|
1518
|
+
let {partitionBySlice}: Reventless.DcbScopeInference.partitionResolution =
|
|
1519
|
+
dcbSliceSchemas
|
|
1520
|
+
->Array.map(Reventless.DcbTag.sliceShape)
|
|
1521
|
+
->Reventless.DcbScopeInference.resolvePartitions
|
|
1522
|
+
let slicePartitionOf = switch Reventless.DcbTag.deriveBoundaryPartition(dcbSliceSchemas) {
|
|
1523
|
+
| boundary =>
|
|
1524
|
+
delegate =>
|
|
1525
|
+
dcbSliceSchemas->Array.some(s => s.name == delegate)
|
|
1526
|
+
? boundary->Reventless.DcbTag.slicePartitionTag(delegate)
|
|
1527
|
+
: None
|
|
1528
|
+
| exception JsExn(_) => _ => None
|
|
1529
|
+
}
|
|
1530
|
+
let partitionFailures = []
|
|
1531
|
+
|
|
1448
1532
|
let stateChangeDefs =
|
|
1449
1533
|
stateChangeSlices->Array.mapWithIndex((module(SCS: ReventlessInfra.StateChangeSlice.T), i) => {
|
|
1450
1534
|
let (_, produced) = scsProduced->Array.getUnsafe(i)
|
|
@@ -1455,6 +1539,7 @@ let make = (
|
|
|
1455
1539
|
Reventless.Plugin.name: SCS.Spec.name,
|
|
1456
1540
|
commands: extractCommandDefs(
|
|
1457
1541
|
~isAggregate=false,
|
|
1542
|
+
~partitionKey=?partitionBySlice->Dict.get(SCS.Spec.name),
|
|
1458
1543
|
~mutationFieldFor=variantName =>
|
|
1459
1544
|
Api_Naming.sliceMutationFieldFor(
|
|
1460
1545
|
~plugin=name,
|
|
@@ -1592,6 +1677,12 @@ let make = (
|
|
|
1592
1677
|
let eventsByCommand: Dict.t<array<string>> = Dict.make()
|
|
1593
1678
|
E.mappings->Array.forEach((module(M: E.Mapping)) => {
|
|
1594
1679
|
let label = `${E.Spec.name} → ${M.delegateName}`
|
|
1680
|
+
extensionPartitionFailure(
|
|
1681
|
+
~label,
|
|
1682
|
+
~delegate=M.delegateName,
|
|
1683
|
+
~expected=slicePartitionOf(M.delegateName),
|
|
1684
|
+
~actual=M.delegatePartition(),
|
|
1685
|
+
)->Option.forEach(f => partitionFailures->Array.push(f))
|
|
1595
1686
|
pushAll(
|
|
1596
1687
|
tableFailures,
|
|
1597
1688
|
handledTableFailures(
|
|
@@ -1965,6 +2056,12 @@ let make = (
|
|
|
1965
2056
|
})
|
|
1966
2057
|
|
|
1967
2058
|
reportTranslationTables(~pluginName=name, ~failures=tableFailures, ~warnings=tableWarnings)
|
|
2059
|
+
if partitionFailures->Array.length > 0 {
|
|
2060
|
+
JsError.throwWithMessage(
|
|
2061
|
+
`${name}: an extension cannot group its commands by the target slice's partition key.\n` ++
|
|
2062
|
+
partitionFailures->Array.join("\n"),
|
|
2063
|
+
)
|
|
2064
|
+
}
|
|
1968
2065
|
|
|
1969
2066
|
// Second pass, on purpose: commands are built well before `linkedViews` is
|
|
1970
2067
|
// assembled, so the check cannot run inline where the defs are made.
|
|
@@ -13,6 +13,7 @@ import * as Trait$Reventless from "@reventlessdev/reventless-spec/src/types/Trai
|
|
|
13
13
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
14
14
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
15
15
|
import * as Lifecycle$Reventless from "@reventlessdev/reventless-spec/src/types/Lifecycle.res.mjs";
|
|
16
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
16
17
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
17
18
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
18
19
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
@@ -23,6 +24,7 @@ import * as Api_Naming$ReventlessCore from "../../components/Api/Api_Naming.res.
|
|
|
23
24
|
import * as CapabilityNeed$Reventless from "@reventlessdev/reventless-spec/src/semantic/CapabilityNeed.res.mjs";
|
|
24
25
|
import * as SchemaType$ReventlessCore from "../../components/Api/SchemaType.res.mjs";
|
|
25
26
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
27
|
+
import * as DcbScopeInference$Reventless from "@reventlessdev/reventless-spec/src/components/DcbScopeInference.res.mjs";
|
|
26
28
|
import * as ApiNoApiHelpers$ReventlessCore from "../../components/Api/ApiNoApiHelpers.res.mjs";
|
|
27
29
|
import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJsonSchema.res.mjs";
|
|
28
30
|
import * as Capability_Inference$ReventlessCore from "./Capability_Inference.res.mjs";
|
|
@@ -289,6 +291,29 @@ function reportTranslationTables(pluginName, failures, warnings) {
|
|
|
289
291
|
}
|
|
290
292
|
}
|
|
291
293
|
|
|
294
|
+
function describePartition(pt) {
|
|
295
|
+
switch (pt.TAG) {
|
|
296
|
+
case "Simple" :
|
|
297
|
+
return pt._0.key;
|
|
298
|
+
case "Composite" :
|
|
299
|
+
return pt._0.keys.join(" + ");
|
|
300
|
+
case "ByEventType" :
|
|
301
|
+
return "a key per event type";
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function extensionPartitionFailure(label, delegate, expected, actual) {
|
|
306
|
+
if (expected === undefined) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (actual.TAG === "Ok" && Primitive_object.equal(expected, actual._0)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
let seen;
|
|
313
|
+
seen = actual.TAG === "Ok" ? `groups its commands by ` + describePartition(actual._0) : `cannot tell which key to group its commands by (` + actual._0 + `)`;
|
|
314
|
+
return label + `: the extension ` + seen + `, but the plugin partitions ` + delegate + ` by ` + describePartition(expected) + `. An extension sees only the slice's command and events, not what it reads or its chapter — ` + (`add @partitionTag ` + describePartition(expected) + ` to ` + delegate + `'s event.`);
|
|
315
|
+
}
|
|
316
|
+
|
|
292
317
|
function checkDeclaredTransitions(pluginName, writables, lifecycleStatesByView) {
|
|
293
318
|
let unvalidated = {
|
|
294
319
|
contents: 0
|
|
@@ -492,7 +517,7 @@ function isCreateCommandName(name) {
|
|
|
492
517
|
].some(p => name.startsWith(p));
|
|
493
518
|
}
|
|
494
519
|
|
|
495
|
-
function commandLevelAndId(isAggregate, variantName, properties) {
|
|
520
|
+
function commandLevelAndId(isAggregate, partitionKey, variantName, properties) {
|
|
496
521
|
if (isAggregate) {
|
|
497
522
|
if (isCreateCommandName(variantName)) {
|
|
498
523
|
return [
|
|
@@ -518,7 +543,16 @@ function commandLevelAndId(isAggregate, variantName, properties) {
|
|
|
518
543
|
return false;
|
|
519
544
|
}
|
|
520
545
|
});
|
|
521
|
-
let
|
|
546
|
+
let keyOf = param => {
|
|
547
|
+
let fieldSchema = param[1];
|
|
548
|
+
let fieldName = param[0];
|
|
549
|
+
if (DcbTag$Reventless.isTaggedArray(fieldSchema)) {
|
|
550
|
+
return DcbTag$Reventless.resolveArrayTagKey(fieldName, fieldSchema);
|
|
551
|
+
} else {
|
|
552
|
+
return DcbTag$Reventless.resolveTagKey(fieldName, fieldSchema);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
let taggedField = Stdlib_Option.orElse(Stdlib_Option.orElse(Stdlib_Option.flatMap(partitionKey, key => taggedFields.find(field => keyOf(field) === key)), taggedFields.find(param => DcbTag$Reventless.isPartitionTag(param[1]))), taggedFields[0]);
|
|
522
556
|
if (taggedField === undefined) {
|
|
523
557
|
return [
|
|
524
558
|
"Collection",
|
|
@@ -563,9 +597,9 @@ function annotateArgTypes(schema, argTypes) {
|
|
|
563
597
|
return schema;
|
|
564
598
|
}
|
|
565
599
|
|
|
566
|
-
function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, derivedEdgeFor, v) {
|
|
600
|
+
function toCommandDef(isAggregate, partitionKey, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, derivedEdgeFor, v) {
|
|
567
601
|
let mkDef = (variantName, properties) => {
|
|
568
|
-
let match = commandLevelAndId(isAggregate, variantName, properties);
|
|
602
|
+
let match = commandLevelAndId(isAggregate, partitionKey, variantName, properties);
|
|
569
603
|
let references = extractReferences(properties);
|
|
570
604
|
let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
|
|
571
605
|
TAG: variantName
|
|
@@ -649,12 +683,12 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
|
|
|
649
683
|
}
|
|
650
684
|
}
|
|
651
685
|
|
|
652
|
-
function extractCommandDefs(isAggregate, mutationFieldFor, commandAuthorization, commandTransition, derivedEdgeForOpt, commandSchema) {
|
|
686
|
+
function extractCommandDefs(isAggregate, partitionKey, mutationFieldFor, commandAuthorization, commandTransition, derivedEdgeForOpt, commandSchema) {
|
|
653
687
|
let derivedEdgeFor = derivedEdgeForOpt !== undefined ? derivedEdgeForOpt : param => {};
|
|
654
688
|
if (commandSchema.type === "anyOf") {
|
|
655
|
-
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, v));
|
|
689
|
+
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, partitionKey, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, v));
|
|
656
690
|
} else {
|
|
657
|
-
return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, commandSchema), [], def => [def]);
|
|
691
|
+
return Stdlib_Option.mapOr(toCommandDef(isAggregate, partitionKey, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, commandSchema), [], def => [def]);
|
|
658
692
|
}
|
|
659
693
|
}
|
|
660
694
|
|
|
@@ -1036,6 +1070,37 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1036
1070
|
requiredAccess: accessKeysFor(SVS.Spec.authorization)
|
|
1037
1071
|
};
|
|
1038
1072
|
});
|
|
1073
|
+
let dcbSliceSchemas = stateChangeSlices.map(SCS => ({
|
|
1074
|
+
name: SCS.Spec.name,
|
|
1075
|
+
commandSchema: SCS.Spec.commandSchema,
|
|
1076
|
+
consumedEventSchema: SCS.Spec.consumedEventSchema,
|
|
1077
|
+
eventSchema: SCS.Spec.eventSchema,
|
|
1078
|
+
moduleUrl: SCS.Spec.moduleUrl
|
|
1079
|
+
}));
|
|
1080
|
+
let match = DcbScopeInference$Reventless.resolvePartitions(dcbSliceSchemas.map(DcbTag$Reventless.sliceShape));
|
|
1081
|
+
let partitionBySlice = match.partitionBySlice;
|
|
1082
|
+
let slicePartitionOf;
|
|
1083
|
+
let exit = 0;
|
|
1084
|
+
let boundary;
|
|
1085
|
+
try {
|
|
1086
|
+
boundary = DcbTag$Reventless.deriveBoundaryPartition(dcbSliceSchemas);
|
|
1087
|
+
exit = 1;
|
|
1088
|
+
} catch (raw_exn) {
|
|
1089
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
1090
|
+
if (exn.RE_EXN_ID === "JsExn") {
|
|
1091
|
+
slicePartitionOf = param => {};
|
|
1092
|
+
} else {
|
|
1093
|
+
throw exn;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (exit === 1) {
|
|
1097
|
+
slicePartitionOf = delegate => {
|
|
1098
|
+
if (dcbSliceSchemas.some(s => s.name === delegate)) {
|
|
1099
|
+
return DcbTag$Reventless.slicePartitionTag(boundary, delegate);
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
let partitionFailures = [];
|
|
1039
1104
|
let stateChangeDefs = stateChangeSlices.map((SCS, i) => {
|
|
1040
1105
|
let match = scsProduced[i];
|
|
1041
1106
|
let produced = match[1];
|
|
@@ -1043,7 +1108,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1043
1108
|
let consumed = match$1[1];
|
|
1044
1109
|
return {
|
|
1045
1110
|
name: SCS.Spec.name,
|
|
1046
|
-
commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, extra => derivedEdgeFor(SCS.Spec.name, extra), SCS.Spec.commandSchema),
|
|
1111
|
+
commands: extractCommandDefs(false, partitionBySlice[SCS.Spec.name], variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, extra => derivedEdgeFor(SCS.Spec.name, extra), SCS.Spec.commandSchema),
|
|
1047
1112
|
producedEventTypes: produced,
|
|
1048
1113
|
consumedEventTypes: consumed,
|
|
1049
1114
|
linkedViews: linkedSvsFor(produced),
|
|
@@ -1058,7 +1123,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1058
1123
|
let produced = match[1];
|
|
1059
1124
|
return {
|
|
1060
1125
|
name: A.Spec.name,
|
|
1061
|
-
commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, extra => derivedEdgeFor(A.Spec.name, extra), A.Spec.commandSchema),
|
|
1126
|
+
commands: extractCommandDefs(true, undefined, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, extra => derivedEdgeFor(A.Spec.name, extra), A.Spec.commandSchema),
|
|
1062
1127
|
producedEventTypes: produced,
|
|
1063
1128
|
consumedEventTypes: [],
|
|
1064
1129
|
linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
|
|
@@ -1109,6 +1174,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1109
1174
|
let eventsByCommand = {};
|
|
1110
1175
|
E.mappings.forEach(M => {
|
|
1111
1176
|
let label = E.Spec.name + ` → ` + M.delegateName;
|
|
1177
|
+
Stdlib_Option.forEach(extensionPartitionFailure(label, M.delegateName, slicePartitionOf(M.delegateName), M.delegatePartition()), f => {
|
|
1178
|
+
partitionFailures.push(f);
|
|
1179
|
+
});
|
|
1112
1180
|
pushAll(tableFailures, handledTableFailures(label, M.handledEvents, epEventNames, M.delegateCommandNames.concat(epCommandNames)));
|
|
1113
1181
|
pushAll(tableFailures, commandTableFailures(label, "issuedCommands", "comes from", M.issuedCommands.map(param => [
|
|
1114
1182
|
param.name,
|
|
@@ -1330,6 +1398,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1330
1398
|
});
|
|
1331
1399
|
});
|
|
1332
1400
|
reportTranslationTables(name, tableFailures, tableWarnings);
|
|
1401
|
+
if (partitionFailures.length !== 0) {
|
|
1402
|
+
Stdlib_JsError.throwWithMessage(name + `: an extension cannot group its commands by the target slice's partition key.\n` + partitionFailures.join("\n"));
|
|
1403
|
+
}
|
|
1333
1404
|
checkDeclaredTransitions(name, stateChangeDefs.concat(aggregateDefs), lifecycleStatesByView);
|
|
1334
1405
|
checkLifecycleTopology(name, stateChangeDefs.concat(aggregateDefs), lifecycleStatesByView);
|
|
1335
1406
|
reportRetiredStates(name, retiredFailures, retiredUnchecked);
|
|
@@ -1374,6 +1445,8 @@ export {
|
|
|
1374
1445
|
probeMeta,
|
|
1375
1446
|
probeQueryEngine,
|
|
1376
1447
|
reportTranslationTables,
|
|
1448
|
+
describePartition,
|
|
1449
|
+
extensionPartitionFailure,
|
|
1377
1450
|
checkDeclaredTransitions,
|
|
1378
1451
|
lifecycleTopologyFindings,
|
|
1379
1452
|
checkLifecycleTopology,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The secret source. What matters here is not that it produces characters, but
|
|
4
|
+
// that it produces them uniformly — a token drawn from a skewed distribution is
|
|
5
|
+
// guessable in a way that no other test would notice.
|
|
6
|
+
|
|
7
|
+
module Secrets = Reventless.Secrets
|
|
8
|
+
module Backend = Secrets_Node_Backend
|
|
9
|
+
|
|
10
|
+
let token = (~length, ~alphabet) =>
|
|
11
|
+
Backend.provider.randomToken(~length, ~alphabet)->Result.getOr("")
|
|
12
|
+
|
|
13
|
+
describe("randomToken", () => {
|
|
14
|
+
testSync("draws the requested length", () =>
|
|
15
|
+
expect(
|
|
16
|
+
[1, 6, 32, 64]->Array.map(length => token(~length, ~alphabet=Digits)->String.length),
|
|
17
|
+
)->toEqual([1, 6, 32, 64])
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
testSync("draws only from the alphabet it was given", () => {
|
|
21
|
+
let allowed = Secrets.characters(UrlSafe)
|
|
22
|
+
expect(
|
|
23
|
+
token(~length=512, ~alphabet=UrlSafe)
|
|
24
|
+
->String.split("")
|
|
25
|
+
->Array.every(c => allowed->String.includes(c)),
|
|
26
|
+
)->toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
testSync("digits are digits", () =>
|
|
30
|
+
expect(
|
|
31
|
+
token(~length=512, ~alphabet=Digits)
|
|
32
|
+
->String.split("")
|
|
33
|
+
->Array.every(c => "0123456789"->String.includes(c)),
|
|
34
|
+
)->toBe(true)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// Not proof of randomness — it is proof the source is not a constant, which is
|
|
38
|
+
// the failure a fixed or broken binding would produce.
|
|
39
|
+
testSync("two draws differ", () =>
|
|
40
|
+
expect(token(~length=32, ~alphabet=UrlSafe) == token(~length=32, ~alphabet=UrlSafe))->toBe(
|
|
41
|
+
false,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// A zero-length token would compare equal to nothing and settle every
|
|
46
|
+
// challenge presented against it, so it is refused rather than clamped.
|
|
47
|
+
testSync("a non-positive length is refused, not clamped", () =>
|
|
48
|
+
expect(
|
|
49
|
+
[0, -1]->Array.map(
|
|
50
|
+
length => Backend.provider.randomToken(~length, ~alphabet=Digits)->Result.isError,
|
|
51
|
+
),
|
|
52
|
+
)->toEqual([true, true])
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// 🚨 Checked over every byte value rather than by sampling the output, and the
|
|
57
|
+
// first attempt here got that wrong. `byte % 10` skews the low six digits by
|
|
58
|
+
// about four percent each — roughly 0.609 of draws landing in the low half
|
|
59
|
+
// against 0.600 for a fair source. At any sample size a test can afford that
|
|
60
|
+
// gap is a couple of standard errors, so a sampled assertion either flakes or,
|
|
61
|
+
// as the first version did, passes happily with the defence deleted.
|
|
62
|
+
describe("accepts — the rejection rule", () => {
|
|
63
|
+
let rejected = size =>
|
|
64
|
+
Array.fromInitializer(~length=256, i => i)->Array.filter(byte => !Backend.accepts(~byte, ~size))
|
|
65
|
+
|
|
66
|
+
testSync("rejects exactly the bytes that would skew a 10-character alphabet", () =>
|
|
67
|
+
expect(rejected(10))->toEqual([250, 251, 252, 253, 254, 255])
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// 64 divides 256 exactly, so nothing needs discarding and a url-safe token
|
|
71
|
+
// costs no extra draws.
|
|
72
|
+
testSync("rejects nothing when the alphabet divides evenly", () =>
|
|
73
|
+
expect(rejected(64))->toEqual([])
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// What survives always folds onto a whole number of cycles, which is the
|
|
77
|
+
// property that makes every character equally likely.
|
|
78
|
+
testSync("what it accepts is a whole multiple of the alphabet", () =>
|
|
79
|
+
expect(
|
|
80
|
+
[2, 3, 7, 10, 16, 26, 62, 64]->Array.map(
|
|
81
|
+
size => mod(256 - rejected(size)->Array.length, size),
|
|
82
|
+
),
|
|
83
|
+
)->toEqual([0, 0, 0, 0, 0, 0, 0, 0])
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
testSync("never discards more than one cycle's worth", () =>
|
|
87
|
+
expect([3, 7, 10, 26, 62]->Array.map(size => rejected(size)->Array.length < size))->toEqual([
|
|
88
|
+
true,
|
|
89
|
+
true,
|
|
90
|
+
true,
|
|
91
|
+
true,
|
|
92
|
+
true,
|
|
93
|
+
])
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe("hash", () => {
|
|
98
|
+
testSync("is deterministic, which is what lets a proof be compared", () =>
|
|
99
|
+
expect(Backend.provider.hash("a-secret") == Backend.provider.hash("a-secret"))->toBe(true)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
testSync("separates different inputs", () =>
|
|
103
|
+
expect(Backend.provider.hash("a-secret") == Backend.provider.hash("a-secret "))->toBe(false)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// One-way: the digest must not carry the secret it was made from.
|
|
107
|
+
testSync("does not contain its input", () =>
|
|
108
|
+
expect(
|
|
109
|
+
Backend.provider.hash("a-secret")
|
|
110
|
+
->Result.getOr("")
|
|
111
|
+
->String.includes("a-secret"),
|
|
112
|
+
)->toBe(false)
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe("Capabilities.none.secrets", () => {
|
|
117
|
+
// Refusing rather than degrading: there is no weaker token that would be safe
|
|
118
|
+
// to hand back, so an unwired platform must fail rather than produce one.
|
|
119
|
+
testSync("refuses both operations", () =>
|
|
120
|
+
expect((
|
|
121
|
+
Reventless.Capabilities.none.secrets.randomToken(
|
|
122
|
+
~length=32,
|
|
123
|
+
~alphabet=UrlSafe,
|
|
124
|
+
)->Result.isError,
|
|
125
|
+
Reventless.Capabilities.none.secrets.hash("x")->Result.isError,
|
|
126
|
+
))->toEqual((true, true))
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe("Secrets.fixed", () => {
|
|
131
|
+
// What a consumer's suite uses to know the secret before presenting it.
|
|
132
|
+
let provider = Secrets.fixed(~token="known", ~hash=input => `hashed:${input}`)
|
|
133
|
+
|
|
134
|
+
testSync("hands back the token it was built with", () =>
|
|
135
|
+
expect(provider.randomToken(~length=32, ~alphabet=UrlSafe))->toEqual(Ok("known"))
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
testSync("hashes through the function it was given", () =>
|
|
139
|
+
expect(provider.hash("known"))->toEqual(Ok("hashed:known"))
|
|
140
|
+
)
|
|
141
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
5
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
7
|
+
import * as Secrets$Reventless from "@reventlessdev/reventless-spec/src/semantic/Secrets.res.mjs";
|
|
8
|
+
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
9
|
+
import * as Secrets_Node_Backend$ReventlessCore from "../src/adapter/Secrets/Secrets_Node_Backend.res.mjs";
|
|
10
|
+
|
|
11
|
+
function token(length, alphabet) {
|
|
12
|
+
return Stdlib_Result.getOr(Secrets_Node_Backend$ReventlessCore.provider.randomToken(length, alphabet), "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
globalThis.describe("randomToken", () => {
|
|
16
|
+
globalThis.test("draws the requested length", () => {
|
|
17
|
+
globalThis.expect([
|
|
18
|
+
1,
|
|
19
|
+
6,
|
|
20
|
+
32,
|
|
21
|
+
64
|
|
22
|
+
].map(length => token(length, "Digits").length)).toEqual([
|
|
23
|
+
1,
|
|
24
|
+
6,
|
|
25
|
+
32,
|
|
26
|
+
64
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
globalThis.test("draws only from the alphabet it was given", () => {
|
|
30
|
+
let allowed = Secrets$Reventless.characters("UrlSafe");
|
|
31
|
+
globalThis.expect(token(512, "UrlSafe").split("").every(c => allowed.includes(c))).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
globalThis.test("digits are digits", () => {
|
|
34
|
+
globalThis.expect(token(512, "Digits").split("").every(c => "0123456789".includes(c))).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
globalThis.test("two draws differ", () => {
|
|
37
|
+
globalThis.expect(token(32, "UrlSafe") === token(32, "UrlSafe")).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
globalThis.test("a non-positive length is refused, not clamped", () => {
|
|
40
|
+
globalThis.expect([
|
|
41
|
+
0,
|
|
42
|
+
-1
|
|
43
|
+
].map(length => Stdlib_Result.isError(Secrets_Node_Backend$ReventlessCore.provider.randomToken(length, "Digits")))).toEqual([
|
|
44
|
+
true,
|
|
45
|
+
true
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
globalThis.describe("accepts — the rejection rule", () => {
|
|
51
|
+
let rejected = size => Stdlib_Array.fromInitializer(256, i => i).filter(byte => !Secrets_Node_Backend$ReventlessCore.accepts(byte, size));
|
|
52
|
+
globalThis.test("rejects exactly the bytes that would skew a 10-character alphabet", () => {
|
|
53
|
+
globalThis.expect(rejected(10)).toEqual([
|
|
54
|
+
250,
|
|
55
|
+
251,
|
|
56
|
+
252,
|
|
57
|
+
253,
|
|
58
|
+
254,
|
|
59
|
+
255
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
globalThis.test("rejects nothing when the alphabet divides evenly", () => {
|
|
63
|
+
globalThis.expect(rejected(64)).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
globalThis.test("what it accepts is a whole multiple of the alphabet", () => {
|
|
66
|
+
globalThis.expect([
|
|
67
|
+
2,
|
|
68
|
+
3,
|
|
69
|
+
7,
|
|
70
|
+
10,
|
|
71
|
+
16,
|
|
72
|
+
26,
|
|
73
|
+
62,
|
|
74
|
+
64
|
|
75
|
+
].map(size => Primitive_int.mod_(256 - rejected(size).length | 0, size))).toEqual([
|
|
76
|
+
0,
|
|
77
|
+
0,
|
|
78
|
+
0,
|
|
79
|
+
0,
|
|
80
|
+
0,
|
|
81
|
+
0,
|
|
82
|
+
0,
|
|
83
|
+
0
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
globalThis.test("never discards more than one cycle's worth", () => {
|
|
87
|
+
globalThis.expect([
|
|
88
|
+
3,
|
|
89
|
+
7,
|
|
90
|
+
10,
|
|
91
|
+
26,
|
|
92
|
+
62
|
|
93
|
+
].map(size => rejected(size).length < size)).toEqual([
|
|
94
|
+
true,
|
|
95
|
+
true,
|
|
96
|
+
true,
|
|
97
|
+
true,
|
|
98
|
+
true
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
globalThis.describe("hash", () => {
|
|
104
|
+
globalThis.test("is deterministic, which is what lets a proof be compared", () => {
|
|
105
|
+
globalThis.expect(Primitive_object.equal(Secrets_Node_Backend$ReventlessCore.provider.hash("a-secret"), Secrets_Node_Backend$ReventlessCore.provider.hash("a-secret"))).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
globalThis.test("separates different inputs", () => {
|
|
108
|
+
globalThis.expect(Primitive_object.equal(Secrets_Node_Backend$ReventlessCore.provider.hash("a-secret"), Secrets_Node_Backend$ReventlessCore.provider.hash("a-secret "))).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
globalThis.test("does not contain its input", () => {
|
|
111
|
+
globalThis.expect(Stdlib_Result.getOr(Secrets_Node_Backend$ReventlessCore.provider.hash("a-secret"), "").includes("a-secret")).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
globalThis.describe("Capabilities.none.secrets", () => {
|
|
116
|
+
globalThis.test("refuses both operations", () => {
|
|
117
|
+
globalThis.expect([
|
|
118
|
+
Stdlib_Result.isError(Capabilities$Reventless.none.secrets.randomToken(32, "UrlSafe")),
|
|
119
|
+
Stdlib_Result.isError(Capabilities$Reventless.none.secrets.hash("x"))
|
|
120
|
+
]).toEqual([
|
|
121
|
+
true,
|
|
122
|
+
true
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
globalThis.describe("Secrets.fixed", () => {
|
|
128
|
+
let provider = Secrets$Reventless.fixed("known", input => `hashed:` + input);
|
|
129
|
+
globalThis.test("hands back the token it was built with", () => {
|
|
130
|
+
globalThis.expect(provider.randomToken(32, "UrlSafe")).toEqual({
|
|
131
|
+
TAG: "Ok",
|
|
132
|
+
_0: "known"
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
globalThis.test("hashes through the function it was given", () => {
|
|
136
|
+
globalThis.expect(provider.hash("known")).toEqual({
|
|
137
|
+
TAG: "Ok",
|
|
138
|
+
_0: "hashed:known"
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let Secrets;
|
|
144
|
+
|
|
145
|
+
let Backend;
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
Secrets,
|
|
149
|
+
Backend,
|
|
150
|
+
token,
|
|
151
|
+
}
|
|
152
|
+
/* Not a pure module */
|
|
@@ -22,7 +22,7 @@ async function publishJsons(cmds) {
|
|
|
22
22
|
published.contents = published.contents.concat(cmds);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "Product", commandSchema, "StateChangeSlice", false);
|
|
25
|
+
let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "Product", commandSchema, "StateChangeSlice", false, undefined);
|
|
26
26
|
|
|
27
27
|
function addChair() {
|
|
28
28
|
return {
|