@reventlessdev/reventless-core 3.0.0-alpha.240 → 3.0.0-alpha.242

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,176 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as SchemaType$ReventlessCore from "../../src/components/Api/SchemaType.res.mjs";
7
+ import * as GraphQL_Stitcher$ReventlessCore from "../../src/components/Api/GraphQL_Stitcher.res.mjs";
8
+ import * as SuryToJsonSchema$ReventlessCore from "../../src/components/Api/SuryToJsonSchema.res.mjs";
9
+ import * as TaggedUnionFixtures$ReventlessCore from "../fixtures/TaggedUnionFixtures.res.mjs";
10
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "../../src/components/Api/GraphQL_FragmentGenerator.res.mjs";
11
+
12
+ function fragmentFor(returnTypeName, specName, stateSchema) {
13
+ return GraphQL_FragmentGenerator$ReventlessCore.generate([], [TaggedUnionFixtures$ReventlessCore.queryEntryFor(returnTypeName, specName, stateSchema)]);
14
+ }
15
+
16
+ function typesOf(fragment) {
17
+ return GraphQL_Stitcher$ReventlessCore.decode(fragment).types;
18
+ }
19
+
20
+ function typeDefFor(fragment, needle) {
21
+ return Stdlib_Option.getOr(GraphQL_Stitcher$ReventlessCore.decode(fragment).types.find(t => t.includes(needle)), "");
22
+ }
23
+
24
+ globalThis.describe("SchemaType classifies a named union", () => {
25
+ let shapeOfField = (schema, field) => {
26
+ let fields = SchemaType$ReventlessCore.fromSuryObject("X", schema);
27
+ if (fields !== undefined) {
28
+ return fields[field];
29
+ }
30
+ };
31
+ globalThis.test("arms are keyed by TAG and carry the member type's own name", () => {
32
+ let other = shapeOfField(TaggedUnionFixtures$ReventlessCore.CustomersSpec.stateSchema, "geolocation");
33
+ if (typeof other === "object" && other.TAG === "TaggedUnion") {
34
+ let arms = other._1;
35
+ globalThis.expect(other._0).toBe("Geolocation");
36
+ globalThis.expect(arms.map(param => param[0])).toEqual([
37
+ "Pending",
38
+ "Located",
39
+ "Unresolvable"
40
+ ]);
41
+ let match = arms.find(param => param[0] === "Located");
42
+ let exit = 0;
43
+ if (match !== undefined) {
44
+ let match$1 = match[1];
45
+ if (typeof match$1 !== "object") {
46
+ exit = 2;
47
+ } else {
48
+ if (match$1.TAG === "ObjectRef") {
49
+ globalThis.expect(match$1._0).toBe("GeolocationLocated");
50
+ globalThis.expect(Object.keys(match$1._1)).toEqual(["point"]);
51
+ return;
52
+ }
53
+ exit = 2;
54
+ }
55
+ } else {
56
+ exit = 2;
57
+ }
58
+ if (exit === 2) {
59
+ globalThis.expect("Located arm").toBe("an ObjectRef");
60
+ return;
61
+ }
62
+ }
63
+ globalThis.expect(Stdlib_Option.isSome(other) ? "some other shape" : "nothing").toBe("a TaggedUnion");
64
+ });
65
+ globalThis.test("an optional union keeps its arms behind Nullable", () => {
66
+ let match = shapeOfField(TaggedUnionFixtures$ReventlessCore.SightingsSpec.stateSchema, "lastSeen");
67
+ if (typeof match === "object" && match.TAG === "Nullable") {
68
+ let match$1 = match._0;
69
+ if (typeof match$1 === "object" && match$1.TAG === "TaggedUnion") {
70
+ globalThis.expect([
71
+ match$1._0,
72
+ match$1._1.length
73
+ ]).toEqual([
74
+ "Geolocation",
75
+ 3
76
+ ]);
77
+ return;
78
+ }
79
+ }
80
+ globalThis.expect("lastSeen").toBe("Nullable(TaggedUnion(...))");
81
+ });
82
+ globalThis.test("an array of unions keeps its arms behind ArrayOf", () => {
83
+ let match = shapeOfField(TaggedUnionFixtures$ReventlessCore.SightingsSpec.stateSchema, "history");
84
+ if (typeof match === "object" && match.TAG === "ArrayOf") {
85
+ let match$1 = match._0;
86
+ if (typeof match$1 === "object" && match$1.TAG === "TaggedUnion") {
87
+ globalThis.expect(match$1._0).toBe("Geolocation");
88
+ return;
89
+ }
90
+ }
91
+ globalThis.expect("history").toBe("ArrayOf(TaggedUnion(...))");
92
+ });
93
+ globalThis.test("an unnamed union is reported by field and by reason", () => {
94
+ let found = SchemaType$ReventlessCore.unclassifiedUnions(TaggedUnionFixtures$ReventlessCore.UnnamedSpec.stateSchema);
95
+ if (found.length !== 1) {
96
+ globalThis.expect(found.length).toBe(1);
97
+ return;
98
+ }
99
+ let match = found[0];
100
+ globalThis.expect(match.path).toBe("verdict");
101
+ globalThis.expect(match.reason.includes("no name")).toBe(true);
102
+ });
103
+ globalThis.test("a classified union and an enum are not reported", () => {
104
+ globalThis.expect(SchemaType$ReventlessCore.unclassifiedUnions(TaggedUnionFixtures$ReventlessCore.CustomersSpec.stateSchema).length).toBe(0);
105
+ });
106
+ });
107
+
108
+ globalThis.describe("GraphQL_FragmentGenerator emits a union", () => {
109
+ let fragment = fragmentFor("Ordering_TuCustomer", "TaggedUnionCustomers", TaggedUnionFixtures$ReventlessCore.CustomersSpec.stateSchema);
110
+ globalThis.test("the field is typed as the union", () => {
111
+ globalThis.expect(typeDefFor(fragment, "type Ordering_TuCustomer ").includes("geolocation: Geolocation!")).toBe(true);
112
+ });
113
+ globalThis.test("the union names one member type per arm", () => {
114
+ globalThis.expect(typeDefFor(fragment, "union Geolocation")).toBe("union Geolocation = GeolocationPending | GeolocationLocated | GeolocationUnresolvable");
115
+ });
116
+ globalThis.test("each member type declares the arm's own fields, and no TAG", () => {
117
+ let located = typeDefFor(fragment, "type GeolocationLocated ");
118
+ globalThis.expect(located.includes("point: GeoPoint!")).toBe(true);
119
+ globalThis.expect(located.includes("TAG")).toBe(false);
120
+ globalThis.expect(typeDefFor(fragment, "type GeolocationUnresolvable ").includes("reason: String!")).toBe(true);
121
+ });
122
+ globalThis.test("a second plugin's copy of the union is byte-identical", () => {
123
+ let other = fragmentFor("Catalog_TuSighting", "TaggedUnionSightings", TaggedUnionFixtures$ReventlessCore.SightingsSpec.stateSchema);
124
+ globalThis.expect(typeDefFor(other, "union Geolocation")).toBe(typeDefFor(fragment, "union Geolocation"));
125
+ globalThis.expect(typeDefFor(other, "type GeolocationLocated ")).toBe(typeDefFor(fragment, "type GeolocationLocated "));
126
+ });
127
+ globalThis.test("an optional union is nullable and an array of them is a list", () => {
128
+ let sightings = fragmentFor("Catalog_TuSighting", "TaggedUnionSightings", TaggedUnionFixtures$ReventlessCore.SightingsSpec.stateSchema);
129
+ let row = typeDefFor(sightings, "type Catalog_TuSighting ");
130
+ globalThis.expect(row.includes("lastSeen: Geolocation\n")).toBe(true);
131
+ globalThis.expect(row.includes("history: [Geolocation!]!")).toBe(true);
132
+ });
133
+ globalThis.test("an unnamed union stays a String", () => {
134
+ let unnamed = fragmentFor("Ordering_TuCase", "TaggedUnionUnnamed", TaggedUnionFixtures$ReventlessCore.UnnamedSpec.stateSchema);
135
+ globalThis.expect(typeDefFor(unnamed, "type Ordering_TuCase ").includes("verdict: String!")).toBe(true);
136
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(unnamed).types.some(t => t.startsWith("union "))).toBe(false);
137
+ });
138
+ });
139
+
140
+ function objAt(json, key) {
141
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o[key]);
142
+ }
143
+
144
+ globalThis.describe("SuryToJsonSchema emits the arms as oneOf", () => {
145
+ let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(TaggedUnionFixtures$ReventlessCore.CustomersSpec.stateSchema);
146
+ let field = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(objAt(json, "properties"), p => Stdlib_JSON.Decode.object(p)), p => p["geolocation"]), null);
147
+ globalThis.test("the field names the union it is", () => {
148
+ globalThis.expect(Stdlib_Option.flatMap(objAt(field, "x-reventless-union"), Stdlib_JSON.Decode.string)).toEqual("Geolocation");
149
+ });
150
+ globalThis.test("one member per arm, each discriminated by its TAG const", () => {
151
+ let members = Stdlib_Option.getOr(Stdlib_Option.flatMap(objAt(field, "oneOf"), Stdlib_JSON.Decode.array), []);
152
+ globalThis.expect(members.length).toBe(3);
153
+ let tags = Stdlib_Array.filterMap(members, m => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(objAt(m, "properties"), Stdlib_JSON.Decode.object), p => p["TAG"]), t => objAt(t, "const")), Stdlib_JSON.Decode.string));
154
+ globalThis.expect(tags).toEqual([
155
+ "Pending",
156
+ "Located",
157
+ "Unresolvable"
158
+ ]);
159
+ });
160
+ globalThis.test("each member publishes the GraphQL type it is emitted as", () => {
161
+ let names = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(objAt(field, "oneOf"), Stdlib_JSON.Decode.array), []), m => Stdlib_Option.flatMap(objAt(m, "x-reventless-union-member"), Stdlib_JSON.Decode.string));
162
+ globalThis.expect(names).toEqual([
163
+ "GeolocationPending",
164
+ "GeolocationLocated",
165
+ "GeolocationUnresolvable"
166
+ ]);
167
+ });
168
+ });
169
+
170
+ export {
171
+ fragmentFor,
172
+ typesOf,
173
+ typeDefFor,
174
+ objAt,
175
+ }
176
+ /* Not a pure module */
@@ -0,0 +1,88 @@
1
+ // A queryable whose state carries a tagged union, which no view in the
2
+ // repository does yet.
3
+ //
4
+ // The mechanism has to be provable before anything adopts it: the SDL emitter,
5
+ // the JSON-Schema walk, the write-time `__typename` stamp and the storage
6
+ // round-trip all assert against this fixture. Pointing those assertions at a real
7
+ // view instead would make the mechanism own that view's breaking retype, its
8
+ // golden refresh and its client pin — which is the coupling the mechanism was cut
9
+ // away from.
10
+ //
11
+ // The union is spelled the way an adopter's would be: every arm an inline record
12
+ // declaring at least one named field of its own, because a bare arm is a string
13
+ // on the wire and an empty one implies a member type with no fields — neither of
14
+ // which GraphQL admits.
15
+
16
+ @schema
17
+ type geolocation =
18
+ | Pending({requestedFor: string})
19
+ | Located({point: Reventless.GeoPoint.t})
20
+ | Unresolvable({reason: string})
21
+
22
+ // The name the type is emitted and stamped under. Written here by hand because
23
+ // this fixture is not a `@@reventless.spec` file; in one, the ppx writes exactly
24
+ // this line.
25
+ let geolocationSchema = Reventless.TaggedUnion.named(~name="Geolocation", geolocationSchema)
26
+
27
+ /** A required union field, the plain case. */
28
+ module CustomersSpec = {
29
+ module Id = Reventless.Id.StringPure
30
+ let name = "TaggedUnionCustomers"
31
+ let moduleUrl: string = %raw(`import.meta.url`)
32
+
33
+ @schema
34
+ type state = {customerId: string, geolocation: geolocation}
35
+
36
+ let config = Reventless.ReadModel.config()
37
+ let subIdConfig = None
38
+ }
39
+
40
+ /** The wrapped cases: an optional union, and an array of them. */
41
+ module SightingsSpec = {
42
+ module Id = Reventless.Id.StringPure
43
+ let name = "TaggedUnionSightings"
44
+ let moduleUrl: string = %raw(`import.meta.url`)
45
+
46
+ @schema
47
+ type state = {
48
+ sightingId: string,
49
+ lastSeen: option<geolocation>,
50
+ history: array<geolocation>,
51
+ }
52
+
53
+ let config = Reventless.ReadModel.config()
54
+ let subIdConfig = None
55
+ }
56
+
57
+ /** The same three arms with no name on the schema — the shape the emitter must
58
+ decline to emit rather than emit under a name it made up. */
59
+ module UnnamedSpec = {
60
+ module Id = Reventless.Id.StringPure
61
+ let name = "TaggedUnionUnnamed"
62
+ let moduleUrl: string = %raw(`import.meta.url`)
63
+
64
+ @schema
65
+ type verdict =
66
+ | Approved({by: string})
67
+ | Rejected({reason: string})
68
+
69
+ @schema
70
+ type state = {caseId: string, verdict: verdict}
71
+
72
+ let config = Reventless.ReadModel.config()
73
+ let subIdConfig = None
74
+ }
75
+
76
+ let queryEntryFor = (
77
+ ~returnTypeName: string,
78
+ ~specName: string,
79
+ stateSchema: S.t<'a>,
80
+ ): ReventlessInfra.Api.querySchemaEntry => {
81
+ singleFieldName: returnTypeName,
82
+ listFieldName: returnTypeName ++ "s",
83
+ returnTypeName,
84
+ specName,
85
+ stateSchema: stateSchema->S.castToUnknown,
86
+ authorization: None,
87
+ connectionSpec: true,
88
+ }
@@ -0,0 +1,117 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as GeoPoint$Reventless from "@reventlessdev/reventless-spec/src/semantic/GeoPoint.res.mjs";
5
+ import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
6
+ import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
7
+
8
+ let geolocationSchema = Sury.union([
9
+ Sury.$schema(s => ({
10
+ TAG: "Pending",
11
+ requestedFor: s.m(Sury.string)
12
+ })),
13
+ Sury.$schema(s => ({
14
+ TAG: "Located",
15
+ point: s.m(GeoPoint$Reventless.schema)
16
+ })),
17
+ Sury.$schema(s => ({
18
+ TAG: "Unresolvable",
19
+ reason: s.m(Sury.string)
20
+ }))
21
+ ]);
22
+
23
+ let geolocationSchema$1 = TaggedUnion$Reventless.named("Geolocation", geolocationSchema);
24
+
25
+ let moduleUrl = import.meta.url;
26
+
27
+ let stateSchema = Sury.$schema(s => ({
28
+ customerId: s.m(Sury.string),
29
+ geolocation: s.m(geolocationSchema$1)
30
+ }));
31
+
32
+ let config = ReadModel$Reventless.config(undefined, undefined, undefined);
33
+
34
+ let CustomersSpec = {
35
+ Id: undefined,
36
+ name: "TaggedUnionCustomers",
37
+ moduleUrl: moduleUrl,
38
+ stateSchema: stateSchema,
39
+ config: config,
40
+ subIdConfig: undefined,
41
+ authorization: "AllowAuthenticated",
42
+ visibility: "Public"
43
+ };
44
+
45
+ let moduleUrl$1 = import.meta.url;
46
+
47
+ let stateSchema$1 = Sury.$schema(s => ({
48
+ sightingId: s.m(Sury.string),
49
+ lastSeen: s.m(Sury.$option(geolocationSchema$1)),
50
+ history: s.m(Sury.array(geolocationSchema$1))
51
+ }));
52
+
53
+ let config$1 = ReadModel$Reventless.config(undefined, undefined, undefined);
54
+
55
+ let SightingsSpec = {
56
+ Id: undefined,
57
+ name: "TaggedUnionSightings",
58
+ moduleUrl: moduleUrl$1,
59
+ stateSchema: stateSchema$1,
60
+ config: config$1,
61
+ subIdConfig: undefined,
62
+ authorization: "AllowAuthenticated",
63
+ visibility: "Public"
64
+ };
65
+
66
+ let moduleUrl$2 = import.meta.url;
67
+
68
+ let verdictSchema = Sury.union([
69
+ Sury.$schema(s => ({
70
+ TAG: "Approved",
71
+ by: s.m(Sury.string)
72
+ })),
73
+ Sury.$schema(s => ({
74
+ TAG: "Rejected",
75
+ reason: s.m(Sury.string)
76
+ }))
77
+ ]);
78
+
79
+ let stateSchema$2 = Sury.$schema(s => ({
80
+ caseId: s.m(Sury.string),
81
+ verdict: s.m(verdictSchema)
82
+ }));
83
+
84
+ let config$2 = ReadModel$Reventless.config(undefined, undefined, undefined);
85
+
86
+ let UnnamedSpec = {
87
+ Id: undefined,
88
+ name: "TaggedUnionUnnamed",
89
+ moduleUrl: moduleUrl$2,
90
+ verdictSchema: verdictSchema,
91
+ stateSchema: stateSchema$2,
92
+ config: config$2,
93
+ subIdConfig: undefined,
94
+ authorization: "AllowAuthenticated",
95
+ visibility: "Public"
96
+ };
97
+
98
+ function queryEntryFor(returnTypeName, specName, stateSchema) {
99
+ return {
100
+ singleFieldName: returnTypeName,
101
+ listFieldName: returnTypeName + "s",
102
+ returnTypeName: returnTypeName,
103
+ stateSchema: stateSchema,
104
+ specName: specName,
105
+ authorization: undefined,
106
+ connectionSpec: true
107
+ };
108
+ }
109
+
110
+ export {
111
+ geolocationSchema$1 as geolocationSchema,
112
+ CustomersSpec,
113
+ SightingsSpec,
114
+ UnnamedSpec,
115
+ queryEntryFor,
116
+ }
117
+ /* geolocationSchema Not a pure module */
@@ -0,0 +1,163 @@
1
+ open JestGlobals
2
+ open QueryDbFixtures
3
+ open TaggedUnionFixtures
4
+
5
+ // The write-time `__typename` stamp, and the storage round trip it has to
6
+ // survive. Both AppSync and graphql-js resolve a union member from `__typename`
7
+ // on the value they are handed; the AppSync resolvers hand back the stored item
8
+ // unchanged, and the live change channel carries the row as raw JSON past the
9
+ // typed field entirely. So the stamp lands here, once, and everything downstream
10
+ // reads a row that already says which arm it is.
11
+
12
+ module Customers = QueryDb_Operations.Make(
13
+ CustomersSpec,
14
+ {
15
+ let jsonOps = mockJsonOps
16
+ },
17
+ )
18
+
19
+ module Sightings = QueryDb_Operations.Make(
20
+ SightingsSpec,
21
+ {
22
+ let jsonOps = mockJsonOps
23
+ },
24
+ )
25
+
26
+ module Items = QueryDb_Operations.Make(
27
+ ItemQueryDbSpec,
28
+ {
29
+ let jsonOps = mockJsonOps
30
+ },
31
+ )
32
+
33
+ let _ = beforeEach(() => reset())
34
+
35
+ let storedRow = id =>
36
+ store.contents->Dict.get(id)->Option.getOr([])->Array.get(0)->Option.getOr(JSON.Encode.null)
37
+
38
+ let field = (json, name) =>
39
+ json->JSON.Decode.object->Option.flatMap(o => o->Dict.get(name))->Option.getOr(JSON.Encode.null)
40
+
41
+ let string = json => json->JSON.Decode.string
42
+
43
+ describe("QueryDb_Operations stamps union members", () => {
44
+ testPromise("the stored value carries __typename beside TAG", async () => {
45
+ let state: CustomersSpec.state = {
46
+ customerId: "c-1",
47
+ geolocation: Located({point: {lat: 48.2082, lng: 16.3738}}),
48
+ }
49
+ let _ = await Customers.save("c-1", state, Init, None)
50
+ let geo = storedRow("c-1")->field("geolocation")
51
+ expect(geo->field("TAG")->string)->toEqual(Some("Located"))
52
+ expect(geo->field("__typename")->string)->toEqual(Some("GeolocationLocated"))
53
+ })
54
+
55
+ testPromise("every arm is stamped as the member type it is", async () => {
56
+ let _ = await Customers.save(
57
+ "c-2",
58
+ {customerId: "c-2", geolocation: Pending({requestedFor: "1 High St"})},
59
+ Init,
60
+ None,
61
+ )
62
+ let _ = await Customers.save(
63
+ "c-3",
64
+ {customerId: "c-3", geolocation: Unresolvable({reason: "ambiguous"})},
65
+ Init,
66
+ None,
67
+ )
68
+ expect(storedRow("c-2")->field("geolocation")->field("__typename")->string)->toEqual(
69
+ Some("GeolocationPending"),
70
+ )
71
+ expect(storedRow("c-3")->field("geolocation")->field("__typename")->string)->toEqual(
72
+ Some("GeolocationUnresolvable"),
73
+ )
74
+ })
75
+
76
+ testPromise("an optional union and an array of them are stamped too", async () => {
77
+ let state: SightingsSpec.state = {
78
+ sightingId: "s-1",
79
+ lastSeen: Some(Located({point: {lat: 1.0, lng: 2.0}})),
80
+ history: [Pending({requestedFor: "somewhere"}), Unresolvable({reason: "gone"})],
81
+ }
82
+ let _ = await Sightings.save("s-1", state, Init, None)
83
+ let row = storedRow("s-1")
84
+ expect(row->field("lastSeen")->field("__typename")->string)->toEqual(
85
+ Some("GeolocationLocated"),
86
+ )
87
+ let history = row->field("history")->JSON.Decode.array->Option.getOr([])
88
+ expect(history->Array.map(h => h->field("__typename")->string))->toEqual([
89
+ Some("GeolocationPending"),
90
+ Some("GeolocationUnresolvable"),
91
+ ])
92
+ })
93
+
94
+ testPromise("an absent optional union leaves nothing behind", async () => {
95
+ let _ = await Sightings.save(
96
+ "s-2",
97
+ {sightingId: "s-2", lastSeen: None, history: []},
98
+ Init,
99
+ None,
100
+ )
101
+ expect(storedRow("s-2")->field("lastSeen"))->toEqual(JSON.Encode.null)
102
+ })
103
+
104
+ // What keeps this releasable ahead of any adopter: a view with no union field
105
+ // is walked and written exactly as it was written before the walk existed.
106
+ testPromise("a view with no union field stores what it always stored", async () => {
107
+ let _ = await Items.save("item-1", {name: "Widget", count: 5}, Init, None)
108
+ expect(storedRow("item-1")->JSON.stringify)->toBe(
109
+ `{"name":"Widget","count":5,"id":"item-1"}`,
110
+ )
111
+ })
112
+
113
+ testPromise("saveBatch stamps every row", async () => {
114
+ let batch = [
115
+ (
116
+ "c-4",
117
+ ({customerId: "c-4", geolocation: Located({point: {lat: 0.0, lng: 0.0}})}: CustomersSpec.state),
118
+ None,
119
+ ),
120
+ (
121
+ "c-5",
122
+ ({customerId: "c-5", geolocation: Pending({requestedFor: "x"})}: CustomersSpec.state),
123
+ None,
124
+ ),
125
+ ]
126
+ let _ = await Customers.saveBatch(batch)
127
+ expect((
128
+ storedRow("c-4")->field("geolocation")->field("__typename")->string,
129
+ storedRow("c-5")->field("geolocation")->field("__typename")->string,
130
+ ))->toEqual((Some("GeolocationLocated"), Some("GeolocationPending")))
131
+ })
132
+ })
133
+
134
+ describe("a stamped row decodes back", () => {
135
+ testPromise("the extra key does not disturb the parse", async () => {
136
+ let _ = await Customers.save(
137
+ "c-6",
138
+ {customerId: "c-6", geolocation: Located({point: {lat: 48.2082, lng: 16.3738}})},
139
+ Init,
140
+ None,
141
+ )
142
+ let result = await Customers.loadStream("c-6")->Stream.runCollect->Effect.runPromise
143
+ switch result {
144
+ | [{geolocation: Located({point})}] => expect((point.lat, point.lng))->toEqual((48.2082, 16.3738))
145
+ | _ => expect("round trip")->toBe("Located with its point intact")
146
+ }
147
+ })
148
+
149
+ // A row written before the field existed. Replay is what a read model is for,
150
+ // so this is a convenience rather than a contract — but it must not throw,
151
+ // because a single unparseable row otherwise fails the whole page.
152
+ testPromise("a row written before the union field arrives as the first arm", async () => {
153
+ store.contents->Dict.set(
154
+ "c-7",
155
+ [JSON.Encode.object(Dict.fromArray([("customerId", JSON.Encode.string("c-7"))]))],
156
+ )
157
+ let result = await Customers.loadStream("c-7")->Stream.runCollect->Effect.runPromise
158
+ switch result {
159
+ | [{geolocation: Pending({requestedFor})}] => expect(requestedFor)->toBe("")
160
+ | _ => expect("healed row")->toBe("Pending")
161
+ }
162
+ })
163
+ })