@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +5 -5
  3. package/src/Projection.res +0 -10
  4. package/src/Projection.res.mjs +0 -14
  5. package/src/ProjectionMapper.res +8 -2
  6. package/src/ProjectionMapper.res.mjs +3 -3
  7. package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
  8. package/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs +3 -0
  9. package/src/components/Api/GraphQL_FragmentGenerator.res +47 -16
  10. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +20 -7
  11. package/src/components/Api/SchemaType.res +13 -3
  12. package/src/components/Api/SchemaType.res.mjs +13 -2
  13. package/src/components/Api/SuryToJsonSchema.res +9 -5
  14. package/src/components/Api/SuryToJsonSchema.res.mjs +14 -2
  15. package/src/components/EventMapper/EventMapper_Builder.res +19 -1
  16. package/src/components/EventMapper/EventMapper_Builder.res.mjs +10 -1
  17. package/src/components/StateViewSlice/StateViewSlice_Builder.res +11 -1
  18. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +2 -1
  19. package/src/components/StateViewSlice/StateViewSlice_Callback.res +8 -1
  20. package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +2 -1
  21. package/src/plugin/component/Plugin_Structure.res +148 -25
  22. package/src/plugin/component/Plugin_Structure.res.mjs +114 -30
  23. package/tests/api/GraphQL_FragmentGeneratorTest.res +76 -1
  24. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +78 -17
  25. package/tests/api/IdentitySemanticTest.res +77 -0
  26. package/tests/api/IdentitySemanticTest.res.mjs +71 -0
  27. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +0 -1
  28. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +0 -3
  29. package/tests/eventmapper/EventMapperFixtures.res +2 -2
  30. package/tests/eventmapper/EventMapperFixtures.res.mjs +14 -2
  31. package/tests/plugin/IdentityReferenceTest.res +99 -0
  32. package/tests/plugin/IdentityReferenceTest.res.mjs +169 -0
  33. package/tests/plugin/PluginStructureAccessTest.res.mjs +14 -1
  34. package/tests/plugin/PluginStructureTest.res.mjs +42 -10
  35. package/tests/plugin/StateView/PsAnnotatedView.res.mjs +3 -0
  36. package/tests/plugin/StateView/PsAvailableProductsView.res.mjs +3 -0
  37. package/tests/plugin/StateView/PsCategoriesView.res.mjs +3 -0
  38. package/tests/plugin/StateView/PsCustomersView.res.mjs +3 -0
  39. package/tests/plugin/StateView/PsGatedView.res.mjs +3 -0
  40. package/tests/plugin/StateView/PsOrdersView.res.mjs +3 -0
  41. 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(GraphQL_FragmentGenerator$ReventlessCore.keyFieldGapMessage(GraphQL_FragmentGenerator$ReventlessCore.classifyKeyField("ProductDemand", schema)), "");
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
- let schema = Sury.$schema(s => ({
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
- let schema = Sury.$schema(s => ({
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
- let schema = Sury.$schema(s => ({
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
- /* commandSchema Not a pure module */
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.String
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.String
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: Id$Reventless.$$String,
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: Id$Reventless.$$String,
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 */