@reventlessdev/reventless-core 3.0.0-alpha.199 → 3.0.0-alpha.201

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.
@@ -0,0 +1,148 @@
1
+ // What the claim component ever gets to see, decided here.
2
+ //
3
+ // `StorageRefFields.fromEventSchema` is the deploy-time reading of the
4
+ // `@storageRef` declarations that becomes the claimer's entire input: an event
5
+ // variant it does not produce is an event the claimer never reads a record for,
6
+ // and a field it does not list is a ref that never gets claimed — so the object
7
+ // keeps its pending tag and, on a store with expiry enabled, is eventually
8
+ // deleted while something still references it.
9
+ //
10
+ // Which makes the interesting assertions the negative ones: what is *absent*
11
+ // from this output is what silently rots.
12
+
13
+ open JestGlobals
14
+
15
+ module Fields = StorageRefFields
16
+
17
+ // An event union, shaped the way sury encodes a ReScript variant: one object per
18
+ // variant, discriminated by a `TAG` string constant.
19
+ let variant = (~tag: string, fields: array<(string, S.t<unknown>)>): S.t<unknown> =>
20
+ S.object(s => {
21
+ let _ = s.field("TAG", S.literal(tag)->S.castToUnknown)
22
+ fields->Array.forEach(((name, schema)) => {
23
+ let _ = s.field(name, schema)
24
+ })
25
+ ()
26
+ })->S.castToUnknown
27
+
28
+ let ref_ = (~plugin: option<string>=?, ~store: string) =>
29
+ Reventless.StorageRef.forStore(~plugin?, ~store)->S.castToUnknown
30
+
31
+ describe("StorageRefFields.fromEventSchema", () => {
32
+ testSync("finds a declared single-valued ref field and qualifies its store", () => {
33
+ let schema = S.union([variant(~tag="ProductImageChanged", [("imageUrl", ref_(~store="productImages"))])])->S.castToUnknown
34
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema))->toEqual([
35
+ {
36
+ Fields.eventType: "ProductImageChanged",
37
+ fields: [
38
+ {Fields.field: "imageUrl", arity: Single, store: "Catalog.productImages"},
39
+ ],
40
+ },
41
+ ])
42
+ })
43
+
44
+ // The annotation's own qualification wins: a field pointing at another
45
+ // plugin's store must not be re-attributed to the plugin that wrote it, or the
46
+ // claimer resolves the wrong bucket and refuses the ref as out-of-store.
47
+ testSync("a qualified declaration keeps the store it names", () => {
48
+ let schema =
49
+ S.union([
50
+ variant(~tag="OrderPhotoAttached", [("photo", ref_(~plugin="Catalog", ~store="productImages"))]),
51
+ ])->S.castToUnknown
52
+ expect(
53
+ Fields.fromEventSchema(~plugin="Ordering", schema)
54
+ ->Array.flatMap(e => e.fields->Array.map(f => f.store)),
55
+ )->toEqual(["Catalog.productImages"])
56
+ })
57
+
58
+ // `@storageRef` on an `array<string>` attaches the marker to the ELEMENT, so
59
+ // reading the field schema directly answers None. The claimer needs the arity
60
+ // to know whether to read one value or iterate.
61
+ testSync("finds a multi-valued ref field and records its arity", () => {
62
+ let schema =
63
+ S.union([
64
+ variant(~tag="GalleryReplaced", [("imageUrls", S.array(ref_(~store="productImages"))->S.castToUnknown)]),
65
+ ])->S.castToUnknown
66
+ expect(
67
+ Fields.fromEventSchema(~plugin="Catalog", schema)->Array.flatMap(e => e.fields),
68
+ )->toEqual([{Fields.field: "imageUrls", arity: Multiple, store: "Catalog.productImages"}])
69
+ })
70
+
71
+ // Narrow input by construction: a platform whose plugins declare no store
72
+ // provisions no claimer at all, and an event log carrying no declaration is
73
+ // never subscribed to.
74
+ testSync("a variant with no declared field is omitted entirely", () => {
75
+ let schema =
76
+ S.union([
77
+ variant(~tag="ProductRenamed", [("name", S.string->S.castToUnknown)]),
78
+ variant(~tag="ProductImageChanged", [("imageUrl", ref_(~store="productImages"))]),
79
+ ])->S.castToUnknown
80
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema)->Array.map(e => e.eventType))->toEqual([
81
+ "ProductImageChanged",
82
+ ])
83
+ })
84
+
85
+ testSync("an event schema with no declaration anywhere yields nothing", () => {
86
+ let schema = S.union([variant(~tag="ProductRenamed", [("name", S.string->S.castToUnknown)])])->S.castToUnknown
87
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema))->toEqual([])
88
+ })
89
+
90
+ // A string field that merely looks like a ref is not one. Declaration
91
+ // outranks inference everywhere else in the framework, and this is the reading
92
+ // that has to agree with `Plugin_Structure`'s — the claimer can only reach a
93
+ // store the deploy actually provisioned.
94
+ testSync("an unannotated string field is not treated as a ref", () => {
95
+ let schema =
96
+ S.union([variant(~tag="ProductImported", [("imageUrl", S.string->S.castToUnknown)])])->S.castToUnknown
97
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema))->toEqual([])
98
+ })
99
+
100
+ testSync("a single-variant (non-union) event schema is read the same way", () => {
101
+ let schema = variant(~tag="ProductImageChanged", [("imageUrl", ref_(~store="productImages"))])
102
+ expect(Fields.fromEventSchema(~plugin="Catalog", schema)->Array.map(e => e.eventType))->toEqual([
103
+ "ProductImageChanged",
104
+ ])
105
+ })
106
+ })
107
+
108
+ describe("StorageRefFields.toJson", () => {
109
+ // The wire form crosses into a Lambda's environment, where the handler decodes
110
+ // it with plain JSON reads. Both ends are pinned here because a drift is
111
+ // silent: an unreadable entry is indistinguishable from an absent one, and an
112
+ // absent one means "nothing to claim".
113
+ testSync("emits field, arity and store per event type", () => {
114
+ let entries: array<Fields.eventRefFields> = [
115
+ {
116
+ eventType: "ProductImageChanged",
117
+ fields: [{field: "imageUrl", arity: Single, store: "Catalog.productImages"}],
118
+ },
119
+ {
120
+ eventType: "GalleryReplaced",
121
+ fields: [{field: "imageUrls", arity: Multiple, store: "Catalog.productImages"}],
122
+ },
123
+ ]
124
+ expect(Fields.toJson(entries)->JSON.stringify)->toBe(
125
+ `{"ProductImageChanged":[{"field":"imageUrl","arity":"one","store":"Catalog.productImages"}],` ++
126
+ `"GalleryReplaced":[{"field":"imageUrls","arity":"many","store":"Catalog.productImages"}]}`,
127
+ )
128
+ })
129
+ })
130
+
131
+ describe("StorageRefFields.storesOf", () => {
132
+ testSync("collects the distinct stores an event log's declarations reach", () => {
133
+ let entries: array<Fields.eventRefFields> = [
134
+ {
135
+ eventType: "A",
136
+ fields: [{field: "a", arity: Single, store: "Catalog.productImages"}],
137
+ },
138
+ {
139
+ eventType: "B",
140
+ fields: [
141
+ {field: "b", arity: Single, store: "Catalog.productImages"},
142
+ {field: "c", arity: Multiple, store: "Ordering.receipts"},
143
+ ],
144
+ },
145
+ ]
146
+ expect(Fields.storesOf(entries))->toEqual(["Catalog.productImages", "Ordering.receipts"])
147
+ })
148
+ })
@@ -0,0 +1,152 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
5
+ import * as StorageRefFields$ReventlessCore from "../../src/components/Api/StorageRefFields.res.mjs";
6
+
7
+ function variant(tag, fields) {
8
+ return S.object(s => {
9
+ s.f("TAG", S.literal(tag));
10
+ fields.forEach(param => {
11
+ s.f(param[0], param[1]);
12
+ });
13
+ });
14
+ }
15
+
16
+ let ref_ = StorageRef$Reventless.forStore;
17
+
18
+ globalThis.describe("StorageRefFields.fromEventSchema", () => {
19
+ globalThis.test("finds a declared single-valued ref field and qualifies its store", () => {
20
+ let schema = S.union([variant("ProductImageChanged", [[
21
+ "imageUrl",
22
+ StorageRef$Reventless.forStore(undefined, "productImages")
23
+ ]])]);
24
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema)).toEqual([{
25
+ eventType: "ProductImageChanged",
26
+ fields: [{
27
+ field: "imageUrl",
28
+ arity: "Single",
29
+ store: "Catalog.productImages"
30
+ }]
31
+ }]);
32
+ });
33
+ globalThis.test("a qualified declaration keeps the store it names", () => {
34
+ let schema = S.union([variant("OrderPhotoAttached", [[
35
+ "photo",
36
+ StorageRef$Reventless.forStore("Catalog", "productImages")
37
+ ]])]);
38
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Ordering", schema).flatMap(e => e.fields.map(f => f.store))).toEqual(["Catalog.productImages"]);
39
+ });
40
+ globalThis.test("finds a multi-valued ref field and records its arity", () => {
41
+ let schema = S.union([variant("GalleryReplaced", [[
42
+ "imageUrls",
43
+ S.array(StorageRef$Reventless.forStore(undefined, "productImages"))
44
+ ]])]);
45
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema).flatMap(e => e.fields)).toEqual([{
46
+ field: "imageUrls",
47
+ arity: "Multiple",
48
+ store: "Catalog.productImages"
49
+ }]);
50
+ });
51
+ globalThis.test("a variant with no declared field is omitted entirely", () => {
52
+ let schema = S.union([
53
+ variant("ProductRenamed", [[
54
+ "name",
55
+ S.string
56
+ ]]),
57
+ variant("ProductImageChanged", [[
58
+ "imageUrl",
59
+ StorageRef$Reventless.forStore(undefined, "productImages")
60
+ ]])
61
+ ]);
62
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema).map(e => e.eventType)).toEqual(["ProductImageChanged"]);
63
+ });
64
+ globalThis.test("an event schema with no declaration anywhere yields nothing", () => {
65
+ let schema = S.union([variant("ProductRenamed", [[
66
+ "name",
67
+ S.string
68
+ ]])]);
69
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema)).toEqual([]);
70
+ });
71
+ globalThis.test("an unannotated string field is not treated as a ref", () => {
72
+ let schema = S.union([variant("ProductImported", [[
73
+ "imageUrl",
74
+ S.string
75
+ ]])]);
76
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema)).toEqual([]);
77
+ });
78
+ globalThis.test("a single-variant (non-union) event schema is read the same way", () => {
79
+ let schema = variant("ProductImageChanged", [[
80
+ "imageUrl",
81
+ StorageRef$Reventless.forStore(undefined, "productImages")
82
+ ]]);
83
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema).map(e => e.eventType)).toEqual(["ProductImageChanged"]);
84
+ });
85
+ });
86
+
87
+ globalThis.describe("StorageRefFields.toJson", () => {
88
+ globalThis.test("emits field, arity and store per event type", () => {
89
+ let entries = [
90
+ {
91
+ eventType: "ProductImageChanged",
92
+ fields: [{
93
+ field: "imageUrl",
94
+ arity: "Single",
95
+ store: "Catalog.productImages"
96
+ }]
97
+ },
98
+ {
99
+ eventType: "GalleryReplaced",
100
+ fields: [{
101
+ field: "imageUrls",
102
+ arity: "Multiple",
103
+ store: "Catalog.productImages"
104
+ }]
105
+ }
106
+ ];
107
+ globalThis.expect(JSON.stringify(StorageRefFields$ReventlessCore.toJson(entries))).toBe(`{"ProductImageChanged":[{"field":"imageUrl","arity":"one","store":"Catalog.productImages"}],"GalleryReplaced":[{"field":"imageUrls","arity":"many","store":"Catalog.productImages"}]}`);
108
+ });
109
+ });
110
+
111
+ globalThis.describe("StorageRefFields.storesOf", () => {
112
+ globalThis.test("collects the distinct stores an event log's declarations reach", () => {
113
+ let entries = [
114
+ {
115
+ eventType: "A",
116
+ fields: [{
117
+ field: "a",
118
+ arity: "Single",
119
+ store: "Catalog.productImages"
120
+ }]
121
+ },
122
+ {
123
+ eventType: "B",
124
+ fields: [
125
+ {
126
+ field: "b",
127
+ arity: "Single",
128
+ store: "Catalog.productImages"
129
+ },
130
+ {
131
+ field: "c",
132
+ arity: "Multiple",
133
+ store: "Ordering.receipts"
134
+ }
135
+ ]
136
+ }
137
+ ];
138
+ globalThis.expect(StorageRefFields$ReventlessCore.storesOf(entries)).toEqual([
139
+ "Catalog.productImages",
140
+ "Ordering.receipts"
141
+ ]);
142
+ });
143
+ });
144
+
145
+ let Fields;
146
+
147
+ export {
148
+ Fields,
149
+ variant,
150
+ ref_,
151
+ }
152
+ /* Not a pure module */
@@ -794,4 +794,50 @@ describe("SuryToJsonSchema:", () => {
794
794
  ))
795
795
  )
796
796
  })
797
+
798
+ // The third composite. Its emission matters more than the other two's, because
799
+ // the reader has been resolving this shape from a *heuristic* since before the
800
+ // marker existed: an object with numeric `lat`/`lng` sub-properties already
801
+ // resolves to the geo-point semantic by shape. So the assertions below are what
802
+ // separate a declaration from a lucky guess — the marker and its `type` source.
803
+ describe("GeoPoint is an object carrying the geoPoint marker:", () => {
804
+ let json = SuryToJsonSchema.deriveObjectSchema(
805
+ S.schema(s =>
806
+ {
807
+ "location": s.matches(Reventless.GeoPoint.schema),
808
+ "lat": s.matches(S.float),
809
+ }
810
+ )->S.castToUnknown,
811
+ )
812
+ let keyOf = (name, key) => getPropertyOf(json, name)->Option.flatMap(s => getProperty(s, key))
813
+
814
+ testSync("the field is an object, not a pair of numbers", () =>
815
+ expect(keyOf("location", "type"))->toEqual(Some(JSON.Encode.string("object")))
816
+ )
817
+
818
+ testSync("it carries the geoPoint id, sourced from the type", () =>
819
+ expect((
820
+ keyOf("location", "x-reventless-semantic"),
821
+ keyOf("location", "x-reventless-semantic-source"),
822
+ ))->toEqual((Some(JSON.Encode.string("geoPoint")), Some(JSON.Encode.string("type"))))
823
+ )
824
+
825
+ // The control, and the whole reason the composite exists: a bare `lat` float
826
+ // is exactly what the flattened pair looked like, and it emits nothing at
827
+ // all. Everything a map knew about it, it inferred from the name.
828
+ testSync("a bare lat float emits no semantic", () =>
829
+ expect((keyOf("lat", "type"), keyOf("lat", "x-reventless-semantic")))->toEqual((
830
+ Some(JSON.Encode.string("number")),
831
+ None,
832
+ ))
833
+ )
834
+
835
+ testSync("both coordinates reach the wire as numbers", () => {
836
+ let inner = getPropertyOf(json, "location")->Option.getOr(JSON.Encode.null)
837
+ expect((
838
+ getPropertyOf(inner, "lat")->Option.flatMap(s => getProperty(s, "type")),
839
+ getPropertyOf(inner, "lng")->Option.flatMap(s => getProperty(s, "type")),
840
+ ))->toEqual((Some(JSON.Encode.string("number")), Some(JSON.Encode.string("number"))))
841
+ })
842
+ })
797
843
  })
@@ -11,6 +11,7 @@ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/component
11
11
  import * as Percent$Reventless from "@reventlessdev/reventless-spec/src/semantic/Percent.res.mjs";
12
12
  import * as Currency$Reventless from "@reventlessdev/reventless-spec/src/semantic/Currency.res.mjs";
13
13
  import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
14
+ import * as GeoPoint$Reventless from "@reventlessdev/reventless-spec/src/semantic/GeoPoint.res.mjs";
14
15
  import * as DateRange$Reventless from "@reventlessdev/reventless-spec/src/semantic/DateRange.res.mjs";
15
16
  import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
16
17
  import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
@@ -1074,6 +1075,44 @@ globalThis.describe("SuryToJsonSchema:", () => {
1074
1075
  ]);
1075
1076
  });
1076
1077
  });
1078
+ globalThis.describe("GeoPoint is an object carrying the geoPoint marker:", () => {
1079
+ let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(S.schema(s => ({
1080
+ location: s.m(GeoPoint$Reventless.schema),
1081
+ lat: s.m(S.float)
1082
+ })));
1083
+ let keyOf = (name, key) => Stdlib_Option.flatMap(getPropertyOf(json, name), s => getProperty(s, key));
1084
+ globalThis.test("the field is an object, not a pair of numbers", () => {
1085
+ globalThis.expect(keyOf("location", "type")).toEqual("object");
1086
+ });
1087
+ globalThis.test("it carries the geoPoint id, sourced from the type", () => {
1088
+ globalThis.expect([
1089
+ keyOf("location", "x-reventless-semantic"),
1090
+ keyOf("location", "x-reventless-semantic-source")
1091
+ ]).toEqual([
1092
+ "geoPoint",
1093
+ "type"
1094
+ ]);
1095
+ });
1096
+ globalThis.test("a bare lat float emits no semantic", () => {
1097
+ globalThis.expect([
1098
+ keyOf("lat", "type"),
1099
+ keyOf("lat", "x-reventless-semantic")
1100
+ ]).toEqual([
1101
+ "number",
1102
+ undefined
1103
+ ]);
1104
+ });
1105
+ globalThis.test("both coordinates reach the wire as numbers", () => {
1106
+ let inner = Stdlib_Option.getOr(getPropertyOf(json, "location"), null);
1107
+ globalThis.expect([
1108
+ Stdlib_Option.flatMap(getPropertyOf(inner, "lat"), s => getProperty(s, "type")),
1109
+ Stdlib_Option.flatMap(getPropertyOf(inner, "lng"), s => getProperty(s, "type"))
1110
+ ]).toEqual([
1111
+ "number",
1112
+ "number"
1113
+ ]);
1114
+ });
1115
+ });
1077
1116
  });
1078
1117
 
1079
1118
  export {