@reventlessdev/reventless-core 3.0.0-alpha.192 → 3.0.0-alpha.194
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 +5 -5
- package/src/plugin/component/Plugin_Structure.res +8 -0
- package/src/plugin/component/Plugin_Structure.res.mjs +9 -5
- package/tests/api/SuryToJsonSchemaTest.res +58 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +44 -0
- package/tests/plugin/PluginStructureTest.res +73 -5
- package/tests/plugin/PluginStructureTest.res.mjs +69 -8
- package/tests/plugin/StateChangeSlice/PsUploadAvatar.res +37 -0
- package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.194 (2026-07-30)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **spec,core:** record the storageRef annotation instead of inferring it ([06fb5d6](https://github.com/ReventlessDev/reventless-core/commit/06fb5d671db91fb536acefd0c2db69d98671da39))
|
|
11
|
+
* **spec:** read a semantic marker through an optional field's wrapper ([abebaa9](https://github.com/ReventlessDev/reventless-core/commit/abebaa9e6c73986765bb3de589ba0414eb0d85da))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.193 (2026-07-30)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **spec:** semantic branded scalars — Email, Url, Phone, Percent, Color, Bytes, Duration ([2e64ff9](https://github.com/ReventlessDev/reventless-core/commit/2e64ff992e913c038a71659c731c25a104e3c5a9))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.192 (2026-07-30)
|
|
7
22
|
|
|
8
23
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.194",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
31
32
|
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
32
33
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
33
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
34
34
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
35
35
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
37
|
-
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
38
37
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
38
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.112",
|
|
39
|
+
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
40
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.87"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -498,6 +498,14 @@ let make = (
|
|
|
498
498
|
Reventless.Plugin.store: target.plugin->Option.getOr(name) ++ "." ++ target.store,
|
|
499
499
|
component,
|
|
500
500
|
field,
|
|
501
|
+
// The annotation as its author wrote it. `target.plugin` is `None`
|
|
502
|
+
// exactly when the field left the store unqualified, so this is the
|
|
503
|
+
// source text rather than a guess at it — and this is the only place
|
|
504
|
+
// that distinction survives.
|
|
505
|
+
annotation: switch target.plugin {
|
|
506
|
+
| None => target.store
|
|
507
|
+
| Some(plugin) => plugin ++ "." ++ target.store
|
|
508
|
+
},
|
|
501
509
|
})
|
|
502
510
|
)
|
|
503
511
|
|
|
@@ -447,11 +447,15 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
447
447
|
let properties = v.properties;
|
|
448
448
|
return Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
449
449
|
let field = param[0];
|
|
450
|
-
return Stdlib_Option.map(StorageRef$Reventless.getStore(param[1]), target =>
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
450
|
+
return Stdlib_Option.map(StorageRef$Reventless.getStore(param[1]), target => {
|
|
451
|
+
let plugin = target.plugin;
|
|
452
|
+
return {
|
|
453
|
+
store: Stdlib_Option.getOr(target.plugin, name) + "." + target.store,
|
|
454
|
+
component: component,
|
|
455
|
+
field: field,
|
|
456
|
+
annotation: plugin !== undefined ? plugin + "." + target.store : target.store
|
|
457
|
+
};
|
|
458
|
+
});
|
|
455
459
|
});
|
|
456
460
|
} else {
|
|
457
461
|
return [];
|
|
@@ -606,6 +606,22 @@ describe("SuryToJsonSchema:", () => {
|
|
|
606
606
|
)->toEqual(Some(JSON.Encode.string("string")))
|
|
607
607
|
})
|
|
608
608
|
|
|
609
|
+
// An optional field's marker sits inside the wrapper sury-ppx builds around
|
|
610
|
+
// it, so this used to emit a plain nullable string: the field was a storage
|
|
611
|
+
// ref in the source and an untyped string on the wire. The semantic now
|
|
612
|
+
// survives, and it rides *beside* the nullable shape rather than replacing
|
|
613
|
+
// it — a reader still learns the value may be absent.
|
|
614
|
+
testSync("an optional storage-ref field keeps both its semantic and its nullability", () => {
|
|
615
|
+
let schema = S.schema(s =>
|
|
616
|
+
{"imageUrl": s.matches(S.option(Reventless.StorageRef.forStore(~store="productImages")))}
|
|
617
|
+
)->S.castToUnknown
|
|
618
|
+
let json = SuryToJsonSchema.deriveObjectSchema(schema)
|
|
619
|
+
expect(semanticOf(json, "imageUrl"))->toEqual(Some(JSON.Encode.string("storageRef")))
|
|
620
|
+
expect(
|
|
621
|
+
getPropertyOf(json, "imageUrl")->Option.flatMap(s => getProperty(s, "oneOf"))->Option.isSome,
|
|
622
|
+
)->toBe(true)
|
|
623
|
+
})
|
|
624
|
+
|
|
609
625
|
testSync("a type-carried semantic beats an annotation naming the same field", () => {
|
|
610
626
|
let withSpec = (schema, spec) =>
|
|
611
627
|
schema->S.Metadata.set(~id=Reventless.StateAnnotations.stateAnnotationsId, spec)
|
|
@@ -619,4 +635,46 @@ describe("SuryToJsonSchema:", () => {
|
|
|
619
635
|
expect(semanticOf(json, "imageUrl"))->toEqual(Some(JSON.Encode.string("storageRef")))
|
|
620
636
|
})
|
|
621
637
|
})
|
|
638
|
+
|
|
639
|
+
// The claim the branded scalars rest on: marking a field adds an annotation,
|
|
640
|
+
// not a shape. A string stays a string and a number stays a number on the
|
|
641
|
+
// wire, which is what makes them retrofittable onto a log that already has
|
|
642
|
+
// events in it. One of each is enough — the emission path is the same for all
|
|
643
|
+
// seven, and it is the *shape* half that is the load-bearing assertion here.
|
|
644
|
+
describe("branded scalars keep their underlying shape:", () => {
|
|
645
|
+
let fieldOf = (json, name) => getPropertyOf(json, name)
|
|
646
|
+
let keyOf = (json, name, key) => fieldOf(json, name)->Option.flatMap(s => getProperty(s, key))
|
|
647
|
+
|
|
648
|
+
let json = SuryToJsonSchema.deriveObjectSchema(
|
|
649
|
+
S.schema(s =>
|
|
650
|
+
{
|
|
651
|
+
"contactEmail": s.matches(Reventless.Email.schema),
|
|
652
|
+
"taxRate": s.matches(Reventless.Percent.schema),
|
|
653
|
+
}
|
|
654
|
+
)->S.castToUnknown,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
testSync("a string scalar stays a string and carries its id", () =>
|
|
658
|
+
expect((
|
|
659
|
+
keyOf(json, "contactEmail", "type"),
|
|
660
|
+
keyOf(json, "contactEmail", "x-reventless-semantic"),
|
|
661
|
+
))->toEqual((Some(JSON.Encode.string("string")), Some(JSON.Encode.string("email"))))
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
testSync("a numeric scalar stays a number and carries its id", () =>
|
|
665
|
+
expect((
|
|
666
|
+
keyOf(json, "taxRate", "type"),
|
|
667
|
+
keyOf(json, "taxRate", "x-reventless-semantic"),
|
|
668
|
+
))->toEqual((Some(JSON.Encode.string("number")), Some(JSON.Encode.string("percent"))))
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
// The provenance half: a typed field outranks an annotated one, and this is
|
|
672
|
+
// the key a consumer reads to tell them apart.
|
|
673
|
+
testSync("both name the type as their source", () =>
|
|
674
|
+
expect((
|
|
675
|
+
keyOf(json, "contactEmail", "x-reventless-semantic-source"),
|
|
676
|
+
keyOf(json, "taxRate", "x-reventless-semantic-source"),
|
|
677
|
+
))->toEqual((Some(JSON.Encode.string("type")), Some(JSON.Encode.string("type"))))
|
|
678
|
+
)
|
|
679
|
+
})
|
|
622
680
|
})
|
|
@@ -4,8 +4,10 @@ import * as S from "sury/src/S.res.mjs";
|
|
|
4
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Email$Reventless from "@reventlessdev/reventless-spec/src/semantic/Email.res.mjs";
|
|
7
8
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
9
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
10
|
+
import * as Percent$Reventless from "@reventlessdev/reventless-spec/src/semantic/Percent.res.mjs";
|
|
9
11
|
import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
|
|
10
12
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
11
13
|
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
@@ -907,6 +909,14 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
907
909
|
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
908
910
|
globalThis.expect(Stdlib_Option.flatMap(getPropertyOf(json, "imageUrl"), s => getProperty(s, "type"))).toEqual("string");
|
|
909
911
|
});
|
|
912
|
+
globalThis.test("an optional storage-ref field keeps both its semantic and its nullability", () => {
|
|
913
|
+
let schema = S.schema(s => ({
|
|
914
|
+
imageUrl: s.m(S.option(StorageRef$Reventless.forStore(undefined, "productImages")))
|
|
915
|
+
}));
|
|
916
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema);
|
|
917
|
+
globalThis.expect(semanticOf(json, "imageUrl")).toEqual("storageRef");
|
|
918
|
+
globalThis.expect(Stdlib_Option.isSome(Stdlib_Option.flatMap(getPropertyOf(json, "imageUrl"), s => getProperty(s, "oneOf")))).toBe(true);
|
|
919
|
+
});
|
|
910
920
|
globalThis.test("a type-carried semantic beats an annotation naming the same field", () => {
|
|
911
921
|
let withSpec = (schema, spec) => S.Metadata.set(schema, StateAnnotations$Reventless.stateAnnotationsId, spec);
|
|
912
922
|
let schema = withSpec(S.schema(s => ({
|
|
@@ -937,6 +947,40 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
937
947
|
globalThis.expect(semanticOf(json, "imageUrl")).toEqual("storageRef");
|
|
938
948
|
});
|
|
939
949
|
});
|
|
950
|
+
globalThis.describe("branded scalars keep their underlying shape:", () => {
|
|
951
|
+
let keyOf = (json, name, key) => Stdlib_Option.flatMap(getPropertyOf(json, name), s => getProperty(s, key));
|
|
952
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(S.schema(s => ({
|
|
953
|
+
contactEmail: s.m(Email$Reventless.schema),
|
|
954
|
+
taxRate: s.m(Percent$Reventless.schema)
|
|
955
|
+
})));
|
|
956
|
+
globalThis.test("a string scalar stays a string and carries its id", () => {
|
|
957
|
+
globalThis.expect([
|
|
958
|
+
keyOf(json, "contactEmail", "type"),
|
|
959
|
+
keyOf(json, "contactEmail", "x-reventless-semantic")
|
|
960
|
+
]).toEqual([
|
|
961
|
+
"string",
|
|
962
|
+
"email"
|
|
963
|
+
]);
|
|
964
|
+
});
|
|
965
|
+
globalThis.test("a numeric scalar stays a number and carries its id", () => {
|
|
966
|
+
globalThis.expect([
|
|
967
|
+
keyOf(json, "taxRate", "type"),
|
|
968
|
+
keyOf(json, "taxRate", "x-reventless-semantic")
|
|
969
|
+
]).toEqual([
|
|
970
|
+
"number",
|
|
971
|
+
"percent"
|
|
972
|
+
]);
|
|
973
|
+
});
|
|
974
|
+
globalThis.test("both name the type as their source", () => {
|
|
975
|
+
globalThis.expect([
|
|
976
|
+
keyOf(json, "contactEmail", "x-reventless-semantic-source"),
|
|
977
|
+
keyOf(json, "taxRate", "x-reventless-semantic-source")
|
|
978
|
+
]).toEqual([
|
|
979
|
+
"type",
|
|
980
|
+
"type"
|
|
981
|
+
]);
|
|
982
|
+
});
|
|
983
|
+
});
|
|
940
984
|
});
|
|
941
985
|
|
|
942
986
|
export {
|
|
@@ -93,6 +93,20 @@ module PsAttachInvoiceSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
93
93
|
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
module PsUploadAvatarSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
97
|
+
module Spec = PsUploadAvatar
|
|
98
|
+
module Behavior = {
|
|
99
|
+
type state = PsUploadAvatar.state
|
|
100
|
+
let initialState = PsUploadAvatar.initialState
|
|
101
|
+
let evolve = PsUploadAvatar.evolve
|
|
102
|
+
let decide = PsUploadAvatar.decide
|
|
103
|
+
let moduleUrl = PsUploadAvatar.moduleUrl
|
|
104
|
+
}
|
|
105
|
+
let isAsync = false
|
|
106
|
+
type component = scsComponent
|
|
107
|
+
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
97
111
|
module Spec = PsChangePhoto
|
|
98
112
|
module Behavior = {
|
|
@@ -694,9 +708,22 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
694
708
|
// `documents` is declared on a command field AND an event field of the
|
|
695
709
|
// same component under the same field name — one declaration site, so
|
|
696
710
|
// the identical triples collapse to one entry.
|
|
711
|
+
// `annotation` is the source text, not the key: the own-plugin store is
|
|
712
|
+
// written bare and the foreign one qualified, which is exactly the
|
|
713
|
+
// distinction no downstream comparison can recover.
|
|
697
714
|
let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
|
|
698
|
-
{
|
|
699
|
-
|
|
715
|
+
{
|
|
716
|
+
store: "TestPlugin.documents",
|
|
717
|
+
component: "AttachInvoice",
|
|
718
|
+
field: "documentUrl",
|
|
719
|
+
annotation: "documents",
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
store: "branding.logos",
|
|
723
|
+
component: "AttachInvoice",
|
|
724
|
+
field: "logoUrl",
|
|
725
|
+
annotation: "branding.logos",
|
|
726
|
+
},
|
|
700
727
|
]
|
|
701
728
|
expect(withStores.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
702
729
|
})
|
|
@@ -718,7 +745,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
718
745
|
let entry =
|
|
719
746
|
manifest.capabilities->Array.find(c => c.key == "TestPlugin.documents")->Option.getOrThrow
|
|
720
747
|
let expected: array<Reventless.CapabilityManifest.provenance> = [
|
|
721
|
-
{component: "AttachInvoice", field: "documentUrl"},
|
|
748
|
+
{component: "AttachInvoice", field: "documentUrl", annotation: "documents"},
|
|
722
749
|
]
|
|
723
750
|
expect(entry.declaredBy)->toEqual(expected)
|
|
724
751
|
})
|
|
@@ -738,7 +765,8 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
738
765
|
"declaredBy": [
|
|
739
766
|
{
|
|
740
767
|
"component": "AttachInvoice",
|
|
741
|
-
"field": "documentUrl"
|
|
768
|
+
"field": "documentUrl",
|
|
769
|
+
"annotation": "documents"
|
|
742
770
|
}
|
|
743
771
|
]
|
|
744
772
|
},
|
|
@@ -748,7 +776,8 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
748
776
|
"declaredBy": [
|
|
749
777
|
{
|
|
750
778
|
"component": "AttachInvoice",
|
|
751
|
-
"field": "logoUrl"
|
|
779
|
+
"field": "logoUrl",
|
|
780
|
+
"annotation": "branding.logos"
|
|
752
781
|
}
|
|
753
782
|
]
|
|
754
783
|
}
|
|
@@ -760,6 +789,45 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
760
789
|
})
|
|
761
790
|
})
|
|
762
791
|
|
|
792
|
+
// Making a field optional says the value may be absent. It does not say the
|
|
793
|
+
// field stopped being a storage ref or a reference — but both walks read the
|
|
794
|
+
// marker through `Semantic.get`, and an optional field's marker sits on the
|
|
795
|
+
// schema inside the wrapper sury-ppx builds. Until that unwrap existed, adding
|
|
796
|
+
// a `?` to a field silently un-declared its store: nothing failed, an S3
|
|
797
|
+
// bucket just stopped being provisioned.
|
|
798
|
+
describe("optional carriers keep their declarations", () => {
|
|
799
|
+
let withOptional = Plugin_Structure.make(
|
|
800
|
+
~name="TestPlugin",
|
|
801
|
+
~stateChangeSlices=[module(PsUploadAvatarSlice)],
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
testSync("an optional storage ref still requires its store", () =>
|
|
805
|
+
expect(withOptional.requiredStores)->toEqual(Some(["TestPlugin.avatars"]))
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
// Declared on the command and on the event under one field name — the same
|
|
809
|
+
// collapse the required case makes, so optionality does not change arity.
|
|
810
|
+
testSync("and still carries one declaring site", () => {
|
|
811
|
+
let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
|
|
812
|
+
{
|
|
813
|
+
store: "TestPlugin.avatars",
|
|
814
|
+
component: "UploadAvatar",
|
|
815
|
+
field: "avatarUrl",
|
|
816
|
+
annotation: "avatars",
|
|
817
|
+
},
|
|
818
|
+
]
|
|
819
|
+
expect(withOptional.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
testSync("an optional reference is still collected", () => {
|
|
823
|
+
let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
|
|
824
|
+
let expected: array<Reventless.Plugin.fieldReference> = [
|
|
825
|
+
{fieldName: "referredBy", entity: "Customers", plugin: None},
|
|
826
|
+
]
|
|
827
|
+
expect(cmd.references)->toEqual(expected)
|
|
828
|
+
})
|
|
829
|
+
})
|
|
830
|
+
|
|
763
831
|
// Declarations are authoritative; the name heuristics are a lint. A
|
|
764
832
|
// heuristic-only match warns and never reaches the manifest, so nothing is
|
|
765
833
|
// provisioned from a guess — `imageUrl` is genuinely ambiguous between an
|
|
@@ -13,6 +13,7 @@ import * as PsOrdersView$ReventlessCore from "./StateViewSlice/PsOrdersView.res.
|
|
|
13
13
|
import * as PsPlaceOrder$ReventlessCore from "./StateChangeSlice/PsPlaceOrder.res.mjs";
|
|
14
14
|
import * as PsChangePhoto$ReventlessCore from "./StateChangeSlice/PsChangePhoto.res.mjs";
|
|
15
15
|
import * as CapabilityManifest$Reventless from "@reventlessdev/reventless-spec/src/components/CapabilityManifest.res.mjs";
|
|
16
|
+
import * as PsUploadAvatar$ReventlessCore from "./StateChangeSlice/PsUploadAvatar.res.mjs";
|
|
16
17
|
import * as PsAnnotatedView$ReventlessCore from "./StateViewSlice/PsAnnotatedView.res.mjs";
|
|
17
18
|
import * as PsAttachInvoice$ReventlessCore from "./StateChangeSlice/PsAttachInvoice.res.mjs";
|
|
18
19
|
import * as PsCustomersView$ReventlessCore from "./StateViewSlice/PsCustomersView.res.mjs";
|
|
@@ -215,13 +216,43 @@ let PsAttachInvoiceSlice = {
|
|
|
215
216
|
};
|
|
216
217
|
|
|
217
218
|
let Behavior$3 = {
|
|
219
|
+
initialState: false,
|
|
220
|
+
evolve: PsUploadAvatar$ReventlessCore.evolve,
|
|
221
|
+
decide: PsUploadAvatar$ReventlessCore.decide,
|
|
222
|
+
moduleUrl: PsUploadAvatar$ReventlessCore.moduleUrl
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
function make$7(param, param$1, param$2, param$3, param$4, param$5) {
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let PsUploadAvatarSlice_Spec = {
|
|
230
|
+
name: PsUploadAvatar$ReventlessCore.name,
|
|
231
|
+
moduleUrl: PsUploadAvatar$ReventlessCore.moduleUrl,
|
|
232
|
+
Id: Id$Reventless.$$String,
|
|
233
|
+
consumedEventSchema: PsUploadAvatar$ReventlessCore.consumedEventSchema,
|
|
234
|
+
errorSchema: PsUploadAvatar$ReventlessCore.errorSchema,
|
|
235
|
+
eventSchema: PsUploadAvatar$ReventlessCore.eventSchema,
|
|
236
|
+
commandSchema: PsUploadAvatar$ReventlessCore.commandSchema,
|
|
237
|
+
commandAuthorization: PsUploadAvatar$ReventlessCore.commandAuthorization,
|
|
238
|
+
readConsistency: PsUploadAvatar$ReventlessCore.readConsistency
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
let PsUploadAvatarSlice = {
|
|
242
|
+
Spec: PsUploadAvatarSlice_Spec,
|
|
243
|
+
Behavior: Behavior$3,
|
|
244
|
+
isAsync: false,
|
|
245
|
+
make: make$7
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
let Behavior$4 = {
|
|
218
249
|
initialState: false,
|
|
219
250
|
evolve: PsChangePhoto$ReventlessCore.evolve,
|
|
220
251
|
decide: PsChangePhoto$ReventlessCore.decide,
|
|
221
252
|
moduleUrl: PsChangePhoto$ReventlessCore.moduleUrl
|
|
222
253
|
};
|
|
223
254
|
|
|
224
|
-
function make$
|
|
255
|
+
function make$8(param, param$1, param$2, param$3, param$4, param$5) {
|
|
225
256
|
return 0;
|
|
226
257
|
}
|
|
227
258
|
|
|
@@ -239,9 +270,9 @@ let PsChangePhotoSlice_Spec = {
|
|
|
239
270
|
|
|
240
271
|
let PsChangePhotoSlice = {
|
|
241
272
|
Spec: PsChangePhotoSlice_Spec,
|
|
242
|
-
Behavior: Behavior$
|
|
273
|
+
Behavior: Behavior$4,
|
|
243
274
|
isAsync: false,
|
|
244
|
-
make: make$
|
|
275
|
+
make: make$8
|
|
245
276
|
};
|
|
246
277
|
|
|
247
278
|
let structure = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [
|
|
@@ -757,12 +788,14 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
757
788
|
{
|
|
758
789
|
store: "TestPlugin.documents",
|
|
759
790
|
component: "AttachInvoice",
|
|
760
|
-
field: "documentUrl"
|
|
791
|
+
field: "documentUrl",
|
|
792
|
+
annotation: "documents"
|
|
761
793
|
},
|
|
762
794
|
{
|
|
763
795
|
store: "branding.logos",
|
|
764
796
|
component: "AttachInvoice",
|
|
765
|
-
field: "logoUrl"
|
|
797
|
+
field: "logoUrl",
|
|
798
|
+
annotation: "branding.logos"
|
|
766
799
|
}
|
|
767
800
|
];
|
|
768
801
|
globalThis.expect(withStores.requiredStoreDeclarations).toEqual(expected);
|
|
@@ -776,7 +809,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
776
809
|
let entry = Stdlib_Option.getOrThrow(manifest.capabilities.find(c => c.key === "TestPlugin.documents"), undefined);
|
|
777
810
|
let expected = [{
|
|
778
811
|
component: "AttachInvoice",
|
|
779
|
-
field: "documentUrl"
|
|
812
|
+
field: "documentUrl",
|
|
813
|
+
annotation: "documents"
|
|
780
814
|
}];
|
|
781
815
|
globalThis.expect(entry.declaredBy).toEqual(expected);
|
|
782
816
|
});
|
|
@@ -793,7 +827,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
793
827
|
"declaredBy": [
|
|
794
828
|
{
|
|
795
829
|
"component": "AttachInvoice",
|
|
796
|
-
"field": "documentUrl"
|
|
830
|
+
"field": "documentUrl",
|
|
831
|
+
"annotation": "documents"
|
|
797
832
|
}
|
|
798
833
|
]
|
|
799
834
|
},
|
|
@@ -803,7 +838,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
803
838
|
"declaredBy": [
|
|
804
839
|
{
|
|
805
840
|
"component": "AttachInvoice",
|
|
806
|
-
"field": "logoUrl"
|
|
841
|
+
"field": "logoUrl",
|
|
842
|
+
"annotation": "branding.logos"
|
|
807
843
|
}
|
|
808
844
|
]
|
|
809
845
|
}
|
|
@@ -813,6 +849,30 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
813
849
|
});
|
|
814
850
|
});
|
|
815
851
|
});
|
|
852
|
+
globalThis.describe("optional carriers keep their declarations", () => {
|
|
853
|
+
let withOptional = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsUploadAvatarSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
854
|
+
globalThis.test("an optional storage ref still requires its store", () => {
|
|
855
|
+
globalThis.expect(withOptional.requiredStores).toEqual(["TestPlugin.avatars"]);
|
|
856
|
+
});
|
|
857
|
+
globalThis.test("and still carries one declaring site", () => {
|
|
858
|
+
let expected = [{
|
|
859
|
+
store: "TestPlugin.avatars",
|
|
860
|
+
component: "UploadAvatar",
|
|
861
|
+
field: "avatarUrl",
|
|
862
|
+
annotation: "avatars"
|
|
863
|
+
}];
|
|
864
|
+
globalThis.expect(withOptional.requiredStoreDeclarations).toEqual(expected);
|
|
865
|
+
});
|
|
866
|
+
globalThis.test("an optional reference is still collected", () => {
|
|
867
|
+
let cmd = withOptional.stateChangeSlices[0].commands[0];
|
|
868
|
+
let expected = [{
|
|
869
|
+
fieldName: "referredBy",
|
|
870
|
+
entity: "Customers",
|
|
871
|
+
plugin: undefined
|
|
872
|
+
}];
|
|
873
|
+
globalThis.expect(cmd.references).toEqual(expected);
|
|
874
|
+
});
|
|
875
|
+
});
|
|
816
876
|
globalThis.describe("capability inference", () => {
|
|
817
877
|
let commandWarnings = Capability_Inference$ReventlessCore.scanSchema("ChangePhoto", PsChangePhoto$ReventlessCore.commandSchema);
|
|
818
878
|
let eventWarnings = Capability_Inference$ReventlessCore.scanSchema("ChangePhoto", PsChangePhoto$ReventlessCore.eventSchema);
|
|
@@ -867,6 +927,7 @@ export {
|
|
|
867
927
|
PsCustomersViewSlice,
|
|
868
928
|
PsAnnotatedViewSlice,
|
|
869
929
|
PsAttachInvoiceSlice,
|
|
930
|
+
PsUploadAvatarSlice,
|
|
870
931
|
PsChangePhotoSlice,
|
|
871
932
|
structure,
|
|
872
933
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Test fixture spec where every semantic carrier is OPTIONAL.
|
|
2
|
+
//
|
|
3
|
+
// The marker of an optional field sits on the schema *inside* the
|
|
4
|
+
// undefined-union sury-ppx wraps it in, so a walk that reads only the outer
|
|
5
|
+
// schema finds nothing. That failure is silent in the worst way: the plugin
|
|
6
|
+
// compiles, deploys green, and provisions no object store — so `avatarUrl` is
|
|
7
|
+
// the only declarer of `avatars` here on purpose. If the unwrap regresses, the
|
|
8
|
+
// store requirement disappears entirely rather than merely losing a site.
|
|
9
|
+
//
|
|
10
|
+
// `referredBy` is the same trap for `@ref`: an optional reference that goes
|
|
11
|
+
// uncollected drops out of `commandDef.references`.
|
|
12
|
+
|
|
13
|
+
@@reventless.spec("UploadAvatar")
|
|
14
|
+
|
|
15
|
+
type state = bool
|
|
16
|
+
let initialState = false
|
|
17
|
+
|
|
18
|
+
@schema
|
|
19
|
+
type consumedEvent = OrderPlaced
|
|
20
|
+
|
|
21
|
+
let evolve = (_state, _event) => true
|
|
22
|
+
|
|
23
|
+
@schema
|
|
24
|
+
type command =
|
|
25
|
+
| UploadAvatar({
|
|
26
|
+
customerId: string,
|
|
27
|
+
@storageRef("avatars") avatarUrl?: string,
|
|
28
|
+
@ref("Customers") referredBy?: string,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
@schema
|
|
32
|
+
type error = CustomerNotFound
|
|
33
|
+
|
|
34
|
+
@schema
|
|
35
|
+
type event = AvatarUploaded({customerId: string, @storageRef("avatars") avatarUrl?: string})
|
|
36
|
+
|
|
37
|
+
let decide = (_state, _command): result<array<event>, error> => Ok([])
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
5
|
+
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
6
|
+
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
7
|
+
|
|
8
|
+
let consumedEventSchema = S.literal("OrderPlaced");
|
|
9
|
+
|
|
10
|
+
function evolve(_state, _event) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let commandSchema = S.schema(s => ({
|
|
15
|
+
TAG: "UploadAvatar",
|
|
16
|
+
customerId: s.m(DcbTag$Reventless.string),
|
|
17
|
+
avatarUrl: s.m(S.option(StorageRef$Reventless.forStore(undefined, "avatars"))),
|
|
18
|
+
referredBy: s.m(S.option(Reference$Reventless.to_(undefined, undefined, "Customers")))
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
let errorSchema = S.literal("CustomerNotFound");
|
|
22
|
+
|
|
23
|
+
let eventSchema = S.schema(s => ({
|
|
24
|
+
TAG: "AvatarUploaded",
|
|
25
|
+
customerId: s.m(DcbTag$Reventless.string),
|
|
26
|
+
avatarUrl: s.m(S.option(StorageRef$Reventless.forStore(undefined, "avatars")))
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
function decide(_state, _command) {
|
|
30
|
+
return {
|
|
31
|
+
TAG: "Ok",
|
|
32
|
+
_0: []
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function commandAuthorization(param) {
|
|
37
|
+
return "AllowAuthenticated";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let name = "UploadAvatar";
|
|
41
|
+
|
|
42
|
+
let Id;
|
|
43
|
+
|
|
44
|
+
let initialState = false;
|
|
45
|
+
|
|
46
|
+
let moduleUrl = "@reventlessdev/reventless-core/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs";
|
|
47
|
+
|
|
48
|
+
let readConsistency = "EscalateOnRetry";
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
name,
|
|
52
|
+
Id,
|
|
53
|
+
initialState,
|
|
54
|
+
consumedEventSchema,
|
|
55
|
+
evolve,
|
|
56
|
+
commandSchema,
|
|
57
|
+
errorSchema,
|
|
58
|
+
eventSchema,
|
|
59
|
+
decide,
|
|
60
|
+
moduleUrl,
|
|
61
|
+
commandAuthorization,
|
|
62
|
+
readConsistency,
|
|
63
|
+
}
|
|
64
|
+
/* consumedEventSchema Not a pure module */
|