@reventlessdev/reventless-core 3.0.0-alpha.241 → 3.0.0-alpha.243
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 +14 -0
- package/package.json +5 -5
- package/src/components/Api/GraphQL_FragmentGenerator.res +49 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +23 -1
- package/src/components/Api/SchemaType.res +133 -1
- package/src/components/Api/SchemaType.res.mjs +133 -8
- package/src/components/Api/SuryToJsonSchema.res +36 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +45 -0
- package/src/components/QueryDb/QueryDb_Operations.res +13 -0
- package/src/components/QueryDb/QueryDb_Operations.res.mjs +5 -0
- package/src/plugin/component/Plugin_Structure.res +9 -0
- package/src/plugin/component/Plugin_Structure.res.mjs +6 -4
- package/src/plugin/component/SchemaWalker.res +16 -3
- package/src/plugin/component/SchemaWalker.res.mjs +17 -6
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +1 -1
- package/tests/admin/Platform_Admin_StructureTest.res +2 -2
- package/tests/api/GeolocationSemanticTest.res +108 -0
- package/tests/api/GeolocationSemanticTest.res.mjs +122 -0
- package/tests/api/TaggedUnionSchemaTest.res +198 -0
- package/tests/api/TaggedUnionSchemaTest.res.mjs +176 -0
- package/tests/fixtures/TaggedUnionFixtures.res +88 -0
- package/tests/fixtures/TaggedUnionFixtures.res.mjs +117 -0
- package/tests/querydb/TaggedUnionStampTest.res +163 -0
- package/tests/querydb/TaggedUnionStampTest.res.mjs +267 -0
|
@@ -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
|
+
})
|