@reventlessdev/reventless-core 3.0.0-alpha.265 → 3.0.0-alpha.267
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 +30 -0
- package/package.json +5 -5
- package/src/Projection.res +0 -10
- package/src/Projection.res.mjs +0 -14
- package/src/ProjectionMapper.res +8 -2
- package/src/ProjectionMapper.res.mjs +3 -3
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
- package/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs +3 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res +47 -16
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +20 -7
- package/src/components/Api/SchemaType.res +13 -3
- package/src/components/Api/SchemaType.res.mjs +13 -2
- package/src/components/Api/SuryToJsonSchema.res +9 -5
- package/src/components/Api/SuryToJsonSchema.res.mjs +14 -2
- package/src/components/EventMapper/EventMapper_Builder.res +19 -1
- package/src/components/EventMapper/EventMapper_Builder.res.mjs +10 -1
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +11 -1
- package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +2 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +8 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +2 -1
- package/src/plugin/component/Plugin_Structure.res +148 -25
- package/src/plugin/component/Plugin_Structure.res.mjs +114 -30
- package/tests/api/GraphQL_FragmentGeneratorTest.res +76 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +78 -17
- package/tests/api/IdentitySemanticTest.res +77 -0
- package/tests/api/IdentitySemanticTest.res.mjs +71 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +0 -1
- package/tests/dcb/DcbStateChangeSliceTest.res.mjs +0 -3
- package/tests/eventmapper/EventMapperFixtures.res +2 -2
- package/tests/eventmapper/EventMapperFixtures.res.mjs +14 -2
- package/tests/plugin/IdentityReferenceTest.res +99 -0
- package/tests/plugin/IdentityReferenceTest.res.mjs +169 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +14 -1
- package/tests/plugin/PluginStructureTest.res.mjs +42 -10
- package/tests/plugin/StateView/PsAnnotatedView.res.mjs +3 -0
- package/tests/plugin/StateView/PsAvailableProductsView.res.mjs +3 -0
- package/tests/plugin/StateView/PsCategoriesView.res.mjs +3 -0
- package/tests/plugin/StateView/PsCustomersView.res.mjs +3 -0
- package/tests/plugin/StateView/PsGatedView.res.mjs +3 -0
- package/tests/plugin/StateView/PsOrdersView.res.mjs +3 -0
- package/tests/plugin/StateView/PsShipmentsView.res.mjs +3 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Money$Reventless from "@reventlessdev/reventless-spec/src/semantic/Money.res.mjs";
|
|
6
7
|
import * as GraphQL_Stitcher$ReventlessCore from "../../src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
@@ -9,6 +10,14 @@ import * as GraphQL_FragmentGenerator$ReventlessCore from "../../src/components/
|
|
|
9
10
|
import * as PsInternalFieldsReadModel$ReventlessCore from "./PsInternalFieldsReadModel.res.mjs";
|
|
10
11
|
import * as OutboundTranslationSlice_Callback$ReventlessCore from "../../src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
|
|
11
12
|
|
|
13
|
+
let OrderId = Id$Reventless.Make({
|
|
14
|
+
key: "orderId"
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let CustomerId = Id$Reventless.Make({
|
|
18
|
+
key: "customerId"
|
|
19
|
+
});
|
|
20
|
+
|
|
12
21
|
let commandSchema = Sury.union([
|
|
13
22
|
Sury.$schema(s => ({
|
|
14
23
|
TAG: "CancelOrder",
|
|
@@ -100,7 +109,7 @@ globalThis.describe("resolveKeyField — the ladder", () => {
|
|
|
100
109
|
productId: s.m(Sury.string),
|
|
101
110
|
name: s.m(Sury.string)
|
|
102
111
|
}));
|
|
103
|
-
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("AvailableProducts", schema)).toEqual([
|
|
112
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "AvailableProducts", schema)).toEqual([
|
|
104
113
|
"productId",
|
|
105
114
|
"sole"
|
|
106
115
|
]);
|
|
@@ -110,7 +119,7 @@ globalThis.describe("resolveKeyField — the ladder", () => {
|
|
|
110
119
|
productId: s.m(Sury.string),
|
|
111
120
|
categoryId: s.m(Sury.string)
|
|
112
121
|
}));
|
|
113
|
-
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("Products", schema)).toEqual([
|
|
122
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "Products", schema)).toEqual([
|
|
114
123
|
"productId",
|
|
115
124
|
"convention"
|
|
116
125
|
]);
|
|
@@ -120,7 +129,7 @@ globalThis.describe("resolveKeyField — the ladder", () => {
|
|
|
120
129
|
categoryId: s.m(Sury.string),
|
|
121
130
|
ownerId: s.m(Sury.string)
|
|
122
131
|
}));
|
|
123
|
-
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("Categories", schema)).toEqual([
|
|
132
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "Categories", schema)).toEqual([
|
|
124
133
|
"categoryId",
|
|
125
134
|
"convention"
|
|
126
135
|
]);
|
|
@@ -130,53 +139,94 @@ globalThis.describe("resolveKeyField — the ladder", () => {
|
|
|
130
139
|
productId: s.m(Sury.string),
|
|
131
140
|
categoryId: s.m(Sury.string)
|
|
132
141
|
}));
|
|
133
|
-
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("ProductDemand", schema)).toEqual(undefined);
|
|
142
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "ProductDemand", schema)).toEqual(undefined);
|
|
134
143
|
});
|
|
135
144
|
globalThis.test("no `*Id` field at all declines", () => {
|
|
136
145
|
let schema = Sury.$schema(s => ({
|
|
137
146
|
email: s.m(Sury.string),
|
|
138
147
|
address: s.m(Sury.string)
|
|
139
148
|
}));
|
|
140
|
-
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("Customers", schema)).toEqual(undefined);
|
|
149
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "Customers", schema)).toEqual(undefined);
|
|
141
150
|
});
|
|
142
151
|
globalThis.test("a word merely ending in `id` is not a key", () => {
|
|
143
152
|
let schema = Sury.$schema(s => ({
|
|
144
153
|
paid: s.m(Sury.string),
|
|
145
154
|
valid: s.m(Sury.string)
|
|
146
155
|
}));
|
|
147
|
-
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("Invoices", schema)).toEqual(undefined);
|
|
156
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByStateField", "Invoices", schema)).toEqual(undefined);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
globalThis.describe("resolveKeyField — a read model is keyed by its envelope", () => {
|
|
161
|
+
globalThis.test("a lone foreign `*Id` field does not name the key", () => {
|
|
162
|
+
let schema = Sury.$schema(s => ({
|
|
163
|
+
customerId: s.m(Sury.string),
|
|
164
|
+
lifecycle: s.m(Sury.string)
|
|
165
|
+
}));
|
|
166
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", "Orders", schema)).toEqual(undefined);
|
|
167
|
+
});
|
|
168
|
+
globalThis.test("the convention rung still names it", () => {
|
|
169
|
+
let schema = Sury.$schema(s => ({
|
|
170
|
+
orderId: s.m(Sury.string),
|
|
171
|
+
customerId: s.m(Sury.string)
|
|
172
|
+
}));
|
|
173
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", "Orders", schema)).toEqual([
|
|
174
|
+
"orderId",
|
|
175
|
+
"convention"
|
|
176
|
+
]);
|
|
177
|
+
});
|
|
178
|
+
globalThis.test("a field typed as the view's identity is its key", () => {
|
|
179
|
+
let schema = Sury.$schema(s => ({
|
|
180
|
+
order: s.m(OrderId.schema),
|
|
181
|
+
customerId: s.m(CustomerId.schema)
|
|
182
|
+
}));
|
|
183
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", "Orders", schema)).toEqual([
|
|
184
|
+
"order",
|
|
185
|
+
"convention"
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
globalThis.test("a field named for the view but typed as another identity is not", () => {
|
|
189
|
+
let schema = Sury.$schema(s => ({
|
|
190
|
+
orderId: s.m(CustomerId.schema)
|
|
191
|
+
}));
|
|
192
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField("RowKeyedByEnvelope", "Orders", schema)).toEqual(undefined);
|
|
193
|
+
});
|
|
194
|
+
globalThis.test("several foreign `*Id` fields are not a gap worth a warning", () => {
|
|
195
|
+
let schema = Sury.$schema(s => ({
|
|
196
|
+
productId: s.m(Sury.string),
|
|
197
|
+
categoryId: s.m(Sury.string)
|
|
198
|
+
}));
|
|
199
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("RowKeyedByEnvelope", "ProductDemand", schema))).toEqual(undefined);
|
|
148
200
|
});
|
|
149
201
|
});
|
|
150
202
|
|
|
151
203
|
globalThis.describe("keyFieldGapMessage — which gap is worth a warning", () => {
|
|
204
|
+
let gapFor = (entityName, schema) => GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("RowKeyedByStateField", entityName, schema));
|
|
152
205
|
globalThis.test("a second `*Id` field taking the key away is named, with both fields", () => {
|
|
153
206
|
let schema = Sury.$schema(s => ({
|
|
154
207
|
productId: s.m(Sury.string),
|
|
155
208
|
categoryId: s.m(Sury.string)
|
|
156
209
|
}));
|
|
157
|
-
let message = Stdlib_Option.getOr(
|
|
210
|
+
let message = Stdlib_Option.getOr(gapFor("ProductDemand", schema), "");
|
|
158
211
|
globalThis.expect(message.includes("productId, categoryId")).toBe(true);
|
|
159
212
|
globalThis.expect(message.includes("productDemandId")).toBe(true);
|
|
160
213
|
});
|
|
161
214
|
globalThis.test("a state with no `*Id` field at all is silent, not warned about", () => {
|
|
162
|
-
|
|
215
|
+
globalThis.expect(gapFor("Customers", Sury.$schema(s => ({
|
|
163
216
|
email: s.m(Sury.string),
|
|
164
217
|
address: s.m(Sury.string)
|
|
165
|
-
}));
|
|
166
|
-
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("Customers", schema))).toEqual(undefined);
|
|
218
|
+
})))).toEqual(undefined);
|
|
167
219
|
});
|
|
168
220
|
globalThis.test("a resolved key says nothing", () => {
|
|
169
|
-
|
|
221
|
+
globalThis.expect(gapFor("Orders", Sury.$schema(s => ({
|
|
170
222
|
orderId: s.m(Sury.string)
|
|
171
|
-
}));
|
|
172
|
-
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("Orders", schema))).toEqual(undefined);
|
|
223
|
+
})))).toEqual(undefined);
|
|
173
224
|
});
|
|
174
225
|
globalThis.test("an @id-less view whose name picks one of its `*Id` fields is fine", () => {
|
|
175
|
-
|
|
226
|
+
globalThis.expect(gapFor("Products", Sury.$schema(s => ({
|
|
176
227
|
productId: s.m(Sury.string),
|
|
177
228
|
categoryId: s.m(Sury.string)
|
|
178
|
-
}));
|
|
179
|
-
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("Products", schema))).toEqual(undefined);
|
|
229
|
+
})))).toEqual(undefined);
|
|
180
230
|
});
|
|
181
231
|
});
|
|
182
232
|
|
|
@@ -190,6 +240,15 @@ globalThis.describe("deriveServerCapability — inferred keys reach the SDL surf
|
|
|
190
240
|
globalThis.expect(c.filterFields.map(f => f.name)).toEqual(["orderId"]);
|
|
191
241
|
globalThis.expect(c.sortFields).toEqual(["orderId"]);
|
|
192
242
|
});
|
|
243
|
+
globalThis.test("a lone foreign `*Id` field stays filterable and sortable", () => {
|
|
244
|
+
let schema = Sury.$schema(s => ({
|
|
245
|
+
customerId: s.m(Sury.string),
|
|
246
|
+
lifecycle: s.m(Sury.string)
|
|
247
|
+
}));
|
|
248
|
+
let c = GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability("Orders", schema);
|
|
249
|
+
globalThis.expect(c.filterFields.map(f => f.name)).toEqual(["customerId"]);
|
|
250
|
+
globalThis.expect(c.sortFields).toEqual(["customerId"]);
|
|
251
|
+
});
|
|
193
252
|
globalThis.test("an unresolvable key still yields nothing — the old behaviour, now narrowed", () => {
|
|
194
253
|
let schema = Sury.$schema(s => ({
|
|
195
254
|
email: s.m(Sury.string)
|
|
@@ -668,6 +727,8 @@ globalThis.describe("@resolves / @resolvesMany reach the SDL", () => {
|
|
|
668
727
|
});
|
|
669
728
|
|
|
670
729
|
export {
|
|
730
|
+
OrderId,
|
|
731
|
+
CustomerId,
|
|
671
732
|
OrderSliceCmd,
|
|
672
733
|
mutationFor,
|
|
673
734
|
GeocodeItem,
|
|
@@ -680,4 +741,4 @@ export {
|
|
|
680
741
|
refDoorFragment,
|
|
681
742
|
ResolvedFields,
|
|
682
743
|
}
|
|
683
|
-
/*
|
|
744
|
+
/* OrderId Not a pure module */
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// An identity-typed field is an id whatever it is called: `buyer: CustomerId.t`
|
|
2
|
+
// must reach the SDL as `ID!` and carry its key on the JSON Schema, or a field
|
|
3
|
+
// that is neither tagged nor referenced loses its id-ness on the wire.
|
|
4
|
+
|
|
5
|
+
open JestGlobals
|
|
6
|
+
|
|
7
|
+
module OrderId = Reventless.Id.Make({
|
|
8
|
+
let key = "orderId"
|
|
9
|
+
})
|
|
10
|
+
module CustomerId = Reventless.Id.Make({
|
|
11
|
+
let key = "customerId"
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
@schema
|
|
15
|
+
type order = {
|
|
16
|
+
buyer: CustomerId.t,
|
|
17
|
+
previous: option<OrderId.t>,
|
|
18
|
+
related: array<OrderId.t>,
|
|
19
|
+
note: string,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let property = (json: JSON.t, name: string): option<JSON.t> =>
|
|
23
|
+
json
|
|
24
|
+
->JSON.Decode.object
|
|
25
|
+
->Option.flatMap(o => o->Dict.get("properties"))
|
|
26
|
+
->Option.flatMap(JSON.Decode.object)
|
|
27
|
+
->Option.flatMap(p => p->Dict.get(name))
|
|
28
|
+
|
|
29
|
+
let key = (json: option<JSON.t>, name: string): option<JSON.t> =>
|
|
30
|
+
json->Option.flatMap(JSON.Decode.object)->Option.flatMap(o => o->Dict.get(name))
|
|
31
|
+
|
|
32
|
+
describe("the identity semantic on the JSON Schema", () => {
|
|
33
|
+
let json = SuryToJsonSchema.deriveObjectSchema(orderSchema->S.castToUnknown)
|
|
34
|
+
|
|
35
|
+
testSync("names the semantic and its key", () => {
|
|
36
|
+
let buyer = json->property("buyer")
|
|
37
|
+
expect(buyer->key("x-reventless-semantic"))->toEqual(Some(JSON.Encode.string("identity")))
|
|
38
|
+
expect(buyer->key("x-reventless-semantic-target"))->toEqual(
|
|
39
|
+
Some(JSON.Encode.object(Dict.fromArray([("key", JSON.Encode.string("customerId"))]))),
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
testSync("reaches an array's elements", () =>
|
|
44
|
+
expect(
|
|
45
|
+
json
|
|
46
|
+
->property("related")
|
|
47
|
+
->key("items")
|
|
48
|
+
->key("x-reventless-semantic-target")
|
|
49
|
+
->key("key"),
|
|
50
|
+
)->toEqual(Some(JSON.Encode.string("orderId")))
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
testSync("leaves a plain string alone", () =>
|
|
54
|
+
expect(json->property("note")->key("x-reventless-semantic"))->toEqual(None)
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe("the identity semantic in the SDL", () => {
|
|
59
|
+
let sdl =
|
|
60
|
+
GraphQL_FragmentGenerator.deriveObjectTypeWithNested(
|
|
61
|
+
~typeName="Ordering_Order",
|
|
62
|
+
~includeIdParam=false,
|
|
63
|
+
orderSchema->S.castToUnknown,
|
|
64
|
+
)->Array.join("\n")
|
|
65
|
+
|
|
66
|
+
testSync("an identity field not named `*Id` is an ID", () =>
|
|
67
|
+
expect(sdl->String.includes("buyer: ID!"))->toBe(true)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
testSync("an optional identity keeps its optionality", () =>
|
|
71
|
+
expect(sdl->String.includes("previous: ID\n"))->toBe(true)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
testSync("an array of identities is a list of IDs", () =>
|
|
75
|
+
expect(sdl->String.includes("related: [ID!]!"))->toBe(true)
|
|
76
|
+
)
|
|
77
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as SuryToJsonSchema$ReventlessCore from "../../src/components/Api/SuryToJsonSchema.res.mjs";
|
|
8
|
+
import * as GraphQL_FragmentGenerator$ReventlessCore from "../../src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
9
|
+
|
|
10
|
+
let OrderId = Id$Reventless.Make({
|
|
11
|
+
key: "orderId"
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
let CustomerId = Id$Reventless.Make({
|
|
15
|
+
key: "customerId"
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let orderSchema = Sury.$schema(s => ({
|
|
19
|
+
buyer: s.m(CustomerId.schema),
|
|
20
|
+
previous: s.m(Sury.$option(OrderId.schema)),
|
|
21
|
+
related: s.m(Sury.array(OrderId.schema)),
|
|
22
|
+
note: s.m(Sury.string)
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
function property(json, name) {
|
|
26
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o["properties"]), Stdlib_JSON.Decode.object), p => p[name]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function key(json, name) {
|
|
30
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(json, Stdlib_JSON.Decode.object), o => o[name]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
globalThis.describe("the identity semantic on the JSON Schema", () => {
|
|
34
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(undefined, undefined, orderSchema);
|
|
35
|
+
globalThis.test("names the semantic and its key", () => {
|
|
36
|
+
let buyer = property(json, "buyer");
|
|
37
|
+
globalThis.expect(key(buyer, "x-reventless-semantic")).toEqual("identity");
|
|
38
|
+
globalThis.expect(key(buyer, "x-reventless-semantic-target")).toEqual(Object.fromEntries([[
|
|
39
|
+
"key",
|
|
40
|
+
"customerId"
|
|
41
|
+
]]));
|
|
42
|
+
});
|
|
43
|
+
globalThis.test("reaches an array's elements", () => {
|
|
44
|
+
globalThis.expect(key(key(key(property(json, "related"), "items"), "x-reventless-semantic-target"), "key")).toEqual("orderId");
|
|
45
|
+
});
|
|
46
|
+
globalThis.test("leaves a plain string alone", () => {
|
|
47
|
+
globalThis.expect(key(property(json, "note"), "x-reventless-semantic")).toEqual(undefined);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
globalThis.describe("the identity semantic in the SDL", () => {
|
|
52
|
+
let sdl = GraphQL_FragmentGenerator$ReventlessCore.deriveObjectTypeWithNested("Ordering_Order", undefined, false, undefined, orderSchema).join("\n");
|
|
53
|
+
globalThis.test("an identity field not named `*Id` is an ID", () => {
|
|
54
|
+
globalThis.expect(sdl.includes("buyer: ID!")).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
globalThis.test("an optional identity keeps its optionality", () => {
|
|
57
|
+
globalThis.expect(sdl.includes("previous: ID\n")).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
globalThis.test("an array of identities is a list of IDs", () => {
|
|
60
|
+
globalThis.expect(sdl.includes("related: [ID!]!")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
OrderId,
|
|
66
|
+
CustomerId,
|
|
67
|
+
orderSchema,
|
|
68
|
+
property,
|
|
69
|
+
key,
|
|
70
|
+
}
|
|
71
|
+
/* OrderId Not a pure module */
|
|
@@ -59,7 +59,6 @@ let SliceBehavior = {
|
|
|
59
59
|
let SliceHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
60
60
|
name: DcbFixtures$ReventlessCore.TestCommandSpec.name,
|
|
61
61
|
moduleUrl: DcbFixtures$ReventlessCore.TestCommandSpec.moduleUrl,
|
|
62
|
-
Id: Id$Reventless.$$String,
|
|
63
62
|
consumedEventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.consumedEventSchema,
|
|
64
63
|
errorSchema: DcbFixtures$ReventlessCore.TestCommandSpec.errorSchema,
|
|
65
64
|
eventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.eventSchema,
|
|
@@ -55,7 +55,6 @@ let TestCommandBehavior = {
|
|
|
55
55
|
let TestHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
56
56
|
name: DcbFixtures$ReventlessCore.TestCommandSpec.name,
|
|
57
57
|
moduleUrl: DcbFixtures$ReventlessCore.TestCommandSpec.moduleUrl,
|
|
58
|
-
Id: Id$Reventless.$$String,
|
|
59
58
|
consumedEventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.consumedEventSchema,
|
|
60
59
|
errorSchema: DcbFixtures$ReventlessCore.TestCommandSpec.errorSchema,
|
|
61
60
|
eventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.eventSchema,
|
|
@@ -155,7 +154,6 @@ let EventualSpec = {
|
|
|
155
154
|
let StrongHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
156
155
|
name: name,
|
|
157
156
|
moduleUrl: moduleUrl$1,
|
|
158
|
-
Id: Id$Reventless.$$String,
|
|
159
157
|
consumedEventSchema: consumedEventSchema,
|
|
160
158
|
errorSchema: errorSchema,
|
|
161
159
|
eventSchema: eventSchema,
|
|
@@ -169,7 +167,6 @@ let StrongHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
|
169
167
|
let EventualHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
170
168
|
name: name$1,
|
|
171
169
|
moduleUrl: moduleUrl$2,
|
|
172
|
-
Id: Id$Reventless.$$String,
|
|
173
170
|
consumedEventSchema: consumedEventSchema$1,
|
|
174
171
|
errorSchema: errorSchema$1,
|
|
175
172
|
eventSchema: eventSchema$1,
|
|
@@ -5,7 +5,7 @@ open Reventless.Projection
|
|
|
5
5
|
// ─────────────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
7
|
module SourceSpec = {
|
|
8
|
-
module Id = Reventless.Id.
|
|
8
|
+
module Id = Reventless.Id.StringPure
|
|
9
9
|
let name = "SourceAggregate"
|
|
10
10
|
|
|
11
11
|
@schema
|
|
@@ -22,7 +22,7 @@ module SourceSpec = {
|
|
|
22
22
|
// ─────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
24
|
module TargetSpec = {
|
|
25
|
-
module Id = Reventless.Id.
|
|
25
|
+
module Id = Reventless.Id.StringPure
|
|
26
26
|
let name = "ItemCatalog"
|
|
27
27
|
|
|
28
28
|
@schema
|
|
@@ -81,11 +81,23 @@ function project(msg) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
let ItemMapping = Projection$Reventless.Mapping.Make({
|
|
84
|
-
Id:
|
|
84
|
+
Id: {
|
|
85
|
+
schema: Id$Reventless.StringPure.schema,
|
|
86
|
+
make: prim => prim,
|
|
87
|
+
makeFromString: prim => prim,
|
|
88
|
+
toString: prim => prim,
|
|
89
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
90
|
+
},
|
|
85
91
|
name: name,
|
|
86
92
|
eventSchema: eventSchema
|
|
87
93
|
})({
|
|
88
|
-
Id:
|
|
94
|
+
Id: {
|
|
95
|
+
schema: Id$Reventless.StringPure.schema,
|
|
96
|
+
make: prim => prim,
|
|
97
|
+
makeFromString: prim => prim,
|
|
98
|
+
toString: prim => prim,
|
|
99
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
100
|
+
},
|
|
89
101
|
name: name$1,
|
|
90
102
|
stateSchema: stateSchema,
|
|
91
103
|
subIdConfig: undefined
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// A typed id finds the view that lists it without an `@ref`, whenever exactly one
|
|
2
|
+
// view is keyed by its identity. Where the type cannot decide, it says so rather
|
|
3
|
+
// than guessing: which list a reference means is a product decision.
|
|
4
|
+
|
|
5
|
+
open JestGlobals
|
|
6
|
+
|
|
7
|
+
module ProductId = Reventless.Id.Make({
|
|
8
|
+
let key = "productId"
|
|
9
|
+
})
|
|
10
|
+
module CustomerId = Reventless.Id.Make({
|
|
11
|
+
let key = "customerId"
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
let refsOf = (~viewsByKey, schema: S.t<'a>) => {
|
|
15
|
+
let reports = []
|
|
16
|
+
let identityViews: Plugin_Structure.identityViews = {
|
|
17
|
+
viewsByKey: Dict.fromArray(viewsByKey),
|
|
18
|
+
viewNames: viewsByKey->Array.flatMap(((_, views)) => views),
|
|
19
|
+
report: message => reports->Array.push(message),
|
|
20
|
+
}
|
|
21
|
+
let properties = switch schema->S.castToUnknown {
|
|
22
|
+
| Object({properties}) => properties
|
|
23
|
+
| _ => Dict.make()
|
|
24
|
+
}
|
|
25
|
+
let refs =
|
|
26
|
+
Plugin_Structure.extractReferences(~identityViews, properties)->Array.map(r => (
|
|
27
|
+
r.fieldName,
|
|
28
|
+
r.entity,
|
|
29
|
+
))
|
|
30
|
+
(refs, reports)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let order = S.schema(s =>
|
|
34
|
+
{
|
|
35
|
+
"buyer": s.matches(CustomerId.schema),
|
|
36
|
+
"productIds": s.matches(S.array(ProductId.schema)),
|
|
37
|
+
"note": s.matches(S.string),
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
describe("a reference derived from the type", () => {
|
|
42
|
+
testSync("exactly one view keyed by the identity is the reference", () => {
|
|
43
|
+
let (refs, reports) = refsOf(
|
|
44
|
+
~viewsByKey=[("customerId", ["Customers"]), ("productId", ["AvailableProducts"])],
|
|
45
|
+
order,
|
|
46
|
+
)
|
|
47
|
+
expect(refs)->toEqual([("buyer", "Customers"), ("productIds", "AvailableProducts")])
|
|
48
|
+
expect(reports)->toEqual([])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
testSync("several views keyed by it ask for @ref instead of guessing", () => {
|
|
52
|
+
let (refs, reports) = refsOf(
|
|
53
|
+
~viewsByKey=[("customerId", ["Customers"]), ("productId", ["Products", "ProductDemand"])],
|
|
54
|
+
order,
|
|
55
|
+
)
|
|
56
|
+
expect(refs)->toEqual([("buyer", "Customers")])
|
|
57
|
+
expect(reports->Array.some(r => r->String.includes("Products and ProductDemand")))->toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
testSync("no view keyed by it is reported", () => {
|
|
61
|
+
let (refs, reports) = refsOf(~viewsByKey=[("productId", ["AvailableProducts"])], order)
|
|
62
|
+
expect(refs)->toEqual([("productIds", "AvailableProducts")])
|
|
63
|
+
expect(reports->Array.some(r => r->String.includes("no view in this plugin")))->toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
testSync("an untyped field is left to its name, as before", () => {
|
|
67
|
+
let (refs, reports) = refsOf(
|
|
68
|
+
~viewsByKey=[("customerId", ["Customers"])],
|
|
69
|
+
S.schema(s => {"customerId": s.matches(S.string)}),
|
|
70
|
+
)
|
|
71
|
+
expect((refs, reports))->toEqual(([], []))
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe("a declared @ref on a typed id", () => {
|
|
76
|
+
let referenced = S.schema(s =>
|
|
77
|
+
{"buyer": s.matches(CustomerId.schema->Reventless.Reference.mark("Products"))}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
testSync("is kept, and checked against the views keyed by the identity", () => {
|
|
81
|
+
let (refs, reports) = refsOf(
|
|
82
|
+
~viewsByKey=[("customerId", ["Customers"]), ("productId", ["Products"])],
|
|
83
|
+
referenced,
|
|
84
|
+
)
|
|
85
|
+
expect(refs)->toEqual([("buyer", "Products")])
|
|
86
|
+
expect(reports->Array.some(r => r->String.includes("not keyed by it")))->toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
testSync("naming a view keyed by the identity is fine", () => {
|
|
90
|
+
let chosen = S.schema(
|
|
91
|
+
s => {"buyer": s.matches(CustomerId.schema->Reventless.Reference.mark("Customers"))},
|
|
92
|
+
)
|
|
93
|
+
let (refs, reports) = refsOf(
|
|
94
|
+
~viewsByKey=[("customerId", ["Customers", "VipCustomers"])],
|
|
95
|
+
chosen,
|
|
96
|
+
)
|
|
97
|
+
expect((refs, reports))->toEqual(([("buyer", "Customers")], []))
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
5
|
+
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
6
|
+
import * as Plugin_Structure$ReventlessCore from "../../src/plugin/component/Plugin_Structure.res.mjs";
|
|
7
|
+
|
|
8
|
+
let ProductId = Id$Reventless.Make({
|
|
9
|
+
key: "productId"
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
let CustomerId = Id$Reventless.Make({
|
|
13
|
+
key: "customerId"
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function refsOf(viewsByKey, schema) {
|
|
17
|
+
let reports = [];
|
|
18
|
+
let identityViews_viewsByKey = Object.fromEntries(viewsByKey);
|
|
19
|
+
let identityViews_viewNames = viewsByKey.flatMap(param => param[1]);
|
|
20
|
+
let identityViews_report = message => {
|
|
21
|
+
reports.push(message);
|
|
22
|
+
};
|
|
23
|
+
let identityViews = {
|
|
24
|
+
viewsByKey: identityViews_viewsByKey,
|
|
25
|
+
viewNames: identityViews_viewNames,
|
|
26
|
+
report: identityViews_report
|
|
27
|
+
};
|
|
28
|
+
let properties;
|
|
29
|
+
properties = schema.type === "object" ? schema.properties : ({});
|
|
30
|
+
let refs = Plugin_Structure$ReventlessCore.extractReferences(identityViews, properties).map(r => [
|
|
31
|
+
r.fieldName,
|
|
32
|
+
r.entity
|
|
33
|
+
]);
|
|
34
|
+
return [
|
|
35
|
+
refs,
|
|
36
|
+
reports
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let order = Sury.$schema(s => ({
|
|
41
|
+
buyer: s.m(CustomerId.schema),
|
|
42
|
+
productIds: s.m(Sury.array(ProductId.schema)),
|
|
43
|
+
note: s.m(Sury.string)
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
globalThis.describe("a reference derived from the type", () => {
|
|
47
|
+
globalThis.test("exactly one view keyed by the identity is the reference", () => {
|
|
48
|
+
let match = refsOf([
|
|
49
|
+
[
|
|
50
|
+
"customerId",
|
|
51
|
+
["Customers"]
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"productId",
|
|
55
|
+
["AvailableProducts"]
|
|
56
|
+
]
|
|
57
|
+
], order);
|
|
58
|
+
globalThis.expect(match[0]).toEqual([
|
|
59
|
+
[
|
|
60
|
+
"buyer",
|
|
61
|
+
"Customers"
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"productIds",
|
|
65
|
+
"AvailableProducts"
|
|
66
|
+
]
|
|
67
|
+
]);
|
|
68
|
+
globalThis.expect(match[1]).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
globalThis.test("several views keyed by it ask for @ref instead of guessing", () => {
|
|
71
|
+
let match = refsOf([
|
|
72
|
+
[
|
|
73
|
+
"customerId",
|
|
74
|
+
["Customers"]
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
"productId",
|
|
78
|
+
[
|
|
79
|
+
"Products",
|
|
80
|
+
"ProductDemand"
|
|
81
|
+
]
|
|
82
|
+
]
|
|
83
|
+
], order);
|
|
84
|
+
globalThis.expect(match[0]).toEqual([[
|
|
85
|
+
"buyer",
|
|
86
|
+
"Customers"
|
|
87
|
+
]]);
|
|
88
|
+
globalThis.expect(match[1].some(r => r.includes("Products and ProductDemand"))).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
globalThis.test("no view keyed by it is reported", () => {
|
|
91
|
+
let match = refsOf([[
|
|
92
|
+
"productId",
|
|
93
|
+
["AvailableProducts"]
|
|
94
|
+
]], order);
|
|
95
|
+
globalThis.expect(match[0]).toEqual([[
|
|
96
|
+
"productIds",
|
|
97
|
+
"AvailableProducts"
|
|
98
|
+
]]);
|
|
99
|
+
globalThis.expect(match[1].some(r => r.includes("no view in this plugin"))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
globalThis.test("an untyped field is left to its name, as before", () => {
|
|
102
|
+
let match = refsOf([[
|
|
103
|
+
"customerId",
|
|
104
|
+
["Customers"]
|
|
105
|
+
]], Sury.$schema(s => ({
|
|
106
|
+
customerId: s.m(Sury.string)
|
|
107
|
+
})));
|
|
108
|
+
globalThis.expect([
|
|
109
|
+
match[0],
|
|
110
|
+
match[1]
|
|
111
|
+
]).toEqual([
|
|
112
|
+
[],
|
|
113
|
+
[]
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
globalThis.describe("a declared @ref on a typed id", () => {
|
|
119
|
+
let referenced = Sury.$schema(s => ({
|
|
120
|
+
buyer: s.m(Reference$Reventless.mark(CustomerId.schema, undefined, undefined, "Products"))
|
|
121
|
+
}));
|
|
122
|
+
globalThis.test("is kept, and checked against the views keyed by the identity", () => {
|
|
123
|
+
let match = refsOf([
|
|
124
|
+
[
|
|
125
|
+
"customerId",
|
|
126
|
+
["Customers"]
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
"productId",
|
|
130
|
+
["Products"]
|
|
131
|
+
]
|
|
132
|
+
], referenced);
|
|
133
|
+
globalThis.expect(match[0]).toEqual([[
|
|
134
|
+
"buyer",
|
|
135
|
+
"Products"
|
|
136
|
+
]]);
|
|
137
|
+
globalThis.expect(match[1].some(r => r.includes("not keyed by it"))).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
globalThis.test("naming a view keyed by the identity is fine", () => {
|
|
140
|
+
let chosen = Sury.$schema(s => ({
|
|
141
|
+
buyer: s.m(Reference$Reventless.mark(CustomerId.schema, undefined, undefined, "Customers"))
|
|
142
|
+
}));
|
|
143
|
+
let match = refsOf([[
|
|
144
|
+
"customerId",
|
|
145
|
+
[
|
|
146
|
+
"Customers",
|
|
147
|
+
"VipCustomers"
|
|
148
|
+
]
|
|
149
|
+
]], chosen);
|
|
150
|
+
globalThis.expect([
|
|
151
|
+
match[0],
|
|
152
|
+
match[1]
|
|
153
|
+
]).toEqual([
|
|
154
|
+
[[
|
|
155
|
+
"buyer",
|
|
156
|
+
"Customers"
|
|
157
|
+
]],
|
|
158
|
+
[]
|
|
159
|
+
]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
ProductId,
|
|
165
|
+
CustomerId,
|
|
166
|
+
refsOf,
|
|
167
|
+
order,
|
|
168
|
+
}
|
|
169
|
+
/* ProductId Not a pure module */
|